@voltro/database 0.18.0 → 0.20.0

This diff represents the content of publicly available package versions that have been released to one of the supported registries. The information contained in this diff is provided for informational purposes only and reflects changes between package versions as they appear in their respective public registries.
package/dist/index.d.ts CHANGED
@@ -159,6 +159,30 @@ export declare const arrayOverlaps: <RowOf = Record<string, unknown>, K extends
159
159
  */
160
160
  export declare const attachEagerLoads: (rows: ReadonlyArray<Row_2>, spec: WithSpec, sourceTable: TableLike, lookup: EagerLookup) => Promise<ReadonlyArray<Row_2>>;
161
161
 
162
+ /**
163
+ * The attribution fields to spread onto a `ChangeEvent` at creation.
164
+ *
165
+ * Returns an object with the keys OMITTED rather than set to `undefined`, so an
166
+ * unattributed event is byte-identical to one from before this existed — no
167
+ * `traceId: undefined` appearing in a snapshot, a broadcast payload, or a
168
+ * consumer's `Object.keys`.
169
+ *
170
+ * **Call it where the event is CREATED, not where it is delivered.** A
171
+ * transactional write queues its events and flushes them after commit, by which
172
+ * time the async-local scope is gone. Stamping at creation is what makes the
173
+ * transactional path carry identity at all.
174
+ */
175
+ export declare const attributionFields: () => {
176
+ traceId?: string;
177
+ subjectId?: string | null;
178
+ };
179
+
180
+ /** The write's identity, as both sides can compute it. NOT the row image: the
181
+ * transport re-encodes values (dates as strings, numbers as strings on some
182
+ * drivers), so an image hash would disagree across the two sides and silently
183
+ * never match. `(table, op, pk)` is stable through any re-encoding. */
184
+ export declare const attributionKey: (table: string, op: string, primaryKey: unknown) => string;
185
+
162
186
  /**
163
187
  * Audit a list of tables in one call. Convenience for the CLI's boot
164
188
  * path; same shape as calling `auditTableIndexes` per table and
@@ -464,6 +488,32 @@ export declare type ChangeEvent = {
464
488
  * so the consumer must elect a single worker instead.
465
489
  */
466
490
  readonly origin?: 'inline' | 'injected';
491
+ /**
492
+ * The trace this write happened under — the SAME id `auditPlugin` records as
493
+ * `AuditEvent.traceId` and `voltro logs --trace` resolves.
494
+ *
495
+ * This is the correlation bridge. Without it `plugin-versioning` knows WHAT
496
+ * changed and the audit sink knows WHO called and whether it was refused, and
497
+ * nothing joins the two. With it, "what did this call touch" is one query.
498
+ *
499
+ * Absent means the write had no request behind it: a seed, a `*.startup.tsx`,
500
+ * a schedule, a workflow step — or an event injected from another replica.
501
+ * For `origin: 'injected'` the honest answer is the ORIGINATING replica's
502
+ * trace or nothing at all; stamping the receiving replica's ambient trace
503
+ * would attribute a remote write to a local request, which is worse than
504
+ * silence.
505
+ */
506
+ readonly traceId?: string;
507
+ /**
508
+ * The acting identity behind the write — the same one `audit()` stamps into
509
+ * `createdBy`/`updatedBy`, so a row's stamp and its change event never
510
+ * disagree. For an API-key subject that is the person, not the credential;
511
+ * which key was used is recoverable from the audit row sharing `traceId`.
512
+ *
513
+ * `null` = a resolved subject with no acting user (anonymous / system).
514
+ * `undefined` = no request context at all. The two are different facts.
515
+ */
516
+ readonly subjectId?: string | null;
467
517
  };
468
518
 
469
519
  /**
@@ -478,9 +528,22 @@ export declare type ChangeEvent = {
478
528
  */
479
529
  export declare type ChangeStrategy = 'inline' | 'cdc';
480
530
 
531
+ /**
532
+ * Claim the attribution for an echo, or `undefined` if this write did not
533
+ * originate here.
534
+ *
535
+ * Claiming REMOVES it: one registration answers exactly one echo, so a second
536
+ * echo of a different write to the same row cannot inherit the first's
537
+ * identity.
538
+ */
539
+ export declare const claimPendingAttribution: (key: string) => WriteAttribution | undefined;
540
+
481
541
  /** Wipe — used by tests + the dev-loop hot-reload. */
482
542
  export declare const clearEnumRenames: () => void;
483
543
 
544
+ /** Test seam. */
545
+ export declare const clearPendingAttribution: () => void;
546
+
484
547
  /** Wipe the registry. Used by tests + the dev-loop hot-reload. */
485
548
  export declare const clearRelationsRegistry: () => void;
486
549
 
@@ -492,6 +555,9 @@ export declare const clearRetentions: () => void;
492
555
  /** Wipe — used by tests + the dev-loop hot-reload. */
493
556
  export declare const clearTableRegistry: () => void;
494
557
 
558
+ /** Test seam / dev-restart seam. */
559
+ export declare const clearWriteRecorders: () => void;
560
+
495
561
  /**
496
562
  * The value to bind for `column`, given what the table declares it as.
497
563
  * Returns `value` unchanged whenever nothing unambiguous applies — including
@@ -564,8 +630,25 @@ export declare class ColumnBuilder<TsType, Type extends ColumnType, HasDefault e
564
630
  * keyHash: text().serverOnly(),
565
631
  *
566
632
  * A column is exposed to both server and client by default; this opts it OUT of
567
- * the client. The `crud.*` read helpers strip it from every returned row, and
568
- * the boot audit fails a wire-reachable query that declares it in its output.
633
+ * the client. The `crud.*` read helpers strip it from every returned row.
634
+ *
635
+ * A HAND-WRITTEN query output is the case `crud.*` cannot cover, so an audit
636
+ * checks it: a wire-reachable query whose `source` table carries a serverOnly
637
+ * column that its output DECLARES. What that costs depends on the command,
638
+ * and the difference is deliberate — a refused boot mid-edit is worse than
639
+ * the bug; in production it is not:
640
+ *
641
+ * | command | on a leak |
642
+ * |--------------------------------|------------------------------------|
643
+ * | `voltro serve` | **boot fails** |
644
+ * | `voltro doctor` | **exits non-zero** |
645
+ * | `voltro dev` | warns |
646
+ *
647
+ * `VOLTRO_SERVER_ONLY=strict` makes `voltro dev` fail too; `=warn` downgrades
648
+ * serve; `=off` silences it. (`voltro check` does NOT run this — it has a
649
+ * live-api mode with no access to your table definitions, and a rule that
650
+ * fires in one of its two modes would be worse than one that fires in
651
+ * neither.)
569
652
  */
570
653
  serverOnly(): this;
571
654
  /**
@@ -1005,8 +1088,9 @@ export declare interface ColumnDefinition<TsType, Type extends ColumnType = Colu
1005
1088
  * but not encrypted; a private note the owner may read is encrypted but not
1006
1089
  * serverOnly. The default is exposed to BOTH server and client; `.serverOnly()`
1007
1090
  * opts a column OUT of the client. The table-aware read paths (`crud.*`) strip
1008
- * it automatically, and the boot audit flags a wire-reachable query whose
1009
- * `source` table carries a serverOnly column that its output declares.
1091
+ * it automatically, and an audit flags a wire-reachable query whose `source`
1092
+ * table carries a serverOnly column that its output declares. What that audit
1093
+ * COSTS is per command — see `serverOnly()` below for the matrix.
1010
1094
  */
1011
1095
  readonly serverOnly?: boolean;
1012
1096
  /**
@@ -1328,6 +1412,9 @@ export declare const count: (alias?: string) => AggregateColumn;
1328
1412
 
1329
1413
  export declare const countDistinct: (column: string, alias?: string) => AggregateColumn;
1330
1414
 
1415
+ /** The attribution on the current async stack, or `undefined` outside a request. */
1416
+ export declare const currentWriteAttribution: () => WriteAttribution | undefined;
1417
+
1331
1418
  /**
1332
1419
  * The application's `database` handle. Indexed by table name; each entry is
1333
1420
  * a `Query` over the table's row type AND a `using()` constraint over the
@@ -2799,6 +2886,24 @@ export declare interface ManyToManyRelation {
2799
2886
  readonly targetKey: string;
2800
2887
  }
2801
2888
 
2889
+ /**
2890
+ * A predicate that matches NO row — the "this caller may see nothing" case a
2891
+ * row filter needs and the one it is most expensive to get wrong.
2892
+ *
2893
+ * The natural spelling is `inSet(col, [])`, and in many query builders that is
2894
+ * the dangerous one: an empty `IN ()` gets dropped, and a DROPPED predicate does
2895
+ * not narrow — it WIDENS, to the whole tenant. Here it is safe (the SQL
2896
+ * compiler emits `FALSE`, the in-memory evaluator returns false, and both are
2897
+ * pinned by tests) but an adopting team wrote `eq('id', '')` instead, because
2898
+ * they could not tell from the outside and would not bet a visibility rule on
2899
+ * it. They were right not to.
2900
+ *
2901
+ * So this exists to say the thing rather than encode it: a reviewer reading
2902
+ * `MATCHES_NO_ROWS` sees the intent, where `eq('id', '')` looks like a bug and
2903
+ * `inSet(col, [])` looks like an oversight.
2904
+ */
2905
+ export declare const MATCHES_NO_ROWS: PredicateLeaf;
2906
+
2802
2907
  /**
2803
2908
  * A mixin's materialised, SERIALIZABLE field shape: `FieldDefinitions<Input>`
2804
2909
  * run through `Resolve` so it's emitted as the concrete
@@ -3154,6 +3259,9 @@ export declare const paginateBy: (descriptor: QueryDescriptor, column: string, c
3154
3259
  */
3155
3260
  export declare const paginateById: (descriptor: QueryDescriptor, cursor: string | undefined, limit: number) => QueryDescriptor;
3156
3261
 
3262
+ /** Test seam — how many registrations are outstanding. */
3263
+ export declare const pendingAttributionCount: () => number;
3264
+
3157
3265
  /**
3158
3266
  * Dispatch the provision plan by mechanism — the instant Neon CoW branch-create,
3159
3267
  * or the portable namespace snapshot fan-out. One call site for the cloud
@@ -4039,6 +4147,29 @@ declare interface RawSqlFragment {
4039
4147
  * would truncate. */
4040
4148
  export declare const real: () => ColumnBuilder<number, "real", boolean>;
4041
4149
 
4150
+ /** One write, as the store sees it at the moment it happens. */
4151
+ export declare interface RecordedWrite {
4152
+ readonly table: string;
4153
+ readonly op: 'insert' | 'update' | 'delete';
4154
+ /** Row AFTER the write — present on insert/update, null on delete. */
4155
+ readonly next: Row | null;
4156
+ /** Row BEFORE the write, where the store had it. */
4157
+ readonly prev: Row | null;
4158
+ /** Request identity, when the write had a request behind it. */
4159
+ readonly traceId?: string | undefined;
4160
+ readonly subjectId?: string | null | undefined;
4161
+ }
4162
+
4163
+ /**
4164
+ * Is ANY recorder interested in `table`?
4165
+ *
4166
+ * The stores call this on every write, so it is the hot path: a store with no
4167
+ * recorders registered pays one `Map.size` check and nothing else, and never
4168
+ * awaits. Keeping `routeEvent`'s cost at zero for the default configuration is
4169
+ * what makes shipping this as an opt-in honest.
4170
+ */
4171
+ export declare const recordsTable: (table: string) => boolean;
4172
+
4042
4173
  /**
4043
4174
  * Foreign-key reference to another table's id column.
4044
4175
  *
@@ -4093,6 +4224,15 @@ export declare const registerCoreTables: (options: CoreTableRegistration) => voi
4093
4224
  */
4094
4225
  export declare const registerDiscoveredRelations: (moduleExports: Record<string, unknown>) => number;
4095
4226
 
4227
+ /**
4228
+ * Remember this write's attribution so its transport echo can be re-attributed.
4229
+ *
4230
+ * No-op when there is nothing to remember — a write with no request behind it
4231
+ * must stay unattributed, and registering an empty entry would make the queue
4232
+ * lie about which echo belongs to which write.
4233
+ */
4234
+ export declare const registerPendingAttribution: (key: string, attribution: WriteAttribution) => void;
4235
+
4096
4236
  /**
4097
4237
  * Register a single `relations()` spec. Multiple specs on the same
4098
4238
  * source MERGE — a second spec adds to the first one. A name
@@ -4113,6 +4253,14 @@ export declare const registerRetention: (spec: RetentionSpec) => void;
4113
4253
  */
4114
4254
  export declare const registerTable: (table: TableLike) => void;
4115
4255
 
4256
+ /** Register (or replace) a recorder. Replacing is what a plugin re-activating
4257
+ * on a dev restart needs. */
4258
+ export declare const registerWriteRecorder: (id: string, registration: {
4259
+ readonly recorder: WriteRecorder;
4260
+ readonly tables: Iterable<string>;
4261
+ readonly ownTables: Iterable<string>;
4262
+ }) => void;
4263
+
4116
4264
  export declare type Relation = OneRelation | ManyRelation | ManyToManyRelation;
4117
4265
 
4118
4266
  /**
@@ -4332,6 +4480,23 @@ export declare const rowSchema: <T extends TableLike>(table: T, options?: {
4332
4480
  readonly omit?: ReadonlyArray<string>;
4333
4481
  }) => Schema.Schema.Any;
4334
4482
 
4483
+ /**
4484
+ * Run `fn` with `attribution` active for its whole (sync + awaited) execution.
4485
+ *
4486
+ * Nesting is last-wins by design: a plugin that re-enters the store inside a
4487
+ * handler is still acting under that request.
4488
+ */
4489
+ export declare const runWithWriteAttribution: <T>(attribution: WriteAttribution, fn: () => T) => T;
4490
+
4491
+ /**
4492
+ * Run every recorder that claims `write.table`.
4493
+ *
4494
+ * Deliberately NOT error-swallowing — see the header. Swallowing here would
4495
+ * reproduce the post-commit hole while looking like it had closed it, which is
4496
+ * the worse of the two by a distance.
4497
+ */
4498
+ export declare const runWriteRecorders: (port: TxnRecorderPort, write: RecordedWrite) => Promise<void>;
4499
+
4335
4500
  /**
4336
4501
  * Lower a tenant id to the safe identifier fragment used inside a
4337
4502
  * namespace name. ONLY `[a-z0-9_]` survive; every other character —
@@ -5334,6 +5499,26 @@ export declare const timestampMs: Schema.Schema<Date, number>;
5334
5499
  */
5335
5500
  export declare const timestampMsOrNull: Schema.Schema<Date | null, number | null>;
5336
5501
 
5502
+ /** Append one row inside the caller's transaction. Insert-only by design. */
5503
+ export declare type TxnAppend = (table: string, row: Row) => Promise<void>;
5504
+
5505
+ /**
5506
+ * `MAX(column)` over the rows matching an equality filter, on the caller's
5507
+ * transaction connection. `null` when nothing matches.
5508
+ *
5509
+ * Equality-only and single-aggregate on purpose — see the header. It exists so
5510
+ * an append-only trail can number its own entries; it is not a query facility,
5511
+ * and widening it into one would give back exactly the read-modify-write reach
5512
+ * this seam was shaped to withhold.
5513
+ */
5514
+ export declare type TxnMaxOf = (table: string, column: string, where: Readonly<Record<string, string>>) => Promise<number | null>;
5515
+
5516
+ /** Everything a recorder may do inside the caller's transaction. */
5517
+ export declare interface TxnRecorderPort {
5518
+ readonly append: TxnAppend;
5519
+ readonly maxOf: TxnMaxOf;
5520
+ }
5521
+
5337
5522
  /** The minimal store surface `insertRow` needs — satisfied by `ctx.store`. */
5338
5523
  export declare interface TypedInsertStore {
5339
5524
  readonly insert: (table: string, row: Row) => Promise<Row>;
@@ -5387,6 +5572,8 @@ export declare interface UniqueSpec {
5387
5572
  readonly dedup?: 'fail' | 'suffix-counter' | Statement.Fragment;
5388
5573
  }
5389
5574
 
5575
+ export declare const unregisterWriteRecorder: (id: string) => void;
5576
+
5390
5577
  /**
5391
5578
  * Bulk-update rows matching `where`, typed against the table: the patch is a
5392
5579
  * `Partial<InferRow<T>>`, so a misspelled column or a value of the wrong type
@@ -5733,4 +5920,22 @@ export declare interface WindowSpec {
5733
5920
  /** Tree of relations to eager-load. */
5734
5921
  export declare type WithSpec = Readonly<Record<string, true | EagerLoadSpec>>;
5735
5922
 
5923
+ /**
5924
+ * Who is writing, and under which call.
5925
+ *
5926
+ * `subjectId` is the ACTING identity — the same one the `audit()` mixin stamps
5927
+ * into `createdBy`/`updatedBy`. For an API key that is the person behind the
5928
+ * credential, not the credential: two answers to "who wrote this row" on one
5929
+ * write would be worse than one. Which KEY was used is recoverable from the
5930
+ * audit sink row sharing this `traceId` — that is what the bridge is for.
5931
+ */
5932
+ export declare interface WriteAttribution {
5933
+ readonly traceId?: string;
5934
+ /** `null` is meaningful: a resolved subject with no acting user (an
5935
+ * anonymous or system call), as distinct from `undefined` = no request. */
5936
+ readonly subjectId?: string | null;
5937
+ }
5938
+
5939
+ export declare type WriteRecorder = (port: TxnRecorderPort, write: RecordedWrite) => Promise<void>;
5940
+
5736
5941
  export { }