@palbase/backend 10.3.0 → 12.0.0

This diff represents the content of publicly available package versions that have been released to one of the supported registries. The information contained in this diff is provided for informational purposes only and reflects changes between package versions as they appear in their respective public registries.
@@ -67,6 +67,333 @@ type MiddlewareHandler = (ctx: MiddlewareContext, next: () => Promise<void>) =>
67
67
  */
68
68
  declare function defineMiddleware(fn: MiddlewareHandler): MiddlewareHandler;
69
69
 
70
+ /**
71
+ * tx-plan.ts — `Database.transaction()` as a PLAN, not a pinned session.
72
+ *
73
+ * A transaction used to be a conversation: BEGIN, then one network round trip
74
+ * per operation, then COMMIT. Each of those round trips cost ~4 ms and, because
75
+ * the pooler runs in transaction mode, an open transaction pinned a Postgres
76
+ * backend for the whole conversation. A 121-operation statement upload pinned
77
+ * one backend for ~490 ms.
78
+ *
79
+ * So the callback no longer TALKS to the database. It DESCRIBES what should
80
+ * happen; the description is serialised and sent once; the broker runs the whole
81
+ * thing inside one transaction and answers once. Committing on return and
82
+ * rolling back on throw is unchanged — that is the only property tenant code
83
+ * actually asked for.
84
+ *
85
+ * The consequences, stated plainly, because they are the whole design:
86
+ *
87
+ * - The callback is SYNCHRONOUS. There is nothing to await: no statement has
88
+ * run yet when it returns. `async` on the callback and `await` inside it are
89
+ * both compile errors (see {@link TxPlan} and {@link NotAwaitable}).
90
+ * - `insert()` does not hand back a row, it hands back {@link TxRows}. Reading
91
+ * a field requires `.expectOne(err)` first, which makes "what if the row
92
+ * isn't there" a question you cannot route around: it is the argument.
93
+ * - A field read from a row is a {@link Ref} — a PROMISE OF A VALUE THE SERVER
94
+ * WILL PRODUCE, not the value. It can be written into a later operation and
95
+ * it can be returned from the callback (it is substituted for the real value
96
+ * before `transaction()` resolves). It cannot be branched on. See the
97
+ * "Truthiness" note below — this is the sharp edge of the whole design.
98
+ * - Control flow that needs a real value must move OUT of the callback: read
99
+ * before the transaction, or express the condition as a guard
100
+ * (`updateWhere({ id, accepted_at: null }, …).expectOne(new Conflict(…))`)
101
+ * which the server evaluates and which rolls the whole plan back.
102
+ *
103
+ * # Truthiness — the hole this file CANNOT close
104
+ *
105
+ * JavaScript does not let a Proxy trap truthiness. `if (ref)` takes the true
106
+ * branch, always, for every Ref, and no `get` handler ever runs. `tsc` is silent
107
+ * because a Ref is a perfectly good object. So:
108
+ *
109
+ * const pot = tx.tables.pots.select({ id }, { limit: 1 }).expectOne(e);
110
+ * if (!pot.balance) { … } // ← ALWAYS false. Silently wrong data.
111
+ *
112
+ * What this file does close: coercion (`Symbol.toPrimitive`/`valueOf`/
113
+ * `toString`), awaiting (`then` is a callable member with a non-thenable
114
+ * signature, which is a *compile* error), serialisation (`toJSON`), and nesting
115
+ * a Ref inside a literal value where the server would store it as data. What it
116
+ * cannot close is a bare truthiness test. The real defence is the build-time
117
+ * static analysis (`tx_analysis.js`, phase P4); until that ships, this hole is
118
+ * open and this comment is the only warning.
119
+ *
120
+ * # Wire contract
121
+ *
122
+ * The JSON this file emits is consumed by
123
+ * `modules/backend/internal/management/tx_program.go`. That decoder rejects
124
+ * unknown fields at every level, so an op carries EXACTLY the fields its kind
125
+ * takes. Everything here that looks like a needless restriction is one of the
126
+ * server's rules made visible early:
127
+ *
128
+ * - `$ref` only points BACKWARDS, and only at an op statically known to yield
129
+ * at most one row (insert, or a `one`/`atMost 1` guard, or `select limit 1`).
130
+ * `.expectOne()` is what this file uses to satisfy that, always.
131
+ * - `$expr` is a closed set: `inc`/`dec` (update only — they read the column's
132
+ * current value) and `now()`.
133
+ * - `update`/`delete` require a `where`; `insert` refuses one.
134
+ * - `insertMany` rows must all set the same columns.
135
+ * - ≤1000 ops, ≤5000 rows per insertMany, ≤8 MiB of JSON.
136
+ *
137
+ * Column keys are emitted SORTED, so the same callback always produces byte-
138
+ * identical JSON. That is what lets the Go decoder be locked to golden files
139
+ * this SDK emits (`testdata/tx_plan_golden/`).
140
+ */
141
+ /**
142
+ * A plan handle was used as if it were a value: awaited, coerced to a string or
143
+ * number, serialised, or nested inside another value.
144
+ *
145
+ * Thrown while the callback is still BUILDING the plan, so nothing has been sent
146
+ * and nothing has been written.
147
+ */
148
+ declare class TxRefError extends Error {
149
+ constructor(message: string);
150
+ }
151
+ /**
152
+ * The plan the callback described cannot be sent: it breaks a rule the server
153
+ * would reject, and rejecting it here names the line that wrote it instead of
154
+ * returning a 400 about an op index.
155
+ */
156
+ declare class TxPlanError extends Error {
157
+ constructor(message: string);
158
+ }
159
+ /** A backwards reference to an earlier op's single-row result. */
160
+ interface TxWireRef {
161
+ $ref: {
162
+ op: number;
163
+ field: string;
164
+ };
165
+ }
166
+ /** A call from the server's closed function set. */
167
+ interface TxWireExpr {
168
+ $expr: {
169
+ fn: "inc" | "dec";
170
+ by: number;
171
+ } | {
172
+ fn: "now";
173
+ };
174
+ }
175
+ /** One value in a `values`/`set`/`where` map: a literal, a `$ref` or a `$expr`. */
176
+ type TxWireValue = TxWireRef | TxWireExpr | unknown;
177
+ /** The tenant's declared expectation about an op's row count. `slot` indexes the
178
+ * client-side error table — the error OBJECT never travels. */
179
+ interface TxWireGuard {
180
+ kind: "one" | "none" | "atLeast" | "atMost";
181
+ n: number;
182
+ slot: number;
183
+ }
184
+ /** One operation in the wire plan. Fields are omitted, never null: the decoder
185
+ * rejects a field that does not belong to the op's kind. */
186
+ interface TxWireOp {
187
+ op: "insert" | "insertMany" | "update" | "delete" | "select";
188
+ table: string;
189
+ values?: Record<string, TxWireValue>;
190
+ rows?: Record<string, TxWireValue>[];
191
+ set?: Record<string, TxWireValue>;
192
+ where?: Record<string, TxWireValue>;
193
+ limit?: number;
194
+ lock?: "update";
195
+ guard?: TxWireGuard;
196
+ }
197
+ /** The request body of `POST /internal-api/db/tx`. */
198
+ interface TxPlanBody {
199
+ ops: TxWireOp[];
200
+ }
201
+ /** One op's outcome, positionally matched to the plan's ops. */
202
+ interface TxPlanOpResult {
203
+ rows: Record<string, unknown>[];
204
+ rows_affected: number;
205
+ }
206
+ /** The response body of `POST /internal-api/db/tx`. */
207
+ interface TxPlanResponse {
208
+ results: TxPlanOpResult[];
209
+ }
210
+ /**
211
+ * The fields the runtime must copy from the broker's error envelope onto the
212
+ * rejection it throws out of {@link DBClient.txPlan}.
213
+ *
214
+ * `slot` is the whole point: on a guard failure the server answers with the
215
+ * INDEX of the expectation that did not hold, never with an error message of its
216
+ * own, and this SDK maps that index back to the `Error` the callback handed to
217
+ * `.expectOne(…)`. Without `slot` a guard failure degrades to a generic 409.
218
+ */
219
+ interface TxPlanRejection {
220
+ status?: number;
221
+ /** `tx_plan_invalid` | `tx_guard_failed` | `tx_ref_unresolved` | a pg class. */
222
+ error_code?: string;
223
+ /** Present only for `tx_guard_failed`: the client-side error table index. */
224
+ slot?: number;
225
+ /** Present on a database error: which op failed. */
226
+ op?: number;
227
+ }
228
+ declare const refBrand: unique symbol;
229
+ declare const rowBrand: unique symbol;
230
+ declare const rowsBrand: unique symbol;
231
+ /**
232
+ * Makes a handle a compile error to `await`.
233
+ *
234
+ * `then` is declared as a CALLABLE member whose signature is not `PromiseLike`,
235
+ * which is precisely the shape TypeScript rejects: `await handle` is TS1320 and
236
+ * `async () => handle` is TS1058. A non-callable `then` would not do it — the
237
+ * compiler simply ignores those.
238
+ */
239
+ interface NotAwaitable {
240
+ /** Not a promise. Nothing here has run yet; there is nothing to await. */
241
+ then(doNotAwaitAPlanHandle: "a transaction plan is built synchronously"): never;
242
+ }
243
+ /**
244
+ * A value the SERVER will produce, standing in for a column of a row this plan
245
+ * writes or reads.
246
+ *
247
+ * Legal uses: write it into a later operation's `values`/`set`/`where`, or
248
+ * return it from the callback (it is replaced by the real value before
249
+ * `transaction()` resolves).
250
+ *
251
+ * Illegal, and caught: `await`, `String(ref)`, `` `${ref}` ``, `ref + 1`,
252
+ * `JSON.stringify(ref)`, burying it inside a jsonb object.
253
+ *
254
+ * Illegal, and NOT caught: `if (ref)`. See the truthiness note at the top.
255
+ */
256
+ interface Ref<T> extends NotAwaitable {
257
+ readonly [refBrand]: T;
258
+ }
259
+ /** The brand carried by a single-row handle, and the seam `Materialized` reads
260
+ * to turn `return st` into the whole row. */
261
+ interface TxRowHandle<Row> extends NotAwaitable {
262
+ readonly [rowBrand]: Row;
263
+ }
264
+ /**
265
+ * A row this plan is known to produce exactly one of. Every property is a
266
+ * {@link Ref}; returning the handle itself yields the whole row.
267
+ *
268
+ * Only `.expectOne(err)` produces one — which is the design: a row you can read
269
+ * fields from is a row whose absence you have already answered for.
270
+ */
271
+ type TxRow<Row> = {
272
+ readonly [K in keyof Row]: Ref<Row[K]>;
273
+ } & TxRowHandle<Row>;
274
+ /**
275
+ * The result of one operation, before any expectation is declared about it.
276
+ *
277
+ * Deliberately not a row and not a list: an operation's row count is not known
278
+ * until the server runs it, so the only thing that can be said about it here is
279
+ * an EXPECTATION. Declaring one is also the only way to get a readable row.
280
+ *
281
+ * At most one expectation per operation — the wire carries one guard per op, and
282
+ * a second call throws rather than silently dropping the first.
283
+ */
284
+ interface TxRows<Row> extends NotAwaitable {
285
+ readonly [rowsBrand]: Row;
286
+ /**
287
+ * Require exactly one row, and read it. On any other count the server rolls
288
+ * the whole transaction back and this `error` is thrown to the caller.
289
+ *
290
+ * This is the only way to reach a row's fields, and the only shape a `$ref`
291
+ * may point at.
292
+ */
293
+ expectOne(error: Error): TxRow<Row>;
294
+ /** Require zero rows (e.g. "this membership must not already exist"). */
295
+ expectNone(error: Error): void;
296
+ /** Require at least `n` rows. */
297
+ expectAtLeast(n: number, error: Error): void;
298
+ /** Require at most `n` rows. */
299
+ expectAtMost(n: number, error: Error): void;
300
+ }
301
+ /** `now()` — the server's clock, usable wherever a value is. */
302
+ interface TxNow extends NotAwaitable {
303
+ readonly $expr: {
304
+ fn: "now";
305
+ };
306
+ }
307
+ /** `inc(n)` / `dec(n)` — read the column's CURRENT value and write it back
308
+ * changed. Only meaningful in an update's `set`, which is where the types allow
309
+ * it and where the server allows it. */
310
+ interface TxColumnExpr extends NotAwaitable {
311
+ readonly $expr: {
312
+ fn: "inc" | "dec";
313
+ by: number;
314
+ };
315
+ }
316
+ /**
317
+ * Resolve a callback's return type against what actually comes back: every
318
+ * {@link Ref} becomes its value, every {@link TxRow} becomes its row, and
319
+ * anything else keeps its shape.
320
+ *
321
+ * A {@link TxRows} resolves to an explanatory string type rather than a row
322
+ * list: it has no single answer to give, and saying so in the type is louder
323
+ * than a runtime throw.
324
+ */
325
+ type Materialized<T> = T extends Ref<infer U> ? U : T extends TxRowHandle<infer R> ? R : T extends TxRows<unknown> ? "a TxRows cannot leave the transaction callback — read a row with .expectOne(err) first" : T extends Date ? T : T extends object ? {
326
+ [K in keyof T]: Materialized<T[K]>;
327
+ } : T;
328
+ /** A value written by an INSERT: a literal, an earlier row's field, or `now()`.
329
+ * `inc`/`dec` are absent on purpose — they read a current value, and an inserted
330
+ * row has none. */
331
+ type TxInsertValue<V> = V | Ref<V> | TxNow;
332
+ /** A value written by an UPDATE's `set`: everything an insert takes, plus the
333
+ * read-modify-write expressions. */
334
+ type TxSetValue<V> = V | Ref<V> | TxNow | TxColumnExpr;
335
+ /** An insert payload: the table's insert shape, with refs and `now()` allowed. */
336
+ type TxInsertShape<Insert> = {
337
+ [K in keyof Insert]: TxInsertValue<Insert[K]>;
338
+ };
339
+ /** An update's `set`: any subset of the insert shape, with expressions allowed. */
340
+ type TxSetShape<Insert> = {
341
+ [K in keyof Insert]?: TxSetValue<Insert[K]>;
342
+ };
343
+ /**
344
+ * A filter. Every entry is an equality test and they are AND-ed; a `null`
345
+ * becomes `IS NULL`, which is what makes `{ accepted_at: null }` a usable
346
+ * "not yet accepted" guard rather than a clause that matches nothing.
347
+ */
348
+ type TxWhere<Row> = {
349
+ [K in keyof Row]?: Row[K] | Ref<Row[K]>;
350
+ };
351
+ /** Options for a plan `select`. */
352
+ interface TxSelectOptions {
353
+ /** Cap the rows read. */
354
+ limit?: number;
355
+ /** Take a real `FOR UPDATE` row lock for the rest of the transaction. */
356
+ lock?: "update";
357
+ }
358
+ /** One table, as the plan sees it. */
359
+ interface TxTable<Row, Insert> {
360
+ /** Insert one row. Returns a handle — call `.expectOne(err)` to read fields. */
361
+ insert(values: TxInsertShape<Insert>): TxRows<Row>;
362
+ /**
363
+ * Insert many rows in ONE statement. Every row must set the same columns
364
+ * (a row that omits one would silently take the column's default).
365
+ *
366
+ * An empty list writes nothing and sends nothing.
367
+ */
368
+ insertMany(rows: readonly TxInsertShape<Insert>[]): TxRows<Row>;
369
+ /**
370
+ * Update every row matching `where`. The filter comes first because it is the
371
+ * dangerous half: an update whose `where` you got wrong rewrites rows you
372
+ * never looked at. The server refuses an update with no `where` at all.
373
+ */
374
+ updateWhere(where: TxWhere<Row>, set: TxSetShape<Insert>): TxRows<Row>;
375
+ /** Delete every row matching `where`. The server refuses an unfiltered delete. */
376
+ deleteWhere(where: TxWhere<Row>): TxRows<Row>;
377
+ /** Read rows, optionally locking them for the rest of the transaction. */
378
+ select(where?: TxWhere<Row>, options?: TxSelectOptions): TxRows<Row>;
379
+ }
380
+ /**
381
+ * The handle a transaction callback receives.
382
+ *
383
+ * It carries tables and nothing else: no `query`, no `findById`, no `asService`.
384
+ * A read whose value the plan does not write belongs OUTSIDE the transaction,
385
+ * where it costs one round trip and can be branched on like an ordinary value.
386
+ */
387
+ interface TxPlanHandle<TTables> {
388
+ tables: TTables;
389
+ }
390
+ /** The server's `now()`. */
391
+ declare function now(): TxNow;
392
+ /** Add `by` to the column's current value. Only valid in an update's `set`. */
393
+ declare function inc(by: number): TxColumnExpr;
394
+ /** Subtract `by` from the column's current value. Only valid in an update's `set`. */
395
+ declare function dec(by: number): TxColumnExpr;
396
+
70
397
  /** HTTP error with structured error response format.
71
398
  *
72
399
  * The base class for the throwable error classes (`PalError`, `Conflict`,
@@ -1397,24 +1724,29 @@ interface DBOps {
1397
1724
  findById(table: string, id: string): Promise<Record<string, unknown> | null>;
1398
1725
  findMany(table: string, query?: Record<string, unknown>): Promise<Record<string, unknown>[]>;
1399
1726
  }
1400
- /** The transaction-scoped client passed to `db.transaction(fn)` — the raw DB
1401
- * ops only. No nested transaction and no `asService` (the DB role is fixed once
1402
- * when the transaction begins; see `Database.asService().transaction(...)`). */
1403
- type TxClient = DBOps;
1404
1727
  /** Database client interface injected into endpoint context. */
1405
1728
  interface DBClient extends DBOps {
1406
1729
  /**
1407
- * Run an interactive transaction. The callback receives a `tx` with the same
1408
- * DB ops as this client; returning normally commits, throwing rolls back.
1409
- * Nested transactions are not supported.
1730
+ * Run a whole transaction in ONE request (`POST /internal-api/db/tx`).
1731
+ *
1732
+ * The low-level seam behind `Database.transaction(plan)`: the SDK builds the
1733
+ * plan, this sends it, and the broker executes every operation inside a single
1734
+ * transaction that commits or rolls back before the response is written.
1735
+ * Nothing pins a Postgres backend across round trips, because there is only
1736
+ * one round trip.
1737
+ *
1738
+ * On failure the runtime must reject with an error carrying the broker's
1739
+ * envelope fields — see {@link TxPlanRejection}. `slot` in particular is what
1740
+ * turns a `tx_guard_failed` back into the `Error` the tenant handed to
1741
+ * `.expectOne(…)`; without it a declared expectation degrades to a generic 409.
1410
1742
  */
1411
- transaction<T>(fn: (tx: TxClient) => Promise<T>): Promise<T>;
1743
+ txPlan(plan: TxPlanBody): Promise<TxPlanResponse>;
1412
1744
  /**
1413
1745
  * Return a sibling DB client that bypasses Row-Level Security by running as
1414
1746
  * the `service_role` (BYPASSRLS). Use sparingly and explicitly — the default
1415
1747
  * `Database.*` path is RLS-enforced. The returned client exposes the same op
1416
- * surface (`query`/`insert`/.../`transaction`) but never re-exposes
1417
- * `asService` (no double-bypass).
1748
+ * surface (`query`/`insert`/.../`txPlan`) but never re-exposes `asService`
1749
+ * (no double-bypass).
1418
1750
  */
1419
1751
  asService(): Omit<DBClient, "asService">;
1420
1752
  }
@@ -1652,4 +1984,4 @@ type Middleware = (ctx: MiddlewareContext, next: () => Promise<void>) => Promise
1652
1984
  */
1653
1985
  type AuthSpec = boolean | Partial<AuthConfig>;
1654
1986
 
1655
- export { type PalbaseDeviceTokenView as $, type AuthSpec as A, BadRequest as B, type CacheClient as C, type DBClient as D, type ErrorDef as E, type FileContext as F, type PalbaseBatchOverrideOperation as G, HttpError as H, type PalbaseBatchSetOverridesResult as I, type PalbaseBindDeviceParams as J, type PalbaseBucketClient as K, type Logger as L, type Middleware as M, NotFound as N, type PalbaseClearAllOverridesResult as O, type PBRequest as P, type QueueClient as Q, type RateLimitConfig as R, type PalbaseClearOverrideResult as S, type PalbaseCohortQueryInput as T, type User as U, type PalbaseCohortResult as V, type PalbaseCollectionRef as W, type PalbaseCountQueryInput as X, type PalbaseCountResult as Y, type PalbaseCreateLinkParams as Z, type PalbaseDeviceInfo as _, type PalbaseModuleClients as a, type PalbaseVerifyRequestSignatureParams as a$, type PalbaseDocumentRef as a0, type PalbaseDocumentSnapshot as a1, type PalbaseEmailClient as a2, type PalbaseEmailSendParams as a3, type PalbaseEmailSendResponse as a4, type PalbaseEventNamesResult as a5, type PalbaseEventsQueryInput as a6, type PalbaseEventsResult as a7, type PalbaseFileObject as a8, type PalbaseFlag as a9, type PalbaseOverviewResult as aA, type PalbasePreferences as aB, type PalbasePreferencesClient as aC, type PalbasePublicUrlResponse as aD, type PalbasePushClient as aE, type PalbasePushSendParams as aF, type PalbasePushSendResponse as aG, type PalbaseQrCodeOptions as aH, type PalbaseQuerySnapshot as aI, type PalbaseRegisterDeviceParams as aJ, type PalbaseResult as aK, type PalbaseRetentionQueryInput as aL, type PalbaseRetentionResult as aM, type PalbaseSession as aN, type PalbaseSetOverrideResult as aO, type PalbaseSetOverridesResult as aP, type PalbaseSignedUrlResponse as aQ, type PalbaseSmsClient as aR, type PalbaseSmsSendParams as aS, type PalbaseSmsSendResponse as aT, type PalbaseTransformOptions as aU, type PalbaseUpdateLinkParams as aV, type PalbaseUploadOptions as aW, type PalbaseUser as aX, type PalbaseUserDetailResult as aY, type PalbaseUsersQueryInput as aZ, type PalbaseUsersResult as a_, type PalbaseFlagContext as aa, type PalbaseFlagSource as ab, type PalbaseFlagValue as ac, type PalbaseFlagVariant as ad, type PalbaseFlagsServiceClient as ae, type PalbaseFunctionsClient as af, type PalbaseFunnelQueryInput as ag, type PalbaseFunnelResult as ah, type PalbaseIdentifyTraits as ai, type PalbaseInboxClient as aj, type PalbaseInboxListOptions as ak, type PalbaseInboxListResult as al, type PalbaseInboxMessage as am, type PalbaseInboxSendParams as an, type PalbaseInboxSendResponse as ao, type PalbaseInitialLink as ap, type PalbaseInvokeOptions as aq, type PalbaseLink as ar, type PalbaseLinkAnalytics as as, type PalbaseLinkDetails as at, type PalbaseLinksClient as au, type PalbaseListLinksOptions as av, type PalbaseListLinksResult as aw, type PalbaseListOptions as ax, type PalbaseMatchParams as ay, type PalbaseMultiChannelResponse as az, type PalbaseDocsClient as b, type PalbaseWhereOperator as b0, TooManyRequests as b1, type TxClient as b2, Unauthorized as b3, type VerifiedDevice as b4, defineMiddleware as b5, type PalbaseFlagsClient as c, type PalbaseNotificationsClient as d, type PalbaseRealtimeClient as e, type PalbaseStorageClient as f, type AuthConfig as g, type ClientInfo as h, Conflict as i, type DBOps as j, type ErrorMap as k, type ErrorThrowers as l, Forbidden as m, type HttpMethod as n, type MiddlewareContext as o, type MiddlewareHandler as p, PalError as q, type PalbaseAnalyticsClient as r, type PalbaseAnalyticsManagementNamespace as s, type PalbaseAnalyticsProperties as t, type PalbaseAnalyticsQueryNamespace as u, type PalbaseAttestAndroidParams as v, type PalbaseAttestAndroidResult as w, type PalbaseAttestiOSParams as x, type PalbaseAttestiOSResult as y, type PalbaseAuthClient as z };
1987
+ export { type PalbaseDeviceInfo as $, type AuthSpec as A, BadRequest as B, type CacheClient as C, type DBClient as D, type ErrorDef as E, type FileContext as F, type PalbaseAuthClient as G, HttpError as H, type PalbaseBatchOverrideOperation as I, type PalbaseBatchSetOverridesResult as J, type PalbaseBindDeviceParams as K, type Logger as L, type Materialized as M, NotFound as N, type PalbaseBucketClient as O, type PBRequest as P, type QueueClient as Q, type RateLimitConfig as R, type PalbaseClearAllOverridesResult as S, type PalbaseClearOverrideResult as T, type User as U, type PalbaseCohortQueryInput as V, type PalbaseCohortResult as W, type PalbaseCollectionRef as X, type PalbaseCountQueryInput as Y, type PalbaseCountResult as Z, type PalbaseCreateLinkParams as _, type PalbaseModuleClients as a, type PalbaseUsersResult as a$, type PalbaseDeviceTokenView as a0, type PalbaseDocumentRef as a1, type PalbaseDocumentSnapshot as a2, type PalbaseEmailClient as a3, type PalbaseEmailSendParams as a4, type PalbaseEmailSendResponse as a5, type PalbaseEventNamesResult as a6, type PalbaseEventsQueryInput as a7, type PalbaseEventsResult as a8, type PalbaseFileObject as a9, type PalbaseMultiChannelResponse as aA, type PalbaseOverviewResult as aB, type PalbasePreferences as aC, type PalbasePreferencesClient as aD, type PalbasePublicUrlResponse as aE, type PalbasePushClient as aF, type PalbasePushSendParams as aG, type PalbasePushSendResponse as aH, type PalbaseQrCodeOptions as aI, type PalbaseQuerySnapshot as aJ, type PalbaseRegisterDeviceParams as aK, type PalbaseResult as aL, type PalbaseRetentionQueryInput as aM, type PalbaseRetentionResult as aN, type PalbaseSession as aO, type PalbaseSetOverrideResult as aP, type PalbaseSetOverridesResult as aQ, type PalbaseSignedUrlResponse as aR, type PalbaseSmsClient as aS, type PalbaseSmsSendParams as aT, type PalbaseSmsSendResponse as aU, type PalbaseTransformOptions as aV, type PalbaseUpdateLinkParams as aW, type PalbaseUploadOptions as aX, type PalbaseUser as aY, type PalbaseUserDetailResult as aZ, type PalbaseUsersQueryInput as a_, type PalbaseFlag as aa, type PalbaseFlagContext as ab, type PalbaseFlagSource as ac, type PalbaseFlagValue as ad, type PalbaseFlagVariant as ae, type PalbaseFlagsServiceClient as af, type PalbaseFunctionsClient as ag, type PalbaseFunnelQueryInput as ah, type PalbaseFunnelResult as ai, type PalbaseIdentifyTraits as aj, type PalbaseInboxClient as ak, type PalbaseInboxListOptions as al, type PalbaseInboxListResult as am, type PalbaseInboxMessage as an, type PalbaseInboxSendParams as ao, type PalbaseInboxSendResponse as ap, type PalbaseInitialLink as aq, type PalbaseInvokeOptions as ar, type PalbaseLink as as, type PalbaseLinkAnalytics as at, type PalbaseLinkDetails as au, type PalbaseLinksClient as av, type PalbaseListLinksOptions as aw, type PalbaseListLinksResult as ax, type PalbaseListOptions as ay, type PalbaseMatchParams as az, type PalbaseDocsClient as b, type PalbaseVerifyRequestSignatureParams as b0, type PalbaseWhereOperator as b1, type Ref as b2, TooManyRequests as b3, type TxColumnExpr as b4, type TxInsertShape as b5, type TxInsertValue as b6, type TxNow as b7, type TxPlanBody as b8, TxPlanError as b9, type TxPlanHandle as ba, type TxPlanOpResult as bb, type TxPlanRejection as bc, type TxPlanResponse as bd, TxRefError as be, type TxRow as bf, type TxRows as bg, type TxSelectOptions as bh, type TxSetShape as bi, type TxSetValue as bj, type TxTable as bk, type TxWhere as bl, type TxWireExpr as bm, type TxWireGuard as bn, type TxWireOp as bo, type TxWireRef as bp, type TxWireValue as bq, Unauthorized as br, type VerifiedDevice as bs, dec as bt, defineMiddleware as bu, inc as bv, now as bw, type PalbaseFlagsClient as c, type PalbaseNotificationsClient as d, type PalbaseRealtimeClient as e, type PalbaseStorageClient as f, type AuthConfig as g, type ClientInfo as h, Conflict as i, type DBOps as j, type ErrorMap as k, type ErrorThrowers as l, Forbidden as m, type HttpMethod as n, type Middleware as o, type MiddlewareContext as p, type MiddlewareHandler as q, PalError as r, type PalbaseAnalyticsClient as s, type PalbaseAnalyticsManagementNamespace as t, type PalbaseAnalyticsProperties as u, type PalbaseAnalyticsQueryNamespace as v, type PalbaseAttestAndroidParams as w, type PalbaseAttestAndroidResult as x, type PalbaseAttestiOSParams as y, type PalbaseAttestiOSResult as z };
@@ -1,5 +1,5 @@
1
1
  import { Tables, TableTypes } from './db/env.cjs';
2
- import { D as DBClient } from './endpoint-92kVepng.cjs';
2
+ import { D as DBClient, ba as TxPlanHandle, bk as TxTable, M as Materialized } from './endpoint-Ck4hER_7.cjs';
3
3
 
4
4
  /** On delete action for foreign key references. */
5
5
  type OnDeleteAction = 'cascade' | 'set null' | 'restrict' | 'no action';
@@ -536,29 +536,27 @@ interface TypedDB<S extends SchemaDef> {
536
536
  tables: {
537
537
  [K in keyof S["tables"]]: TypedTable<S["tables"][K]>;
538
538
  };
539
- transaction<T>(fn: (tx: TypedTx<S>) => Promise<T>): Promise<T>;
540
- }
541
- /** Transaction-scoped typed facade: same typed tables, no nested transaction. */
542
- interface TypedTx<S extends SchemaDef> {
543
- tables: {
544
- [K in keyof S["tables"]]: TypedTable<S["tables"][K]>;
545
- };
539
+ /** Run a transaction plan. See {@link EnvTypedDatabase.transaction}. */
540
+ transaction<T>(fn: (tx: TypedTx<S>) => T extends Promise<unknown> ? never : T): Promise<Materialized<T>>;
546
541
  }
542
+ /** The plan-building handle a `TypedDB<S>` transaction callback receives: the
543
+ * schema's tables, expressed as plan operations rather than awaited calls. */
544
+ type TypedTx<S extends SchemaDef> = TxPlanHandle<{
545
+ [K in keyof S["tables"]]: TxTable<RowShape<S["tables"][K]>, InsertShape<S["tables"][K]>>;
546
+ }>;
547
547
  /**
548
548
  * Wraps a raw `DBClient` with the type-safe `TypedDB<S>` facade derived from
549
- * the provided schema. No behavior change — all calls delegate to `raw` with
550
- * the table name as a plain string.
549
+ * the provided schema. No behavior change for the direct ops — all calls
550
+ * delegate to `raw` with the table name as a plain string.
551
551
  *
552
- * `buildTables` is the reusable factory that wraps any op-bearing client
553
- * (`TxClient` — the surface shared by `DBClient` and the transaction-scoped
554
- * client) into the typed tables map. It is used both for the top-level db
555
- * (wrapping `raw`) and inside `transaction`, where it wraps the raw `TxClient`
556
- * the runtime yields so the callback sees the same typed `.tables` API.
552
+ * `transaction` does NOT delegate to a per-op client: the callback describes a
553
+ * plan against a fresh {@link TxPlanBuilder}, and the whole plan travels in one
554
+ * `raw.txPlan` call. The schema is used only for its table NAMES; the values
555
+ * are typed by `S` at compile time and are plain strings at run time.
557
556
  *
558
- * `transaction` delegates straight to `raw.transaction`; the two narrow
559
- * `as TypedTx<S>` / `as TypedDB<S>` casts are single structural narrowings
560
- * from the dynamically-built tables object to the precise mapped type (TS
561
- * cannot infer through `Object.keys` iteration) — see module-level doc comment.
557
+ * The `as` casts are single structural narrowings from a dynamically-built
558
+ * object to the precise mapped type (TS cannot infer the mapped-type result
559
+ * through `Object.keys` iteration) — see the module-level doc comment.
562
560
  */
563
561
  declare function makeTypedDB<S extends SchemaDef>(schema: S, raw: DBClient): TypedDB<S>;
564
562
  /** A typed table accessor derived from one env `Tables` entry's flat shapes. */
@@ -578,34 +576,66 @@ interface EnvTypedTable<T extends TableTypes> {
578
576
  type EnvTables = {
579
577
  [K in keyof Tables]: EnvTypedTable<Tables[K]>;
580
578
  };
581
- /** Transaction-scoped typed facade for the env-augmented surface: same typed
582
- * tables, no nested transaction. */
583
- interface EnvTypedTx {
584
- tables: EnvTables;
585
- }
579
+ /** The project's tables as PLAN operations, keyed by the env `Tables`
580
+ * interface. The transaction twin of {@link EnvTables}. */
581
+ type TxTables = {
582
+ [K in keyof Tables]: TxTable<Tables[K]["row"], Tables[K]["insert"]>;
583
+ };
584
+ /**
585
+ * The handle a `Database.transaction(…)` callback receives.
586
+ *
587
+ * Tables only — no `query`, no `findById`, no `asService`. A read whose value
588
+ * the plan does not write belongs outside the transaction, where it costs one
589
+ * round trip and is an ordinary value you can branch on.
590
+ */
591
+ type TxPlan = TxPlanHandle<TxTables>;
586
592
  /**
587
593
  * The RLS-bypass sibling returned by `Database.asService()`. Same typed surface
588
594
  * as {@link EnvTypedDatabase} — `tables`, the raw string ops, and a typed
589
595
  * `transaction` — but it does NOT re-expose `asService` (no double-bypass).
590
596
  * Every op it performs runs as the `service_role` (BYPASSRLS).
591
597
  */
592
- interface EnvServiceDatabase extends Omit<DBClient, "transaction" | "asService"> {
598
+ interface EnvServiceDatabase extends Omit<DBClient, "txPlan" | "asService"> {
593
599
  tables: EnvTables;
594
- transaction<T>(fn: (tx: EnvTypedTx) => Promise<T>): Promise<T>;
600
+ transaction<T>(fn: (tx: TxPlan) => T extends Promise<unknown> ? never : T): Promise<Materialized<T>>;
595
601
  }
596
602
  /**
597
603
  * The typed-by-default Database surface: the raw string-keyed `DBClient` ops
598
604
  * PLUS a `tables` map typed against the project's generated `palbase-env.d.ts`,
599
- * a `transaction` whose callback receives the typed tables, and `asService()`
600
- * for the explicit RLS-bypass sibling.
605
+ * a `transaction` that runs a whole plan in one request, and `asService()` for
606
+ * the explicit RLS-bypass sibling.
601
607
  *
602
- * `transaction` is declared here (overriding `DBClient["transaction"]`) so the
603
- * `tx` the callback receives carries the typed `.tables` API. `asService` is
604
- * re-typed to return the typed {@link EnvServiceDatabase} sibling.
608
+ * The low-level `txPlan` op is deliberately NOT re-exposed here: `transaction`
609
+ * is the surface, and a hand-built plan would bypass the ref/guard machinery
610
+ * that makes one safe to write.
605
611
  */
606
- interface EnvTypedDatabase extends Omit<DBClient, "transaction" | "asService"> {
612
+ interface EnvTypedDatabase extends Omit<DBClient, "txPlan" | "asService"> {
607
613
  tables: EnvTables;
608
- transaction<T>(fn: (tx: EnvTypedTx) => Promise<T>): Promise<T>;
614
+ /**
615
+ * Run a transaction. The callback DESCRIBES the operations; the whole
616
+ * description travels in one request and the broker runs it inside a single
617
+ * transaction — committing when it finishes, rolling back on any failure.
618
+ *
619
+ * The callback is SYNCHRONOUS: nothing has run when it returns, so there is
620
+ * nothing to await. `async` on it and `await` inside it are compile errors.
621
+ * Values a later operation needs are {@link Ref}s, written straight into the
622
+ * next operation; values the CALLER needs are returned and substituted before
623
+ * this promise resolves.
624
+ *
625
+ * @example
626
+ * const { statementId } = await Database.transaction((tx) => {
627
+ * const st = tx.tables.statements
628
+ * .insert({ household_id: hid, file_sha256: sha, status: "reviewing" })
629
+ * .expectOne(new Internal("statement insert failed"));
630
+ *
631
+ * tx.tables.statement_lines.insertMany(
632
+ * lines.map((l) => ({ statement_id: st.id, category: resolveCategory(l) })),
633
+ * );
634
+ *
635
+ * return { statementId: st.id };
636
+ * });
637
+ */
638
+ transaction<T>(fn: (tx: TxPlan) => T extends Promise<unknown> ? never : T): Promise<Materialized<T>>;
609
639
  /**
610
640
  * Return a sibling that bypasses RLS by running as the `service_role`. Use
611
641
  * sparingly and explicitly — the default `Database.*` path is RLS-enforced.
@@ -617,4 +647,4 @@ interface EnvTypedDatabase extends Omit<DBClient, "transaction" | "asService"> {
617
647
  asService(): EnvServiceDatabase;
618
648
  }
619
649
 
620
- export { makeTypedDB as A, numeric as B, ColumnBuilder as C, policy as D, type EnvTypedDatabase as E, raw as F, text as G, timestamp as H, type InsertShape as I, uuid as J, type OnDeleteAction as O, PALBASE_EXTENSIONS as P, type RawConstraintDef as R, type SchemaDef as S, type TableDef as T, type ColumnDef as a, type ColumnMap as b, type ColumnType as c, EXTENSION_DEPENDENCIES as d, type EnvServiceDatabase as e, type EnvTables as f, type EnvTypedTable as g, type EnvTypedTx as h, type PalbaseExtension as i, PolicyBuilder as j, type PolicyCommand as k, type PolicyDef as l, type PolicyMode as m, type RowShape as n, type SchemaInput as o, type TableInput as p, type TypedDB as q, type TypedTable as r, type TypedTx as s, bigint as t, boolean as u, defineSchema as v, enumType as w, integer as x, isPalbaseExtension as y, jsonb as z };
650
+ export { jsonb as A, makeTypedDB as B, ColumnBuilder as C, numeric as D, type EnvTypedDatabase as E, policy as F, raw as G, text as H, type InsertShape as I, timestamp as J, uuid as K, type OnDeleteAction as O, PALBASE_EXTENSIONS as P, type RawConstraintDef as R, type SchemaDef as S, type TableDef as T, type ColumnDef as a, type ColumnMap as b, type ColumnType as c, EXTENSION_DEPENDENCIES as d, type EnvServiceDatabase as e, type EnvTables as f, type EnvTypedTable as g, type PalbaseExtension as h, PolicyBuilder as i, type PolicyCommand as j, type PolicyDef as k, type PolicyMode as l, type RowShape as m, type SchemaInput as n, type TableInput as o, type TxPlan as p, type TxTables as q, type TypedDB as r, type TypedTable as s, type TypedTx as t, bigint as u, boolean as v, defineSchema as w, enumType as x, integer as y, isPalbaseExtension as z };