@palbase/backend 13.0.0 → 14.0.0
This diff represents the content of publicly available package versions that have been released to one of the supported registries. The information contained in this diff is provided for informational purposes only and reflects changes between package versions as they appear in their respective public registries.
- package/dist/{chunk-XATG7BRC.js → chunk-I72YYSEI.js} +1 -3
- package/dist/chunk-I72YYSEI.js.map +1 -0
- package/dist/db/index.d.cts +2 -2
- package/dist/db/index.d.ts +2 -2
- package/dist/{endpoint-Ck4hER_7.d.cts → endpoint-CAWdScEH.d.cts} +14 -8
- package/dist/{endpoint-Ck4hER_7.d.ts → endpoint-CAWdScEH.d.ts} +14 -8
- package/dist/{index-BJAf1uPC.d.cts → index-CaRM7qe_.d.cts} +1 -1
- package/dist/{index-l7DhBDtn.d.ts → index-EAlQyErf.d.ts} +1 -1
- package/dist/index.cjs +22 -40
- package/dist/index.cjs.map +1 -1
- package/dist/index.d.cts +40 -71
- package/dist/index.d.ts +40 -71
- package/dist/index.js +20 -39
- package/dist/index.js.map +1 -1
- package/dist/test/index.cjs +0 -6
- package/dist/test/index.cjs.map +1 -1
- package/dist/test/index.d.cts +1 -2
- package/dist/test/index.d.ts +1 -2
- package/dist/test/index.js +1 -6
- package/dist/test/index.js.map +1 -1
- package/docs/README.md +2 -4
- package/docs/auth.md +111 -0
- package/docs/background.md +15 -39
- package/docs/config.md +51 -10
- package/docs/database.md +11 -3
- package/docs/events.md +1 -1
- package/docs/llms-full.txt +358 -59
- package/docs/llms.txt +3 -1
- package/docs/migrations.md +30 -0
- package/docs/schema.md +31 -3
- package/docs/services.md +0 -9
- package/package.json +4 -2
- package/dist/chunk-XATG7BRC.js.map +0 -1
package/dist/test/index.js
CHANGED
|
@@ -1,6 +1,6 @@
|
|
|
1
1
|
import {
|
|
2
2
|
__setRuntime
|
|
3
|
-
} from "../chunk-
|
|
3
|
+
} from "../chunk-I72YYSEI.js";
|
|
4
4
|
import "../chunk-7LAXRLPG.js";
|
|
5
5
|
|
|
6
6
|
// src/test/mock-db.ts
|
|
@@ -435,9 +435,6 @@ function createTestContext(options = {}) {
|
|
|
435
435
|
db.seed(table, data);
|
|
436
436
|
}
|
|
437
437
|
}
|
|
438
|
-
const mockQueue = {
|
|
439
|
-
push: async (_worker, _payload) => ({ jobId: "" })
|
|
440
|
-
};
|
|
441
438
|
const log = createMockLogger(logs);
|
|
442
439
|
const cache = createMockCache();
|
|
443
440
|
const moduleClients = createMockModuleClients();
|
|
@@ -446,7 +443,6 @@ function createTestContext(options = {}) {
|
|
|
446
443
|
Documents: moduleClients.docs,
|
|
447
444
|
Storage: moduleClients.storage,
|
|
448
445
|
Cache: cache,
|
|
449
|
-
Queue: mockQueue,
|
|
450
446
|
Log: log,
|
|
451
447
|
Notifications: moduleClients.notifications,
|
|
452
448
|
Flags: moduleClients.flags,
|
|
@@ -466,7 +462,6 @@ function createTestContext(options = {}) {
|
|
|
466
462
|
env: options.env ?? {},
|
|
467
463
|
log,
|
|
468
464
|
cache,
|
|
469
|
-
queue: mockQueue,
|
|
470
465
|
...moduleClients,
|
|
471
466
|
// Empty errors map in tests by default. Tests that exercise an endpoint's
|
|
472
467
|
// declared errors construct their own throwers; this stub satisfies the
|
package/dist/test/index.js.map
CHANGED
|
@@ -1 +1 @@
|
|
|
1
|
-
{"version":3,"sources":["../../src/test/mock-db.ts","../../src/test/context.ts"],"sourcesContent":["import type { DBClient, DBOps } from \"../endpoint.js\";\nimport type {\n TxPlanBody,\n TxPlanOpResult,\n TxPlanRejection,\n TxPlanResponse,\n TxWireGuard,\n TxWireOp,\n TxWireValue,\n} from \"../db/tx-plan.js\";\n\n/** Tracked records for assertions. */\ninterface TrackedRecords {\n inserted: Map<string, Record<string, unknown>[]>;\n updated: Map<string, Record<string, unknown>[]>;\n deleted: Map<string, string[]>;\n}\n\n/** Mock DB client with tracking and seed data support. */\nexport interface MockDBClient extends DBClient {\n /** Get records inserted into a table. */\n inserted(table: string): Record<string, unknown>[];\n /** Get records updated in a table. */\n updated(table: string): Record<string, unknown>[];\n /** Get IDs deleted from a table. */\n deleted(table: string): string[];\n /** Pre-seed data into a table for findById/findMany. */\n seed(table: string, data: Record<string, unknown>[]): void;\n}\n\n/** Create a mock DB client with in-memory tracking. */\nexport function createMockDB(): MockDBClient {\n const store = new Map<string, Record<string, unknown>[]>();\n const tracked: TrackedRecords = {\n inserted: new Map(),\n updated: new Map(),\n deleted: new Map(),\n };\n\n function rowsOf(table: string): Record<string, unknown>[] {\n let rows = store.get(table);\n if (!rows) {\n rows = [];\n store.set(table, rows);\n }\n return rows;\n }\n\n function track(\n map: Map<string, Record<string, unknown>[]>,\n table: string,\n row: Record<string, unknown>,\n ): void {\n const list = map.get(table);\n if (list) list.push(row);\n else map.set(table, [row]);\n }\n\n // Build the op surface first (the six string-keyed ops). `txPlan` below\n // interprets a whole plan against the SAME in-memory store and tracking maps,\n // so a transaction's writes are visible to later assertions exactly as a\n // direct write would be.\n const ops: DBOps = {\n async query(_sql: string, _params?: unknown[]) {\n return [];\n },\n\n async insert(table: string, data: Record<string, unknown>) {\n const record = { id: crypto.randomUUID(), ...data };\n rowsOf(table).push(record);\n track(tracked.inserted, table, record);\n return record;\n },\n\n async update(table: string, id: string, data: Record<string, unknown>) {\n const rows = store.get(table) ?? [];\n const idx = rows.findIndex((r) => r[\"id\"] === id);\n const updated = idx >= 0\n ? { ...rows[idx], ...data }\n : { id, ...data };\n if (idx >= 0) {\n rows[idx] = updated;\n }\n track(tracked.updated, table, updated);\n return updated;\n },\n\n async delete(table: string, id: string) {\n const rows = store.get(table) ?? [];\n const idx = rows.findIndex((r) => r[\"id\"] === id);\n if (idx >= 0) rows.splice(idx, 1);\n const list = tracked.deleted.get(table);\n if (list) list.push(id);\n else tracked.deleted.set(table, [id]);\n },\n\n async findById(table: string, id: string) {\n const rows = store.get(table) ?? [];\n return rows.find((r) => r[\"id\"] === id) ?? null;\n },\n\n async findMany(table: string, query?: Record<string, unknown>) {\n const rows = store.get(table) ?? [];\n if (!query) return rows;\n return rows.filter((row) =>\n Object.entries(query).every(([key, val]) => row[key] === val),\n );\n },\n };\n\n /**\n * Interpret a whole plan, atomically.\n *\n * The rollback is the point. A test that asserts \"the second write failed, so\n * the first one is not there\" must be able to FAIL — a mock that applied ops\n * and left them applied would pass that test while the real broker rolled the\n * transaction back, or the other way round. So the store and the tracking maps\n * are snapshotted, and any failure restores both before rejecting.\n *\n * The rejection carries the same envelope fields the runtime copies off the\n * broker's response (`error_code`, `slot`), because the SDK maps `slot` back\n * to the caller's own Error — a mock that rejected with a bare Error would\n * make every guard in every tenant test look like a generic failure.\n */\n async function txPlan(plan: TxPlanBody): Promise<TxPlanResponse> {\n const snapshot = new Map<string, Record<string, unknown>[]>();\n for (const [table, rows] of store) snapshot.set(table, [...rows]);\n const trackedSnapshot: TrackedRecords = {\n inserted: cloneTracked(tracked.inserted),\n updated: cloneTracked(tracked.updated),\n deleted: new Map([...tracked.deleted].map(([k, v]) => [k, [...v]])),\n };\n\n const results: TxPlanOpResult[] = [];\n try {\n for (const op of plan.ops) {\n const result = applyOp(op, results);\n results.push(result);\n const failure = guardFailure(op.guard, result.rows.length);\n if (failure) throw failure;\n }\n } catch (err) {\n store.clear();\n for (const [table, rows] of snapshot) store.set(table, rows);\n tracked.inserted = trackedSnapshot.inserted;\n tracked.updated = trackedSnapshot.updated;\n tracked.deleted = trackedSnapshot.deleted;\n throw err;\n }\n return { results };\n }\n\n function applyOp(op: TxWireOp, results: TxPlanOpResult[]): TxPlanOpResult {\n switch (op.op) {\n case \"insert\": {\n const record = { id: crypto.randomUUID(), ...resolveMap(op.values ?? {}, results, null) };\n rowsOf(op.table).push(record);\n track(tracked.inserted, op.table, record);\n return { rows: [record], rows_affected: 1 };\n }\n case \"insertMany\": {\n const written = (op.rows ?? []).map((row) => {\n const record = { id: crypto.randomUUID(), ...resolveMap(row, results, null) };\n rowsOf(op.table).push(record);\n track(tracked.inserted, op.table, record);\n return record;\n });\n return { rows: written, rows_affected: written.length };\n }\n case \"update\": {\n const rows = rowsOf(op.table);\n const where = resolveMap(op.where ?? {}, results, null);\n const written: Record<string, unknown>[] = [];\n for (let i = 0; i < rows.length; i++) {\n const row = rows[i];\n if (!row || !matches(row, where)) continue;\n const next = { ...row, ...resolveMap(op.set ?? {}, results, row) };\n rows[i] = next;\n track(tracked.updated, op.table, next);\n written.push(next);\n }\n return { rows: written, rows_affected: written.length };\n }\n case \"delete\": {\n const rows = rowsOf(op.table);\n const where = resolveMap(op.where ?? {}, results, null);\n const removed = rows.filter((row) => matches(row, where));\n for (const row of removed) {\n rows.splice(rows.indexOf(row), 1);\n const id = row[\"id\"];\n const list = tracked.deleted.get(op.table);\n const key = typeof id === \"string\" ? id : String(id);\n if (list) list.push(key);\n else tracked.deleted.set(op.table, [key]);\n }\n return { rows: removed, rows_affected: removed.length };\n }\n case \"select\": {\n const where = resolveMap(op.where ?? {}, results, null);\n let found = rowsOf(op.table).filter((row) => matches(row, where));\n if (op.limit !== undefined) found = found.slice(0, op.limit);\n return { rows: found, rows_affected: found.length };\n }\n }\n }\n\n const client: MockDBClient = {\n ...ops,\n\n txPlan,\n\n // In tests there is no real DB role; `asService()` returns the same\n // in-memory client so RLS-bypass code paths still hit the same store and\n // tracking maps. The omitted `asService` matches the contract (no\n // double-bypass), so callers can't recurse.\n asService(): Omit<DBClient, \"asService\"> {\n return client;\n },\n\n inserted(table: string) {\n return tracked.inserted.get(table) ?? [];\n },\n\n updated(table: string) {\n return tracked.updated.get(table) ?? [];\n },\n\n deleted(table: string) {\n return tracked.deleted.get(table) ?? [];\n },\n\n seed(table: string, data: Record<string, unknown>[]) {\n store.set(table, [...data]);\n },\n };\n\n return client;\n}\n\nfunction cloneTracked(\n map: Map<string, Record<string, unknown>[]>,\n): Map<string, Record<string, unknown>[]> {\n return new Map([...map].map(([k, v]) => [k, [...v]]));\n}\n\n/** Resolve one plan value: a `$ref` into an earlier result, a `$expr`, or a\n * literal. `current` is the row being updated, which is what `inc`/`dec` read. */\nfunction resolveValue(\n value: TxWireValue,\n results: TxPlanOpResult[],\n current: Record<string, unknown> | null,\n column: string,\n): unknown {\n if (typeof value !== \"object\" || value === null) return value;\n const tagged = value as { $ref?: { op: number; field: string }; $expr?: Record<string, unknown> };\n\n if (tagged.$ref) {\n const row = results[tagged.$ref.op]?.rows[0];\n if (!row) {\n throw txRejection(409, \"tx_ref_unresolved\", {\n message: `operation ${tagged.$ref.op} produced no row to reference`,\n });\n }\n return row[tagged.$ref.field];\n }\n\n if (tagged.$expr) {\n const fn = tagged.$expr[\"fn\"];\n if (fn === \"now\") return new Date().toISOString();\n const by = Number(tagged.$expr[\"by\"]);\n const base = Number(current?.[column] ?? 0);\n return fn === \"dec\" ? base - by : base + by;\n }\n\n return value;\n}\n\nfunction resolveMap(\n map: Record<string, TxWireValue>,\n results: TxPlanOpResult[],\n current: Record<string, unknown> | null,\n): Record<string, unknown> {\n const out: Record<string, unknown> = {};\n for (const [key, value] of Object.entries(map)) {\n out[key] = resolveValue(value, results, current, key);\n }\n return out;\n}\n\n/** Equality filter, with `null` meaning IS NULL — the broker's rule, so a\n * `{ accepted_at: null }` guard behaves the same in a test as in production. */\nfunction matches(row: Record<string, unknown>, where: Record<string, unknown>): boolean {\n return Object.entries(where).every(([key, value]) =>\n value === null ? row[key] === null || row[key] === undefined : row[key] === value,\n );\n}\n\nfunction guardFailure(guard: TxWireGuard | undefined, count: number): unknown {\n if (!guard) return null;\n const ok =\n guard.kind === \"one\"\n ? count === 1\n : guard.kind === \"none\"\n ? count === 0\n : guard.kind === \"atLeast\"\n ? count >= guard.n\n : count <= guard.n;\n if (ok) return null;\n return txRejection(409, \"tx_guard_failed\", {\n slot: guard.slot,\n message: `expected ${guard.kind} ${guard.n} row(s), got ${count}`,\n });\n}\n\n/** Build a rejection shaped like the one the runtime throws for a broker error:\n * an Error carrying the envelope's `status`/`error_code`/`slot`. */\nfunction txRejection(\n status: number,\n code: string,\n extra: { slot?: number; message: string },\n): Error & TxPlanRejection {\n const err = new Error(extra.message) as Error & TxPlanRejection;\n err.status = status;\n err.error_code = code;\n if (extra.slot !== undefined) err.slot = extra.slot;\n return err;\n}\n","import type {\n CacheClient,\n ClientInfo,\n Logger,\n PBRequest,\n PalbaseModuleClients,\n QueueClient,\n} from \"../endpoint.js\";\nimport type {\n PalbaseAuthClient,\n PalbaseStorageClient,\n PalbaseRealtimeClient,\n PalbaseFunctionsClient,\n PalbaseFlagsClient,\n PalbaseNotificationsClient,\n PalbaseAnalyticsClient,\n PalbaseLinksClient,\n} from \"../clients.js\";\nimport type { User } from \"../types.js\";\nimport { __setRuntime } from \"../runtime.js\";\nimport type { PurchasesService } from \"../purchases/service.js\";\nimport { createMockDB, type MockDBClient } from \"./mock-db.js\";\n\n/** Options for creating a test context. */\nexport interface TestContextOptions<TInput = unknown> {\n user?: User | null;\n input?: TInput;\n params?: Record<string, string>;\n query?: Record<string, string>;\n headers?: Record<string, string>;\n env?: Record<string, string>;\n db?: { seed?: Record<string, Record<string, unknown>[]> };\n}\n\n/** Log entry captured by the mock logger. */\nexport interface LogEntry {\n level: \"info\" | \"warn\" | \"error\" | \"debug\";\n message: string;\n args: unknown[];\n}\n\n/** Test context for exercising endpoint handlers.\n *\n * Handlers now receive a {@link PBRequest} (no services attached) and reach\n * services via the PascalCase singletons (`Database`, `Log`, …). So\n * `createTestContext` does two things:\n * 1. returns a `PBRequest` (the object you pass to `handler(...)`), and\n * 2. installs mock services into the runtime via `__setRuntime`, so the\n * singletons resolve to the same mocks while the handler runs.\n *\n * For assertions and for building sibling (worker/job/hook/webhook) contexts,\n * the mock service handles are also attached here (`db`, `log`, `cache`,\n * `queue`, `env`, plus the module clients and captured `logs`). The user\n * defaults to nullable in tests (`PBRequest<TInput, false>`) so test code can\n * pass any auth shape without a cast. */\nexport interface TestContext<TInput = unknown>\n extends PBRequest<TInput, false>,\n PalbaseModuleClients {\n db: MockDBClient;\n env: Record<string, string>;\n log: Logger;\n cache: CacheClient;\n queue: QueueClient;\n /** Captured log entries. */\n logs: LogEntry[];\n}\n\n/** Create a mock logger that captures entries. */\nfunction createMockLogger(logs: LogEntry[]): Logger {\n return {\n info(message: string, ...args: unknown[]) {\n logs.push({ level: \"info\", message, args });\n },\n warn(message: string, ...args: unknown[]) {\n logs.push({ level: \"warn\", message, args });\n },\n error(message: string, ...args: unknown[]) {\n logs.push({ level: \"error\", message, args });\n },\n debug(message: string, ...args: unknown[]) {\n logs.push({ level: \"debug\", message, args });\n },\n };\n}\n\n/** Create a mock in-memory cache.\n *\n * Mirrors the runtime's JSON-typed semantics (values are arbitrary JSON, not\n * just strings). getOrSet is single-process here, so it does not need the\n * distributed lock the real runtime uses — it is just get-miss → fn → set.\n * The cross-replica stampede protection is covered by the worker.js tests.\n */\nfunction createMockCache(): CacheClient {\n const store = new Map<string, unknown>();\n\n const get = async <T = unknown>(key: string): Promise<T | null> => {\n return store.has(key) ? (store.get(key) as T) : null;\n };\n const set = async (key: string, value: unknown, _ttl?: number): Promise<void> => {\n store.set(key, value);\n };\n\n return {\n get,\n set,\n async del(key: string) {\n store.delete(key);\n },\n async incr(key: string) {\n const raw = store.get(key);\n const current = typeof raw === \"number\" ? raw : parseInt(String(raw ?? \"0\"), 10);\n const next = current + 1;\n store.set(key, next);\n return next;\n },\n async getOrSet<T>(key: string, ttl: number, fn: () => Promise<T> | T): Promise<T> {\n const hit = await get<T>(key);\n if (hit !== null) {\n return hit;\n }\n const value = await fn();\n await set(key, value, ttl);\n return value;\n },\n };\n}\n\n/** Create mock Palbase module clients (Documents, Storage, …).\n *\n * Every slot throws with a descriptive error so tests that access a module\n * client surface without configuring it fail loudly rather than silently\n * returning undefined. Override individual clients on the returned context for\n * tests that need them.\n */\nfunction createMockModuleClients(): PalbaseModuleClients {\n const notImpl = (label: string): never => {\n throw new Error(\n `${label} not configured in test mock — override the matching client on the returned context`,\n );\n };\n\n const docs: PalbaseModuleClients[\"docs\"] = {\n collection: () => notImpl(\"docs.collection\"),\n doc: () => notImpl(\"docs.doc\"),\n };\n\n const auth: PalbaseAuthClient = {\n verifyUserToken: () => notImpl(\"auth.verifyUserToken\"),\n getSession: () => notImpl(\"auth.getSession\"),\n mfa: {\n enroll: () => notImpl(\"auth.mfa.enroll\"),\n verifyEnrollment: () => notImpl(\"auth.mfa.verifyEnrollment\"),\n challenge: () => notImpl(\"auth.mfa.challenge\"),\n recovery: () => notImpl(\"auth.mfa.recovery\"),\n listFactors: () => notImpl(\"auth.mfa.listFactors\"),\n removeFactor: () => notImpl(\"auth.mfa.removeFactor\"),\n regenerateRecoveryCodes: () => notImpl(\"auth.mfa.regenerateRecoveryCodes\"),\n emailEnroll: () => notImpl(\"auth.mfa.emailEnroll\"),\n emailChallenge: () => notImpl(\"auth.mfa.emailChallenge\"),\n emailVerify: () => notImpl(\"auth.mfa.emailVerify\"),\n },\n device: {\n generateChallenge: () => notImpl(\"auth.device.generateChallenge\"),\n attestAndroid: () => notImpl(\"auth.device.attestAndroid\"),\n attestiOS: () => notImpl(\"auth.device.attestiOS\"),\n bind: () => notImpl(\"auth.device.bind\"),\n list: () => notImpl(\"auth.device.list\"),\n delete: () => notImpl(\"auth.device.delete\"),\n verifyRequestSignature: () => notImpl(\"auth.device.verifyRequestSignature\"),\n getToken: () => notImpl(\"auth.device.getToken\"),\n get isActive(): never {\n return notImpl(\"auth.device.isActive\");\n },\n setCachedToken: () => notImpl(\"auth.device.setCachedToken\"),\n dispose: () => notImpl(\"auth.device.dispose\"),\n },\n };\n\n const storage: PalbaseStorageClient = {\n bucket: () => notImpl(\"storage.bucket\"),\n };\n\n const realtime: PalbaseRealtimeClient = {\n broadcast: async () => ({ data: undefined, error: null }),\n };\n\n const functions: PalbaseFunctionsClient = {\n invoke: () => notImpl(\"functions.invoke\"),\n };\n\n const flags: PalbaseFlagsClient = {\n isEnabled: () => notImpl(\"flags.isEnabled\"),\n getVariant: () => notImpl(\"flags.getVariant\"),\n getAll: () => notImpl(\"flags.getAll\"),\n setOverride: () => notImpl(\"flags.setOverride\"),\n asService: () => ({\n setOverrideForUser: () => notImpl(\"flags.asService.setOverrideForUser\"),\n setOverridesForUser: () => notImpl(\"flags.asService.setOverridesForUser\"),\n clearOverrideForUser: () => notImpl(\"flags.asService.clearOverrideForUser\"),\n clearAllOverridesForUser: () => notImpl(\"flags.asService.clearAllOverridesForUser\"),\n batchSetOverrides: () => notImpl(\"flags.asService.batchSetOverrides\"),\n }),\n };\n\n const notifications: PalbaseNotificationsClient = {\n push: { send: () => notImpl(\"notifications.push.send\") },\n email: { send: () => notImpl(\"notifications.email.send\") },\n sms: { send: () => notImpl(\"notifications.sms.send\") },\n verifications: {\n start: () => notImpl(\"notifications.verifications.start\"),\n check: () => notImpl(\"notifications.verifications.check\"),\n },\n inbox: {\n send: () => notImpl(\"notifications.inbox.send\"),\n list: () => notImpl(\"notifications.inbox.list\"),\n unreadCount: () => notImpl(\"notifications.inbox.unreadCount\"),\n markRead: () => notImpl(\"notifications.inbox.markRead\"),\n markAllRead: () => notImpl(\"notifications.inbox.markAllRead\"),\n archive: () => notImpl(\"notifications.inbox.archive\"),\n },\n preferences: {\n get: () => notImpl(\"notifications.preferences.get\"),\n update: () => notImpl(\"notifications.preferences.update\"),\n },\n templates: {\n email: {\n list: () => notImpl(\"notifications.templates.email.list\"),\n get: () => notImpl(\"notifications.templates.email.get\"),\n create: () => notImpl(\"notifications.templates.email.create\"),\n update: () => notImpl(\"notifications.templates.email.update\"),\n delete: () => notImpl(\"notifications.templates.email.delete\"),\n },\n sms: {\n list: () => notImpl(\"notifications.templates.sms.list\"),\n get: () => notImpl(\"notifications.templates.sms.get\"),\n create: () => notImpl(\"notifications.templates.sms.create\"),\n update: () => notImpl(\"notifications.templates.sms.update\"),\n delete: () => notImpl(\"notifications.templates.sms.delete\"),\n },\n },\n registerDevice: () => notImpl(\"notifications.registerDevice\"),\n unregisterDevice: () => notImpl(\"notifications.unregisterDevice\"),\n };\n\n const analytics: PalbaseAnalyticsClient = {\n capture: () => notImpl(\"analytics.capture\"),\n identify: () => notImpl(\"analytics.identify\"),\n screen: () => notImpl(\"analytics.screen\"),\n query: {\n count: () => notImpl(\"analytics.query.count\"),\n events: () => notImpl(\"analytics.query.events\"),\n properties: () => notImpl(\"analytics.query.properties\"),\n users: () => notImpl(\"analytics.query.users\"),\n funnel: () => notImpl(\"analytics.query.funnel\"),\n retention: () => notImpl(\"analytics.query.retention\"),\n cohort: () => notImpl(\"analytics.query.cohort\"),\n },\n management: {\n overview: () => notImpl(\"analytics.management.overview\"),\n eventNames: () => notImpl(\"analytics.management.eventNames\"),\n userDetail: () => notImpl(\"analytics.management.userDetail\"),\n deleteUser: () => notImpl(\"analytics.management.deleteUser\"),\n },\n };\n\n const links: PalbaseLinksClient = {\n create: () => notImpl(\"links.create\"),\n list: () => notImpl(\"links.list\"),\n get: () => notImpl(\"links.get\"),\n update: () => notImpl(\"links.update\"),\n delete: () => notImpl(\"links.delete\"),\n analytics: () => notImpl(\"links.analytics\"),\n qrCode: () => notImpl(\"links.qrCode\"),\n match: () => notImpl(\"links.match\"),\n };\n\n return {\n auth,\n storage,\n docs,\n realtime,\n functions,\n flags,\n notifications,\n analytics,\n links,\n };\n}\n\n/** A permissive purchases double: the subject resolves, the entitlement is\n * present, and a spend just runs its handler. That keeps a unit test of a\n * decorated handler about the handler's own logic instead of about billing.\n *\n * ponytail: deliberately has no \"deny\" mode. The 403/429 paths are about the\n * server's real accounting (reserve/commit/cancel, quota windows), and a double\n * that answered them would be asserting its own script — those belong in an\n * integration test against a real palstore, which is how they are covered.\n */\nfunction createMockPurchases(): PurchasesService {\n return {\n resolveSubject: async ({ userRef }) => ({ subjectId: `psj_test_${userRef}` }),\n require: async () => undefined,\n withSpend: async (_subjectId, _key, _opts, handler) => handler(),\n };\n}\n\n/** Null-by-default calling-client metadata for tests. */\nconst NULL_CLIENT_INFO: ClientInfo = {\n sdkVersion: null,\n appVersion: null,\n platform: null,\n osVersion: null,\n};\n\n/** Create a fully mocked endpoint test context.\n *\n * Returns a `PBRequest` (pass it to `handler(...)`) with the mock service\n * handles attached for assertions, and installs those mocks into the runtime\n * via `__setRuntime` so the `Database`/`Log`/… singletons resolve to them\n * while the handler runs.\n */\nexport function createTestContext<TInput = unknown>(\n options: TestContextOptions<TInput> = {},\n): TestContext<TInput> {\n const logs: LogEntry[] = [];\n const db = createMockDB();\n\n // Seed data if provided\n if (options.db?.seed) {\n for (const [table, data] of Object.entries(options.db.seed)) {\n db.seed(table, data);\n }\n }\n\n const mockQueue: QueueClient = {\n push: async (_worker: string, _payload: unknown) => ({ jobId: \"\" }),\n };\n const log = createMockLogger(logs);\n const cache = createMockCache();\n const moduleClients = createMockModuleClients();\n\n // Install the mocks so the PascalCase singletons (Database, Log, …) resolve\n // to them while the handler under test runs.\n __setRuntime({\n Database: db,\n Documents: moduleClients.docs,\n Storage: moduleClients.storage,\n Cache: cache,\n Queue: mockQueue,\n Log: log,\n Notifications: moduleClients.notifications,\n Flags: moduleClients.flags,\n Realtime: moduleClients.realtime,\n Purchases: createMockPurchases(),\n });\n\n const ctx: TestContext<TInput> = {\n input: (options.input ?? {}) as TInput,\n params: options.params ?? {},\n query: options.query ?? {},\n headers: options.headers ?? {},\n user: options.user ?? null,\n client: NULL_CLIENT_INFO,\n method: \"POST\",\n file: null,\n db,\n env: options.env ?? {},\n log,\n cache,\n queue: mockQueue,\n ...moduleClients,\n // Empty errors map in tests by default. Tests that exercise an endpoint's\n // declared errors construct their own throwers; this stub satisfies the\n // PBRequest shape without forcing every test to declare `errors:`.\n errors: {},\n requestId: \"req_test_000000000000\",\n traceId: \"0\".repeat(32),\n spanId: \"0\".repeat(16),\n logs,\n };\n\n return ctx;\n}\n"],"mappings":"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","names":[]}
|
|
1
|
+
{"version":3,"sources":["../../src/test/mock-db.ts","../../src/test/context.ts"],"sourcesContent":["import type { DBClient, DBOps } from \"../endpoint.js\";\nimport type {\n TxPlanBody,\n TxPlanOpResult,\n TxPlanRejection,\n TxPlanResponse,\n TxWireGuard,\n TxWireOp,\n TxWireValue,\n} from \"../db/tx-plan.js\";\n\n/** Tracked records for assertions. */\ninterface TrackedRecords {\n inserted: Map<string, Record<string, unknown>[]>;\n updated: Map<string, Record<string, unknown>[]>;\n deleted: Map<string, string[]>;\n}\n\n/** Mock DB client with tracking and seed data support. */\nexport interface MockDBClient extends DBClient {\n /** Get records inserted into a table. */\n inserted(table: string): Record<string, unknown>[];\n /** Get records updated in a table. */\n updated(table: string): Record<string, unknown>[];\n /** Get IDs deleted from a table. */\n deleted(table: string): string[];\n /** Pre-seed data into a table for findById/findMany. */\n seed(table: string, data: Record<string, unknown>[]): void;\n}\n\n/** Create a mock DB client with in-memory tracking. */\nexport function createMockDB(): MockDBClient {\n const store = new Map<string, Record<string, unknown>[]>();\n const tracked: TrackedRecords = {\n inserted: new Map(),\n updated: new Map(),\n deleted: new Map(),\n };\n\n function rowsOf(table: string): Record<string, unknown>[] {\n let rows = store.get(table);\n if (!rows) {\n rows = [];\n store.set(table, rows);\n }\n return rows;\n }\n\n function track(\n map: Map<string, Record<string, unknown>[]>,\n table: string,\n row: Record<string, unknown>,\n ): void {\n const list = map.get(table);\n if (list) list.push(row);\n else map.set(table, [row]);\n }\n\n // Build the op surface first (the six string-keyed ops). `txPlan` below\n // interprets a whole plan against the SAME in-memory store and tracking maps,\n // so a transaction's writes are visible to later assertions exactly as a\n // direct write would be.\n const ops: DBOps = {\n async query(_sql: string, _params?: unknown[]) {\n return [];\n },\n\n async insert(table: string, data: Record<string, unknown>) {\n const record = { id: crypto.randomUUID(), ...data };\n rowsOf(table).push(record);\n track(tracked.inserted, table, record);\n return record;\n },\n\n async update(table: string, id: string, data: Record<string, unknown>) {\n const rows = store.get(table) ?? [];\n const idx = rows.findIndex((r) => r[\"id\"] === id);\n const updated = idx >= 0\n ? { ...rows[idx], ...data }\n : { id, ...data };\n if (idx >= 0) {\n rows[idx] = updated;\n }\n track(tracked.updated, table, updated);\n return updated;\n },\n\n async delete(table: string, id: string) {\n const rows = store.get(table) ?? [];\n const idx = rows.findIndex((r) => r[\"id\"] === id);\n if (idx >= 0) rows.splice(idx, 1);\n const list = tracked.deleted.get(table);\n if (list) list.push(id);\n else tracked.deleted.set(table, [id]);\n },\n\n async findById(table: string, id: string) {\n const rows = store.get(table) ?? [];\n return rows.find((r) => r[\"id\"] === id) ?? null;\n },\n\n async findMany(table: string, query?: Record<string, unknown>) {\n const rows = store.get(table) ?? [];\n if (!query) return rows;\n return rows.filter((row) =>\n Object.entries(query).every(([key, val]) => row[key] === val),\n );\n },\n };\n\n /**\n * Interpret a whole plan, atomically.\n *\n * The rollback is the point. A test that asserts \"the second write failed, so\n * the first one is not there\" must be able to FAIL — a mock that applied ops\n * and left them applied would pass that test while the real broker rolled the\n * transaction back, or the other way round. So the store and the tracking maps\n * are snapshotted, and any failure restores both before rejecting.\n *\n * The rejection carries the same envelope fields the runtime copies off the\n * broker's response (`error_code`, `slot`), because the SDK maps `slot` back\n * to the caller's own Error — a mock that rejected with a bare Error would\n * make every guard in every tenant test look like a generic failure.\n */\n async function txPlan(plan: TxPlanBody): Promise<TxPlanResponse> {\n const snapshot = new Map<string, Record<string, unknown>[]>();\n for (const [table, rows] of store) snapshot.set(table, [...rows]);\n const trackedSnapshot: TrackedRecords = {\n inserted: cloneTracked(tracked.inserted),\n updated: cloneTracked(tracked.updated),\n deleted: new Map([...tracked.deleted].map(([k, v]) => [k, [...v]])),\n };\n\n const results: TxPlanOpResult[] = [];\n try {\n for (const op of plan.ops) {\n const result = applyOp(op, results);\n results.push(result);\n const failure = guardFailure(op.guard, result.rows.length);\n if (failure) throw failure;\n }\n } catch (err) {\n store.clear();\n for (const [table, rows] of snapshot) store.set(table, rows);\n tracked.inserted = trackedSnapshot.inserted;\n tracked.updated = trackedSnapshot.updated;\n tracked.deleted = trackedSnapshot.deleted;\n throw err;\n }\n return { results };\n }\n\n function applyOp(op: TxWireOp, results: TxPlanOpResult[]): TxPlanOpResult {\n switch (op.op) {\n case \"insert\": {\n const record = { id: crypto.randomUUID(), ...resolveMap(op.values ?? {}, results, null) };\n rowsOf(op.table).push(record);\n track(tracked.inserted, op.table, record);\n return { rows: [record], rows_affected: 1 };\n }\n case \"insertMany\": {\n const written = (op.rows ?? []).map((row) => {\n const record = { id: crypto.randomUUID(), ...resolveMap(row, results, null) };\n rowsOf(op.table).push(record);\n track(tracked.inserted, op.table, record);\n return record;\n });\n return { rows: written, rows_affected: written.length };\n }\n case \"update\": {\n const rows = rowsOf(op.table);\n const where = resolveMap(op.where ?? {}, results, null);\n const written: Record<string, unknown>[] = [];\n for (let i = 0; i < rows.length; i++) {\n const row = rows[i];\n if (!row || !matches(row, where)) continue;\n const next = { ...row, ...resolveMap(op.set ?? {}, results, row) };\n rows[i] = next;\n track(tracked.updated, op.table, next);\n written.push(next);\n }\n return { rows: written, rows_affected: written.length };\n }\n case \"delete\": {\n const rows = rowsOf(op.table);\n const where = resolveMap(op.where ?? {}, results, null);\n const removed = rows.filter((row) => matches(row, where));\n for (const row of removed) {\n rows.splice(rows.indexOf(row), 1);\n const id = row[\"id\"];\n const list = tracked.deleted.get(op.table);\n const key = typeof id === \"string\" ? id : String(id);\n if (list) list.push(key);\n else tracked.deleted.set(op.table, [key]);\n }\n return { rows: removed, rows_affected: removed.length };\n }\n case \"select\": {\n const where = resolveMap(op.where ?? {}, results, null);\n let found = rowsOf(op.table).filter((row) => matches(row, where));\n if (op.limit !== undefined) found = found.slice(0, op.limit);\n return { rows: found, rows_affected: found.length };\n }\n }\n }\n\n const client: MockDBClient = {\n ...ops,\n\n txPlan,\n\n // In tests there is no real DB role; `asService()` returns the same\n // in-memory client so RLS-bypass code paths still hit the same store and\n // tracking maps. The omitted `asService` matches the contract (no\n // double-bypass), so callers can't recurse.\n asService(): Omit<DBClient, \"asService\"> {\n return client;\n },\n\n inserted(table: string) {\n return tracked.inserted.get(table) ?? [];\n },\n\n updated(table: string) {\n return tracked.updated.get(table) ?? [];\n },\n\n deleted(table: string) {\n return tracked.deleted.get(table) ?? [];\n },\n\n seed(table: string, data: Record<string, unknown>[]) {\n store.set(table, [...data]);\n },\n };\n\n return client;\n}\n\nfunction cloneTracked(\n map: Map<string, Record<string, unknown>[]>,\n): Map<string, Record<string, unknown>[]> {\n return new Map([...map].map(([k, v]) => [k, [...v]]));\n}\n\n/** Resolve one plan value: a `$ref` into an earlier result, a `$expr`, or a\n * literal. `current` is the row being updated, which is what `inc`/`dec` read. */\nfunction resolveValue(\n value: TxWireValue,\n results: TxPlanOpResult[],\n current: Record<string, unknown> | null,\n column: string,\n): unknown {\n if (typeof value !== \"object\" || value === null) return value;\n const tagged = value as { $ref?: { op: number; field: string }; $expr?: Record<string, unknown> };\n\n if (tagged.$ref) {\n const row = results[tagged.$ref.op]?.rows[0];\n if (!row) {\n throw txRejection(409, \"tx_ref_unresolved\", {\n message: `operation ${tagged.$ref.op} produced no row to reference`,\n });\n }\n return row[tagged.$ref.field];\n }\n\n if (tagged.$expr) {\n const fn = tagged.$expr[\"fn\"];\n if (fn === \"now\") return new Date().toISOString();\n const by = Number(tagged.$expr[\"by\"]);\n const base = Number(current?.[column] ?? 0);\n return fn === \"dec\" ? base - by : base + by;\n }\n\n return value;\n}\n\nfunction resolveMap(\n map: Record<string, TxWireValue>,\n results: TxPlanOpResult[],\n current: Record<string, unknown> | null,\n): Record<string, unknown> {\n const out: Record<string, unknown> = {};\n for (const [key, value] of Object.entries(map)) {\n out[key] = resolveValue(value, results, current, key);\n }\n return out;\n}\n\n/** Equality filter, with `null` meaning IS NULL — the broker's rule, so a\n * `{ accepted_at: null }` guard behaves the same in a test as in production. */\nfunction matches(row: Record<string, unknown>, where: Record<string, unknown>): boolean {\n return Object.entries(where).every(([key, value]) =>\n value === null ? row[key] === null || row[key] === undefined : row[key] === value,\n );\n}\n\nfunction guardFailure(guard: TxWireGuard | undefined, count: number): unknown {\n if (!guard) return null;\n const ok =\n guard.kind === \"one\"\n ? count === 1\n : guard.kind === \"none\"\n ? count === 0\n : guard.kind === \"atLeast\"\n ? count >= guard.n\n : count <= guard.n;\n if (ok) return null;\n return txRejection(409, \"tx_guard_failed\", {\n slot: guard.slot,\n message: `expected ${guard.kind} ${guard.n} row(s), got ${count}`,\n });\n}\n\n/** Build a rejection shaped like the one the runtime throws for a broker error:\n * an Error carrying the envelope's `status`/`error_code`/`slot`. */\nfunction txRejection(\n status: number,\n code: string,\n extra: { slot?: number; message: string },\n): Error & TxPlanRejection {\n const err = new Error(extra.message) as Error & TxPlanRejection;\n err.status = status;\n err.error_code = code;\n if (extra.slot !== undefined) err.slot = extra.slot;\n return err;\n}\n","import type {\n CacheClient,\n ClientInfo,\n Logger,\n PBRequest,\n PalbaseModuleClients,\n} from \"../endpoint.js\";\nimport type {\n PalbaseAuthClient,\n PalbaseStorageClient,\n PalbaseRealtimeClient,\n PalbaseFunctionsClient,\n PalbaseFlagsClient,\n PalbaseNotificationsClient,\n PalbaseAnalyticsClient,\n PalbaseLinksClient,\n} from \"../clients.js\";\nimport type { User } from \"../types.js\";\nimport { __setRuntime } from \"../runtime.js\";\nimport type { PurchasesService } from \"../purchases/service.js\";\nimport { createMockDB, type MockDBClient } from \"./mock-db.js\";\n\n/** Options for creating a test context. */\nexport interface TestContextOptions<TInput = unknown> {\n user?: User | null;\n input?: TInput;\n params?: Record<string, string>;\n query?: Record<string, string>;\n headers?: Record<string, string>;\n env?: Record<string, string>;\n db?: { seed?: Record<string, Record<string, unknown>[]> };\n}\n\n/** Log entry captured by the mock logger. */\nexport interface LogEntry {\n level: \"info\" | \"warn\" | \"error\" | \"debug\";\n message: string;\n args: unknown[];\n}\n\n/** Test context for exercising endpoint handlers.\n *\n * Handlers now receive a {@link PBRequest} (no services attached) and reach\n * services via the PascalCase singletons (`Database`, `Log`, …). So\n * `createTestContext` does two things:\n * 1. returns a `PBRequest` (the object you pass to `handler(...)`), and\n * 2. installs mock services into the runtime via `__setRuntime`, so the\n * singletons resolve to the same mocks while the handler runs.\n *\n * For assertions and for building sibling (worker/job/hook/webhook) contexts,\n * the mock service handles are also attached here (`db`, `log`, `cache`,\n * `queue`, `env`, plus the module clients and captured `logs`). The user\n * defaults to nullable in tests (`PBRequest<TInput, false>`) so test code can\n * pass any auth shape without a cast. */\nexport interface TestContext<TInput = unknown>\n extends PBRequest<TInput, false>,\n PalbaseModuleClients {\n db: MockDBClient;\n env: Record<string, string>;\n log: Logger;\n cache: CacheClient;\n /** Captured log entries. */\n logs: LogEntry[];\n}\n\n/** Create a mock logger that captures entries. */\nfunction createMockLogger(logs: LogEntry[]): Logger {\n return {\n info(message: string, ...args: unknown[]) {\n logs.push({ level: \"info\", message, args });\n },\n warn(message: string, ...args: unknown[]) {\n logs.push({ level: \"warn\", message, args });\n },\n error(message: string, ...args: unknown[]) {\n logs.push({ level: \"error\", message, args });\n },\n debug(message: string, ...args: unknown[]) {\n logs.push({ level: \"debug\", message, args });\n },\n };\n}\n\n/** Create a mock in-memory cache.\n *\n * Mirrors the runtime's JSON-typed semantics (values are arbitrary JSON, not\n * just strings). getOrSet is single-process here, so it does not need the\n * distributed lock the real runtime uses — it is just get-miss → fn → set.\n * The cross-replica stampede protection is covered by the worker.js tests.\n */\nfunction createMockCache(): CacheClient {\n const store = new Map<string, unknown>();\n\n const get = async <T = unknown>(key: string): Promise<T | null> => {\n return store.has(key) ? (store.get(key) as T) : null;\n };\n const set = async (key: string, value: unknown, _ttl?: number): Promise<void> => {\n store.set(key, value);\n };\n\n return {\n get,\n set,\n async del(key: string) {\n store.delete(key);\n },\n async incr(key: string) {\n const raw = store.get(key);\n const current = typeof raw === \"number\" ? raw : parseInt(String(raw ?? \"0\"), 10);\n const next = current + 1;\n store.set(key, next);\n return next;\n },\n async getOrSet<T>(key: string, ttl: number, fn: () => Promise<T> | T): Promise<T> {\n const hit = await get<T>(key);\n if (hit !== null) {\n return hit;\n }\n const value = await fn();\n await set(key, value, ttl);\n return value;\n },\n };\n}\n\n/** Create mock Palbase module clients (Documents, Storage, …).\n *\n * Every slot throws with a descriptive error so tests that access a module\n * client surface without configuring it fail loudly rather than silently\n * returning undefined. Override individual clients on the returned context for\n * tests that need them.\n */\nfunction createMockModuleClients(): PalbaseModuleClients {\n const notImpl = (label: string): never => {\n throw new Error(\n `${label} not configured in test mock — override the matching client on the returned context`,\n );\n };\n\n const docs: PalbaseModuleClients[\"docs\"] = {\n collection: () => notImpl(\"docs.collection\"),\n doc: () => notImpl(\"docs.doc\"),\n };\n\n const auth: PalbaseAuthClient = {\n verifyUserToken: () => notImpl(\"auth.verifyUserToken\"),\n getSession: () => notImpl(\"auth.getSession\"),\n mfa: {\n enroll: () => notImpl(\"auth.mfa.enroll\"),\n verifyEnrollment: () => notImpl(\"auth.mfa.verifyEnrollment\"),\n challenge: () => notImpl(\"auth.mfa.challenge\"),\n recovery: () => notImpl(\"auth.mfa.recovery\"),\n listFactors: () => notImpl(\"auth.mfa.listFactors\"),\n removeFactor: () => notImpl(\"auth.mfa.removeFactor\"),\n regenerateRecoveryCodes: () => notImpl(\"auth.mfa.regenerateRecoveryCodes\"),\n emailEnroll: () => notImpl(\"auth.mfa.emailEnroll\"),\n emailChallenge: () => notImpl(\"auth.mfa.emailChallenge\"),\n emailVerify: () => notImpl(\"auth.mfa.emailVerify\"),\n },\n device: {\n generateChallenge: () => notImpl(\"auth.device.generateChallenge\"),\n attestAndroid: () => notImpl(\"auth.device.attestAndroid\"),\n attestiOS: () => notImpl(\"auth.device.attestiOS\"),\n bind: () => notImpl(\"auth.device.bind\"),\n list: () => notImpl(\"auth.device.list\"),\n delete: () => notImpl(\"auth.device.delete\"),\n verifyRequestSignature: () => notImpl(\"auth.device.verifyRequestSignature\"),\n getToken: () => notImpl(\"auth.device.getToken\"),\n get isActive(): never {\n return notImpl(\"auth.device.isActive\");\n },\n setCachedToken: () => notImpl(\"auth.device.setCachedToken\"),\n dispose: () => notImpl(\"auth.device.dispose\"),\n },\n };\n\n const storage: PalbaseStorageClient = {\n bucket: () => notImpl(\"storage.bucket\"),\n };\n\n const realtime: PalbaseRealtimeClient = {\n broadcast: async () => ({ data: undefined, error: null }),\n };\n\n const functions: PalbaseFunctionsClient = {\n invoke: () => notImpl(\"functions.invoke\"),\n };\n\n const flags: PalbaseFlagsClient = {\n isEnabled: () => notImpl(\"flags.isEnabled\"),\n getVariant: () => notImpl(\"flags.getVariant\"),\n getAll: () => notImpl(\"flags.getAll\"),\n setOverride: () => notImpl(\"flags.setOverride\"),\n asService: () => ({\n setOverrideForUser: () => notImpl(\"flags.asService.setOverrideForUser\"),\n setOverridesForUser: () => notImpl(\"flags.asService.setOverridesForUser\"),\n clearOverrideForUser: () => notImpl(\"flags.asService.clearOverrideForUser\"),\n clearAllOverridesForUser: () => notImpl(\"flags.asService.clearAllOverridesForUser\"),\n batchSetOverrides: () => notImpl(\"flags.asService.batchSetOverrides\"),\n }),\n };\n\n const notifications: PalbaseNotificationsClient = {\n push: { send: () => notImpl(\"notifications.push.send\") },\n email: { send: () => notImpl(\"notifications.email.send\") },\n sms: { send: () => notImpl(\"notifications.sms.send\") },\n verifications: {\n start: () => notImpl(\"notifications.verifications.start\"),\n check: () => notImpl(\"notifications.verifications.check\"),\n },\n inbox: {\n send: () => notImpl(\"notifications.inbox.send\"),\n list: () => notImpl(\"notifications.inbox.list\"),\n unreadCount: () => notImpl(\"notifications.inbox.unreadCount\"),\n markRead: () => notImpl(\"notifications.inbox.markRead\"),\n markAllRead: () => notImpl(\"notifications.inbox.markAllRead\"),\n archive: () => notImpl(\"notifications.inbox.archive\"),\n },\n preferences: {\n get: () => notImpl(\"notifications.preferences.get\"),\n update: () => notImpl(\"notifications.preferences.update\"),\n },\n templates: {\n email: {\n list: () => notImpl(\"notifications.templates.email.list\"),\n get: () => notImpl(\"notifications.templates.email.get\"),\n create: () => notImpl(\"notifications.templates.email.create\"),\n update: () => notImpl(\"notifications.templates.email.update\"),\n delete: () => notImpl(\"notifications.templates.email.delete\"),\n },\n sms: {\n list: () => notImpl(\"notifications.templates.sms.list\"),\n get: () => notImpl(\"notifications.templates.sms.get\"),\n create: () => notImpl(\"notifications.templates.sms.create\"),\n update: () => notImpl(\"notifications.templates.sms.update\"),\n delete: () => notImpl(\"notifications.templates.sms.delete\"),\n },\n },\n registerDevice: () => notImpl(\"notifications.registerDevice\"),\n unregisterDevice: () => notImpl(\"notifications.unregisterDevice\"),\n };\n\n const analytics: PalbaseAnalyticsClient = {\n capture: () => notImpl(\"analytics.capture\"),\n identify: () => notImpl(\"analytics.identify\"),\n screen: () => notImpl(\"analytics.screen\"),\n query: {\n count: () => notImpl(\"analytics.query.count\"),\n events: () => notImpl(\"analytics.query.events\"),\n properties: () => notImpl(\"analytics.query.properties\"),\n users: () => notImpl(\"analytics.query.users\"),\n funnel: () => notImpl(\"analytics.query.funnel\"),\n retention: () => notImpl(\"analytics.query.retention\"),\n cohort: () => notImpl(\"analytics.query.cohort\"),\n },\n management: {\n overview: () => notImpl(\"analytics.management.overview\"),\n eventNames: () => notImpl(\"analytics.management.eventNames\"),\n userDetail: () => notImpl(\"analytics.management.userDetail\"),\n deleteUser: () => notImpl(\"analytics.management.deleteUser\"),\n },\n };\n\n const links: PalbaseLinksClient = {\n create: () => notImpl(\"links.create\"),\n list: () => notImpl(\"links.list\"),\n get: () => notImpl(\"links.get\"),\n update: () => notImpl(\"links.update\"),\n delete: () => notImpl(\"links.delete\"),\n analytics: () => notImpl(\"links.analytics\"),\n qrCode: () => notImpl(\"links.qrCode\"),\n match: () => notImpl(\"links.match\"),\n };\n\n return {\n auth,\n storage,\n docs,\n realtime,\n functions,\n flags,\n notifications,\n analytics,\n links,\n };\n}\n\n/** A permissive purchases double: the subject resolves, the entitlement is\n * present, and a spend just runs its handler. That keeps a unit test of a\n * decorated handler about the handler's own logic instead of about billing.\n *\n * ponytail: deliberately has no \"deny\" mode. The 403/429 paths are about the\n * server's real accounting (reserve/commit/cancel, quota windows), and a double\n * that answered them would be asserting its own script — those belong in an\n * integration test against a real palstore, which is how they are covered.\n */\nfunction createMockPurchases(): PurchasesService {\n return {\n resolveSubject: async ({ userRef }) => ({ subjectId: `psj_test_${userRef}` }),\n require: async () => undefined,\n withSpend: async (_subjectId, _key, _opts, handler) => handler(),\n };\n}\n\n/** Null-by-default calling-client metadata for tests. */\nconst NULL_CLIENT_INFO: ClientInfo = {\n sdkVersion: null,\n appVersion: null,\n platform: null,\n osVersion: null,\n};\n\n/** Create a fully mocked endpoint test context.\n *\n * Returns a `PBRequest` (pass it to `handler(...)`) with the mock service\n * handles attached for assertions, and installs those mocks into the runtime\n * via `__setRuntime` so the `Database`/`Log`/… singletons resolve to them\n * while the handler runs.\n */\nexport function createTestContext<TInput = unknown>(\n options: TestContextOptions<TInput> = {},\n): TestContext<TInput> {\n const logs: LogEntry[] = [];\n const db = createMockDB();\n\n // Seed data if provided\n if (options.db?.seed) {\n for (const [table, data] of Object.entries(options.db.seed)) {\n db.seed(table, data);\n }\n }\n\n const log = createMockLogger(logs);\n const cache = createMockCache();\n const moduleClients = createMockModuleClients();\n\n // Install the mocks so the PascalCase singletons (Database, Log, …) resolve\n // to them while the handler under test runs.\n __setRuntime({\n Database: db,\n Documents: moduleClients.docs,\n Storage: moduleClients.storage,\n Cache: cache,\n Log: log,\n Notifications: moduleClients.notifications,\n Flags: moduleClients.flags,\n Realtime: moduleClients.realtime,\n Purchases: createMockPurchases(),\n });\n\n const ctx: TestContext<TInput> = {\n input: (options.input ?? {}) as TInput,\n params: options.params ?? {},\n query: options.query ?? {},\n headers: options.headers ?? {},\n user: options.user ?? null,\n client: NULL_CLIENT_INFO,\n method: \"POST\",\n file: null,\n db,\n env: options.env ?? {},\n log,\n cache,\n ...moduleClients,\n // Empty errors map in tests by default. Tests that exercise an endpoint's\n // declared errors construct their own throwers; this stub satisfies the\n // PBRequest shape without forcing every test to declare `errors:`.\n errors: {},\n requestId: \"req_test_000000000000\",\n traceId: \"0\".repeat(32),\n spanId: \"0\".repeat(16),\n logs,\n };\n\n return ctx;\n}\n"],"mappings":"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","names":[]}
|
package/docs/README.md
CHANGED
|
@@ -25,7 +25,7 @@ db/schema.ts # config-as-code Postgres schema (tables, co
|
|
|
25
25
|
|
|
26
26
|
The four folders above are the daily surface. These also exist (own docs, linked
|
|
27
27
|
below): `resources/` (external connections — [resources.md](./resources.md)),
|
|
28
|
-
`seeds/` (seed data), `jobs/`
|
|
28
|
+
`seeds/` (seed data), `jobs/` (background — [background.md](./background.md)),
|
|
29
29
|
`webhooks/` + `hooks/` (events — [events.md](./events.md)), `middleware/`.
|
|
30
30
|
|
|
31
31
|
### The 7 rules (checklist)
|
|
@@ -216,14 +216,13 @@ The **only difference** is the trigger argument:
|
|
|
216
216
|
| You are writing… | Handler signature | Trigger arg |
|
|
217
217
|
|------------------|-------------------|-------------|
|
|
218
218
|
| **Endpoints** (`controllers/` class controllers) | method `(…params)` | parameter decorators `@Body`/`@QueryParams`/`@Param`/`@User`/… — [endpoints.md](./endpoints.md) |
|
|
219
|
-
| **Workers** (`workers/**`) | `(payload, meta)` | typed payload + `WorkerMeta` |
|
|
220
219
|
| **Jobs** (`jobs/**`) | `(meta)` | `JobMeta` |
|
|
221
220
|
| **Hooks** (`hooks/**`) | `(event, meta)` | typed event + `HookMeta` |
|
|
222
221
|
| **Webhooks** (`webhooks/**`) | `(event, meta)` | typed event + `WebhookMeta` |
|
|
223
222
|
| **Middleware** (`middleware/**`) | `(ctx, next)` | `MiddlewareContext` — the **one exception** |
|
|
224
223
|
|
|
225
224
|
`meta` carries non-service data: `env` (Environment variables),
|
|
226
|
-
`environmentId`, and for
|
|
225
|
+
`environmentId`, and for webhooks `requestId`. Services always come from
|
|
227
226
|
the imported singletons — not from `ctx` or any argument.
|
|
228
227
|
|
|
229
228
|
## Project shape
|
|
@@ -241,7 +240,6 @@ my-backend/
|
|
|
241
240
|
├── db/migrations/ # explicit SQL migrations for type changes (optional)
|
|
242
241
|
├── resources/ # external connections, set up once at boot (optional)
|
|
243
242
|
├── seeds/ # seed data (optional)
|
|
244
|
-
├── workers/ # background job handlers (optional)
|
|
245
243
|
├── jobs/ # cron-scheduled jobs (optional)
|
|
246
244
|
├── hooks/ # auth/storage/document event hooks (optional)
|
|
247
245
|
├── webhooks/ # inbound provider webhooks (optional)
|
package/docs/auth.md
ADDED
|
@@ -0,0 +1,111 @@
|
|
|
1
|
+
# Authentication
|
|
2
|
+
|
|
3
|
+
Routes are **secure by default**: every endpoint requires a signed-in user
|
|
4
|
+
unless it opts out with `auth: false`. Client SDKs attach the user's token
|
|
5
|
+
automatically, so on the backend you declare what a route needs and inject the
|
|
6
|
+
user.
|
|
7
|
+
|
|
8
|
+
```ts
|
|
9
|
+
import { Controller, Get, Post, Body, User, OptionalUser } from "@palbase/backend";
|
|
10
|
+
import type { UserT } from "@palbase/backend";
|
|
11
|
+
|
|
12
|
+
@Controller("/todos") // no auth option → every route needs a user
|
|
13
|
+
export default class TodosController {
|
|
14
|
+
@Post("")
|
|
15
|
+
create(@Body(CreateTodoBody) body: CreateTodoBody, @User() user: UserT) {
|
|
16
|
+
return todoService.create(user.id, body.title); // user is non-null — guaranteed
|
|
17
|
+
}
|
|
18
|
+
|
|
19
|
+
@Get("/featured", { auth: false }) // one public route
|
|
20
|
+
featured(@OptionalUser() user: UserT | null) {
|
|
21
|
+
return todoService.featured(user?.id ?? null); // may be null — handle it
|
|
22
|
+
}
|
|
23
|
+
}
|
|
24
|
+
```
|
|
25
|
+
|
|
26
|
+
## What `@User()` gives you
|
|
27
|
+
|
|
28
|
+
```ts
|
|
29
|
+
interface User {
|
|
30
|
+
id: string;
|
|
31
|
+
email?: string; // absent for phone-only users
|
|
32
|
+
emailVerified: boolean;
|
|
33
|
+
role: string;
|
|
34
|
+
metadata: Record<string, unknown>;
|
|
35
|
+
device: VerifiedDevice | null;
|
|
36
|
+
}
|
|
37
|
+
```
|
|
38
|
+
|
|
39
|
+
Every field is **server-resolved** from the verified token — nothing here is
|
|
40
|
+
client-settable. `emailVerified` in particular is read from the user's verified
|
|
41
|
+
profile, not from a JWT claim: a claim is only true as of when the token was
|
|
42
|
+
minted, so a user who verifies mid-session would keep reporting `false` until
|
|
43
|
+
their token expired.
|
|
44
|
+
|
|
45
|
+
## Email verification
|
|
46
|
+
|
|
47
|
+
The platform handles verification end to end. **You do not configure a sender**,
|
|
48
|
+
and `config/notifications.ts` is unrelated — that file declares providers for
|
|
49
|
+
**your app's own** notifications. Auth email goes out through Palbase's own
|
|
50
|
+
notification tenant, not yours.
|
|
51
|
+
|
|
52
|
+
What happens on `POST /auth/signup`:
|
|
53
|
+
|
|
54
|
+
1. The account is created with `email_verified = false`.
|
|
55
|
+
2. A verification email is sent — by default a **6-digit code, valid 5 minutes**.
|
|
56
|
+
(An Environment configured for link-based verification instead sends a link
|
|
57
|
+
token valid **24 hours**.)
|
|
58
|
+
3. The client calls `verifyEmail({ code, email })` — or `verifyEmail({ token })`
|
|
59
|
+
for the link form — then `resendVerification(email)` if it expired.
|
|
60
|
+
|
|
61
|
+
A send failure does **not** fail the signup: the account exists and the user can
|
|
62
|
+
re-trigger delivery. Resend is rate-limited per IP on the same budget as signup.
|
|
63
|
+
|
|
64
|
+
Branding (app name, logo, colours, support address) comes from the Environment's
|
|
65
|
+
auth branding settings, not from your code.
|
|
66
|
+
|
|
67
|
+
### Requiring a verified email
|
|
68
|
+
|
|
69
|
+
By default a new account **can sign in immediately**, verified or not.
|
|
70
|
+
|
|
71
|
+
To require verification, turn on **confirm email** for the Environment (Studio →
|
|
72
|
+
Auth → Policy, or `confirm_email_required` on the auth settings API). With it on:
|
|
73
|
+
|
|
74
|
+
- signup creates the account and returns the user, but **no tokens** — the
|
|
75
|
+
response carries no `access_token`, so there is no session until the address
|
|
76
|
+
is confirmed;
|
|
77
|
+
- login returns `403 email_not_confirmed` until it is.
|
|
78
|
+
|
|
79
|
+
That is the whole gate, and it sits at the credential layer where it cannot be
|
|
80
|
+
routed around.
|
|
81
|
+
|
|
82
|
+
For finer control — say, letting a user finish a profile before confirming —
|
|
83
|
+
read the flag in your handler. It costs nothing; it is already on the request:
|
|
84
|
+
|
|
85
|
+
```ts
|
|
86
|
+
@Post("/publish")
|
|
87
|
+
publish(@User() user: UserT) {
|
|
88
|
+
if (!user.emailVerified) {
|
|
89
|
+
throw new Forbidden("Confirm your email address before publishing.");
|
|
90
|
+
}
|
|
91
|
+
return postService.publish(user.id);
|
|
92
|
+
}
|
|
93
|
+
```
|
|
94
|
+
|
|
95
|
+
> There is no per-route `requireVerifiedEmail` option. A route-level flag would
|
|
96
|
+
> have to be enforced by the runtime, and the one-line check above is enforced by
|
|
97
|
+
> your own code — visible where it applies, and impossible to declare on a route
|
|
98
|
+
> and have quietly do nothing.
|
|
99
|
+
|
|
100
|
+
## Password reset and magic links
|
|
101
|
+
|
|
102
|
+
Both are client-driven and need no backend code: the client SDK calls the auth
|
|
103
|
+
endpoints, Palbase sends the mail, the user completes the flow, and your next
|
|
104
|
+
request simply arrives with a valid token. Reset tokens and magic links are
|
|
105
|
+
single-use and expire; a used or expired one fails closed with a `400`.
|
|
106
|
+
|
|
107
|
+
## Related
|
|
108
|
+
|
|
109
|
+
- [Row-Level Security](./schema.md#row-level-security-rls) — pushing per-user
|
|
110
|
+
access rules into Postgres, where `auth.uid()` is this same verified user.
|
|
111
|
+
- [Database](./database.md) — how `Database.asService()` steps outside RLS.
|
package/docs/background.md
CHANGED
|
@@ -1,42 +1,18 @@
|
|
|
1
|
-
#
|
|
2
|
-
|
|
3
|
-
|
|
4
|
-
singletons as endpoints (`import { Database, Log } from
|
|
5
|
-
|
|
6
|
-
non-service data (`env`,
|
|
7
|
-
|
|
8
|
-
|
|
9
|
-
|
|
10
|
-
|
|
11
|
-
|
|
12
|
-
|
|
13
|
-
|
|
14
|
-
|
|
15
|
-
|
|
16
|
-
|
|
17
|
-
interface OrderPayload { orderId: string; amount: number; }
|
|
18
|
-
|
|
19
|
-
export default defineWorker<OrderPayload>({
|
|
20
|
-
name: "process-order", // must match the Queue.push() name
|
|
21
|
-
retry: 5, // optional, default 3
|
|
22
|
-
timeout: 60, // optional, seconds
|
|
23
|
-
backoff: "exponential", // "exponential" | "linear" | "fixed", default exponential
|
|
24
|
-
handler: async (payload, meta) => {
|
|
25
|
-
Log.info(`processing ${payload.orderId} (env ${meta.environmentId})`);
|
|
26
|
-
await Database.update("orders", payload.orderId, { status: "processed" });
|
|
27
|
-
},
|
|
28
|
-
});
|
|
29
|
-
```
|
|
30
|
-
|
|
31
|
-
`meta` shape: `{ env, user, requestId, environmentId }`. Environment
|
|
32
|
-
variables are in `meta.env`; services come from the imported singletons.
|
|
33
|
-
|
|
34
|
-
Enqueue from an endpoint:
|
|
35
|
-
|
|
36
|
-
```ts
|
|
37
|
-
import { Queue } from "@palbase/backend";
|
|
38
|
-
await Queue.push("process-order", { orderId: "ord_1", amount: 1000 });
|
|
39
|
-
```
|
|
1
|
+
# Background Jobs
|
|
2
|
+
|
|
3
|
+
`jobs/` is the background rail. A job uses the **singleton model** — the same
|
|
4
|
+
imported service singletons as endpoints (`import { Database, Log } from
|
|
5
|
+
"@palbase/backend"`). It does **not** receive a `req`; a small `meta` argument
|
|
6
|
+
carries the non-service data (`env`, correlation ids).
|
|
7
|
+
|
|
8
|
+
> **There is no queue.** `Queue.push` and `defineWorker` existed in earlier
|
|
9
|
+
> versions and never ran: nothing consumed the queue, so a push returned a job id
|
|
10
|
+
> for work that was never performed. Both are removed, and a `workers/` directory
|
|
11
|
+
> now fails the deploy rather than deploying green and doing nothing. Model
|
|
12
|
+
> queue-shaped work as a job that sweeps its own table: write a row with a
|
|
13
|
+
> `status` column, and let a cron job pick up the pending ones. A job may run for
|
|
14
|
+
> up to 300 seconds, which is the longest budget available anywhere on the
|
|
15
|
+
> platform.
|
|
40
16
|
|
|
41
17
|
## Jobs (cron-scheduled)
|
|
42
18
|
|
package/docs/config.md
CHANGED
|
@@ -1,11 +1,11 @@
|
|
|
1
1
|
# Module Config (config-as-code)
|
|
2
2
|
|
|
3
|
-
Beyond `db/schema.ts`,
|
|
4
|
-
buckets, notification providers,
|
|
5
|
-
in `config/*.ts` files (typed, imported from
|
|
6
|
-
push` the deploy creates/updates them. Secrets
|
|
7
|
-
go in git — they live in a reserved encrypted env
|
|
8
|
-
guided CLI.
|
|
3
|
+
Beyond `db/schema.ts`, four more module surfaces are git-authoritative: storage
|
|
4
|
+
buckets, notification providers, feature-flag definitions, and the outbound-HTTP
|
|
5
|
+
allowlist. You declare them in `config/*.ts` files (typed, imported from
|
|
6
|
+
`@palbase/backend`) and on `git push` the deploy creates/updates them. Secrets
|
|
7
|
+
(certs, keys, API tokens) NEVER go in git — they live in a reserved encrypted env
|
|
8
|
+
namespace, uploaded by the guided CLI.
|
|
9
9
|
|
|
10
10
|
You normally author these with `palbase <module> add …` (the CLI writes the
|
|
11
11
|
config file + uploads any secret); the files below are what it generates.
|
|
@@ -98,9 +98,50 @@ Author it: `palbase flags add new_checkout --type boolean --default false`, or
|
|
|
98
98
|
deploy, the definitions are upserted to the flags service (idempotent). A flag
|
|
99
99
|
removed from the file is **not auto-deleted** (orphan definitions are harmless).
|
|
100
100
|
|
|
101
|
+
## Outbound HTTP — `config/egress.ts`
|
|
102
|
+
|
|
103
|
+
Your backend has **no ambient network**. A `fetch()` to an external host is
|
|
104
|
+
refused unless the host is declared here, and with no `config/egress.ts` at all
|
|
105
|
+
there is no outbound network whatsoever.
|
|
106
|
+
|
|
107
|
+
```ts
|
|
108
|
+
import { defineEgress } from "@palbase/backend";
|
|
109
|
+
|
|
110
|
+
export default defineEgress({
|
|
111
|
+
hosts: ["api.openai.com", ".example.com"], // leading dot also covers subdomains
|
|
112
|
+
timeoutMs: 90_000, // per-call ceiling; omitted ⇒ 30_000
|
|
113
|
+
});
|
|
114
|
+
```
|
|
115
|
+
|
|
116
|
+
Hosts are bare hostnames — https on :443 only, so no scheme, port, path or
|
|
117
|
+
wildcard. `timeoutMs` is 1_000–300_000.
|
|
118
|
+
|
|
119
|
+
Unlike the three above, this one is **fail-closed**: a malformed host or an
|
|
120
|
+
out-of-range `timeoutMs` ABORTS the deploy rather than logging a warning. An
|
|
121
|
+
allowlist that silently dropped an entry would be a broken feature, and one that
|
|
122
|
+
silently widened would be a hole; an out-of-range timeout is rejected rather than
|
|
123
|
+
clamped so your config file and the running system never disagree.
|
|
124
|
+
|
|
125
|
+
### How long a call may take
|
|
126
|
+
|
|
127
|
+
`timeoutMs` is a ceiling, not a grant — the call still ends when the invocation
|
|
128
|
+
around it ends, and it covers the whole call including redirects (three hops do
|
|
129
|
+
not get three budgets).
|
|
130
|
+
|
|
131
|
+
| Where the fetch runs | What else bounds it |
|
|
132
|
+
|---|---|
|
|
133
|
+
| Job (`jobs/`) | Its own `@Job({ timeout })`, max 300s — the longest budget available. |
|
|
134
|
+
| Endpoint / webhook | The gateway's request ceiling. Long work belongs in a job. |
|
|
135
|
+
|
|
136
|
+
Responses are **buffered whole** (5 MB cap) before your `fetch()` resolves.
|
|
137
|
+
Requesting a streaming response from an upstream (`stream: true`, SSE) therefore
|
|
138
|
+
buys nothing: no partial output, no earlier first byte, and the entire stream
|
|
139
|
+
must still finish inside `timeoutMs`.
|
|
140
|
+
|
|
101
141
|
## How it's applied
|
|
102
142
|
|
|
103
|
-
All
|
|
104
|
-
|
|
105
|
-
|
|
106
|
-
|
|
143
|
+
All four are evaluated + applied **on deploy**, the same place `db/schema.ts`
|
|
144
|
+
migrations run, reaching each module through your project's gateway with a
|
|
145
|
+
service-role key. Storage/notifications/flags are fail-soft — a config error logs
|
|
146
|
+
a warning but never aborts the deploy of your code. `config/egress.ts` is the
|
|
147
|
+
exception and is fail-closed, for the reason above.
|
package/docs/database.md
CHANGED
|
@@ -164,9 +164,17 @@ A plan may carry at most 1000 operations, 5000 rows in one `insertMany`, and
|
|
|
164
164
|
## Bypassing RLS — `Database.asService()`
|
|
165
165
|
|
|
166
166
|
When a table has [Row-Level Security](./schema.md#row-level-security-rls)
|
|
167
|
-
policies, every `Database.*` call runs as the request's verified user
|
|
168
|
-
|
|
169
|
-
|
|
167
|
+
policies, every `Database.*` call runs as the request's verified user, so the
|
|
168
|
+
database filters out rows the user's policies don't allow. That is the secure
|
|
169
|
+
default.
|
|
170
|
+
|
|
171
|
+
The Postgres role it connects as is **`backend_authenticated`** (or
|
|
172
|
+
`backend_anon` when there is no signed-in user) — not `authenticated`. You
|
|
173
|
+
rarely need to know that, because a policy declared in `db/schema.ts` is
|
|
174
|
+
deployed targeting both. It matters in exactly one place: **hand-written
|
|
175
|
+
`CREATE POLICY` SQL in a migration must name both roles**, or it applies to
|
|
176
|
+
nothing your code does. See
|
|
177
|
+
[Row-Level Security](./schema.md#row-level-security-rls).
|
|
170
178
|
|
|
171
179
|
Sometimes you need to read or write **across all users** — an admin endpoint, a
|
|
172
180
|
background job that fans out notifications, a cleanup task. For that, call
|
package/docs/events.md
CHANGED
|
@@ -1,6 +1,6 @@
|
|
|
1
1
|
# Hooks & Webhooks
|
|
2
2
|
|
|
3
|
-
Like
|
|
3
|
+
Like jobs, hooks and webhooks use the **singleton model** — the same
|
|
4
4
|
imported service singletons as endpoints (`import { Database, Log } from
|
|
5
5
|
"@palbase/backend"`). They do **not** receive a `req`. A second `meta` argument
|
|
6
6
|
carries the non-service data (`env`, `environmentId`; webhooks also
|