@voltro/database 0.17.0 → 0.19.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,24 @@ 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
+
162
180
  /**
163
181
  * Audit a list of tables in one call. Convenience for the CLI's boot
164
182
  * path; same shape as calling `auditTableIndexes` per table and
@@ -464,6 +482,32 @@ export declare type ChangeEvent = {
464
482
  * so the consumer must elect a single worker instead.
465
483
  */
466
484
  readonly origin?: 'inline' | 'injected';
485
+ /**
486
+ * The trace this write happened under — the SAME id `auditPlugin` records as
487
+ * `AuditEvent.traceId` and `voltro logs --trace` resolves.
488
+ *
489
+ * This is the correlation bridge. Without it `plugin-versioning` knows WHAT
490
+ * changed and the audit sink knows WHO called and whether it was refused, and
491
+ * nothing joins the two. With it, "what did this call touch" is one query.
492
+ *
493
+ * Absent means the write had no request behind it: a seed, a `*.startup.tsx`,
494
+ * a schedule, a workflow step — or an event injected from another replica.
495
+ * For `origin: 'injected'` the honest answer is the ORIGINATING replica's
496
+ * trace or nothing at all; stamping the receiving replica's ambient trace
497
+ * would attribute a remote write to a local request, which is worse than
498
+ * silence.
499
+ */
500
+ readonly traceId?: string;
501
+ /**
502
+ * The acting identity behind the write — the same one `audit()` stamps into
503
+ * `createdBy`/`updatedBy`, so a row's stamp and its change event never
504
+ * disagree. For an API-key subject that is the person, not the credential;
505
+ * which key was used is recoverable from the audit row sharing `traceId`.
506
+ *
507
+ * `null` = a resolved subject with no acting user (anonymous / system).
508
+ * `undefined` = no request context at all. The two are different facts.
509
+ */
510
+ readonly subjectId?: string | null;
467
511
  };
468
512
 
469
513
  /**
@@ -492,6 +536,9 @@ export declare const clearRetentions: () => void;
492
536
  /** Wipe — used by tests + the dev-loop hot-reload. */
493
537
  export declare const clearTableRegistry: () => void;
494
538
 
539
+ /** Test seam / dev-restart seam. */
540
+ export declare const clearWriteRecorders: () => void;
541
+
495
542
  /**
496
543
  * The value to bind for `column`, given what the table declares it as.
497
544
  * Returns `value` unchanged whenever nothing unambiguous applies — including
@@ -564,8 +611,25 @@ export declare class ColumnBuilder<TsType, Type extends ColumnType, HasDefault e
564
611
  * keyHash: text().serverOnly(),
565
612
  *
566
613
  * 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.
614
+ * the client. The `crud.*` read helpers strip it from every returned row.
615
+ *
616
+ * A HAND-WRITTEN query output is the case `crud.*` cannot cover, so an audit
617
+ * checks it: a wire-reachable query whose `source` table carries a serverOnly
618
+ * column that its output DECLARES. What that costs depends on the command,
619
+ * and the difference is deliberate — a refused boot mid-edit is worse than
620
+ * the bug; in production it is not:
621
+ *
622
+ * | command | on a leak |
623
+ * |--------------------------------|------------------------------------|
624
+ * | `voltro serve` | **boot fails** |
625
+ * | `voltro doctor` | **exits non-zero** |
626
+ * | `voltro dev` | warns |
627
+ *
628
+ * `VOLTRO_SERVER_ONLY=strict` makes `voltro dev` fail too; `=warn` downgrades
629
+ * serve; `=off` silences it. (`voltro check` does NOT run this — it has a
630
+ * live-api mode with no access to your table definitions, and a rule that
631
+ * fires in one of its two modes would be worse than one that fires in
632
+ * neither.)
569
633
  */
570
634
  serverOnly(): this;
571
635
  /**
@@ -1005,8 +1069,9 @@ export declare interface ColumnDefinition<TsType, Type extends ColumnType = Colu
1005
1069
  * but not encrypted; a private note the owner may read is encrypted but not
1006
1070
  * serverOnly. The default is exposed to BOTH server and client; `.serverOnly()`
1007
1071
  * 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.
1072
+ * it automatically, and an audit flags a wire-reachable query whose `source`
1073
+ * table carries a serverOnly column that its output declares. What that audit
1074
+ * COSTS is per command — see `serverOnly()` below for the matrix.
1010
1075
  */
1011
1076
  readonly serverOnly?: boolean;
1012
1077
  /**
@@ -1328,6 +1393,9 @@ export declare const count: (alias?: string) => AggregateColumn;
1328
1393
 
1329
1394
  export declare const countDistinct: (column: string, alias?: string) => AggregateColumn;
1330
1395
 
1396
+ /** The attribution on the current async stack, or `undefined` outside a request. */
1397
+ export declare const currentWriteAttribution: () => WriteAttribution | undefined;
1398
+
1331
1399
  /**
1332
1400
  * The application's `database` handle. Indexed by table name; each entry is
1333
1401
  * a `Query` over the table's row type AND a `using()` constraint over the
@@ -2651,6 +2719,7 @@ export declare interface JsonFieldFilter {
2651
2719
  readonly inSet: (values: ReadonlyArray<unknown>) => PredicateLeaf;
2652
2720
  readonly notInSet: (values: ReadonlyArray<unknown>) => PredicateLeaf;
2653
2721
  readonly contains: (substring: string) => PredicateLeaf;
2722
+ readonly startsWith: (prefix: string) => PredicateLeaf;
2654
2723
  readonly isNull: () => PredicateLeaf;
2655
2724
  readonly isNotNull: () => PredicateLeaf;
2656
2725
  }
@@ -2727,7 +2796,7 @@ export declare const lag: (column: string, offset?: number, alias?: string) => W
2727
2796
 
2728
2797
  export declare const lead: (column: string, offset?: number, alias?: string) => WindowBuilder;
2729
2798
 
2730
- export declare type LeafOperator = 'eq' | 'neq' | 'gt' | 'gte' | 'lt' | 'lte' | 'in' | 'notIn' | 'contains' | 'arrayContains' | 'arrayOverlaps' | 'arrayHas' | 'isNull' | 'isNotNull' | 'spatial';
2799
+ export declare type LeafOperator = 'eq' | 'neq' | 'gt' | 'gte' | 'lt' | 'lte' | 'in' | 'notIn' | 'contains' | 'startsWith' | 'arrayContains' | 'arrayOverlaps' | 'arrayHas' | 'isNull' | 'isNotNull' | 'spatial';
2731
2800
 
2732
2801
  /** Every currently-registered retention spec. The boot sweep reads this each tick. */
2733
2802
  export declare const listRetentions: () => ReadonlyArray<RetentionSpec>;
@@ -2798,6 +2867,24 @@ export declare interface ManyToManyRelation {
2798
2867
  readonly targetKey: string;
2799
2868
  }
2800
2869
 
2870
+ /**
2871
+ * A predicate that matches NO row — the "this caller may see nothing" case a
2872
+ * row filter needs and the one it is most expensive to get wrong.
2873
+ *
2874
+ * The natural spelling is `inSet(col, [])`, and in many query builders that is
2875
+ * the dangerous one: an empty `IN ()` gets dropped, and a DROPPED predicate does
2876
+ * not narrow — it WIDENS, to the whole tenant. Here it is safe (the SQL
2877
+ * compiler emits `FALSE`, the in-memory evaluator returns false, and both are
2878
+ * pinned by tests) but an adopting team wrote `eq('id', '')` instead, because
2879
+ * they could not tell from the outside and would not bet a visibility rule on
2880
+ * it. They were right not to.
2881
+ *
2882
+ * So this exists to say the thing rather than encode it: a reviewer reading
2883
+ * `MATCHES_NO_ROWS` sees the intent, where `eq('id', '')` looks like a bug and
2884
+ * `inSet(col, [])` looks like an oversight.
2885
+ */
2886
+ export declare const MATCHES_NO_ROWS: PredicateLeaf;
2887
+
2801
2888
  /**
2802
2889
  * A mixin's materialised, SERIALIZABLE field shape: `FieldDefinitions<Input>`
2803
2890
  * run through `Resolve` so it's emitted as the concrete
@@ -4038,6 +4125,29 @@ declare interface RawSqlFragment {
4038
4125
  * would truncate. */
4039
4126
  export declare const real: () => ColumnBuilder<number, "real", boolean>;
4040
4127
 
4128
+ /** One write, as the store sees it at the moment it happens. */
4129
+ export declare interface RecordedWrite {
4130
+ readonly table: string;
4131
+ readonly op: 'insert' | 'update' | 'delete';
4132
+ /** Row AFTER the write — present on insert/update, null on delete. */
4133
+ readonly next: Row | null;
4134
+ /** Row BEFORE the write, where the store had it. */
4135
+ readonly prev: Row | null;
4136
+ /** Request identity, when the write had a request behind it. */
4137
+ readonly traceId?: string | undefined;
4138
+ readonly subjectId?: string | null | undefined;
4139
+ }
4140
+
4141
+ /**
4142
+ * Is ANY recorder interested in `table`?
4143
+ *
4144
+ * The stores call this on every write, so it is the hot path: a store with no
4145
+ * recorders registered pays one `Map.size` check and nothing else, and never
4146
+ * awaits. Keeping `routeEvent`'s cost at zero for the default configuration is
4147
+ * what makes shipping this as an opt-in honest.
4148
+ */
4149
+ export declare const recordsTable: (table: string) => boolean;
4150
+
4041
4151
  /**
4042
4152
  * Foreign-key reference to another table's id column.
4043
4153
  *
@@ -4112,6 +4222,14 @@ export declare const registerRetention: (spec: RetentionSpec) => void;
4112
4222
  */
4113
4223
  export declare const registerTable: (table: TableLike) => void;
4114
4224
 
4225
+ /** Register (or replace) a recorder. Replacing is what a plugin re-activating
4226
+ * on a dev restart needs. */
4227
+ export declare const registerWriteRecorder: (id: string, registration: {
4228
+ readonly recorder: WriteRecorder;
4229
+ readonly tables: Iterable<string>;
4230
+ readonly ownTables: Iterable<string>;
4231
+ }) => void;
4232
+
4115
4233
  export declare type Relation = OneRelation | ManyRelation | ManyToManyRelation;
4116
4234
 
4117
4235
  /**
@@ -4331,6 +4449,23 @@ export declare const rowSchema: <T extends TableLike>(table: T, options?: {
4331
4449
  readonly omit?: ReadonlyArray<string>;
4332
4450
  }) => Schema.Schema.Any;
4333
4451
 
4452
+ /**
4453
+ * Run `fn` with `attribution` active for its whole (sync + awaited) execution.
4454
+ *
4455
+ * Nesting is last-wins by design: a plugin that re-enters the store inside a
4456
+ * handler is still acting under that request.
4457
+ */
4458
+ export declare const runWithWriteAttribution: <T>(attribution: WriteAttribution, fn: () => T) => T;
4459
+
4460
+ /**
4461
+ * Run every recorder that claims `write.table`.
4462
+ *
4463
+ * Deliberately NOT error-swallowing — see the header. Swallowing here would
4464
+ * reproduce the post-commit hole while looking like it had closed it, which is
4465
+ * the worse of the two by a distance.
4466
+ */
4467
+ export declare const runWriteRecorders: (append: TxnAppend, write: RecordedWrite) => Promise<void>;
4468
+
4334
4469
  /**
4335
4470
  * Lower a tenant id to the safe identifier fragment used inside a
4336
4471
  * namespace name. ONLY `[a-z0-9_]` survive; every other character —
@@ -4643,6 +4778,26 @@ export declare const stampGeneratedId: <T extends Record<string, unknown>>(table
4643
4778
  /** `stampGeneratedId` over a batch. */
4644
4779
  export declare const stampGeneratedIds: <T extends Record<string, unknown>>(table: string, rows: ReadonlyArray<T>) => ReadonlyArray<T>;
4645
4780
 
4781
+ /**
4782
+ * Prefix match on a text column — `startsWith('key', 'awb_')`.
4783
+ *
4784
+ * **Case-SENSITIVE**, unlike {@link contains}, and the asymmetry is deliberate
4785
+ * rather than an oversight. `contains` is a search primitive (a human typing
4786
+ * into a box), so folding case is what they mean. A prefix is a NAMESPACE — a
4787
+ * key prefix, a path segment, a tenant-scoped id — where `awb_` and `AWB_` are
4788
+ * two different namespaces and quietly merging them is a bug. It also matches
4789
+ * the semantics of the JS method it is named after.
4790
+ *
4791
+ * It is the one string predicate a database can answer from an index: it lowers
4792
+ * to `LIKE 'literal%'`, which a btree on the column can range-scan. `contains`
4793
+ * (`%…%`) and a suffix match cannot, which is why there is no `endsWith` —
4794
+ * `contains` already covers the un-indexable case and a second un-indexable
4795
+ * operator would only look cheaper than it is.
4796
+ *
4797
+ * `%` and `_` in the argument are escaped, so the value is matched literally.
4798
+ */
4799
+ export declare const startsWith: <RowOf = Record<string, unknown>, K extends keyof RowOf & string = keyof RowOf & string>(column: K, prefix: string) => PredicateLeaf;
4800
+
4646
4801
  /**
4647
4802
  * Stream every row of a table as a lazy `Stream<Row>`, keyset-paginated so
4648
4803
  * memory stays flat regardless of table size. Backpressure is inherent: a
@@ -5313,6 +5468,9 @@ export declare const timestampMs: Schema.Schema<Date, number>;
5313
5468
  */
5314
5469
  export declare const timestampMsOrNull: Schema.Schema<Date | null, number | null>;
5315
5470
 
5471
+ /** Append one row inside the caller's transaction. Insert-only by design. */
5472
+ export declare type TxnAppend = (table: string, row: Row) => Promise<void>;
5473
+
5316
5474
  /** The minimal store surface `insertRow` needs — satisfied by `ctx.store`. */
5317
5475
  export declare interface TypedInsertStore {
5318
5476
  readonly insert: (table: string, row: Row) => Promise<Row>;
@@ -5366,6 +5524,8 @@ export declare interface UniqueSpec {
5366
5524
  readonly dedup?: 'fail' | 'suffix-counter' | Statement.Fragment;
5367
5525
  }
5368
5526
 
5527
+ export declare const unregisterWriteRecorder: (id: string) => void;
5528
+
5369
5529
  /**
5370
5530
  * Bulk-update rows matching `where`, typed against the table: the patch is a
5371
5531
  * `Partial<InferRow<T>>`, so a misspelled column or a value of the wrong type
@@ -5712,4 +5872,22 @@ export declare interface WindowSpec {
5712
5872
  /** Tree of relations to eager-load. */
5713
5873
  export declare type WithSpec = Readonly<Record<string, true | EagerLoadSpec>>;
5714
5874
 
5875
+ /**
5876
+ * Who is writing, and under which call.
5877
+ *
5878
+ * `subjectId` is the ACTING identity — the same one the `audit()` mixin stamps
5879
+ * into `createdBy`/`updatedBy`. For an API key that is the person behind the
5880
+ * credential, not the credential: two answers to "who wrote this row" on one
5881
+ * write would be worse than one. Which KEY was used is recoverable from the
5882
+ * audit sink row sharing this `traceId` — that is what the bridge is for.
5883
+ */
5884
+ export declare interface WriteAttribution {
5885
+ readonly traceId?: string;
5886
+ /** `null` is meaningful: a resolved subject with no acting user (an
5887
+ * anonymous or system call), as distinct from `undefined` = no request. */
5888
+ readonly subjectId?: string | null;
5889
+ }
5890
+
5891
+ export declare type WriteRecorder = (append: TxnAppend, write: RecordedWrite) => Promise<void>;
5892
+
5715
5893
  export { }