chore: remove old cli package

This commit is contained in:
Ben Allfree 2023-10-01 07:15:27 -07:00
parent 59158af307
commit 30ed96e589
25 changed files with 13 additions and 812 deletions

View File

@ -14,9 +14,22 @@ Highlights in this release:
- Improved secrets - secrets are now passed to `pocketbase` executable and are available in JS hooks - Improved secrets - secrets are now passed to `pocketbase` executable and are available in JS hooks
- Security - All `pocketbase` instances now run in Docker sandboxes in isolated environments. Reduces security risks and bad neighbor effects. - Security - All `pocketbase` instances now run in Docker sandboxes in isolated environments. Reduces security risks and bad neighbor effects.
- Started using `pb_hooks` internally to replace some complex listener logic - Started using `pb_hooks` internally to replace some complex listener logic
- SSG frontend for better SEO and load times
## Change log ## Change log
- chore: remove old cli package
- enh: update sftp link
- chore: add permalink to live publishing step
- enh: Cloudflare Pages SSG publishing
- enh: SSG
- fix: sveltekit environment variables
- enh: invocation indexes
- chore: comment template environment variables
- enh: run PocketBase in debugging mode when DEBUG=true
- enh: gitignore update
- fix: db migrations
- fix: secondsThisMonth in users table
- enh: usage tracking to JS hooks - enh: usage tracking to JS hooks
- enh: add docker-compose sample for better dx - enh: add docker-compose sample for better dx
- enh: mothership backup script - enh: mothership backup script

View File

@ -1 +0,0 @@
dist

View File

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

View File

@ -1,58 +0,0 @@
{
"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.8.0",
"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"
]
}
}
}

View File

@ -1,25 +0,0 @@
# 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

@ -1,90 +0,0 @@
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

@ -1,99 +0,0 @@
import { DEFAULT_PB_DEV_URL } from '$constants'
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 { 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

@ -1,100 +0,0 @@
import { DEFAULT_PB_DEV_URL } from '$constants'
import { Command } from 'commander'
import { existsSync, readFileSync } from 'fs'
import { join } from 'path'
import pocketbaseEs from 'pocketbase'
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

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

View File

@ -1,13 +0,0 @@
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

@ -1,22 +0,0 @@
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 { FieldStruct, buildQueryFilter } 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

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

View File

@ -1,27 +0,0 @@
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

@ -1,11 +0,0 @@
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

@ -1,9 +0,0 @@
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

@ -1,22 +0,0 @@
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

@ -1,49 +0,0 @@
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 { FieldStruct, buildQueryFilter } 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)
}
}

View File

@ -1,20 +0,0 @@
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

@ -1,57 +0,0 @@
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

@ -1,8 +0,0 @@
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

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

View File

@ -1,71 +0,0 @@
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

@ -1,47 +0,0 @@
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

@ -1,52 +0,0 @@
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

@ -1,18 +0,0 @@
{
"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"]
}