@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/CHANGELOG.md +423 -0
- package/dist/index.d.ts +183 -5
- package/dist/index.js +424 -386
- package/dist/sql.d.ts +3 -8
- package/dist/sql.js +221 -221
- package/package.json +2 -2
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
|
|
568
|
-
*
|
|
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
|
|
1009
|
-
*
|
|
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 { }
|