Merge branch 'master' of github.com:benallfree/pockethost

This commit is contained in:
Ben Allfree 2022-11-02 04:34:53 +00:00
commit 3a445cc844
93 changed files with 4698 additions and 308 deletions

View File

@ -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

View File

@ -1 +1,2 @@
.svelte-kit .svelte-kit
dist

View File

@ -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

View File

@ -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:

View File

@ -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
View File

@ -0,0 +1 @@
dist

3
packages/cli/.npmignore Normal file
View File

@ -0,0 +1,3 @@
src
tsconfig.json
dist/index.*

58
packages/cli/package.json Normal file
View 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
View 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).

View 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')
})
}

View 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')
})
}

View 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)
})
}

View File

@ -0,0 +1 @@
export const DEFAULT_PB_DEV_URL = `http://127.0.0.1:8090`

View 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
}

View 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 }
}

View 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
}

View File

@ -0,0 +1,6 @@
export * from './getOne'
export * from './onAuthStateChanged'
export * from './pbUid'
export * from './RealtimeSubscriptionManager'
export * from './signInAnonymously'
export * from './upsert'

View 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
}

View 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)
})
}

View 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)
}

View 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}`)
})
}

View 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
View 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()

View 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()`)
}
}

View 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)
}
}

View File

@ -0,0 +1,4 @@
export function die(msg: string): never {
console.error(msg)
process.exit(1)
}

View 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
}

View 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
}

View 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>
}

View 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"]
}

View File

@ -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 {

View File

@ -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,

View File

@ -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 })
} }
) )

View File

@ -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
View File

@ -0,0 +1 @@
dist

View File

@ -0,0 +1,2 @@
src
tsconfig.json

View 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"
}
}

View 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

View 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 }
}

View 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)
// }

View File

@ -0,0 +1,7 @@
export enum HttpMethods {
Get = 'GET',
}
export enum HttpResponseStatuses {
Ok = 200,
}

View 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`)

View 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)

View 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 || [],
})
}

View 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)
})
}

View File

@ -0,0 +1,3 @@
export type ConsoleApi = {
log: (...args: any) => void
}

View 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
}

View File

@ -0,0 +1,4 @@
export type PingResult = string
export type PBScriptApi = {
ping: () => PingResult
}

View 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
}

View 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
}
}

View File

@ -0,0 +1,5 @@
export type RecordId = string
export type IsoDate = string
export type Url = string
export * from './User'

View 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
}

View 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
}

View 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[]
}

View 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
}

View File

@ -0,0 +1,4 @@
export * from './Dao'
export * from './Dbx'
export * from './Echo'
export * from './Sql'

View File

@ -0,0 +1,5 @@
export * from './database'
export * from './go-namespaces'
export * from './GoApi'
export * from './PBScriptApi'
export * from './__global'

View 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)
}
}

View 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]
}

View File

@ -0,0 +1,3 @@
export function isFunction(value: any) {
return typeof value === 'function'
}

View 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"]
}

View 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
}

View 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)
}
}

View 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
)

File diff suppressed because it is too large Load Diff

View 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
}

View File

@ -1 +1,2 @@
dist dist
build

View File

@ -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.

View File

@ -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}

View File

@ -0,0 +1,10 @@
export enum AlertTypes {
Primary = 'primary',
Secondary = 'secondary',
Success = 'success',
Danger = 'danger',
Warning = 'warning',
Info = 'info',
Light = 'light',
Dark = 'dark'
}

View File

@ -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>

View File

@ -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"

View File

@ -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>

View File

@ -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>

View File

@ -0,0 +1,10 @@
<script lang="ts">
import { isAuthStateInitialized } from '$util/stores'
import { onMount } from 'svelte'
onMount(() => {})
</script>
{#if $isAuthStateInitialized}
<slot />
{/if}

View 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>

View File

@ -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>

View File

@ -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
} }
} }

View File

@ -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
}
})()

View File

@ -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>

View File

@ -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">

View File

@ -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>

View File

@ -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 {

View File

@ -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 {

View File

@ -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 = '/'
}

View 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]
}

View File

@ -0,0 +1 @@
["/", "/signup", "/login", "/faq"]

View 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
})
}
}

View File

@ -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)
})
}

View File

@ -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
}

View File

@ -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
View File

@ -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==