@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.
@@ -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":[]}
@@ -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 { defineJob, Database, Log } from "@palbase/backend";
48
+ import { Database, Job, Log, type JobMeta } from "@palbase/backend";
48
49
 
49
- export default defineJob({
50
- name: "cleanup-expired",
51
- schedule: "0 3 * * *", // standard cron
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 `fn(tx)` in a transaction |
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
- `transaction(fn)` gives you a `tx` with the same DB ops (no nested
62
- transaction). Returning commits; throwing rolls back.
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
- await Database.transaction(async (tx) => {
66
- const order = await tx.tables.orders.insert({ amount: 1000, status: "pending" });
67
- await tx.tables.order_items.insert({ order_id: order.id, sku: "ABC" });
68
- // throw here → both inserts roll back
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
- The `tx` carries the same typed `tx.tables.<name>` API as `Database.tables`
73
- (no nested transaction). See [schema.md](./schema.md) for the full surface.
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 tx):
99
- await Database.asService().transaction(async (tx) => {
100
- await tx.tables.todos.update(id, { done: true });
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 transaction's role is fixed when
111
- it begins. Use `Database.transaction(...)` for an authenticated tx and
112
- `Database.asService().transaction(...)` for a service-role tx; you cannot mix
113
- enforced and bypassed ops inside one interactive transaction.
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 { defineWebhook, Database, Log } from "@palbase/backend";
47
+ import { Database, Log, On, Webhook, type WebhookMeta } from "@palbase/backend";
47
48
 
48
- export default defineWebhook({
49
- provider: "stripe",
50
- secret: { env: "STRIPE_WEBHOOK_SECRET" }, // signing secret resolved from env
51
- events: {
52
- "checkout.session.completed": async (event, meta) => {
53
- await Database.insert("orders", { status: "paid", data: event });
54
- },
55
- "payment_intent.payment_failed": async (event, meta) => {
56
- Log.error("payment failed");
57
- await Database.insert("payment_failures", { data: event });
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
- The signing secret is resolved by the runtime from `secret: { env: "NAME" }`;
64
- your handlers access Environment variables via `meta.env`. The runtime verifies the
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 }`.