@voltro/database 0.11.0 → 0.11.1
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 +50 -0
- package/dist/index.d.ts +140 -20
- package/dist/index.js +540 -497
- package/dist/sql.d.ts +2 -1
- package/package.json +2 -2
package/CHANGELOG.md
CHANGED
|
@@ -39,6 +39,56 @@ _Changes staged for the next release accumulate here (rolled up from
|
|
|
39
39
|
|
|
40
40
|
---
|
|
41
41
|
|
|
42
|
+
## [0.11.1] — 2026-07-23
|
|
43
|
+
|
|
44
|
+
### Added
|
|
45
|
+
|
|
46
|
+
- **@voltro/runtime** — `defineExecutor(descriptor, fn)` type-checks a query/mutation/action handler's return against the descriptor's `output` schema Type — so returning a `number` where `output` is `timestampMs` (Type = `Date`) is a COMPILE error at the handler, not a runtime encode failure that Dies the subscription. The gap it closes: the executor is a separate default export whose return was never tied to `output`, so a handler that builds a plain object with a leftover `.getTime()` compiled green and only threw `Expected DateFromSelf, actual 1784…` at encode time — invisible while a nullable date was null, exploding the instant it became non-null.
|
|
47
|
+
|
|
48
|
+
Opt-in and zero-cost: it's a runtime identity (returns `fn` unchanged, so the codegen wires it exactly as the bare default export), and the compile check is the whole value. Wrap the handler and import the descriptor into its `*.server.ts`:
|
|
49
|
+
|
|
50
|
+
```ts
|
|
51
|
+
export default defineExecutor(getRoadmapsByYear, (input, ctx) => …)
|
|
52
|
+
```
|
|
53
|
+
|
|
54
|
+
The Effect error and requirement channels stay inferred from the handler; only the success value is constrained. A reactive query that returns a `{ descriptor }` builder is allowed through unchecked — the store produces its rows, so a value-level return type can't express that row-vs-output check.
|
|
55
|
+
- **@voltro/database, @voltro/runtime** — An `.encrypted()` column that can't be decrypted with the active key now fails as a typed, readable `FieldDecryptionError` naming the `table.column`, instead of a raw `Error: field cipher: malformed ciphertext` with no context. It carries a `_tag` (the same tagged shape `storeErrors` uses, so `Effect.catchTag` matches), and never includes the ciphertext. The common trigger is restoring a prod/staging snapshot into a dev DB whose `VOLTRO_FIELD_ENCRYPTION_KEY` differs.
|
|
56
|
+
|
|
57
|
+
New dev/migration escape hatch: `VOLTRO_FIELD_DECRYPT_ON_ERROR=null` degrades an undecryptable column to `null` (with one deduped warning per `table.column`, scope `store.fieldEncryption`) instead of letting one bad row nuke the whole read — its readable siblings still decrypt. Default stays `'throw'`; never set `null` in production, where a key mismatch must fail loud. `decryptFieldsOnRead` gains an optional `{ onError, warn }` argument (additive); the raw throw is replaced by the typed one, which existing `catch (e: Error)` handlers still catch.
|
|
58
|
+
- **@voltro/i18n** — `assertCatalogParity({ en, de })` checks every locale uses the SAME ICU `{var}` set per key. `defineLocale` enforces KEY parity but not PLACEHOLDER parity — a translation that drops or renames a `{var}` (`'Published on {date}'` → `'Veröffentlicht'`) compiles and boots, then throws `The intl string context variable "date" was not provided` only in that locale, only when the message renders. Call it in a test or at boot; it throws listing every drift (or warns with `onMismatch: 'warn'`). Plural argument names are extracted; a plural's inner `{# item}` branches are not mistaken for placeholders.
|
|
59
|
+
- **@voltro/i18n** — `createTypedMessages<typeof en>()` binds a catalog's LITERAL message types to `useT` / `useTFn` / `<T>`, so a missing ICU placeholder is a COMPILE error instead of a runtime throw at format time. Until now `defineCatalog` / `defineLocale` enforced key PARITY across locales, but the call site `t('key', values)` was untyped — a message like `'Published on {date}'` called as `t('roadmap.publishedAt')` (or via the `t('key').replace('{{date}}', …)` idiom from other i18n systems) threw `The intl string context variable "date" was not provided` only when it rendered. Now `useT('roadmap.publishedAt')` demands `{ date }` at compile time, and a wrong/missing key is caught too.
|
|
60
|
+
|
|
61
|
+
Opt-in and purely additive: call `createTypedMessages` once with your base catalog (`as const`) and re-export the returned `useT`/`useTFn`/`T`; the bare hooks keep their existing loose signatures. Scope: simple `{name}` and single-argument `{count, number}` forms are extracted; messages with nested inline ICU (`{n, plural, one {…} other {…}}` / `select`) accept a loose values bag rather than a wrong strict one — apps that pluralise in JS over simple `{count}` messages stay fully typed. `<T>` gets a typed key with loose values, because its rich-text `<tag>` renderers can't be modelled by `{var}` extraction.
|
|
62
|
+
- **@voltro/plugin-ai-flows** — An AI-flow `MediaGenerator` (and the `makeMediaGenerator` persistence seam) now receives the run it executes within — `run: { runId, tenantId }` — resolved from the durable run row rather than the caller subject. This is what a resume needs: a BOOTSTRAP/crash resume runs under a tenant-less `SYSTEM_SUBJECT`, so a host that persists artifacts per tenant could not read the tenant from `ctx.request.subject` (it isn't there) and would either fail closed or, under an old anonymous fallback, write into the wrong tenant. The engine already loads the run row (for `ownerId`); it now reads `tenantId` from the same row and hands it down to both the deterministic and agentic media steps, and `MediaPersist.put` / `ingestUrl` forward it to `storage.put` / `ingestUrl` so persistence continues in the run's own tenant on replay.
|
|
63
|
+
|
|
64
|
+
Additive: `run` is appended to the generator/persist arguments, so a host that ignores it keeps compiling; the tenant is simply available when it doesn't. No `@voltro/web` change — the engine is server-only and not re-exported to the browser surface.
|
|
65
|
+
- **@voltro/client, @voltro/web** — A sequence step's `undo` now receives the accumulated context as a second argument — `undo: (result, ctx) => …` — alongside the step's own result. An inverse usually needs an id from an EARLIER step as well as this one's (`deleteJiraDraftTicket({ jiraKey: created.key, draftId: ctx.draft.id })`), and until now the only way to reach it was to re-return that id from the step purely so the undo could read it back. `ctx` is typed as of the step's definition — the steps before it, the same context `covers` and `when` already see — so a later step's result is deliberately not visible (it is rolled back before this one).
|
|
66
|
+
|
|
67
|
+
Additive, not breaking: `StepUndo<Result>` became `StepUndo<Result, Ctx = Record<string, unknown>>` with the context parameter defaulted and appended, so a named `StepUndo<T>` still resolves and an existing single-argument `undo: (r) => …` stays assignable. `@voltro/web` re-exports the client surface, which is why it moves too.
|
|
68
|
+
- **@voltro/cli** — SSR `ctx.query` now has a first-class, server-only api origin for split web/api deployments (#18). Previously, a `renderMode:'ssr'` page reloaded in a split deployment 500'd: the web pod's `POST <origin>/rpc` fell back to the DEV proxy target (`http://localhost:4000`), which nothing serves in production → `ECONNREFUSED` buried in a render error. Browser-reachability and SSR-reachability were conflated into the one `url` field.
|
|
69
|
+
|
|
70
|
+
New `apis.<name>.serverUrl` (and env overrides `VOLTRO_API_ORIGIN_<NAME>` / `VOLTRO_API_ORIGIN`) set the origin the WEB POD uses for SSR — the api's internal cluster DNS (`http://api.<ns>.svc.cluster.local`) — distinct from the browser's relative wsPath, and NEVER emitted into the browser bundle. Resolution: env > `serverUrl` > (dev only) the vite proxy target > an external api's absolute url. Under `voltro start` a package api with none resolves to `undefined` and the loader query FAILS LOUD naming the api and the config to set — it never dials the dev localhost port. A transport failure is wrapped naming the api and the origin attempted, instead of a bare `fetch failed`.
|
|
71
|
+
- **@voltro/database, @voltro/runtime** — `store.insert` / `upsert` / `insertIgnore` now raise a clear, typed `TableValidationFailed` naming the column when the payload omits one that is NOT NULL, has no default, and isn't auto-stamped — instead of a raw dialect `SqlError: Failed to execute statement` (`Field '…' doesn't have a default value`) surfaced only on the INSERT path (so it lay dormant until the first row with no existing cache entry). An upsert / insertIgnore whose payload is missing one of its own `conflictColumns` is likewise named at the call (an absent conflict key can't match its target). The check runs AFTER stamping, so auto-id / tenant / audit columns never trip it, and skips nullable, defaulted, and id (`idScheme`) columns — exactly the ones a caller may legitimately omit.
|
|
72
|
+
|
|
73
|
+
Two pure helpers back it — `missingRequiredColumns(table, row)` and `missingConflictColumns(conflictColumns, row)` (exported from `@voltro/database`). This is the runtime half of the "handler data silently disagrees with the schema" class; a compile-time payload type needs the column DSL to track `hasDefault` at the type level, which is a separate change.
|
|
74
|
+
- **@voltro/database, @voltro/plugin-ai-flows, @voltro/plugin-audit, @voltro/plugin-deactivation, @voltro/plugin-soft-delete** — Compile-time payload typing for writes (#15) — the type-level half that the runtime `TableValidationFailed` guard flagged as a separate change. `insertRow` / `upsertRow` take the TABLE OBJECT (not a string name), so the payload is checked against `InferInsertRow<T>`: every column is required EXCEPT nullable ones, columns with a default, and the framework-filled id/tenant/audit columns. A missing NOT-NULL-no-default column — the exact `lastRefreshedAt` / `teamId` omission from the report — is now a COMPILE error at the call, not a runtime SqlError only on the INSERT path; `upsertRow`'s `conflictColumns` are constrained to the table's own columns too.
|
|
75
|
+
|
|
76
|
+
import { insertRow } from '@voltro/database' await insertRow(ctx.store, roadmapEpicStats, { teamId, lastRefreshedAt: new Date() })
|
|
77
|
+
|
|
78
|
+
Enabled by a type-level default flag: `ColumnDefinition` / `ColumnBuilder` gained a third `HasDefault` parameter that `.default()` narrows to `true`. It defaults to `boolean`, so every existing `ColumnDefinition<unknown>` (mixins, query builder, migrate, plugins) is unaffected — the only golden churn is the additive third parameter rendering (e.g. an audit mixin's defaulted `createdAt` now shows `ColumnDefinition<Date, "timestamp", true>`). The string-keyed `store.insert` / `upsert` are unchanged; the typed seam is opt-in.
|
|
79
|
+
- **@voltro/runtime, @voltro/cli** — `voltro dev` now warns, once per tenant-scoped table, when a request reads it with the empty-string tenant sentinel — an authenticated subject that has no resolved org (`tenantId === ''`). The auto-merged tenant filter becomes `eq('tenantId', '')`, which matches no real row, so every such read returns empty WITH NO ERROR — indistinguishable, from the response alone, between "no such row", "filtered by an empty tenant", and "auth half-resolved". The warning names the cause and the fix. `voltro serve` deliberately stays silent (a prod diagnostic on every scoped read is noise). The decision is a pure predicate, `isEmptyTenantScopedRead`, exported from `@voltro/runtime` so it is unit-tested apart from the 6k-line dev boot; `applyTenantScope` itself stays pure.
|
|
80
|
+
- **@voltro/runtime, @voltro/cli** — `voltro dev` now warns once, the first time an authenticated subject resolves with NO active org (a `user` carrying the empty-string tenant sentinel) — "authenticated, but no active org → all tenant-scoped reads will be empty". Broader and earlier than the per-table empty-tenant read warning: it catches the whole class at the door instead of on a specific read. Backed by the pure `isOrglessUserSubject` predicate in `@voltro/runtime`; dev owns the one-time log.
|
|
81
|
+
- **@voltro/cli** — `voltro start` (web) now reports boot timing, matching `voltro serve`. It always logs a structured `start: ready in <n>ms` line (with `bootMs`), counted from PROCESS start so the module-graph load — the phase that dominates a scale-to-zero cold start — is included instead of being missed by a mid-boot baseline; the banner's `bootMs` uses the same total. Under `VOLTRO_BOOT_TIMING=1` the line also carries a per-phase `phases` breakdown (`modules`, `config`, `scan`, `provider`, `routes`, `cdc`, `ready`). Because the total is a structured log record, it is retrievable from the container's `/_voltro/inspect/logs` endpoint, not only from stdout.
|
|
82
|
+
- **@voltro/cli** — Production web images are now self-contained and dramatically smaller. `voltro build` makes every runtime artefact framework-inlined + tree-shaken — the SSR bundle (vite `noExternal: true`), the start bundle, and the precompiled `appConfig` — so a booted `voltro start` needs from `node_modules` only the runtime-external NATIVE leaves it actually reaches (a SQL driver an ISR/config path touches). A new hidden `voltro prune-runtime <deploy-dir>` command (`@vercel/nft`, a new build-time optionalDependency) traces the real reachable set from those bundles + the app's installed native leaves and drops the rest — the whole `@voltro`/effect/react tree that's now dead weight. The standalone web Dockerfiles run it after `pnpm --prod deploy`; for a static/SSR site with no native runtime dep, `node_modules` collapses to nothing. Measured on a real marketing app: the app tree drops from ~210 MB to ~65 MB (node_modules ~145 MB → 0) and still boots + renders. Fully automatic — a used native driver is traced + kept, an unused one dropped, no per-app allow-list — and non-fatal: any trace failure keeps the fuller tree (a bigger image, never a broken one). The appConfig `@voltro/*` imports are now inlined (were external); this is safe because `runEnvGate` consumes the env schema structurally (`isEnvContract` duck-types `{ vars }`, no `instanceof`/Symbol), so a schema built by the config's inlined `@voltro/env` still validates.
|
|
83
|
+
- **@voltro/cli** — `voltro build` now precompiles the web START runtime into a single bundle (`.framework/dist-web/startBundle/startEntry.js`, framework inlined) — the web counterpart to the api serve bundle. `bin/voltro.mjs`'s `voltro start` fast path prefers it, so a cold scale-from-zero web boot loads ONE artefact instead of resolving the whole `@voltro`/effect module graph. Measured on the web-spa-shell fixture: the `modules` boot phase collapses from ~1130 ms to ~67 ms (~17×), directly cutting the phase measured to dominate a scale-to-zero web cold start. Non-fatal — if the bundle build or its import fails, `voltro start` falls back to the per-module CLI entry (correct, just slower). No app or Dockerfile change is needed; the bundle rides in `.framework`, which `pnpm deploy` already copies.
|
|
84
|
+
|
|
85
|
+
### Fixed
|
|
86
|
+
|
|
87
|
+
- **@voltro/runtime** — An undeclared infra error no longer reaches the browser as an `ExitEncoded` schema-tree dump (a wall of text that leaks internals and no client can pattern-match). `FieldDecryptionError` — a `.encrypted()` column that can't be decrypted with the active key — now collapses to a generic `InternalError` on both the unary and the subscription paths (it was previously only `SqlError` / `ResultLengthMismatch`, and only on the unary path). Its message names an internal `table.column`, so this also stops that leak; the real cause is logged server-side with the traceId. `TableValidationFailed` is deliberately left through — its summary/issues are meant to be shown to a user. Typed app errors are untouched.
|
|
88
|
+
- **@voltro/cli** — Security: the `/_voltro/inspect/logs` endpoint no longer bypasses the inspect gate. It dumps the process LogBuffer (request URLs + error payloads), but it returned early — above the `isInspectDisabled` / `VOLTRO_INSPECT_TOKEN` checks — so `VOLTRO_INSPECT=off` closed the manifest and metrics (503) while leaving the log buffer publicly readable, and a configured `VOLTRO_INSPECT_TOKEN` was ignored for it. The gate now lives inside `handleLogsRequest`, single-sourced across the three call sites (`voltro start` / `voltro dev` / the web-dev server) that each drifted (start ungated, dev gated nothing, web-dev gated disabled-but-not-token): disabled → 503, missing/bad bearer → 401.
|
|
89
|
+
|
|
90
|
+
---
|
|
91
|
+
|
|
42
92
|
## [0.11.0] — 2026-07-22
|
|
43
93
|
|
|
44
94
|
### ⚠ BREAKING
|
package/dist/index.d.ts
CHANGED
|
@@ -82,6 +82,9 @@ export declare interface AnnClause {
|
|
|
82
82
|
|
|
83
83
|
export declare type AnyMixin = MixinDefinition<Record<string, ColumnDefinition<unknown>>>;
|
|
84
84
|
|
|
85
|
+
/** Any table — the loose shape these helpers accept structurally. */
|
|
86
|
+
declare type AnyTable = Table<string, Record<string, ColumnDefinition<unknown>>, boolean, string>;
|
|
87
|
+
|
|
85
88
|
/**
|
|
86
89
|
* Array column — wraps another column constructor to declare an
|
|
87
90
|
* array of that element type. Postgres-native (`text[]`, `integer[]`,
|
|
@@ -173,6 +176,12 @@ export declare const auditAllTableIndexes: (tables: ReadonlyArray<Table<string,
|
|
|
173
176
|
*/
|
|
174
177
|
export declare const auditTableIndexes: (table: Table<string, Record<string, ColumnDefinition<unknown>>, boolean, string>) => ReadonlyArray<IndexAuditIssue>;
|
|
175
178
|
|
|
179
|
+
/** Columns the framework fills on write (auto-id floor + tenant/audit mixins),
|
|
180
|
+
* so an insert need not supply them. Name-based on purpose: a hand-declared
|
|
181
|
+
* `createdAt` with no default reads as optional here (the lenient direction —
|
|
182
|
+
* never rejects valid code), while the mixin-managed ones are always filled. */
|
|
183
|
+
declare type AutoFilledColumn = 'id' | 'tenantId' | 'createdAt' | 'updatedAt' | 'createdBy' | 'updatedBy' | 'deletedAt' | 'deletedBy';
|
|
184
|
+
|
|
176
185
|
export declare const avg: (column: string, alias?: string) => AggregateColumn;
|
|
177
186
|
|
|
178
187
|
export declare const avgOver: (column: string, alias?: string) => WindowBuilder;
|
|
@@ -223,7 +232,7 @@ export declare interface BackfillSpec<TsType = unknown> {
|
|
|
223
232
|
* bytesStored: bigint().default('0'),
|
|
224
233
|
* ```
|
|
225
234
|
*/
|
|
226
|
-
export declare const bigint: () => ColumnBuilder<string, "bigint">;
|
|
235
|
+
export declare const bigint: () => ColumnBuilder<string, "bigint", boolean>;
|
|
227
236
|
|
|
228
237
|
/**
|
|
229
238
|
* Bind the request to the store for the subject's home region. `stores` is the
|
|
@@ -238,7 +247,7 @@ export declare const bindResidentStore: <S>(subject: ResidencySubject, config: R
|
|
|
238
247
|
readonly placement: ResidentPlacement;
|
|
239
248
|
};
|
|
240
249
|
|
|
241
|
-
export declare const boolean: () => ColumnBuilder<boolean, "boolean">;
|
|
250
|
+
export declare const boolean: () => ColumnBuilder<boolean, "boolean", boolean>;
|
|
242
251
|
|
|
243
252
|
export declare type BranchEvent = 'provision' | 'provisioned' | 'fail' | 'destroy' | 'destroyed';
|
|
244
253
|
|
|
@@ -387,7 +396,7 @@ export declare class BranchTransitionInvalid extends Error {
|
|
|
387
396
|
* (mysql/mariadb) / `VARBINARY(MAX)` (mssql). Values round-trip as
|
|
388
397
|
* `Uint8Array`. Use for storing bytes IN the database (e.g. the
|
|
389
398
|
* `database` storage provider); keep large blobs in object storage. */
|
|
390
|
-
export declare const bytes: () => ColumnBuilder<Uint8Array<ArrayBufferLike>, "bytes">;
|
|
399
|
+
export declare const bytes: () => ColumnBuilder<Uint8Array<ArrayBufferLike>, "bytes", boolean>;
|
|
391
400
|
|
|
392
401
|
export declare type CaughtUpVerdict = 'caught-up' | 'behind';
|
|
393
402
|
|
|
@@ -504,9 +513,9 @@ export declare const collectSubqueries: (predicate: Predicate) => Generator<{
|
|
|
504
513
|
*/
|
|
505
514
|
export declare const column: <T = string | number | boolean | Date | null>(columnName: string, alias?: string) => AggregateColumn<T>;
|
|
506
515
|
|
|
507
|
-
export declare class ColumnBuilder<TsType, Type extends ColumnType> {
|
|
516
|
+
export declare class ColumnBuilder<TsType, Type extends ColumnType, HasDefault extends boolean = boolean> {
|
|
508
517
|
private readonly definition;
|
|
509
|
-
constructor(definition: ColumnDefinition<TsType, Type>);
|
|
518
|
+
constructor(definition: ColumnDefinition<TsType, Type, HasDefault>);
|
|
510
519
|
/**
|
|
511
520
|
* `raw(ddl)` columns reject any modifier that would add a second
|
|
512
521
|
* source of truth for nullability / default / uniqueness. The DDL
|
|
@@ -516,7 +525,7 @@ export declare class ColumnBuilder<TsType, Type extends ColumnType> {
|
|
|
516
525
|
* the error site is the schema file.
|
|
517
526
|
*/
|
|
518
527
|
private rejectIfRaw;
|
|
519
|
-
nullable(): ColumnBuilder<TsType | null, Type>;
|
|
528
|
+
nullable(): ColumnBuilder<TsType | null, Type, HasDefault>;
|
|
520
529
|
/**
|
|
521
530
|
* Encrypt this column's value at rest (AES-256-GCM). The runtime
|
|
522
531
|
* store middleware transparently encrypts on write + decrypts on
|
|
@@ -601,7 +610,7 @@ export declare class ColumnBuilder<TsType, Type extends ColumnType> {
|
|
|
601
610
|
* SQL-side fill (legacy seeds, raw inserts); factory wins for
|
|
602
611
|
* framework-routed inserts.
|
|
603
612
|
*/
|
|
604
|
-
default(value: TsType | 'now' | (() => TsType)):
|
|
613
|
+
default(value: TsType | 'now' | (() => TsType)): ColumnBuilder<TsType, Type, true>;
|
|
605
614
|
/**
|
|
606
615
|
* Mark this column as computed from other row fields. The function
|
|
607
616
|
* runs on INSERT AFTER defaults are applied, and again on every
|
|
@@ -754,7 +763,7 @@ export declare class ColumnBuilder<TsType, Type extends ColumnType> {
|
|
|
754
763
|
* Pass `as const` or a `const`-typed array — the TS inference
|
|
755
764
|
* preserves the literals only when the array is a tuple of literals.
|
|
756
765
|
*/
|
|
757
|
-
oneOf<const Values extends readonly [string, ...string[]]>(this: ColumnBuilder<TsType, 'text'>, values: Values): ColumnBuilder<null extends TsType ? Values[number] | null : Values[number], 'text'>;
|
|
766
|
+
oneOf<const Values extends readonly [string, ...string[]]>(this: ColumnBuilder<TsType, 'text', HasDefault>, values: Values): ColumnBuilder<null extends TsType ? Values[number] | null : Values[number], 'text', HasDefault>;
|
|
758
767
|
/**
|
|
759
768
|
* Cap a text column's length → `VARCHAR(n)` (postgres / mysql / mariadb) /
|
|
760
769
|
* `NVARCHAR(n)` (mssql), instead of the unbounded `LONGTEXT` / `TEXT`
|
|
@@ -777,7 +786,7 @@ export declare class ColumnBuilder<TsType, Type extends ColumnType> {
|
|
|
777
786
|
* Method only resolves on `text()` builders (the `Type extends 'text'`
|
|
778
787
|
* guard), like `oneOf`. `n` is a character count.
|
|
779
788
|
*/
|
|
780
|
-
maxLength(this: ColumnBuilder<TsType, 'text'>, n: number): ColumnBuilder<TsType, 'text'>;
|
|
789
|
+
maxLength(this: ColumnBuilder<TsType, 'text', HasDefault>, n: number): ColumnBuilder<TsType, 'text', HasDefault>;
|
|
781
790
|
/**
|
|
782
791
|
* Attach a raw SQL `CHECK` to this column — a DB-ENFORCED invariant
|
|
783
792
|
* that holds regardless of which client writes the row (defense-in-
|
|
@@ -804,11 +813,12 @@ export declare class ColumnBuilder<TsType, Type extends ColumnType> {
|
|
|
804
813
|
__definition(): ColumnDefinition<TsType, Type>;
|
|
805
814
|
}
|
|
806
815
|
|
|
807
|
-
export declare interface ColumnDefinition<TsType, Type extends ColumnType = ColumnType> {
|
|
816
|
+
export declare interface ColumnDefinition<TsType, Type extends ColumnType = ColumnType, HasDefault extends boolean = boolean> {
|
|
808
817
|
readonly type: Type;
|
|
809
818
|
readonly nullable: boolean;
|
|
810
819
|
readonly unique: boolean;
|
|
811
820
|
readonly hasDefault: boolean;
|
|
821
|
+
/* Excluded from this release type: __hasDefault */
|
|
812
822
|
readonly defaultValue?: unknown;
|
|
813
823
|
/**
|
|
814
824
|
* Application-side default factory. When set, the MutationStore
|
|
@@ -1128,6 +1138,15 @@ export declare interface ColumnDefinition<TsType, Type extends ColumnType = Colu
|
|
|
1128
1138
|
readonly __tsType?: TsType;
|
|
1129
1139
|
}
|
|
1130
1140
|
|
|
1141
|
+
/** `true` when the column's declared default flag is `true` (`.default()` narrows
|
|
1142
|
+
* it; a plain column reads `boolean`, which is NOT `[true]`). Tuple-wrapped so
|
|
1143
|
+
* `boolean` does not distribute. */
|
|
1144
|
+
declare type ColumnHasDefault<D> = D extends ColumnDefinition<unknown, ColumnType, infer HD> ? ([HD] extends [true] ? true : false) : false;
|
|
1145
|
+
|
|
1146
|
+
/** A column may be omitted from an insert when it is auto-filled, has a default,
|
|
1147
|
+
* or is nullable. */
|
|
1148
|
+
declare type ColumnOptionalForInsert<K extends PropertyKey, D> = K extends AutoFilledColumn ? true : ColumnHasDefault<D> extends true ? true : null extends InferColumn<D> ? true : false;
|
|
1149
|
+
|
|
1131
1150
|
/**
|
|
1132
1151
|
* The per-column mapping, over a column DEFINITION — what `table.fields` holds.
|
|
1133
1152
|
*
|
|
@@ -1562,7 +1581,7 @@ export declare interface DataStore {
|
|
|
1562
1581
|
}
|
|
1563
1582
|
|
|
1564
1583
|
/** Calendar date without time. */
|
|
1565
|
-
export declare const date: () => ColumnBuilder<Date, "date">;
|
|
1584
|
+
export declare const date: () => ColumnBuilder<Date, "date", boolean>;
|
|
1566
1585
|
|
|
1567
1586
|
export declare const dbEnum: <const Values extends ReadonlyArray<string>>(name: string, values: Values) => DbEnumHandle<Values>;
|
|
1568
1587
|
|
|
@@ -1670,7 +1689,17 @@ export declare const decodeRowsFromSchema: <T extends Record<string, unknown>>(r
|
|
|
1670
1689
|
* through untouched, so turning encryption on doesn't break reads of
|
|
1671
1690
|
* rows written before it. Operates per-row on shallow copies.
|
|
1672
1691
|
*/
|
|
1673
|
-
export declare const decryptFieldsOnRead: (rows: ReadonlyArray<Row>, table: TableLike | undefined, cipher: FieldCipher | undefined
|
|
1692
|
+
export declare const decryptFieldsOnRead: (rows: ReadonlyArray<Row>, table: TableLike | undefined, cipher: FieldCipher | undefined, options?: {
|
|
1693
|
+
/** Default `'throw'` — see `OnDecryptError`. */
|
|
1694
|
+
readonly onError?: OnDecryptError;
|
|
1695
|
+
/** Called once per undecryptable column when `onError: 'null'`, so the
|
|
1696
|
+
* degraded read leaves a trace naming the `table.column`. */
|
|
1697
|
+
readonly warn?: (info: {
|
|
1698
|
+
readonly table: string;
|
|
1699
|
+
readonly column: string;
|
|
1700
|
+
readonly reason: string;
|
|
1701
|
+
}) => void;
|
|
1702
|
+
}) => ReadonlyArray<Row>;
|
|
1674
1703
|
|
|
1675
1704
|
export declare const defineMigration: (input: MigrationDefinitionInput) => MigrationDefinition;
|
|
1676
1705
|
|
|
@@ -2002,8 +2031,31 @@ export declare interface FieldCipher {
|
|
|
2002
2031
|
readonly decrypt: (ciphertext: string) => string;
|
|
2003
2032
|
}
|
|
2004
2033
|
|
|
2034
|
+
/**
|
|
2035
|
+
* An `.encrypted()` column could not be decrypted with the ACTIVE cipher —
|
|
2036
|
+
* almost always a value encrypted under a DIFFERENT key (a prod/staging snapshot
|
|
2037
|
+
* restored into a dev DB whose key differs). Typed and `_tag`-carrying (the same
|
|
2038
|
+
* shape `storeErrors.ts` uses, deliberately NOT `Data.TaggedError` — that trips
|
|
2039
|
+
* TS2742 on the `.d.ts` and drags `effect/Cause` into this browser-safe codec) so
|
|
2040
|
+
* it is `catchTag`-matchable and, above all, READABLE: the message names the
|
|
2041
|
+
* `table.column` and the reason instead of surfacing a raw
|
|
2042
|
+
* `field cipher: malformed ciphertext` with no context. The ciphertext itself is
|
|
2043
|
+
* NEVER included.
|
|
2044
|
+
*/
|
|
2045
|
+
export declare class FieldDecryptionError extends Error {
|
|
2046
|
+
readonly _tag = "FieldDecryptionError";
|
|
2047
|
+
readonly table: string;
|
|
2048
|
+
readonly column: string;
|
|
2049
|
+
readonly reason: string;
|
|
2050
|
+
constructor(info: {
|
|
2051
|
+
readonly table: string;
|
|
2052
|
+
readonly column: string;
|
|
2053
|
+
readonly reason: string;
|
|
2054
|
+
});
|
|
2055
|
+
}
|
|
2056
|
+
|
|
2005
2057
|
export declare type FieldDefinitions<F extends FieldsInput> = {
|
|
2006
|
-
[K in keyof F]: F[K] extends ColumnBuilder<infer T, infer Type> ? ColumnDefinition<T, Type> : never;
|
|
2058
|
+
[K in keyof F]: F[K] extends ColumnBuilder<infer T, infer Type, infer HD> ? ColumnDefinition<T, Type, HD> : never;
|
|
2007
2059
|
};
|
|
2008
2060
|
|
|
2009
2061
|
export declare type FieldsInput = Record<string, ColumnBuilder<unknown, ColumnType>>;
|
|
@@ -2069,6 +2121,11 @@ export declare interface FileMigrationContext {
|
|
|
2069
2121
|
readonly appliedAt: string;
|
|
2070
2122
|
}
|
|
2071
2123
|
|
|
2124
|
+
/** Flatten an intersection into a single object type for readable errors/hovers. */
|
|
2125
|
+
declare type Flatten<O> = {
|
|
2126
|
+
[K in keyof O]: O[K];
|
|
2127
|
+
};
|
|
2128
|
+
|
|
2072
2129
|
/**
|
|
2073
2130
|
* Multi-line pretty block for the terminal. Tries to teach, not just
|
|
2074
2131
|
* complain — labels every section (`table` / `redundant` / `covered
|
|
@@ -2218,7 +2275,7 @@ export declare interface HybridSearchOptions {
|
|
|
2218
2275
|
* `row.id`; for `numeric` the key is omitted so the dialect's
|
|
2219
2276
|
* SERIAL/AUTO_INCREMENT/IDENTITY clause fires.
|
|
2220
2277
|
*/
|
|
2221
|
-
export declare const id: (options?: IdSchemeInput) => ColumnBuilder<string, "id">;
|
|
2278
|
+
export declare const id: (options?: IdSchemeInput) => ColumnBuilder<string, "id", boolean>;
|
|
2222
2279
|
|
|
2223
2280
|
/**
|
|
2224
2281
|
* Fully-resolved scheme stored on the `id` column definition AFTER
|
|
@@ -2390,6 +2447,12 @@ export declare const inferForeignKey: (target: TableLike, sourceTableName: strin
|
|
|
2390
2447
|
*/
|
|
2391
2448
|
export declare type InferIndexNames<T> = T extends Table<string, Record<string, ColumnDefinition<unknown>>, boolean, infer N> ? N : never;
|
|
2392
2449
|
|
|
2450
|
+
export declare type InferInsertRow<T> = T extends Table<string, infer F, boolean, string> ? Flatten<{
|
|
2451
|
+
readonly [K in keyof F as ColumnOptionalForInsert<K, F[K]> extends true ? never : K]: InferColumn<F[K]>;
|
|
2452
|
+
} & {
|
|
2453
|
+
readonly [K in keyof F as ColumnOptionalForInsert<K, F[K]> extends true ? K : never]?: InferColumn<F[K]>;
|
|
2454
|
+
}> : never;
|
|
2455
|
+
|
|
2393
2456
|
/** Row type for the table — useful for typing application code. */
|
|
2394
2457
|
export declare type InferRow<T> = T extends Table<string, infer F, boolean, string> ? InferRowFromFields<F> : never;
|
|
2395
2458
|
|
|
@@ -2400,11 +2463,18 @@ export declare type InferRowFromFields<F extends Record<string, ColumnDefinition
|
|
|
2400
2463
|
/** Row type for a view — mirrors `InferRow` for tables. */
|
|
2401
2464
|
export declare type InferViewRow<V> = V extends View<string, infer F> ? InferRowFromFields<F> : never;
|
|
2402
2465
|
|
|
2466
|
+
/**
|
|
2467
|
+
* Insert a row, typed against the table: the payload must carry every required
|
|
2468
|
+
* column (NOT NULL, no default, not auto-filled) or it is a compile error.
|
|
2469
|
+
* Returns the stored row typed as `InferRow<T>`.
|
|
2470
|
+
*/
|
|
2471
|
+
export declare const insertRow: <T extends AnyTable>(store: TypedInsertStore, table: T, row: InferInsertRow<T>) => Promise<InferRow<T>>;
|
|
2472
|
+
|
|
2403
2473
|
export declare const inSet: <RowOf = Record<string, unknown>, K extends keyof RowOf & string = keyof RowOf & string>(column: K, values: ReadonlyArray<RowOf[K]>) => PredicateLeaf;
|
|
2404
2474
|
|
|
2405
2475
|
export declare const inSubquery: <RowOf = Record<string, unknown>, K extends keyof RowOf & string = keyof RowOf & string>(column: K, subquery: QueryWithDescriptor | QueryDescriptor) => SubqueryInPredicate;
|
|
2406
2476
|
|
|
2407
|
-
export declare const integer: () => ColumnBuilder<number, "integer">;
|
|
2477
|
+
export declare const integer: () => ColumnBuilder<number, "integer", boolean>;
|
|
2408
2478
|
|
|
2409
2479
|
/** `INTERSECT` — rows present in EVERY input. */
|
|
2410
2480
|
export declare const intersect: (...queries: ReadonlyArray<QueryWithDescriptorAny>) => Query<Row, string>;
|
|
@@ -2419,7 +2489,7 @@ export declare const intersect: (...queries: ReadonlyArray<QueryWithDescriptorAn
|
|
|
2419
2489
|
* slaDeadline: interval()
|
|
2420
2490
|
* ```
|
|
2421
2491
|
*/
|
|
2422
|
-
export declare const interval: () => ColumnBuilder<string, "interval">;
|
|
2492
|
+
export declare const interval: () => ColumnBuilder<string, "interval", boolean>;
|
|
2423
2493
|
|
|
2424
2494
|
/** Is this namespace one of ours? (so a teardown sweep never touches a real
|
|
2425
2495
|
* tenant namespace). */
|
|
@@ -2427,6 +2497,8 @@ export declare const isBranchNamespace: (namespace: string) => boolean;
|
|
|
2427
2497
|
|
|
2428
2498
|
export declare const isEncrypted: (value: unknown) => value is string;
|
|
2429
2499
|
|
|
2500
|
+
export declare const isFieldDecryptionError: (e: unknown) => e is FieldDecryptionError;
|
|
2501
|
+
|
|
2430
2502
|
/** Type-guard for the discovery walker. */
|
|
2431
2503
|
export declare const isFileMigration: (value: unknown) => value is FileMigration;
|
|
2432
2504
|
|
|
@@ -2494,7 +2566,7 @@ export declare const isView: (t: TableLike) => t is View<string, Record<string,
|
|
|
2494
2566
|
export declare type JoinsMap = Record<string, Record<string, unknown>>;
|
|
2495
2567
|
|
|
2496
2568
|
/** Typed JSON column. Pass the type parameter to record the expected shape. */
|
|
2497
|
-
export declare const json: <T = unknown>() => ColumnBuilder<T, "json">;
|
|
2569
|
+
export declare const json: <T = unknown>() => ColumnBuilder<T, "json", boolean>;
|
|
2498
2570
|
|
|
2499
2571
|
export declare const jsonField: (column: string, ...path: ReadonlyArray<string | number>) => JsonFieldFilter;
|
|
2500
2572
|
|
|
@@ -2760,6 +2832,21 @@ export declare interface MigrationStepContext {
|
|
|
2760
2832
|
|
|
2761
2833
|
export declare const min: (column: string, alias?: string) => AggregateColumn;
|
|
2762
2834
|
|
|
2835
|
+
/**
|
|
2836
|
+
* The `conflictColumns` an upsert names that are absent from its payload. An
|
|
2837
|
+
* upsert keyed on a column the row doesn't set can't match a conflict target —
|
|
2838
|
+
* the dialect fails obscurely (or worse, inserts a duplicate). Naming it at the
|
|
2839
|
+
* call is the difference between a one-line fix and reading a driver error.
|
|
2840
|
+
*/
|
|
2841
|
+
export declare const missingConflictColumns: (conflictColumns: ReadonlyArray<string>, row: Row) => ReadonlyArray<string>;
|
|
2842
|
+
|
|
2843
|
+
/**
|
|
2844
|
+
* Columns the row must carry but doesn't. Empty when the row is complete.
|
|
2845
|
+
* `null` counts as missing for a NOT NULL column — the dialect would reject it
|
|
2846
|
+
* just the same, and a clear message beats a driver error either way.
|
|
2847
|
+
*/
|
|
2848
|
+
export declare const missingRequiredColumns: (table: TableLike, row: Row) => ReadonlyArray<string>;
|
|
2849
|
+
|
|
2763
2850
|
export declare const mixin: <const Input extends FieldsInput>(options: MixinOptions<Input>) => MixinDefinition<MaterializedMixinFields<Input>>;
|
|
2764
2851
|
|
|
2765
2852
|
export declare interface MixinDefinition<F extends Record<string, ColumnDefinition<unknown>>> {
|
|
@@ -2887,6 +2974,17 @@ export declare interface NotPredicate {
|
|
|
2887
2974
|
*/
|
|
2888
2975
|
export declare const numeric: (precision: number, scale?: number) => ColumnBuilder<string, "decimal">;
|
|
2889
2976
|
|
|
2977
|
+
/**
|
|
2978
|
+
* What a decrypt failure does. `'throw'` (default, and the ONLY safe production
|
|
2979
|
+
* behaviour) surfaces a typed `FieldDecryptionError`. `'null'` degrades the one
|
|
2980
|
+
* unreadable column to `null` and warns — for DEV / a data migration where a
|
|
2981
|
+
* snapshot carries ciphertext bound to another key: one undecryptable row must
|
|
2982
|
+
* not nuke every read (and its siblings that WOULD decrypt), it should surface as
|
|
2983
|
+
* "re-enter this credential", not a 500. Never enable `'null'` in production — it
|
|
2984
|
+
* silently hides a real key mismatch.
|
|
2985
|
+
*/
|
|
2986
|
+
export declare type OnDecryptError = 'throw' | 'null';
|
|
2987
|
+
|
|
2890
2988
|
export declare const one: (target: TableRef, options?: {
|
|
2891
2989
|
readonly foreignKey?: string;
|
|
2892
2990
|
readonly sourceKey?: string;
|
|
@@ -3867,7 +3965,7 @@ declare interface RawSqlFragment {
|
|
|
3867
3965
|
* mssql `FLOAT`, sqlite `REAL`. Drivers return JS numbers — no codec. Use for
|
|
3868
3966
|
* fractional values (ratings, percentages, measurements) where `integer()`
|
|
3869
3967
|
* would truncate. */
|
|
3870
|
-
export declare const real: () => ColumnBuilder<number, "real">;
|
|
3968
|
+
export declare const real: () => ColumnBuilder<number, "real", boolean>;
|
|
3871
3969
|
|
|
3872
3970
|
/**
|
|
3873
3971
|
* Foreign-key reference to another table's id column.
|
|
@@ -5061,9 +5159,9 @@ export declare class TenantResidencyUnresolved extends Error {
|
|
|
5061
5159
|
constructor(message: string);
|
|
5062
5160
|
}
|
|
5063
5161
|
|
|
5064
|
-
export declare const text: () => ColumnBuilder<string, "text">;
|
|
5162
|
+
export declare const text: () => ColumnBuilder<string, "text", boolean>;
|
|
5065
5163
|
|
|
5066
|
-
export declare const timestamp: () => ColumnBuilder<Date, "timestamp">;
|
|
5164
|
+
export declare const timestamp: () => ColumnBuilder<Date, "timestamp", boolean>;
|
|
5067
5165
|
|
|
5068
5166
|
/**
|
|
5069
5167
|
* The timestamp mapping as a STANDALONE field schema: `Date` in the
|
|
@@ -5106,6 +5204,19 @@ export declare const timestampMs: Schema.Schema<Date, number>;
|
|
|
5106
5204
|
*/
|
|
5107
5205
|
export declare const timestampMsOrNull: Schema.Schema<Date | null, number | null>;
|
|
5108
5206
|
|
|
5207
|
+
/** The minimal store surface `insertRow` needs — satisfied by `ctx.store`. */
|
|
5208
|
+
export declare interface TypedInsertStore {
|
|
5209
|
+
readonly insert: (table: string, row: Row) => Promise<Row>;
|
|
5210
|
+
}
|
|
5211
|
+
|
|
5212
|
+
/** The minimal store surface `upsertRow` needs. */
|
|
5213
|
+
export declare interface TypedUpsertStore {
|
|
5214
|
+
readonly upsert: (table: string, row: Row, options: {
|
|
5215
|
+
conflictColumns: ReadonlyArray<string>;
|
|
5216
|
+
update?: ReadonlyArray<string> | ((existing: Row) => Readonly<Record<string, unknown>>);
|
|
5217
|
+
}) => Promise<Row>;
|
|
5218
|
+
}
|
|
5219
|
+
|
|
5109
5220
|
/**
|
|
5110
5221
|
* `(SELECT ...) UNION (SELECT ...) ...` — dedup-merge. Q6.
|
|
5111
5222
|
*
|
|
@@ -5139,6 +5250,15 @@ export declare interface UniqueSpec {
|
|
|
5139
5250
|
readonly dedup?: 'fail' | 'suffix-counter' | Statement.Fragment;
|
|
5140
5251
|
}
|
|
5141
5252
|
|
|
5253
|
+
/**
|
|
5254
|
+
* Upsert a row, typed the same way. `conflictColumns` is constrained to the
|
|
5255
|
+
* table's own column names, so a typo'd conflict key is a compile error too.
|
|
5256
|
+
*/
|
|
5257
|
+
export declare const upsertRow: <T extends AnyTable>(store: TypedUpsertStore, table: T, row: InferInsertRow<T>, options: {
|
|
5258
|
+
readonly conflictColumns: ReadonlyArray<keyof InferRow<T> & string>;
|
|
5259
|
+
readonly update?: ReadonlyArray<keyof InferRow<T> & string> | ((existing: InferRow<T>) => Readonly<Partial<InferRow<T>>>);
|
|
5260
|
+
}) => Promise<InferRow<T>>;
|
|
5261
|
+
|
|
5142
5262
|
/**
|
|
5143
5263
|
* Validate one column-name key on a table's fields map. Throws on
|
|
5144
5264
|
* empty / invalid characters / > 63 bytes. Records a WARN above 50.
|