@voltro/database 0.23.0 → 0.25.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 +537 -0
- package/dist/{frameworkLiveTables-BvPIm2cD.js → frameworkLiveTables-BQwB8K6R.js} +9 -0
- package/dist/index.d.ts +178 -0
- package/dist/index.js +457 -426
- package/dist/sql.d.ts +143 -6
- package/dist/sql.js +600 -497
- package/package.json +2 -2
|
@@ -32,6 +32,15 @@ var r = (e) => {
|
|
|
32
32
|
serverOnly: !0
|
|
33
33
|
});
|
|
34
34
|
}
|
|
35
|
+
version() {
|
|
36
|
+
if (this.rejectIfRaw("version"), this.definition.type !== "integer") throw Error(`.version() requires an integer() column (got ${this.definition.type}). The store increments it, so it has to be a number — and a timestamp version reintroduces exactly the clock problem optimistic locking exists to avoid: two writes in the same millisecond are indistinguishable, and replica clocks disagree.`);
|
|
37
|
+
return new e({
|
|
38
|
+
...this.definition,
|
|
39
|
+
versionColumn: !0,
|
|
40
|
+
hasDefault: !0,
|
|
41
|
+
defaultValue: 1
|
|
42
|
+
});
|
|
43
|
+
}
|
|
35
44
|
sensitive(t) {
|
|
36
45
|
return new e({
|
|
37
46
|
...this.definition,
|
package/dist/index.d.ts
CHANGED
|
@@ -670,6 +670,34 @@ export declare class ColumnBuilder<TsType, Type extends ColumnType, HasDefault e
|
|
|
670
670
|
* neither.)
|
|
671
671
|
*/
|
|
672
672
|
serverOnly(): this;
|
|
673
|
+
/**
|
|
674
|
+
* Mark this column as the row's OPTIMISTIC-CONCURRENCY version.
|
|
675
|
+
*
|
|
676
|
+
* ```ts
|
|
677
|
+
* table('documents', { id: id(), title: text(), version: integer().version() })
|
|
678
|
+
* ```
|
|
679
|
+
*
|
|
680
|
+
* From then on the store increments it on every update, and an update that
|
|
681
|
+
* carries `expectedVersion` fails with a typed `VersionConflict` when the row
|
|
682
|
+
* has moved on. That is the whole feature, and the reason it is a COLUMN
|
|
683
|
+
* marker rather than a table mixin is that only the author knows which column
|
|
684
|
+
* (if any) their schema already uses for this — several apps arrive with one.
|
|
685
|
+
*
|
|
686
|
+
* WHAT IT REPLACES. Nothing, which is the point: today the second of two
|
|
687
|
+
* concurrent writers wins silently. `updatedAt` is the obvious substitute and
|
|
688
|
+
* it does not work — same-millisecond writes are indistinguishable and replica
|
|
689
|
+
* clocks disagree, so a comparison that looks correct in a test loses rows
|
|
690
|
+
* under load. An integer the DATABASE owns has neither problem.
|
|
691
|
+
*
|
|
692
|
+
* WHAT IT DOES NOT DO. It is not a history: it records that a row changed, not
|
|
693
|
+
* what it changed to. For that use `plugin-versioning`. It is also not a
|
|
694
|
+
* distributed lock — a conflicting write is REPORTED, never queued or merged,
|
|
695
|
+
* because merging two intents is a decision only the application can make.
|
|
696
|
+
*
|
|
697
|
+
* Must be an `integer()`. A `text()` version cannot be incremented and a
|
|
698
|
+
* `timestamp()` one reintroduces the clock, so both fail at declaration.
|
|
699
|
+
*/
|
|
700
|
+
version(): this;
|
|
673
701
|
/**
|
|
674
702
|
* Classify this column as SENSITIVE — it holds personal / sensitive data of
|
|
675
703
|
* `class` (`'email'`, `'fullName'`, `'phone'`, `'address'`, `'secret'`, …).
|
|
@@ -1112,6 +1140,23 @@ export declare interface ColumnDefinition<TsType, Type extends ColumnType = Colu
|
|
|
1112
1140
|
* COSTS is per command — see `serverOnly()` below for the matrix.
|
|
1113
1141
|
*/
|
|
1114
1142
|
readonly serverOnly?: boolean;
|
|
1143
|
+
/**
|
|
1144
|
+
* Optimistic-concurrency marker, set by `.version()`.
|
|
1145
|
+
*
|
|
1146
|
+
* The column the store INCREMENTS on every update, and the one an update may
|
|
1147
|
+
* carry an expectation of. Two clients that read the same row and both write
|
|
1148
|
+
* it are the normal case, not an exotic one — and without this the second
|
|
1149
|
+
* write silently wins, which is the shape of every "my change disappeared"
|
|
1150
|
+
* report ever filed.
|
|
1151
|
+
*
|
|
1152
|
+
* A TIMESTAMP cannot do this job. Two writes in the same millisecond are
|
|
1153
|
+
* indistinguishable, and across replicas the clocks disagree; this repo has
|
|
1154
|
+
* already lost rows to exactly that (an analytics sink dropped 7 of 40 events
|
|
1155
|
+
* written in the same millisecond as the query bounding them). A monotonic
|
|
1156
|
+
* integer has neither problem, and it also answers the other half of the
|
|
1157
|
+
* question — which of two copies is NEWER — without consulting a clock.
|
|
1158
|
+
*/
|
|
1159
|
+
readonly versionColumn?: boolean;
|
|
1115
1160
|
/**
|
|
1116
1161
|
* Data-sensitivity classification, set by `.sensitive(class)`. Declares that
|
|
1117
1162
|
* this column holds personal / sensitive data of a given CLASS (`'email'`,
|
|
@@ -1404,6 +1449,31 @@ export declare interface ConnectionConfig {
|
|
|
1404
1449
|
*/
|
|
1405
1450
|
readonly ssl?: boolean;
|
|
1406
1451
|
readonly maxConnections?: number;
|
|
1452
|
+
/**
|
|
1453
|
+
* Per-statement timeout in ms — a runaway query (missing index, cartesian
|
|
1454
|
+
* join) is aborted instead of holding a pooled connection forever, which under
|
|
1455
|
+
* load exhausts the pool and stalls the whole app. Applied at the CONNECTION
|
|
1456
|
+
* level so every query is bounded (no per-call opt-in). Env `DB_STATEMENT_TIMEOUT_MS`.
|
|
1457
|
+
* Runtime-path only — the migration path (`voltro db apply`) deliberately does
|
|
1458
|
+
* NOT read this, so a long backfill / index build is never cancelled by the app
|
|
1459
|
+
* query ceiling.
|
|
1460
|
+
*
|
|
1461
|
+
* **Wired for postgres only** (the default dialect), enforced server-side via
|
|
1462
|
+
* node-postgres' `statement_timeout` pool option — a runaway query is cancelled
|
|
1463
|
+
* with SQLSTATE 57014, not merely disconnected. Every OTHER dialect currently
|
|
1464
|
+
* ACCEPTS the field but IGNORES it, and the honest reasons differ:
|
|
1465
|
+
* - **mssql** — `@effect/sql-mssql` exposes only `connectTimeout` (connection
|
|
1466
|
+
* establishment), not tedious's per-request `requestTimeout`; mapping this
|
|
1467
|
+
* onto `connectTimeout` would bound the wrong phase, so it is left unwired
|
|
1468
|
+
* rather than wrong.
|
|
1469
|
+
* - **mysql/mariadb** — the server's `max_execution_time` bounds SELECTs only
|
|
1470
|
+
* (writes run unbounded), so a connection-level setting would be a partial,
|
|
1471
|
+
* misleading guarantee; not wired in V1.
|
|
1472
|
+
* - **sqlite** — in-process, one connection, no pool to exhaust; nothing to
|
|
1473
|
+
* protect.
|
|
1474
|
+
* Unset (or any non-postgres dialect) = no timeout — behaviour is unchanged.
|
|
1475
|
+
*/
|
|
1476
|
+
readonly statementTimeoutMs?: number;
|
|
1407
1477
|
/**
|
|
1408
1478
|
* Postgres only: the schema the app's tables live in, pinned as the
|
|
1409
1479
|
* connection `search_path` (env `DB_SCHEMA`). When set, EVERY pooled
|
|
@@ -2190,6 +2260,35 @@ export declare interface ExistsPredicate {
|
|
|
2190
2260
|
readonly subquery: QueryDescriptor;
|
|
2191
2261
|
}
|
|
2192
2262
|
|
|
2263
|
+
/**
|
|
2264
|
+
* Add `expiresAt` — after that instant the row is invisible to reads.
|
|
2265
|
+
*
|
|
2266
|
+
* ```ts
|
|
2267
|
+
* table('inviteLinks', { id: id(), email: text() }).with(expires())
|
|
2268
|
+
*
|
|
2269
|
+
* await ctx.store.insert('inviteLinks', {
|
|
2270
|
+
* email, expiresAt: new Date(Date.now() + 24 * 3_600_000),
|
|
2271
|
+
* })
|
|
2272
|
+
* ```
|
|
2273
|
+
*
|
|
2274
|
+
* `expiresAt` is NULLABLE and null means never — so adding the mixin to an
|
|
2275
|
+
* existing table does not make its rows vanish, and a row that should outlive
|
|
2276
|
+
* the others simply leaves it unset. A non-null default would have been the
|
|
2277
|
+
* tidier API and the wrong one: it would turn "I added a mixin" into "I set an
|
|
2278
|
+
* expiry on every existing row".
|
|
2279
|
+
*
|
|
2280
|
+
* Reads filter it out on EVERY dialect from the moment it passes; the physical
|
|
2281
|
+
* delete is a postgres-only sweep. Read the header before relying on the second
|
|
2282
|
+
* half — an expired row is INVISIBLE everywhere and still PRESENT on four of
|
|
2283
|
+
* five dialects, which is the right trade and a surprising one.
|
|
2284
|
+
*
|
|
2285
|
+
* Opt out for a deliberate read of expired rows the same way soft-delete does:
|
|
2286
|
+
* `ctx.store.select('inviteLinks').includeExpired()`.
|
|
2287
|
+
*/
|
|
2288
|
+
export declare const expires: () => MixinDefinition<{
|
|
2289
|
+
readonly expiresAt: ColumnDefinition<Date | null, "timestamp", boolean>;
|
|
2290
|
+
}>;
|
|
2291
|
+
|
|
2193
2292
|
/** The cipher the store middleware injects. Operates on opaque strings. */
|
|
2194
2293
|
export declare interface FieldCipher {
|
|
2195
2294
|
readonly encrypt: (plaintext: string) => string;
|
|
@@ -2639,6 +2738,16 @@ export declare type InferRowFromFields<F extends Record<string, ColumnDefinition
|
|
|
2639
2738
|
/** Row type for a view — mirrors `InferRow` for tables. */
|
|
2640
2739
|
export declare type InferViewRow<V> = V extends View<string, infer F> ? InferRowFromFields<F> : never;
|
|
2641
2740
|
|
|
2741
|
+
/**
|
|
2742
|
+
* The version a freshly-inserted row starts at.
|
|
2743
|
+
*
|
|
2744
|
+
* 1 rather than 0 so that "has this row ever been written?" and "what version is
|
|
2745
|
+
* it?" are never the same question — a 0 reads as absent in too many places
|
|
2746
|
+
* (`if (row.version)`), and a version that is falsy on a real row is a trap
|
|
2747
|
+
* waiting for the first person who writes that check.
|
|
2748
|
+
*/
|
|
2749
|
+
export declare const INITIAL_VERSION = 1;
|
|
2750
|
+
|
|
2642
2751
|
/**
|
|
2643
2752
|
* Insert a row, typed against the table: the payload must carry every required
|
|
2644
2753
|
* column (NOT NULL, no default, not auto-filled) or it is a compile error.
|
|
@@ -3348,6 +3457,26 @@ export declare const planNeonBranchProvision: (branchId: string, options?: NeonB
|
|
|
3348
3457
|
* never delete a non-branch; idempotent on the executor side. */
|
|
3349
3458
|
export declare const planNeonBranchTeardown: (branchId: string) => ReadonlyArray<BranchStep>;
|
|
3350
3459
|
|
|
3460
|
+
/**
|
|
3461
|
+
* The version an UPDATE must write, and the guard it must carry.
|
|
3462
|
+
*
|
|
3463
|
+
* Returns `undefined` when the table has no version column, so a store's fast
|
|
3464
|
+
* path is a single check and an app that does not use the feature pays nothing.
|
|
3465
|
+
*
|
|
3466
|
+
* `expected` is read from the PATCH rather than passed separately, because the
|
|
3467
|
+
* alternative — a second argument on every store method — is a change every
|
|
3468
|
+
* caller has to make and most would skip. Putting it in the patch means the
|
|
3469
|
+
* caller who cares opts in at the one call site that cares.
|
|
3470
|
+
*/
|
|
3471
|
+
export declare const planVersionedUpdate: (input: {
|
|
3472
|
+
readonly versionColumn: string | undefined;
|
|
3473
|
+
readonly patch: Readonly<Record<string, unknown>>;
|
|
3474
|
+
}) => {
|
|
3475
|
+
readonly nextVersion: number | undefined;
|
|
3476
|
+
readonly expected: number | undefined;
|
|
3477
|
+
readonly patch: Record<string, unknown>;
|
|
3478
|
+
};
|
|
3479
|
+
|
|
3351
3480
|
export declare type Predicate = PredicateLeaf | AndPredicate | OrPredicate | NotPredicate | SubqueryInPredicate | ExistsPredicate;
|
|
3352
3481
|
|
|
3353
3482
|
export declare interface PredicateLeaf {
|
|
@@ -3824,6 +3953,10 @@ export declare interface QueryDescriptor<R = Row> {
|
|
|
3824
3953
|
* (storeMiddleware read path + reactive-query finalize) honors.
|
|
3825
3954
|
*/
|
|
3826
3955
|
readonly includeDeleted?: boolean;
|
|
3956
|
+
/** Opt OUT of the `expires()` read filter — a deliberate read of expired
|
|
3957
|
+
* rows (an admin view, a grace-period check). Same shape and same posture as
|
|
3958
|
+
* `includeDeleted`. */
|
|
3959
|
+
readonly includeExpired?: boolean;
|
|
3827
3960
|
/**
|
|
3828
3961
|
* Opt OUT of the runtime's automatic tenant scope on `tenant()` tables.
|
|
3829
3962
|
* Set by `.unscoped()`. Honored by the runtime, same as `includeDeleted`.
|
|
@@ -4785,6 +4918,20 @@ export declare const setResidencyConfig: (config: ResidencyConfig) => ResidencyC
|
|
|
4785
4918
|
*/
|
|
4786
4919
|
export declare const settleTransactionExit: <A>(exit: Exit.Exit<A, unknown>) => A;
|
|
4787
4920
|
|
|
4921
|
+
/**
|
|
4922
|
+
* Decide the outcome of a versioned update from what the row actually held.
|
|
4923
|
+
*
|
|
4924
|
+
* Separated from the SQL so every store shares the DECISION and only the
|
|
4925
|
+
* statement differs. `current` is the row's version before the write, or `null`
|
|
4926
|
+
* when the row is gone.
|
|
4927
|
+
*/
|
|
4928
|
+
export declare const settleVersionedUpdate: (input: {
|
|
4929
|
+
readonly table: string;
|
|
4930
|
+
readonly id: string;
|
|
4931
|
+
readonly expected: number | undefined;
|
|
4932
|
+
readonly current: number | null;
|
|
4933
|
+
}) => VersionConflict | undefined;
|
|
4934
|
+
|
|
4788
4935
|
/**
|
|
4789
4936
|
* PostGIS spatial-distance clause carried on a {@link QueryDescriptor}.
|
|
4790
4937
|
* Drives a `ST_Distance` projected column (distance-as-a-value) and/or a
|
|
@@ -5898,6 +6045,34 @@ export declare interface VectorOptions {
|
|
|
5898
6045
|
readonly precision?: 'float32' | 'half';
|
|
5899
6046
|
}
|
|
5900
6047
|
|
|
6048
|
+
/** The column marked `.version()`, or `undefined` when a table has none. */
|
|
6049
|
+
export declare const versionColumnOf: (columns: Readonly<Record<string, {
|
|
6050
|
+
readonly definition?: {
|
|
6051
|
+
readonly versionColumn?: boolean;
|
|
6052
|
+
};
|
|
6053
|
+
}>>) => string | undefined;
|
|
6054
|
+
|
|
6055
|
+
/**
|
|
6056
|
+
* A write lost the race.
|
|
6057
|
+
*
|
|
6058
|
+
* Carries both numbers because "someone else changed it" is not actionable and
|
|
6059
|
+
* "you had 3, it is now 7" is: four intervening writes is a different situation
|
|
6060
|
+
* from one, and a UI can say so.
|
|
6061
|
+
*/
|
|
6062
|
+
export declare class VersionConflict extends VersionConflict_base {
|
|
6063
|
+
}
|
|
6064
|
+
|
|
6065
|
+
declare const VersionConflict_base: Schema.TaggedErrorClass<VersionConflict, "VersionConflict", {
|
|
6066
|
+
readonly _tag: Schema.tag<"VersionConflict">;
|
|
6067
|
+
} & {
|
|
6068
|
+
table: typeof Schema.String;
|
|
6069
|
+
id: typeof Schema.String;
|
|
6070
|
+
/** What the caller believed the row was at. */
|
|
6071
|
+
expected: typeof Schema.Number;
|
|
6072
|
+
/** What it is actually at. `null` when the row is gone entirely. */
|
|
6073
|
+
actual: Schema.NullOr<typeof Schema.Number>;
|
|
6074
|
+
}>;
|
|
6075
|
+
|
|
5901
6076
|
/**
|
|
5902
6077
|
* A read-only SQL view descriptor. Structurally a `TableLike` (carries
|
|
5903
6078
|
* `tableName` + `fields`) so the query builder + row decoder + by-name
|
|
@@ -5945,6 +6120,9 @@ export declare const view: <const Name extends string, const Input extends Field
|
|
|
5945
6120
|
*/
|
|
5946
6121
|
export declare const viewSql: (v: View<string, Record<string, ColumnDefinition<unknown>>>, dialect: DialectId, schema?: string | null) => string;
|
|
5947
6122
|
|
|
6123
|
+
/** Stable mixin id — the runtime keys read-filtering off it. */
|
|
6124
|
+
export declare const VOLTRO_EXPIRES_MIXIN_ID = "voltro/expires";
|
|
6125
|
+
|
|
5948
6126
|
/**
|
|
5949
6127
|
* `_voltro_api_keys` — first-class API key management. Only the SHA-256 HASH
|
|
5950
6128
|
* of a key is stored (`hashedKey`, unique); the raw token is shown once at
|