mirror of
https://github.com/pockethost/pockethost.git
synced 2025-05-29 18:26:39 +00:00
Merge branch 'master' of github.com:benallfree/pockethost
This commit is contained in:
commit
3a445cc844
2
.github/workflows/pockethost.yaml
vendored
2
.github/workflows/pockethost.yaml
vendored
@ -16,4 +16,4 @@ jobs:
|
|||||||
with:
|
with:
|
||||||
node-version: ${{ matrix.node-version }}
|
node-version: ${{ matrix.node-version }}
|
||||||
- run: yarn
|
- run: yarn
|
||||||
- run: yarn prettier:check
|
- run: yarn lint
|
||||||
|
@ -1 +1,2 @@
|
|||||||
.svelte-kit
|
.svelte-kit
|
||||||
|
dist
|
18
Dockerfile
18
Dockerfile
@ -2,21 +2,3 @@ FROM node:18-alpine as buildbox
|
|||||||
COPY --from=golang:1.19-alpine /usr/local/go/ /usr/local/go/
|
COPY --from=golang:1.19-alpine /usr/local/go/ /usr/local/go/
|
||||||
ENV PATH="/usr/local/go/bin:${PATH}"
|
ENV PATH="/usr/local/go/bin:${PATH}"
|
||||||
RUN apk add python3 py3-pip make gcc musl-dev g++ bash
|
RUN apk add python3 py3-pip make gcc musl-dev g++ bash
|
||||||
# WORKDIR /src
|
|
||||||
|
|
||||||
# COPY packages/pocketbase/src packages/pocketbase/src
|
|
||||||
# WORKDIR /src/packages/pocketbase/src
|
|
||||||
# RUN go get
|
|
||||||
|
|
||||||
# WORKDIR /src
|
|
||||||
# COPY packages/common/package.json packages/common/
|
|
||||||
# COPY packages/daemon/package.json packages/daemon/
|
|
||||||
# COPY packages/daemon/yarn.lock packages/daemon/
|
|
||||||
# COPY packages/pockethost.io/package.json packages/pockethost.io/
|
|
||||||
# COPY packages/pockethost.io/yarn.lock packages/pockethost.io/
|
|
||||||
# COPY package.json ./
|
|
||||||
# COPY yarn.lock ./
|
|
||||||
# RUN yarn
|
|
||||||
# COPY . .
|
|
||||||
# RUN yarn build
|
|
||||||
# RUN ls -lah node_modules
|
|
||||||
|
@ -14,6 +14,26 @@ services:
|
|||||||
- ./mount/cache/go:/go
|
- ./mount/cache/go:/go
|
||||||
- ./mount/cache/yarn:/usr/local/share/.cache/yarn/v6
|
- ./mount/cache/yarn:/usr/local/share/.cache/yarn/v6
|
||||||
- ..:/src
|
- ..:/src
|
||||||
|
profiles: ['build']
|
||||||
|
buildbox:
|
||||||
|
environment:
|
||||||
|
- GOPATH=/go
|
||||||
|
env_file:
|
||||||
|
- .env.local
|
||||||
|
build:
|
||||||
|
context: ..
|
||||||
|
dockerfile: Dockerfile
|
||||||
|
container_name: buildbox
|
||||||
|
working_dir: /src
|
||||||
|
command: bash -c "yarn build"
|
||||||
|
volumes:
|
||||||
|
- ./mount/cache/go:/go
|
||||||
|
- ./mount/cache/yarn:/usr/local/share/.cache/yarn/v6
|
||||||
|
- ..:/src
|
||||||
|
depends_on:
|
||||||
|
prepbox:
|
||||||
|
condition: service_completed_successfully
|
||||||
|
profiles: ['build']
|
||||||
www:
|
www:
|
||||||
env_file:
|
env_file:
|
||||||
- .env.local
|
- .env.local
|
||||||
@ -34,8 +54,7 @@ services:
|
|||||||
depends_on:
|
depends_on:
|
||||||
daemon:
|
daemon:
|
||||||
condition: service_started
|
condition: service_started
|
||||||
prepbox:
|
profiles: ['serve']
|
||||||
condition: service_completed_successfully
|
|
||||||
daemon:
|
daemon:
|
||||||
env_file:
|
env_file:
|
||||||
- .env.local
|
- .env.local
|
||||||
@ -55,9 +74,7 @@ services:
|
|||||||
- app-network
|
- app-network
|
||||||
ports:
|
ports:
|
||||||
- '9001:3000'
|
- '9001:3000'
|
||||||
depends_on:
|
profiles: ['serve']
|
||||||
prepbox:
|
|
||||||
condition: service_completed_successfully
|
|
||||||
nginx:
|
nginx:
|
||||||
image: nginx:mainline-alpine
|
image: nginx:mainline-alpine
|
||||||
container_name: nginx
|
container_name: nginx
|
||||||
@ -74,6 +91,7 @@ services:
|
|||||||
- ./mount/nginx/ssl:/mount/nginx/ssl
|
- ./mount/nginx/ssl:/mount/nginx/ssl
|
||||||
networks:
|
networks:
|
||||||
- app-network
|
- app-network
|
||||||
|
profiles: ['serve']
|
||||||
|
|
||||||
networks:
|
networks:
|
||||||
app-network:
|
app-network:
|
||||||
|
@ -5,8 +5,8 @@
|
|||||||
"license": "MIT",
|
"license": "MIT",
|
||||||
"private": true,
|
"private": true,
|
||||||
"scripts": {
|
"scripts": {
|
||||||
"prettier:check": "prettier -c \"./**/*.ts\"",
|
"lint": "prettier -c \"./**/*.ts\"",
|
||||||
"prettier:fix": "prettier -w \"./**/*.ts\"",
|
"lint:fix": "prettier -w \"./**/*.ts\"",
|
||||||
"build": "concurrently 'yarn build:pocketbase' 'yarn build:daemon' 'yarn build:www'",
|
"build": "concurrently 'yarn build:pocketbase' 'yarn build:daemon' 'yarn build:www'",
|
||||||
"build:pocketbase": "cd packages/pocketbase && yarn build",
|
"build:pocketbase": "cd packages/pocketbase && yarn build",
|
||||||
"build:daemon": "cd packages/daemon && yarn build",
|
"build:daemon": "cd packages/daemon && yarn build",
|
||||||
|
1
packages/cli/.gitignore
vendored
Normal file
1
packages/cli/.gitignore
vendored
Normal file
@ -0,0 +1 @@
|
|||||||
|
dist
|
3
packages/cli/.npmignore
Normal file
3
packages/cli/.npmignore
Normal file
@ -0,0 +1,3 @@
|
|||||||
|
src
|
||||||
|
tsconfig.json
|
||||||
|
dist/index.*
|
58
packages/cli/package.json
Normal file
58
packages/cli/package.json
Normal file
@ -0,0 +1,58 @@
|
|||||||
|
{
|
||||||
|
"name": "pockethost",
|
||||||
|
"version": "0.0.5",
|
||||||
|
"main": "./dist/index.js",
|
||||||
|
"bin": {
|
||||||
|
"pbscript": "./dist/pbscript"
|
||||||
|
},
|
||||||
|
"author": {
|
||||||
|
"name": "Ben Allfree",
|
||||||
|
"url": "https://github.com/benallfree"
|
||||||
|
},
|
||||||
|
"repository": {
|
||||||
|
"url": "https://github.com/benallfree/pockethost"
|
||||||
|
},
|
||||||
|
"scripts": {
|
||||||
|
"dev": "chokidar package.json 'src/**' './node_modules/**' -c 'yarn build' --initial",
|
||||||
|
"build": "parcel build --no-cache && yarn build:shebang && npm i -g --force .",
|
||||||
|
"build:shebang": "echo \"#!/usr/bin/env node\"|cat - ./dist/index.js > ./dist/pbscript",
|
||||||
|
"publish": "npm publish --access public"
|
||||||
|
},
|
||||||
|
"license": "MIT",
|
||||||
|
"devDependencies": {
|
||||||
|
"parcel": "^2.7.0"
|
||||||
|
},
|
||||||
|
"dependencies": {
|
||||||
|
"@types/eventsource": "^1.1.9",
|
||||||
|
"@types/node": "^18.8.2",
|
||||||
|
"@types/prompts": "^2.4.1",
|
||||||
|
"@types/tmp": "^0.2.3",
|
||||||
|
"commander": "^9.4.0",
|
||||||
|
"cross-fetch": "^3.1.5",
|
||||||
|
"eventsource": "^2.0.2",
|
||||||
|
"find-up": "^6.3.0",
|
||||||
|
"pocketbase": "^0.7.1",
|
||||||
|
"prompts": "^2.4.2",
|
||||||
|
"tmp": "^0.2.1"
|
||||||
|
},
|
||||||
|
"targets": {
|
||||||
|
"node": {
|
||||||
|
"engines": {
|
||||||
|
"node": ">= 12"
|
||||||
|
},
|
||||||
|
"source": "./src/index.ts",
|
||||||
|
"context": "node",
|
||||||
|
"outputFormat": "commonjs",
|
||||||
|
"includeNodeModules": [
|
||||||
|
"@s-libs/micro-dash",
|
||||||
|
"find-up",
|
||||||
|
"locate-path",
|
||||||
|
"path-exists",
|
||||||
|
"p-locate",
|
||||||
|
"p-limit",
|
||||||
|
"yocto-queue",
|
||||||
|
"pocketbase"
|
||||||
|
]
|
||||||
|
}
|
||||||
|
}
|
||||||
|
}
|
25
packages/cli/readme.md
Normal file
25
packages/cli/readme.md
Normal file
@ -0,0 +1,25 @@
|
|||||||
|
# TS/JS Cloud Functions for PocketBase
|
||||||
|
|
||||||
|
[PBScript](https://github.com/benallfree/pbscript) allows you to write [PocketBase](https://pocketbase.io) server-side functions in Typescript or Javascript without recompiling.
|
||||||
|
|
||||||
|
This package is the CLI tool.
|
||||||
|
|
||||||
|
```bash
|
||||||
|
npm i -g pbscript
|
||||||
|
```
|
||||||
|
|
||||||
|
or
|
||||||
|
|
||||||
|
```bash
|
||||||
|
npx pbscript --help
|
||||||
|
```
|
||||||
|
|
||||||
|
or
|
||||||
|
|
||||||
|
```bash
|
||||||
|
npm i -D pbscript
|
||||||
|
```
|
||||||
|
|
||||||
|
And then reference from `package.json`.
|
||||||
|
|
||||||
|
See official docs at the [PBScript repo](https://github.com/benallfree/pbscript).
|
90
packages/cli/src/commands/build.ts
Normal file
90
packages/cli/src/commands/build.ts
Normal file
@ -0,0 +1,90 @@
|
|||||||
|
import { Parcel } from '@parcel/core'
|
||||||
|
import { debounce } from '@s-libs/micro-dash'
|
||||||
|
import chokidar from 'chokidar'
|
||||||
|
import { Command } from 'commander'
|
||||||
|
import { mkdirSync } from 'fs'
|
||||||
|
import { join } from 'path'
|
||||||
|
import { cwd } from 'process'
|
||||||
|
import { getProjectRoot, readSettings } from '../util/project'
|
||||||
|
|
||||||
|
export type BuildConfig = {
|
||||||
|
dist: string
|
||||||
|
src: string
|
||||||
|
}
|
||||||
|
|
||||||
|
export const addDevCommand = (program: Command) => {
|
||||||
|
program
|
||||||
|
.command('build')
|
||||||
|
.description('Build the JS bundle')
|
||||||
|
.option(
|
||||||
|
'--src <path>',
|
||||||
|
`Path to source (default: <project>/src/index.{ts|js})`
|
||||||
|
)
|
||||||
|
.option('--dist <path>', `Path to dist (default: <project>/dist/index.js)`)
|
||||||
|
.action(async (options) => {
|
||||||
|
const defaultSrc = options.src
|
||||||
|
const defaultDist = options.dist
|
||||||
|
|
||||||
|
const config: BuildConfig = {
|
||||||
|
src: join(getProjectRoot(), './src/index.ts'),
|
||||||
|
dist: join(getProjectRoot(), './dist/index.js'),
|
||||||
|
...readSettings('build'),
|
||||||
|
}
|
||||||
|
if (defaultDist) config.dist = defaultDist
|
||||||
|
if (defaultSrc) config.src = defaultSrc
|
||||||
|
|
||||||
|
const { src, dist } = config
|
||||||
|
mkdirSync(dist, { recursive: true })
|
||||||
|
console.log(cwd())
|
||||||
|
|
||||||
|
const bundler = new Parcel({
|
||||||
|
entries: './src/index.ts',
|
||||||
|
defaultConfig: '@parcel/config-default',
|
||||||
|
mode: 'production',
|
||||||
|
defaultTargetOptions: {
|
||||||
|
distDir: join(cwd(), 'dist'),
|
||||||
|
outputFormat: 'global',
|
||||||
|
sourceMaps: false,
|
||||||
|
},
|
||||||
|
shouldDisableCache: true,
|
||||||
|
// targets: {
|
||||||
|
// iife: {
|
||||||
|
// distDir: './dist',
|
||||||
|
// source: './src/index.ts',
|
||||||
|
// context: 'browser',
|
||||||
|
// outputFormat: 'global',
|
||||||
|
// sourceMap: {
|
||||||
|
// inline: true,
|
||||||
|
// },
|
||||||
|
// },
|
||||||
|
// },
|
||||||
|
})
|
||||||
|
|
||||||
|
const build = debounce(() => {
|
||||||
|
console.log(`Building...`)
|
||||||
|
bundler
|
||||||
|
.run()
|
||||||
|
.then((e) => {
|
||||||
|
console.log(`Build succeeded`, e)
|
||||||
|
console.log(e.bundleGraph.getBundles({ includeInline: true }))
|
||||||
|
let { bundleGraph } = e
|
||||||
|
})
|
||||||
|
.catch((e) => {
|
||||||
|
console.error(`Build failed with ${e}`)
|
||||||
|
})
|
||||||
|
}, 100)
|
||||||
|
|
||||||
|
build()
|
||||||
|
console.log(join(getProjectRoot(), 'src/**'))
|
||||||
|
chokidar
|
||||||
|
.watch([
|
||||||
|
join(getProjectRoot(), 'src/**'),
|
||||||
|
join(getProjectRoot(), 'node_modules/**'),
|
||||||
|
])
|
||||||
|
.on('all', (event, path) => {
|
||||||
|
build()
|
||||||
|
})
|
||||||
|
|
||||||
|
console.log('here')
|
||||||
|
})
|
||||||
|
}
|
99
packages/cli/src/commands/dev.ts
Normal file
99
packages/cli/src/commands/dev.ts
Normal file
@ -0,0 +1,99 @@
|
|||||||
|
import { Parcel } from '@parcel/core'
|
||||||
|
import { debounce } from '@s-libs/micro-dash'
|
||||||
|
import chokidar from 'chokidar'
|
||||||
|
import { Command } from 'commander'
|
||||||
|
import { join } from 'path'
|
||||||
|
import { cwd } from 'process'
|
||||||
|
import tmp from 'tmp'
|
||||||
|
import { DEFAULT_PB_DEV_URL } from '../constants'
|
||||||
|
import { SessionState } from '../providers/CustomAuthStore'
|
||||||
|
import { ensureAdminClient } from '../util/ensureAdminClient'
|
||||||
|
import { getProjectRoot, readSettings } from '../util/project'
|
||||||
|
|
||||||
|
export type DevConfig = {
|
||||||
|
session: SessionState
|
||||||
|
host: string
|
||||||
|
src: string
|
||||||
|
dist: string
|
||||||
|
}
|
||||||
|
export const addDevCommand = (program: Command) => {
|
||||||
|
program
|
||||||
|
.command('dev')
|
||||||
|
.description('Watch for source code changes in development mode')
|
||||||
|
.option(
|
||||||
|
'--src <path>',
|
||||||
|
`Path to source (default: <project>/src/index.{ts|js})`
|
||||||
|
)
|
||||||
|
.option('--dist <path>', `Path to dist (default: <project>/dist/index.js)`)
|
||||||
|
.option('--host', 'PocketBase host', DEFAULT_PB_DEV_URL)
|
||||||
|
.action(async (options) => {
|
||||||
|
const defaultSrc = options.src
|
||||||
|
const defaultDist = options.dist
|
||||||
|
const defaultHost = options.host
|
||||||
|
|
||||||
|
const config: DevConfig = {
|
||||||
|
session: { token: '', model: null },
|
||||||
|
host: DEFAULT_PB_DEV_URL,
|
||||||
|
src: join(getProjectRoot(), './src/index.ts'),
|
||||||
|
dist: join(getProjectRoot(), './dist/index.js'),
|
||||||
|
...readSettings('dev'),
|
||||||
|
}
|
||||||
|
if (defaultDist) config.dist = defaultDist
|
||||||
|
if (defaultSrc) config.src = defaultSrc
|
||||||
|
|
||||||
|
const client = await ensureAdminClient('dev', config)
|
||||||
|
|
||||||
|
const distDir = tmp.dirSync().name
|
||||||
|
console.log(cwd())
|
||||||
|
|
||||||
|
const bundler = new Parcel({
|
||||||
|
entries: './src/index.ts',
|
||||||
|
defaultConfig: '@parcel/config-default',
|
||||||
|
mode: 'production',
|
||||||
|
defaultTargetOptions: {
|
||||||
|
distDir: join(cwd(), 'dist'),
|
||||||
|
outputFormat: 'global',
|
||||||
|
sourceMaps: false,
|
||||||
|
},
|
||||||
|
shouldDisableCache: true,
|
||||||
|
// targets: {
|
||||||
|
// iife: {
|
||||||
|
// distDir: './dist',
|
||||||
|
// source: './src/index.ts',
|
||||||
|
// context: 'browser',
|
||||||
|
// outputFormat: 'global',
|
||||||
|
// sourceMap: {
|
||||||
|
// inline: true,
|
||||||
|
// },
|
||||||
|
// },
|
||||||
|
// },
|
||||||
|
})
|
||||||
|
|
||||||
|
const build = debounce(() => {
|
||||||
|
console.log(`Building...`)
|
||||||
|
bundler
|
||||||
|
.run()
|
||||||
|
.then((e) => {
|
||||||
|
console.log(`Build succeeded`, e)
|
||||||
|
console.log(e.bundleGraph.getBundles({ includeInline: true }))
|
||||||
|
let { bundleGraph } = e
|
||||||
|
})
|
||||||
|
.catch((e) => {
|
||||||
|
console.error(`Build failed with ${e}`)
|
||||||
|
})
|
||||||
|
}, 100)
|
||||||
|
|
||||||
|
build()
|
||||||
|
console.log(join(getProjectRoot(), 'src/**'))
|
||||||
|
chokidar
|
||||||
|
.watch([
|
||||||
|
join(getProjectRoot(), 'src/**'),
|
||||||
|
join(getProjectRoot(), 'node_modules/**'),
|
||||||
|
])
|
||||||
|
.on('all', (event, path) => {
|
||||||
|
build()
|
||||||
|
})
|
||||||
|
|
||||||
|
console.log('here')
|
||||||
|
})
|
||||||
|
}
|
100
packages/cli/src/commands/publish.ts
Normal file
100
packages/cli/src/commands/publish.ts
Normal file
@ -0,0 +1,100 @@
|
|||||||
|
import { Command } from 'commander'
|
||||||
|
import { existsSync, readFileSync } from 'fs'
|
||||||
|
import { join } from 'path'
|
||||||
|
import pocketbaseEs from 'pocketbase'
|
||||||
|
import { DEFAULT_PB_DEV_URL } from '../constants'
|
||||||
|
import { SessionState } from '../providers/CustomAuthStore'
|
||||||
|
import { die } from '../util/die'
|
||||||
|
import { ensureAdminClient } from '../util/ensureAdminClient'
|
||||||
|
import { getProjectRoot, readSettings } from '../util/project'
|
||||||
|
|
||||||
|
export type PublishConfig = {
|
||||||
|
session: SessionState
|
||||||
|
host: string
|
||||||
|
dist: string
|
||||||
|
}
|
||||||
|
|
||||||
|
const migrate = async (client: pocketbaseEs) => {
|
||||||
|
{
|
||||||
|
// VERSION 1
|
||||||
|
const res = await client.collections.getList(1, 1, {
|
||||||
|
filter: `name='pbscript'`,
|
||||||
|
})
|
||||||
|
const [item] = res.items
|
||||||
|
if (!item) {
|
||||||
|
await client.collections.create({
|
||||||
|
name: 'pbscript',
|
||||||
|
schema: [
|
||||||
|
{
|
||||||
|
name: 'type',
|
||||||
|
type: 'text',
|
||||||
|
required: true,
|
||||||
|
},
|
||||||
|
{
|
||||||
|
name: 'isActive',
|
||||||
|
type: 'bool',
|
||||||
|
},
|
||||||
|
{
|
||||||
|
name: 'data',
|
||||||
|
type: 'json',
|
||||||
|
required: true,
|
||||||
|
},
|
||||||
|
],
|
||||||
|
})
|
||||||
|
}
|
||||||
|
}
|
||||||
|
}
|
||||||
|
|
||||||
|
const publish = async (client: pocketbaseEs, fname: string) => {
|
||||||
|
const js = readFileSync(fname).toString()
|
||||||
|
const url = `${client.baseUrl}/api/pbscript/deploy`
|
||||||
|
const res = await client
|
||||||
|
.send(`api/pbscript/deploy`, {
|
||||||
|
method: 'post',
|
||||||
|
body: JSON.stringify({
|
||||||
|
source: js,
|
||||||
|
}),
|
||||||
|
headers: { 'Content-Type': 'application/json' },
|
||||||
|
})
|
||||||
|
.catch((e) => {
|
||||||
|
console.error(e)
|
||||||
|
throw e
|
||||||
|
})
|
||||||
|
}
|
||||||
|
|
||||||
|
export const addPublishCommand = (program: Command) => {
|
||||||
|
const _srcDefault = join(getProjectRoot(), './dist/index.js')
|
||||||
|
const _hostDefault = DEFAULT_PB_DEV_URL
|
||||||
|
program
|
||||||
|
.command('publish')
|
||||||
|
.description('Publish JS bundle to PBScript-enabled PocketBase instance')
|
||||||
|
.option(
|
||||||
|
'--dist <src>',
|
||||||
|
`Path to dist bundle (default: <project>/dist/index.js)`
|
||||||
|
)
|
||||||
|
.option('--host <host>', `PocketBase host (default: ${DEFAULT_PB_DEV_URL})`)
|
||||||
|
.action(async (options) => {
|
||||||
|
const defaultHost = options.host
|
||||||
|
const defaultDist = options.dist
|
||||||
|
|
||||||
|
const config: PublishConfig = {
|
||||||
|
session: { token: '', model: null },
|
||||||
|
host: DEFAULT_PB_DEV_URL,
|
||||||
|
dist: join(getProjectRoot(), './dist/index.js'),
|
||||||
|
...readSettings<PublishConfig>('publish'),
|
||||||
|
}
|
||||||
|
if (defaultHost) config.host = defaultHost
|
||||||
|
if (defaultDist) config.dist = defaultDist
|
||||||
|
|
||||||
|
const { host, dist } = config
|
||||||
|
|
||||||
|
if (!existsSync(dist)) {
|
||||||
|
die(`${dist} does not exist. Nothing to publish.`)
|
||||||
|
}
|
||||||
|
|
||||||
|
const client = await ensureAdminClient('publish', config)
|
||||||
|
console.log(`Deploying from ${dist} to ${host}`)
|
||||||
|
await migrate(client)
|
||||||
|
await publish(client, dist)
|
||||||
|
})
|
||||||
|
}
|
1
packages/cli/src/constants.ts
Normal file
1
packages/cli/src/constants.ts
Normal file
@ -0,0 +1 @@
|
|||||||
|
export const DEFAULT_PB_DEV_URL = `http://127.0.0.1:8090`
|
38
packages/cli/src/helpers/RealtimeSubscriptionManager.ts
Normal file
38
packages/cli/src/helpers/RealtimeSubscriptionManager.ts
Normal file
@ -0,0 +1,38 @@
|
|||||||
|
import { client } from '../client'
|
||||||
|
import {
|
||||||
|
Pb_Any_Record_Db,
|
||||||
|
Pb_CollectionName,
|
||||||
|
Pb_PkId,
|
||||||
|
Pb_Untrusted_Db,
|
||||||
|
} from '../schema/base'
|
||||||
|
|
||||||
|
export const createRealtimeSubscriptionManager = () => {
|
||||||
|
const subscriptions: { [_: string]: number } = {}
|
||||||
|
|
||||||
|
const subscribe = <TRec extends Pb_Any_Record_Db>(
|
||||||
|
collectionName: Pb_CollectionName,
|
||||||
|
cb: (rec: Pb_Untrusted_Db<TRec>) => void,
|
||||||
|
id?: Pb_PkId
|
||||||
|
) => {
|
||||||
|
const slug = id ? `${collectionName}/${id}` : collectionName
|
||||||
|
|
||||||
|
if (subscriptions[slug]) {
|
||||||
|
subscriptions[slug]++
|
||||||
|
} else {
|
||||||
|
subscriptions[slug] = 1
|
||||||
|
client.realtime.subscribe(slug, (e) => {
|
||||||
|
console.log(`Realtime update`, { e })
|
||||||
|
cb(e.record as unknown as Pb_Untrusted_Db<TRec>)
|
||||||
|
})
|
||||||
|
}
|
||||||
|
return () => {
|
||||||
|
subscriptions[slug]--
|
||||||
|
if (subscriptions[slug] === 0) {
|
||||||
|
console.log(`Realtime unsub`)
|
||||||
|
client.realtime.unsubscribe(slug)
|
||||||
|
}
|
||||||
|
}
|
||||||
|
}
|
||||||
|
|
||||||
|
return subscribe
|
||||||
|
}
|
13
packages/cli/src/helpers/buildQueryFilter.ts
Normal file
13
packages/cli/src/helpers/buildQueryFilter.ts
Normal file
@ -0,0 +1,13 @@
|
|||||||
|
import { map } from '@s-libs/micro-dash'
|
||||||
|
import { Pb_Any_Record_Db, Pb_QueryParams } from '../schema/base'
|
||||||
|
|
||||||
|
export type FieldStruct<TRec extends Pb_Any_Record_Db> = Partial<{
|
||||||
|
[_ in keyof TRec]: TRec[_]
|
||||||
|
}>
|
||||||
|
|
||||||
|
export const buildQueryFilter = <TRec extends Pb_Any_Record_Db>(
|
||||||
|
fields: FieldStruct<TRec>
|
||||||
|
): Pb_QueryParams => {
|
||||||
|
const filter = map(fields, (v, k) => `${k.toString()} = "${v}"`).join(' and ')
|
||||||
|
return { filter }
|
||||||
|
}
|
22
packages/cli/src/helpers/getOne.ts
Normal file
22
packages/cli/src/helpers/getOne.ts
Normal file
@ -0,0 +1,22 @@
|
|||||||
|
import { assertTruthy } from 'util/assert'
|
||||||
|
import { client } from '../client'
|
||||||
|
import {
|
||||||
|
Pb_Any_Collection_Name,
|
||||||
|
Pb_Any_Record_Db,
|
||||||
|
Pb_Untrusted_Db,
|
||||||
|
} from '../schema/base'
|
||||||
|
import { buildQueryFilter, FieldStruct } from './buildQueryFilter'
|
||||||
|
|
||||||
|
export const getOne = async <
|
||||||
|
TRec extends Pb_Any_Record_Db,
|
||||||
|
TFields extends FieldStruct<TRec> = FieldStruct<TRec>
|
||||||
|
>(
|
||||||
|
collectionName: Pb_Any_Collection_Name,
|
||||||
|
fields: TFields
|
||||||
|
) => {
|
||||||
|
const queryParams = buildQueryFilter(fields)
|
||||||
|
const recs = await client.records.getList(collectionName, 1, 2, queryParams)
|
||||||
|
assertTruthy(recs.totalItems < 2, `Expected exactly 0 or 1 items here`)
|
||||||
|
const [item] = recs.items
|
||||||
|
return item ? (recs.items[0] as unknown as Pb_Untrusted_Db<TRec>) : null
|
||||||
|
}
|
6
packages/cli/src/helpers/index.ts
Normal file
6
packages/cli/src/helpers/index.ts
Normal file
@ -0,0 +1,6 @@
|
|||||||
|
export * from './getOne'
|
||||||
|
export * from './onAuthStateChanged'
|
||||||
|
export * from './pbUid'
|
||||||
|
export * from './RealtimeSubscriptionManager'
|
||||||
|
export * from './signInAnonymously'
|
||||||
|
export * from './upsert'
|
27
packages/cli/src/helpers/mergeDeep.ts
Normal file
27
packages/cli/src/helpers/mergeDeep.ts
Normal file
@ -0,0 +1,27 @@
|
|||||||
|
import { forEach, isObject } from '@s-libs/micro-dash'
|
||||||
|
|
||||||
|
export const mergeDeep = <TObject>(dst: any, src: TObject) => {
|
||||||
|
forEach(src, (v, k) => {
|
||||||
|
if (isObject(v)) {
|
||||||
|
if (dst[k] === undefined) dst[k] = {}
|
||||||
|
if (!isObject(dst[k])) {
|
||||||
|
throw new Error(
|
||||||
|
`${k.toString()} is an object in default, but not in target`
|
||||||
|
)
|
||||||
|
}
|
||||||
|
dst[k] = mergeDeep(dst[k], v)
|
||||||
|
} else {
|
||||||
|
if (isObject(dst[k])) {
|
||||||
|
throw new Error(
|
||||||
|
`${k.toString()} is an object in target, but not in default`
|
||||||
|
)
|
||||||
|
}
|
||||||
|
// The magic: if the target has no value for this field, use the
|
||||||
|
// default value
|
||||||
|
if (dst[k] === undefined) {
|
||||||
|
dst[k] = v
|
||||||
|
}
|
||||||
|
}
|
||||||
|
})
|
||||||
|
return dst as TObject
|
||||||
|
}
|
11
packages/cli/src/helpers/onAuthStateChanged.ts
Normal file
11
packages/cli/src/helpers/onAuthStateChanged.ts
Normal file
@ -0,0 +1,11 @@
|
|||||||
|
import { UnsubFunc } from 'store/backend/types'
|
||||||
|
import { client } from '../client'
|
||||||
|
|
||||||
|
export const onAuthStateChanged = (
|
||||||
|
cb: (user: typeof client.authStore.model) => void
|
||||||
|
): UnsubFunc => {
|
||||||
|
setTimeout(() => cb(client.authStore.model), 0)
|
||||||
|
return client.authStore.onChange(() => {
|
||||||
|
cb(client.authStore.model)
|
||||||
|
})
|
||||||
|
}
|
9
packages/cli/src/helpers/pbUid.ts
Normal file
9
packages/cli/src/helpers/pbUid.ts
Normal file
@ -0,0 +1,9 @@
|
|||||||
|
import { identity } from 'ts-brand'
|
||||||
|
import { client } from '../client'
|
||||||
|
import { Pb_UserId } from '../schema/base'
|
||||||
|
|
||||||
|
export const pbUid = () => {
|
||||||
|
const { id } = client.authStore.model || {}
|
||||||
|
if (!id) return
|
||||||
|
return identity<Pb_UserId>(id)
|
||||||
|
}
|
22
packages/cli/src/helpers/signInAnonymously.ts
Normal file
22
packages/cli/src/helpers/signInAnonymously.ts
Normal file
@ -0,0 +1,22 @@
|
|||||||
|
import { customAlphabet } from 'nanoid'
|
||||||
|
import { identity } from 'ts-brand'
|
||||||
|
import { client } from '../client'
|
||||||
|
import { Email, Password, PbCreds } from '../schema/base'
|
||||||
|
|
||||||
|
export const nanoid = customAlphabet('abcdefghijklmnopqrstuvwxyz')
|
||||||
|
|
||||||
|
export const signInAnonymously = () => {
|
||||||
|
const { email, password } = (() => {
|
||||||
|
const credsJson = localStorage.getItem('__pb_creds')
|
||||||
|
if (credsJson) {
|
||||||
|
return JSON.parse(credsJson) as PbCreds
|
||||||
|
}
|
||||||
|
const email = identity<Email>(`${nanoid()}@harvest.io`)
|
||||||
|
const password = identity<Password>(nanoid())
|
||||||
|
return { email, password }
|
||||||
|
})()
|
||||||
|
|
||||||
|
return client.users.authViaEmail(email, password).catch((e) => {
|
||||||
|
console.error(`Couldn't long in anonymously: ${e}`)
|
||||||
|
})
|
||||||
|
}
|
49
packages/cli/src/helpers/upsert.ts
Normal file
49
packages/cli/src/helpers/upsert.ts
Normal file
@ -0,0 +1,49 @@
|
|||||||
|
import { reduce } from '@s-libs/micro-dash'
|
||||||
|
import produce, { Draft, produceWithPatches } from 'immer'
|
||||||
|
import { assertExists, assertTruthy } from 'util/assert'
|
||||||
|
import { client } from '../client'
|
||||||
|
import {
|
||||||
|
Pb_Any_Collection_Name,
|
||||||
|
Pb_Any_Record_Db,
|
||||||
|
Pb_Untrusted_Db,
|
||||||
|
Pb_UserFields,
|
||||||
|
} from '../schema/base'
|
||||||
|
import { buildQueryFilter, FieldStruct } from './buildQueryFilter'
|
||||||
|
import { mergeDeep } from './mergeDeep'
|
||||||
|
|
||||||
|
export const upsert = async <TRow extends Pb_Any_Record_Db>(
|
||||||
|
collectionName: Pb_Any_Collection_Name,
|
||||||
|
filterFields: FieldStruct<TRow>,
|
||||||
|
mutate: (draft: Draft<Pb_UserFields<TRow>>) => void,
|
||||||
|
defaultRec: Pb_UserFields<TRow>
|
||||||
|
) => {
|
||||||
|
const queryParams = buildQueryFilter(filterFields)
|
||||||
|
const recs = await client.records.getList(collectionName, 1, 2, queryParams)
|
||||||
|
assertTruthy(recs.totalItems < 2, `Expected exactly 0 or 1 item to upsert`)
|
||||||
|
if (recs.totalItems === 0) {
|
||||||
|
// Insert
|
||||||
|
client.records.create(collectionName, produce(defaultRec, mutate))
|
||||||
|
} else {
|
||||||
|
// Update
|
||||||
|
const [item] = recs.items as unknown as Pb_Untrusted_Db<TRow>[]
|
||||||
|
assertExists(item, `Expected item here`)
|
||||||
|
const { id } = item
|
||||||
|
const safeItem = mergeDeep(item, defaultRec)
|
||||||
|
const [rec, patches] = produceWithPatches(safeItem, mutate)
|
||||||
|
console.log({ patches })
|
||||||
|
const final = reduce(
|
||||||
|
patches,
|
||||||
|
(carry, patch) => {
|
||||||
|
if (patch.op === 'add' || patch.op === 'replace') {
|
||||||
|
const [key] = patch.path
|
||||||
|
assertExists(key, `Expected key here`)
|
||||||
|
const _key = `${key}` as keyof Pb_UserFields<TRow>
|
||||||
|
carry[_key] = rec[_key]
|
||||||
|
}
|
||||||
|
return carry
|
||||||
|
},
|
||||||
|
{} as Partial<Pb_UserFields<TRow>>
|
||||||
|
)
|
||||||
|
client.records.update(collectionName, id, final)
|
||||||
|
}
|
||||||
|
}
|
20
packages/cli/src/index.ts
Normal file
20
packages/cli/src/index.ts
Normal file
@ -0,0 +1,20 @@
|
|||||||
|
import { program } from 'commander'
|
||||||
|
|
||||||
|
declare global {
|
||||||
|
let __go_app: any
|
||||||
|
}
|
||||||
|
|
||||||
|
import 'cross-fetch/polyfill'
|
||||||
|
import 'eventsource'
|
||||||
|
import packagex from '../package.json'
|
||||||
|
import { addPublishCommand } from './commands/publish'
|
||||||
|
console.log(`PBScript ${packagex.version}`)
|
||||||
|
|
||||||
|
program
|
||||||
|
.name('pbscript')
|
||||||
|
.description('CLI for JavaScript extensions for PocketBase ')
|
||||||
|
.version('0.0.1')
|
||||||
|
addPublishCommand(program)
|
||||||
|
// addDevCommand(program)
|
||||||
|
|
||||||
|
program.parse()
|
57
packages/cli/src/providers/CustomAuthStore.ts
Normal file
57
packages/cli/src/providers/CustomAuthStore.ts
Normal file
@ -0,0 +1,57 @@
|
|||||||
|
import { Admin, BaseAuthStore, User } from 'pocketbase'
|
||||||
|
|
||||||
|
export interface SerializeOptions {
|
||||||
|
encode?: (val: string | number | boolean) => string
|
||||||
|
maxAge?: number
|
||||||
|
domain?: string
|
||||||
|
path?: string
|
||||||
|
expires?: Date
|
||||||
|
httpOnly?: boolean
|
||||||
|
secure?: boolean
|
||||||
|
priority?: string
|
||||||
|
sameSite?: boolean | string
|
||||||
|
}
|
||||||
|
|
||||||
|
export type SessionState = { token: string; model: User | Admin | null }
|
||||||
|
export type SessionStateSaver = (state: SessionState) => void
|
||||||
|
|
||||||
|
export class CustomAuthStore extends BaseAuthStore {
|
||||||
|
_save: SessionStateSaver
|
||||||
|
|
||||||
|
constructor(state: SessionState, _save: SessionStateSaver) {
|
||||||
|
super()
|
||||||
|
const { token, model } = state
|
||||||
|
this.baseToken = token
|
||||||
|
this.baseModel = model
|
||||||
|
this._save = _save
|
||||||
|
}
|
||||||
|
get get() {
|
||||||
|
// console.log(`Get token`)
|
||||||
|
return this.baseToken
|
||||||
|
}
|
||||||
|
get model(): User | Admin | null {
|
||||||
|
// console.log(`get model`)
|
||||||
|
return this.baseModel
|
||||||
|
}
|
||||||
|
get isValid(): boolean {
|
||||||
|
// console.log(`isValid`)
|
||||||
|
return !!this.baseToken
|
||||||
|
}
|
||||||
|
save(token: string, model: User | Admin | null) {
|
||||||
|
this._save({ token, model })
|
||||||
|
this.baseModel = model
|
||||||
|
this.baseToken = token
|
||||||
|
}
|
||||||
|
clear(): void {
|
||||||
|
throw new Error(`Unsupported clear()`)
|
||||||
|
}
|
||||||
|
loadFromCookie(cookie: string, key?: string | undefined): void {
|
||||||
|
throw new Error(`Unsupported loadFromCookie()`)
|
||||||
|
}
|
||||||
|
exportToCookie(
|
||||||
|
options?: SerializeOptions | undefined,
|
||||||
|
key?: string | undefined
|
||||||
|
): string {
|
||||||
|
throw new Error(`Unsupported exportToCookie()`)
|
||||||
|
}
|
||||||
|
}
|
8
packages/cli/src/util/assert.ts
Normal file
8
packages/cli/src/util/assert.ts
Normal file
@ -0,0 +1,8 @@
|
|||||||
|
export function assertExists<TType>(
|
||||||
|
v: TType,
|
||||||
|
message = `Value does not exist`
|
||||||
|
): asserts v is NonNullable<TType> {
|
||||||
|
if (typeof v === 'undefined') {
|
||||||
|
throw new Error(message)
|
||||||
|
}
|
||||||
|
}
|
4
packages/cli/src/util/die.ts
Normal file
4
packages/cli/src/util/die.ts
Normal file
@ -0,0 +1,4 @@
|
|||||||
|
export function die(msg: string): never {
|
||||||
|
console.error(msg)
|
||||||
|
process.exit(1)
|
||||||
|
}
|
71
packages/cli/src/util/ensureAdminClient.ts
Normal file
71
packages/cli/src/util/ensureAdminClient.ts
Normal file
@ -0,0 +1,71 @@
|
|||||||
|
import prompts from 'prompts'
|
||||||
|
import { SessionState } from '../providers/CustomAuthStore'
|
||||||
|
import { die } from './die'
|
||||||
|
import { isAdmin, pbClient } from './pbClient'
|
||||||
|
import { mkProjectSaver } from './project'
|
||||||
|
|
||||||
|
function isEmail(email: string) {
|
||||||
|
var emailFormat = /^[a-zA-Z0-9_.+-]+@[a-zA-Z0-9-]+\.[a-zA-Z0-9-.]+$/
|
||||||
|
if (email !== '' && email.match(emailFormat)) {
|
||||||
|
return true
|
||||||
|
}
|
||||||
|
|
||||||
|
return false
|
||||||
|
}
|
||||||
|
|
||||||
|
export type ConnectionConfig = {
|
||||||
|
session: SessionState
|
||||||
|
host: string
|
||||||
|
}
|
||||||
|
|
||||||
|
export const ensureAdminClient = async (
|
||||||
|
slug: string,
|
||||||
|
config: ConnectionConfig
|
||||||
|
) => {
|
||||||
|
const saver = mkProjectSaver<ConnectionConfig>(slug)
|
||||||
|
const client = pbClient(config, (session) =>
|
||||||
|
saver((config) => ({ ...config, session }))
|
||||||
|
)
|
||||||
|
const _isAdmin = await isAdmin(client)
|
||||||
|
if (_isAdmin) {
|
||||||
|
return client
|
||||||
|
}
|
||||||
|
|
||||||
|
const { host } = config
|
||||||
|
|
||||||
|
console.log(
|
||||||
|
`You must be logged in to ${host}/_ as a PocketBase admin to continue.`
|
||||||
|
)
|
||||||
|
|
||||||
|
while (true) {
|
||||||
|
const response = await prompts(
|
||||||
|
[
|
||||||
|
{
|
||||||
|
type: 'text',
|
||||||
|
name: 'username',
|
||||||
|
message: 'Username (email):',
|
||||||
|
validate: (value: string) =>
|
||||||
|
isEmail(value) ? true : `Enter an email address`,
|
||||||
|
},
|
||||||
|
{
|
||||||
|
type: 'password',
|
||||||
|
name: 'password',
|
||||||
|
message: 'Password:',
|
||||||
|
validate: (value: string) =>
|
||||||
|
value.length > 0 ? true : `Enter a password`,
|
||||||
|
},
|
||||||
|
],
|
||||||
|
{ onCancel: () => die(`Exited.`) }
|
||||||
|
)
|
||||||
|
const { username, password } = response
|
||||||
|
try {
|
||||||
|
await client.admins.authViaEmail(username, password)
|
||||||
|
saver((config) => ({ ...config, host }))
|
||||||
|
console.log(`Successfully logged in as ${username} for project `)
|
||||||
|
break
|
||||||
|
} catch (e) {
|
||||||
|
console.error(`Login failed for ${username}. Try again.`)
|
||||||
|
}
|
||||||
|
}
|
||||||
|
return client
|
||||||
|
}
|
47
packages/cli/src/util/pbClient.ts
Normal file
47
packages/cli/src/util/pbClient.ts
Normal file
@ -0,0 +1,47 @@
|
|||||||
|
import { default as PocketBase, default as pocketbaseEs } from 'pocketbase'
|
||||||
|
import {
|
||||||
|
CustomAuthStore,
|
||||||
|
SessionStateSaver,
|
||||||
|
} from '../providers/CustomAuthStore'
|
||||||
|
import { assertExists } from './assert'
|
||||||
|
import { die } from './die'
|
||||||
|
import { ConnectionConfig } from './ensureAdminClient'
|
||||||
|
|
||||||
|
export const pbClient = (
|
||||||
|
config: ConnectionConfig,
|
||||||
|
saver: SessionStateSaver
|
||||||
|
) => {
|
||||||
|
const { host, session } = config
|
||||||
|
const client = new PocketBase(
|
||||||
|
host,
|
||||||
|
'en-US',
|
||||||
|
new CustomAuthStore(session, saver)
|
||||||
|
)
|
||||||
|
return client
|
||||||
|
}
|
||||||
|
|
||||||
|
export const isAdmin = async (client: pocketbaseEs) => {
|
||||||
|
if (!client.authStore.isValid) return false
|
||||||
|
const { model } = client.authStore
|
||||||
|
if (!model) return false
|
||||||
|
const res = await client.admins.getOne(model.id)
|
||||||
|
if (!res) return false
|
||||||
|
return true
|
||||||
|
}
|
||||||
|
|
||||||
|
export const adminPbClient = async (
|
||||||
|
config: ConnectionConfig,
|
||||||
|
saver: SessionStateSaver
|
||||||
|
) => {
|
||||||
|
const client = pbClient(config, saver)
|
||||||
|
if (!client.authStore.isValid) {
|
||||||
|
die(`Must be logged in to PocketBase as an admin.`)
|
||||||
|
}
|
||||||
|
const { model } = client.authStore
|
||||||
|
assertExists(model, `Expected a valid model here`)
|
||||||
|
const res = await client.admins.getOne(model.id)
|
||||||
|
if (!res) {
|
||||||
|
die(`User must be an admin user.`)
|
||||||
|
}
|
||||||
|
return client
|
||||||
|
}
|
52
packages/cli/src/util/project.ts
Normal file
52
packages/cli/src/util/project.ts
Normal file
@ -0,0 +1,52 @@
|
|||||||
|
import { findUpSync } from 'find-up'
|
||||||
|
import { existsSync, mkdirSync, readFileSync, writeFileSync } from 'fs'
|
||||||
|
import { dirname, join } from 'path'
|
||||||
|
import { cwd } from 'process'
|
||||||
|
import { SessionState } from '../providers/CustomAuthStore'
|
||||||
|
|
||||||
|
export type TConfigProjectConfig = {
|
||||||
|
dev: {
|
||||||
|
session: SessionState
|
||||||
|
host: string
|
||||||
|
}
|
||||||
|
publish: {
|
||||||
|
session: SessionState
|
||||||
|
host: string
|
||||||
|
}
|
||||||
|
src: string
|
||||||
|
dist: string
|
||||||
|
}
|
||||||
|
|
||||||
|
export const CACHE_FNAME = '.pbcache'
|
||||||
|
|
||||||
|
export const mkProjectSaver = <TConfig>(slug: string) => {
|
||||||
|
type ConfigMutator = (config: Partial<TConfig>) => Partial<TConfig>
|
||||||
|
return (m: ConfigMutator) => {
|
||||||
|
const root = getProjectRoot()
|
||||||
|
const cachePath = join(root, CACHE_FNAME)
|
||||||
|
if (!existsSync(cachePath)) {
|
||||||
|
mkdirSync(cachePath, { recursive: true })
|
||||||
|
}
|
||||||
|
const currentConfig = readSettings<TConfig>(slug)
|
||||||
|
const nextConfig = m(currentConfig)
|
||||||
|
const fname = join(cachePath, slug)
|
||||||
|
const json = JSON.stringify(nextConfig, null, 2)
|
||||||
|
console.log(`Saving to ${fname}`, json)
|
||||||
|
writeFileSync(`${fname}.json`, json)
|
||||||
|
}
|
||||||
|
}
|
||||||
|
|
||||||
|
export const getProjectRoot = () => {
|
||||||
|
const root = findUpSync(`package.json`)
|
||||||
|
if (!root) return cwd()
|
||||||
|
return dirname(root)
|
||||||
|
}
|
||||||
|
|
||||||
|
export const readSettings = <TConfig>(name: string): Partial<TConfig> => {
|
||||||
|
const root = getProjectRoot()
|
||||||
|
const fname = join(root, CACHE_FNAME, `${name}.json`)
|
||||||
|
if (!existsSync(fname)) return {}
|
||||||
|
const json = readFileSync(fname).toString()
|
||||||
|
const settings = JSON.parse(json)
|
||||||
|
return settings as Partial<TConfig>
|
||||||
|
}
|
18
packages/cli/tsconfig.json
Normal file
18
packages/cli/tsconfig.json
Normal file
@ -0,0 +1,18 @@
|
|||||||
|
{
|
||||||
|
"compilerOptions": {
|
||||||
|
"allowJs": true,
|
||||||
|
"checkJs": true,
|
||||||
|
"esModuleInterop": true,
|
||||||
|
"allowSyntheticDefaultImports": true,
|
||||||
|
"forceConsistentCasingInFileNames": true,
|
||||||
|
"resolveJsonModule": true,
|
||||||
|
"skipLibCheck": true,
|
||||||
|
"strict": true,
|
||||||
|
"module": "ESNext",
|
||||||
|
"target": "ESNext",
|
||||||
|
"moduleResolution": "node",
|
||||||
|
"noUncheckedIndexedAccess": true,
|
||||||
|
"strictNullChecks": true
|
||||||
|
},
|
||||||
|
"include": ["./src"]
|
||||||
|
}
|
@ -1,13 +1,9 @@
|
|||||||
import PocketBase from 'pocketbase'
|
import PocketBase from 'pocketbase'
|
||||||
import { Any_Record_Out } from './schema'
|
|
||||||
|
|
||||||
export const createRealtimeSubscriptionManager = (pocketbase: PocketBase) => {
|
export const createRealtimeSubscriptionManager = (pocketbase: PocketBase) => {
|
||||||
const subscriptions: { [_: string]: number } = {}
|
const subscriptions: { [_: string]: number } = {}
|
||||||
|
|
||||||
const subscribe = <TRec extends Any_Record_Out>(
|
const subscribe = <TRec>(slug: string, cb: (rec: TRec) => void) => {
|
||||||
slug: string,
|
|
||||||
cb: (rec: TRec) => void
|
|
||||||
) => {
|
|
||||||
if (subscriptions[slug]) {
|
if (subscriptions[slug]) {
|
||||||
subscriptions[slug]++
|
subscriptions[slug]++
|
||||||
} else {
|
} else {
|
||||||
|
@ -11,6 +11,8 @@ import {
|
|||||||
DAEMON_PB_PASSWORD,
|
DAEMON_PB_PASSWORD,
|
||||||
DAEMON_PB_PORT_BASE,
|
DAEMON_PB_PORT_BASE,
|
||||||
DAEMON_PB_USERNAME,
|
DAEMON_PB_USERNAME,
|
||||||
|
PUBLIC_APP_DOMAIN,
|
||||||
|
PUBLIC_APP_PROTOCOL,
|
||||||
PUBLIC_PB_DOMAIN,
|
PUBLIC_PB_DOMAIN,
|
||||||
PUBLIC_PB_PROTOCOL,
|
PUBLIC_PB_PROTOCOL,
|
||||||
PUBLIC_PB_SUBDOMAIN,
|
PUBLIC_PB_SUBDOMAIN,
|
||||||
@ -130,11 +132,11 @@ export const createInstanceManger = async () => {
|
|||||||
|
|
||||||
const getInstance = (subdomain: string) =>
|
const getInstance = (subdomain: string) =>
|
||||||
limiter.schedule(async () => {
|
limiter.schedule(async () => {
|
||||||
console.log(`Getting instance ${subdomain}`)
|
// console.log(`Getting instance ${subdomain}`)
|
||||||
{
|
{
|
||||||
const instance = instances[subdomain]
|
const instance = instances[subdomain]
|
||||||
if (instance) {
|
if (instance) {
|
||||||
console.log(`Found in cache: ${subdomain}`)
|
// console.log(`Found in cache: ${subdomain}`)
|
||||||
instance.heartbeat()
|
instance.heartbeat()
|
||||||
return instance
|
return instance
|
||||||
}
|
}
|
||||||
@ -142,13 +144,18 @@ export const createInstanceManger = async () => {
|
|||||||
|
|
||||||
console.log(`Checking ${subdomain} for permission`)
|
console.log(`Checking ${subdomain} for permission`)
|
||||||
|
|
||||||
const recs = await client.getInstanceBySubdomain(subdomain)
|
const [instance, owner] = await client.getInstanceBySubdomain(subdomain)
|
||||||
const [instance] = recs.items
|
|
||||||
if (!instance) {
|
if (!instance) {
|
||||||
console.log(`${subdomain} not found`)
|
console.log(`${subdomain} not found`)
|
||||||
return
|
return
|
||||||
}
|
}
|
||||||
|
|
||||||
|
if (!owner?.verified) {
|
||||||
|
throw new Error(
|
||||||
|
`Log in at ${PUBLIC_APP_PROTOCOL}://${PUBLIC_APP_DOMAIN} to verify your account.`
|
||||||
|
)
|
||||||
|
}
|
||||||
|
|
||||||
await client.updateInstanceStatus(subdomain, InstanceStatus.Port)
|
await client.updateInstanceStatus(subdomain, InstanceStatus.Port)
|
||||||
console.log(`${subdomain} found in DB`)
|
console.log(`${subdomain} found in DB`)
|
||||||
const exclude = map(instances, (i) => i.port)
|
const exclude = map(instances, (i) => i.port)
|
||||||
@ -162,6 +169,7 @@ export const createInstanceManger = async () => {
|
|||||||
console.log(`Found port for ${subdomain}: ${newPort}`)
|
console.log(`Found port for ${subdomain}: ${newPort}`)
|
||||||
|
|
||||||
await client.updateInstanceStatus(subdomain, InstanceStatus.Starting)
|
await client.updateInstanceStatus(subdomain, InstanceStatus.Starting)
|
||||||
|
|
||||||
const childProcess = await _spawn({
|
const childProcess = await _spawn({
|
||||||
subdomain,
|
subdomain,
|
||||||
port: newPort,
|
port: newPort,
|
||||||
|
@ -1,5 +1,5 @@
|
|||||||
import { InstanceStatus } from '@pockethost/common'
|
import { InstanceStatus } from '@pockethost/common'
|
||||||
import PocketBase from 'pocketbase'
|
import PocketBase, { Record, User } from 'pocketbase'
|
||||||
import { Collection_Serialized } from './migrations'
|
import { Collection_Serialized } from './migrations'
|
||||||
|
|
||||||
const safeCatch = <TIn extends any[], TOut>(
|
const safeCatch = <TIn extends any[], TOut>(
|
||||||
@ -27,24 +27,34 @@ export const createPbClient = (url: string) => {
|
|||||||
|
|
||||||
const getInstanceBySubdomain = safeCatch(
|
const getInstanceBySubdomain = safeCatch(
|
||||||
`getInstanceBySubdomain`,
|
`getInstanceBySubdomain`,
|
||||||
(subdomain: string) =>
|
(subdomain: string): Promise<[Record, User] | []> =>
|
||||||
client.records.getList(`instances`, 1, 1, {
|
client.records
|
||||||
filter: `subdomain = '${subdomain}'`,
|
.getList(`instances`, 1, 1, {
|
||||||
})
|
filter: `subdomain = '${subdomain}'`,
|
||||||
|
})
|
||||||
|
.then((recs) => {
|
||||||
|
if (recs.totalItems > 1) {
|
||||||
|
throw new Error(
|
||||||
|
`Expected just one or zero instance records for ${subdomain}`
|
||||||
|
)
|
||||||
|
}
|
||||||
|
const [instance] = recs.items
|
||||||
|
if (!instance) return []
|
||||||
|
return client.users.getOne(instance.uid).then((user) => {
|
||||||
|
return [instance, user]
|
||||||
|
})
|
||||||
|
})
|
||||||
)
|
)
|
||||||
|
|
||||||
const updateInstanceStatus = safeCatch(
|
const updateInstanceStatus = safeCatch(
|
||||||
`updateInstanceStatus`,
|
`updateInstanceStatus`,
|
||||||
async (subdomain: string, status: InstanceStatus) => {
|
async (subdomain: string, status: InstanceStatus) => {
|
||||||
const recs = await getInstanceBySubdomain(subdomain)
|
const [instance, owner] = await getInstanceBySubdomain(subdomain)
|
||||||
if (recs.totalItems !== 1) {
|
|
||||||
throw new Error(`Expected just one subdomain record for ${subdomain}`)
|
if (!instance) {
|
||||||
}
|
|
||||||
const [item] = recs.items
|
|
||||||
if (!item) {
|
|
||||||
throw new Error(`Expected item here for ${subdomain}`)
|
throw new Error(`Expected item here for ${subdomain}`)
|
||||||
}
|
}
|
||||||
await client.records.update(`instances`, item.id, { status })
|
await client.records.update(`instances`, instance.id, { status })
|
||||||
}
|
}
|
||||||
)
|
)
|
||||||
|
|
||||||
|
@ -9,38 +9,42 @@ export const createProxyServer = async () => {
|
|||||||
const proxy = httpProxy.createProxyServer({})
|
const proxy = httpProxy.createProxyServer({})
|
||||||
|
|
||||||
const server = createServer(async (req, res) => {
|
const server = createServer(async (req, res) => {
|
||||||
console.log(`Incoming request ${req.headers.host}/${req.url}`)
|
// console.log(`Incoming request ${req.headers.host}/${req.url}`)
|
||||||
|
|
||||||
const die = (msg: string) => {
|
const die = (msg: string) => {
|
||||||
console.error(`ERROR: ${msg}`)
|
console.error(`ERROR: ${msg}`)
|
||||||
res.writeHead(200, {
|
res.writeHead(403, {
|
||||||
'Content-Type': `text/plain`,
|
'Content-Type': `text/plain`,
|
||||||
})
|
})
|
||||||
res.end(msg)
|
res.end(msg)
|
||||||
}
|
}
|
||||||
|
|
||||||
const host = req.headers.host
|
const host = req.headers.host
|
||||||
if (!host) {
|
if (!host) {
|
||||||
die(`Host not found`)
|
throw new Error(`Host not found`)
|
||||||
return
|
|
||||||
}
|
}
|
||||||
const [subdomain, ...junk] = host.split('.')
|
const [subdomain, ...junk] = host.split('.')
|
||||||
if (!subdomain) {
|
if (!subdomain) {
|
||||||
die(`${host} has no subdomain.`)
|
throw new Error(`${host} has no subdomain.`)
|
||||||
|
}
|
||||||
|
try {
|
||||||
|
const instance = await instanceManager.getInstance(subdomain)
|
||||||
|
if (!instance) {
|
||||||
|
throw new Error(
|
||||||
|
`${host} not found. Please check the instance URL and try again, or create one at ${PUBLIC_APP_PROTOCOL}://${PUBLIC_APP_DOMAIN}.`
|
||||||
|
)
|
||||||
|
}
|
||||||
|
|
||||||
|
// console.log(
|
||||||
|
// `Forwarding proxy request for ${req.url} to instance ${instance.internalUrl}`
|
||||||
|
// )
|
||||||
|
const endRequest = instance.startRequest()
|
||||||
|
req.on('close', endRequest)
|
||||||
|
proxy.web(req, res, { target: instance.internalUrl })
|
||||||
|
} catch (e) {
|
||||||
|
die(`${e}`)
|
||||||
return
|
return
|
||||||
}
|
}
|
||||||
const instance = await instanceManager.getInstance(subdomain)
|
|
||||||
if (!instance) {
|
|
||||||
die(
|
|
||||||
`${host} not found. Please check the instance URL and try again, or create one at ${PUBLIC_APP_PROTOCOL}://${PUBLIC_APP_DOMAIN}`
|
|
||||||
)
|
|
||||||
return
|
|
||||||
}
|
|
||||||
console.log(
|
|
||||||
`Forwarding proxy request for ${req.url} to instance ${instance.internalUrl}`
|
|
||||||
)
|
|
||||||
const endRequest = instance.startRequest()
|
|
||||||
req.on('close', endRequest)
|
|
||||||
proxy.web(req, res, { target: instance.internalUrl })
|
|
||||||
})
|
})
|
||||||
|
|
||||||
console.log('daemon on port 3000')
|
console.log('daemon on port 3000')
|
||||||
|
1
packages/js-cloud-funcs/.gitignore
vendored
Normal file
1
packages/js-cloud-funcs/.gitignore
vendored
Normal file
@ -0,0 +1 @@
|
|||||||
|
dist
|
2
packages/js-cloud-funcs/.npmignore
Normal file
2
packages/js-cloud-funcs/.npmignore
Normal file
@ -0,0 +1,2 @@
|
|||||||
|
src
|
||||||
|
tsconfig.json
|
40
packages/js-cloud-funcs/package.json
Normal file
40
packages/js-cloud-funcs/package.json
Normal file
@ -0,0 +1,40 @@
|
|||||||
|
{
|
||||||
|
"name": "@pockethost/cloud-functions",
|
||||||
|
"version": "0.0.3",
|
||||||
|
"source": "src/index.ts",
|
||||||
|
"main": "dist/index.js",
|
||||||
|
"types": "dist/index.d.ts",
|
||||||
|
"type": "module",
|
||||||
|
"license": "MIT",
|
||||||
|
"author": {
|
||||||
|
"name": "Ben Allfree",
|
||||||
|
"url": "https://github.com/benallfree"
|
||||||
|
},
|
||||||
|
"repository": {
|
||||||
|
"url": "https://github.com/benallfree/pockethost"
|
||||||
|
},
|
||||||
|
"scripts": {
|
||||||
|
"dev": "chokidar '**' --ignore 'dist' -c 'parcel build --no-cache' --initial",
|
||||||
|
"build": "parcel build --no-cache"
|
||||||
|
},
|
||||||
|
"devDependencies": {
|
||||||
|
"parcel": "^2.7.0",
|
||||||
|
"typescript": "^4.8.3"
|
||||||
|
},
|
||||||
|
"targets": {
|
||||||
|
"main": {
|
||||||
|
"isLibrary": true,
|
||||||
|
"context": "browser",
|
||||||
|
"outputFormat": "esmodule",
|
||||||
|
"sourceMap": true,
|
||||||
|
"includeNodeModules": true
|
||||||
|
}
|
||||||
|
},
|
||||||
|
"dependencies": {
|
||||||
|
"@s-libs/micro-dash": "^14.1.0",
|
||||||
|
"nanoevents": "^7.0.1",
|
||||||
|
"nanoid": "^4.0.0",
|
||||||
|
"svelte": "^3.51.0",
|
||||||
|
"ts-brand": "^0.0.2"
|
||||||
|
}
|
||||||
|
}
|
282
packages/js-cloud-funcs/readme.md
Normal file
282
packages/js-cloud-funcs/readme.md
Normal file
@ -0,0 +1,282 @@
|
|||||||
|
# TS/JS Cloud Functions for PocketBase
|
||||||
|
|
||||||
|
**_Write all your PocketBase server-side logic in TS/JS_**
|
||||||
|
|
||||||
|
PBScript allows you to write [PocketBase](https://pocketbase.io) server-side functions in Typescript or Javascript without recompiling.
|
||||||
|
|
||||||
|
With PBScript, you can:
|
||||||
|
|
||||||
|
- ✅ Write your server-side logic in Typescript or JS
|
||||||
|
- ✅ Access models, collections, transactions, hooks, and all server-side features of PocketBase
|
||||||
|
- ✅ Communicate with PocketBase using a streamlined JavaScript-style API
|
||||||
|
- ✅ Deploy and alter cloud functions without rebuilding _or even restarting_ PocketBase
|
||||||
|
- ✅ Roll back to previous versions
|
||||||
|
- ✅ Use the `pbscript` CLI tool for intelligent dev mode, live deployment, and rollback
|
||||||
|
|
||||||
|
<h3>Table of Contents</h3>
|
||||||
|
|
||||||
|
<!-- @import "[TOC]" {cmd="toc" depthFrom=2 depthTo=6 orderedList=false} -->
|
||||||
|
|
||||||
|
<!-- code_chunk_output -->
|
||||||
|
|
||||||
|
- [Introduction](#introduction)
|
||||||
|
- [Getting Started](#getting-started)
|
||||||
|
- [Developing your script](#developing-your-script)
|
||||||
|
- [Building and deploying your script](#building-and-deploying-your-script)
|
||||||
|
- [API](#api)
|
||||||
|
- [`__go.ping()`](#__goping)
|
||||||
|
- [`__go.addRoute()`](#__goaddroute)
|
||||||
|
- [`__go.app`](#__goapp)
|
||||||
|
- [Advanced](#advanced)
|
||||||
|
- [Upgrading an Existing Custom PocketBase](#upgrading-an-existing-custom-pocketbase)
|
||||||
|
|
||||||
|
<!-- /code_chunk_output -->
|
||||||
|
|
||||||
|
## Introduction
|
||||||
|
|
||||||
|
PBScript extends PocketBase with an [ES-5.1 (ECMA 262)](https://262.ecma-international.org/5.1/) scripting engine powered by [goja](https://github.com/dop251/goja).
|
||||||
|
|
||||||
|
Code executes in a secure sandboxed environment without a [Node.js API](https://nodejs.org/docs/latest/api/) or [Web APIs](https://developer.mozilla.org/en-US/docs/Web/API). Instead, the runtime environment is PocketBase-specific, with full access to PocketBase's [native Go extension API](https://pocketbase.io/docs/use-as-framework/) and includes streamlined functions and helper utilities written in JavaScript (@pbscript/core).
|
||||||
|
|
||||||
|
Use `pbscript` command line tool to build and publish cloud functions to any PBScript-enabled PocketBase instance.
|
||||||
|
|
||||||
|
## Getting Started
|
||||||
|
|
||||||
|
<h3>1. Create a PocketBase instance</h3>
|
||||||
|
|
||||||
|
To run JS functions, you need to run a PocketBase instance which has been enhanced with PBScript.
|
||||||
|
|
||||||
|
You can do it any way you like, just as long as you end up with an admin login for the next section.
|
||||||
|
|
||||||
|
**Option 1 (free): run fully managed on [pockethost.io](https://pockethost.io)**
|
||||||
|
|
||||||
|
The absolute easiest way to provision a new PocketBase instance enhanced with PBScript is to use [pockethost.io](https://pockethost.io). You'll be up and running with a PocketBase URL in under 30 seconds. This is as close to a Firebase/Supabase BaaS experience as you can get.
|
||||||
|
|
||||||
|
**Option 2 (free): run self-managed on fly.io**
|
||||||
|
|
||||||
|
If you'd rather manage your resources yourself, you can follow the instructions in [this thread](https://github.com/pocketbase/pocketbase/discussions/537) to get up and running on fly.io.
|
||||||
|
|
||||||
|
This option takes about 30 minutes to set up.
|
||||||
|
|
||||||
|
**Option 3 (free): run a custom build locally**
|
||||||
|
|
||||||
|
If you just want to run locally or have a special use case, you can create your own build.
|
||||||
|
|
||||||
|
Create `pocketbase.go`:
|
||||||
|
|
||||||
|
```go
|
||||||
|
package main
|
||||||
|
|
||||||
|
import (
|
||||||
|
"log"
|
||||||
|
|
||||||
|
pocketbase "github.com/benallfree/pbscript/modules/pbscript"
|
||||||
|
)
|
||||||
|
|
||||||
|
|
||||||
|
func main() {
|
||||||
|
app := pocketbase.New()
|
||||||
|
|
||||||
|
if err := app.Start(); err != nil {
|
||||||
|
log.Fatal(err)
|
||||||
|
}
|
||||||
|
}
|
||||||
|
```
|
||||||
|
|
||||||
|
On the command line, run:
|
||||||
|
|
||||||
|
```bash
|
||||||
|
go get github.com/benallfree/pbscript/modules/pbscript
|
||||||
|
go run pocketbase.go serve
|
||||||
|
|
||||||
|
> Server started at: http://127.0.0.1:8090
|
||||||
|
- REST API: http://127.0.0.1:8090/api/
|
||||||
|
- Admin UI: http://127.0.0.1:8090/_/
|
||||||
|
```
|
||||||
|
|
||||||
|
<h3>2. Create a new JS/TS project</h3>
|
||||||
|
|
||||||
|
You can create any type of TS/JS project you want, but here's the `package.json` we recommend. We also have a [sample PBScript project](https://github.com/benallfree/pbscript/tree/master/packages/sample) you can check out.
|
||||||
|
|
||||||
|
The important part is that your script gets bundled as ES5:
|
||||||
|
|
||||||
|
```json
|
||||||
|
{
|
||||||
|
"name": "sample",
|
||||||
|
"version": "0.0.1",
|
||||||
|
"dependencies": {
|
||||||
|
"@pbscript/core": "^0.0.1"
|
||||||
|
},
|
||||||
|
"scripts": {
|
||||||
|
"build": "parcel build --no-cache",
|
||||||
|
"deploy:local": "pbscript deploy --host 'http://127.0.0.1:8090'",
|
||||||
|
"dev": "chokidar 'src/**' './node_modules/**' -c 'yarn build && yarn deploy:local' --initial"
|
||||||
|
},
|
||||||
|
"targets": {
|
||||||
|
"iife": {
|
||||||
|
"source": "./src/index.ts",
|
||||||
|
"context": "browser",
|
||||||
|
"outputFormat": "global",
|
||||||
|
"sourceMap": {
|
||||||
|
"inline": true
|
||||||
|
}
|
||||||
|
}
|
||||||
|
},
|
||||||
|
"devDependencies": {
|
||||||
|
"chokidar-cli": "^3.0.0",
|
||||||
|
"parcel": "^2.7.0"
|
||||||
|
}
|
||||||
|
}
|
||||||
|
```
|
||||||
|
|
||||||
|
<h3>3. Use the `pbscript` CLI tool to log in</h3>
|
||||||
|
|
||||||
|
Enter your project directory and log in to your PocketBase instance using the admin account you created in Step #1:
|
||||||
|
|
||||||
|
```
|
||||||
|
npx pbscript login <username> <password> --host <pocketbase URL>
|
||||||
|
```
|
||||||
|
|
||||||
|
This will create a file named `.pbcache`. Add it to `.gitignore`.
|
||||||
|
|
||||||
|
## Developing your script
|
||||||
|
|
||||||
|
In your command shell, run:
|
||||||
|
|
||||||
|
```bash
|
||||||
|
npx pbscript dev
|
||||||
|
```
|
||||||
|
|
||||||
|
PBScript is designed for a fast development cycle. If you used our `package.json`, this command will watch for changes (in `./dist`) and re-deploy your script on every change.
|
||||||
|
|
||||||
|
PBScript updates do not require a PocketBase restart. Old versions of your script are kept in an archive table for easy rollbacks.
|
||||||
|
|
||||||
|
## Building and deploying your script
|
||||||
|
|
||||||
|
`pbscript` knows how to deploy to any PBScript-enabled PocketBase instance.
|
||||||
|
|
||||||
|
**Connect to your live site**
|
||||||
|
|
||||||
|
First, connect to your live site. `pbscript` will remember your credentials for future commands.
|
||||||
|
|
||||||
|
```bash
|
||||||
|
npx pbscript login <username> <password> --host https://yourproject.pockethost.io
|
||||||
|
Saved to .pbcache
|
||||||
|
```
|
||||||
|
|
||||||
|
**Build your `./dist/index.js` bundle**
|
||||||
|
|
||||||
|
You can build your script bundle however you want, just make sure you end up with ONE bundle file in `./dist/index.js`. If you use source maps, inline them. `pbscript` only deploys this one file.
|
||||||
|
|
||||||
|
**Deploy your latest changes**
|
||||||
|
|
||||||
|
You can deploy changes at any time without restarting PocketBase. All realtime connections and users will remain connected.
|
||||||
|
|
||||||
|
```bash
|
||||||
|
pbscript deploy --host <your pocketbase URL>
|
||||||
|
```
|
||||||
|
|
||||||
|
Or, add it to `package.json`:
|
||||||
|
|
||||||
|
```json
|
||||||
|
"scripts": {
|
||||||
|
"deploy": "pbscript deploy --host https://yourproject.pockethost.io"
|
||||||
|
}
|
||||||
|
```
|
||||||
|
|
||||||
|
## API
|
||||||
|
|
||||||
|
PBScript runs in a secure sandboxed environment inside PocketBase. A simplified subset of PocketBase's hooks are available. Complete Typescript definitions are included.
|
||||||
|
|
||||||
|
You might be accustomed to using the [NodeJS API](https://nodejs.org/docs/latest/api/) or the browser [Web API](https://developer.mozilla.org/en-US/docs/Web/API), but those APIs are not core features of ECMAScript. They are not safe or allowed in the PocketBase execution environment.
|
||||||
|
|
||||||
|
Instead, your script runs in the `PocketBaseApi` execution environment. `PocketBaseApi` set of API calls to interact with PocketBase. With it, you can do CRUD, transactions, hook into PocketBase events, new API endpoints, and generally extend PocketBase.
|
||||||
|
|
||||||
|
PBScript imports a `__go` variable containing low-level access to the PocketBase native API and other helpers implemented in Go. @pbscript/core uses `__go` internally, but if there is something missing, you may be able to accomplish it yourself by accessing `__go` directly.
|
||||||
|
|
||||||
|
### `__go.ping()`
|
||||||
|
|
||||||
|
Test function that should return `Hello from Go!`
|
||||||
|
|
||||||
|
Example:
|
||||||
|
|
||||||
|
```ts
|
||||||
|
console.log(__go.ping())
|
||||||
|
```
|
||||||
|
|
||||||
|
### `__go.addRoute()`
|
||||||
|
|
||||||
|
Add an API route.
|
||||||
|
|
||||||
|
Example:
|
||||||
|
|
||||||
|
```ts
|
||||||
|
__go.addRoute({
|
||||||
|
method: HttpMethods.Get,
|
||||||
|
path: `/api/hello`
|
||||||
|
handler: (context)=>{
|
||||||
|
context.send(HttpStatus.Ok, `Hello back!`)
|
||||||
|
}
|
||||||
|
})
|
||||||
|
```
|
||||||
|
|
||||||
|
### `__go.app`
|
||||||
|
|
||||||
|
Low-level primitive providing direct access to the `PocketBase` app instance. Normally you will not access this directly. The @pbscript/core library is built on top of this.
|
||||||
|
|
||||||
|
## Advanced
|
||||||
|
|
||||||
|
### Upgrading an Existing Custom PocketBase
|
||||||
|
|
||||||
|
The easiest way to get PBScript is to use our custom PocketBase module [github.com/benallfree/pbscript/modules/pocketbase](https://github.com/benallfree/pbscript/modules/pocketbase):
|
||||||
|
|
||||||
|
```go
|
||||||
|
package main
|
||||||
|
|
||||||
|
import (
|
||||||
|
"log"
|
||||||
|
|
||||||
|
"github.com/benallfree/pbscript/packages/pocketbase" // Notice this is a custom version of the PocketBase module
|
||||||
|
)
|
||||||
|
|
||||||
|
func main() {
|
||||||
|
app := pocketbase.New()
|
||||||
|
|
||||||
|
if err := app.Start(); err != nil {
|
||||||
|
log.Fatal(err)
|
||||||
|
}
|
||||||
|
}
|
||||||
|
```
|
||||||
|
|
||||||
|
If you are already using a custom PocketBase build, just swap out `github.com/pocketbase/pocketbase` with `github.com/benallfree/pbscript/packages/pocketbase` and everything will work as expected.
|
||||||
|
|
||||||
|
Or, if you prefer, you can do exactly what our custom module does:
|
||||||
|
|
||||||
|
```go
|
||||||
|
package main
|
||||||
|
|
||||||
|
import (
|
||||||
|
"github.com/pocketbase/pocketbase"
|
||||||
|
|
||||||
|
"github.com/benallfree/pbscript/packages/pbscript"
|
||||||
|
)
|
||||||
|
|
||||||
|
func main() {
|
||||||
|
app := pocketbase.New()
|
||||||
|
pbscript.StartPBScript(app) // Magic line to enable PBScript
|
||||||
|
|
||||||
|
if err := app.Start(); err != nil {
|
||||||
|
log.Fatal(err)
|
||||||
|
}
|
||||||
|
}
|
||||||
|
```
|
||||||
|
|
||||||
|
## Changelog
|
||||||
|
|
||||||
|
### 0.0.2
|
||||||
|
|
||||||
|
- Transaction support
|
||||||
|
|
||||||
|
### 0.0.1
|
||||||
|
|
||||||
|
- Initial release
|
38
packages/js-cloud-funcs/src/FunctionMarshaler.ts
Normal file
38
packages/js-cloud-funcs/src/FunctionMarshaler.ts
Normal file
@ -0,0 +1,38 @@
|
|||||||
|
import { PackedData } from './index'
|
||||||
|
import { assertExists } from './util/assert'
|
||||||
|
import { isFunction } from './util/isFunction'
|
||||||
|
|
||||||
|
// JSON encoder to tokenize functions and store their references
|
||||||
|
|
||||||
|
export type FunctionToken = string
|
||||||
|
export const createFunctionMarshaler = () => {
|
||||||
|
// Create a unique ID for this instance
|
||||||
|
const nanoid = (() => {
|
||||||
|
let i = 0
|
||||||
|
return () => i++
|
||||||
|
})()
|
||||||
|
|
||||||
|
const funcCache: { [_: FunctionToken]: () => any } = {}
|
||||||
|
const encode = (key: string, value: any) => {
|
||||||
|
if (isFunction(value)) {
|
||||||
|
const uuid = `fn_${nanoid()}`
|
||||||
|
funcCache[uuid] = value
|
||||||
|
return uuid
|
||||||
|
}
|
||||||
|
if (value === undefined) {
|
||||||
|
return `ü`
|
||||||
|
}
|
||||||
|
return value
|
||||||
|
}
|
||||||
|
|
||||||
|
const exec = (tok: FunctionToken) => {
|
||||||
|
const fn = funcCache[tok]
|
||||||
|
assertExists(fn, `Function ${tok} does not exist`)
|
||||||
|
const ret = fn()
|
||||||
|
return pack(ret)
|
||||||
|
}
|
||||||
|
|
||||||
|
const pack = (o: any) => JSON.stringify(o, encode) as PackedData
|
||||||
|
|
||||||
|
return { pack, exec }
|
||||||
|
}
|
40
packages/js-cloud-funcs/src/OnBeforeServe.ts
Normal file
40
packages/js-cloud-funcs/src/OnBeforeServe.ts
Normal file
@ -0,0 +1,40 @@
|
|||||||
|
// import { emitter, NativePocketBaseEvents } from './index'
|
||||||
|
|
||||||
|
// export const CoreMiddleware = {
|
||||||
|
// requireAdminOrUserAuth: () => 'RequireAdminOrUserAuth',
|
||||||
|
// }
|
||||||
|
|
||||||
|
// export type JsMiddlewareToken = string
|
||||||
|
|
||||||
|
// export type JsHttpRoutePath = string
|
||||||
|
|
||||||
|
// export type JsAddRouteConfig = {
|
||||||
|
// method: HttpMethods
|
||||||
|
// path: JsHttpRoutePath
|
||||||
|
// handler: (context: HttpRequestContext) => void
|
||||||
|
// middlewares: JsMiddlewareToken[]
|
||||||
|
// }
|
||||||
|
|
||||||
|
// export type OnBeforeServeEvent = {
|
||||||
|
// Router: {
|
||||||
|
// addRoute: (config: JsAddRouteConfig) => void
|
||||||
|
// }
|
||||||
|
// }
|
||||||
|
|
||||||
|
// export const onBeforeServe = (cb: (e: OnBeforeServeEvent) => void) => {
|
||||||
|
// emitter.on(NativePocketBaseEvents.OnBeforeServe, cb)
|
||||||
|
// }
|
||||||
|
|
||||||
|
// export const dispatchObBeforeServe = ()=>{
|
||||||
|
// case NativePocketBaseEvents.OnBeforeServe:
|
||||||
|
// const e: OnBeforeServeEvent = {
|
||||||
|
// Router: {
|
||||||
|
// addRoute: (config) => {
|
||||||
|
// const packed = pack(config)
|
||||||
|
// console.log(`Sending config back ${packed}`)
|
||||||
|
// __go_addRoute(packed)
|
||||||
|
// },
|
||||||
|
// },
|
||||||
|
// }
|
||||||
|
// emitter.emit(eventName, e)
|
||||||
|
// }
|
7
packages/js-cloud-funcs/src/constants.ts
Normal file
7
packages/js-cloud-funcs/src/constants.ts
Normal file
@ -0,0 +1,7 @@
|
|||||||
|
export enum HttpMethods {
|
||||||
|
Get = 'GET',
|
||||||
|
}
|
||||||
|
|
||||||
|
export enum HttpResponseStatuses {
|
||||||
|
Ok = 200,
|
||||||
|
}
|
616
packages/js-cloud-funcs/src/events.ts
Normal file
616
packages/js-cloud-funcs/src/events.ts
Normal file
@ -0,0 +1,616 @@
|
|||||||
|
import { createEvent } from './util/event'
|
||||||
|
|
||||||
|
export type ModelBeforeCreateEvent = {}
|
||||||
|
const [onModelBeforeCreate, fireModelBeforeCreate] =
|
||||||
|
createEvent<ModelBeforeCreateEvent>(`OnModelBeforeCreate`)
|
||||||
|
export { onModelBeforeCreate, fireModelBeforeCreate }
|
||||||
|
export { onModelAfterCreate, fireModelAfterCreate }
|
||||||
|
export { onModelBeforeUpdate, fireModelBeforeUpdate }
|
||||||
|
export { onModelAfterUpdate, fireModelAfterUpdate }
|
||||||
|
export { onModelBeforeDelete, fireModelBeforeDelete }
|
||||||
|
export { onModelAfterDelete, fireModelAfterDelete }
|
||||||
|
export {
|
||||||
|
onMailerBeforeAdminResetPasswordSend,
|
||||||
|
fireMailerBeforeAdminResetPasswordSend,
|
||||||
|
}
|
||||||
|
export {
|
||||||
|
onMailerAfterAdminResetPasswordSend,
|
||||||
|
fireMailerAfterAdminResetPasswordSend,
|
||||||
|
}
|
||||||
|
export {
|
||||||
|
onMailerBeforeUserResetPasswordSend,
|
||||||
|
fireMailerBeforeUserResetPasswordSend,
|
||||||
|
}
|
||||||
|
export {
|
||||||
|
onMailerAfterUserResetPasswordSend,
|
||||||
|
fireMailerAfterUserResetPasswordSend,
|
||||||
|
}
|
||||||
|
export {
|
||||||
|
onMailerBeforeUserVerificationSend,
|
||||||
|
fireMailerBeforeUserVerificationSend,
|
||||||
|
}
|
||||||
|
export {
|
||||||
|
onMailerAfterUserVerificationSend,
|
||||||
|
fireMailerAfterUserVerificationSend,
|
||||||
|
}
|
||||||
|
export {
|
||||||
|
onMailerBeforeUserChangeEmailSend,
|
||||||
|
fireMailerBeforeUserChangeEmailSend,
|
||||||
|
}
|
||||||
|
export { onMailerAfterUserChangeEmailSend, fireMailerAfterUserChangeEmailSend }
|
||||||
|
export { onRealtimeConnectRequest, fireRealtimeConnectRequest }
|
||||||
|
export { onRealtimeBeforeSubscribeRequest, fireRealtimeBeforeSubscribeRequest }
|
||||||
|
export { onRealtimeAfterSubscribeRequest, fireRealtimeAfterSubscribeRequest }
|
||||||
|
export { onSettingsListRequest, fireSettingsListRequest }
|
||||||
|
export { onSettingsBeforeUpdateRequest, fireSettingsBeforeUpdateRequest }
|
||||||
|
export { onSettingsAfterUpdateRequest, fireSettingsAfterUpdateRequest }
|
||||||
|
export { onFileDownloadRequest, fireFileDownloadRequest }
|
||||||
|
export { onAdminsListRequest, fireAdminsListRequest }
|
||||||
|
export { onAdminViewRequest, fireAdminViewRequest }
|
||||||
|
export { onAdminBeforeCreateRequest, fireAdminBeforeCreateRequest }
|
||||||
|
export { onAdminAfterCreateRequest, fireAdminAfterCreateRequest }
|
||||||
|
export { onAdminBeforeUpdateRequest, fireAdminBeforeUpdateRequest }
|
||||||
|
export { onAdminAfterUpdateRequest, fireAdminAfterUpdateRequest }
|
||||||
|
export { onAdminBeforeDeleteRequest, fireAdminBeforeDeleteRequest }
|
||||||
|
export { onAdminAfterDeleteRequest, fireAdminAfterDeleteRequest }
|
||||||
|
export { onAdminAuthRequest, fireAdminAuthRequest }
|
||||||
|
export { onUsersListRequest, fireUsersListRequest }
|
||||||
|
export { onUserViewRequest, fireUserViewRequest }
|
||||||
|
export { onUserBeforeCreateRequest, fireUserBeforeCreateRequest }
|
||||||
|
export { onUserAfterCreateRequest, fireUserAfterCreateRequest }
|
||||||
|
export { onUserBeforeUpdateRequest, fireUserBeforeUpdateRequest }
|
||||||
|
export { onUserAfterUpdateRequest, fireUserAfterUpdateRequest }
|
||||||
|
export { onUserBeforeDeleteRequest, fireUserBeforeDeleteRequest }
|
||||||
|
export { onUserAfterDeleteRequest, fireUserAfterDeleteRequest }
|
||||||
|
export { onUserAuthRequest, fireUserAuthRequest }
|
||||||
|
export { onUserListExternalAuths, fireUserListExternalAuths }
|
||||||
|
export {
|
||||||
|
onUserBeforeUnlinkExternalAuthRequest,
|
||||||
|
fireUserBeforeUnlinkExternalAuthRequest,
|
||||||
|
}
|
||||||
|
export {
|
||||||
|
onUserAfterUnlinkExternalAuthRequest,
|
||||||
|
fireUserAfterUnlinkExternalAuthRequest,
|
||||||
|
}
|
||||||
|
export { onRecordsListRequest, fireRecordsListRequest }
|
||||||
|
export { onRecordViewRequest, fireRecordViewRequest }
|
||||||
|
export { onRecordBeforeCreateRequest, fireRecordBeforeCreateRequest }
|
||||||
|
export { onRecordAfterCreateRequest, fireRecordAfterCreateRequest }
|
||||||
|
export { onRecordBeforeUpdateRequest, fireRecordBeforeUpdateRequest }
|
||||||
|
export { onRecordAfterUpdateRequest, fireRecordAfterUpdateRequest }
|
||||||
|
export { onRecordBeforeDeleteRequest, fireRecordBeforeDeleteRequest }
|
||||||
|
export { onRecordAfterDeleteRequest, fireRecordAfterDeleteRequest }
|
||||||
|
export { onCollectionsListRequest, fireCollectionsListRequest }
|
||||||
|
export { onCollectionViewRequest, fireCollectionViewRequest }
|
||||||
|
export { onCollectionBeforeCreateRequest, fireCollectionBeforeCreateRequest }
|
||||||
|
export { onCollectionAfterCreateRequest, fireCollectionAfterCreateRequest }
|
||||||
|
export { onCollectionBeforeUpdateRequest, fireCollectionBeforeUpdateRequest }
|
||||||
|
export { onCollectionAfterUpdateRequest, fireCollectionAfterUpdateRequest }
|
||||||
|
export { onCollectionBeforeDeleteRequest, fireCollectionBeforeDeleteRequest }
|
||||||
|
export { onCollectionAfterDeleteRequest, fireCollectionAfterDeleteRequest }
|
||||||
|
export { onCollectionsBeforeImportRequest, fireCollectionsBeforeImportRequest }
|
||||||
|
export { onCollectionsAfterImportRequest, fireCollectionsAfterImportRequest }
|
||||||
|
export { onBeforeServe, fireBeforeServe }
|
||||||
|
|
||||||
|
// OnModelAfterCreate hook is triggered after successfully
|
||||||
|
// inserting a new entry in the DB.
|
||||||
|
export type ModelAfterCreateEvent = {}
|
||||||
|
const [onModelAfterCreate, fireModelAfterCreate] =
|
||||||
|
createEvent<ModelAfterCreateEvent>(`OnModelAfterCreate`)
|
||||||
|
|
||||||
|
// OnModelBeforeUpdate hook is triggered before updating existing
|
||||||
|
// entry in the DB, allowing you to modify or validate the stored data.
|
||||||
|
export type ModelBeforeUpdateEvent = {}
|
||||||
|
const [onModelBeforeUpdate, fireModelBeforeUpdate] =
|
||||||
|
createEvent<ModelBeforeUpdateEvent>(`OnModelBeforeUpdate`)
|
||||||
|
|
||||||
|
// OnModelAfterUpdate hook is triggered after successfully updating
|
||||||
|
// existing entry in the DB.
|
||||||
|
export type ModelAfterUpdateEvent = {}
|
||||||
|
const [onModelAfterUpdate, fireModelAfterUpdate] =
|
||||||
|
createEvent<ModelAfterUpdateEvent>(`OnModelAfterUpdate`)
|
||||||
|
|
||||||
|
// OnModelBeforeDelete hook is triggered before deleting an
|
||||||
|
// existing entry from the DB.
|
||||||
|
export type ModelBeforeDeleteEvent = {}
|
||||||
|
const [onModelBeforeDelete, fireModelBeforeDelete] =
|
||||||
|
createEvent<ModelBeforeDeleteEvent>(`OnModelBeforeDelete`)
|
||||||
|
|
||||||
|
// OnModelAfterDelete is triggered after successfully deleting an
|
||||||
|
// existing entry from the DB.
|
||||||
|
export type ModelAfterDeleteEvent = {}
|
||||||
|
const [onModelAfterDelete, fireModelAfterDelete] =
|
||||||
|
createEvent<ModelAfterDeleteEvent>(`OnModelAfterDelete`)
|
||||||
|
|
||||||
|
// ---------------------------------------------------------------
|
||||||
|
// Mailer event hooks
|
||||||
|
// ---------------------------------------------------------------
|
||||||
|
|
||||||
|
// OnMailerBeforeAdminResetPasswordSend hook is triggered right before
|
||||||
|
// sending a password reset email to an admin.
|
||||||
|
//
|
||||||
|
// Could be used to send your own custom email template if
|
||||||
|
// [hook.StopPropagation] is returned in one of its listeners.
|
||||||
|
export type MailerBeforeAdminResetPasswordSendEvent = {}
|
||||||
|
const [
|
||||||
|
onMailerBeforeAdminResetPasswordSend,
|
||||||
|
fireMailerBeforeAdminResetPasswordSend,
|
||||||
|
] = createEvent<MailerBeforeAdminResetPasswordSendEvent>(
|
||||||
|
`OnMailerBeforeAdminResetPasswordSend`
|
||||||
|
)
|
||||||
|
|
||||||
|
// OnMailerAfterAdminResetPasswordSend hook is triggered after
|
||||||
|
// admin password reset email was successfully sent.
|
||||||
|
export type MailerAfterAdminResetPasswordSendEvent = {}
|
||||||
|
const [
|
||||||
|
onMailerAfterAdminResetPasswordSend,
|
||||||
|
fireMailerAfterAdminResetPasswordSend,
|
||||||
|
] = createEvent<MailerAfterAdminResetPasswordSendEvent>(
|
||||||
|
`OnMailerBeforeAdminResetPasswordSend`
|
||||||
|
)
|
||||||
|
|
||||||
|
// OnMailerBeforeUserResetPasswordSend hook is triggered right before
|
||||||
|
// sending a password reset email to a user.
|
||||||
|
//
|
||||||
|
// Could be used to send your own custom email template if
|
||||||
|
// [hook.StopPropagation] is returned in one of its listeners.
|
||||||
|
export type MailerBeforeUserResetPasswordSendEvent = {}
|
||||||
|
const [
|
||||||
|
onMailerBeforeUserResetPasswordSend,
|
||||||
|
fireMailerBeforeUserResetPasswordSend,
|
||||||
|
] = createEvent<MailerBeforeUserResetPasswordSendEvent>(
|
||||||
|
`OnMailerBeforeUserResetPasswordSend`
|
||||||
|
)
|
||||||
|
|
||||||
|
// OnMailerAfterUserResetPasswordSend hook is triggered after
|
||||||
|
// a user password reset email was successfully sent.
|
||||||
|
export type MailerAfterUserResetPasswordSendEvent = {}
|
||||||
|
const [
|
||||||
|
onMailerAfterUserResetPasswordSend,
|
||||||
|
fireMailerAfterUserResetPasswordSend,
|
||||||
|
] = createEvent<MailerAfterUserResetPasswordSendEvent>(
|
||||||
|
`OnMailerAfterUserResetPasswordSend`
|
||||||
|
)
|
||||||
|
|
||||||
|
// OnMailerBeforeUserVerificationSend hook is triggered right before
|
||||||
|
// sending a verification email to a user.
|
||||||
|
//
|
||||||
|
// Could be used to send your own custom email template if
|
||||||
|
// [hook.StopPropagation] is returned in one of its listeners.
|
||||||
|
export type MailerBeforeUserVerificationSendEvent = {}
|
||||||
|
const [
|
||||||
|
onMailerBeforeUserVerificationSend,
|
||||||
|
fireMailerBeforeUserVerificationSend,
|
||||||
|
] = createEvent<MailerBeforeUserVerificationSendEvent>(
|
||||||
|
`OnMailerBeforeUserVerificationSend`
|
||||||
|
)
|
||||||
|
|
||||||
|
// OnMailerAfterUserVerificationSend hook is triggered after a user
|
||||||
|
// verification email was successfully sent.
|
||||||
|
export type MailerAfterUserVerificationSendEvent = {}
|
||||||
|
const [onMailerAfterUserVerificationSend, fireMailerAfterUserVerificationSend] =
|
||||||
|
createEvent<MailerAfterUserVerificationSendEvent>(
|
||||||
|
`OnMailerAfterUserVerificationSend`
|
||||||
|
)
|
||||||
|
|
||||||
|
// OnMailerBeforeUserChangeEmailSend hook is triggered right before
|
||||||
|
// sending a confirmation new address email to a a user.
|
||||||
|
//
|
||||||
|
// Could be used to send your own custom email template if
|
||||||
|
// [hook.StopPropagation] is returned in one of its listeners.
|
||||||
|
export type MailerBeforeUserChangeEmailSendEvent = {}
|
||||||
|
const [onMailerBeforeUserChangeEmailSend, fireMailerBeforeUserChangeEmailSend] =
|
||||||
|
createEvent<MailerBeforeUserChangeEmailSendEvent>(
|
||||||
|
`OnMailerBeforeUserChangeEmailSend`
|
||||||
|
)
|
||||||
|
|
||||||
|
// OnMailerAfterUserChangeEmailSend hook is triggered after a user
|
||||||
|
// change address email was successfully sent.
|
||||||
|
export type MailerAfterUserChangeEmailSendEvent = {}
|
||||||
|
const [onMailerAfterUserChangeEmailSend, fireMailerAfterUserChangeEmailSend] =
|
||||||
|
createEvent<MailerAfterUserChangeEmailSendEvent>(
|
||||||
|
`OnMailerAfterUserChangeEmailSend`
|
||||||
|
)
|
||||||
|
|
||||||
|
// ---------------------------------------------------------------
|
||||||
|
// Realtime API event hooks
|
||||||
|
// ---------------------------------------------------------------
|
||||||
|
|
||||||
|
// OnRealtimeConnectRequest hook is triggered right before establishing
|
||||||
|
// the SSE client connection.
|
||||||
|
export type RealtimeConnectRequestEvent = {}
|
||||||
|
const [onRealtimeConnectRequest, fireRealtimeConnectRequest] =
|
||||||
|
createEvent<RealtimeConnectRequestEvent>(`OnRealtimeConnectRequest`)
|
||||||
|
|
||||||
|
// OnRealtimeBeforeSubscribeRequest hook is triggered before changing
|
||||||
|
// the client subscriptions, allowing you to further validate and
|
||||||
|
// modify the submitted change.
|
||||||
|
export type RealtimeBeforeSubscribeRequestEvent = {}
|
||||||
|
const [onRealtimeBeforeSubscribeRequest, fireRealtimeBeforeSubscribeRequest] =
|
||||||
|
createEvent<RealtimeBeforeSubscribeRequestEvent>(
|
||||||
|
`OnRealtimeBeforeSubscribeRequest`
|
||||||
|
)
|
||||||
|
|
||||||
|
// OnRealtimeAfterSubscribeRequest hook is triggered after the client
|
||||||
|
// subscriptions were successfully changed.
|
||||||
|
export type RealtimeAfterSubscribeRequestEvent = {}
|
||||||
|
const [onRealtimeAfterSubscribeRequest, fireRealtimeAfterSubscribeRequest] =
|
||||||
|
createEvent<RealtimeAfterSubscribeRequestEvent>(
|
||||||
|
`OnRealtimeAfterSubscribeRequest`
|
||||||
|
)
|
||||||
|
|
||||||
|
// ---------------------------------------------------------------
|
||||||
|
// Settings API event hooks
|
||||||
|
// ---------------------------------------------------------------
|
||||||
|
|
||||||
|
// OnSettingsListRequest hook is triggered on each successful
|
||||||
|
// API Settings list request.
|
||||||
|
//
|
||||||
|
// Could be used to validate or modify the response before
|
||||||
|
// returning it to the client.
|
||||||
|
export type SettingsListRequestEvent = {}
|
||||||
|
const [onSettingsListRequest, fireSettingsListRequest] =
|
||||||
|
createEvent<SettingsListRequestEvent>(`OnSettingsListRequest`)
|
||||||
|
|
||||||
|
// OnSettingsBeforeUpdateRequest hook is triggered before each API
|
||||||
|
// Settings update request (after request data load and before settings persistence).
|
||||||
|
//
|
||||||
|
// Could be used to additionally validate the request data or
|
||||||
|
// implement completely different persistence behavior
|
||||||
|
// (returning [hook.StopPropagation]).
|
||||||
|
export type SettingsBeforeUpdateRequestEvent = {}
|
||||||
|
const [onSettingsBeforeUpdateRequest, fireSettingsBeforeUpdateRequest] =
|
||||||
|
createEvent<SettingsBeforeUpdateRequestEvent>(`OnSettingsBeforeUpdateRequest`)
|
||||||
|
|
||||||
|
// OnSettingsAfterUpdateRequest hook is triggered after each
|
||||||
|
// successful API Settings update request.
|
||||||
|
export type SettingsAfterUpdateRequestEvent = {}
|
||||||
|
const [onSettingsAfterUpdateRequest, fireSettingsAfterUpdateRequest] =
|
||||||
|
createEvent<SettingsAfterUpdateRequestEvent>(`OnSettingsAfterUpdateRequest`)
|
||||||
|
|
||||||
|
// ---------------------------------------------------------------
|
||||||
|
// File API event hooks
|
||||||
|
// ---------------------------------------------------------------
|
||||||
|
|
||||||
|
// OnFileDownloadRequest hook is triggered before each API File download request.
|
||||||
|
//
|
||||||
|
// Could be used to validate or modify the file response before
|
||||||
|
// returning it to the client.
|
||||||
|
export type FileDownloadRequestEvent = {}
|
||||||
|
const [onFileDownloadRequest, fireFileDownloadRequest] =
|
||||||
|
createEvent<FileDownloadRequestEvent>(`OnFileDownloadRequest`)
|
||||||
|
|
||||||
|
// ---------------------------------------------------------------
|
||||||
|
// Admin API event hooks
|
||||||
|
// ---------------------------------------------------------------
|
||||||
|
|
||||||
|
// OnAdminsListRequest hook is triggered on each API Admins list request.
|
||||||
|
//
|
||||||
|
// Could be used to validate or modify the response before returning it to the client.
|
||||||
|
export type AdminsListRequestEvent = {}
|
||||||
|
const [onAdminsListRequest, fireAdminsListRequest] =
|
||||||
|
createEvent<AdminsListRequestEvent>(`OnAdminsListRequest`)
|
||||||
|
|
||||||
|
// OnAdminViewRequest hook is triggered on each API Admin view request.
|
||||||
|
//
|
||||||
|
// Could be used to validate or modify the response before returning it to the client.
|
||||||
|
export type AdminViewRequestEvent = {}
|
||||||
|
const [onAdminViewRequest, fireAdminViewRequest] =
|
||||||
|
createEvent<AdminViewRequestEvent>(`OnAdminViewRequest`)
|
||||||
|
|
||||||
|
// OnAdminBeforeCreateRequest hook is triggered before each API
|
||||||
|
// Admin create request (after request data load and before model persistence).
|
||||||
|
//
|
||||||
|
// Could be used to additionally validate the request data or implement
|
||||||
|
// completely different persistence behavior (returning [hook.StopPropagation]).
|
||||||
|
export type AdminBeforeCreateRequestEvent = {}
|
||||||
|
const [onAdminBeforeCreateRequest, fireAdminBeforeCreateRequest] =
|
||||||
|
createEvent<AdminBeforeCreateRequestEvent>(`OnAdminBeforeCreateRequest`)
|
||||||
|
|
||||||
|
// OnAdminAfterCreateRequest hook is triggered after each
|
||||||
|
// successful API Admin create request.
|
||||||
|
export type AdminAfterCreateRequestEvent = {}
|
||||||
|
const [onAdminAfterCreateRequest, fireAdminAfterCreateRequest] =
|
||||||
|
createEvent<AdminAfterCreateRequestEvent>(`OnAdminAfterCreateRequest`)
|
||||||
|
|
||||||
|
// OnAdminBeforeUpdateRequest hook is triggered before each API
|
||||||
|
// Admin update request (after request data load and before model persistence).
|
||||||
|
//
|
||||||
|
// Could be used to additionally validate the request data or implement
|
||||||
|
// completely different persistence behavior (returning [hook.StopPropagation]).
|
||||||
|
export type AdminBeforeUpdateRequestEvent = {}
|
||||||
|
const [onAdminBeforeUpdateRequest, fireAdminBeforeUpdateRequest] =
|
||||||
|
createEvent<AdminBeforeUpdateRequestEvent>(`OnAdminBeforeUpdateRequest`)
|
||||||
|
|
||||||
|
// OnAdminAfterUpdateRequest hook is triggered after each
|
||||||
|
// successful API Admin update request.
|
||||||
|
export type AdminAfterUpdateRequestEvent = {}
|
||||||
|
const [onAdminAfterUpdateRequest, fireAdminAfterUpdateRequest] =
|
||||||
|
createEvent<AdminAfterUpdateRequestEvent>(`OnAdminAfterUpdateRequest`)
|
||||||
|
|
||||||
|
// OnAdminBeforeDeleteRequest hook is triggered before each API
|
||||||
|
// Admin delete request (after model load and before actual deletion).
|
||||||
|
//
|
||||||
|
// Could be used to additionally validate the request data or implement
|
||||||
|
// completely different delete behavior (returning [hook.StopPropagation]).
|
||||||
|
export type AdminBeforeDeleteRequestEvent = {}
|
||||||
|
const [onAdminBeforeDeleteRequest, fireAdminBeforeDeleteRequest] =
|
||||||
|
createEvent<AdminBeforeDeleteRequestEvent>(`OnAdminBeforeDeleteRequest`)
|
||||||
|
|
||||||
|
// OnAdminAfterDeleteRequest hook is triggered after each
|
||||||
|
// successful API Admin delete request.
|
||||||
|
export type AdminAfterDeleteRequestEvent = {}
|
||||||
|
const [onAdminAfterDeleteRequest, fireAdminAfterDeleteRequest] =
|
||||||
|
createEvent<AdminAfterDeleteRequestEvent>(`OnAdminAfterDeleteRequest`)
|
||||||
|
|
||||||
|
// OnAdminAuthRequest hook is triggered on each successful API Admin
|
||||||
|
// authentication request (sign-in, token refresh, etc.).
|
||||||
|
//
|
||||||
|
// Could be used to additionally validate or modify the
|
||||||
|
// authenticated admin data and token.
|
||||||
|
export type AdminAuthRequestEvent = {}
|
||||||
|
const [onAdminAuthRequest, fireAdminAuthRequest] =
|
||||||
|
createEvent<AdminAuthRequestEvent>(`OnAdminAuthRequest`)
|
||||||
|
|
||||||
|
// ---------------------------------------------------------------
|
||||||
|
// User API event hooks
|
||||||
|
// ---------------------------------------------------------------
|
||||||
|
|
||||||
|
// OnUsersListRequest hook is triggered on each API Users list request.
|
||||||
|
//
|
||||||
|
// Could be used to validate or modify the response before returning it to the client.
|
||||||
|
export type UsersListRequestEvent = {}
|
||||||
|
const [onUsersListRequest, fireUsersListRequest] =
|
||||||
|
createEvent<UsersListRequestEvent>(`OnUsersListRequest`)
|
||||||
|
|
||||||
|
// OnUserViewRequest hook is triggered on each API User view request.
|
||||||
|
//
|
||||||
|
// Could be used to validate or modify the response before returning it to the client.
|
||||||
|
export type UserViewRequestEvent = {}
|
||||||
|
const [onUserViewRequest, fireUserViewRequest] =
|
||||||
|
createEvent<UserViewRequestEvent>(`OnUserViewRequest`)
|
||||||
|
|
||||||
|
// OnUserBeforeCreateRequest hook is triggered before each API User
|
||||||
|
// create request (after request data load and before model persistence).
|
||||||
|
//
|
||||||
|
// Could be used to additionally validate the request data or implement
|
||||||
|
// completely different persistence behavior (returning [hook.StopPropagation]).
|
||||||
|
export type UserBeforeCreateRequestEvent = {}
|
||||||
|
const [onUserBeforeCreateRequest, fireUserBeforeCreateRequest] =
|
||||||
|
createEvent<UserBeforeCreateRequestEvent>(`OnUserBeforeCreateRequest`)
|
||||||
|
|
||||||
|
// OnUserAfterCreateRequest hook is triggered after each
|
||||||
|
// successful API User create request.
|
||||||
|
export type UserAfterCreateRequestEvent = {}
|
||||||
|
const [onUserAfterCreateRequest, fireUserAfterCreateRequest] =
|
||||||
|
createEvent<UserAfterCreateRequestEvent>(`OnUserAfterCreateRequest`)
|
||||||
|
|
||||||
|
// OnUserBeforeUpdateRequest hook is triggered before each API User
|
||||||
|
// update request (after request data load and before model persistence).
|
||||||
|
//
|
||||||
|
// Could be used to additionally validate the request data or implement
|
||||||
|
// completely different persistence behavior (returning [hook.StopPropagation]).
|
||||||
|
export type UserBeforeUpdateRequestEvent = {}
|
||||||
|
const [onUserBeforeUpdateRequest, fireUserBeforeUpdateRequest] =
|
||||||
|
createEvent<UserBeforeUpdateRequestEvent>(`OnUserBeforeUpdateRequest`)
|
||||||
|
|
||||||
|
// OnUserAfterUpdateRequest hook is triggered after each
|
||||||
|
// successful API User update request.
|
||||||
|
export type UserAfterUpdateRequestEvent = {}
|
||||||
|
const [onUserAfterUpdateRequest, fireUserAfterUpdateRequest] =
|
||||||
|
createEvent<UserAfterUpdateRequestEvent>(`OnUserAfterUpdateRequest`)
|
||||||
|
|
||||||
|
// OnUserBeforeDeleteRequest hook is triggered before each API User
|
||||||
|
// delete request (after model load and before actual deletion).
|
||||||
|
//
|
||||||
|
// Could be used to additionally validate the request data or implement
|
||||||
|
// completely different delete behavior (returning [hook.StopPropagation]).
|
||||||
|
export type UserBeforeDeleteRequestEvent = {}
|
||||||
|
const [onUserBeforeDeleteRequest, fireUserBeforeDeleteRequest] =
|
||||||
|
createEvent<UserBeforeDeleteRequestEvent>(`OnUserBeforeDeleteRequest`)
|
||||||
|
|
||||||
|
// OnUserAfterDeleteRequest hook is triggered after each
|
||||||
|
// successful API User delete request.
|
||||||
|
export type UserAfterDeleteRequestEvent = {}
|
||||||
|
const [onUserAfterDeleteRequest, fireUserAfterDeleteRequest] =
|
||||||
|
createEvent<UserAfterDeleteRequestEvent>(`OnUserAfterDeleteRequest`)
|
||||||
|
|
||||||
|
// OnUserAuthRequest hook is triggered on each successful API User
|
||||||
|
// authentication request (sign-in, token refresh, etc.).
|
||||||
|
//
|
||||||
|
// Could be used to additionally validate or modify the
|
||||||
|
// authenticated user data and token.
|
||||||
|
export type UserAuthRequestEvent = {}
|
||||||
|
const [onUserAuthRequest, fireUserAuthRequest] =
|
||||||
|
createEvent<UserAuthRequestEvent>(`OnUserAuthRequest`)
|
||||||
|
|
||||||
|
// OnUserListExternalAuths hook is triggered on each API user's external auths list request.
|
||||||
|
//
|
||||||
|
// Could be used to validate or modify the response before returning it to the client.
|
||||||
|
export type UserListExternalAuthsEvent = {}
|
||||||
|
const [onUserListExternalAuths, fireUserListExternalAuths] =
|
||||||
|
createEvent<UserListExternalAuthsEvent>(`OnUserListExternalAuths`)
|
||||||
|
|
||||||
|
// OnUserBeforeUnlinkExternalAuthRequest hook is triggered before each API user's
|
||||||
|
// external auth unlink request (after models load and before the actual relation deletion).
|
||||||
|
//
|
||||||
|
// Could be used to additionally validate the request data or implement
|
||||||
|
// completely different delete behavior (returning [hook.StopPropagation]).
|
||||||
|
export type UserBeforeUnlinkExternalAuthRequestEvent = {}
|
||||||
|
const [
|
||||||
|
onUserBeforeUnlinkExternalAuthRequest,
|
||||||
|
fireUserBeforeUnlinkExternalAuthRequest,
|
||||||
|
] = createEvent<UserBeforeUnlinkExternalAuthRequestEvent>(
|
||||||
|
`OnUserBeforeUnlinkExternalAuthRequest`
|
||||||
|
)
|
||||||
|
|
||||||
|
// OnUserAfterUnlinkExternalAuthRequest hook is triggered after each
|
||||||
|
// successful API user's external auth unlink request.
|
||||||
|
export type UserAfterUnlinkExternalAuthRequestEvent = {}
|
||||||
|
const [
|
||||||
|
onUserAfterUnlinkExternalAuthRequest,
|
||||||
|
fireUserAfterUnlinkExternalAuthRequest,
|
||||||
|
] = createEvent<UserAfterUnlinkExternalAuthRequestEvent>(
|
||||||
|
`OnUserAfterUnlinkExternalAuthRequest`
|
||||||
|
)
|
||||||
|
|
||||||
|
// ---------------------------------------------------------------
|
||||||
|
// Record API event hooks
|
||||||
|
// ---------------------------------------------------------------
|
||||||
|
|
||||||
|
// OnRecordsListRequest hook is triggered on each API Records list request.
|
||||||
|
//
|
||||||
|
// Could be used to validate or modify the response before returning it to the client.
|
||||||
|
export type RecordsListRequestEvent = {}
|
||||||
|
const [onRecordsListRequest, fireRecordsListRequest] =
|
||||||
|
createEvent<RecordsListRequestEvent>(`OnRecordsListRequest`)
|
||||||
|
|
||||||
|
// OnRecordViewRequest hook is triggered on each API Record view request.
|
||||||
|
//
|
||||||
|
// Could be used to validate or modify the response before returning it to the client.
|
||||||
|
export type RecordViewRequestEvent = {}
|
||||||
|
const [onRecordViewRequest, fireRecordViewRequest] =
|
||||||
|
createEvent<RecordViewRequestEvent>(`OnRecordViewRequest`)
|
||||||
|
|
||||||
|
// OnRecordBeforeCreateRequest hook is triggered before each API Record
|
||||||
|
// create request (after request data load and before model persistence).
|
||||||
|
//
|
||||||
|
// Could be used to additionally validate the request data or implement
|
||||||
|
// completely different persistence behavior (returning [hook.StopPropagation]).
|
||||||
|
export type RecordBeforeCreateRequestEvent = {}
|
||||||
|
const [onRecordBeforeCreateRequest, fireRecordBeforeCreateRequest] =
|
||||||
|
createEvent<RecordBeforeCreateRequestEvent>(`OnRecordBeforeCreateRequest`)
|
||||||
|
|
||||||
|
// OnRecordAfterCreateRequest hook is triggered after each
|
||||||
|
// successful API Record create request.
|
||||||
|
export type RecordAfterCreateRequestEvent = {}
|
||||||
|
const [onRecordAfterCreateRequest, fireRecordAfterCreateRequest] =
|
||||||
|
createEvent<RecordAfterCreateRequestEvent>(`OnRecordAfterCreateRequest`)
|
||||||
|
|
||||||
|
// OnRecordBeforeUpdateRequest hook is triggered before each API Record
|
||||||
|
// update request (after request data load and before model persistence).
|
||||||
|
//
|
||||||
|
// Could be used to additionally validate the request data or implement
|
||||||
|
// completely different persistence behavior (returning [hook.StopPropagation]).
|
||||||
|
export type RecordBeforeUpdateRequestEvent = {}
|
||||||
|
const [onRecordBeforeUpdateRequest, fireRecordBeforeUpdateRequest] =
|
||||||
|
createEvent<RecordBeforeUpdateRequestEvent>(`OnRecordBeforeUpdateRequest`)
|
||||||
|
|
||||||
|
// OnRecordAfterUpdateRequest hook is triggered after each
|
||||||
|
// successful API Record update request.
|
||||||
|
export type RecordAfterUpdateRequestEvent = {}
|
||||||
|
const [onRecordAfterUpdateRequest, fireRecordAfterUpdateRequest] =
|
||||||
|
createEvent<RecordAfterUpdateRequestEvent>(`OnRecordAfterUpdateRequest`)
|
||||||
|
|
||||||
|
// OnRecordBeforeDeleteRequest hook is triggered before each API Record
|
||||||
|
// delete request (after model load and before actual deletion).
|
||||||
|
//
|
||||||
|
// Could be used to additionally validate the request data or implement
|
||||||
|
// completely different delete behavior (returning [hook.StopPropagation]).
|
||||||
|
export type RecordBeforeDeleteRequestEvent = {}
|
||||||
|
const [onRecordBeforeDeleteRequest, fireRecordBeforeDeleteRequest] =
|
||||||
|
createEvent<RecordBeforeDeleteRequestEvent>(`OnRecordBeforeDeleteRequest`)
|
||||||
|
|
||||||
|
// OnRecordAfterDeleteRequest hook is triggered after each
|
||||||
|
// successful API Record delete request.
|
||||||
|
export type RecordAfterDeleteRequestEvent = {}
|
||||||
|
const [onRecordAfterDeleteRequest, fireRecordAfterDeleteRequest] =
|
||||||
|
createEvent<RecordAfterDeleteRequestEvent>(`OnRecordAfterDeleteRequest`)
|
||||||
|
|
||||||
|
// ---------------------------------------------------------------
|
||||||
|
// Collection API event hooks
|
||||||
|
// ---------------------------------------------------------------
|
||||||
|
|
||||||
|
// OnCollectionsListRequest hook is triggered on each API Collections list request.
|
||||||
|
//
|
||||||
|
// Could be used to validate or modify the response before returning it to the client.
|
||||||
|
export type CollectionsListRequestEvent = {}
|
||||||
|
const [onCollectionsListRequest, fireCollectionsListRequest] =
|
||||||
|
createEvent<CollectionsListRequestEvent>(`OnCollectionsListRequest`)
|
||||||
|
|
||||||
|
// OnCollectionViewRequest hook is triggered on each API Collection view request.
|
||||||
|
//
|
||||||
|
// Could be used to validate or modify the response before returning it to the client.
|
||||||
|
export type CollectionViewRequestEvent = {}
|
||||||
|
const [onCollectionViewRequest, fireCollectionViewRequest] =
|
||||||
|
createEvent<CollectionViewRequestEvent>(`OnCollectionViewRequest`)
|
||||||
|
|
||||||
|
// OnCollectionBeforeCreateRequest hook is triggered before each API Collection
|
||||||
|
// create request (after request data load and before model persistence).
|
||||||
|
//
|
||||||
|
// Could be used to additionally validate the request data or implement
|
||||||
|
// completely different persistence behavior (returning [hook.StopPropagation]).
|
||||||
|
export type CollectionBeforeCreateRequestEvent = {}
|
||||||
|
const [onCollectionBeforeCreateRequest, fireCollectionBeforeCreateRequest] =
|
||||||
|
createEvent<CollectionBeforeCreateRequestEvent>(
|
||||||
|
`OnCollectionBeforeCreateRequest`
|
||||||
|
)
|
||||||
|
|
||||||
|
// OnCollectionAfterCreateRequest hook is triggered after each
|
||||||
|
// successful API Collection create request.
|
||||||
|
export type CollectionAfterCreateRequestEvent = {}
|
||||||
|
const [onCollectionAfterCreateRequest, fireCollectionAfterCreateRequest] =
|
||||||
|
createEvent<CollectionAfterCreateRequestEvent>(
|
||||||
|
`OnCollectionAfterCreateRequest`
|
||||||
|
)
|
||||||
|
|
||||||
|
// OnCollectionBeforeUpdateRequest hook is triggered before each API Collection
|
||||||
|
// update request (after request data load and before model persistence).
|
||||||
|
//
|
||||||
|
// Could be used to additionally validate the request data or implement
|
||||||
|
// completely different persistence behavior (returning [hook.StopPropagation]).
|
||||||
|
export type CollectionBeforeUpdateRequestEvent = {}
|
||||||
|
const [onCollectionBeforeUpdateRequest, fireCollectionBeforeUpdateRequest] =
|
||||||
|
createEvent<CollectionBeforeUpdateRequestEvent>(
|
||||||
|
`OnCollectionBeforeUpdateRequest`
|
||||||
|
)
|
||||||
|
|
||||||
|
// OnCollectionAfterUpdateRequest hook is triggered after each
|
||||||
|
// successful API Collection update request.
|
||||||
|
export type CollectionAfterUpdateRequestEvent = {}
|
||||||
|
const [onCollectionAfterUpdateRequest, fireCollectionAfterUpdateRequest] =
|
||||||
|
createEvent<CollectionAfterUpdateRequestEvent>(
|
||||||
|
`OnCollectionAfterUpdateRequest`
|
||||||
|
)
|
||||||
|
|
||||||
|
// OnCollectionBeforeDeleteRequest hook is triggered before each API
|
||||||
|
// Collection delete request (after model load and before actual deletion).
|
||||||
|
//
|
||||||
|
// Could be used to additionally validate the request data or implement
|
||||||
|
// completely different delete behavior (returning [hook.StopPropagation]).
|
||||||
|
export type CollectionBeforeDeleteRequestEvent = {}
|
||||||
|
const [onCollectionBeforeDeleteRequest, fireCollectionBeforeDeleteRequest] =
|
||||||
|
createEvent<CollectionBeforeDeleteRequestEvent>(
|
||||||
|
`OnCollectionBeforeDeleteRequest`
|
||||||
|
)
|
||||||
|
|
||||||
|
// OnCollectionAfterDeleteRequest hook is triggered after each
|
||||||
|
// successful API Collection delete request.
|
||||||
|
export type CollectionAfterDeleteRequestEvent = {}
|
||||||
|
const [onCollectionAfterDeleteRequest, fireCollectionAfterDeleteRequest] =
|
||||||
|
createEvent<CollectionAfterDeleteRequestEvent>(
|
||||||
|
`OnCollectionAfterDeleteRequest`
|
||||||
|
)
|
||||||
|
|
||||||
|
// OnCollectionsBeforeImportRequest hook is triggered before each API
|
||||||
|
// collections import request (after request data load and before the actual import).
|
||||||
|
//
|
||||||
|
// Could be used to additionally validate the imported collections or
|
||||||
|
// to implement completely different import behavior (returning [hook.StopPropagation]).
|
||||||
|
export type CollectionsBeforeImportRequestEvent = {}
|
||||||
|
const [onCollectionsBeforeImportRequest, fireCollectionsBeforeImportRequest] =
|
||||||
|
createEvent<CollectionsBeforeImportRequestEvent>(
|
||||||
|
`OnCollectionsBeforeImportRequest`
|
||||||
|
)
|
||||||
|
|
||||||
|
// OnCollectionsAfterImportRequest hook is triggered after each
|
||||||
|
// successful API collections import request.
|
||||||
|
export type CollectionsAfterImportRequestEvent = {}
|
||||||
|
const [onCollectionsAfterImportRequest, fireCollectionsAfterImportRequest] =
|
||||||
|
createEvent<CollectionsAfterImportRequestEvent>(
|
||||||
|
`OnCollectionsAfterImportRequest`
|
||||||
|
)
|
||||||
|
|
||||||
|
export type BeforeServeEvent = {}
|
||||||
|
const [onBeforeServe, fireBeforeServe] =
|
||||||
|
createEvent<BeforeServeEvent>(`OnBeforeServe`)
|
11
packages/js-cloud-funcs/src/index.ts
Normal file
11
packages/js-cloud-funcs/src/index.ts
Normal file
@ -0,0 +1,11 @@
|
|||||||
|
export * from './routes'
|
||||||
|
export * from './transaction'
|
||||||
|
export * from './types'
|
||||||
|
|
||||||
|
import { PBScriptApi } from './types/PBScriptApi'
|
||||||
|
|
||||||
|
const api: PBScriptApi = {
|
||||||
|
ping: () => 'Hello from PBScript!',
|
||||||
|
}
|
||||||
|
|
||||||
|
registerJsFuncs(api)
|
71
packages/js-cloud-funcs/src/routes.ts
Normal file
71
packages/js-cloud-funcs/src/routes.ts
Normal file
@ -0,0 +1,71 @@
|
|||||||
|
import {
|
||||||
|
EchoHttpMethods,
|
||||||
|
EchoHttpResponseStatuses,
|
||||||
|
EchoMiddlewareFunc,
|
||||||
|
EchoRoute,
|
||||||
|
User,
|
||||||
|
} from './types'
|
||||||
|
|
||||||
|
export type HandlerApi = {
|
||||||
|
ok: (s: string) => void
|
||||||
|
error: (s: string) => void
|
||||||
|
user: () => User
|
||||||
|
}
|
||||||
|
|
||||||
|
export type HandlerFunc = (api: HandlerApi) => void
|
||||||
|
|
||||||
|
export type Route = {
|
||||||
|
method: EchoHttpMethods
|
||||||
|
path: string
|
||||||
|
handler: HandlerFunc
|
||||||
|
middlewares: EchoMiddlewareFunc[]
|
||||||
|
}
|
||||||
|
|
||||||
|
export const addRoute = (route: Route) => {
|
||||||
|
// Ensure path begins with `/api/`
|
||||||
|
const { path } = route
|
||||||
|
if (!path.startsWith(`/api/`)) {
|
||||||
|
console.log(`API error ${path}`)
|
||||||
|
throw new Error(`All routes must take the form /api/<path...>`)
|
||||||
|
}
|
||||||
|
|
||||||
|
const _route: EchoRoute = {
|
||||||
|
method: route.method,
|
||||||
|
path: route.path,
|
||||||
|
handler: (c) => {
|
||||||
|
const api: HandlerApi = {
|
||||||
|
ok: (message) => {
|
||||||
|
c.string(EchoHttpResponseStatuses.Ok, message)
|
||||||
|
},
|
||||||
|
error: (message) => {
|
||||||
|
c.string(EchoHttpResponseStatuses.Error, message)
|
||||||
|
},
|
||||||
|
user: () => c.get<User>('user'),
|
||||||
|
}
|
||||||
|
route.handler(api)
|
||||||
|
},
|
||||||
|
middlewares: route.middlewares,
|
||||||
|
}
|
||||||
|
return __go.addRoute(_route)
|
||||||
|
}
|
||||||
|
|
||||||
|
export const auth = __go.requireAdminOrUserAuth()
|
||||||
|
|
||||||
|
export const get = (
|
||||||
|
path: string,
|
||||||
|
handler: HandlerFunc,
|
||||||
|
middlewares?: EchoMiddlewareFunc[]
|
||||||
|
) => {
|
||||||
|
addRoute({
|
||||||
|
method: EchoHttpMethods.Get,
|
||||||
|
path: `/api${path}`,
|
||||||
|
handler: (api) => {
|
||||||
|
try {
|
||||||
|
handler(api)
|
||||||
|
} catch (e) {
|
||||||
|
api.error(`${e}`)
|
||||||
|
}
|
||||||
|
},
|
||||||
|
middlewares: middlewares || [],
|
||||||
|
})
|
||||||
|
}
|
36
packages/js-cloud-funcs/src/transaction.ts
Normal file
36
packages/js-cloud-funcs/src/transaction.ts
Normal file
@ -0,0 +1,36 @@
|
|||||||
|
export type TransactionApi = {
|
||||||
|
execute: (sql: string) => void
|
||||||
|
all: <TRow>(sql: string) => TRow[]
|
||||||
|
one: <TRow>(sql: string) => TRow
|
||||||
|
}
|
||||||
|
|
||||||
|
export type TransactionCallback = (api: TransactionApi) => void
|
||||||
|
|
||||||
|
export const runInTransaction = (cb: TransactionCallback) => {
|
||||||
|
__go.app.dao().runInTransaction((txDao) => {
|
||||||
|
const execute = (sql: string) => {
|
||||||
|
const q = txDao.dB().newQuery(sql)
|
||||||
|
return q.execute()
|
||||||
|
}
|
||||||
|
const all = <TRow>(sql: string): TRow[] => {
|
||||||
|
const q = txDao.dB().newQuery(sql)
|
||||||
|
const rowPtr = __go.newNullStringMapArrayPtr<TRow>()
|
||||||
|
q.all(rowPtr)
|
||||||
|
console.log({ rowPtr })
|
||||||
|
return []
|
||||||
|
}
|
||||||
|
const one = <TRow>(sql: string): TRow => {
|
||||||
|
const q = txDao.dB().newQuery(sql)
|
||||||
|
const row = __go.newNullStringMap<TRow>()
|
||||||
|
q.one(row)
|
||||||
|
return row
|
||||||
|
}
|
||||||
|
|
||||||
|
const api: TransactionApi = {
|
||||||
|
execute,
|
||||||
|
all,
|
||||||
|
one,
|
||||||
|
}
|
||||||
|
cb(api)
|
||||||
|
})
|
||||||
|
}
|
3
packages/js-cloud-funcs/src/types/ConsoleApi.ts
Normal file
3
packages/js-cloud-funcs/src/types/ConsoleApi.ts
Normal file
@ -0,0 +1,3 @@
|
|||||||
|
export type ConsoleApi = {
|
||||||
|
log: (...args: any) => void
|
||||||
|
}
|
17
packages/js-cloud-funcs/src/types/GoApi.ts
Normal file
17
packages/js-cloud-funcs/src/types/GoApi.ts
Normal file
@ -0,0 +1,17 @@
|
|||||||
|
import { Dao } from './go-namespaces/Dao'
|
||||||
|
import { EchoMiddlewareFunc, EchoRoute } from './go-namespaces/Echo'
|
||||||
|
|
||||||
|
export type GoApi = {
|
||||||
|
app: {
|
||||||
|
dao: () => Dao
|
||||||
|
}
|
||||||
|
addRoute: (route: EchoRoute) => void
|
||||||
|
ping: () => string
|
||||||
|
onModelBeforeCreate: any
|
||||||
|
requireAdminAuth: () => EchoMiddlewareFunc
|
||||||
|
requireAdminAuthOnlyIfAny: () => EchoMiddlewareFunc
|
||||||
|
requireAdminOrOwnerAuth: () => EchoMiddlewareFunc
|
||||||
|
requireAdminOrUserAuth: () => EchoMiddlewareFunc
|
||||||
|
newNullStringMapArrayPtr: <TFields>() => TFields[]
|
||||||
|
newNullStringMap: <TFields>() => TFields
|
||||||
|
}
|
4
packages/js-cloud-funcs/src/types/PBScriptApi.ts
Normal file
4
packages/js-cloud-funcs/src/types/PBScriptApi.ts
Normal file
@ -0,0 +1,4 @@
|
|||||||
|
export type PingResult = string
|
||||||
|
export type PBScriptApi = {
|
||||||
|
ping: () => PingResult
|
||||||
|
}
|
9
packages/js-cloud-funcs/src/types/__global.ts
Normal file
9
packages/js-cloud-funcs/src/types/__global.ts
Normal file
@ -0,0 +1,9 @@
|
|||||||
|
import { ConsoleApi } from './ConsoleApi'
|
||||||
|
import { GoApi } from './GoApi'
|
||||||
|
import { PBScriptApi } from './PBScriptApi'
|
||||||
|
|
||||||
|
declare global {
|
||||||
|
let console: ConsoleApi
|
||||||
|
function registerJsFuncs(api: PBScriptApi): void
|
||||||
|
let __go: GoApi
|
||||||
|
}
|
47
packages/js-cloud-funcs/src/types/database/User.ts
Normal file
47
packages/js-cloud-funcs/src/types/database/User.ts
Normal file
@ -0,0 +1,47 @@
|
|||||||
|
import { IsoDate, RecordId, Url } from '.'
|
||||||
|
|
||||||
|
export type ProfileId = RecordId
|
||||||
|
export type Email = string
|
||||||
|
export type UserId = RecordId
|
||||||
|
export type Token = string
|
||||||
|
export type PasswordHash = string
|
||||||
|
|
||||||
|
export type User = {
|
||||||
|
baseAccount: {
|
||||||
|
baseModel: {
|
||||||
|
id: UserId
|
||||||
|
created: IsoDate
|
||||||
|
updated: IsoDate
|
||||||
|
}
|
||||||
|
id: UserId
|
||||||
|
created: IsoDate
|
||||||
|
updated: IsoDate
|
||||||
|
email: Email
|
||||||
|
tokenKey: Token
|
||||||
|
passwordHash: PasswordHash
|
||||||
|
lastResetSentAt: IsoDate
|
||||||
|
}
|
||||||
|
baseModel: {
|
||||||
|
id: UserId
|
||||||
|
created: IsoDate
|
||||||
|
updated: IsoDate
|
||||||
|
}
|
||||||
|
id: UserId
|
||||||
|
created: IsoDate
|
||||||
|
updated: IsoDate
|
||||||
|
email: Email
|
||||||
|
tokenKey: Token
|
||||||
|
passwordHash: PasswordHash
|
||||||
|
lastResetSentAt: IsoDate
|
||||||
|
verified: boolean
|
||||||
|
lastVerificationSentAt: IsoDate
|
||||||
|
profile: {
|
||||||
|
avatar: Url
|
||||||
|
created: IsoDate
|
||||||
|
|
||||||
|
id: ProfileId
|
||||||
|
name: string
|
||||||
|
updated: IsoDate
|
||||||
|
userId: UserId
|
||||||
|
}
|
||||||
|
}
|
5
packages/js-cloud-funcs/src/types/database/index.ts
Normal file
5
packages/js-cloud-funcs/src/types/database/index.ts
Normal file
@ -0,0 +1,5 @@
|
|||||||
|
export type RecordId = string
|
||||||
|
export type IsoDate = string
|
||||||
|
export type Url = string
|
||||||
|
|
||||||
|
export * from './User'
|
9
packages/js-cloud-funcs/src/types/go-namespaces/Dao.ts
Normal file
9
packages/js-cloud-funcs/src/types/go-namespaces/Dao.ts
Normal file
@ -0,0 +1,9 @@
|
|||||||
|
import { DbxBuilder } from './Dbx'
|
||||||
|
|
||||||
|
export type DaoTransactionCallback = (txDao: Dao) => void
|
||||||
|
|
||||||
|
export type Dao = {
|
||||||
|
// https://pkg.go.dev/github.com/pocketbase/pocketbase@v0.7.9/daos
|
||||||
|
runInTransaction: (cb: DaoTransactionCallback) => void
|
||||||
|
dB: () => DbxBuilder
|
||||||
|
}
|
14
packages/js-cloud-funcs/src/types/go-namespaces/Dbx.ts
Normal file
14
packages/js-cloud-funcs/src/types/go-namespaces/Dbx.ts
Normal file
@ -0,0 +1,14 @@
|
|||||||
|
import { SqlResult } from './Sql'
|
||||||
|
|
||||||
|
export type DbxQuery = {
|
||||||
|
// https://pkg.go.dev/github.com/pocketbase/dbx#Query
|
||||||
|
// https://pkg.go.dev/github.com/pocketbase/dbx#Query.Execute
|
||||||
|
execute: () => SqlResult
|
||||||
|
all: <TRow>(rows: TRow[]) => void
|
||||||
|
one: <TRow>(row: TRow) => void
|
||||||
|
}
|
||||||
|
|
||||||
|
export type DbxBuilder = {
|
||||||
|
// https://pkg.go.dev/github.com/pocketbase/dbx#Builder
|
||||||
|
newQuery: (sql: string) => DbxQuery
|
||||||
|
}
|
25
packages/js-cloud-funcs/src/types/go-namespaces/Echo.ts
Normal file
25
packages/js-cloud-funcs/src/types/go-namespaces/Echo.ts
Normal file
@ -0,0 +1,25 @@
|
|||||||
|
export enum EchoHttpMethods {
|
||||||
|
Get = 'GET',
|
||||||
|
}
|
||||||
|
|
||||||
|
export enum EchoHttpResponseStatuses {
|
||||||
|
Ok = 200,
|
||||||
|
Error = 400,
|
||||||
|
}
|
||||||
|
|
||||||
|
export type EchoHandlerFunc = (context: EchoContext) => void
|
||||||
|
|
||||||
|
export type EchoMiddlewareFunc = (next: EchoHandlerFunc) => EchoHandlerFunc
|
||||||
|
|
||||||
|
export type EchoContext = {
|
||||||
|
get: <T>(key: string) => T
|
||||||
|
string: (code: number, s: string) => void
|
||||||
|
json: (status: EchoHttpResponseStatuses, data: object) => void
|
||||||
|
}
|
||||||
|
|
||||||
|
export type EchoRoute = {
|
||||||
|
method: EchoHttpMethods
|
||||||
|
path: string
|
||||||
|
handler: EchoHandlerFunc
|
||||||
|
middlewares: EchoMiddlewareFunc[]
|
||||||
|
}
|
10
packages/js-cloud-funcs/src/types/go-namespaces/Sql.ts
Normal file
10
packages/js-cloud-funcs/src/types/go-namespaces/Sql.ts
Normal file
@ -0,0 +1,10 @@
|
|||||||
|
export type SqlResult = {
|
||||||
|
// https://pkg.go.dev/database/sql#Result
|
||||||
|
// statements varies.
|
||||||
|
lastInsertId: () => number
|
||||||
|
|
||||||
|
// RowsAffected returns the number of rows affected by an
|
||||||
|
// update, insert, or delete. Not every database or database
|
||||||
|
// driver may support this.
|
||||||
|
rowsAffected: () => number
|
||||||
|
}
|
4
packages/js-cloud-funcs/src/types/go-namespaces/index.ts
Normal file
4
packages/js-cloud-funcs/src/types/go-namespaces/index.ts
Normal file
@ -0,0 +1,4 @@
|
|||||||
|
export * from './Dao'
|
||||||
|
export * from './Dbx'
|
||||||
|
export * from './Echo'
|
||||||
|
export * from './Sql'
|
5
packages/js-cloud-funcs/src/types/index.ts
Normal file
5
packages/js-cloud-funcs/src/types/index.ts
Normal file
@ -0,0 +1,5 @@
|
|||||||
|
export * from './database'
|
||||||
|
export * from './go-namespaces'
|
||||||
|
export * from './GoApi'
|
||||||
|
export * from './PBScriptApi'
|
||||||
|
export * from './__global'
|
8
packages/js-cloud-funcs/src/util/assert.ts
Normal file
8
packages/js-cloud-funcs/src/util/assert.ts
Normal file
@ -0,0 +1,8 @@
|
|||||||
|
export function assertExists<TType>(
|
||||||
|
v: TType,
|
||||||
|
message = `Value does not exist`
|
||||||
|
): asserts v is NonNullable<TType> {
|
||||||
|
if (typeof v === 'undefined') {
|
||||||
|
throw new Error(message)
|
||||||
|
}
|
||||||
|
}
|
34
packages/js-cloud-funcs/src/util/event.ts
Normal file
34
packages/js-cloud-funcs/src/util/event.ts
Normal file
@ -0,0 +1,34 @@
|
|||||||
|
import { createNanoEvents } from 'nanoevents'
|
||||||
|
|
||||||
|
const emitter = createNanoEvents()
|
||||||
|
|
||||||
|
export type Event<TPayload extends any = void> = TPayload
|
||||||
|
|
||||||
|
export type EventListenerCallback<TEvent extends Event<any>> = (
|
||||||
|
event: TEvent
|
||||||
|
) => void
|
||||||
|
|
||||||
|
export type EventDispatcher<TEvent extends Event<any> = Event> = (
|
||||||
|
event: TEvent
|
||||||
|
) => void
|
||||||
|
|
||||||
|
export type EventUnsubscriber = () => void
|
||||||
|
|
||||||
|
export type EventSubscriber<TEvent extends Event<any>> = (
|
||||||
|
callback: EventListenerCallback<TEvent>
|
||||||
|
) => EventUnsubscriber
|
||||||
|
|
||||||
|
export const createEvent = <TEvent extends Event<any>>(
|
||||||
|
eventName: string
|
||||||
|
): [EventSubscriber<TEvent>, EventDispatcher<TEvent>] => {
|
||||||
|
const fire: EventDispatcher<TEvent> = (event) => {
|
||||||
|
emitter.emit(eventName, event)
|
||||||
|
}
|
||||||
|
|
||||||
|
const listen: EventSubscriber<TEvent> = (cb) => {
|
||||||
|
const unsub = emitter.on(eventName, cb)
|
||||||
|
return unsub
|
||||||
|
}
|
||||||
|
|
||||||
|
return [listen, fire]
|
||||||
|
}
|
3
packages/js-cloud-funcs/src/util/isFunction.ts
Normal file
3
packages/js-cloud-funcs/src/util/isFunction.ts
Normal file
@ -0,0 +1,3 @@
|
|||||||
|
export function isFunction(value: any) {
|
||||||
|
return typeof value === 'function'
|
||||||
|
}
|
19
packages/js-cloud-funcs/tsconfig.json
Normal file
19
packages/js-cloud-funcs/tsconfig.json
Normal file
@ -0,0 +1,19 @@
|
|||||||
|
{
|
||||||
|
"compilerOptions": {
|
||||||
|
"allowJs": true,
|
||||||
|
"checkJs": true,
|
||||||
|
"esModuleInterop": true,
|
||||||
|
"forceConsistentCasingInFileNames": true,
|
||||||
|
"resolveJsonModule": true,
|
||||||
|
"skipLibCheck": true,
|
||||||
|
"strict": true,
|
||||||
|
"module": "ES2015",
|
||||||
|
"moduleResolution": "node",
|
||||||
|
"noUncheckedIndexedAccess": true,
|
||||||
|
"strictNullChecks": true,
|
||||||
|
"typeRoots": ["./node_modules/@types"],
|
||||||
|
"inlineSourceMap": true,
|
||||||
|
"lib": ["ES2015"]
|
||||||
|
},
|
||||||
|
"include": ["./src"]
|
||||||
|
}
|
376
packages/pocketbase-cloud-funcs/engine/pbscript.go
Normal file
376
packages/pocketbase-cloud-funcs/engine/pbscript.go
Normal file
@ -0,0 +1,376 @@
|
|||||||
|
package engine
|
||||||
|
|
||||||
|
import (
|
||||||
|
"fmt"
|
||||||
|
"net/http"
|
||||||
|
"unsafe"
|
||||||
|
|
||||||
|
"github.com/benallfree/pbscript/modules/pbscript/event"
|
||||||
|
|
||||||
|
"github.com/goccy/go-json"
|
||||||
|
|
||||||
|
"github.com/dop251/goja"
|
||||||
|
"github.com/labstack/echo/v5"
|
||||||
|
"github.com/pocketbase/dbx"
|
||||||
|
"github.com/pocketbase/pocketbase"
|
||||||
|
"github.com/pocketbase/pocketbase/apis"
|
||||||
|
"github.com/pocketbase/pocketbase/core"
|
||||||
|
"github.com/pocketbase/pocketbase/daos"
|
||||||
|
"github.com/pocketbase/pocketbase/models"
|
||||||
|
"github.com/pocketbase/pocketbase/models/schema"
|
||||||
|
)
|
||||||
|
|
||||||
|
var app *pocketbase.PocketBase
|
||||||
|
var router *echo.Echo
|
||||||
|
var vm *goja.Runtime
|
||||||
|
var cleanups = []func(){}
|
||||||
|
var __go_apis *goja.Object
|
||||||
|
|
||||||
|
const (
|
||||||
|
colorReset = "\033[0m"
|
||||||
|
colorRed = "\033[31m"
|
||||||
|
colorGreen = "\033[32m"
|
||||||
|
colorYellow = "\033[33m"
|
||||||
|
colorBlue = "\033[34m"
|
||||||
|
colorPurple = "\033[35m"
|
||||||
|
colorCyan = "\033[36m"
|
||||||
|
colorWhite = "\033[37m"
|
||||||
|
)
|
||||||
|
|
||||||
|
func logErrorf(format string, args ...any) (n int, err error) {
|
||||||
|
|
||||||
|
s := append(args, string(colorReset))
|
||||||
|
fmt.Print(colorRed)
|
||||||
|
res, err := fmt.Printf(format, s...)
|
||||||
|
fmt.Print(colorReset)
|
||||||
|
return res, err
|
||||||
|
}
|
||||||
|
|
||||||
|
func bindApis() {
|
||||||
|
__go_apis = vm.NewObject()
|
||||||
|
__go_apis.Set("addRoute", func(route echo.Route) {
|
||||||
|
method := route.Method
|
||||||
|
path := route.Path
|
||||||
|
fmt.Printf("Adding route: %s %s\n", method, path)
|
||||||
|
|
||||||
|
router.AddRoute(route)
|
||||||
|
cleanup(
|
||||||
|
fmt.Sprintf("route %s %s", method, path),
|
||||||
|
func() {
|
||||||
|
router.Router().Remove(method, path)
|
||||||
|
})
|
||||||
|
})
|
||||||
|
__go_apis.Set("onModelBeforeCreate", func(cb func(e *core.ModelEvent)) {
|
||||||
|
fmt.Println("Listening in Go for onModelBeforeCreate")
|
||||||
|
unsub := event.On(event.EVT_ON_MODEL_BEFORE_CREATE, func(e *event.UnknownPayload) {
|
||||||
|
// fmt.Println("syntheticevent: OnModelBeforeCreate")
|
||||||
|
// fmt.Println("e", e)
|
||||||
|
// fmt.Println("cb", cb)
|
||||||
|
cb((*core.ModelEvent)(unsafe.Pointer(e)))
|
||||||
|
})
|
||||||
|
cleanup("onModelBeforeCreate", unsub)
|
||||||
|
})
|
||||||
|
__go_apis.Set("onModelAfterCreate", func(cb func(e *core.ModelEvent)) {
|
||||||
|
fmt.Println("Listening in Go for onModelAfterCreate")
|
||||||
|
unsub := event.On(event.EVT_ON_MODEL_AFTER_CREATE, func(e *event.UnknownPayload) {
|
||||||
|
// fmt.Println("syntheticevent: OnModelAfterCreate")
|
||||||
|
// fmt.Println("e", e)
|
||||||
|
// fmt.Println("cb", cb)
|
||||||
|
cb((*core.ModelEvent)(unsafe.Pointer(e)))
|
||||||
|
})
|
||||||
|
cleanup("onModelAfterCreate", unsub)
|
||||||
|
})
|
||||||
|
|
||||||
|
// type TransactionApi struct {
|
||||||
|
// Execute func(sql string)
|
||||||
|
// }
|
||||||
|
// __go_apis.Set("withTransaction", func(cb func(e *TransactionApi)) {
|
||||||
|
// app.Dao().RunInTransaction(func(txDao *daos.Dao) error {
|
||||||
|
// var api = TransactionApi{
|
||||||
|
// Execute: func(sql string) error {
|
||||||
|
// res, err := txDao.DB().Select().NewQuery(sql).Execute()
|
||||||
|
// if err != nil {
|
||||||
|
// return err
|
||||||
|
// }
|
||||||
|
|
||||||
|
// }}
|
||||||
|
// })
|
||||||
|
|
||||||
|
// })
|
||||||
|
|
||||||
|
__go_apis.Set("requireAdminAuth", apis.RequireAdminAuth)
|
||||||
|
__go_apis.Set("requireAdminAuthOnlyIfAny", apis.RequireAdminAuthOnlyIfAny)
|
||||||
|
__go_apis.Set("requireAdminOrOwnerAuth", apis.RequireAdminOrOwnerAuth)
|
||||||
|
__go_apis.Set("requireAdminOrUserAuth", apis.RequireAdminOrUserAuth)
|
||||||
|
__go_apis.Set("app", app)
|
||||||
|
__go_apis.Set("ping", func() string {
|
||||||
|
return "Hello from Go!"
|
||||||
|
})
|
||||||
|
__go_apis.Set("newNullStringMapArrayPtr", func() *[]dbx.NullStringMap {
|
||||||
|
var users2 []dbx.NullStringMap
|
||||||
|
return &users2
|
||||||
|
})
|
||||||
|
__go_apis.Set("newNullStringMap", func() dbx.NullStringMap {
|
||||||
|
var users2 dbx.NullStringMap
|
||||||
|
return users2
|
||||||
|
})
|
||||||
|
}
|
||||||
|
|
||||||
|
func cleanup(msg string, cb func()) {
|
||||||
|
fmt.Printf("adding cleanup: %s\n", msg)
|
||||||
|
cleanups = append(cleanups, func() {
|
||||||
|
fmt.Printf("executing cleanup: %s\n", msg)
|
||||||
|
cb()
|
||||||
|
})
|
||||||
|
}
|
||||||
|
|
||||||
|
func loadActiveScript() (string, error) {
|
||||||
|
|
||||||
|
collection, err := app.Dao().FindCollectionByNameOrId("pbscript")
|
||||||
|
if err != nil {
|
||||||
|
return "", err
|
||||||
|
}
|
||||||
|
recs, err := app.Dao().FindRecordsByExpr(collection, dbx.HashExp{"type": "script", "isActive": true})
|
||||||
|
if err != nil {
|
||||||
|
return "", err
|
||||||
|
}
|
||||||
|
if len(recs) > 1 {
|
||||||
|
return "", fmt.Errorf("expected one active script record but got %d", len(recs))
|
||||||
|
}
|
||||||
|
if len(recs) == 0 {
|
||||||
|
return "", nil // Empty script
|
||||||
|
}
|
||||||
|
rec := recs[0]
|
||||||
|
jsonData := rec.GetStringDataValue("data")
|
||||||
|
type Data struct {
|
||||||
|
Source string `json:"source"`
|
||||||
|
}
|
||||||
|
var json_map Data
|
||||||
|
err = json.Unmarshal([]byte(jsonData), &json_map)
|
||||||
|
if err != nil {
|
||||||
|
return "", err
|
||||||
|
}
|
||||||
|
|
||||||
|
script := json_map.Source
|
||||||
|
fmt.Printf("Script has been loaded.\n")
|
||||||
|
return script, nil
|
||||||
|
|
||||||
|
}
|
||||||
|
|
||||||
|
func reloadVm() error {
|
||||||
|
fmt.Println("Initializing PBScript engine")
|
||||||
|
vm = goja.New()
|
||||||
|
vm.SetFieldNameMapper(goja.UncapFieldNameMapper())
|
||||||
|
|
||||||
|
// Clean up all handlers
|
||||||
|
fmt.Println("Executing cleanups")
|
||||||
|
for i := 0; i < len(cleanups); i++ {
|
||||||
|
cleanups[i]()
|
||||||
|
}
|
||||||
|
cleanups = nil
|
||||||
|
|
||||||
|
// Load the main script
|
||||||
|
fmt.Println("Loading JS")
|
||||||
|
script, err := loadActiveScript()
|
||||||
|
if err != nil {
|
||||||
|
return err
|
||||||
|
}
|
||||||
|
|
||||||
|
// Console proxy
|
||||||
|
fmt.Println("Creating console proxy")
|
||||||
|
console := vm.NewObject()
|
||||||
|
console.Set("log", func(s ...goja.Value) {
|
||||||
|
for _, v := range s {
|
||||||
|
fmt.Printf("%s ", v.String())
|
||||||
|
}
|
||||||
|
fmt.Print("\n")
|
||||||
|
})
|
||||||
|
vm.Set("console", console)
|
||||||
|
|
||||||
|
fmt.Println("Creating apis proxy")
|
||||||
|
bindApis()
|
||||||
|
vm.Set("__go", __go_apis)
|
||||||
|
|
||||||
|
fmt.Println("Go initialization complete. Running script.")
|
||||||
|
source := fmt.Sprintf(`
|
||||||
|
console.log('Top of PBScript bootstrap')
|
||||||
|
let __jsfuncs = {ping: ()=>'Hello from PBScript!'}
|
||||||
|
function registerJsFuncs(funcs) {
|
||||||
|
__jsfuncs = {__jsfuncs, ...funcs }
|
||||||
|
}
|
||||||
|
%s
|
||||||
|
console.log('Pinging Go')
|
||||||
|
console.log('Pinging Go succeeded with:', __go.ping())
|
||||||
|
console.log('Bottom of PBScript bootstrap')
|
||||||
|
`, script)
|
||||||
|
_, err = vm.RunString(source)
|
||||||
|
if err != nil {
|
||||||
|
return err
|
||||||
|
}
|
||||||
|
|
||||||
|
// js api wireup
|
||||||
|
fmt.Println("Wiring up JS API")
|
||||||
|
type S struct {
|
||||||
|
Ping func() (string, *goja.Exception) `json:"ping"`
|
||||||
|
}
|
||||||
|
jsFuncs := S{}
|
||||||
|
err = vm.ExportTo(vm.Get("__jsfuncs"), &jsFuncs)
|
||||||
|
if err != nil {
|
||||||
|
return err
|
||||||
|
}
|
||||||
|
|
||||||
|
{
|
||||||
|
fmt.Println("Pinging JS")
|
||||||
|
res, err := jsFuncs.Ping()
|
||||||
|
if err != nil {
|
||||||
|
return fmt.Errorf("ping() failed with %s", err.Value().Export())
|
||||||
|
} else {
|
||||||
|
fmt.Printf("Ping succeeded with: %s\n", res)
|
||||||
|
}
|
||||||
|
}
|
||||||
|
return nil
|
||||||
|
}
|
||||||
|
|
||||||
|
func migrate() error {
|
||||||
|
fmt.Println("Finding collection")
|
||||||
|
_, err := app.Dao().FindCollectionByNameOrId("anything")
|
||||||
|
fmt.Println("Finished collection")
|
||||||
|
if err != nil {
|
||||||
|
err = app.Dao().SaveCollection(&models.Collection{
|
||||||
|
Name: "pbscript",
|
||||||
|
Schema: schema.NewSchema(
|
||||||
|
&schema.SchemaField{
|
||||||
|
Type: schema.FieldTypeText,
|
||||||
|
Name: "type",
|
||||||
|
},
|
||||||
|
&schema.SchemaField{
|
||||||
|
Type: schema.FieldTypeBool,
|
||||||
|
Name: "isActive",
|
||||||
|
},
|
||||||
|
&schema.SchemaField{
|
||||||
|
Type: schema.FieldTypeJson,
|
||||||
|
Name: "data",
|
||||||
|
},
|
||||||
|
),
|
||||||
|
})
|
||||||
|
if err != nil {
|
||||||
|
return err
|
||||||
|
}
|
||||||
|
}
|
||||||
|
|
||||||
|
return nil
|
||||||
|
}
|
||||||
|
|
||||||
|
func watchForScriptChanges() {
|
||||||
|
app.OnModelAfterUpdate().Add(func(e *core.ModelEvent) error {
|
||||||
|
if e.Model.TableName() == "pbscript" {
|
||||||
|
reloadVm()
|
||||||
|
}
|
||||||
|
return nil
|
||||||
|
})
|
||||||
|
|
||||||
|
app.OnModelAfterCreate().Add(func(e *core.ModelEvent) error {
|
||||||
|
if e.Model.TableName() == "pbscript" {
|
||||||
|
reloadVm()
|
||||||
|
}
|
||||||
|
return nil
|
||||||
|
})
|
||||||
|
|
||||||
|
app.OnBeforeServe().Add(func(e *core.ServeEvent) error {
|
||||||
|
// add new "GET /api/hello" route
|
||||||
|
|
||||||
|
e.Router.AddRoute(echo.Route{
|
||||||
|
Method: http.MethodPost,
|
||||||
|
Path: "/api/pbscript/deploy",
|
||||||
|
Handler: func(c echo.Context) error {
|
||||||
|
json_map := make(map[string]interface{})
|
||||||
|
err := json.NewDecoder(c.Request().Body).Decode(&json_map)
|
||||||
|
if err != nil {
|
||||||
|
return err
|
||||||
|
}
|
||||||
|
//json_map has the JSON Payload decoded into a map
|
||||||
|
src := json_map["source"]
|
||||||
|
|
||||||
|
err = app.Dao().RunInTransaction(func(txDao *daos.Dao) error {
|
||||||
|
fmt.Println("Deactivating active script")
|
||||||
|
_, err := txDao.DB().
|
||||||
|
NewQuery("UPDATE pbscript SET isActive=false WHERE type='script'").Execute()
|
||||||
|
if err != nil {
|
||||||
|
return err
|
||||||
|
}
|
||||||
|
|
||||||
|
fmt.Println("Packaging new record data")
|
||||||
|
bytes, err := json.Marshal(dbx.Params{"source": src})
|
||||||
|
if err != nil {
|
||||||
|
return err
|
||||||
|
}
|
||||||
|
_json := string(bytes)
|
||||||
|
|
||||||
|
fmt.Println("Saving new model")
|
||||||
|
collection, err := txDao.FindCollectionByNameOrId("pbscript")
|
||||||
|
if err != nil {
|
||||||
|
return err
|
||||||
|
}
|
||||||
|
record := models.NewRecord(collection)
|
||||||
|
record.SetDataValue("type", "script")
|
||||||
|
record.SetDataValue("isActive", "true")
|
||||||
|
record.SetDataValue("data", _json)
|
||||||
|
err = txDao.SaveRecord(record)
|
||||||
|
if err != nil {
|
||||||
|
return err
|
||||||
|
}
|
||||||
|
fmt.Println(("Record saved"))
|
||||||
|
// _, err = txDao.DB().
|
||||||
|
// NewQuery("INSERT INTO pbscript (type,isActive,data) values ('script', true, {data})").Bind(dbx.Params{"data": _json}).Execute()
|
||||||
|
// if err != nil {
|
||||||
|
// return err
|
||||||
|
// }
|
||||||
|
return nil
|
||||||
|
})
|
||||||
|
if err != nil {
|
||||||
|
return err
|
||||||
|
}
|
||||||
|
return c.String(http.StatusOK, "ok")
|
||||||
|
|
||||||
|
},
|
||||||
|
Middlewares: []echo.MiddlewareFunc{
|
||||||
|
apis.RequireAdminAuth(),
|
||||||
|
},
|
||||||
|
})
|
||||||
|
|
||||||
|
return nil
|
||||||
|
})
|
||||||
|
}
|
||||||
|
|
||||||
|
func initAppEvents() {
|
||||||
|
app.OnModelBeforeCreate().Add(func(e *core.ModelEvent) error {
|
||||||
|
fmt.Println("event: OnModelBeforeCreate")
|
||||||
|
event.Fire(event.EVT_ON_MODEL_BEFORE_CREATE, (*event.UnknownPayload)(unsafe.Pointer(e)))
|
||||||
|
return nil
|
||||||
|
})
|
||||||
|
app.OnModelAfterCreate().Add(func(e *core.ModelEvent) error {
|
||||||
|
fmt.Println("event: OnModelAfterCreate")
|
||||||
|
event.Fire(event.EVT_ON_MODEL_AFTER_CREATE, (*event.UnknownPayload)(unsafe.Pointer(e)))
|
||||||
|
return nil
|
||||||
|
})
|
||||||
|
}
|
||||||
|
|
||||||
|
func StartPBScript(_app *pocketbase.PocketBase) error {
|
||||||
|
app = _app
|
||||||
|
|
||||||
|
watchForScriptChanges()
|
||||||
|
|
||||||
|
app.OnBeforeServe().Add(func(e *core.ServeEvent) error {
|
||||||
|
migrate()
|
||||||
|
initAppEvents()
|
||||||
|
router = e.Router
|
||||||
|
err := reloadVm()
|
||||||
|
if err != nil {
|
||||||
|
logErrorf("Error loading VM: %s\n", err)
|
||||||
|
}
|
||||||
|
return nil
|
||||||
|
})
|
||||||
|
|
||||||
|
return nil
|
||||||
|
|
||||||
|
}
|
49
packages/pocketbase-cloud-funcs/event/event.go
Normal file
49
packages/pocketbase-cloud-funcs/event/event.go
Normal file
@ -0,0 +1,49 @@
|
|||||||
|
package event
|
||||||
|
|
||||||
|
import "fmt"
|
||||||
|
|
||||||
|
type UnknownPayload any
|
||||||
|
|
||||||
|
var inc = 0
|
||||||
|
var events = map[string]map[int]func(payload *UnknownPayload){}
|
||||||
|
|
||||||
|
const (
|
||||||
|
EVT_ON_MODEL_BEFORE_CREATE = "OnModelBeforeCreate"
|
||||||
|
EVT_ON_MODEL_AFTER_CREATE = "OnModelAfterCreate"
|
||||||
|
)
|
||||||
|
|
||||||
|
func isValid(eventName string) bool {
|
||||||
|
return eventName == EVT_ON_MODEL_BEFORE_CREATE || eventName== EVT_ON_MODEL_AFTER_CREATE
|
||||||
|
}
|
||||||
|
|
||||||
|
func ensureEvent(eventName string) {
|
||||||
|
if !isValid((eventName)) {
|
||||||
|
panic(fmt.Sprintf("%s is not a valid event name", eventName))
|
||||||
|
}
|
||||||
|
if _, ok := events[eventName]; !ok {
|
||||||
|
fmt.Printf("Creating collection for %s\n", eventName)
|
||||||
|
events[eventName] = make(map[int]func(payload *UnknownPayload))
|
||||||
|
}
|
||||||
|
}
|
||||||
|
|
||||||
|
func On(eventName string, cb func(payload *UnknownPayload)) func() {
|
||||||
|
ensureEvent(eventName)
|
||||||
|
|
||||||
|
inc++
|
||||||
|
idx := inc
|
||||||
|
events[eventName][idx] = cb
|
||||||
|
fmt.Printf("Adding %d to %s\n", idx, eventName)
|
||||||
|
return func() {
|
||||||
|
delete(events[eventName], idx)
|
||||||
|
}
|
||||||
|
}
|
||||||
|
|
||||||
|
func Fire(eventName string, payload *UnknownPayload) {
|
||||||
|
ensureEvent(eventName)
|
||||||
|
|
||||||
|
fmt.Printf("Firing %s\n", eventName)
|
||||||
|
for fnId, v := range events[eventName] {
|
||||||
|
fmt.Printf("Dispatching %s to %d\n", eventName, fnId)
|
||||||
|
v(payload)
|
||||||
|
}
|
||||||
|
}
|
90
packages/pocketbase-cloud-funcs/go.mod
Normal file
90
packages/pocketbase-cloud-funcs/go.mod
Normal file
@ -0,0 +1,90 @@
|
|||||||
|
module github.com/benallfree/pbscript/modules/pbscript
|
||||||
|
|
||||||
|
go 1.19
|
||||||
|
|
||||||
|
require (
|
||||||
|
github.com/dop251/goja v0.0.0-20220927172339-ea66e911853d
|
||||||
|
github.com/goccy/go-json v0.9.11
|
||||||
|
github.com/labstack/echo/v5 v5.0.0-20220201181537-ed2888cfa198
|
||||||
|
github.com/pocketbase/dbx v1.6.0
|
||||||
|
github.com/pocketbase/pocketbase v0.7.5
|
||||||
|
)
|
||||||
|
|
||||||
|
require (
|
||||||
|
github.com/AlecAivazis/survey/v2 v2.3.5 // indirect
|
||||||
|
github.com/asaskevich/govalidator v0.0.0-20210307081110-f21760c49a8d // indirect
|
||||||
|
github.com/aws/aws-sdk-go v1.44.85 // indirect
|
||||||
|
github.com/aws/aws-sdk-go-v2 v1.16.11 // indirect
|
||||||
|
github.com/aws/aws-sdk-go-v2/aws/protocol/eventstream v1.4.4 // indirect
|
||||||
|
github.com/aws/aws-sdk-go-v2/config v1.17.1 // indirect
|
||||||
|
github.com/aws/aws-sdk-go-v2/credentials v1.12.14 // indirect
|
||||||
|
github.com/aws/aws-sdk-go-v2/feature/ec2/imds v1.12.12 // indirect
|
||||||
|
github.com/aws/aws-sdk-go-v2/feature/s3/manager v1.11.27 // indirect
|
||||||
|
github.com/aws/aws-sdk-go-v2/internal/configsources v1.1.18 // indirect
|
||||||
|
github.com/aws/aws-sdk-go-v2/internal/endpoints/v2 v2.4.12 // indirect
|
||||||
|
github.com/aws/aws-sdk-go-v2/internal/ini v1.3.19 // indirect
|
||||||
|
github.com/aws/aws-sdk-go-v2/internal/v4a v1.0.9 // indirect
|
||||||
|
github.com/aws/aws-sdk-go-v2/service/internal/accept-encoding v1.9.5 // indirect
|
||||||
|
github.com/aws/aws-sdk-go-v2/service/internal/checksum v1.1.13 // indirect
|
||||||
|
github.com/aws/aws-sdk-go-v2/service/internal/presigned-url v1.9.12 // indirect
|
||||||
|
github.com/aws/aws-sdk-go-v2/service/internal/s3shared v1.13.12 // indirect
|
||||||
|
github.com/aws/aws-sdk-go-v2/service/s3 v1.27.5 // indirect
|
||||||
|
github.com/aws/aws-sdk-go-v2/service/sso v1.11.17 // indirect
|
||||||
|
github.com/aws/aws-sdk-go-v2/service/sts v1.16.13 // indirect
|
||||||
|
github.com/aws/smithy-go v1.12.1 // indirect
|
||||||
|
github.com/disintegration/imaging v1.6.2 // indirect
|
||||||
|
github.com/dlclark/regexp2 v1.7.0 // indirect
|
||||||
|
github.com/domodwyer/mailyak/v3 v3.3.4 // indirect
|
||||||
|
github.com/fatih/color v1.13.0 // indirect
|
||||||
|
github.com/gabriel-vasile/mimetype v1.4.1 // indirect
|
||||||
|
github.com/ganigeorgiev/fexpr v0.1.1 // indirect
|
||||||
|
github.com/go-ozzo/ozzo-validation/v4 v4.3.0 // indirect
|
||||||
|
github.com/go-sourcemap/sourcemap v2.1.3+incompatible // indirect
|
||||||
|
github.com/golang-jwt/jwt/v4 v4.4.2 // indirect
|
||||||
|
github.com/golang/groupcache v0.0.0-20210331224755-41bb18bfe9da // indirect
|
||||||
|
github.com/golang/protobuf v1.5.2 // indirect
|
||||||
|
github.com/google/uuid v1.3.0 // indirect
|
||||||
|
github.com/google/wire v0.5.0 // indirect
|
||||||
|
github.com/googleapis/gax-go/v2 v2.5.1 // indirect
|
||||||
|
github.com/inconshreveable/mousetrap v1.0.1 // indirect
|
||||||
|
github.com/jmespath/go-jmespath v0.4.0 // indirect
|
||||||
|
github.com/kballard/go-shellquote v0.0.0-20180428030007-95032a82bc51 // indirect
|
||||||
|
github.com/mattn/go-colorable v0.1.13 // indirect
|
||||||
|
github.com/mattn/go-isatty v0.0.16 // indirect
|
||||||
|
github.com/mattn/go-sqlite3 v1.14.15 // indirect
|
||||||
|
github.com/mgutz/ansi v0.0.0-20200706080929-d51e80ef957d // indirect
|
||||||
|
github.com/remyoudompheng/bigfft v0.0.0-20200410134404-eec4a21b6bb0 // indirect
|
||||||
|
github.com/spf13/cast v1.5.0 // indirect
|
||||||
|
github.com/spf13/cobra v1.5.0 // indirect
|
||||||
|
github.com/spf13/pflag v1.0.5 // indirect
|
||||||
|
github.com/valyala/bytebufferpool v1.0.0 // indirect
|
||||||
|
github.com/valyala/fasttemplate v1.2.1 // indirect
|
||||||
|
go.opencensus.io v0.23.0 // indirect
|
||||||
|
gocloud.dev v0.26.0 // indirect
|
||||||
|
golang.org/x/crypto v0.0.0-20220824171710-5757bc0c5503 // indirect
|
||||||
|
golang.org/x/image v0.0.0-20220722155232-062f8c9fd539 // indirect
|
||||||
|
golang.org/x/mod v0.6.0-dev.0.20220419223038-86c51ed26bb4 // indirect
|
||||||
|
golang.org/x/net v0.0.0-20220822230855-b0a4917ee28c // indirect
|
||||||
|
golang.org/x/oauth2 v0.0.0-20220822191816-0ebed06d0094 // indirect
|
||||||
|
golang.org/x/sys v0.0.0-20220825204002-c680a09ffe64 // indirect
|
||||||
|
golang.org/x/term v0.0.0-20220722155259-a9ba230a4035 // indirect
|
||||||
|
golang.org/x/text v0.3.7 // indirect
|
||||||
|
golang.org/x/time v0.0.0-20220722155302-e5dcc9cfc0b9 // indirect
|
||||||
|
golang.org/x/tools v0.1.12 // indirect
|
||||||
|
golang.org/x/xerrors v0.0.0-20220609144429-65e65417b02f // indirect
|
||||||
|
google.golang.org/api v0.94.0 // indirect
|
||||||
|
google.golang.org/appengine v1.6.7 // indirect
|
||||||
|
google.golang.org/genproto v0.0.0-20220822174746-9e6da59bd2fc // indirect
|
||||||
|
google.golang.org/grpc v1.49.0 // indirect
|
||||||
|
google.golang.org/protobuf v1.28.1 // indirect
|
||||||
|
lukechampine.com/uint128 v1.2.0 // indirect
|
||||||
|
modernc.org/cc/v3 v3.36.3 // indirect
|
||||||
|
modernc.org/ccgo/v3 v3.16.9 // indirect
|
||||||
|
modernc.org/libc v1.17.0 // indirect
|
||||||
|
modernc.org/mathutil v1.5.0 // indirect
|
||||||
|
modernc.org/memory v1.2.0 // indirect
|
||||||
|
modernc.org/opt v0.1.3 // indirect
|
||||||
|
modernc.org/sqlite v1.18.1 // indirect
|
||||||
|
modernc.org/strutil v1.1.2 // indirect
|
||||||
|
modernc.org/token v1.0.0 // indirect
|
||||||
|
)
|
1158
packages/pocketbase-cloud-funcs/go.sum
Normal file
1158
packages/pocketbase-cloud-funcs/go.sum
Normal file
File diff suppressed because it is too large
Load Diff
12
packages/pocketbase-cloud-funcs/pocketbase.go
Normal file
12
packages/pocketbase-cloud-funcs/pocketbase.go
Normal file
@ -0,0 +1,12 @@
|
|||||||
|
package pocketbase
|
||||||
|
|
||||||
|
import (
|
||||||
|
"github.com/benallfree/pbscript/modules/pbscript/engine"
|
||||||
|
"github.com/pocketbase/pocketbase"
|
||||||
|
)
|
||||||
|
|
||||||
|
func New() *pocketbase.PocketBase {
|
||||||
|
app := pocketbase.New()
|
||||||
|
engine.StartPBScript(app)
|
||||||
|
return app
|
||||||
|
}
|
3
packages/pocketbase/.gitignore
vendored
3
packages/pocketbase/.gitignore
vendored
@ -1 +1,2 @@
|
|||||||
dist
|
dist
|
||||||
|
build
|
@ -1,38 +1,41 @@
|
|||||||
# create-svelte
|
# PocketHost UI
|
||||||
|
|
||||||
Everything you need to build a Svelte project, powered by [`create-svelte`](https://github.com/sveltejs/kit/tree/master/packages/create-svelte).
|
## Built with SvelteKit and Typescript
|
||||||
|
|
||||||
## Creating a project
|
Description about PocketHost goes here!
|
||||||
|
|
||||||
If you're seeing this, you've probably already done this step. Congrats!
|
## Developing Locally
|
||||||
|
|
||||||
```bash
|
To run this project, navigate to the `/packages/pockethost.io` folder and run `vite dev`.
|
||||||
# create a new project in the current directory
|
|
||||||
npm create svelte@latest
|
|
||||||
|
|
||||||
# create a new project in my-app
|
It will start up the server here: [http://127.0.0.1:5173/](http://127.0.0.1:5173/) and now you're ready to code!
|
||||||
npm create svelte@latest my-app
|
|
||||||
|
## Routing
|
||||||
|
|
||||||
|
There is a file called `public-routes.json` that controls which URLs are accessible to non-authenticated users. Any public facing page needs to have its URL added to this list. Otherwise, the authentication system will kick in and send them to the homepage.
|
||||||
|
|
||||||
|
## User Management
|
||||||
|
|
||||||
|
This app uses [Svelte Stores](https://svelte.dev/docs#run-time-svelte-store) to track the user's information. At the top is the `globalUserData` store. This contains everything about the user that comes from Pocketbase, including their JWT Token.
|
||||||
|
|
||||||
|
### Derived User Values
|
||||||
|
|
||||||
|
There are additional derived values that are useful for showing and hiding components across the site. The first one is `isUserLoggedIn`. This one will return a true or false depending on the state of the logged in user. It is dependent on the `email` property in the Pocketbase response.
|
||||||
|
|
||||||
|
The second derived value is `isUserVerified`. This will return a true or false boolean depending on if their Pocketbase account has been verified via the email they got when they initially registered.
|
||||||
|
|
||||||
|
An example of showing or hiding components can be found with the `<VerifyAccountBar>` component, that prompts the user to make sure to verify their account before continuing. The code looks roughly like this:
|
||||||
|
|
||||||
|
```svelte
|
||||||
|
<script>
|
||||||
|
import { isUserLoggedIn, isUserVerified } from '$util/stores'
|
||||||
|
</script>
|
||||||
|
|
||||||
|
{#if $isUserLoggedIn && !$isUserVerified}
|
||||||
|
<YourComponentHere />
|
||||||
|
{/if}
|
||||||
```
|
```
|
||||||
|
|
||||||
## Developing
|
This particular example will only render the component if the user is logged in, and their account **has not** been verified. Notice the `$` symbol as well, this is required for [Svelte Stores](https://svelte.dev/docs#run-time-svelte-store) when using Store values in the UI.
|
||||||
|
|
||||||
Once you've created a project and installed dependencies with `npm install` (or `pnpm install` or `yarn`), start a development server:
|
If you need to use these values in a normal javascript/typescript file instead, you can utilize [Svelte's `get()` method](https://svelte.dev/docs#run-time-svelte-store-get) instead.
|
||||||
|
|
||||||
```bash
|
|
||||||
npm run dev
|
|
||||||
|
|
||||||
# or start the server and open the app in a new browser tab
|
|
||||||
npm run dev -- --open
|
|
||||||
```
|
|
||||||
|
|
||||||
## Building
|
|
||||||
|
|
||||||
To create a production version of your app:
|
|
||||||
|
|
||||||
```bash
|
|
||||||
npm run build
|
|
||||||
```
|
|
||||||
|
|
||||||
You can preview the production build with `npm run preview`.
|
|
||||||
|
|
||||||
> To deploy your app, you may need to install an [adapter](https://kit.svelte.dev/docs/adapters) for your target environment.
|
|
||||||
|
@ -1,15 +1,18 @@
|
|||||||
<script lang="ts">
|
<script lang="ts">
|
||||||
|
import { AlertTypes } from '$components/AlertBar.types'
|
||||||
|
|
||||||
export let title: string = ''
|
export let title: string = ''
|
||||||
export let text: string = ''
|
export let text: string = ''
|
||||||
export let icon: string = ''
|
export let icon: string = ''
|
||||||
|
export let alertType: AlertTypes = AlertTypes.Warning
|
||||||
</script>
|
</script>
|
||||||
|
|
||||||
<div class="alert alert-warning d-flex gap-3 align-items-center" role="alert">
|
<div class="alert alert-{alertType} d-flex gap-3 align-items-center" role="alert">
|
||||||
{#if icon}
|
{#if icon}
|
||||||
<i class={icon} />
|
<i class={icon} />
|
||||||
{/if}
|
{/if}
|
||||||
|
|
||||||
<div>
|
<div class="w-100">
|
||||||
{#if title}<p class="fw-bold mb-0">{title}</p>{/if}
|
{#if title}<p class="fw-bold mb-0">{title}</p>{/if}
|
||||||
|
|
||||||
{#if text}
|
{#if text}
|
||||||
|
10
packages/pockethost.io/src/components/AlertBar.types.ts
Normal file
10
packages/pockethost.io/src/components/AlertBar.types.ts
Normal file
@ -0,0 +1,10 @@
|
|||||||
|
export enum AlertTypes {
|
||||||
|
Primary = 'primary',
|
||||||
|
Secondary = 'secondary',
|
||||||
|
Success = 'success',
|
||||||
|
Danger = 'danger',
|
||||||
|
Warning = 'warning',
|
||||||
|
Info = 'info',
|
||||||
|
Light = 'light',
|
||||||
|
Dark = 'dark'
|
||||||
|
}
|
@ -1,9 +1,9 @@
|
|||||||
<script lang="ts">
|
<script lang="ts">
|
||||||
import { browser } from '$app/environment'
|
import { browser } from '$app/environment'
|
||||||
import { client } from '$src/pocketbase'
|
|
||||||
import { goto } from '$app/navigation'
|
import { goto } from '$app/navigation'
|
||||||
|
import { client } from '$src/pocketbase'
|
||||||
|
|
||||||
if (browser && client.isLoggedIn()) {
|
if (browser && client().isLoggedIn()) {
|
||||||
goto(`/dashboard`)
|
goto(`/dashboard`)
|
||||||
}
|
}
|
||||||
</script>
|
</script>
|
||||||
|
@ -1,30 +1,9 @@
|
|||||||
<script lang="ts">
|
<script lang="ts">
|
||||||
import MediaQuery from '$components/MediaQuery.svelte'
|
import MediaQuery from '$components/MediaQuery.svelte'
|
||||||
import ThemeToggle from '$components/ThemeToggle.svelte'
|
import ThemeToggle from '$components/ThemeToggle.svelte'
|
||||||
import { client } from '$src/pocketbase'
|
import { handleLogoutAndRedirect } from '$util/database'
|
||||||
import { createCleanupManagerSync } from '$util/CleanupManager'
|
import { isUserLoggedIn } from '$util/stores'
|
||||||
import { onDestroy, onMount } from 'svelte'
|
import AuthStateGuard from './helpers/AuthStateGuard.svelte'
|
||||||
const { isLoggedIn, logOut, onAuthChange } = client
|
|
||||||
|
|
||||||
const cm = createCleanupManagerSync()
|
|
||||||
|
|
||||||
let _isLoggedIn = isLoggedIn()
|
|
||||||
onMount(() => {
|
|
||||||
_isLoggedIn = isLoggedIn()
|
|
||||||
const unsub = onAuthChange(() => {
|
|
||||||
_isLoggedIn = isLoggedIn()
|
|
||||||
})
|
|
||||||
cm.add(unsub)
|
|
||||||
})
|
|
||||||
|
|
||||||
onDestroy(cm.cleanupAll)
|
|
||||||
|
|
||||||
const handleLogout = (e: Event) => {
|
|
||||||
e.preventDefault()
|
|
||||||
logOut()
|
|
||||||
// Hard refresh to make sure any remaining data is cleared
|
|
||||||
window.location.href = '/'
|
|
||||||
}
|
|
||||||
</script>
|
</script>
|
||||||
|
|
||||||
<header class="container-fluid">
|
<header class="container-fluid">
|
||||||
@ -48,49 +27,56 @@
|
|||||||
|
|
||||||
<div class="collapse navbar-collapse" id="nav-links">
|
<div class="collapse navbar-collapse" id="nav-links">
|
||||||
<ul class="navbar-nav ms-auto mb-2 mb-md-0">
|
<ul class="navbar-nav ms-auto mb-2 mb-md-0">
|
||||||
{#if _isLoggedIn}
|
<AuthStateGuard>
|
||||||
<li class="nav-item text-md-start text-center">
|
{#if $isUserLoggedIn}
|
||||||
<a class="nav-link" href="/dashboard">Dashboard</a>
|
<li class="nav-item text-md-start text-center">
|
||||||
</li>
|
<a class="nav-link" href="/dashboard">Dashboard</a>
|
||||||
|
</li>
|
||||||
|
|
||||||
<MediaQuery query="(min-width: 768px)" let:matches>
|
<MediaQuery query="(min-width: 768px)" let:matches>
|
||||||
{#if matches}
|
{#if matches}
|
||||||
<li class="nav-item dropdown">
|
<li class="nav-item dropdown">
|
||||||
<button
|
<button
|
||||||
class="btn border-0 nav-link"
|
class="btn border-0 nav-link"
|
||||||
type="button"
|
type="button"
|
||||||
data-bs-toggle="dropdown"
|
data-bs-toggle="dropdown"
|
||||||
aria-label="Click to expand the Account Dropdown"
|
aria-label="Click to expand the Account Dropdown"
|
||||||
title="Account Dropdown"
|
title="Account Dropdown"
|
||||||
aria-expanded="false"
|
aria-expanded="false"
|
||||||
>
|
>
|
||||||
<i class="bi bi-person-circle" />
|
<i class="bi bi-person-circle" />
|
||||||
</button>
|
</button>
|
||||||
|
|
||||||
<ul class="dropdown-menu dropdown-menu-end">
|
<ul class="dropdown-menu dropdown-menu-end">
|
||||||
<li><a class="dropdown-item" href="/" on:click={handleLogout}>Logout</a></li>
|
<li>
|
||||||
</ul>
|
<button class="dropdown-item" type="button" on:click={handleLogoutAndRedirect}
|
||||||
</li>
|
>Logout</button
|
||||||
{:else}
|
>
|
||||||
<li class="nav-item">
|
</li>
|
||||||
<a class="nav-link text-md-start text-center" href="/" on:click={handleLogout}
|
</ul>
|
||||||
>Logout</a
|
</li>
|
||||||
>
|
{:else}
|
||||||
</li>
|
<li class="nav-item">
|
||||||
{/if}
|
<a
|
||||||
</MediaQuery>
|
class="nav-link text-md-start text-center"
|
||||||
{/if}
|
href="/"
|
||||||
|
on:click={handleLogoutAndRedirect}>Logout</a
|
||||||
|
>
|
||||||
|
</li>
|
||||||
|
{/if}
|
||||||
|
</MediaQuery>
|
||||||
|
{/if}
|
||||||
|
|
||||||
{#if !_isLoggedIn}
|
{#if !$isUserLoggedIn}
|
||||||
<li class="nav-item">
|
<li class="nav-item">
|
||||||
<a class="nav-link text-md-start text-center" href="/signup">Sign up</a>
|
<a class="nav-link text-md-start text-center" href="/signup">Sign up</a>
|
||||||
</li>
|
</li>
|
||||||
|
|
||||||
<li class="nav-item">
|
|
||||||
<a class="nav-link text-md-start text-center" href="/login">Log in</a>
|
|
||||||
</li>
|
|
||||||
{/if}
|
|
||||||
|
|
||||||
|
<li class="nav-item">
|
||||||
|
<a class="nav-link text-md-start text-center" href="/login">Log in</a>
|
||||||
|
</li>
|
||||||
|
{/if}
|
||||||
|
</AuthStateGuard>
|
||||||
<li class="nav-item">
|
<li class="nav-item">
|
||||||
<a
|
<a
|
||||||
class="nav-link text-md-start text-center"
|
class="nav-link text-md-start text-center"
|
||||||
|
@ -1,16 +0,0 @@
|
|||||||
<script lang="ts">
|
|
||||||
import { browser } from '$app/environment'
|
|
||||||
import { client } from '$src/pocketbase'
|
|
||||||
import { goto } from '$app/navigation'
|
|
||||||
|
|
||||||
const { isLoggedIn } = client
|
|
||||||
|
|
||||||
if (browser && !isLoggedIn()) {
|
|
||||||
goto(`/signup`)
|
|
||||||
}
|
|
||||||
</script>
|
|
||||||
|
|
||||||
<slot />
|
|
||||||
|
|
||||||
<style lang="scss">
|
|
||||||
</style>
|
|
@ -0,0 +1,63 @@
|
|||||||
|
<script lang="ts">
|
||||||
|
import AlertBar from '$components/AlertBar.svelte'
|
||||||
|
import { AlertTypes } from '$components/AlertBar.types'
|
||||||
|
import { handleResendVerificationEmail } from '$util/database'
|
||||||
|
import { isUserLoggedIn, isUserVerified } from '$util/stores'
|
||||||
|
|
||||||
|
let defaultAlertBarType: AlertTypes = AlertTypes.Warning
|
||||||
|
|
||||||
|
let isButtonProcessing: boolean = false
|
||||||
|
let formError: string = ''
|
||||||
|
|
||||||
|
const handleClick = () => {
|
||||||
|
// Reset the alert type if resubmitted
|
||||||
|
defaultAlertBarType = AlertTypes.Warning
|
||||||
|
|
||||||
|
// Update the state
|
||||||
|
isButtonProcessing = true
|
||||||
|
|
||||||
|
handleResendVerificationEmail((error) => {
|
||||||
|
formError = error
|
||||||
|
defaultAlertBarType = AlertTypes.Danger
|
||||||
|
isButtonProcessing = false
|
||||||
|
})
|
||||||
|
|
||||||
|
// Wait a bit after the success before showing the button again
|
||||||
|
setTimeout(() => {
|
||||||
|
isButtonProcessing = false
|
||||||
|
}, 5000)
|
||||||
|
}
|
||||||
|
</script>
|
||||||
|
|
||||||
|
{#if $isUserLoggedIn && !$isUserVerified}
|
||||||
|
<div class="container py-3">
|
||||||
|
<AlertBar alertType={defaultAlertBarType}>
|
||||||
|
<div class="d-flex flex-wrap align-items-center justify-content-center gap-3">
|
||||||
|
<i class="bi bi-envelope-exclamation" />
|
||||||
|
|
||||||
|
<div>Please verify your account by clicking the link in your email</div>
|
||||||
|
|
||||||
|
{#if isButtonProcessing}
|
||||||
|
<div class="success-icon">
|
||||||
|
<i class="bi bi-check-square" />
|
||||||
|
Sent!
|
||||||
|
</div>
|
||||||
|
{:else}
|
||||||
|
<button type="button" class="btn btn-outline-secondary" on:click={handleClick}
|
||||||
|
>Resend Email</button
|
||||||
|
>
|
||||||
|
{/if}
|
||||||
|
</div>
|
||||||
|
|
||||||
|
{#if formError}
|
||||||
|
<div class="border-top text-center mt-2 pt-2">{formError}</div>
|
||||||
|
{/if}
|
||||||
|
</AlertBar>
|
||||||
|
</div>
|
||||||
|
{/if}
|
||||||
|
|
||||||
|
<style>
|
||||||
|
.success-icon {
|
||||||
|
padding: 0.375rem 0.75rem;
|
||||||
|
}
|
||||||
|
</style>
|
@ -0,0 +1,10 @@
|
|||||||
|
<script lang="ts">
|
||||||
|
import { isAuthStateInitialized } from '$util/stores'
|
||||||
|
import { onMount } from 'svelte'
|
||||||
|
|
||||||
|
onMount(() => {})
|
||||||
|
</script>
|
||||||
|
|
||||||
|
{#if $isAuthStateInitialized}
|
||||||
|
<slot />
|
||||||
|
{/if}
|
18
packages/pockethost.io/src/components/helpers/Protect.svelte
Normal file
18
packages/pockethost.io/src/components/helpers/Protect.svelte
Normal file
@ -0,0 +1,18 @@
|
|||||||
|
<script lang="ts">
|
||||||
|
import { client } from '$src/pocketbase'
|
||||||
|
import publicRoutes from '$util/public-routes.json'
|
||||||
|
import { getRouter } from '$util/utilities'
|
||||||
|
import { onMount } from 'svelte'
|
||||||
|
onMount(() => {
|
||||||
|
const { isLoggedIn } = client()
|
||||||
|
if (isLoggedIn()) return
|
||||||
|
// Send user to the homepage
|
||||||
|
const router = getRouter()
|
||||||
|
|
||||||
|
const { pathname } = router
|
||||||
|
if (!publicRoutes.includes(pathname)) {
|
||||||
|
console.warn(`${pathname} is a private route`)
|
||||||
|
window.location.href = '/'
|
||||||
|
}
|
||||||
|
})
|
||||||
|
</script>
|
@ -7,9 +7,8 @@
|
|||||||
.split('; ')
|
.split('; ')
|
||||||
.find((row) => row.startsWith('theme='))
|
.find((row) => row.startsWith('theme='))
|
||||||
?.split('=')?.[1] || 'light'
|
?.split('=')?.[1] || 'light'
|
||||||
console.log(`Current theme is ${currentTheme}`)
|
|
||||||
document.querySelector('html')?.setAttribute(THEME_ATTRIBUTE, currentTheme)
|
document.querySelector('html')?.setAttribute(THEME_ATTRIBUTE, currentTheme)
|
||||||
console.log(document.querySelector('html'))
|
|
||||||
}
|
}
|
||||||
</script>
|
</script>
|
||||||
</svelte:head>
|
</svelte:head>
|
@ -1,11 +1,27 @@
|
|||||||
import type { InstanceId, Instance_In, Instance_Out } from '@pockethost/common'
|
import { createGenericSyncEvent } from '$util/events'
|
||||||
import { createRealtimeSubscriptionManager } from '@pockethost/common'
|
import {
|
||||||
|
assertExists,
|
||||||
|
createRealtimeSubscriptionManager,
|
||||||
|
type InstanceId,
|
||||||
|
type Instance_In,
|
||||||
|
type Instance_Out
|
||||||
|
} from '@pockethost/common'
|
||||||
import { keys, map } from '@s-libs/micro-dash'
|
import { keys, map } from '@s-libs/micro-dash'
|
||||||
import PocketBase, { BaseAuthStore, ClientResponseError, Record } from 'pocketbase'
|
import PocketBase, { Admin, BaseAuthStore, ClientResponseError, Record, User } from 'pocketbase'
|
||||||
import type { Unsubscriber } from 'svelte/store'
|
import type { Unsubscriber } from 'svelte/store'
|
||||||
|
import { safeCatch } from '../util/safeCatch'
|
||||||
|
|
||||||
export type AuthChangeHandler = (user: BaseAuthStore) => void
|
export type AuthChangeHandler = (user: BaseAuthStore) => void
|
||||||
|
|
||||||
|
export type AuthToken = string
|
||||||
|
export type AuthStoreProps = {
|
||||||
|
token: AuthToken
|
||||||
|
model: User | null
|
||||||
|
isValid: boolean
|
||||||
|
}
|
||||||
|
|
||||||
|
export type PocketbaseClientApi = ReturnType<typeof createPocketbaseClient>
|
||||||
|
|
||||||
export const createPocketbaseClient = (url: string) => {
|
export const createPocketbaseClient = (url: string) => {
|
||||||
const client = new PocketBase(url)
|
const client = new PocketBase(url)
|
||||||
|
|
||||||
@ -15,29 +31,34 @@ export const createPocketbaseClient = (url: string) => {
|
|||||||
|
|
||||||
const isLoggedIn = () => authStore.isValid
|
const isLoggedIn = () => authStore.isValid
|
||||||
|
|
||||||
const onAuthChange = (cb: AuthChangeHandler): Unsubscriber =>
|
|
||||||
authStore.onChange(() => {
|
|
||||||
cb(authStore)
|
|
||||||
})
|
|
||||||
|
|
||||||
const logOut = () => authStore.clear()
|
const logOut = () => authStore.clear()
|
||||||
|
|
||||||
const createUser = (email: string, password: string) =>
|
const createUser = safeCatch(`createUser`, (email: string, password: string) =>
|
||||||
client.users.create({
|
client.users.create({
|
||||||
email,
|
email,
|
||||||
password,
|
password,
|
||||||
passwordConfirm: password
|
passwordConfirm: password
|
||||||
})
|
})
|
||||||
|
)
|
||||||
|
|
||||||
const authViaEmail = (email: string, password: string) =>
|
const authViaEmail = safeCatch(`authViaEmail`, (email: string, password: string) =>
|
||||||
client.users.authViaEmail(email, password)
|
client.users.authViaEmail(email, password)
|
||||||
|
)
|
||||||
|
|
||||||
const createInstance = (payload: Instance_In): Promise<Instance_Out> => {
|
const refreshAuthToken = safeCatch(`refreshAuthToken`, () => client.users.refresh())
|
||||||
return client.records.create('instances', payload).then((r) => r as unknown as Instance_Out)
|
|
||||||
}
|
|
||||||
|
|
||||||
const getInstanceById = (id: InstanceId): Promise<Instance_Out | undefined> =>
|
const createInstance = safeCatch(
|
||||||
client.records.getOne('instances', id).then((r) => r as unknown as Instance_Out)
|
`createInstance`,
|
||||||
|
(payload: Instance_In): Promise<Instance_Out> => {
|
||||||
|
return client.records.create('instances', payload).then((r) => r as unknown as Instance_Out)
|
||||||
|
}
|
||||||
|
)
|
||||||
|
|
||||||
|
const getInstanceById = safeCatch(
|
||||||
|
`getInstanceById`,
|
||||||
|
(id: InstanceId): Promise<Instance_Out | undefined> =>
|
||||||
|
client.records.getOne('instances', id).then((r) => r as unknown as Instance_Out)
|
||||||
|
)
|
||||||
|
|
||||||
const subscribe = createRealtimeSubscriptionManager(client)
|
const subscribe = createRealtimeSubscriptionManager(client)
|
||||||
|
|
||||||
@ -45,13 +66,12 @@ export const createPocketbaseClient = (url: string) => {
|
|||||||
const slug = `instances/${id}`
|
const slug = `instances/${id}`
|
||||||
getInstanceById(id).then((v) => {
|
getInstanceById(id).then((v) => {
|
||||||
if (!v) return
|
if (!v) return
|
||||||
console.log(`Initial record`, { v })
|
|
||||||
cb(v)
|
cb(v)
|
||||||
})
|
})
|
||||||
return subscribe(slug, cb)
|
return subscribe(slug, cb)
|
||||||
}
|
}
|
||||||
|
|
||||||
const getAllInstancesById = async () =>
|
const getAllInstancesById = safeCatch(`getAllInstancesById`, async () =>
|
||||||
(
|
(
|
||||||
await client.records.getFullList('instances').catch((e) => {
|
await client.records.getFullList('instances').catch((e) => {
|
||||||
console.error(`getAllInstancesById failed with ${e}`)
|
console.error(`getAllInstancesById failed with ${e}`)
|
||||||
@ -61,16 +81,11 @@ export const createPocketbaseClient = (url: string) => {
|
|||||||
c[v.id] = v
|
c[v.id] = v
|
||||||
return c
|
return c
|
||||||
}, {} as Record)
|
}, {} as Record)
|
||||||
|
)
|
||||||
|
|
||||||
const setInstance = (instanceId: InstanceId, fields: Instance_In) => {
|
const setInstance = safeCatch(`setInstance`, (instanceId: InstanceId, fields: Instance_In) => {
|
||||||
console.log(`${instanceId} setting fields`, { fields })
|
return client.records.update('instances', instanceId, fields)
|
||||||
return client.records.update('instances', instanceId, fields).catch((e) => {
|
})
|
||||||
console.error(`setInstance failed for ${instanceId} with ${e}`, {
|
|
||||||
fields
|
|
||||||
})
|
|
||||||
throw e
|
|
||||||
})
|
|
||||||
}
|
|
||||||
|
|
||||||
const parseError = (e: Error): string[] => {
|
const parseError = (e: Error): string[] => {
|
||||||
if (!(e instanceof ClientResponseError)) return [e.message]
|
if (!(e instanceof ClientResponseError)) return [e.message]
|
||||||
@ -78,7 +93,77 @@ export const createPocketbaseClient = (url: string) => {
|
|||||||
return map(e.data.data, (v, k) => (v ? v.message : undefined)).filter((v) => !!v)
|
return map(e.data.data, (v, k) => (v ? v.message : undefined)).filter((v) => !!v)
|
||||||
}
|
}
|
||||||
|
|
||||||
|
const resendVerificationEmail = safeCatch(`resendVerificationEmail`, async () => {
|
||||||
|
const user = client.authStore.model
|
||||||
|
assertExists(user, `Login required`)
|
||||||
|
await client.users.requestVerification(user.email)
|
||||||
|
})
|
||||||
|
|
||||||
|
const getAuthStoreProps = (): AuthStoreProps => {
|
||||||
|
const { token, model, isValid } = client.authStore
|
||||||
|
// console.log(`curent authstore`, { token, model, isValid })
|
||||||
|
if (model instanceof Admin) throw new Error(`Admin models not supported`)
|
||||||
|
return {
|
||||||
|
token,
|
||||||
|
model,
|
||||||
|
isValid
|
||||||
|
}
|
||||||
|
}
|
||||||
|
|
||||||
|
/**
|
||||||
|
* Use synthetic event for authStore changers so we can broadcast just
|
||||||
|
* the props we want and not the actual authStore object.
|
||||||
|
*/
|
||||||
|
const [onAuthChange, fireAuthChange] = createGenericSyncEvent<AuthStoreProps>()
|
||||||
|
|
||||||
|
/**
|
||||||
|
* This section is for initialization
|
||||||
|
*/
|
||||||
|
{
|
||||||
|
/**
|
||||||
|
* Listen for native authStore changes and convert to synthetic event
|
||||||
|
*/
|
||||||
|
client.authStore.onChange(() => {
|
||||||
|
fireAuthChange(getAuthStoreProps())
|
||||||
|
})
|
||||||
|
|
||||||
|
/**
|
||||||
|
* Refresh the auth token immediately upon creating the client. The auth token may be
|
||||||
|
* out of date, or fields in the user record may have changed in the backend.
|
||||||
|
*/
|
||||||
|
refreshAuthToken()
|
||||||
|
|
||||||
|
/**
|
||||||
|
* Listen for auth state changes and subscribe to realtime _user events.
|
||||||
|
* This way, when the verified flag is flipped, it will appear that the
|
||||||
|
* authstore model is updated.
|
||||||
|
*
|
||||||
|
* Polling is a stopgap til v.0.8. Once 0.8 comes along, we can do a realtime
|
||||||
|
* watch on the user record and update auth accordingly.
|
||||||
|
*/
|
||||||
|
const unsub = onAuthChange((authStore) => {
|
||||||
|
console.log(`onAuthChange`, { ...authStore })
|
||||||
|
const { model } = authStore
|
||||||
|
if (!model) return
|
||||||
|
if (model instanceof Admin) return
|
||||||
|
if (model.verified) {
|
||||||
|
unsub()
|
||||||
|
return
|
||||||
|
}
|
||||||
|
const _check = safeCatch(`_checkVerified`, () => client.users.refresh())
|
||||||
|
setTimeout(_check, 1000)
|
||||||
|
|
||||||
|
// FIXME - THIS DOES NOT WORK, WE HAVE TO POLL INSTEAD. FIX IN V0.8
|
||||||
|
// console.log(`watching _users`)
|
||||||
|
// unsub = subscribe<User>(`users/${model.id}`, (user) => {
|
||||||
|
// console.log(`realtime _users change`, { ...user })
|
||||||
|
// fireAuthChange({ ...authStore, model: user })
|
||||||
|
// })
|
||||||
|
})
|
||||||
|
}
|
||||||
|
|
||||||
return {
|
return {
|
||||||
|
getAuthStoreProps,
|
||||||
parseError,
|
parseError,
|
||||||
subscribe,
|
subscribe,
|
||||||
getInstanceById,
|
getInstanceById,
|
||||||
@ -91,6 +176,7 @@ export const createPocketbaseClient = (url: string) => {
|
|||||||
user,
|
user,
|
||||||
watchInstanceById,
|
watchInstanceById,
|
||||||
getAllInstancesById,
|
getAllInstancesById,
|
||||||
setInstance
|
setInstance,
|
||||||
|
resendVerificationEmail
|
||||||
}
|
}
|
||||||
}
|
}
|
||||||
|
@ -1,7 +1,15 @@
|
|||||||
|
import { browser } from '$app/environment'
|
||||||
import { PUBLIC_PB_DOMAIN, PUBLIC_PB_SUBDOMAIN } from '$src/env'
|
import { PUBLIC_PB_DOMAIN, PUBLIC_PB_SUBDOMAIN } from '$src/env'
|
||||||
import { createPocketbaseClient } from './PocketbaseClient'
|
import { createPocketbaseClient, type PocketbaseClientApi } from './PocketbaseClient'
|
||||||
|
|
||||||
const url = `https://${PUBLIC_PB_SUBDOMAIN}.${PUBLIC_PB_DOMAIN}`
|
export const client = (() => {
|
||||||
const client = createPocketbaseClient(url)
|
let clientInstance: PocketbaseClientApi | undefined
|
||||||
|
return () => {
|
||||||
export { client }
|
if (!browser) throw new Error(`PocketBase client not supported in SSR`)
|
||||||
|
if (clientInstance) return clientInstance
|
||||||
|
console.log(`Initializing pocketbase client`)
|
||||||
|
const url = `https://${PUBLIC_PB_SUBDOMAIN}.${PUBLIC_PB_DOMAIN}`
|
||||||
|
clientInstance = createPocketbaseClient(url)
|
||||||
|
return clientInstance
|
||||||
|
}
|
||||||
|
})()
|
||||||
|
@ -1,13 +1,22 @@
|
|||||||
<script>
|
<script>
|
||||||
import Meta from '$components/Meta.svelte'
|
import AuthStateGuard from '$components/helpers/AuthStateGuard.svelte'
|
||||||
|
import Meta from '$components/helpers/Meta.svelte'
|
||||||
|
import Protect from '$components/helpers/Protect.svelte'
|
||||||
|
import ThemeDetector from '$components/helpers/ThemeDetector.svelte'
|
||||||
import Navbar from '$components/Navbar.svelte'
|
import Navbar from '$components/Navbar.svelte'
|
||||||
import ThemeDetector from '$components/ThemeDetector.svelte'
|
import VerifyAccountBar from '$components/VerifyAccountBar.svelte'
|
||||||
</script>
|
</script>
|
||||||
|
|
||||||
<Meta />
|
<Meta />
|
||||||
|
<Protect />
|
||||||
<ThemeDetector />
|
<ThemeDetector />
|
||||||
|
|
||||||
<Navbar />
|
<Navbar />
|
||||||
|
|
||||||
|
<AuthStateGuard>
|
||||||
|
<VerifyAccountBar />
|
||||||
|
</AuthStateGuard>
|
||||||
|
|
||||||
<main data-sveltekit-prefetch>
|
<main data-sveltekit-prefetch>
|
||||||
<slot />
|
<slot />
|
||||||
</main>
|
</main>
|
||||||
|
@ -1,11 +1,10 @@
|
|||||||
<script lang="ts">
|
<script lang="ts">
|
||||||
import FeatureCard from '$components/FeatureCard.svelte'
|
import FeatureCard from '$components/FeatureCard.svelte'
|
||||||
|
import AuthStateGuard from '$components/helpers/AuthStateGuard.svelte'
|
||||||
import HomepageHeroAnimation from '$components/HomepageHeroAnimation.svelte'
|
import HomepageHeroAnimation from '$components/HomepageHeroAnimation.svelte'
|
||||||
import InstanceGeneratorWidget from '$components/InstanceGeneratorWidget.svelte'
|
import InstanceGeneratorWidget from '$components/InstanceGeneratorWidget.svelte'
|
||||||
import { PUBLIC_APP_DOMAIN } from '$src/env'
|
import { PUBLIC_APP_DOMAIN } from '$src/env'
|
||||||
import { client } from '$src/pocketbase'
|
import { isUserLoggedIn } from '$util/stores'
|
||||||
|
|
||||||
const { isLoggedIn } = client
|
|
||||||
</script>
|
</script>
|
||||||
|
|
||||||
<svelte:head>
|
<svelte:head>
|
||||||
@ -22,17 +21,19 @@
|
|||||||
web app.
|
web app.
|
||||||
</p>
|
</p>
|
||||||
|
|
||||||
{#if isLoggedIn()}
|
<AuthStateGuard>
|
||||||
<div>
|
{#if $isUserLoggedIn}
|
||||||
<a href="/dashboard" class="btn btn-primary"
|
<div>
|
||||||
>Go to Your Dashboard <i class="bi bi-arrow-right-short" /></a
|
<a href="/dashboard" class="btn btn-primary"
|
||||||
>
|
>Go to Your Dashboard <i class="bi bi-arrow-right-short" /></a
|
||||||
</div>
|
>
|
||||||
{/if}
|
</div>
|
||||||
|
{/if}
|
||||||
|
|
||||||
{#if !isLoggedIn()}
|
{#if !$isUserLoggedIn}
|
||||||
<InstanceGeneratorWidget />
|
<InstanceGeneratorWidget />
|
||||||
{/if}
|
{/if}
|
||||||
|
</AuthStateGuard>
|
||||||
</div>
|
</div>
|
||||||
|
|
||||||
<div class="col-lg-5 d-none d-sm-block">
|
<div class="col-lg-5 d-none d-sm-block">
|
||||||
|
@ -1,7 +1,6 @@
|
|||||||
<script lang="ts">
|
<script lang="ts">
|
||||||
import { page } from '$app/stores'
|
import { page } from '$app/stores'
|
||||||
import CodeSample from '$components/CodeSample.svelte'
|
import CodeSample from '$components/CodeSample.svelte'
|
||||||
import Protected from '$components/Protected.svelte'
|
|
||||||
import ProvisioningStatus from '$components/ProvisioningStatus.svelte'
|
import ProvisioningStatus from '$components/ProvisioningStatus.svelte'
|
||||||
import { PUBLIC_PB_PROTOCOL } from '$env/static/public'
|
import { PUBLIC_PB_PROTOCOL } from '$env/static/public'
|
||||||
import { PUBLIC_PB_DOMAIN } from '$src/env'
|
import { PUBLIC_PB_DOMAIN } from '$src/env'
|
||||||
@ -15,14 +14,13 @@
|
|||||||
|
|
||||||
let instance: Instance_Out | undefined
|
let instance: Instance_Out | undefined
|
||||||
|
|
||||||
const { watchInstanceById } = client
|
|
||||||
let url: string
|
let url: string
|
||||||
let code: string = ''
|
let code: string = ''
|
||||||
let unsub: Unsubscriber = () => {}
|
let unsub: Unsubscriber = () => {}
|
||||||
|
|
||||||
onMount(() => {
|
onMount(() => {
|
||||||
|
const { watchInstanceById } = client()
|
||||||
unsub = watchInstanceById(instanceId, (r) => {
|
unsub = watchInstanceById(instanceId, (r) => {
|
||||||
console.log(`got a record`, r)
|
|
||||||
instance = r
|
instance = r
|
||||||
assertExists(instance, `Expected instance here`)
|
assertExists(instance, `Expected instance here`)
|
||||||
const { subdomain } = instance
|
const { subdomain } = instance
|
||||||
@ -37,31 +35,29 @@
|
|||||||
<title>Your Instance - PocketHost</title>
|
<title>Your Instance - PocketHost</title>
|
||||||
</svelte:head>
|
</svelte:head>
|
||||||
|
|
||||||
<Protected>
|
<div class="container">
|
||||||
<div class="container">
|
{#if instance}
|
||||||
{#if instance}
|
<div class="py-4">
|
||||||
<div class="py-4">
|
<div class="d-flex gap-3 align-items-center mb-3">
|
||||||
<div class="d-flex gap-3 align-items-center mb-3">
|
<h1 class="mb-0">Admin URL</h1>
|
||||||
<h1 class="mb-0">Admin URL</h1>
|
<ProvisioningStatus status={instance.status} />
|
||||||
<ProvisioningStatus status={instance.status} />
|
|
||||||
</div>
|
|
||||||
|
|
||||||
<h2><a href={`${url}/_`} target="_blank">{`${url}/_`}</a></h2>
|
|
||||||
</div>
|
</div>
|
||||||
|
|
||||||
<div>
|
<h2><a href={`${url}/_`} target="_blank">{`${url}/_`}</a></h2>
|
||||||
JavaScript:
|
|
||||||
<CodeSample {code} />
|
|
||||||
</div>
|
|
||||||
{/if}
|
|
||||||
|
|
||||||
<div class="text-center py-5">
|
|
||||||
<a href="/dashboard" class="btn btn-light"
|
|
||||||
><i class="bi bi-arrow-left-short" /> Back to Dashboard</a
|
|
||||||
>
|
|
||||||
</div>
|
</div>
|
||||||
|
|
||||||
|
<div>
|
||||||
|
JavaScript:
|
||||||
|
<CodeSample {code} />
|
||||||
|
</div>
|
||||||
|
{/if}
|
||||||
|
|
||||||
|
<div class="text-center py-5">
|
||||||
|
<a href="/dashboard" class="btn btn-light"
|
||||||
|
><i class="bi bi-arrow-left-short" /> Back to Dashboard</a
|
||||||
|
>
|
||||||
</div>
|
</div>
|
||||||
</Protected>
|
</div>
|
||||||
|
|
||||||
<style lang="scss">
|
<style lang="scss">
|
||||||
</style>
|
</style>
|
||||||
|
@ -1,6 +1,5 @@
|
|||||||
<script lang="ts">
|
<script lang="ts">
|
||||||
import AlertBar from '$components/AlertBar.svelte'
|
import AlertBar from '$components/AlertBar.svelte'
|
||||||
import Protected from '$components/Protected.svelte'
|
|
||||||
import { PUBLIC_PB_DOMAIN } from '$src/env'
|
import { PUBLIC_PB_DOMAIN } from '$src/env'
|
||||||
import { handleCreateNewInstance } from '$util/database'
|
import { handleCreateNewInstance } from '$util/database'
|
||||||
import { generateSlug } from 'random-word-slugs'
|
import { generateSlug } from 'random-word-slugs'
|
||||||
@ -32,51 +31,49 @@
|
|||||||
<title>New Instance - PocketHost</title>
|
<title>New Instance - PocketHost</title>
|
||||||
</svelte:head>
|
</svelte:head>
|
||||||
|
|
||||||
<Protected>
|
<div class="container">
|
||||||
<div class="container">
|
<div class="py-4">
|
||||||
<div class="py-4">
|
<h1 class="text-center">Choose a name for your PocketBase app.</h1>
|
||||||
<h1 class="text-center">Choose a name for your PocketBase app.</h1>
|
</div>
|
||||||
</div>
|
|
||||||
|
|
||||||
<form on:submit={handleSubmit}>
|
<form on:submit={handleSubmit}>
|
||||||
<div class="row g-3 align-items-center justify-content-center mb-4">
|
<div class="row g-3 align-items-center justify-content-center mb-4">
|
||||||
<div class="col-auto">
|
<div class="col-auto">
|
||||||
<label for="instance-name" class="col-form-label">Instance Name:</label>
|
<label for="instance-name" class="col-form-label">Instance Name:</label>
|
||||||
</div>
|
|
||||||
|
|
||||||
<div class="col-auto pe-1 position-relative">
|
|
||||||
<input type="text" id="instance-name" class="form-control" bind:value={instanceName} />
|
|
||||||
|
|
||||||
<button
|
|
||||||
aria-label="Regenerate Instance Name"
|
|
||||||
type="button"
|
|
||||||
style="transform: rotate({rotationCounter}deg);"
|
|
||||||
class="btn btn-light rounded-circle regenerate-instance-name-btn"
|
|
||||||
on:click={handleInstanceNameRegeneration}
|
|
||||||
>
|
|
||||||
<i class="bi bi-arrow-repeat" />
|
|
||||||
</button>
|
|
||||||
</div>
|
|
||||||
|
|
||||||
<div class="col-auto ps-0">
|
|
||||||
<span class="form-text">.{PUBLIC_PB_DOMAIN}</span>
|
|
||||||
</div>
|
|
||||||
</div>
|
</div>
|
||||||
|
|
||||||
{#if formError}
|
<div class="col-auto pe-1 position-relative">
|
||||||
<AlertBar icon="bi bi-exclamation-triangle-fill" text={formError} />
|
<input type="text" id="instance-name" class="form-control" bind:value={instanceName} />
|
||||||
{/if}
|
|
||||||
|
|
||||||
<div class="text-center">
|
<button
|
||||||
<a href="/dashboard" class="btn btn-light">Cancel</a>
|
aria-label="Regenerate Instance Name"
|
||||||
|
type="button"
|
||||||
<button type="submit" class="btn btn-primary" disabled={isFormButtonDisabled}>
|
style="transform: rotate({rotationCounter}deg);"
|
||||||
Create <i class="bi bi-arrow-right-short" />
|
class="btn btn-light rounded-circle regenerate-instance-name-btn"
|
||||||
|
on:click={handleInstanceNameRegeneration}
|
||||||
|
>
|
||||||
|
<i class="bi bi-arrow-repeat" />
|
||||||
</button>
|
</button>
|
||||||
</div>
|
</div>
|
||||||
</form>
|
|
||||||
</div>
|
<div class="col-auto ps-0">
|
||||||
</Protected>
|
<span class="form-text">.{PUBLIC_PB_DOMAIN}</span>
|
||||||
|
</div>
|
||||||
|
</div>
|
||||||
|
|
||||||
|
{#if formError}
|
||||||
|
<AlertBar icon="bi bi-exclamation-triangle-fill" text={formError} />
|
||||||
|
{/if}
|
||||||
|
|
||||||
|
<div class="text-center">
|
||||||
|
<a href="/dashboard" class="btn btn-light">Cancel</a>
|
||||||
|
|
||||||
|
<button type="submit" class="btn btn-primary" disabled={isFormButtonDisabled}>
|
||||||
|
Create <i class="bi bi-arrow-right-short" />
|
||||||
|
</button>
|
||||||
|
</div>
|
||||||
|
</form>
|
||||||
|
</div>
|
||||||
|
|
||||||
<style lang="scss">
|
<style lang="scss">
|
||||||
.container {
|
.container {
|
||||||
|
@ -1,11 +1,11 @@
|
|||||||
<script lang="ts">
|
<script lang="ts">
|
||||||
import Protected from '$components/Protected.svelte'
|
import AuthStateGuard from '$components/helpers/AuthStateGuard.svelte'
|
||||||
import ProvisioningStatus from '$components/ProvisioningStatus.svelte'
|
import ProvisioningStatus from '$components/ProvisioningStatus.svelte'
|
||||||
import RetroBoxContainer from '$components/RetroBoxContainer.svelte'
|
import RetroBoxContainer from '$components/RetroBoxContainer.svelte'
|
||||||
import { PUBLIC_PB_DOMAIN } from '$src/env'
|
import { PUBLIC_PB_DOMAIN } from '$src/env'
|
||||||
import { client } from '$src/pocketbase'
|
import { client } from '$src/pocketbase'
|
||||||
import { createCleanupManagerSync } from '$util/CleanupManager'
|
import { createCleanupManagerSync } from '$util/CleanupManager'
|
||||||
import type { Instance_Out_ByIdCollection } from '@pockethost/common/src/schema'
|
import type { Instance_Out, Instance_Out_ByIdCollection } from '@pockethost/common/src/schema'
|
||||||
import { forEach, values } from '@s-libs/micro-dash'
|
import { forEach, values } from '@s-libs/micro-dash'
|
||||||
import { onDestroy, onMount } from 'svelte'
|
import { onDestroy, onMount } from 'svelte'
|
||||||
import { fade } from 'svelte/transition'
|
import { fade } from 'svelte/transition'
|
||||||
@ -13,12 +13,15 @@
|
|||||||
// Wait for the instance call to complete before rendering the UI
|
// Wait for the instance call to complete before rendering the UI
|
||||||
let hasPageLoaded = false
|
let hasPageLoaded = false
|
||||||
|
|
||||||
const { getAllInstancesById, watchInstanceById } = client
|
|
||||||
let apps: Instance_Out_ByIdCollection = {}
|
let apps: Instance_Out_ByIdCollection = {}
|
||||||
|
|
||||||
// This will update when the `apps` value changes
|
// This will update when the `apps` value changes
|
||||||
$: isFirstApplication = values(apps).length === 0
|
$: isFirstApplication = values(apps).length === 0
|
||||||
|
|
||||||
|
let appsArray: Instance_Out[]
|
||||||
|
$: {
|
||||||
|
appsArray = values(apps)
|
||||||
|
}
|
||||||
const cm = createCleanupManagerSync()
|
const cm = createCleanupManagerSync()
|
||||||
let _touch = 0 // This is a fake var because without it the watcher callback will not update UI when the apps object changes
|
let _touch = 0 // This is a fake var because without it the watcher callback will not update UI when the apps object changes
|
||||||
const _update = (_apps: Instance_Out_ByIdCollection) => {
|
const _update = (_apps: Instance_Out_ByIdCollection) => {
|
||||||
@ -26,6 +29,7 @@
|
|||||||
_touch++
|
_touch++
|
||||||
}
|
}
|
||||||
onMount(() => {
|
onMount(() => {
|
||||||
|
const { getAllInstancesById, watchInstanceById } = client()
|
||||||
getAllInstancesById()
|
getAllInstancesById()
|
||||||
.then((instances) => {
|
.then((instances) => {
|
||||||
_update(instances)
|
_update(instances)
|
||||||
@ -34,7 +38,6 @@
|
|||||||
const instanceId = app.id
|
const instanceId = app.id
|
||||||
|
|
||||||
const unsub = watchInstanceById(instanceId, (r) => {
|
const unsub = watchInstanceById(instanceId, (r) => {
|
||||||
console.log(`got a record`, r)
|
|
||||||
_update({ ...apps, [r.id]: r })
|
_update({ ...apps, [r.id]: r })
|
||||||
})
|
})
|
||||||
cm.add(unsub)
|
cm.add(unsub)
|
||||||
@ -55,15 +58,15 @@
|
|||||||
<title>Dashboard - PocketHost</title>
|
<title>Dashboard - PocketHost</title>
|
||||||
</svelte:head>
|
</svelte:head>
|
||||||
|
|
||||||
<Protected>
|
<AuthStateGuard>
|
||||||
<div class="container" in:fade={{ duration: 30 }}>
|
<div class="container" in:fade={{ duration: 30 }}>
|
||||||
{#if values(apps).length}
|
{#if appsArray.length}
|
||||||
<div class="py-4">
|
<div class="py-4">
|
||||||
<h1 class="text-center">Your Apps</h1>
|
<h1 class="text-center">Your Apps</h1>
|
||||||
</div>
|
</div>
|
||||||
|
|
||||||
<div class="row justify-content-center">
|
<div class="row justify-content-center">
|
||||||
{#each values(apps) as app}
|
{#each appsArray as app}
|
||||||
<div class="col-xl-4 col-md-6 col-12 mb-5">
|
<div class="col-xl-4 col-md-6 col-12 mb-5">
|
||||||
<div class="card">
|
<div class="card">
|
||||||
<div class="server-status">
|
<div class="server-status">
|
||||||
@ -102,7 +105,7 @@
|
|||||||
</RetroBoxContainer>
|
</RetroBoxContainer>
|
||||||
</div>
|
</div>
|
||||||
</div>
|
</div>
|
||||||
</Protected>
|
</AuthStateGuard>
|
||||||
|
|
||||||
<style lang="scss">
|
<style lang="scss">
|
||||||
.first-app-screen {
|
.first-app-screen {
|
||||||
|
@ -1,15 +1,15 @@
|
|||||||
import { goto } from '$app/navigation'
|
import { goto } from '$app/navigation'
|
||||||
import { client } from '$src/pocketbase'
|
import { client } from '$src/pocketbase'
|
||||||
import { InstanceStatus } from '@pockethost/common'
|
import { InstanceStatus } from '@pockethost/common'
|
||||||
const { authViaEmail, createUser, user, createInstance } = client
|
|
||||||
|
|
||||||
export type FormErrorHandler = (value: string) => void
|
export type FormErrorHandler = (value: string) => void
|
||||||
|
|
||||||
export const handleFormError = (error: any, setError?: FormErrorHandler) => {
|
export const handleFormError = (error: any, setError?: FormErrorHandler) => {
|
||||||
|
const { parseError } = client()
|
||||||
console.error(`Form error: ${error}`, { error })
|
console.error(`Form error: ${error}`, { error })
|
||||||
|
|
||||||
if (setError) {
|
if (setError) {
|
||||||
const message = client.parseError(error)[0]
|
const message = parseError(error)[0]
|
||||||
setError(message)
|
setError(message)
|
||||||
} else {
|
} else {
|
||||||
throw error
|
throw error
|
||||||
@ -29,6 +29,7 @@ export const handleLogin = async (
|
|||||||
setError?: FormErrorHandler,
|
setError?: FormErrorHandler,
|
||||||
shouldRedirect: boolean = true
|
shouldRedirect: boolean = true
|
||||||
) => {
|
) => {
|
||||||
|
const { authViaEmail } = client()
|
||||||
// Reset the form error if the form is submitted
|
// Reset the form error if the form is submitted
|
||||||
setError?.('')
|
setError?.('')
|
||||||
|
|
||||||
@ -36,7 +37,7 @@ export const handleLogin = async (
|
|||||||
await authViaEmail(email, password)
|
await authViaEmail(email, password)
|
||||||
|
|
||||||
if (shouldRedirect) {
|
if (shouldRedirect) {
|
||||||
goto('/dashboard')
|
await goto('/dashboard')
|
||||||
}
|
}
|
||||||
} catch (error: any) {
|
} catch (error: any) {
|
||||||
handleFormError(error, setError)
|
handleFormError(error, setError)
|
||||||
@ -54,6 +55,7 @@ export const handleRegistration = async (
|
|||||||
password: string,
|
password: string,
|
||||||
setError?: FormErrorHandler
|
setError?: FormErrorHandler
|
||||||
) => {
|
) => {
|
||||||
|
const { createUser } = client()
|
||||||
// Reset the form error if the form is submitted
|
// Reset the form error if the form is submitted
|
||||||
setError?.('')
|
setError?.('')
|
||||||
|
|
||||||
@ -68,6 +70,7 @@ export const handleCreateNewInstance = async (
|
|||||||
instanceName: string,
|
instanceName: string,
|
||||||
setError?: FormErrorHandler
|
setError?: FormErrorHandler
|
||||||
) => {
|
) => {
|
||||||
|
const { user, createInstance } = client()
|
||||||
// Get the newly created user id
|
// Get the newly created user id
|
||||||
const { id } = user() || {}
|
const { id } = user() || {}
|
||||||
|
|
||||||
@ -95,6 +98,7 @@ export const handleInstanceGeneratorWidget = async (
|
|||||||
instanceName: string,
|
instanceName: string,
|
||||||
setError = (value: string) => {}
|
setError = (value: string) => {}
|
||||||
) => {
|
) => {
|
||||||
|
const { user, parseError } = client()
|
||||||
try {
|
try {
|
||||||
// Handle user creation/signin
|
// Handle user creation/signin
|
||||||
// First, attempt to log in using the provided credentials.
|
// First, attempt to log in using the provided credentials.
|
||||||
@ -138,7 +142,7 @@ export const handleInstanceGeneratorWidget = async (
|
|||||||
// This means there is something wrong with the user input.
|
// This means there is something wrong with the user input.
|
||||||
// Bail out to show errors
|
// Bail out to show errors
|
||||||
// Transform the errors so they mention a problem with account creation.
|
// Transform the errors so they mention a problem with account creation.
|
||||||
const messages = client.parseError(e)
|
const messages = parseError(e)
|
||||||
throw new Error(`Account creation: ${messages[0]}`)
|
throw new Error(`Account creation: ${messages[0]}`)
|
||||||
})
|
})
|
||||||
})
|
})
|
||||||
@ -161,7 +165,7 @@ export const handleInstanceGeneratorWidget = async (
|
|||||||
}
|
}
|
||||||
// The errors remaining errors are kind of generic, so transofrm them into something about
|
// The errors remaining errors are kind of generic, so transofrm them into something about
|
||||||
// the instance name.
|
// the instance name.
|
||||||
const messages = client.parseError(e)
|
const messages = parseError(e)
|
||||||
throw new Error(`Instance creation: ${messages[0]}`)
|
throw new Error(`Instance creation: ${messages[0]}`)
|
||||||
})
|
})
|
||||||
} catch (error: any) {
|
} catch (error: any) {
|
||||||
@ -169,3 +173,25 @@ export const handleInstanceGeneratorWidget = async (
|
|||||||
handleFormError(error, setError)
|
handleFormError(error, setError)
|
||||||
}
|
}
|
||||||
}
|
}
|
||||||
|
|
||||||
|
export const handleResendVerificationEmail = async (setError = (value: string) => {}) => {
|
||||||
|
const { resendVerificationEmail } = client()
|
||||||
|
try {
|
||||||
|
await resendVerificationEmail()
|
||||||
|
} catch (error: any) {
|
||||||
|
handleFormError(error, setError)
|
||||||
|
}
|
||||||
|
}
|
||||||
|
|
||||||
|
export const handleLogout = () => {
|
||||||
|
const { logOut } = client()
|
||||||
|
// Clear the Pocketbase session
|
||||||
|
logOut()
|
||||||
|
}
|
||||||
|
|
||||||
|
export const handleLogoutAndRedirect = () => {
|
||||||
|
handleLogout()
|
||||||
|
|
||||||
|
// Hard refresh to make sure any remaining data is cleared
|
||||||
|
window.location.href = '/'
|
||||||
|
}
|
||||||
|
50
packages/pockethost.io/src/util/events.ts
Normal file
50
packages/pockethost.io/src/util/events.ts
Normal file
@ -0,0 +1,50 @@
|
|||||||
|
import { forEach, reduce } from '@s-libs/micro-dash'
|
||||||
|
|
||||||
|
export type Unsubscribe = () => void
|
||||||
|
|
||||||
|
export const createGenericAsyncEvent = <TPayload>(): [
|
||||||
|
(cb: (payload: TPayload) => Promise<void>) => Unsubscribe,
|
||||||
|
(payload: TPayload) => Promise<void>
|
||||||
|
] => {
|
||||||
|
let i = 0
|
||||||
|
const callbacks: any = {}
|
||||||
|
const onEvent = (cb: (payload: TPayload) => Promise<void>) => {
|
||||||
|
const id = i++
|
||||||
|
callbacks[id] = cb
|
||||||
|
return () => {
|
||||||
|
delete callbacks[id]
|
||||||
|
}
|
||||||
|
}
|
||||||
|
|
||||||
|
const fireEvent = (payload: TPayload) =>
|
||||||
|
reduce(
|
||||||
|
callbacks,
|
||||||
|
(c, cb) => {
|
||||||
|
return c.then(cb(payload))
|
||||||
|
},
|
||||||
|
Promise.resolve()
|
||||||
|
)
|
||||||
|
|
||||||
|
return [onEvent, fireEvent]
|
||||||
|
}
|
||||||
|
|
||||||
|
export const createGenericSyncEvent = <TPayload>(): [
|
||||||
|
(cb: (payload: TPayload) => void) => Unsubscribe,
|
||||||
|
(payload: TPayload) => void
|
||||||
|
] => {
|
||||||
|
let i = 0
|
||||||
|
const callbacks: any = {}
|
||||||
|
const onEvent = (cb: (payload: TPayload) => void) => {
|
||||||
|
const id = i++
|
||||||
|
callbacks[id] = cb
|
||||||
|
return () => {
|
||||||
|
delete callbacks[id]
|
||||||
|
}
|
||||||
|
}
|
||||||
|
|
||||||
|
const fireEvent = (payload: TPayload) => {
|
||||||
|
forEach(callbacks, (cb) => cb(payload))
|
||||||
|
}
|
||||||
|
|
||||||
|
return [onEvent, fireEvent]
|
||||||
|
}
|
1
packages/pockethost.io/src/util/public-routes.json
Normal file
1
packages/pockethost.io/src/util/public-routes.json
Normal file
@ -0,0 +1 @@
|
|||||||
|
["/", "/signup", "/login", "/faq"]
|
16
packages/pockethost.io/src/util/safeCatch.ts
Normal file
16
packages/pockethost.io/src/util/safeCatch.ts
Normal file
@ -0,0 +1,16 @@
|
|||||||
|
import { dev } from '$app/environment'
|
||||||
|
|
||||||
|
export const safeCatch = <TIn extends any[], TOut>(
|
||||||
|
name: string,
|
||||||
|
cb: (...args: TIn) => Promise<TOut>
|
||||||
|
) => {
|
||||||
|
return (...args: TIn) => {
|
||||||
|
if (dev) {
|
||||||
|
console.log(`${name}`)
|
||||||
|
}
|
||||||
|
return cb(...args).catch((e: any) => {
|
||||||
|
console.error(`${name} failed: ${e}`)
|
||||||
|
throw e
|
||||||
|
})
|
||||||
|
}
|
||||||
|
}
|
@ -1,3 +1,29 @@
|
|||||||
|
import { browser } from '$app/environment'
|
||||||
|
import { client } from '$src/pocketbase'
|
||||||
|
import type { AuthStoreProps } from '$src/pocketbase/PocketbaseClient'
|
||||||
import { writable } from 'svelte/store'
|
import { writable } from 'svelte/store'
|
||||||
|
|
||||||
const instanceCreationWidgetName = writable(0)
|
export const authStoreState = writable<AuthStoreProps>({ isValid: false, model: null, token: '' })
|
||||||
|
export const isUserLoggedIn = writable(false)
|
||||||
|
export const isUserVerified = writable(false)
|
||||||
|
export const isAuthStateInitialized = writable(false)
|
||||||
|
|
||||||
|
if (browser) {
|
||||||
|
const { onAuthChange } = client()
|
||||||
|
|
||||||
|
/**
|
||||||
|
* Listen for auth change events. When we get at least one, the auth state is initialized.
|
||||||
|
*/
|
||||||
|
onAuthChange((authStoreProps) => {
|
||||||
|
console.log(`onAuthChange in store`, { ...authStoreProps })
|
||||||
|
authStoreState.set(authStoreProps)
|
||||||
|
isAuthStateInitialized.set(true)
|
||||||
|
})
|
||||||
|
|
||||||
|
// Update derived stores when authStore changes
|
||||||
|
authStoreState.subscribe((authStoreProps) => {
|
||||||
|
console.log(`subscriber change`, authStoreProps)
|
||||||
|
isUserLoggedIn.set(authStoreProps.isValid)
|
||||||
|
isUserVerified.set(!!authStoreProps.model?.verified)
|
||||||
|
})
|
||||||
|
}
|
||||||
|
@ -1,3 +1,12 @@
|
|||||||
|
import { page as pageStore } from '$app/stores'
|
||||||
|
import { get } from 'svelte/store'
|
||||||
|
|
||||||
export const getRandomElementFromArray = (array: string[]) => {
|
export const getRandomElementFromArray = (array: string[]) => {
|
||||||
return array[Math.floor(Math.random() * array.length)]
|
return array[Math.floor(Math.random() * array.length)]
|
||||||
}
|
}
|
||||||
|
|
||||||
|
// This returns an object with the current URL information
|
||||||
|
export const getRouter = () => {
|
||||||
|
const router = get(pageStore)
|
||||||
|
return router.url
|
||||||
|
}
|
||||||
|
17
readme.md
17
readme.md
@ -94,7 +94,8 @@ cd pockethost/docker
|
|||||||
cp .env-template-dev .env.local # Edit as needed - defaults should work
|
cp .env-template-dev .env.local # Edit as needed - defaults should work
|
||||||
cd ..
|
cd ..
|
||||||
docker-compose -f docker/docker-compose-dev.yaml build
|
docker-compose -f docker/docker-compose-dev.yaml build
|
||||||
docker-compose -f docker/docker-compose-dev.yaml up
|
docker-compose -f docker/docker-compose-dev.yaml --profile=build up --remove-orphans
|
||||||
|
docker-compose -f docker/docker-compose-dev.yaml --profile=serve up --remove-orphans
|
||||||
open https://pockethost.test
|
open https://pockethost.test
|
||||||
```
|
```
|
||||||
|
|
||||||
@ -129,6 +130,20 @@ open https://pockethost.io
|
|||||||
|
|
||||||
# Release History
|
# Release History
|
||||||
|
|
||||||
|
**next**
|
||||||
|
|
||||||
|
- [ ]
|
||||||
|
|
||||||
|
**0.3.2**
|
||||||
|
|
||||||
|
- [x] Migrated PBScript repository to here
|
||||||
|
- [x] Accounts must now be verified before running an instance
|
||||||
|
|
||||||
|
**0.3.1**
|
||||||
|
|
||||||
|
- [x] OpenGraph support
|
||||||
|
- [x] Darkmode enhancements
|
||||||
|
|
||||||
**0.3.0**
|
**0.3.0**
|
||||||
|
|
||||||
- [x] Improved realtime support in proxy
|
- [x] Improved realtime support in proxy
|
||||||
|
155
yarn.lock
155
yarn.lock
@ -917,6 +917,11 @@
|
|||||||
resolved "https://registry.yarnpkg.com/@types/estree/-/estree-1.0.0.tgz#5fb2e536c1ae9bf35366eed879e827fa59ca41c2"
|
resolved "https://registry.yarnpkg.com/@types/estree/-/estree-1.0.0.tgz#5fb2e536c1ae9bf35366eed879e827fa59ca41c2"
|
||||||
integrity sha512-WulqXMDUTYAXCjZnk6JtIHPigp55cVtDgDrO2gHRwhyJto21+1zbVCtOYB2L1F9w4qCQ0rOGWBnBe0FNTiEJIQ==
|
integrity sha512-WulqXMDUTYAXCjZnk6JtIHPigp55cVtDgDrO2gHRwhyJto21+1zbVCtOYB2L1F9w4qCQ0rOGWBnBe0FNTiEJIQ==
|
||||||
|
|
||||||
|
"@types/eventsource@^1.1.9":
|
||||||
|
version "1.1.10"
|
||||||
|
resolved "https://registry.yarnpkg.com/@types/eventsource/-/eventsource-1.1.10.tgz#e085be22c913e893f83f5ace21e9c10f87cbf6d2"
|
||||||
|
integrity sha512-rYzRmJSnm44Xb7FICRXEjwe/26ZiiS+VMGmuD17PevMP56cGgLEsaM955sYQW0S+K7h+mPOL70vGf1hi4WDjVA==
|
||||||
|
|
||||||
"@types/http-proxy@^1.17.9":
|
"@types/http-proxy@^1.17.9":
|
||||||
version "1.17.9"
|
version "1.17.9"
|
||||||
resolved "https://registry.yarnpkg.com/@types/http-proxy/-/http-proxy-1.17.9.tgz#7f0e7931343761efde1e2bf48c40f02f3f75705a"
|
resolved "https://registry.yarnpkg.com/@types/http-proxy/-/http-proxy-1.17.9.tgz#7f0e7931343761efde1e2bf48c40f02f3f75705a"
|
||||||
@ -934,11 +939,23 @@
|
|||||||
resolved "https://registry.yarnpkg.com/@types/node/-/node-18.8.5.tgz#6a31f820c1077c3f8ce44f9e203e68a176e8f59e"
|
resolved "https://registry.yarnpkg.com/@types/node/-/node-18.8.5.tgz#6a31f820c1077c3f8ce44f9e203e68a176e8f59e"
|
||||||
integrity sha512-Bq7G3AErwe5A/Zki5fdD3O6+0zDChhg671NfPjtIcbtzDNZTv4NPKMRFr7gtYPG7y+B8uTiNK4Ngd9T0FTar6Q==
|
integrity sha512-Bq7G3AErwe5A/Zki5fdD3O6+0zDChhg671NfPjtIcbtzDNZTv4NPKMRFr7gtYPG7y+B8uTiNK4Ngd9T0FTar6Q==
|
||||||
|
|
||||||
|
"@types/node@^18.8.2":
|
||||||
|
version "18.11.9"
|
||||||
|
resolved "https://registry.yarnpkg.com/@types/node/-/node-18.11.9.tgz#02d013de7058cea16d36168ef2fc653464cfbad4"
|
||||||
|
integrity sha512-CRpX21/kGdzjOpFsZSkcrXMGIBWMGNIHXXBVFSH+ggkftxg+XYP20TESbh+zFvFj3EQOl5byk0HTRn1IL6hbqg==
|
||||||
|
|
||||||
"@types/parse-json@^4.0.0":
|
"@types/parse-json@^4.0.0":
|
||||||
version "4.0.0"
|
version "4.0.0"
|
||||||
resolved "https://registry.yarnpkg.com/@types/parse-json/-/parse-json-4.0.0.tgz#2f8bb441434d163b35fb8ffdccd7138927ffb8c0"
|
resolved "https://registry.yarnpkg.com/@types/parse-json/-/parse-json-4.0.0.tgz#2f8bb441434d163b35fb8ffdccd7138927ffb8c0"
|
||||||
integrity sha512-//oorEZjL6sbPcKUaCdIGlIUeH26mgzimjBB77G6XRgnDl/L5wOnpyBGRe/Mmf5CVW3PwEBE1NjiMZ/ssFh4wA==
|
integrity sha512-//oorEZjL6sbPcKUaCdIGlIUeH26mgzimjBB77G6XRgnDl/L5wOnpyBGRe/Mmf5CVW3PwEBE1NjiMZ/ssFh4wA==
|
||||||
|
|
||||||
|
"@types/prompts@^2.4.1":
|
||||||
|
version "2.4.1"
|
||||||
|
resolved "https://registry.yarnpkg.com/@types/prompts/-/prompts-2.4.1.tgz#d47adcb608a0afcd48121ff7c75244694a3a04c5"
|
||||||
|
integrity sha512-1Mqzhzi9W5KlooNE4o0JwSXGUDeQXKldbGn9NO4tpxwZbHXYd+WcKpCksG2lbhH7U9I9LigfsdVsP2QAY0lNPA==
|
||||||
|
dependencies:
|
||||||
|
"@types/node" "*"
|
||||||
|
|
||||||
"@types/pug@^2.0.4":
|
"@types/pug@^2.0.4":
|
||||||
version "2.0.6"
|
version "2.0.6"
|
||||||
resolved "https://registry.yarnpkg.com/@types/pug/-/pug-2.0.6.tgz#f830323c88172e66826d0bde413498b61054b5a6"
|
resolved "https://registry.yarnpkg.com/@types/pug/-/pug-2.0.6.tgz#f830323c88172e66826d0bde413498b61054b5a6"
|
||||||
@ -956,6 +973,11 @@
|
|||||||
dependencies:
|
dependencies:
|
||||||
"@types/node" "*"
|
"@types/node" "*"
|
||||||
|
|
||||||
|
"@types/tmp@^0.2.3":
|
||||||
|
version "0.2.3"
|
||||||
|
resolved "https://registry.yarnpkg.com/@types/tmp/-/tmp-0.2.3.tgz#908bfb113419fd6a42273674c00994d40902c165"
|
||||||
|
integrity sha512-dDZH/tXzwjutnuk4UacGgFRwV+JSLaXL1ikvidfJprkb7L9Nx1njcRHHmi3Dsvt7pgqqTEeucQuOrWHPFgzVHA==
|
||||||
|
|
||||||
abbrev@1:
|
abbrev@1:
|
||||||
version "1.1.1"
|
version "1.1.1"
|
||||||
resolved "https://registry.yarnpkg.com/abbrev/-/abbrev-1.1.1.tgz#f8f2c887ad10bf67f634f005b6987fed3179aac8"
|
resolved "https://registry.yarnpkg.com/abbrev/-/abbrev-1.1.1.tgz#f8f2c887ad10bf67f634f005b6987fed3179aac8"
|
||||||
@ -1213,6 +1235,11 @@ commander@^7.0.0, commander@^7.2.0:
|
|||||||
resolved "https://registry.yarnpkg.com/commander/-/commander-7.2.0.tgz#a36cb57d0b501ce108e4d20559a150a391d97ab7"
|
resolved "https://registry.yarnpkg.com/commander/-/commander-7.2.0.tgz#a36cb57d0b501ce108e4d20559a150a391d97ab7"
|
||||||
integrity sha512-QrWXB+ZQSVPmIWIhtEO9H+gwHaMGYiF5ChvoJ+K9ZGHG/sVsa6yiesAD1GC/x46sET00Xlwo1u49RVVVzvcSkw==
|
integrity sha512-QrWXB+ZQSVPmIWIhtEO9H+gwHaMGYiF5ChvoJ+K9ZGHG/sVsa6yiesAD1GC/x46sET00Xlwo1u49RVVVzvcSkw==
|
||||||
|
|
||||||
|
commander@^9.4.0:
|
||||||
|
version "9.4.1"
|
||||||
|
resolved "https://registry.yarnpkg.com/commander/-/commander-9.4.1.tgz#d1dd8f2ce6faf93147295c0df13c7c21141cfbdd"
|
||||||
|
integrity sha512-5EEkTNyHNGFPD2H+c/dXXfQZYa/scCKasxWcXJaWnNJ99pnQN9Vnmqow+p+PlFPE63Q6mThaZws1T+HxfpgtPw==
|
||||||
|
|
||||||
commondir@^1.0.1:
|
commondir@^1.0.1:
|
||||||
version "1.0.1"
|
version "1.0.1"
|
||||||
resolved "https://registry.yarnpkg.com/commondir/-/commondir-1.0.1.tgz#ddd800da0c66127393cca5950ea968a3aaf1253b"
|
resolved "https://registry.yarnpkg.com/commondir/-/commondir-1.0.1.tgz#ddd800da0c66127393cca5950ea968a3aaf1253b"
|
||||||
@ -1259,6 +1286,13 @@ create-require@^1.1.0:
|
|||||||
resolved "https://registry.yarnpkg.com/create-require/-/create-require-1.1.1.tgz#c1d7e8f1e5f6cfc9ff65f9cd352d37348756c333"
|
resolved "https://registry.yarnpkg.com/create-require/-/create-require-1.1.1.tgz#c1d7e8f1e5f6cfc9ff65f9cd352d37348756c333"
|
||||||
integrity sha512-dcKFX3jn0MpIaXjisoRvexIJVEKzaq7z2rZKxf+MSr9TkdmHmsU4m2lcLojrj/FHl8mk5VxMmYA+ftRkP/3oKQ==
|
integrity sha512-dcKFX3jn0MpIaXjisoRvexIJVEKzaq7z2rZKxf+MSr9TkdmHmsU4m2lcLojrj/FHl8mk5VxMmYA+ftRkP/3oKQ==
|
||||||
|
|
||||||
|
cross-fetch@^3.1.5:
|
||||||
|
version "3.1.5"
|
||||||
|
resolved "https://registry.yarnpkg.com/cross-fetch/-/cross-fetch-3.1.5.tgz#e1389f44d9e7ba767907f7af8454787952ab534f"
|
||||||
|
integrity sha512-lvb1SBsI0Z7GDwmuid+mU3kWVBwTVUbe7S0H52yaaAdQOXq2YktTCZdlAcNKFzE6QtRz0snpw9bNiPeOIkkQvw==
|
||||||
|
dependencies:
|
||||||
|
node-fetch "2.6.7"
|
||||||
|
|
||||||
css-select@^4.1.3:
|
css-select@^4.1.3:
|
||||||
version "4.3.0"
|
version "4.3.0"
|
||||||
resolved "https://registry.yarnpkg.com/css-select/-/css-select-4.3.0.tgz#db7129b2846662fd8628cfc496abb2b59e41529b"
|
resolved "https://registry.yarnpkg.com/css-select/-/css-select-4.3.0.tgz#db7129b2846662fd8628cfc496abb2b59e41529b"
|
||||||
@ -1702,6 +1736,11 @@ eventemitter3@^4.0.0:
|
|||||||
resolved "https://registry.yarnpkg.com/eventemitter3/-/eventemitter3-4.0.7.tgz#2de9b68f6528d5644ef5c59526a1b4a07306169f"
|
resolved "https://registry.yarnpkg.com/eventemitter3/-/eventemitter3-4.0.7.tgz#2de9b68f6528d5644ef5c59526a1b4a07306169f"
|
||||||
integrity sha512-8guHBZCwKnFhYdHr2ysuRWErTwhoN2X8XELRlrRwpmfeY2jjuUN4taQMsULKUVo1K4DvZl+0pgfyoysHxvmvEw==
|
integrity sha512-8guHBZCwKnFhYdHr2ysuRWErTwhoN2X8XELRlrRwpmfeY2jjuUN4taQMsULKUVo1K4DvZl+0pgfyoysHxvmvEw==
|
||||||
|
|
||||||
|
eventsource@^2.0.2:
|
||||||
|
version "2.0.2"
|
||||||
|
resolved "https://registry.yarnpkg.com/eventsource/-/eventsource-2.0.2.tgz#76dfcc02930fb2ff339520b6d290da573a9e8508"
|
||||||
|
integrity sha512-IzUmBGPR3+oUG9dUeXynyNmf91/3zUSJg1lCktzKw47OXuhco54U3r9B7O4XX+Rb1Itm9OZ2b0RkTs10bICOxA==
|
||||||
|
|
||||||
fast-glob@^3.2.7:
|
fast-glob@^3.2.7:
|
||||||
version "3.2.12"
|
version "3.2.12"
|
||||||
resolved "https://registry.yarnpkg.com/fast-glob/-/fast-glob-3.2.12.tgz#7f39ec99c2e6ab030337142da9e0c18f37afae80"
|
resolved "https://registry.yarnpkg.com/fast-glob/-/fast-glob-3.2.12.tgz#7f39ec99c2e6ab030337142da9e0c18f37afae80"
|
||||||
@ -1742,6 +1781,14 @@ find-up@^3.0.0:
|
|||||||
dependencies:
|
dependencies:
|
||||||
locate-path "^3.0.0"
|
locate-path "^3.0.0"
|
||||||
|
|
||||||
|
find-up@^6.3.0:
|
||||||
|
version "6.3.0"
|
||||||
|
resolved "https://registry.yarnpkg.com/find-up/-/find-up-6.3.0.tgz#2abab3d3280b2dc7ac10199ef324c4e002c8c790"
|
||||||
|
integrity sha512-v2ZsoEuVHYy8ZIlYqwPe/39Cy+cFDzp4dXPaxNvkEuouymu+2Jbz0PxpKarJHYJTmv2HWT3O382qY8l4jMWthw==
|
||||||
|
dependencies:
|
||||||
|
locate-path "^7.1.0"
|
||||||
|
path-exists "^5.0.0"
|
||||||
|
|
||||||
follow-redirects@^1.0.0:
|
follow-redirects@^1.0.0:
|
||||||
version "1.15.2"
|
version "1.15.2"
|
||||||
resolved "https://registry.yarnpkg.com/follow-redirects/-/follow-redirects-1.15.2.tgz#b460864144ba63f2681096f274c4e57026da2c13"
|
resolved "https://registry.yarnpkg.com/follow-redirects/-/follow-redirects-1.15.2.tgz#b460864144ba63f2681096f274c4e57026da2c13"
|
||||||
@ -2007,6 +2054,11 @@ json5@^2.2.0, json5@^2.2.1:
|
|||||||
resolved "https://registry.yarnpkg.com/json5/-/json5-2.2.1.tgz#655d50ed1e6f95ad1a3caababd2b0efda10b395c"
|
resolved "https://registry.yarnpkg.com/json5/-/json5-2.2.1.tgz#655d50ed1e6f95ad1a3caababd2b0efda10b395c"
|
||||||
integrity sha512-1hqLFMSrGHRHxav9q9gNjJ5EXznIxGVO09xQRrwplcS8qs28pZ8s8hupZAmqDwZUmVZ2Qb2jnyPOWcDH8m8dlA==
|
integrity sha512-1hqLFMSrGHRHxav9q9gNjJ5EXznIxGVO09xQRrwplcS8qs28pZ8s8hupZAmqDwZUmVZ2Qb2jnyPOWcDH8m8dlA==
|
||||||
|
|
||||||
|
kleur@^3.0.3:
|
||||||
|
version "3.0.3"
|
||||||
|
resolved "https://registry.yarnpkg.com/kleur/-/kleur-3.0.3.tgz#a79c9ecc86ee1ce3fa6206d1216c501f147fc07e"
|
||||||
|
integrity sha512-eTIzlVOSUR+JxdDFepEYcBMtZ9Qqdef+rnzWdRZuMbOywu5tO2w2N7rqjoANZ5k9vywhL6Br1VRjUIgTQx4E8w==
|
||||||
|
|
||||||
kleur@^4.1.4, kleur@^4.1.5:
|
kleur@^4.1.4, kleur@^4.1.5:
|
||||||
version "4.1.5"
|
version "4.1.5"
|
||||||
resolved "https://registry.yarnpkg.com/kleur/-/kleur-4.1.5.tgz#95106101795f7050c6c650f350c683febddb1780"
|
resolved "https://registry.yarnpkg.com/kleur/-/kleur-4.1.5.tgz#95106101795f7050c6c650f350c683febddb1780"
|
||||||
@ -2099,6 +2151,13 @@ locate-path@^3.0.0:
|
|||||||
p-locate "^3.0.0"
|
p-locate "^3.0.0"
|
||||||
path-exists "^3.0.0"
|
path-exists "^3.0.0"
|
||||||
|
|
||||||
|
locate-path@^7.1.0:
|
||||||
|
version "7.1.1"
|
||||||
|
resolved "https://registry.yarnpkg.com/locate-path/-/locate-path-7.1.1.tgz#8e1e5a75c7343770cef02ff93c4bf1f0aa666374"
|
||||||
|
integrity sha512-vJXaRMJgRVD3+cUZs3Mncj2mxpt5mP0EmNOsxRSZRMlbqjvxzDEOIUWXGmavo0ZC9+tNZCBLQ66reA11nbpHZg==
|
||||||
|
dependencies:
|
||||||
|
p-locate "^6.0.0"
|
||||||
|
|
||||||
lodash.debounce@^4.0.8:
|
lodash.debounce@^4.0.8:
|
||||||
version "4.0.8"
|
version "4.0.8"
|
||||||
resolved "https://registry.yarnpkg.com/lodash.debounce/-/lodash.debounce-4.0.8.tgz#82d79bff30a67c4005ffd5e2515300ad9ca4d7af"
|
resolved "https://registry.yarnpkg.com/lodash.debounce/-/lodash.debounce-4.0.8.tgz#82d79bff30a67c4005ffd5e2515300ad9ca4d7af"
|
||||||
@ -2228,6 +2287,11 @@ msgpackr@^1.5.4:
|
|||||||
optionalDependencies:
|
optionalDependencies:
|
||||||
msgpackr-extract "^2.1.2"
|
msgpackr-extract "^2.1.2"
|
||||||
|
|
||||||
|
nanoevents@^7.0.1:
|
||||||
|
version "7.0.1"
|
||||||
|
resolved "https://registry.yarnpkg.com/nanoevents/-/nanoevents-7.0.1.tgz#181580b47787688d8cac775b977b1cf24e26e570"
|
||||||
|
integrity sha512-o6lpKiCxLeijK4hgsqfR6CNToPyRU3keKyyI6uwuHRvpRTbZ0wXw51WRgyldVugZqoJfkGFrjrIenYH3bfEO3Q==
|
||||||
|
|
||||||
nanoid@^3.3.4:
|
nanoid@^3.3.4:
|
||||||
version "3.3.4"
|
version "3.3.4"
|
||||||
resolved "https://registry.yarnpkg.com/nanoid/-/nanoid-3.3.4.tgz#730b67e3cd09e2deacf03c027c81c9d9dbc5e8ab"
|
resolved "https://registry.yarnpkg.com/nanoid/-/nanoid-3.3.4.tgz#730b67e3cd09e2deacf03c027c81c9d9dbc5e8ab"
|
||||||
@ -2253,6 +2317,13 @@ node-domexception@^1.0.0:
|
|||||||
resolved "https://registry.yarnpkg.com/node-domexception/-/node-domexception-1.0.0.tgz#6888db46a1f71c0b76b3f7555016b63fe64766e5"
|
resolved "https://registry.yarnpkg.com/node-domexception/-/node-domexception-1.0.0.tgz#6888db46a1f71c0b76b3f7555016b63fe64766e5"
|
||||||
integrity sha512-/jKZoMpw0F8GRwl4/eLROPA3cfcXtLApP0QzLmUT/HuPCZWyB7IY9ZrMeKw2O/nFIqPQB3PVM9aYm0F312AXDQ==
|
integrity sha512-/jKZoMpw0F8GRwl4/eLROPA3cfcXtLApP0QzLmUT/HuPCZWyB7IY9ZrMeKw2O/nFIqPQB3PVM9aYm0F312AXDQ==
|
||||||
|
|
||||||
|
node-fetch@2.6.7:
|
||||||
|
version "2.6.7"
|
||||||
|
resolved "https://registry.yarnpkg.com/node-fetch/-/node-fetch-2.6.7.tgz#24de9fba827e3b4ae44dc8b20256a379160052ad"
|
||||||
|
integrity sha512-ZjMPFEfVx5j+y2yF35Kzx5sF7kDzxuDj6ziH4FFbOp87zKDZNx8yExJIb05OGF4Nlt9IHFIMBkRl41VdvcNdbQ==
|
||||||
|
dependencies:
|
||||||
|
whatwg-url "^5.0.0"
|
||||||
|
|
||||||
node-fetch@^3.2.10:
|
node-fetch@^3.2.10:
|
||||||
version "3.2.10"
|
version "3.2.10"
|
||||||
resolved "https://registry.yarnpkg.com/node-fetch/-/node-fetch-3.2.10.tgz#e8347f94b54ae18b57c9c049ef641cef398a85c8"
|
resolved "https://registry.yarnpkg.com/node-fetch/-/node-fetch-3.2.10.tgz#e8347f94b54ae18b57c9c049ef641cef398a85c8"
|
||||||
@ -2336,6 +2407,13 @@ p-limit@^2.0.0:
|
|||||||
dependencies:
|
dependencies:
|
||||||
p-try "^2.0.0"
|
p-try "^2.0.0"
|
||||||
|
|
||||||
|
p-limit@^4.0.0:
|
||||||
|
version "4.0.0"
|
||||||
|
resolved "https://registry.yarnpkg.com/p-limit/-/p-limit-4.0.0.tgz#914af6544ed32bfa54670b061cafcbd04984b644"
|
||||||
|
integrity sha512-5b0R4txpzjPWVw/cXXUResoD4hb6U/x9BH08L7nw+GN1sezDzPdxeRvpc9c433fZhBan/wusjbCsqwqm4EIBIQ==
|
||||||
|
dependencies:
|
||||||
|
yocto-queue "^1.0.0"
|
||||||
|
|
||||||
p-locate@^3.0.0:
|
p-locate@^3.0.0:
|
||||||
version "3.0.0"
|
version "3.0.0"
|
||||||
resolved "https://registry.yarnpkg.com/p-locate/-/p-locate-3.0.0.tgz#322d69a05c0264b25997d9f40cd8a891ab0064a4"
|
resolved "https://registry.yarnpkg.com/p-locate/-/p-locate-3.0.0.tgz#322d69a05c0264b25997d9f40cd8a891ab0064a4"
|
||||||
@ -2343,6 +2421,13 @@ p-locate@^3.0.0:
|
|||||||
dependencies:
|
dependencies:
|
||||||
p-limit "^2.0.0"
|
p-limit "^2.0.0"
|
||||||
|
|
||||||
|
p-locate@^6.0.0:
|
||||||
|
version "6.0.0"
|
||||||
|
resolved "https://registry.yarnpkg.com/p-locate/-/p-locate-6.0.0.tgz#3da9a49d4934b901089dca3302fa65dc5a05c04f"
|
||||||
|
integrity sha512-wPrq66Llhl7/4AGC6I+cqxT07LhXvWL08LNXz1fENOw0Ap4sRZZ/gZpTTJ5jpurzzzfS2W/Ge9BY3LgLjCShcw==
|
||||||
|
dependencies:
|
||||||
|
p-limit "^4.0.0"
|
||||||
|
|
||||||
p-try@^2.0.0:
|
p-try@^2.0.0:
|
||||||
version "2.2.0"
|
version "2.2.0"
|
||||||
resolved "https://registry.yarnpkg.com/p-try/-/p-try-2.2.0.tgz#cb2868540e313d61de58fafbe35ce9004d5540e6"
|
resolved "https://registry.yarnpkg.com/p-try/-/p-try-2.2.0.tgz#cb2868540e313d61de58fafbe35ce9004d5540e6"
|
||||||
@ -2390,6 +2475,11 @@ path-exists@^3.0.0:
|
|||||||
resolved "https://registry.yarnpkg.com/path-exists/-/path-exists-3.0.0.tgz#ce0ebeaa5f78cb18925ea7d810d7b59b010fd515"
|
resolved "https://registry.yarnpkg.com/path-exists/-/path-exists-3.0.0.tgz#ce0ebeaa5f78cb18925ea7d810d7b59b010fd515"
|
||||||
integrity sha512-bpC7GYwiDYQ4wYLe+FA8lhRjhQCMcQGuSgGGqDkg/QerRWw9CmGRT0iSOVRSZJ29NMLZgIzqaljJ63oaL4NIJQ==
|
integrity sha512-bpC7GYwiDYQ4wYLe+FA8lhRjhQCMcQGuSgGGqDkg/QerRWw9CmGRT0iSOVRSZJ29NMLZgIzqaljJ63oaL4NIJQ==
|
||||||
|
|
||||||
|
path-exists@^5.0.0:
|
||||||
|
version "5.0.0"
|
||||||
|
resolved "https://registry.yarnpkg.com/path-exists/-/path-exists-5.0.0.tgz#a6aad9489200b21fab31e49cf09277e5116fb9e7"
|
||||||
|
integrity sha512-RjhtfwJOxzcFmNOi6ltcbcu4Iu+FL3zEj83dk4kAS+fVpTxXLO1b38RvJgT/0QwvV/L3aY9TAnyv0EOqW4GoMQ==
|
||||||
|
|
||||||
path-is-absolute@^1.0.0:
|
path-is-absolute@^1.0.0:
|
||||||
version "1.0.1"
|
version "1.0.1"
|
||||||
resolved "https://registry.yarnpkg.com/path-is-absolute/-/path-is-absolute-1.0.1.tgz#174b9268735534ffbc7ace6bf53a5a9e1b5c5f5f"
|
resolved "https://registry.yarnpkg.com/path-is-absolute/-/path-is-absolute-1.0.1.tgz#174b9268735534ffbc7ace6bf53a5a9e1b5c5f5f"
|
||||||
@ -2420,6 +2510,11 @@ pocketbase@^0.7.0:
|
|||||||
resolved "https://registry.yarnpkg.com/pocketbase/-/pocketbase-0.7.1.tgz#fdff7e9c6b23f3543fa14b412a496279329ce12d"
|
resolved "https://registry.yarnpkg.com/pocketbase/-/pocketbase-0.7.1.tgz#fdff7e9c6b23f3543fa14b412a496279329ce12d"
|
||||||
integrity sha512-uA/Ltci3OrZ/sYfOjB/SCQX87xnB0Gowci0eUNgK7Sb0gk68Yy79ynTKgcmrw6wdXXVrY/vOoNR/DU8oahCBAA==
|
integrity sha512-uA/Ltci3OrZ/sYfOjB/SCQX87xnB0Gowci0eUNgK7Sb0gk68Yy79ynTKgcmrw6wdXXVrY/vOoNR/DU8oahCBAA==
|
||||||
|
|
||||||
|
pocketbase@^0.7.1:
|
||||||
|
version "0.7.4"
|
||||||
|
resolved "https://registry.yarnpkg.com/pocketbase/-/pocketbase-0.7.4.tgz#2ca1fe03215ec278ae7239ef63befb4c002d9ad9"
|
||||||
|
integrity sha512-PvBRi4hbgbiBwDjhHa9lGD/ala8dSTjKeNAsHAgsXdIo4v9RgCk2s3Zqd/4UXVBgTJHVM6F7fGOZPvvJfSNVLQ==
|
||||||
|
|
||||||
postcss-value-parser@^4.2.0:
|
postcss-value-parser@^4.2.0:
|
||||||
version "4.2.0"
|
version "4.2.0"
|
||||||
resolved "https://registry.yarnpkg.com/postcss-value-parser/-/postcss-value-parser-4.2.0.tgz#723c09920836ba6d3e5af019f92bc0971c02e514"
|
resolved "https://registry.yarnpkg.com/postcss-value-parser/-/postcss-value-parser-4.2.0.tgz#723c09920836ba6d3e5af019f92bc0971c02e514"
|
||||||
@ -2478,6 +2573,14 @@ prettier@^2.7.1:
|
|||||||
resolved "https://registry.yarnpkg.com/prettier/-/prettier-2.7.1.tgz#e235806850d057f97bb08368a4f7d899f7760c64"
|
resolved "https://registry.yarnpkg.com/prettier/-/prettier-2.7.1.tgz#e235806850d057f97bb08368a4f7d899f7760c64"
|
||||||
integrity sha512-ujppO+MkdPqoVINuDFDRLClm7D78qbDt0/NR+wp5FqEZOoTNAjPHWj17QRhu7geIHJfcNhRk1XVQmF8Bp3ye+g==
|
integrity sha512-ujppO+MkdPqoVINuDFDRLClm7D78qbDt0/NR+wp5FqEZOoTNAjPHWj17QRhu7geIHJfcNhRk1XVQmF8Bp3ye+g==
|
||||||
|
|
||||||
|
prompts@^2.4.2:
|
||||||
|
version "2.4.2"
|
||||||
|
resolved "https://registry.yarnpkg.com/prompts/-/prompts-2.4.2.tgz#7b57e73b3a48029ad10ebd44f74b01722a4cb069"
|
||||||
|
integrity sha512-NxNv/kLguCA7p3jE8oL2aEBsrJWgAakBpgmgK6lpPWV+WuOmY6r2/zbAVnP+T8bQlA0nzHXSJSJW0Hq7ylaD2Q==
|
||||||
|
dependencies:
|
||||||
|
kleur "^3.0.3"
|
||||||
|
sisteransi "^1.0.5"
|
||||||
|
|
||||||
pstree.remy@^1.1.8:
|
pstree.remy@^1.1.8:
|
||||||
version "1.1.8"
|
version "1.1.8"
|
||||||
resolved "https://registry.yarnpkg.com/pstree.remy/-/pstree.remy-1.1.8.tgz#c242224f4a67c21f686839bbdb4ac282b8373d3a"
|
resolved "https://registry.yarnpkg.com/pstree.remy/-/pstree.remy-1.1.8.tgz#c242224f4a67c21f686839bbdb4ac282b8373d3a"
|
||||||
@ -2556,6 +2659,13 @@ rimraf@^2.5.2:
|
|||||||
dependencies:
|
dependencies:
|
||||||
glob "^7.1.3"
|
glob "^7.1.3"
|
||||||
|
|
||||||
|
rimraf@^3.0.0:
|
||||||
|
version "3.0.2"
|
||||||
|
resolved "https://registry.yarnpkg.com/rimraf/-/rimraf-3.0.2.tgz#f1a5402ba6220ad52cc1282bac1ae3aa49fd061a"
|
||||||
|
integrity sha512-JZkJMZkAGFFPP2YqXZXPbMlMBgsxzE8ILs4lMIX/2o0L9UBw9O/Y3o6wFw/i9YLapcUJWwqbi3kdxIPdC62TIA==
|
||||||
|
dependencies:
|
||||||
|
glob "^7.1.3"
|
||||||
|
|
||||||
rollup@^2.78.1:
|
rollup@^2.78.1:
|
||||||
version "2.79.1"
|
version "2.79.1"
|
||||||
resolved "https://registry.yarnpkg.com/rollup/-/rollup-2.79.1.tgz#bedee8faef7c9f93a2647ac0108748f497f081c7"
|
resolved "https://registry.yarnpkg.com/rollup/-/rollup-2.79.1.tgz#bedee8faef7c9f93a2647ac0108748f497f081c7"
|
||||||
@ -2656,6 +2766,11 @@ sirv@^2.0.2:
|
|||||||
mrmime "^1.0.0"
|
mrmime "^1.0.0"
|
||||||
totalist "^3.0.0"
|
totalist "^3.0.0"
|
||||||
|
|
||||||
|
sisteransi@^1.0.5:
|
||||||
|
version "1.0.5"
|
||||||
|
resolved "https://registry.yarnpkg.com/sisteransi/-/sisteransi-1.0.5.tgz#134d681297756437cc05ca01370d3a7a571075ed"
|
||||||
|
integrity sha512-bLGGlR1QxBcynn2d5YmDX4MGjlZvy2MRBDRNHLJ8VI6l6+9FUiyTFNJ0IveOSP0bcXgVDPRcfGqA0pjaqUpfVg==
|
||||||
|
|
||||||
sorcery@^0.10.0:
|
sorcery@^0.10.0:
|
||||||
version "0.10.0"
|
version "0.10.0"
|
||||||
resolved "https://registry.yarnpkg.com/sorcery/-/sorcery-0.10.0.tgz#8ae90ad7d7cb05fc59f1ab0c637845d5c15a52b7"
|
resolved "https://registry.yarnpkg.com/sorcery/-/sorcery-0.10.0.tgz#8ae90ad7d7cb05fc59f1ab0c637845d5c15a52b7"
|
||||||
@ -2812,6 +2927,11 @@ svelte@^3.44.0:
|
|||||||
resolved "https://registry.yarnpkg.com/svelte/-/svelte-3.51.0.tgz#a1a0afb25dc518217f353dd73ea6471c128ddf84"
|
resolved "https://registry.yarnpkg.com/svelte/-/svelte-3.51.0.tgz#a1a0afb25dc518217f353dd73ea6471c128ddf84"
|
||||||
integrity sha512-PBITYIrsNOuW+Dtds00gSY68raNZQn7i59Dg/fjgf6WwyawPKeBwle692coO7ILZqSO+UJe9899aDn9sMdeOHA==
|
integrity sha512-PBITYIrsNOuW+Dtds00gSY68raNZQn7i59Dg/fjgf6WwyawPKeBwle692coO7ILZqSO+UJe9899aDn9sMdeOHA==
|
||||||
|
|
||||||
|
svelte@^3.51.0:
|
||||||
|
version "3.52.0"
|
||||||
|
resolved "https://registry.yarnpkg.com/svelte/-/svelte-3.52.0.tgz#08259eff20904c63882b66a5d409a55e8c6743b8"
|
||||||
|
integrity sha512-FxcnEUOAVfr10vDU5dVgJN19IvqeHQCS1zfe8vayTfis9A2t5Fhx+JDe5uv/C3j//bB1umpLJ6quhgs9xyUbCQ==
|
||||||
|
|
||||||
svgo@^2.4.0:
|
svgo@^2.4.0:
|
||||||
version "2.8.0"
|
version "2.8.0"
|
||||||
resolved "https://registry.yarnpkg.com/svgo/-/svgo-2.8.0.tgz#4ff80cce6710dc2795f0c7c74101e6764cfccd24"
|
resolved "https://registry.yarnpkg.com/svgo/-/svgo-2.8.0.tgz#4ff80cce6710dc2795f0c7c74101e6764cfccd24"
|
||||||
@ -2853,6 +2973,13 @@ tiny-glob@^0.2.9:
|
|||||||
globalyzer "0.1.0"
|
globalyzer "0.1.0"
|
||||||
globrex "^0.1.2"
|
globrex "^0.1.2"
|
||||||
|
|
||||||
|
tmp@^0.2.1:
|
||||||
|
version "0.2.1"
|
||||||
|
resolved "https://registry.yarnpkg.com/tmp/-/tmp-0.2.1.tgz#8457fc3037dcf4719c251367a1af6500ee1ccf14"
|
||||||
|
integrity sha512-76SUhtfqR2Ijn+xllcI5P1oyannHNHByD80W1q447gU3mp9G9PSpGdWmjUOHRDPiHYacIk66W7ubDTuPF3BEtQ==
|
||||||
|
dependencies:
|
||||||
|
rimraf "^3.0.0"
|
||||||
|
|
||||||
to-regex-range@^5.0.1:
|
to-regex-range@^5.0.1:
|
||||||
version "5.0.1"
|
version "5.0.1"
|
||||||
resolved "https://registry.yarnpkg.com/to-regex-range/-/to-regex-range-5.0.1.tgz#1648c44aae7c8d988a326018ed72f5b4dd0392e4"
|
resolved "https://registry.yarnpkg.com/to-regex-range/-/to-regex-range-5.0.1.tgz#1648c44aae7c8d988a326018ed72f5b4dd0392e4"
|
||||||
@ -2872,11 +2999,21 @@ touch@^3.1.0:
|
|||||||
dependencies:
|
dependencies:
|
||||||
nopt "~1.0.10"
|
nopt "~1.0.10"
|
||||||
|
|
||||||
|
tr46@~0.0.3:
|
||||||
|
version "0.0.3"
|
||||||
|
resolved "https://registry.yarnpkg.com/tr46/-/tr46-0.0.3.tgz#8184fd347dac9cdc185992f3a6622e14b9d9ab6a"
|
||||||
|
integrity sha512-N3WMsuqV66lT30CrXNbEjx4GEwlow3v6rr4mCcv6prnfwhS01rkgyFdjPNBYd9br7LpXV1+Emh01fHnq2Gdgrw==
|
||||||
|
|
||||||
tree-kill@^1.2.2:
|
tree-kill@^1.2.2:
|
||||||
version "1.2.2"
|
version "1.2.2"
|
||||||
resolved "https://registry.yarnpkg.com/tree-kill/-/tree-kill-1.2.2.tgz#4ca09a9092c88b73a7cdc5e8a01b507b0790a0cc"
|
resolved "https://registry.yarnpkg.com/tree-kill/-/tree-kill-1.2.2.tgz#4ca09a9092c88b73a7cdc5e8a01b507b0790a0cc"
|
||||||
integrity sha512-L0Orpi8qGpRG//Nd+H90vFB+3iHnue1zSSGmNOOCh1GLJ7rUKVwV2HvijphGQS2UmhUZewS9VgvxYIdgr+fG1A==
|
integrity sha512-L0Orpi8qGpRG//Nd+H90vFB+3iHnue1zSSGmNOOCh1GLJ7rUKVwV2HvijphGQS2UmhUZewS9VgvxYIdgr+fG1A==
|
||||||
|
|
||||||
|
ts-brand@^0.0.2:
|
||||||
|
version "0.0.2"
|
||||||
|
resolved "https://registry.yarnpkg.com/ts-brand/-/ts-brand-0.0.2.tgz#b6cbca6ac94df1050a05844e23944eaeda1738a0"
|
||||||
|
integrity sha512-UhSzWY4On9ZHIj6DKkRYVN/8OaprbLAZ3b/Y2AJwdl6oozSABsQ0PvwDh4vOVdkvOtWQOkIrjctZ1kj8YfF3jA==
|
||||||
|
|
||||||
ts-node@^10.9.1:
|
ts-node@^10.9.1:
|
||||||
version "10.9.1"
|
version "10.9.1"
|
||||||
resolved "https://registry.yarnpkg.com/ts-node/-/ts-node-10.9.1.tgz#e73de9102958af9e1f0b168a6ff320e25adcff4b"
|
resolved "https://registry.yarnpkg.com/ts-node/-/ts-node-10.9.1.tgz#e73de9102958af9e1f0b168a6ff320e25adcff4b"
|
||||||
@ -2968,6 +3105,19 @@ web-streams-polyfill@^3.0.3:
|
|||||||
resolved "https://registry.yarnpkg.com/web-streams-polyfill/-/web-streams-polyfill-3.2.1.tgz#71c2718c52b45fd49dbeee88634b3a60ceab42a6"
|
resolved "https://registry.yarnpkg.com/web-streams-polyfill/-/web-streams-polyfill-3.2.1.tgz#71c2718c52b45fd49dbeee88634b3a60ceab42a6"
|
||||||
integrity sha512-e0MO3wdXWKrLbL0DgGnUV7WHVuw9OUvL4hjgnPkIeEvESk74gAITi5G606JtZPp39cd8HA9VQzCIvA49LpPN5Q==
|
integrity sha512-e0MO3wdXWKrLbL0DgGnUV7WHVuw9OUvL4hjgnPkIeEvESk74gAITi5G606JtZPp39cd8HA9VQzCIvA49LpPN5Q==
|
||||||
|
|
||||||
|
webidl-conversions@^3.0.0:
|
||||||
|
version "3.0.1"
|
||||||
|
resolved "https://registry.yarnpkg.com/webidl-conversions/-/webidl-conversions-3.0.1.tgz#24534275e2a7bc6be7bc86611cc16ae0a5654871"
|
||||||
|
integrity sha512-2JAn3z8AR6rjK8Sm8orRC0h/bcl/DqL7tRPdGZ4I1CjdF+EaMLmYxBHyXuKL849eucPFhvBoxMsflfOb8kxaeQ==
|
||||||
|
|
||||||
|
whatwg-url@^5.0.0:
|
||||||
|
version "5.0.0"
|
||||||
|
resolved "https://registry.yarnpkg.com/whatwg-url/-/whatwg-url-5.0.0.tgz#966454e8765462e37644d3626f6742ce8b70965d"
|
||||||
|
integrity sha512-saE57nupxk6v3HY35+jzBwYa0rKSy0XR8JSxZPwgLr7ys0IBzhGviA1/TUGJLmSVqs8pb9AnvICXEuOHLprYTw==
|
||||||
|
dependencies:
|
||||||
|
tr46 "~0.0.3"
|
||||||
|
webidl-conversions "^3.0.0"
|
||||||
|
|
||||||
which-module@^2.0.0:
|
which-module@^2.0.0:
|
||||||
version "2.0.0"
|
version "2.0.0"
|
||||||
resolved "https://registry.yarnpkg.com/which-module/-/which-module-2.0.0.tgz#d9ef07dce77b9902b8a3a8fa4b31c3e3f7e6e87a"
|
resolved "https://registry.yarnpkg.com/which-module/-/which-module-2.0.0.tgz#d9ef07dce77b9902b8a3a8fa4b31c3e3f7e6e87a"
|
||||||
@ -3062,3 +3212,8 @@ yn@3.1.1:
|
|||||||
version "3.1.1"
|
version "3.1.1"
|
||||||
resolved "https://registry.yarnpkg.com/yn/-/yn-3.1.1.tgz#1e87401a09d767c1d5eab26a6e4c185182d2eb50"
|
resolved "https://registry.yarnpkg.com/yn/-/yn-3.1.1.tgz#1e87401a09d767c1d5eab26a6e4c185182d2eb50"
|
||||||
integrity sha512-Ux4ygGWsu2c7isFWe8Yu1YluJmqVhxqK2cLXNQA5AcC3QfbGNpM7fu0Y8b/z16pXLnFxZYvWhd3fhBY9DLmC6Q==
|
integrity sha512-Ux4ygGWsu2c7isFWe8Yu1YluJmqVhxqK2cLXNQA5AcC3QfbGNpM7fu0Y8b/z16pXLnFxZYvWhd3fhBY9DLmC6Q==
|
||||||
|
|
||||||
|
yocto-queue@^1.0.0:
|
||||||
|
version "1.0.0"
|
||||||
|
resolved "https://registry.yarnpkg.com/yocto-queue/-/yocto-queue-1.0.0.tgz#7f816433fb2cbc511ec8bf7d263c3b58a1a3c251"
|
||||||
|
integrity sha512-9bnSc/HEW2uRy67wc+T8UwauLuPJVn28jb+GtJY16iiKWyvmYJRXVT4UamsAEGQfPohgr2q4Tq0sQbQlxTfi1g==
|
||||||
|
Loading…
x
Reference in New Issue
Block a user