@palbase/backend 15.0.0 → 17.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/index.ts","../../src/runtime.ts","../../src/db/tx-plan.ts","../../src/test/mock-db.ts","../../src/test/context.ts"],"sourcesContent":["export { createTestContext } from \"./context.js\";\nexport type { TestContextOptions, TestContext, LogEntry } from \"./context.js\";\nexport { createMockDB } from \"./mock-db.js\";\nexport type { MockDBClient } from \"./mock-db.js\";\n","/**\n * runtime.ts — request-scoped service singletons.\n *\n * The backend SDK no longer threads a `ctx` god-object through every handler.\n * Instead, controller methods import PascalCase service singletons directly:\n *\n * import { Controller, Post, Body, Database } from \"@palbase/backend\";\n *\n * \\@Controller(\"/todos\")\n * export default class TodosController {\n * \\@Post(\"\") create(\\@Body(CreateTodoBody) body: CreateTodoBody): unknown {\n * return Database.insert(\"todos\", { title: body.title });\n * }\n * }\n *\n * The singletons are thin Proxies. Every property access forwards to the live\n * client for the CURRENT request scope, resolved through {@link __getRuntime}.\n *\n * # Request-scope resolution (persistent app-server)\n *\n * The runtime is a long-running Node process that serves many concurrent\n * requests on one event loop (NOT a fresh subprocess per request). A single\n * module-global slot would let one in-flight request's services bleed into\n * another's. So the services are carried in an {@link AsyncLocalStorage} store\n * ({@link __requestALS}) that the runtime sets per request with\n * {@link __runWithRuntime}; every async continuation of that request reads its\n * own store. `__getRuntime` reads the ALS store first; the module-global slot\n * (set by {@link __setRuntime}) is only a fallback for callers that run OUTSIDE\n * an ALS scope (dev-server, unit tests, the legacy single-shot path). Because\n * each `br-<ref>` pod is single-tenant, there is no cross-tenant leakage; the\n * ALS store is what prevents cross-REQUEST leakage within the shared process.\n *\n * The seam that makes `import { Database } from \"@palbase/backend\"` resolve to\n * the runtime-injected client: `@palbase/backend` is marked esbuild-EXTERNAL\n * when the tenant bundle is built, and the package is installed globally in the\n * pod (NODE_PATH=/usr/local/lib/node_modules). So worker.js's\n * `require('@palbase/backend')` and the bundle's `import` resolve to ONE shared\n * module instance — the ALS store and `__setRuntime` slot on that instance are\n * visible to the singletons the bundle imported.\n */\n\nimport { AsyncLocalStorage } from \"node:async_hooks\";\n\nimport type {\n DBClient,\n DBOps,\n CacheClient,\n Logger,\n PalbaseDocsClient,\n} from \"./endpoint.js\";\nimport type {\n PalbaseStorageClient,\n PalbaseNotificationsClient,\n PalbaseFlagsClient,\n PalbaseFlagsServiceClient,\n PalbaseFlagContext,\n PalbaseFlagVariant,\n PalbaseFlag,\n PalbaseFlagValue,\n PalbaseSetOverrideResult,\n PalbaseRealtimeClient,\n} from \"./clients.js\";\nimport type { PalbaseResult } from \"./endpoint.js\";\nimport type {\n EnvTypedDatabase,\n EnvServiceDatabase,\n EnvTables,\n TxPlan,\n TxTables,\n} from \"./db/typed-db.js\";\nimport type { Materialized } from \"./db/tx-plan.js\";\nimport { TxPlanBuilder, runTxPlan } from \"./db/tx-plan.js\";\nimport type { PurchasesService } from \"./purchases/service.js\";\n\n/** The set of live clients the runtime injects per request scope.\n *\n * Realtime is BROADCAST-ONLY here (a stateless handler can push an event but\n * cannot hold a subscription socket — `subscribe()` lives on the client SDK).\n *\n * EXCLUDED on purpose: Functions, Links, Analytics, Auth. They are not\n * exposed as backend handler singletons (auth lives on the client SDK; the rest\n * are out of scope for backend endpoints). */\nexport interface RuntimeServices {\n Database: DBClient;\n Documents: PalbaseDocsClient;\n Storage: PalbaseStorageClient;\n Cache: CacheClient;\n Log: Logger;\n Notifications: PalbaseNotificationsClient;\n Flags: PalbaseFlagsClient;\n Realtime: PalbaseRealtimeClient;\n Purchases: PurchasesService;\n}\n\n/**\n * The per-request ALS box.\n *\n * `runtime` is the service bundle. `userId` is the request's authenticated user\n * id, written by the runtime immediately after it assembles the request object\n * (worker.js: `requestALS.getStore().userId = pbReq.user?.id || null`) and\n * `null` on an anonymous request. It was already being written there for the\n * Flags client's auto-bind; declaring it here makes the existing contract typed\n * instead of implicit, which is what lets `currentSubjectId()` read a\n * server-owned identity rather than trusting anything the caller sent.\n */\nexport interface RequestStore {\n runtime: RuntimeServices;\n userId?: string | null;\n /** This request's id (`req_…`), written by the runtime. Used as the spend\n * idempotency scope when the caller supplied no `Idempotency-Key`. */\n requestId?: string;\n /** The caller's `Idempotency-Key` header, or `null` when absent. What makes a\n * client's retry replay its first result instead of charging twice. */\n idempotencyKey?: string | null;\n}\n\n/**\n * Per-request store. The persistent runtime runs each request inside\n * {@link __runWithRuntime}, so every async continuation of that request reads\n * its OWN `runtime` (and any other request-scoped fields the runtime adds).\n *\n * Exported with a `__` prefix so the runtime (worker.js) shares the SAME ALS\n * instance across the one module instance — two ALS instances would silently\n * not see each other's stores. NOT part of the public author-facing API.\n */\nexport const __requestALS = new AsyncLocalStorage<RequestStore>();\n\n/** Process-global fallback slot. Used only OUTSIDE an ALS scope (dev-server,\n * unit tests, legacy single-shot worker). Inside the persistent server every\n * request runs in {@link __requestALS}, which takes precedence. */\nlet runtime: RuntimeServices | null = null;\n\n/** Install the live clients in the process-global fallback slot.\n *\n * Persistent-server requests should use {@link __runWithRuntime} instead; this\n * remains for dev-server / tests / the legacy single-shot path that run without\n * an ALS scope. NOT part of the public author-facing API. */\nexport function __setRuntime(services: RuntimeServices): void {\n runtime = services;\n}\n\n/** Run `fn` with `services` bound as the request-scoped runtime.\n *\n * The persistent worker calls this once per request so concurrent requests\n * never share a services slot. NOT part of the public author-facing API. */\nexport function __runWithRuntime<T>(services: RuntimeServices, fn: () => T): T {\n return __requestALS.run({ runtime: services }, fn);\n}\n\n/** Read the live clients, throwing if accessed outside a request scope.\n *\n * Resolves the ALS store first (persistent server, per-request), then the\n * process-global fallback (dev-server / tests). NOT part of the public\n * author-facing API — used by the runtime and the singleton Proxies. */\nexport function __getRuntime(): RuntimeServices {\n const scoped = __requestALS.getStore();\n if (scoped) return scoped.runtime;\n if (runtime === null) {\n throw new Error(\n \"Palbase services accessed outside a request scope. The Database/Documents/… \" +\n \"singletons are only available inside an endpoint handler (or after the \" +\n \"runtime has called __runWithRuntime / __setRuntime).\",\n );\n }\n return runtime;\n}\n\n/**\n * Build a Proxy singleton that forwards every property access to the live\n * client named `key` on the current runtime.\n *\n * The single `as RuntimeServices[K]` is the only contained cast in the surface:\n * `Reflect.get` on a typed object returns `unknown` for a `string | symbol`\n * key, but `prop` is constrained to keys of the client interface at the call\n * sites (the exported singletons are typed below), so the forward is sound.\n */\nfunction makeServiceProxy<K extends keyof RuntimeServices>(key: K): RuntimeServices[K] {\n const handler: ProxyHandler<RuntimeServices[K]> = {\n get(_target, prop, receiver) {\n const client = __getRuntime()[key];\n const value = Reflect.get(client as object, prop, receiver) as unknown;\n // Bind methods to their owning client so `this` stays correct when the\n // author destructures or calls `Database.query(...)`.\n return typeof value === \"function\" ? value.bind(client) : value;\n },\n };\n // The Proxy target is irrelevant (all access goes through `get`); the cast\n // names the surface type the singleton presents to authors.\n return new Proxy({} as RuntimeServices[K], handler);\n}\n\n/**\n * Build the `.tables` accessor for the top-level `Database`. Each\n * `tables.<name>` access returns a small object that forwards the five CRUD ops\n * to the underlying client using `name` as the string table identifier. The\n * shapes are typed against the generated `palbase-env.d.ts` (`EnvTables`); at\n * runtime they are plain string-keyed calls, so no schema value is needed here.\n *\n * Returns `EnvTables` — TS cannot infer the mapped type through the Proxy, so\n * a single structural narrowing names the surface (the proxy returns a\n * correctly-shaped accessor for whatever string member is read).\n */\nfunction makeTablesAccessor(ops: () => DBOps): EnvTables {\n const tablesProxy = new Proxy(\n {},\n {\n get(_t, prop: string | symbol) {\n if (typeof prop !== \"string\") return undefined;\n const name = prop;\n return {\n insert: (data: Record<string, unknown>) => ops().insert(name, data),\n update: (id: string, data: Record<string, unknown>) => ops().update(name, id, data),\n delete: (id: string) => ops().delete(name, id),\n findById: (id: string) => ops().findById(name, id),\n findMany: (query?: Record<string, unknown>) => ops().findMany(name, query),\n };\n },\n },\n );\n return tablesProxy as EnvTables;\n}\n\n/** The raw string-keyed `DBClient` for the current request scope. */\nconst rawDatabase: DBClient = makeServiceProxy(\"Database\");\n\n/**\n * Wrap a raw `DBClient` into the typed `{ ...ops, tables, transaction }`\n * surface. The five string ops forward straight through; `tables` is the\n * env-typed accessor; `transaction` yields typed tables. Reused for both the\n * default (RLS-enforced) `Database` and the `asService()` sibling — each is\n * fed its own raw client (the default proxy vs `rawDatabase.asService()`).\n *\n * The `satisfies` pins the op surface so a missing/renamed op is a compile\n * error; the assembled object carries `tables`/`transaction` alongside.\n */\nfunction makeTypedSurface(raw: Omit<DBClient, \"asService\">): EnvServiceDatabase {\n const ops = {\n query: (sql: string, params?: unknown[]) => raw.query(sql, params),\n insert: (table: string, data: Record<string, unknown>) => raw.insert(table, data),\n update: (table: string, id: string, data: Record<string, unknown>) =>\n raw.update(table, id, data),\n delete: (table: string, id: string) => raw.delete(table, id),\n findById: (table: string, id: string) => raw.findById(table, id),\n findMany: (table: string, query?: Record<string, unknown>) => raw.findMany(table, query),\n } satisfies DBOps;\n return Object.assign(ops, {\n tables: makeTablesAccessor(() => raw),\n transaction<T>(\n fn: (tx: TxPlan) => T extends Promise<unknown> ? never : T,\n ): Promise<Materialized<T>> {\n // A FRESH builder per call: it holds this transaction's ops and its error\n // slot table, and the runtime serves concurrent requests on one event loop.\n // A shared builder would splice one request's writes into another's plan.\n const builder = new TxPlanBuilder();\n return runTxPlan(raw, makeTxTablesAccessor(builder), builder, fn) as Promise<\n Materialized<T>\n >;\n },\n });\n}\n\n/**\n * The transaction twin of {@link makeTablesAccessor}: `tables.<name>` yields the\n * table's PLAN operations, recorded into `builder` instead of sent one by one.\n *\n * Same Proxy shape and same reason for the single narrowing — TS cannot infer a\n * mapped type through a Proxy, so the cast names what the trap returns.\n */\nfunction makeTxTablesAccessor(builder: TxPlanBuilder): TxTables {\n const tablesProxy = new Proxy(\n {},\n {\n get(_t, prop: string | symbol) {\n if (typeof prop !== \"string\") return undefined;\n return builder.table(prop);\n },\n },\n );\n return tablesProxy as TxTables;\n}\n\n/**\n * The project's own Postgres (pgx, schema `env_<envId>`).\n *\n * Typed by default: `Database.tables.<name>.insert({...})` is typed against\n * the project's generated `palbase-env.d.ts` with NO import and NO generic.\n * The raw string ops (`query`/`insert`/`update`/`delete`/`findById`/`findMany`)\n * are also available for dynamic table names and read-only SQL.\n *\n * RLS is enforced by default (the runtime runs each op as `authenticated` with\n * the verified user's claims). To bypass RLS, call `Database.asService()` —\n * explicit and greppable — which runs as the `service_role` (BYPASSRLS).\n *\n * @example\n * import { Database } from \"@palbase/backend\";\n *\n * const todo = await Database.tables.todos.insert({ title: req.input.title });\n * todo.id; // string ✓\n * const rows = await Database.query(\"SELECT id FROM todos WHERE done = $1\", [false]);\n * const all = await Database.asService().tables.todos.findMany({}); // RLS bypass\n */\nexport const Database: EnvTypedDatabase = Object.assign(makeTypedSurface(rawDatabase), {\n /**\n * Lazily resolve the runtime's service-role sibling on each call. We do NOT\n * cache it: `rawDatabase.asService()` reads the CURRENT request scope through\n * the runtime proxy, and the per-request runtime injects a service client\n * bound to that request's identity headers — caching would leak one request's\n * sibling into another concurrent request.\n */\n asService(): EnvServiceDatabase {\n return makeTypedSurface(rawDatabase.asService());\n },\n});\n\n/** Firestore-like document client (PalDocs). */\nexport const Documents: PalbaseDocsClient = makeServiceProxy(\"Documents\");\n\n/** Object storage client (buckets, signed URLs). */\nexport const Storage: PalbaseStorageClient = makeServiceProxy(\"Storage\");\n\n/** JSON-typed cache (get/set/incr/getOrSet). */\nexport const Cache: CacheClient = makeServiceProxy(\"Cache\");\n\n\n/** Structured logger. */\nexport const Log: Logger = makeServiceProxy(\"Log\");\n\n/** Push / email / SMS / in-app notifications. */\nexport const Notifications: PalbaseNotificationsClient = makeServiceProxy(\"Notifications\");\n\n/**\n * Palstore purchases (entitlements + quota/credit spend).\n *\n * Reached by handlers through the `@RequireEntitlement` / `@Spend` decorators\n * rather than called directly in the common case; exposed as a singleton for\n * the cases the decorators deliberately do not cover (a dynamic spend count,\n * which must run BEFORE the billable side-effect).\n */\nexport const Purchases: PurchasesService = makeServiceProxy(\"Purchases\");\n\n/**\n * The raw runtime Flags client for the current request scope. Carries the\n * default-surface reads + `setOverride` AND the runtime's `asService()` sibling\n * (the br-pod's `buildFlagsClient` returns both). The default `Flags` singleton\n * below forwards reads + `setOverride` through here; `Flags.asService()`\n * forwards to this client's own `asService()`.\n */\nconst rawFlags: PalbaseFlagsClient = makeServiceProxy(\"Flags\");\n\n/**\n * Feature flags.\n *\n * Mirrors the `Database` / `Database.asService()` model. The default surface is\n * RLS-equivalent for flags: reads resolve against the CURRENT request user and\n * `Flags.setOverride(key, value)` writes an override for that same signed-in\n * user (no userId argument, no admin power). Cross-user admin writes\n * (`setOverrideForUser`, …) live behind `Flags.asService()` — explicit and\n * greppable, just like `Database.asService()`.\n *\n * @example\n * import { Flags } from \"@palbase/backend\";\n *\n * if (await Flags.isEnabled(\"new_checkout\")) { ... } // current user\n * await Flags.setOverride(\"new_checkout\", true); // current user\n * await Flags.asService().setOverrideForUser(\"u_9\", \"x\", true); // cross-user\n */\nexport const Flags: PalbaseFlagsClient = Object.assign(\n {\n isEnabled(\n flagName: string,\n context?: PalbaseFlagContext,\n ): Promise<PalbaseResult<boolean>> {\n return rawFlags.isEnabled(flagName, context);\n },\n getVariant(\n flagName: string,\n context?: PalbaseFlagContext,\n ): Promise<PalbaseResult<PalbaseFlagVariant>> {\n return rawFlags.getVariant(flagName, context);\n },\n getAll(context?: PalbaseFlagContext): Promise<PalbaseResult<PalbaseFlag[]>> {\n return rawFlags.getAll(context);\n },\n setOverride(\n key: string,\n value: PalbaseFlagValue,\n ): Promise<PalbaseResult<PalbaseSetOverrideResult>> {\n return rawFlags.setOverride(key, value);\n },\n },\n {\n /**\n * Lazily resolve the runtime's cross-user sibling on each call. We do NOT\n * cache it: `rawFlags.asService()` reads the CURRENT request scope through\n * the runtime proxy, so caching would leak one request's sibling into\n * another concurrent request. Mirrors `Database.asService()`.\n */\n asService(): PalbaseFlagsServiceClient {\n return rawFlags.asService();\n },\n },\n);\n\n/**\n * The Realtime broadcast singleton for the current request scope. Backend-side\n * Realtime is BROADCAST-ONLY (a stateless handler can push but not subscribe —\n * `subscribe()` lives on the client SDK's `pb.realtime`). Fire-and-forget:\n * `broadcast` resolves once accepted (or with an `error`), never blocking the\n * handler on subscribers.\n *\n * @example\n * import { Realtime } from \"@palbase/backend\";\n *\n * await Realtime.broadcast(\"room:42\", \"message\", { text, from: user.id });\n */\nexport const Realtime: PalbaseRealtimeClient = makeServiceProxy(\"Realtime\");\n","/**\n * tx-plan.ts — `Database.transaction()` as a PLAN, not a pinned session.\n *\n * A transaction used to be a conversation: BEGIN, then one network round trip\n * per operation, then COMMIT. Each of those round trips cost ~4 ms and, because\n * the pooler runs in transaction mode, an open transaction pinned a Postgres\n * backend for the whole conversation. A 121-operation statement upload pinned\n * one backend for ~490 ms.\n *\n * So the callback no longer TALKS to the database. It DESCRIBES what should\n * happen; the description is serialised and sent once; the broker runs the whole\n * thing inside one transaction and answers once. Committing on return and\n * rolling back on throw is unchanged — that is the only property tenant code\n * actually asked for.\n *\n * The consequences, stated plainly, because they are the whole design:\n *\n * - The callback is SYNCHRONOUS. There is nothing to await: no statement has\n * run yet when it returns. `async` on the callback and `await` inside it are\n * both compile errors (see {@link TxPlan} and {@link NotAwaitable}).\n * - `insert()` does not hand back a row, it hands back {@link TxRows}. Reading\n * a field requires `.expectOne(err)` first, which makes \"what if the row\n * isn't there\" a question you cannot route around: it is the argument.\n * - A field read from a row is a {@link Ref} — a PROMISE OF A VALUE THE SERVER\n * WILL PRODUCE, not the value. It can be written into a later operation and\n * it can be returned from the callback (it is substituted for the real value\n * before `transaction()` resolves). It cannot be branched on. See the\n * \"Truthiness\" note below — this is the sharp edge of the whole design.\n * - Control flow that needs a real value must move OUT of the callback: read\n * before the transaction, or express the condition as a guard\n * (`updateWhere({ id, accepted_at: null }, …).expectOne(new Conflict(…))`)\n * which the server evaluates and which rolls the whole plan back.\n *\n * # Truthiness — the hole this file CANNOT close\n *\n * JavaScript does not let a Proxy trap truthiness. `if (ref)` takes the true\n * branch, always, for every Ref, and no `get` handler ever runs. `tsc` is silent\n * because a Ref is a perfectly good object. So:\n *\n * const pot = tx.tables.pots.select({ id }, { limit: 1 }).expectOne(e);\n * if (!pot.balance) { … } // ← ALWAYS false. Silently wrong data.\n *\n * What this file does close: coercion (`Symbol.toPrimitive`/`valueOf`/\n * `toString`), awaiting (`then` is a callable member with a non-thenable\n * signature, which is a *compile* error), serialisation (`toJSON`), and nesting\n * a Ref inside a literal value where the server would store it as data. What it\n * cannot close is a bare truthiness test. The real defence is the build-time\n * static analysis (`tx_analysis.js`, phase P4); until that ships, this hole is\n * open and this comment is the only warning.\n *\n * # Wire contract\n *\n * The JSON this file emits is consumed by\n * `modules/backend/internal/management/tx_program.go`. That decoder rejects\n * unknown fields at every level, so an op carries EXACTLY the fields its kind\n * takes. Everything here that looks like a needless restriction is one of the\n * server's rules made visible early:\n *\n * - `$ref` only points BACKWARDS, and only at an op statically known to yield\n * at most one row (insert, or a `one`/`atMost 1` guard, or `select limit 1`).\n * `.expectOne()` is what this file uses to satisfy that, always.\n * - `$expr` is a closed set: `inc`/`dec` (update only — they read the column's\n * current value) and `now()`.\n * - `update`/`delete` require a `where`; `insert` refuses one.\n * - `insertMany` rows must all set the same columns.\n * - ≤1000 ops, ≤5000 rows per insertMany, ≤8 MiB of JSON.\n *\n * Column keys are emitted SORTED, so the same callback always produces byte-\n * identical JSON. That is what lets the Go decoder be locked to golden files\n * this SDK emits (`testdata/tx_plan_golden/`).\n */\n\n// ---------------------------------------------------------------------------\n// Errors\n// ---------------------------------------------------------------------------\n\n/**\n * A plan handle was used as if it were a value: awaited, coerced to a string or\n * number, serialised, or nested inside another value.\n *\n * Thrown while the callback is still BUILDING the plan, so nothing has been sent\n * and nothing has been written.\n */\nexport class TxRefError extends Error {\n constructor(message: string) {\n super(message);\n this.name = \"TxRefError\";\n }\n}\n\n/**\n * The plan the callback described cannot be sent: it breaks a rule the server\n * would reject, and rejecting it here names the line that wrote it instead of\n * returning a 400 about an op index.\n */\nexport class TxPlanError extends Error {\n constructor(message: string) {\n super(message);\n this.name = \"TxPlanError\";\n }\n}\n\n// ---------------------------------------------------------------------------\n// Wire types — mirror tx_program.go's decoder exactly.\n// ---------------------------------------------------------------------------\n\n/** A backwards reference to an earlier op's single-row result. */\nexport interface TxWireRef {\n $ref: { op: number; field: string };\n}\n\n/** A call from the server's closed function set. */\nexport interface TxWireExpr {\n $expr: { fn: \"inc\" | \"dec\"; by: number } | { fn: \"now\" };\n}\n\n/** One value in a `values`/`set`/`where` map: a literal, a `$ref` or a `$expr`. */\nexport type TxWireValue = TxWireRef | TxWireExpr | unknown;\n\n/** The tenant's declared expectation about an op's row count. `slot` indexes the\n * client-side error table — the error OBJECT never travels. */\nexport interface TxWireGuard {\n kind: \"one\" | \"none\" | \"atLeast\" | \"atMost\";\n n: number;\n slot: number;\n}\n\n/** One operation in the wire plan. Fields are omitted, never null: the decoder\n * rejects a field that does not belong to the op's kind. */\nexport interface TxWireOp {\n op: \"insert\" | \"insertMany\" | \"update\" | \"delete\" | \"select\";\n table: string;\n values?: Record<string, TxWireValue>;\n rows?: Record<string, TxWireValue>[];\n set?: Record<string, TxWireValue>;\n where?: Record<string, TxWireValue>;\n limit?: number;\n lock?: \"update\";\n guard?: TxWireGuard;\n}\n\n/** The request body of `POST /internal-api/db/tx`. */\nexport interface TxPlanBody {\n ops: TxWireOp[];\n}\n\n/** One op's outcome, positionally matched to the plan's ops. */\nexport interface TxPlanOpResult {\n rows: Record<string, unknown>[];\n rows_affected: number;\n}\n\n/** The response body of `POST /internal-api/db/tx`. */\nexport interface TxPlanResponse {\n results: TxPlanOpResult[];\n}\n\n/**\n * The fields the runtime must copy from the broker's error envelope onto the\n * rejection it throws out of {@link DBClient.txPlan}.\n *\n * `slot` is the whole point: on a guard failure the server answers with the\n * INDEX of the expectation that did not hold, never with an error message of its\n * own, and this SDK maps that index back to the `Error` the callback handed to\n * `.expectOne(…)`. Without `slot` a guard failure degrades to a generic 409.\n */\nexport interface TxPlanRejection {\n status?: number;\n /** `tx_plan_invalid` | `tx_guard_failed` | `tx_ref_unresolved` | a pg class. */\n error_code?: string;\n /** Present only for `tx_guard_failed`: the client-side error table index. */\n slot?: number;\n /** Present on a database error: which op failed. */\n op?: number;\n}\n\n// ---------------------------------------------------------------------------\n// Handle types\n// ---------------------------------------------------------------------------\n\ndeclare const refBrand: unique symbol;\ndeclare const rowBrand: unique symbol;\ndeclare const rowsBrand: unique symbol;\n\n/**\n * Makes a handle a compile error to `await`.\n *\n * `then` is declared as a CALLABLE member whose signature is not `PromiseLike`,\n * which is precisely the shape TypeScript rejects: `await handle` is TS1320 and\n * `async () => handle` is TS1058. A non-callable `then` would not do it — the\n * compiler simply ignores those.\n */\nexport interface NotAwaitable {\n /** Not a promise. Nothing here has run yet; there is nothing to await. */\n then(doNotAwaitAPlanHandle: \"a transaction plan is built synchronously\"): never;\n}\n\n/**\n * A value the SERVER will produce, standing in for a column of a row this plan\n * writes or reads.\n *\n * Legal uses: write it into a later operation's `values`/`set`/`where`, or\n * return it from the callback (it is replaced by the real value before\n * `transaction()` resolves).\n *\n * Illegal, and caught: `await`, `String(ref)`, `` `${ref}` ``, `ref + 1`,\n * `JSON.stringify(ref)`, burying it inside a jsonb object.\n *\n * Illegal, and NOT caught: `if (ref)`. See the truthiness note at the top.\n */\nexport interface Ref<T> extends NotAwaitable {\n readonly [refBrand]: T;\n}\n\n/** The brand carried by a single-row handle, and the seam `Materialized` reads\n * to turn `return st` into the whole row. */\nexport interface TxRowHandle<Row> extends NotAwaitable {\n readonly [rowBrand]: Row;\n}\n\n/**\n * A row this plan is known to produce exactly one of. Every property is a\n * {@link Ref}; returning the handle itself yields the whole row.\n *\n * Only `.expectOne(err)` produces one — which is the design: a row you can read\n * fields from is a row whose absence you have already answered for.\n */\nexport type TxRow<Row> = { readonly [K in keyof Row]: Ref<Row[K]> } & TxRowHandle<Row>;\n\n/**\n * The result of one operation, before any expectation is declared about it.\n *\n * Deliberately not a row and not a list: an operation's row count is not known\n * until the server runs it, so the only thing that can be said about it here is\n * an EXPECTATION. Declaring one is also the only way to get a readable row.\n *\n * At most one expectation per operation — the wire carries one guard per op, and\n * a second call throws rather than silently dropping the first.\n */\nexport interface TxRows<Row> extends NotAwaitable {\n readonly [rowsBrand]: Row;\n /**\n * Require exactly one row, and read it. On any other count the server rolls\n * the whole transaction back and this `error` is thrown to the caller.\n *\n * This is the only way to reach a row's fields, and the only shape a `$ref`\n * may point at.\n */\n expectOne(error: Error): TxRow<Row>;\n /** Require zero rows (e.g. \"this membership must not already exist\"). */\n expectNone(error: Error): void;\n /** Require at least `n` rows. */\n expectAtLeast(n: number, error: Error): void;\n /** Require at most `n` rows. */\n expectAtMost(n: number, error: Error): void;\n}\n\n/** `now()` — the server's clock, usable wherever a value is. */\nexport interface TxNow extends NotAwaitable {\n readonly $expr: { fn: \"now\" };\n}\n\n/** `inc(n)` / `dec(n)` — read the column's CURRENT value and write it back\n * changed. Only meaningful in an update's `set`, which is where the types allow\n * it and where the server allows it. */\nexport interface TxColumnExpr extends NotAwaitable {\n readonly $expr: { fn: \"inc\" | \"dec\"; by: number };\n}\n\n/**\n * Resolve a callback's return type against what actually comes back: every\n * {@link Ref} becomes its value, every {@link TxRow} becomes its row, and\n * anything else keeps its shape.\n *\n * A {@link TxRows} resolves to an explanatory string type rather than a row\n * list: it has no single answer to give, and saying so in the type is louder\n * than a runtime throw.\n */\nexport type Materialized<T> = T extends Ref<infer U>\n ? U\n : T extends TxRowHandle<infer R>\n ? R\n : T extends TxRows<unknown>\n ? \"a TxRows cannot leave the transaction callback — read a row with .expectOne(err) first\"\n : T extends Date\n ? T\n : T extends object\n ? { [K in keyof T]: Materialized<T[K]> }\n : T;\n\n// ---------------------------------------------------------------------------\n// Author-facing value types\n// ---------------------------------------------------------------------------\n\n/** A value written by an INSERT: a literal, an earlier row's field, or `now()`.\n * `inc`/`dec` are absent on purpose — they read a current value, and an inserted\n * row has none. */\nexport type TxInsertValue<V> = V | Ref<V> | TxNow;\n\n/** A value written by an UPDATE's `set`: everything an insert takes, plus the\n * read-modify-write expressions. */\nexport type TxSetValue<V> = V | Ref<V> | TxNow | TxColumnExpr;\n\n/** An insert payload: the table's insert shape, with refs and `now()` allowed. */\nexport type TxInsertShape<Insert> = { [K in keyof Insert]: TxInsertValue<Insert[K]> };\n\n/** An update's `set`: any subset of the insert shape, with expressions allowed. */\nexport type TxSetShape<Insert> = { [K in keyof Insert]?: TxSetValue<Insert[K]> };\n\n/**\n * A filter. Every entry is an equality test and they are AND-ed; a `null`\n * becomes `IS NULL`, which is what makes `{ accepted_at: null }` a usable\n * \"not yet accepted\" guard rather than a clause that matches nothing.\n */\nexport type TxWhere<Row> = { [K in keyof Row]?: Row[K] | Ref<Row[K]> };\n\n/** Options for a plan `select`. */\nexport interface TxSelectOptions {\n /** Cap the rows read. */\n limit?: number;\n /** Take a real `FOR UPDATE` row lock for the rest of the transaction. */\n lock?: \"update\";\n}\n\n/** One table, as the plan sees it. */\nexport interface TxTable<Row, Insert> {\n /** Insert one row. Returns a handle — call `.expectOne(err)` to read fields. */\n insert(values: TxInsertShape<Insert>): TxRows<Row>;\n /**\n * Insert many rows in ONE statement. Every row must set the same columns\n * (a row that omits one would silently take the column's default).\n *\n * An empty list writes nothing and sends nothing.\n */\n insertMany(rows: readonly TxInsertShape<Insert>[]): TxRows<Row>;\n /**\n * Update every row matching `where`. The filter comes first because it is the\n * dangerous half: an update whose `where` you got wrong rewrites rows you\n * never looked at. The server refuses an update with no `where` at all.\n */\n updateWhere(where: TxWhere<Row>, set: TxSetShape<Insert>): TxRows<Row>;\n /** Delete every row matching `where`. The server refuses an unfiltered delete. */\n deleteWhere(where: TxWhere<Row>): TxRows<Row>;\n /** Read rows, optionally locking them for the rest of the transaction. */\n select(where?: TxWhere<Row>, options?: TxSelectOptions): TxRows<Row>;\n}\n\n/**\n * The handle a transaction callback receives.\n *\n * It carries tables and nothing else: no `query`, no `findById`, no `asService`.\n * A read whose value the plan does not write belongs OUTSIDE the transaction,\n * where it costs one round trip and can be branched on like an ordinary value.\n */\nexport interface TxPlanHandle<TTables> {\n tables: TTables;\n}\n\n// ---------------------------------------------------------------------------\n// Runtime — expressions\n// ---------------------------------------------------------------------------\n\n/** Internal marker read by the serialiser. */\nconst EXPR = Symbol.for(\"palbase.tx.expr\");\nconst REF = Symbol.for(\"palbase.tx.ref\");\nconst ROW = Symbol.for(\"palbase.tx.row\");\nconst ROWS = Symbol.for(\"palbase.tx.rows\");\n\ninterface RefDescriptor {\n op: number;\n field: string;\n}\n\n/** Property reads that must not silently produce a value on a plan handle.\n * `then` is the one that matters for correctness (it makes `await` a compile\n * error AND stops a handle from being adopted by a promise); the rest turn a\n * silent wrong answer — `\"[object Object]\"`, `NaN`, `{}` — into a thrown one. */\nconst TRAPPED_PROPS: readonly (string | symbol)[] = [\n \"then\",\n \"valueOf\",\n \"toString\",\n \"toJSON\",\n Symbol.toPrimitive,\n];\n\nfunction trap(prop: string | symbol, what: string, hint: string): never {\n const name = typeof prop === \"symbol\" ? prop.description ?? String(prop) : prop;\n throw new TxRefError(\n `${what} was used as a value (via \\`${name}\\`). Nothing in a transaction ` +\n `callback has run yet, so there is no value to read. ${hint}`,\n );\n}\n\n/** The server's `now()`. */\nexport function now(): TxNow {\n return makeExpr({ fn: \"now\" }) as TxNow;\n}\n\n/** Add `by` to the column's current value. Only valid in an update's `set`. */\nexport function inc(by: number): TxColumnExpr {\n assertFiniteNumber(by, \"inc\");\n return makeExpr({ fn: \"inc\", by }) as TxColumnExpr;\n}\n\n/** Subtract `by` from the column's current value. Only valid in an update's `set`. */\nexport function dec(by: number): TxColumnExpr {\n assertFiniteNumber(by, \"dec\");\n return makeExpr({ fn: \"dec\", by }) as TxColumnExpr;\n}\n\nfunction assertFiniteNumber(by: number, fn: string): void {\n if (typeof by !== \"number\" || !Number.isFinite(by)) {\n // JSON has no NaN/Infinity: they would serialise to `null` and the server\n // would reject the plan with an unhelpful decode error.\n throw new TxPlanError(`${fn}() needs a finite number, got ${String(by)}`);\n }\n}\n\nfunction makeExpr(expr: TxWireExpr[\"$expr\"]): unknown {\n return new Proxy(\n { [EXPR]: expr } as Record<string | symbol, unknown>,\n {\n get(target, prop) {\n if (prop === EXPR) return target[EXPR];\n if (TRAPPED_PROPS.includes(prop)) {\n trap(prop, \"A plan expression\", \"Write it into an operation instead.\");\n }\n return undefined;\n },\n },\n );\n}\n\n// ---------------------------------------------------------------------------\n// Runtime — refs and row handles\n// ---------------------------------------------------------------------------\n\nfunction makeRef(op: number, field: string): unknown {\n const target: Record<string | symbol, unknown> = { [REF]: { op, field } satisfies RefDescriptor };\n return new Proxy(target, {\n get(t, prop) {\n if (prop === REF) return t[REF];\n if (TRAPPED_PROPS.includes(prop)) {\n trap(\n prop,\n `\\`${field}\\` of a row this transaction has not written yet`,\n \"Pass it to another operation in the same plan, or return it from the \" +\n \"callback and read it after `transaction()` resolves.\",\n );\n }\n return undefined;\n },\n });\n}\n\nfunction makeRowHandle(op: number): unknown {\n const target: Record<string | symbol, unknown> = { [ROW]: op };\n return new Proxy(target, {\n get(t, prop) {\n if (prop === ROW) return t[ROW];\n if (TRAPPED_PROPS.includes(prop)) {\n trap(\n prop,\n \"A row this transaction has not written yet\",\n \"Read one of its columns to reference it, or return the row from the \" +\n \"callback and read it after `transaction()` resolves.\",\n );\n }\n if (typeof prop === \"symbol\") return undefined;\n return makeRef(op, prop);\n },\n });\n}\n\nfunction refDescriptor(v: unknown): RefDescriptor | null {\n if (typeof v !== \"object\" || v === null) return null;\n const d = (v as Record<symbol, unknown>)[REF];\n return isRefDescriptor(d) ? d : null;\n}\n\nfunction isRefDescriptor(d: unknown): d is RefDescriptor {\n return (\n typeof d === \"object\" &&\n d !== null &&\n typeof (d as RefDescriptor).op === \"number\" &&\n typeof (d as RefDescriptor).field === \"string\"\n );\n}\n\nfunction rowOpIndex(v: unknown): number | null {\n if (typeof v !== \"object\" || v === null) return null;\n const op = (v as Record<symbol, unknown>)[ROW];\n return typeof op === \"number\" ? op : null;\n}\n\nfunction exprOf(v: unknown): TxWireExpr[\"$expr\"] | null {\n if (typeof v !== \"object\" || v === null) return null;\n const e = (v as Record<symbol, unknown>)[EXPR];\n return typeof e === \"object\" && e !== null ? (e as TxWireExpr[\"$expr\"]) : null;\n}\n\nfunction isRowsHandle(v: unknown): boolean {\n return typeof v === \"object\" && v !== null && (v as Record<symbol, unknown>)[ROWS] !== undefined;\n}\n\n// ---------------------------------------------------------------------------\n// Runtime — value encoding\n// ---------------------------------------------------------------------------\n\n/**\n * Encode one value of a `values`/`set`/`where` map.\n *\n * The nesting check is not defensive tidiness. The server treats `$ref` as a\n * tagged value only at the TOP of a column's value; a ref buried inside a jsonb\n * payload is just data, and would be stored as the literal object\n * `{\"$ref\":{...}}` — a write that succeeds, commits, and is wrong. So a nested\n * handle is a hard error here, where the line that wrote it is still on the\n * stack.\n */\nfunction encodeValue(value: unknown, column: string, allowColumnExpr: boolean): TxWireValue {\n const ref = refDescriptor(value);\n if (ref) return { $ref: { op: ref.op, field: ref.field } } satisfies TxWireRef;\n\n const expr = exprOf(value);\n if (expr) {\n if (expr.fn !== \"now\" && !allowColumnExpr) {\n throw new TxPlanError(\n `\\`${column}\\`: ${expr.fn}() reads the column's current value, so it is ` +\n `only valid in updateWhere(where, set).`,\n );\n }\n return { $expr: expr } satisfies TxWireExpr;\n }\n\n if (rowOpIndex(value) !== null) {\n throw new TxPlanError(\n `\\`${column}\\`: a row handle is not a value. Read the column you meant ` +\n `(e.g. \\`row.id\\`).`,\n );\n }\n if (isRowsHandle(value)) {\n throw new TxPlanError(\n `\\`${column}\\`: an operation result is not a value. Declare an expectation ` +\n `first (\\`.expectOne(err)\\`) and read a column from the row.`,\n );\n }\n\n assertNoNestedHandles(value, column);\n return value;\n}\n\nfunction assertNoNestedHandles(value: unknown, column: string): void {\n if (typeof value !== \"object\" || value === null) return;\n if (value instanceof Date) return;\n if (refDescriptor(value) || exprOf(value) || rowOpIndex(value) !== null || isRowsHandle(value)) {\n throw new TxPlanError(\n `\\`${column}\\`: a plan handle is nested inside a value. The server would ` +\n `store it as literal JSON, not resolve it. Put the reference directly in ` +\n `the column.`,\n );\n }\n if (Array.isArray(value)) {\n for (const item of value) assertNoNestedHandles(item, column);\n return;\n }\n for (const item of Object.values(value as Record<string, unknown>)) {\n assertNoNestedHandles(item, column);\n }\n}\n\n/**\n * Encode a column map, dropping `undefined` and emitting keys SORTED.\n *\n * Sorting is what makes the same callback produce byte-identical JSON every\n * time, which is what lets the Go decoder be locked to goldens this SDK emits.\n * Dropping `undefined` mirrors what `JSON.stringify` would do anyway, but does\n * it where the resulting column list is still checkable.\n */\nfunction encodeMap(\n map: Record<string, unknown>,\n allowColumnExpr: boolean,\n): Record<string, TxWireValue> {\n const out: Record<string, TxWireValue> = {};\n for (const key of Object.keys(map).sort()) {\n const value = map[key];\n if (value === undefined) continue;\n out[key] = encodeValue(value, key, allowColumnExpr);\n }\n return out;\n}\n\n// ---------------------------------------------------------------------------\n// Runtime — the builder\n// ---------------------------------------------------------------------------\n\n/** How many rows an op can produce, as far as the CLIENT can tell before it\n * runs. Only the empty `insertMany` is knowable, and knowing it is what lets a\n * doomed expectation fail on the line that declared it. */\nconst SKIPPED_OP = -1;\n\nclass TxRowsImpl<Row> implements TxRows<Row> {\n // Present so `isRowsHandle` recognises the object; never read for its value.\n readonly [ROWS] = true;\n // Phantom: the type brand that stops a bare object passing as a TxRows. Never\n // present at run time, which is what `declare` says.\n declare readonly [rowsBrand]: Row;\n private guarded = false;\n\n constructor(\n private readonly builder: TxPlanBuilder,\n private readonly opIndex: number,\n private readonly what: string,\n ) {}\n\n // The type-level `await` guard made real: TS rejects `await rows` at compile\n // time, and reaching this means someone called `.then(...)` by hand.\n then(): never {\n throw new TxRefError(\n `${this.what} cannot be awaited: a transaction callback builds a plan, it ` +\n `does not run statements. Remove the \\`await\\`.`,\n );\n }\n\n expectOne(error: Error): TxRow<Row> {\n this.declareGuard(\"one\", 1, error);\n if (this.opIndex === SKIPPED_OP) throw error;\n return makeRowHandle(this.opIndex) as TxRow<Row>;\n }\n\n expectNone(error: Error): void {\n this.declareGuard(\"none\", 0, error);\n }\n\n expectAtLeast(n: number, error: Error): void {\n assertGuardCount(n, \"expectAtLeast\");\n this.declareGuard(\"atLeast\", n, error);\n if (this.opIndex === SKIPPED_OP && n > 0) throw error;\n }\n\n expectAtMost(n: number, error: Error): void {\n assertGuardCount(n, \"expectAtMost\");\n this.declareGuard(\"atMost\", n, error);\n }\n\n private declareGuard(kind: TxWireGuard[\"kind\"], n: number, error: Error): void {\n if (!(error instanceof Error)) {\n // The error object is the ONLY thing that describes this failure to the\n // caller — the server sends back an index, never a message.\n throw new TxPlanError(\n `${this.what}: an expectation needs the Error to throw when it does not ` +\n `hold (e.g. \\`.expect…(new Conflict(\"already accepted\"))\\`).`,\n );\n }\n if (this.guarded) {\n throw new TxPlanError(\n `${this.what} already has an expectation. One operation carries one ` +\n `expectation; declare the second one on its own operation.`,\n );\n }\n this.guarded = true;\n if (this.opIndex === SKIPPED_OP) return;\n this.builder.attachGuard(this.opIndex, kind, n, error);\n }\n}\n\nfunction assertGuardCount(n: number, fn: string): void {\n if (!Number.isInteger(n) || n < 0) {\n throw new TxPlanError(`${fn}(n) needs a non-negative integer, got ${String(n)}`);\n }\n}\n\n/** Bounds mirrored from tx_program.go so an over-sized plan is named here rather\n * than rejected as an opaque 400 after it has crossed the network. */\nconst MAX_OPS = 1000;\nconst MAX_ROWS = 5000;\n\n/**\n * Accumulates ops and the client-side error table while the callback runs.\n *\n * Exported for the runtime and for tests that need the serialised plan without\n * a server; author code never sees it.\n */\nexport class TxPlanBuilder {\n private readonly ops: TxWireOp[] = [];\n /** Errors handed to expectations, indexed by the `slot` the server echoes. */\n private readonly slots: Error[] = [];\n\n /** The table surface handed to the callback. Untyped here; the public\n * `transaction()` signatures put the schema types on top. */\n table(name: string): TxTable<Record<string, unknown>, Record<string, unknown>> {\n return {\n insert: (values) => {\n const encoded = encodeMap(values as Record<string, unknown>, false);\n if (Object.keys(encoded).length === 0) {\n throw new TxPlanError(`${name}.insert() needs at least one column`);\n }\n return this.push({ op: \"insert\", table: name, values: encoded }, `${name}.insert()`);\n },\n\n insertMany: (rows) => {\n if (rows.length === 0) {\n // Nothing to write. Emitting an op would be a guaranteed 400 (the\n // server requires rows), and refusing outright would punish the\n // ordinary \"filtered every candidate out\" case.\n return new TxRowsImpl(this, SKIPPED_OP, `${name}.insertMany()`);\n }\n if (rows.length > MAX_ROWS) {\n throw new TxPlanError(\n `${name}.insertMany() has ${rows.length} rows; the limit is ${MAX_ROWS}. ` +\n `Split the write across requests.`,\n );\n }\n const encoded = rows.map((row) => encodeMap(row as Record<string, unknown>, false));\n assertUniformRows(encoded, name);\n return this.push({ op: \"insertMany\", table: name, rows: encoded }, `${name}.insertMany()`);\n },\n\n updateWhere: (where, set) => {\n const encodedWhere = encodeMap(where as Record<string, unknown>, false);\n const encodedSet = encodeMap(set as Record<string, unknown>, true);\n if (Object.keys(encodedWhere).length === 0) {\n throw new TxPlanError(\n `${name}.updateWhere() needs a filter. An update with no filter ` +\n `rewrites the whole table.`,\n );\n }\n if (Object.keys(encodedSet).length === 0) {\n throw new TxPlanError(`${name}.updateWhere() needs at least one column to set`);\n }\n return this.push(\n { op: \"update\", table: name, set: encodedSet, where: encodedWhere },\n `${name}.updateWhere()`,\n );\n },\n\n deleteWhere: (where) => {\n const encodedWhere = encodeMap(where as Record<string, unknown>, false);\n if (Object.keys(encodedWhere).length === 0) {\n throw new TxPlanError(\n `${name}.deleteWhere() needs a filter. A delete with no filter empties ` +\n `the table.`,\n );\n }\n return this.push(\n { op: \"delete\", table: name, where: encodedWhere },\n `${name}.deleteWhere()`,\n );\n },\n\n select: (where, options) => {\n const op: TxWireOp = { op: \"select\", table: name };\n const encodedWhere = encodeMap((where ?? {}) as Record<string, unknown>, false);\n if (Object.keys(encodedWhere).length > 0) op.where = encodedWhere;\n if (options?.limit !== undefined) {\n if (!Number.isInteger(options.limit) || options.limit < 0) {\n throw new TxPlanError(\n `${name}.select(): limit needs a non-negative integer, got ${String(options.limit)}`,\n );\n }\n op.limit = options.limit;\n }\n if (options?.lock !== undefined) op.lock = options.lock;\n return this.push(op, `${name}.select()`);\n },\n };\n }\n\n private push(op: TxWireOp, what: string): TxRows<Record<string, unknown>> {\n if (this.ops.length >= MAX_OPS) {\n throw new TxPlanError(\n `this transaction has ${MAX_OPS} operations, which is the limit. Use ` +\n `insertMany() for bulk writes, or split the work across requests.`,\n );\n }\n const index = this.ops.length;\n this.ops.push(op);\n return new TxRowsImpl(this, index, what);\n }\n\n /** Attach an expectation to an op and record its error in the slot table. */\n attachGuard(opIndex: number, kind: TxWireGuard[\"kind\"], n: number, error: Error): void {\n const op = this.ops[opIndex];\n // Unreachable: the index came from `push`. A loud failure beats a silently\n // unguarded write if that ever stops being true.\n if (!op) throw new TxPlanError(`internal: expectation on unknown operation ${opIndex}`);\n const slot = this.slots.length;\n this.slots.push(error);\n op.guard = { kind, n, slot };\n }\n\n /** The serialisable plan. Empty when the callback described no writes. */\n body(): TxPlanBody {\n return { ops: this.ops };\n }\n\n /** The error the server's `slot` selects, or `null` when it names one this\n * plan never declared (a server/client disagreement, not a tenant error). */\n errorForSlot(slot: number): Error | null {\n return this.slots[slot] ?? null;\n }\n}\n\nfunction assertUniformRows(rows: Record<string, TxWireValue>[], table: string): void {\n const first = rows[0];\n if (!first) return;\n const want = Object.keys(first);\n const wantKey = want.join(\",\");\n for (let i = 1; i < rows.length; i++) {\n const got = Object.keys(rows[i] as Record<string, TxWireValue>);\n if (got.join(\",\") !== wantKey) {\n // One statement, one column list. A ragged row would take the DB default\n // for the column it omitted — a write that succeeds and is wrong.\n throw new TxPlanError(\n `${table}.insertMany(): every row must set the same columns. Row 0 sets ` +\n `[${want.join(\", \")}] but row ${i} sets [${got.join(\", \")}]. ` +\n `(A property set to \\`undefined\\` counts as absent — use \\`null\\`.)`,\n );\n }\n }\n}\n\n// ---------------------------------------------------------------------------\n// Runtime — materialisation\n// ---------------------------------------------------------------------------\n\n/**\n * Replace every handle in the callback's return value with what the server\n * actually produced.\n *\n * Walks arrays and PLAIN objects only. Class instances (a Date, a Zod schema, a\n * domain object) are returned untouched — recursing into them would rebuild them\n * as bare objects, and nothing inside one can be a handle that this SDK created.\n */\nexport function materializeResult(value: unknown, results: TxPlanOpResult[]): unknown {\n const ref = refDescriptor(value);\n if (ref) {\n const row = rowOf(results, ref.op, `\\`${ref.field}\\``);\n if (!(ref.field in row)) {\n throw new TxPlanError(\n `the transaction's operation ${ref.op} returned no column \\`${ref.field}\\`.`,\n );\n }\n return row[ref.field];\n }\n\n const rowOp = rowOpIndex(value);\n if (rowOp !== null) return rowOf(results, rowOp, \"a row\");\n\n if (isRowsHandle(value)) {\n throw new TxPlanError(\n \"an operation result cannot be returned from a transaction callback: its \" +\n \"row count is not known until the plan runs. Declare an expectation \" +\n \"(`.expectOne(err)`) and return the row, or a column of it.\",\n );\n }\n\n if (Array.isArray(value)) return value.map((item) => materializeResult(item, results));\n\n if (isPlainObject(value)) {\n const out: Record<string, unknown> = {};\n for (const [key, item] of Object.entries(value)) out[key] = materializeResult(item, results);\n return out;\n }\n\n return value;\n}\n\nfunction rowOf(results: TxPlanOpResult[], opIndex: number, what: string): Record<string, unknown> {\n const result = results[opIndex];\n if (!result) {\n throw new TxPlanError(\n `the transaction returned no result for operation ${opIndex}, so ${what} ` +\n `cannot be read.`,\n );\n }\n const row = result.rows[0];\n if (!row) {\n // Unreachable through the public API: a handle only exists behind an\n // `expectOne`, and the server rolls back rather than answering 200 with a\n // guard unmet. Loud, because the alternative is `undefined` in tenant data.\n throw new TxPlanError(\n `the transaction's operation ${opIndex} returned no row, so ${what} cannot ` +\n `be read.`,\n );\n }\n return row;\n}\n\nfunction isPlainObject(value: unknown): value is Record<string, unknown> {\n if (typeof value !== \"object\" || value === null) return false;\n const proto: unknown = Object.getPrototypeOf(value);\n return proto === Object.prototype || proto === null;\n}\n\n// ---------------------------------------------------------------------------\n// Runtime — the driver\n// ---------------------------------------------------------------------------\n\n/** What {@link runTxPlan} needs from the runtime: one call, one transaction. */\nexport interface TxPlanTransport {\n txPlan(plan: TxPlanBody): Promise<TxPlanResponse>;\n}\n\n/**\n * Build the plan, send it, and resolve the callback's return value.\n *\n * A callback that throws never reaches the network: there is nothing to roll\n * back because nothing was sent. A callback that describes no writes also skips\n * the round trip entirely.\n *\n * The RETURN type is `unknown` here on purpose. `Materialized<T>` is a compile-\n * time rewrite of the callback's return type, and it belongs on the public\n * `transaction()` signatures where `T` is inferred from the author's callback;\n * threading it through this driver as well would mean inferring `T` twice, from\n * two different positions, and reconciling them. The public wrappers narrow\n * once, at the seam where the value types were erased anyway.\n */\nexport async function runTxPlan<TTables>(\n transport: TxPlanTransport,\n tables: TTables,\n builder: TxPlanBuilder,\n fn: (tx: TxPlanHandle<TTables>) => unknown,\n): Promise<unknown> {\n const returned = fn({ tables });\n const body = builder.body();\n if (body.ops.length === 0) {\n return materializeResult(returned, []);\n }\n\n let response: TxPlanResponse;\n try {\n response = await transport.txPlan(body);\n } catch (err) {\n throw translateRejection(err, builder);\n }\n return materializeResult(returned, response.results);\n}\n\n/**\n * Turn the broker's rejection back into the tenant's own error.\n *\n * The server never sees the `Error` an expectation was given — only its slot\n * index — so this is the only place the intended error can be produced. A\n * rejection this SDK does not recognise passes through untouched: inventing an\n * error for it would hide the real failure.\n */\nfunction translateRejection(err: unknown, builder: TxPlanBuilder): unknown {\n if (typeof err !== \"object\" || err === null) return err;\n const rejection = err as TxPlanRejection;\n if (rejection.error_code !== \"tx_guard_failed\" || typeof rejection.slot !== \"number\") {\n return err;\n }\n return builder.errorForSlot(rejection.slot) ?? err;\n}\n","import type { DBClient, DBOps } from \"../endpoint.js\";\nimport type {\n TxPlanBody,\n TxPlanOpResult,\n TxPlanRejection,\n TxPlanResponse,\n TxWireGuard,\n TxWireOp,\n TxWireValue,\n} from \"../db/tx-plan.js\";\n\n/** Tracked records for assertions. */\ninterface TrackedRecords {\n inserted: Map<string, Record<string, unknown>[]>;\n updated: Map<string, Record<string, unknown>[]>;\n deleted: Map<string, string[]>;\n}\n\n/** Mock DB client with tracking and seed data support. */\nexport interface MockDBClient extends DBClient {\n /** Get records inserted into a table. */\n inserted(table: string): Record<string, unknown>[];\n /** Get records updated in a table. */\n updated(table: string): Record<string, unknown>[];\n /** Get IDs deleted from a table. */\n deleted(table: string): string[];\n /** Pre-seed data into a table for findById/findMany. */\n seed(table: string, data: Record<string, unknown>[]): void;\n}\n\n/** Create a mock DB client with in-memory tracking. */\nexport function createMockDB(): MockDBClient {\n const store = new Map<string, Record<string, unknown>[]>();\n const tracked: TrackedRecords = {\n inserted: new Map(),\n updated: new Map(),\n deleted: new Map(),\n };\n\n function rowsOf(table: string): Record<string, unknown>[] {\n let rows = store.get(table);\n if (!rows) {\n rows = [];\n store.set(table, rows);\n }\n return rows;\n }\n\n function track(\n map: Map<string, Record<string, unknown>[]>,\n table: string,\n row: Record<string, unknown>,\n ): void {\n const list = map.get(table);\n if (list) list.push(row);\n else map.set(table, [row]);\n }\n\n // Build the op surface first (the six string-keyed ops). `txPlan` below\n // interprets a whole plan against the SAME in-memory store and tracking maps,\n // so a transaction's writes are visible to later assertions exactly as a\n // direct write would be.\n const ops: DBOps = {\n async query(_sql: string, _params?: unknown[]) {\n return [];\n },\n\n async insert(table: string, data: Record<string, unknown>) {\n const record = { id: crypto.randomUUID(), ...data };\n rowsOf(table).push(record);\n track(tracked.inserted, table, record);\n return record;\n },\n\n async update(table: string, id: string, data: Record<string, unknown>) {\n const rows = store.get(table) ?? [];\n const idx = rows.findIndex((r) => r[\"id\"] === id);\n const updated = idx >= 0\n ? { ...rows[idx], ...data }\n : { id, ...data };\n if (idx >= 0) {\n rows[idx] = updated;\n }\n track(tracked.updated, table, updated);\n return updated;\n },\n\n async delete(table: string, id: string) {\n const rows = store.get(table) ?? [];\n const idx = rows.findIndex((r) => r[\"id\"] === id);\n if (idx >= 0) rows.splice(idx, 1);\n const list = tracked.deleted.get(table);\n if (list) list.push(id);\n else tracked.deleted.set(table, [id]);\n },\n\n async findById(table: string, id: string) {\n const rows = store.get(table) ?? [];\n return rows.find((r) => r[\"id\"] === id) ?? null;\n },\n\n async findMany(table: string, query?: Record<string, unknown>) {\n const rows = store.get(table) ?? [];\n if (!query) return rows;\n return rows.filter((row) =>\n Object.entries(query).every(([key, val]) => row[key] === val),\n );\n },\n };\n\n /**\n * Interpret a whole plan, atomically.\n *\n * The rollback is the point. A test that asserts \"the second write failed, so\n * the first one is not there\" must be able to FAIL — a mock that applied ops\n * and left them applied would pass that test while the real broker rolled the\n * transaction back, or the other way round. So the store and the tracking maps\n * are snapshotted, and any failure restores both before rejecting.\n *\n * The rejection carries the same envelope fields the runtime copies off the\n * broker's response (`error_code`, `slot`), because the SDK maps `slot` back\n * to the caller's own Error — a mock that rejected with a bare Error would\n * make every guard in every tenant test look like a generic failure.\n */\n async function txPlan(plan: TxPlanBody): Promise<TxPlanResponse> {\n const snapshot = new Map<string, Record<string, unknown>[]>();\n for (const [table, rows] of store) snapshot.set(table, [...rows]);\n const trackedSnapshot: TrackedRecords = {\n inserted: cloneTracked(tracked.inserted),\n updated: cloneTracked(tracked.updated),\n deleted: new Map([...tracked.deleted].map(([k, v]) => [k, [...v]])),\n };\n\n const results: TxPlanOpResult[] = [];\n try {\n for (const op of plan.ops) {\n const result = applyOp(op, results);\n results.push(result);\n const failure = guardFailure(op.guard, result.rows.length);\n if (failure) throw failure;\n }\n } catch (err) {\n store.clear();\n for (const [table, rows] of snapshot) store.set(table, rows);\n tracked.inserted = trackedSnapshot.inserted;\n tracked.updated = trackedSnapshot.updated;\n tracked.deleted = trackedSnapshot.deleted;\n throw err;\n }\n return { results };\n }\n\n function applyOp(op: TxWireOp, results: TxPlanOpResult[]): TxPlanOpResult {\n switch (op.op) {\n case \"insert\": {\n const record = { id: crypto.randomUUID(), ...resolveMap(op.values ?? {}, results, null) };\n rowsOf(op.table).push(record);\n track(tracked.inserted, op.table, record);\n return { rows: [record], rows_affected: 1 };\n }\n case \"insertMany\": {\n const written = (op.rows ?? []).map((row) => {\n const record = { id: crypto.randomUUID(), ...resolveMap(row, results, null) };\n rowsOf(op.table).push(record);\n track(tracked.inserted, op.table, record);\n return record;\n });\n return { rows: written, rows_affected: written.length };\n }\n case \"update\": {\n const rows = rowsOf(op.table);\n const where = resolveMap(op.where ?? {}, results, null);\n const written: Record<string, unknown>[] = [];\n for (let i = 0; i < rows.length; i++) {\n const row = rows[i];\n if (!row || !matches(row, where)) continue;\n const next = { ...row, ...resolveMap(op.set ?? {}, results, row) };\n rows[i] = next;\n track(tracked.updated, op.table, next);\n written.push(next);\n }\n return { rows: written, rows_affected: written.length };\n }\n case \"delete\": {\n const rows = rowsOf(op.table);\n const where = resolveMap(op.where ?? {}, results, null);\n const removed = rows.filter((row) => matches(row, where));\n for (const row of removed) {\n rows.splice(rows.indexOf(row), 1);\n const id = row[\"id\"];\n const list = tracked.deleted.get(op.table);\n const key = typeof id === \"string\" ? id : String(id);\n if (list) list.push(key);\n else tracked.deleted.set(op.table, [key]);\n }\n return { rows: removed, rows_affected: removed.length };\n }\n case \"select\": {\n const where = resolveMap(op.where ?? {}, results, null);\n let found = rowsOf(op.table).filter((row) => matches(row, where));\n if (op.limit !== undefined) found = found.slice(0, op.limit);\n return { rows: found, rows_affected: found.length };\n }\n }\n }\n\n const client: MockDBClient = {\n ...ops,\n\n txPlan,\n\n // In tests there is no real DB role; `asService()` returns the same\n // in-memory client so RLS-bypass code paths still hit the same store and\n // tracking maps. The omitted `asService` matches the contract (no\n // double-bypass), so callers can't recurse.\n asService(): Omit<DBClient, \"asService\"> {\n return client;\n },\n\n inserted(table: string) {\n return tracked.inserted.get(table) ?? [];\n },\n\n updated(table: string) {\n return tracked.updated.get(table) ?? [];\n },\n\n deleted(table: string) {\n return tracked.deleted.get(table) ?? [];\n },\n\n seed(table: string, data: Record<string, unknown>[]) {\n store.set(table, [...data]);\n },\n };\n\n return client;\n}\n\nfunction cloneTracked(\n map: Map<string, Record<string, unknown>[]>,\n): Map<string, Record<string, unknown>[]> {\n return new Map([...map].map(([k, v]) => [k, [...v]]));\n}\n\n/** Resolve one plan value: a `$ref` into an earlier result, a `$expr`, or a\n * literal. `current` is the row being updated, which is what `inc`/`dec` read. */\nfunction resolveValue(\n value: TxWireValue,\n results: TxPlanOpResult[],\n current: Record<string, unknown> | null,\n column: string,\n): unknown {\n if (typeof value !== \"object\" || value === null) return value;\n const tagged = value as { $ref?: { op: number; field: string }; $expr?: Record<string, unknown> };\n\n if (tagged.$ref) {\n const row = results[tagged.$ref.op]?.rows[0];\n if (!row) {\n throw txRejection(409, \"tx_ref_unresolved\", {\n message: `operation ${tagged.$ref.op} produced no row to reference`,\n });\n }\n return row[tagged.$ref.field];\n }\n\n if (tagged.$expr) {\n const fn = tagged.$expr[\"fn\"];\n if (fn === \"now\") return new Date().toISOString();\n const by = Number(tagged.$expr[\"by\"]);\n const base = Number(current?.[column] ?? 0);\n return fn === \"dec\" ? base - by : base + by;\n }\n\n return value;\n}\n\nfunction resolveMap(\n map: Record<string, TxWireValue>,\n results: TxPlanOpResult[],\n current: Record<string, unknown> | null,\n): Record<string, unknown> {\n const out: Record<string, unknown> = {};\n for (const [key, value] of Object.entries(map)) {\n out[key] = resolveValue(value, results, current, key);\n }\n return out;\n}\n\n/** Equality filter, with `null` meaning IS NULL — the broker's rule, so a\n * `{ accepted_at: null }` guard behaves the same in a test as in production. */\nfunction matches(row: Record<string, unknown>, where: Record<string, unknown>): boolean {\n return Object.entries(where).every(([key, value]) =>\n value === null ? row[key] === null || row[key] === undefined : row[key] === value,\n );\n}\n\nfunction guardFailure(guard: TxWireGuard | undefined, count: number): unknown {\n if (!guard) return null;\n const ok =\n guard.kind === \"one\"\n ? count === 1\n : guard.kind === \"none\"\n ? count === 0\n : guard.kind === \"atLeast\"\n ? count >= guard.n\n : count <= guard.n;\n if (ok) return null;\n return txRejection(409, \"tx_guard_failed\", {\n slot: guard.slot,\n message: `expected ${guard.kind} ${guard.n} row(s), got ${count}`,\n });\n}\n\n/** Build a rejection shaped like the one the runtime throws for a broker error:\n * an Error carrying the envelope's `status`/`error_code`/`slot`. */\nfunction txRejection(\n status: number,\n code: string,\n extra: { slot?: number; message: string },\n): Error & TxPlanRejection {\n const err = new Error(extra.message) as Error & TxPlanRejection;\n err.status = status;\n err.error_code = code;\n if (extra.slot !== undefined) err.slot = extra.slot;\n return err;\n}\n","import type {\n CacheClient,\n ClientInfo,\n Logger,\n PBRequest,\n PalbaseModuleClients,\n} from \"../endpoint.js\";\nimport type {\n PalbaseAuthClient,\n PalbaseStorageClient,\n PalbaseRealtimeClient,\n PalbaseFunctionsClient,\n PalbaseFlagsClient,\n PalbaseNotificationsClient,\n PalbaseAnalyticsClient,\n PalbaseLinksClient,\n} from \"../clients.js\";\nimport type { User } from \"../types.js\";\nimport { __setRuntime } from \"../runtime.js\";\nimport type { PurchasesService } from \"../purchases/service.js\";\nimport { createMockDB, type MockDBClient } from \"./mock-db.js\";\n\n/** Options for creating a test context. */\nexport interface TestContextOptions<TInput = unknown> {\n user?: User | null;\n input?: TInput;\n params?: Record<string, string>;\n query?: Record<string, string>;\n headers?: Record<string, string>;\n env?: Record<string, string>;\n db?: { seed?: Record<string, Record<string, unknown>[]> };\n}\n\n/** Log entry captured by the mock logger. */\nexport interface LogEntry {\n level: \"info\" | \"warn\" | \"error\" | \"debug\";\n message: string;\n args: unknown[];\n}\n\n/** Test context for exercising endpoint handlers.\n *\n * Handlers now receive a {@link PBRequest} (no services attached) and reach\n * services via the PascalCase singletons (`Database`, `Log`, …). So\n * `createTestContext` does two things:\n * 1. returns a `PBRequest` (the object you pass to `handler(...)`), and\n * 2. installs mock services into the runtime via `__setRuntime`, so the\n * singletons resolve to the same mocks while the handler runs.\n *\n * For assertions and for building sibling (worker/job/hook/webhook) contexts,\n * the mock service handles are also attached here (`db`, `log`, `cache`,\n * `queue`, `env`, plus the module clients and captured `logs`). The user\n * defaults to nullable in tests (`PBRequest<TInput, false>`) so test code can\n * pass any auth shape without a cast. */\nexport interface TestContext<TInput = unknown>\n extends PBRequest<TInput, false>,\n PalbaseModuleClients {\n db: MockDBClient;\n env: Record<string, string>;\n log: Logger;\n cache: CacheClient;\n /** Captured log entries. */\n logs: LogEntry[];\n}\n\n/** Create a mock logger that captures entries. */\nfunction createMockLogger(logs: LogEntry[]): Logger {\n return {\n info(message: string, ...args: unknown[]) {\n logs.push({ level: \"info\", message, args });\n },\n warn(message: string, ...args: unknown[]) {\n logs.push({ level: \"warn\", message, args });\n },\n error(message: string, ...args: unknown[]) {\n logs.push({ level: \"error\", message, args });\n },\n debug(message: string, ...args: unknown[]) {\n logs.push({ level: \"debug\", message, args });\n },\n };\n}\n\n/** Create a mock in-memory cache.\n *\n * Mirrors the runtime's JSON-typed semantics (values are arbitrary JSON, not\n * just strings). getOrSet is single-process here, so it does not need the\n * distributed lock the real runtime uses — it is just get-miss → fn → set.\n * The cross-replica stampede protection is covered by the worker.js tests.\n */\nfunction createMockCache(): CacheClient {\n const store = new Map<string, unknown>();\n\n const get = async <T = unknown>(key: string): Promise<T | null> => {\n return store.has(key) ? (store.get(key) as T) : null;\n };\n const set = async (key: string, value: unknown, _ttl?: number): Promise<void> => {\n store.set(key, value);\n };\n\n return {\n get,\n set,\n async del(key: string) {\n store.delete(key);\n },\n async incr(key: string) {\n const raw = store.get(key);\n const current = typeof raw === \"number\" ? raw : parseInt(String(raw ?? \"0\"), 10);\n const next = current + 1;\n store.set(key, next);\n return next;\n },\n async getOrSet<T>(key: string, ttl: number, fn: () => Promise<T> | T): Promise<T> {\n const hit = await get<T>(key);\n if (hit !== null) {\n return hit;\n }\n const value = await fn();\n await set(key, value, ttl);\n return value;\n },\n };\n}\n\n/** Create mock Palbase module clients (Documents, Storage, …).\n *\n * Every slot throws with a descriptive error so tests that access a module\n * client surface without configuring it fail loudly rather than silently\n * returning undefined. Override individual clients on the returned context for\n * tests that need them.\n */\nfunction createMockModuleClients(): PalbaseModuleClients {\n const notImpl = (label: string): never => {\n throw new Error(\n `${label} not configured in test mock — override the matching client on the returned context`,\n );\n };\n\n const docs: PalbaseModuleClients[\"docs\"] = {\n collection: () => notImpl(\"docs.collection\"),\n doc: () => notImpl(\"docs.doc\"),\n };\n\n const auth: PalbaseAuthClient = {\n verifyUserToken: () => notImpl(\"auth.verifyUserToken\"),\n getSession: () => notImpl(\"auth.getSession\"),\n mfa: {\n enroll: () => notImpl(\"auth.mfa.enroll\"),\n verifyEnrollment: () => notImpl(\"auth.mfa.verifyEnrollment\"),\n challenge: () => notImpl(\"auth.mfa.challenge\"),\n recovery: () => notImpl(\"auth.mfa.recovery\"),\n listFactors: () => notImpl(\"auth.mfa.listFactors\"),\n removeFactor: () => notImpl(\"auth.mfa.removeFactor\"),\n regenerateRecoveryCodes: () => notImpl(\"auth.mfa.regenerateRecoveryCodes\"),\n emailEnroll: () => notImpl(\"auth.mfa.emailEnroll\"),\n emailChallenge: () => notImpl(\"auth.mfa.emailChallenge\"),\n emailVerify: () => notImpl(\"auth.mfa.emailVerify\"),\n },\n device: {\n generateChallenge: () => notImpl(\"auth.device.generateChallenge\"),\n attestAndroid: () => notImpl(\"auth.device.attestAndroid\"),\n attestiOS: () => notImpl(\"auth.device.attestiOS\"),\n bind: () => notImpl(\"auth.device.bind\"),\n list: () => notImpl(\"auth.device.list\"),\n delete: () => notImpl(\"auth.device.delete\"),\n verifyRequestSignature: () => notImpl(\"auth.device.verifyRequestSignature\"),\n getToken: () => notImpl(\"auth.device.getToken\"),\n get isActive(): never {\n return notImpl(\"auth.device.isActive\");\n },\n setCachedToken: () => notImpl(\"auth.device.setCachedToken\"),\n dispose: () => notImpl(\"auth.device.dispose\"),\n },\n };\n\n const storage: PalbaseStorageClient = {\n bucket: () => notImpl(\"storage.bucket\"),\n };\n\n const realtime: PalbaseRealtimeClient = {\n broadcast: async () => ({ data: undefined, error: null }),\n };\n\n const functions: PalbaseFunctionsClient = {\n invoke: () => notImpl(\"functions.invoke\"),\n };\n\n const flags: PalbaseFlagsClient = {\n isEnabled: () => notImpl(\"flags.isEnabled\"),\n getVariant: () => notImpl(\"flags.getVariant\"),\n getAll: () => notImpl(\"flags.getAll\"),\n setOverride: () => notImpl(\"flags.setOverride\"),\n asService: () => ({\n setOverrideForUser: () => notImpl(\"flags.asService.setOverrideForUser\"),\n setOverridesForUser: () => notImpl(\"flags.asService.setOverridesForUser\"),\n clearOverrideForUser: () => notImpl(\"flags.asService.clearOverrideForUser\"),\n clearAllOverridesForUser: () => notImpl(\"flags.asService.clearAllOverridesForUser\"),\n batchSetOverrides: () => notImpl(\"flags.asService.batchSetOverrides\"),\n }),\n };\n\n const notifications: PalbaseNotificationsClient = {\n push: { send: () => notImpl(\"notifications.push.send\") },\n email: { send: () => notImpl(\"notifications.email.send\") },\n sms: { send: () => notImpl(\"notifications.sms.send\") },\n verifications: {\n start: () => notImpl(\"notifications.verifications.start\"),\n check: () => notImpl(\"notifications.verifications.check\"),\n },\n inbox: {\n send: () => notImpl(\"notifications.inbox.send\"),\n list: () => notImpl(\"notifications.inbox.list\"),\n unreadCount: () => notImpl(\"notifications.inbox.unreadCount\"),\n markRead: () => notImpl(\"notifications.inbox.markRead\"),\n markAllRead: () => notImpl(\"notifications.inbox.markAllRead\"),\n archive: () => notImpl(\"notifications.inbox.archive\"),\n },\n preferences: {\n get: () => notImpl(\"notifications.preferences.get\"),\n update: () => notImpl(\"notifications.preferences.update\"),\n },\n templates: {\n email: {\n list: () => notImpl(\"notifications.templates.email.list\"),\n get: () => notImpl(\"notifications.templates.email.get\"),\n create: () => notImpl(\"notifications.templates.email.create\"),\n update: () => notImpl(\"notifications.templates.email.update\"),\n delete: () => notImpl(\"notifications.templates.email.delete\"),\n },\n sms: {\n list: () => notImpl(\"notifications.templates.sms.list\"),\n get: () => notImpl(\"notifications.templates.sms.get\"),\n create: () => notImpl(\"notifications.templates.sms.create\"),\n update: () => notImpl(\"notifications.templates.sms.update\"),\n delete: () => notImpl(\"notifications.templates.sms.delete\"),\n },\n },\n registerDevice: () => notImpl(\"notifications.registerDevice\"),\n unregisterDevice: () => notImpl(\"notifications.unregisterDevice\"),\n };\n\n const analytics: PalbaseAnalyticsClient = {\n capture: () => notImpl(\"analytics.capture\"),\n identify: () => notImpl(\"analytics.identify\"),\n screen: () => notImpl(\"analytics.screen\"),\n query: {\n count: () => notImpl(\"analytics.query.count\"),\n events: () => notImpl(\"analytics.query.events\"),\n properties: () => notImpl(\"analytics.query.properties\"),\n users: () => notImpl(\"analytics.query.users\"),\n funnel: () => notImpl(\"analytics.query.funnel\"),\n retention: () => notImpl(\"analytics.query.retention\"),\n cohort: () => notImpl(\"analytics.query.cohort\"),\n },\n management: {\n overview: () => notImpl(\"analytics.management.overview\"),\n eventNames: () => notImpl(\"analytics.management.eventNames\"),\n userDetail: () => notImpl(\"analytics.management.userDetail\"),\n deleteUser: () => notImpl(\"analytics.management.deleteUser\"),\n },\n };\n\n const links: PalbaseLinksClient = {\n create: () => notImpl(\"links.create\"),\n list: () => notImpl(\"links.list\"),\n get: () => notImpl(\"links.get\"),\n update: () => notImpl(\"links.update\"),\n delete: () => notImpl(\"links.delete\"),\n analytics: () => notImpl(\"links.analytics\"),\n qrCode: () => notImpl(\"links.qrCode\"),\n match: () => notImpl(\"links.match\"),\n };\n\n return {\n auth,\n storage,\n docs,\n realtime,\n functions,\n flags,\n notifications,\n analytics,\n links,\n };\n}\n\n/** A permissive purchases double: the subject resolves, the entitlement is\n * present, and a spend just runs its handler. That keeps a unit test of a\n * decorated handler about the handler's own logic instead of about billing.\n *\n * ponytail: deliberately has no \"deny\" mode. The 403/429 paths are about the\n * server's real accounting (reserve/commit/cancel, quota windows), and a double\n * that answered them would be asserting its own script — those belong in an\n * integration test against a real palstore, which is how they are covered.\n */\nfunction createMockPurchases(): PurchasesService {\n return {\n resolveSubject: async ({ userRef }) => ({ subjectId: `psj_test_${userRef}` }),\n require: async () => undefined,\n withSpend: async (_subjectId, _key, _opts, handler) => handler(),\n };\n}\n\n/** Null-by-default calling-client metadata for tests. */\nconst NULL_CLIENT_INFO: ClientInfo = {\n sdkVersion: null,\n appVersion: null,\n platform: null,\n osVersion: null,\n};\n\n/** Create a fully mocked endpoint test context.\n *\n * Returns a `PBRequest` (pass it to `handler(...)`) with the mock service\n * handles attached for assertions, and installs those mocks into the runtime\n * via `__setRuntime` so the `Database`/`Log`/… singletons resolve to them\n * while the handler runs.\n */\nexport function createTestContext<TInput = unknown>(\n options: TestContextOptions<TInput> = {},\n): TestContext<TInput> {\n const logs: LogEntry[] = [];\n const db = createMockDB();\n\n // Seed data if provided\n if (options.db?.seed) {\n for (const [table, data] of Object.entries(options.db.seed)) {\n db.seed(table, data);\n }\n }\n\n const log = createMockLogger(logs);\n const cache = createMockCache();\n const moduleClients = createMockModuleClients();\n\n // Install the mocks so the PascalCase singletons (Database, Log, …) resolve\n // to them while the handler under test runs.\n __setRuntime({\n Database: db,\n Documents: moduleClients.docs,\n Storage: moduleClients.storage,\n Cache: cache,\n Log: log,\n Notifications: moduleClients.notifications,\n Flags: moduleClients.flags,\n Realtime: moduleClients.realtime,\n Purchases: createMockPurchases(),\n });\n\n const ctx: TestContext<TInput> = {\n input: (options.input ?? {}) as TInput,\n params: options.params ?? {},\n query: options.query ?? {},\n headers: options.headers ?? {},\n user: options.user ?? null,\n client: NULL_CLIENT_INFO,\n method: \"POST\",\n file: null,\n db,\n env: options.env ?? {},\n log,\n cache,\n ...moduleClients,\n // Empty errors map in tests by default. Tests that exercise an endpoint's\n // declared errors construct their own throwers; this stub satisfies the\n // PBRequest shape without forcing every test to declare `errors:`.\n errors: {},\n requestId: \"req_test_000000000000\",\n traceId: \"0\".repeat(32),\n spanId: \"0\".repeat(16),\n logs,\n };\n\n return ctx;\n}\n"],"mappings":"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","names":[]}
1
+ {"version":3,"sources":["../../src/test/index.ts","../../src/test/api.ts"],"sourcesContent":["export { api, createTestApi, TestApiError } from \"./api.js\";\nexport type { TestApi, TestApiConfig, RecordedRequest, CallOptions } from \"./api.js\";\n","/** The client a tenant's tests use to call their own backend.\n *\n * These tests run against a REAL deployment — the release the deploy just built,\n * serving from the same Environment as production, with the same database, the\n * same secrets and the same gateway in front of it. So this client is a plain\n * HTTP client, not a simulation: every call crosses Kong, the API key check, the\n * auth rail, the zod validation at the boundary, and row-level security, exactly\n * as a shipped app's call does.\n *\n * There is deliberately no schema knowledge here. The tenant already wrote their\n * types — `import type { TodoSchema } from \"../models/todos/shared.js\"` — so a\n * test types its own call (`api.get<TodoSchema[]>(\"/todos\")`) and can validate it\n * with the same zod schema the endpoint declares. A second generated client would\n * be a second thing to keep in step.\n */\n\n/** How to reach the release under test. Supplied by the deploy, never guessed. */\nexport interface TestApiConfig {\n baseUrl: string;\n apiKey: string;\n /** This deploy's secret. Without it the request is served the LIVE release. */\n candidateToken: string;\n}\n\n/** One call the suite made, in the order it was made. */\nexport interface RecordedRequest {\n method: string;\n path: string;\n status: number;\n ms: number;\n}\n\n/** A non-2xx answer, carrying the platform's error envelope.\n *\n * The envelope is the contract every Palbase endpoint answers with, so a test\n * asserts on `status`/`error`/`data` rather than parsing a message. The message\n * exists for the human reading a failed deploy.\n */\nexport class TestApiError extends Error {\n readonly status: number;\n readonly error: string;\n readonly data: unknown;\n\n constructor(method: string, path: string, status: number, body: unknown) {\n const envelope = (body ?? {}) as { error?: string; error_description?: string; data?: unknown };\n const code = envelope.error ?? String(status);\n super(`${method} ${path} → ${status} ${code}${envelope.error_description ? `: ${envelope.error_description}` : \"\"}`);\n this.name = \"TestApiError\";\n this.status = status;\n this.error = code;\n this.data = envelope.data;\n }\n}\n\nexport interface CallOptions {\n headers?: Record<string, string>;\n}\n\nexport interface TestApi {\n get<T = unknown>(path: string, opts?: CallOptions): Promise<T>;\n post<T = unknown>(path: string, body?: unknown, opts?: CallOptions): Promise<T>;\n patch<T = unknown>(path: string, body?: unknown, opts?: CallOptions): Promise<T>;\n put<T = unknown>(path: string, body?: unknown, opts?: CallOptions): Promise<T>;\n delete<T = unknown>(path: string, opts?: CallOptions): Promise<T>;\n /** HTTP QUERY (RFC 10008): a safe, idempotent read whose filter travels in the body. */\n query<T = unknown>(path: string, body?: unknown, opts?: CallOptions): Promise<T>;\n /** Sign in as a fixture declared in `config/test-users.ts`. */\n signIn(credentials: { email: string; password: string }): Promise<{ id: string; email?: string }>;\n signOut(): Promise<void>;\n /** Drop the bearer without calling the server — the anonymous caller. */\n asAnonymous(): void;\n /** Every call made, in order. Printed for the failing test in a red deploy. */\n readonly requests: readonly RecordedRequest[];\n}\n\nfunction required(value: string, envName: string): string {\n if (!value) {\n throw new Error(\n `${envName} is not set — the test client has nowhere to send requests. ` +\n `This is set by the deploy that runs your tests; if you are running them by hand, set it yourself.`,\n );\n }\n return value;\n}\n\nexport function createTestApi(config: TestApiConfig): TestApi {\n const baseUrl = required(config.baseUrl, \"PALBASE_TEST_BASE_URL\").replace(/\\/$/, \"\");\n const apiKey = required(config.apiKey, \"PALBASE_TEST_API_KEY\");\n const candidateToken = required(config.candidateToken, \"PALBASE_TEST_CANDIDATE\");\n\n const requests: RecordedRequest[] = [];\n let bearer: string | null = null;\n\n async function call<T>(method: string, path: string, body: unknown, opts: CallOptions = {}): Promise<T> {\n const headers: Record<string, string> = {\n apikey: apiKey,\n // Selects the release under test. Omit it and the gateway serves the LIVE\n // one, which would make the whole suite grade the wrong code.\n \"x-palbase-candidate\": candidateToken,\n ...opts.headers,\n };\n if (bearer) headers.authorization = `Bearer ${bearer}`;\n if (body !== undefined) headers[\"content-type\"] = \"application/json\";\n\n const startedAt = Date.now();\n const res = await fetch(`${baseUrl}${path}`, {\n method,\n headers,\n body: body === undefined ? undefined : JSON.stringify(body),\n });\n const text = await res.text();\n const parsed: unknown = text ? safeParse(text) : undefined;\n\n requests.push({ method, path, status: res.status, ms: Date.now() - startedAt });\n\n if (!res.ok) throw new TestApiError(method, path, res.status, parsed);\n return parsed as T;\n }\n\n return {\n requests,\n get: (path, opts) => call(\"GET\", path, undefined, opts),\n post: (path, body, opts) => call(\"POST\", path, body, opts),\n patch: (path, body, opts) => call(\"PATCH\", path, body, opts),\n put: (path, body, opts) => call(\"PUT\", path, body, opts),\n delete: (path, opts) => call(\"DELETE\", path, undefined, opts),\n query: (path, body, opts) => call(\"QUERY\", path, body, opts),\n\n async signIn(credentials) {\n const result = await call<{ access_token: string; user?: { id: string; email?: string } }>(\n \"POST\",\n \"/auth/login\",\n credentials,\n );\n bearer = result.access_token;\n return result.user ?? { id: \"\" };\n },\n async signOut() {\n await call(\"POST\", \"/auth/logout\", undefined);\n bearer = null;\n },\n asAnonymous() {\n bearer = null;\n },\n };\n}\n\nfunction safeParse(text: string): unknown {\n try {\n return JSON.parse(text);\n } catch {\n return text;\n }\n}\n\n/** The client the deploy configured, from the environment it set.\n *\n * Constructed lazily so importing this module outside a test run — a typecheck,\n * an editor — does not fail on absent configuration.\n */\nlet configured: TestApi | null = null;\n\nexport const api: TestApi = new Proxy({} as TestApi, {\n get(_target, prop) {\n configured ??= createTestApi({\n baseUrl: process.env.PALBASE_TEST_BASE_URL ?? \"\",\n apiKey: process.env.PALBASE_TEST_API_KEY ?? \"\",\n candidateToken: process.env.PALBASE_TEST_CANDIDATE ?? \"\",\n });\n return Reflect.get(configured, prop, configured);\n },\n});\n"],"mappings":"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","names":[]}
@@ -1,67 +1,70 @@
1
- import { D as DBClient, P as PBRequest, a as PalbaseModuleClients, L as Logger, C as CacheClient, U as User } from '../endpoint-BW8bn1y-.cjs';
2
- import 'zod';
3
-
4
- /** Mock DB client with tracking and seed data support. */
5
- interface MockDBClient extends DBClient {
6
- /** Get records inserted into a table. */
7
- inserted(table: string): Record<string, unknown>[];
8
- /** Get records updated in a table. */
9
- updated(table: string): Record<string, unknown>[];
10
- /** Get IDs deleted from a table. */
11
- deleted(table: string): string[];
12
- /** Pre-seed data into a table for findById/findMany. */
13
- seed(table: string, data: Record<string, unknown>[]): void;
14
- }
15
- /** Create a mock DB client with in-memory tracking. */
16
- declare function createMockDB(): MockDBClient;
17
-
18
- /** Options for creating a test context. */
19
- interface TestContextOptions<TInput = unknown> {
20
- user?: User | null;
21
- input?: TInput;
22
- params?: Record<string, string>;
23
- query?: Record<string, string>;
24
- headers?: Record<string, string>;
25
- env?: Record<string, string>;
26
- db?: {
27
- seed?: Record<string, Record<string, unknown>[]>;
28
- };
29
- }
30
- /** Log entry captured by the mock logger. */
31
- interface LogEntry {
32
- level: "info" | "warn" | "error" | "debug";
33
- message: string;
34
- args: unknown[];
35
- }
36
- /** Test context for exercising endpoint handlers.
1
+ /** The client a tenant's tests use to call their own backend.
37
2
  *
38
- * Handlers now receive a {@link PBRequest} (no services attached) and reach
39
- * services via the PascalCase singletons (`Database`, `Log`, …). So
40
- * `createTestContext` does two things:
41
- * 1. returns a `PBRequest` (the object you pass to `handler(...)`), and
42
- * 2. installs mock services into the runtime via `__setRuntime`, so the
43
- * singletons resolve to the same mocks while the handler runs.
3
+ * These tests run against a REAL deployment the release the deploy just built,
4
+ * serving from the same Environment as production, with the same database, the
5
+ * same secrets and the same gateway in front of it. So this client is a plain
6
+ * HTTP client, not a simulation: every call crosses Kong, the API key check, the
7
+ * auth rail, the zod validation at the boundary, and row-level security, exactly
8
+ * as a shipped app's call does.
44
9
  *
45
- * For assertions and for building sibling (worker/job/hook/webhook) contexts,
46
- * the mock service handles are also attached here (`db`, `log`, `cache`,
47
- * `queue`, `env`, plus the module clients and captured `logs`). The user
48
- * defaults to nullable in tests (`PBRequest<TInput, false>`) so test code can
49
- * pass any auth shape without a cast. */
50
- interface TestContext<TInput = unknown> extends PBRequest<TInput, false>, PalbaseModuleClients {
51
- db: MockDBClient;
52
- env: Record<string, string>;
53
- log: Logger;
54
- cache: CacheClient;
55
- /** Captured log entries. */
56
- logs: LogEntry[];
10
+ * There is deliberately no schema knowledge here. The tenant already wrote their
11
+ * types `import type { TodoSchema } from "../models/todos/shared.js"` so a
12
+ * test types its own call (`api.get<TodoSchema[]>("/todos")`) and can validate it
13
+ * with the same zod schema the endpoint declares. A second generated client would
14
+ * be a second thing to keep in step.
15
+ */
16
+ /** How to reach the release under test. Supplied by the deploy, never guessed. */
17
+ interface TestApiConfig {
18
+ baseUrl: string;
19
+ apiKey: string;
20
+ /** This deploy's secret. Without it the request is served the LIVE release. */
21
+ candidateToken: string;
22
+ }
23
+ /** One call the suite made, in the order it was made. */
24
+ interface RecordedRequest {
25
+ method: string;
26
+ path: string;
27
+ status: number;
28
+ ms: number;
57
29
  }
58
- /** Create a fully mocked endpoint test context.
30
+ /** A non-2xx answer, carrying the platform's error envelope.
59
31
  *
60
- * Returns a `PBRequest` (pass it to `handler(...)`) with the mock service
61
- * handles attached for assertions, and installs those mocks into the runtime
62
- * via `__setRuntime` so the `Database`/`Log`/… singletons resolve to them
63
- * while the handler runs.
32
+ * The envelope is the contract every Palbase endpoint answers with, so a test
33
+ * asserts on `status`/`error`/`data` rather than parsing a message. The message
34
+ * exists for the human reading a failed deploy.
64
35
  */
65
- declare function createTestContext<TInput = unknown>(options?: TestContextOptions<TInput>): TestContext<TInput>;
36
+ declare class TestApiError extends Error {
37
+ readonly status: number;
38
+ readonly error: string;
39
+ readonly data: unknown;
40
+ constructor(method: string, path: string, status: number, body: unknown);
41
+ }
42
+ interface CallOptions {
43
+ headers?: Record<string, string>;
44
+ }
45
+ interface TestApi {
46
+ get<T = unknown>(path: string, opts?: CallOptions): Promise<T>;
47
+ post<T = unknown>(path: string, body?: unknown, opts?: CallOptions): Promise<T>;
48
+ patch<T = unknown>(path: string, body?: unknown, opts?: CallOptions): Promise<T>;
49
+ put<T = unknown>(path: string, body?: unknown, opts?: CallOptions): Promise<T>;
50
+ delete<T = unknown>(path: string, opts?: CallOptions): Promise<T>;
51
+ /** HTTP QUERY (RFC 10008): a safe, idempotent read whose filter travels in the body. */
52
+ query<T = unknown>(path: string, body?: unknown, opts?: CallOptions): Promise<T>;
53
+ /** Sign in as a fixture declared in `config/test-users.ts`. */
54
+ signIn(credentials: {
55
+ email: string;
56
+ password: string;
57
+ }): Promise<{
58
+ id: string;
59
+ email?: string;
60
+ }>;
61
+ signOut(): Promise<void>;
62
+ /** Drop the bearer without calling the server — the anonymous caller. */
63
+ asAnonymous(): void;
64
+ /** Every call made, in order. Printed for the failing test in a red deploy. */
65
+ readonly requests: readonly RecordedRequest[];
66
+ }
67
+ declare function createTestApi(config: TestApiConfig): TestApi;
68
+ declare const api: TestApi;
66
69
 
67
- export { type LogEntry, type MockDBClient, type TestContext, type TestContextOptions, createMockDB, createTestContext };
70
+ export { type CallOptions, type RecordedRequest, type TestApi, type TestApiConfig, TestApiError, api, createTestApi };
@@ -1,67 +1,70 @@
1
- import { D as DBClient, P as PBRequest, a as PalbaseModuleClients, L as Logger, C as CacheClient, U as User } from '../endpoint-BW8bn1y-.js';
2
- import 'zod';
3
-
4
- /** Mock DB client with tracking and seed data support. */
5
- interface MockDBClient extends DBClient {
6
- /** Get records inserted into a table. */
7
- inserted(table: string): Record<string, unknown>[];
8
- /** Get records updated in a table. */
9
- updated(table: string): Record<string, unknown>[];
10
- /** Get IDs deleted from a table. */
11
- deleted(table: string): string[];
12
- /** Pre-seed data into a table for findById/findMany. */
13
- seed(table: string, data: Record<string, unknown>[]): void;
14
- }
15
- /** Create a mock DB client with in-memory tracking. */
16
- declare function createMockDB(): MockDBClient;
17
-
18
- /** Options for creating a test context. */
19
- interface TestContextOptions<TInput = unknown> {
20
- user?: User | null;
21
- input?: TInput;
22
- params?: Record<string, string>;
23
- query?: Record<string, string>;
24
- headers?: Record<string, string>;
25
- env?: Record<string, string>;
26
- db?: {
27
- seed?: Record<string, Record<string, unknown>[]>;
28
- };
29
- }
30
- /** Log entry captured by the mock logger. */
31
- interface LogEntry {
32
- level: "info" | "warn" | "error" | "debug";
33
- message: string;
34
- args: unknown[];
35
- }
36
- /** Test context for exercising endpoint handlers.
1
+ /** The client a tenant's tests use to call their own backend.
37
2
  *
38
- * Handlers now receive a {@link PBRequest} (no services attached) and reach
39
- * services via the PascalCase singletons (`Database`, `Log`, …). So
40
- * `createTestContext` does two things:
41
- * 1. returns a `PBRequest` (the object you pass to `handler(...)`), and
42
- * 2. installs mock services into the runtime via `__setRuntime`, so the
43
- * singletons resolve to the same mocks while the handler runs.
3
+ * These tests run against a REAL deployment the release the deploy just built,
4
+ * serving from the same Environment as production, with the same database, the
5
+ * same secrets and the same gateway in front of it. So this client is a plain
6
+ * HTTP client, not a simulation: every call crosses Kong, the API key check, the
7
+ * auth rail, the zod validation at the boundary, and row-level security, exactly
8
+ * as a shipped app's call does.
44
9
  *
45
- * For assertions and for building sibling (worker/job/hook/webhook) contexts,
46
- * the mock service handles are also attached here (`db`, `log`, `cache`,
47
- * `queue`, `env`, plus the module clients and captured `logs`). The user
48
- * defaults to nullable in tests (`PBRequest<TInput, false>`) so test code can
49
- * pass any auth shape without a cast. */
50
- interface TestContext<TInput = unknown> extends PBRequest<TInput, false>, PalbaseModuleClients {
51
- db: MockDBClient;
52
- env: Record<string, string>;
53
- log: Logger;
54
- cache: CacheClient;
55
- /** Captured log entries. */
56
- logs: LogEntry[];
10
+ * There is deliberately no schema knowledge here. The tenant already wrote their
11
+ * types `import type { TodoSchema } from "../models/todos/shared.js"` so a
12
+ * test types its own call (`api.get<TodoSchema[]>("/todos")`) and can validate it
13
+ * with the same zod schema the endpoint declares. A second generated client would
14
+ * be a second thing to keep in step.
15
+ */
16
+ /** How to reach the release under test. Supplied by the deploy, never guessed. */
17
+ interface TestApiConfig {
18
+ baseUrl: string;
19
+ apiKey: string;
20
+ /** This deploy's secret. Without it the request is served the LIVE release. */
21
+ candidateToken: string;
22
+ }
23
+ /** One call the suite made, in the order it was made. */
24
+ interface RecordedRequest {
25
+ method: string;
26
+ path: string;
27
+ status: number;
28
+ ms: number;
57
29
  }
58
- /** Create a fully mocked endpoint test context.
30
+ /** A non-2xx answer, carrying the platform's error envelope.
59
31
  *
60
- * Returns a `PBRequest` (pass it to `handler(...)`) with the mock service
61
- * handles attached for assertions, and installs those mocks into the runtime
62
- * via `__setRuntime` so the `Database`/`Log`/… singletons resolve to them
63
- * while the handler runs.
32
+ * The envelope is the contract every Palbase endpoint answers with, so a test
33
+ * asserts on `status`/`error`/`data` rather than parsing a message. The message
34
+ * exists for the human reading a failed deploy.
64
35
  */
65
- declare function createTestContext<TInput = unknown>(options?: TestContextOptions<TInput>): TestContext<TInput>;
36
+ declare class TestApiError extends Error {
37
+ readonly status: number;
38
+ readonly error: string;
39
+ readonly data: unknown;
40
+ constructor(method: string, path: string, status: number, body: unknown);
41
+ }
42
+ interface CallOptions {
43
+ headers?: Record<string, string>;
44
+ }
45
+ interface TestApi {
46
+ get<T = unknown>(path: string, opts?: CallOptions): Promise<T>;
47
+ post<T = unknown>(path: string, body?: unknown, opts?: CallOptions): Promise<T>;
48
+ patch<T = unknown>(path: string, body?: unknown, opts?: CallOptions): Promise<T>;
49
+ put<T = unknown>(path: string, body?: unknown, opts?: CallOptions): Promise<T>;
50
+ delete<T = unknown>(path: string, opts?: CallOptions): Promise<T>;
51
+ /** HTTP QUERY (RFC 10008): a safe, idempotent read whose filter travels in the body. */
52
+ query<T = unknown>(path: string, body?: unknown, opts?: CallOptions): Promise<T>;
53
+ /** Sign in as a fixture declared in `config/test-users.ts`. */
54
+ signIn(credentials: {
55
+ email: string;
56
+ password: string;
57
+ }): Promise<{
58
+ id: string;
59
+ email?: string;
60
+ }>;
61
+ signOut(): Promise<void>;
62
+ /** Drop the bearer without calling the server — the anonymous caller. */
63
+ asAnonymous(): void;
64
+ /** Every call made, in order. Printed for the failing test in a red deploy. */
65
+ readonly requests: readonly RecordedRequest[];
66
+ }
67
+ declare function createTestApi(config: TestApiConfig): TestApi;
68
+ declare const api: TestApi;
66
69
 
67
- export { type LogEntry, type MockDBClient, type TestContext, type TestContextOptions, createMockDB, createTestContext };
70
+ export { type CallOptions, type RecordedRequest, type TestApi, type TestApiConfig, TestApiError, api, createTestApi };