@palbase/backend 10.3.0 → 12.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-7LAXRLPG.js +418 -0
- package/dist/chunk-7LAXRLPG.js.map +1 -0
- package/dist/{chunk-4WOQWFUP.js → chunk-LUV36KQU.js} +26 -11
- package/dist/{chunk-4WOQWFUP.js.map → chunk-LUV36KQU.js.map} +1 -1
- package/dist/{chunk-2PCNZBNZ.js → chunk-XATG7BRC.js} +20 -2
- package/dist/chunk-XATG7BRC.js.map +1 -0
- package/dist/db/index.cjs +438 -10
- package/dist/db/index.cjs.map +1 -1
- package/dist/db/index.d.cts +2 -2
- package/dist/db/index.d.ts +2 -2
- package/dist/db/index.js +13 -1
- package/dist/{endpoint-92kVepng.d.cts → endpoint-Ck4hER_7.d.cts} +343 -11
- package/dist/{endpoint-92kVepng.d.ts → endpoint-Ck4hER_7.d.ts} +343 -11
- package/dist/{index-BaHs33jr.d.cts → index-BJAf1uPC.d.cts} +64 -34
- package/dist/{index-DfryIw9U.d.ts → index-l7DhBDtn.d.ts} +64 -34
- package/dist/index.cjs +589 -124
- package/dist/index.cjs.map +1 -1
- package/dist/index.d.cts +60 -162
- package/dist/index.d.ts +60 -162
- package/dist/index.js +139 -108
- package/dist/index.js.map +1 -1
- package/dist/test/index.cjs +545 -12
- package/dist/test/index.cjs.map +1 -1
- package/dist/test/index.d.cts +1 -1
- package/dist/test/index.d.ts +1 -1
- package/dist/test/index.js +153 -12
- package/dist/test/index.js.map +1 -1
- package/docs/background.md +8 -9
- package/docs/database.md +106 -16
- package/docs/events.md +20 -17
- package/docs/llms-full.txt +141 -47
- package/docs/schema.md +6 -4
- package/package.json +1 -1
- package/dist/chunk-2PCNZBNZ.js.map +0 -1
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, TxClient } from \"../endpoint.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 // Build the op surface first (TxClient = the five DB ops + query). The\n // transaction below reuses this same `ops` object as the tx-scoped client,\n // so writes inside a transaction land in the same in-memory store and\n // tracking maps — exactly the semantics a user endpoint test expects.\n const ops: TxClient = {\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 if (!store.has(table)) store.set(table, []);\n store.get(table)!.push(record);\n if (!tracked.inserted.has(table)) tracked.inserted.set(table, []);\n tracked.inserted.get(table)!.push(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 if (!tracked.updated.has(table)) tracked.updated.set(table, []);\n tracked.updated.get(table)!.push(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 if (!tracked.deleted.has(table)) tracked.deleted.set(table, []);\n tracked.deleted.get(table)!.push(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 const client: MockDBClient = {\n ...ops,\n\n transaction<T>(fn: (tx: TxClient) => Promise<T>): Promise<T> {\n return fn(ops);\n },\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","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 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":[]}
|
package/docs/background.md
CHANGED
|
@@ -40,22 +40,21 @@ await Queue.push("process-order", { orderId: "ord_1", amount: 1000 });
|
|
|
40
40
|
|
|
41
41
|
## Jobs (cron-scheduled)
|
|
42
42
|
|
|
43
|
-
A job runs on a cron schedule. File lives under `jobs
|
|
43
|
+
A job runs on a cron schedule. File lives under `jobs/` — the job's name is
|
|
44
|
+
the file name, there is no `name` option.
|
|
44
45
|
|
|
45
46
|
```ts
|
|
46
47
|
// jobs/cleanup.ts
|
|
47
|
-
import {
|
|
48
|
+
import { Database, Job, Log, type JobMeta } from "@palbase/backend";
|
|
48
49
|
|
|
49
|
-
|
|
50
|
-
|
|
51
|
-
|
|
52
|
-
timeout: 120, // optional, seconds
|
|
53
|
-
handler: async (meta) => {
|
|
50
|
+
@Job({ schedule: "0 3 * * *", timeout: 120 }) // schedule: standard cron; timeout: optional, seconds
|
|
51
|
+
export default class CleanupJob {
|
|
52
|
+
async run(meta: JobMeta) {
|
|
54
53
|
const expired = await Database.findMany("sessions", { expired: true });
|
|
55
54
|
for (const s of expired) await Database.delete("sessions", s.id as string);
|
|
56
55
|
Log.info(`cleaned ${expired.length} sessions in ${meta.environmentId}`);
|
|
57
|
-
}
|
|
58
|
-
}
|
|
56
|
+
}
|
|
57
|
+
}
|
|
59
58
|
```
|
|
60
59
|
|
|
61
60
|
`meta` shape: `{ env, environmentId }`. No `user` (jobs are
|
package/docs/database.md
CHANGED
|
@@ -41,7 +41,7 @@ available:
|
|
|
41
41
|
| `Database.findById(table, id)` | the row or `null` |
|
|
42
42
|
| `Database.findMany(table, query?)` | matching rows (array) |
|
|
43
43
|
| `Database.query(sql, params?)` | rows from a read-only SQL query (runs in a READ ONLY transaction) |
|
|
44
|
-
| `Database.transaction(fn)` | runs
|
|
44
|
+
| `Database.transaction(fn)` | runs a whole transaction plan in one request |
|
|
45
45
|
|
|
46
46
|
`findMany`'s `query` is an equality filter: keys are ANDed together. For
|
|
47
47
|
anything richer (ranges, ordering, joins) use `Database.query`.
|
|
@@ -58,19 +58,108 @@ const rows = await Database.query(
|
|
|
58
58
|
|
|
59
59
|
## Transactions
|
|
60
60
|
|
|
61
|
-
|
|
62
|
-
|
|
61
|
+
A transaction is a **plan**, not a conversation. The callback DESCRIBES the
|
|
62
|
+
operations; the whole description travels in one request, and the broker runs it
|
|
63
|
+
inside a single transaction — committing when it finishes, rolling back on any
|
|
64
|
+
failure. That is unchanged. What changed is that no round trip happens in the
|
|
65
|
+
middle, so nothing holds a database connection open while your code thinks.
|
|
63
66
|
|
|
64
67
|
```ts
|
|
65
|
-
|
|
66
|
-
|
|
67
|
-
|
|
68
|
-
|
|
68
|
+
import { Database, NotFound } from "@palbase/backend";
|
|
69
|
+
|
|
70
|
+
const { orderId } = await Database.transaction((tx) => {
|
|
71
|
+
const order = tx.tables.orders
|
|
72
|
+
.insert({ amount: 1000, status: "pending" })
|
|
73
|
+
.expectOne(new NotFound("order could not be created"));
|
|
74
|
+
|
|
75
|
+
tx.tables.order_items.insertMany(
|
|
76
|
+
cart.map((line) => ({ order_id: order.id, sku: line.sku })),
|
|
77
|
+
);
|
|
78
|
+
|
|
79
|
+
return { orderId: order.id };
|
|
69
80
|
});
|
|
70
81
|
```
|
|
71
82
|
|
|
72
|
-
|
|
73
|
-
|
|
83
|
+
Four things follow from "it is a plan", and the compiler enforces all four.
|
|
84
|
+
|
|
85
|
+
**The callback is synchronous.** Nothing has run when it returns, so there is
|
|
86
|
+
nothing to await. `async` on the callback and `await` inside it are compile
|
|
87
|
+
errors.
|
|
88
|
+
|
|
89
|
+
**An operation returns a result handle, not a row.** `insert()` gives you a
|
|
90
|
+
`TxRows`; to read a column you first say what you expect:
|
|
91
|
+
|
|
92
|
+
| Expectation | Holds when | Gives you |
|
|
93
|
+
|---|---|---|
|
|
94
|
+
| `.expectOne(err)` | exactly 1 row | the row — the only way to read columns |
|
|
95
|
+
| `.expectNone(err)` | 0 rows | nothing |
|
|
96
|
+
| `.expectAtLeast(n, err)` | ≥ n rows | nothing |
|
|
97
|
+
| `.expectAtMost(n, err)` | ≤ n rows | nothing |
|
|
98
|
+
|
|
99
|
+
If an expectation does not hold, the whole transaction rolls back and **your**
|
|
100
|
+
`err` is thrown — the error object stays on this side, the server only reports
|
|
101
|
+
which expectation failed.
|
|
102
|
+
|
|
103
|
+
**A column you read is a `Ref`, not a value.** It is a promise of what the
|
|
104
|
+
server will produce. Write it into a later operation, or return it and read the
|
|
105
|
+
real value after `transaction()` resolves. You cannot print it, concatenate it,
|
|
106
|
+
do arithmetic on it, or `JSON.stringify` it — all of those throw.
|
|
107
|
+
|
|
108
|
+
> ⚠️ **`if (ref)` is always true.** JavaScript does not let a Ref refuse a
|
|
109
|
+
> truthiness test, so branching on one silently takes the wrong path and commits
|
|
110
|
+
> the wrong data. Never branch on a value the transaction has not produced yet:
|
|
111
|
+
> read it before the transaction, or express the condition as a filter plus an
|
|
112
|
+
> expectation.
|
|
113
|
+
|
|
114
|
+
**Reads you do not write move outside.** A lookup whose result the transaction
|
|
115
|
+
does not write is not part of the transaction:
|
|
116
|
+
|
|
117
|
+
```ts
|
|
118
|
+
// Before the transaction: an ordinary value you can branch on.
|
|
119
|
+
const overrides = await Database.tables.category_overrides.findMany({ household_id });
|
|
120
|
+
|
|
121
|
+
const stmt = await Database.transaction((tx) => { /* … */ });
|
|
122
|
+
```
|
|
123
|
+
|
|
124
|
+
### Writing conditions as filters
|
|
125
|
+
|
|
126
|
+
`if (row.accepted_at) throw new Conflict(…)` needs a real value, so it becomes a
|
|
127
|
+
filter plus an expectation — which is also stronger, because the check and the
|
|
128
|
+
write are now the same statement and nothing can slip between them:
|
|
129
|
+
|
|
130
|
+
```ts
|
|
131
|
+
tx.tables.invites
|
|
132
|
+
.updateWhere({ token, accepted_at: null }, { accepted_at: now() })
|
|
133
|
+
.expectOne(new Conflict("invite already used", "invite_used"));
|
|
134
|
+
```
|
|
135
|
+
|
|
136
|
+
### The table surface inside a transaction
|
|
137
|
+
|
|
138
|
+
| Operation | Notes |
|
|
139
|
+
|---|---|
|
|
140
|
+
| `insert(values)` | one row |
|
|
141
|
+
| `insertMany(rows)` | one statement; every row must set the same columns. An empty list writes nothing |
|
|
142
|
+
| `updateWhere(where, set)` | **filter first**. A filterless update is refused |
|
|
143
|
+
| `deleteWhere(where)` | a filterless delete is refused |
|
|
144
|
+
| `select(where?, { limit, lock })` | `lock: "update"` takes a real `FOR UPDATE` row lock |
|
|
145
|
+
|
|
146
|
+
A `where` is equality-only and ANDed; `null` means `IS NULL`.
|
|
147
|
+
|
|
148
|
+
Three expressions may appear in the values you write:
|
|
149
|
+
|
|
150
|
+
| Expression | Where | Meaning |
|
|
151
|
+
|---|---|---|
|
|
152
|
+
| `now()` | anywhere | the server's clock |
|
|
153
|
+
| `inc(n)` | `updateWhere`'s `set` | `column = column + n`, atomically |
|
|
154
|
+
| `dec(n)` | `updateWhere`'s `set` | `column = column - n`, atomically |
|
|
155
|
+
|
|
156
|
+
`inc`/`dec` read the column's current value, which an inserted row does not
|
|
157
|
+
have — using them in an `insert` is a compile error.
|
|
158
|
+
|
|
159
|
+
### Limits
|
|
160
|
+
|
|
161
|
+
A plan may carry at most 1000 operations, 5000 rows in one `insertMany`, and
|
|
162
|
+
8 MiB of JSON. Exceeding any of them is reported before the request is sent.
|
|
74
163
|
|
|
75
164
|
## Bypassing RLS — `Database.asService()`
|
|
76
165
|
|
|
@@ -95,9 +184,10 @@ const mine = await Database.tables.todos.findMany({});
|
|
|
95
184
|
const all = await Database.asService().tables.todos.findMany({});
|
|
96
185
|
const rows = await Database.asService().query("SELECT count(*) FROM todos");
|
|
97
186
|
|
|
98
|
-
// A service-role transaction (the role is fixed for the whole
|
|
99
|
-
await Database.asService().transaction(
|
|
100
|
-
|
|
187
|
+
// A service-role transaction (the role is fixed for the whole plan):
|
|
188
|
+
await Database.asService().transaction((tx) => {
|
|
189
|
+
tx.tables.todos.updateWhere({ id }, { done: true });
|
|
190
|
+
return null;
|
|
101
191
|
});
|
|
102
192
|
```
|
|
103
193
|
|
|
@@ -107,7 +197,7 @@ Guidelines:
|
|
|
107
197
|
only where you genuinely need cross-user access. It is intentionally easy to
|
|
108
198
|
grep for in review.
|
|
109
199
|
- **No double-bypass / no nesting.** The sibling does not re-expose
|
|
110
|
-
`asService()`, and `tx` never exposes it — a
|
|
111
|
-
|
|
112
|
-
`Database.asService().transaction(...)` for a service-role
|
|
113
|
-
enforced and bypassed
|
|
200
|
+
`asService()`, and `tx` never exposes it — a plan's role is fixed for the whole
|
|
201
|
+
transaction. Use `Database.transaction(...)` for an authenticated one and
|
|
202
|
+
`Database.asService().transaction(...)` for a service-role one; you cannot mix
|
|
203
|
+
enforced and bypassed operations inside a single plan.
|
package/docs/events.md
CHANGED
|
@@ -39,29 +39,32 @@ Available hook builders: `auth.onUserCreated`, `auth.onSignIn`, `auth.onSignOut`
|
|
|
39
39
|
## Webhooks (inbound provider events)
|
|
40
40
|
|
|
41
41
|
Receive and verify webhooks from third-party providers. Files live under
|
|
42
|
-
`webhooks
|
|
42
|
+
`webhooks/` — the URL is `POST /webhooks/<file-name>` (e.g. `webhooks/stripe.ts`
|
|
43
|
+
→ `POST /webhooks/stripe`); there is no `path` option.
|
|
43
44
|
|
|
44
45
|
```ts
|
|
45
46
|
// webhooks/stripe.ts
|
|
46
|
-
import {
|
|
47
|
+
import { Database, Log, On, Webhook, type WebhookMeta } from "@palbase/backend";
|
|
47
48
|
|
|
48
|
-
|
|
49
|
-
|
|
50
|
-
|
|
51
|
-
|
|
52
|
-
|
|
53
|
-
|
|
54
|
-
|
|
55
|
-
|
|
56
|
-
|
|
57
|
-
|
|
58
|
-
}
|
|
59
|
-
}
|
|
60
|
-
}
|
|
49
|
+
@Webhook({ provider: "stripe", secret: { env: "STRIPE_WEBHOOK_SECRET" } }) // signing secret resolved from env
|
|
50
|
+
export default class StripeWebhook {
|
|
51
|
+
@On("checkout.session.completed")
|
|
52
|
+
async checkoutCompleted(event: unknown, meta: WebhookMeta) {
|
|
53
|
+
await Database.insert("orders", { status: "paid", data: event });
|
|
54
|
+
}
|
|
55
|
+
|
|
56
|
+
@On("payment_intent.payment_failed")
|
|
57
|
+
async paymentFailed(event: unknown, meta: WebhookMeta) {
|
|
58
|
+
Log.error("payment failed");
|
|
59
|
+
await Database.insert("payment_failures", { data: event });
|
|
60
|
+
}
|
|
61
|
+
}
|
|
61
62
|
```
|
|
62
63
|
|
|
63
|
-
|
|
64
|
-
|
|
64
|
+
`provider` selects a preset signature scheme; a service with no preset spells
|
|
65
|
+
one out with `signature` instead — one of the two is required. The signing
|
|
66
|
+
secret is resolved by the runtime from `secret: { env: "NAME" }`; your
|
|
67
|
+
handlers access Environment variables via `meta.env`. The runtime verifies the
|
|
65
68
|
signature before dispatching to your event handlers.
|
|
66
69
|
|
|
67
70
|
`meta` shape: `{ env, requestId, environmentId }`.
|