okengine 0.17.2 → 0.18.3
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/README.md +9 -5
- package/manifest.v1.schema.json +61 -2
- package/package.json +18 -4
- package/site/content/docs/elements/clock.mdx +1 -1
- package/site/content/docs/elements/flow.mdx +25 -1
- package/site/content/docs/elements/store.mdx +287 -341
- package/site/content/docs/elements/vault.mdx +5 -5
- package/site/content/docs/get-started/installation.mdx +1 -2
- package/site/content/docs/get-started/introduction.mdx +54 -110
- package/site/content/docs/get-started/meta.json +9 -1
- package/site/content/docs/get-started/testing.mdx +328 -0
- package/site/content/docs/get-started/why.mdx +94 -70
- package/site/content/docs/index.mdx +1 -1
- package/site/content/docs/plugins/apple.mdx +151 -0
- package/site/content/docs/plugins/discord.mdx +139 -0
- package/site/content/docs/plugins/facebook.mdx +134 -0
- package/site/content/docs/plugins/figma.mdx +138 -0
- package/site/content/docs/plugins/github.mdx +138 -0
- package/site/content/docs/plugins/google.mdx +153 -0
- package/site/content/docs/plugins/index.mdx +47 -1
- package/site/content/docs/plugins/meta.json +10 -0
- package/site/content/docs/plugins/microsoft.mdx +151 -0
- package/site/content/docs/plugins/oauth.mdx +188 -0
- package/site/content/docs/plugins/x.mdx +125 -0
- package/site/content/docs/reference/cli.md +3 -2
- package/site/content/docs/reference/client.mdx +58 -1
- package/site/content/docs/reference/configuration.mdx +2 -4
- package/site/content/docs/reference/fx.mdx +3 -1
- package/site/content/docs/reference/index.mdx +0 -5
- package/site/content/docs/reference/meta.json +2 -2
- package/site/content/docs/reference/okid.mdx +137 -0
- package/src/auth/bindings.ts +1 -1
- package/src/auth/config.ts +9 -0
- package/src/auth/identity-sql.ts +314 -0
- package/src/auth/identity.ts +140 -2
- package/src/auth/index.ts +17 -1
- package/src/auth/method-context.ts +3 -0
- package/src/auth/oauth-as/cimd.ts +132 -0
- package/src/auth/oauth-as/crypto.test.ts +101 -0
- package/src/auth/oauth-as/crypto.ts +393 -0
- package/src/auth/oauth-as/errors.ts +68 -0
- package/src/auth/oauth-as/http.test.ts +419 -0
- package/src/auth/oauth-as/http.ts +842 -0
- package/src/auth/oauth-as/stores.ts +61 -0
- package/src/auth/oauth-as/tables.ts +142 -0
- package/src/auth/tables.ts +0 -11
- package/src/bench/README.md +83 -0
- package/src/bench/REPORT.md +176 -0
- package/src/bench/g01-rls-stamp.bench.ts +194 -0
- package/src/bench/g02-clock-per-tenant.bench.ts +158 -0
- package/src/bench/g03-signal-once.bench.ts +157 -0
- package/src/bench/g03-signal-reconnect.bench.ts +254 -0
- package/src/bench/g03-signal-sse-memory.bench.ts +191 -0
- package/src/bench/g04-auth-vault-hotpath.bench.ts +170 -0
- package/src/bench/g05-sustained-full.bench.ts +265 -0
- package/src/bench/g06-mixed-load.bench.ts +260 -0
- package/src/bench/g07-vault-crypto.bench.ts +100 -0
- package/src/bench/g07-vault-rotate-under-read.bench.ts +285 -0
- package/src/bench/g08-conn-oversubscribe.bench.ts +194 -0
- package/src/bench/g08-store-kv-durable.bench.ts +133 -0
- package/src/bench/g08-store-sql.bench.ts +178 -0
- package/src/bench/g09-journal-sustained.bench.ts +203 -0
- package/src/bench/g10-observability-contention.bench.ts +246 -0
- package/src/bench/g11-cold-start-cycle.bench.ts +164 -0
- package/src/bench/g13-elements.bench.ts +427 -0
- package/src/bench/g14-graceful-shutdown.bench.ts +244 -0
- package/src/bench/g15-postgres-degradation.bench.ts +264 -0
- package/src/bench/g16-live-query-fanout.bench.ts +206 -0
- package/src/bench/lib/event-loop-lag.ts +26 -0
- package/src/bench/lib/infra.ts +60 -0
- package/src/bench/lib/report.ts +52 -0
- package/src/bench/lib/rss-sampler.ts +61 -0
- package/src/bench/lib/signal-pg.ts +88 -0
- package/src/bench/load-app.ts +337 -0
- package/src/bench/load-child.ts +108 -0
- package/src/bench/smoke.bench.ts +43 -0
- package/src/cli/competitor-mention-removal.test.ts +28 -0
- package/src/cli/doctor-fd.ts +117 -0
- package/src/cli/doctor.test.ts +192 -0
- package/src/cli/doctor.ts +129 -1
- package/src/client/create.ts +95 -1
- package/src/client/index.ts +9 -2
- package/src/client/transport.ts +11 -4
- package/src/client/use-live-query.ts +154 -0
- package/src/client-react/index.ts +15 -1
- package/src/client-react/live-resource.ts +246 -0
- package/src/client-react/use-live-query.test.ts +475 -0
- package/src/client-react/use-live-query.ts +530 -0
- package/src/compiler/extract.test.ts +518 -0
- package/src/compiler/extract.ts +386 -19
- package/src/console/server/invoke-user-flow.ts +2 -1
- package/src/console/ui-next/dist/assets/{access-page-DnWbnGzq.js → access-page-DY4N6nnk.js} +1 -1
- package/src/console/ui-next/dist/assets/{flows-page-BiZ4-6yQ.js → flows-page-CsPDMrVM.js} +1 -1
- package/src/console/ui-next/dist/assets/{index-C8NRK2R-.js → index-CcTDXHuz.js} +3 -3
- package/src/console/ui-next/dist/assets/{observability-page-CrB6vd1T.js → observability-page-CKR595wP.js} +1 -1
- package/src/console/ui-next/dist/assets/{store-page-CS5-aETQ.js → store-page-02xOiqIK.js} +3 -3
- package/src/console/ui-next/dist/assets/{units-page-CjtdlW8l.js → units-page-CpPFFKyE.js} +1 -1
- package/src/console/ui-next/dist/assets/{vault-page-C6Xxm9SA.js → vault-page-BsMf-9_W.js} +1 -1
- package/src/console/ui-next/dist/index.html +1 -1
- package/src/console/ui-next/src/features/store/lib/fields-from-table.ts +36 -2
- package/src/drivers/cdc-outbox.ts +389 -0
- package/src/drivers/memory.ts +20 -0
- package/src/drivers/oauth-apple.ts +156 -0
- package/src/drivers/oauth-discord.ts +79 -0
- package/src/drivers/oauth-facebook.ts +80 -0
- package/src/drivers/oauth-figma.ts +116 -0
- package/src/drivers/oauth-github.ts +92 -0
- package/src/drivers/oauth-google.ts +142 -0
- package/src/drivers/oauth-microsoft.ts +174 -0
- package/src/drivers/oauth-oidc.ts +293 -0
- package/src/drivers/oauth-shared.ts +326 -0
- package/src/drivers/oauth-types.ts +159 -0
- package/src/drivers/oauth-x.ts +77 -0
- package/src/drivers/oauth2-common.ts +95 -0
- package/src/drivers/oauth2-token.ts +61 -0
- package/src/drivers/pg-rls-row-passes.ts +251 -0
- package/src/drivers/pg-rls.ts +2 -0
- package/src/drivers/postgres.ts +45 -2
- package/src/drivers/signal-postgres.ts +2 -1
- package/src/elements/channel/runtime.ts +29 -2
- package/src/elements/channel.test.ts +52 -0
- package/src/elements/gate/boot.ts +29 -2
- package/src/elements/store/emit-drizzle.ts +147 -14
- package/src/elements/store/field-ddl.test.ts +118 -0
- package/src/elements/store/field-types.test.ts +455 -0
- package/src/elements/store/list-query.golden.json +777 -0
- package/src/elements/store/list-query.parity.test.ts +396 -0
- package/src/elements/store/list-query.ts +792 -0
- package/src/elements/store/live-default.test.ts +136 -0
- package/src/elements/store/live-http.test.ts +160 -0
- package/src/elements/store/live-isolation.test.ts +291 -0
- package/src/elements/store/live-query-runtime.test.ts +323 -0
- package/src/elements/store/live-query-runtime.ts +403 -0
- package/src/elements/store/live-query-server.test.ts +377 -0
- package/src/elements/store/live-query-server.ts +102 -0
- package/src/elements/store/live-query.ts +97 -0
- package/src/elements/store/resource.ts +189 -680
- package/src/elements/store/rls-row-passes-policies.parity.test.ts +665 -0
- package/src/elements/store/schema-decl.ts +539 -41
- package/src/elements/store/sql-rls-stamp.test.ts +27 -0
- package/src/elements/store/sql-session.ts +297 -35
- package/src/elements/store/table.ts +102 -21
- package/src/elements/store.test.ts +3 -1
- package/src/elements/store.ts +12 -1
- package/src/elements/vault/chaos-child.ts +74 -1
- package/src/elements/vault/chaos.test.ts +4 -2
- package/src/elements/vault/storage.ts +4 -2
- package/src/index.ts +4 -1
- package/src/kernel/app-auth.ts +1 -0
- package/src/kernel/app.ts +116 -2
- package/src/kernel/auth-sharing.test.ts +196 -0
- package/src/kernel/boot.test.ts +3 -3
- package/src/kernel/errors.ts +8 -0
- package/src/kernel/fx.test.ts +1 -0
- package/src/kernel/fx.ts +14 -2
- package/src/kernel/horizontal-child.ts +2 -1
- package/src/kernel/http-resource.ts +33 -7
- package/src/kernel/identity-host-persist.test.ts +119 -0
- package/src/kernel/instance-id.ts +4 -2
- package/src/kernel/journal.ts +2 -1
- package/src/kernel/mcp-tool.test.ts +95 -0
- package/src/kernel/on.ts +9 -0
- package/src/kernel/realtime-bind.ts +326 -0
- package/src/kernel/resource-live.ts +117 -0
- package/src/kernel/triggers.ts +86 -4
- package/src/manifest/diff.ts +37 -0
- package/src/manifest/types.ts +64 -2
- package/src/okid.bench.test.ts +64 -0
- package/src/okid.test.ts +338 -0
- package/src/okid.ts +245 -0
- package/src/plugins/anonymous.ts +19 -1
- package/src/plugins/auth/shared.ts +15 -0
- package/src/plugins/index.ts +2 -0
- package/src/plugins/magic-link.ts +10 -8
- package/src/plugins/mcp-oauth.ts +208 -0
- package/src/plugins/oauth/flow-store.ts +117 -0
- package/src/plugins/oauth/link.ts +69 -0
- package/src/plugins/oauth/shared.ts +108 -0
- package/src/plugins/oauth/token-vault.ts +100 -0
- package/src/plugins/oauth.security.test.ts +535 -0
- package/src/plugins/oauth.ts +532 -0
- package/src/plugins/otp.ts +48 -6
- package/src/plugins/passkey.ts +20 -1
- package/src/plugins/two-factor.ts +11 -0
- package/src/plugins/username.ts +40 -7
- package/src/release/build-lib.ts +7 -1
- package/src/release/measure.ts +1 -0
- package/src/release/official-plugins.ts +4 -1
- package/src/runs/collect.ts +2 -1
- package/src/runs/drivers/files.ts +2 -1
- package/src/test/create-test-app.ts +114 -5
- package/src/test/export-bundle.test.ts +33 -0
- package/src/test/live-signals.test.ts +83 -0
- package/src/test/tenant-isolation.test.ts +175 -0
- package/src/testing.ts +26 -0
- package/src/upgrade/codemods.ts +1 -1
- package/site/content/docs/reference/migrating-environments.mdx +0 -158
|
@@ -7,14 +7,106 @@
|
|
|
7
7
|
|
|
8
8
|
import type { ColumnClassification } from "../../manifest/types.ts";
|
|
9
9
|
import { lazyRequire } from "../../kernel/lazy-require.ts";
|
|
10
|
+
import type { PolicyGateDecl } from "../gate/declare.ts";
|
|
10
11
|
import type { ColumnDef, TableHandle } from "./table.ts";
|
|
11
|
-
import {
|
|
12
|
+
import {
|
|
13
|
+
id as idHelper,
|
|
14
|
+
now as nowHelper,
|
|
15
|
+
nowIso as nowIsoHelper,
|
|
16
|
+
nowDate as nowDateHelper,
|
|
17
|
+
} from "./table.ts";
|
|
12
18
|
|
|
13
|
-
/**
|
|
14
|
-
|
|
19
|
+
/**
|
|
20
|
+
* SQL type primitives — the full Drizzle Postgres column-type surface.
|
|
21
|
+
* Factory names mirror `drizzle-orm/pg-core` exactly.
|
|
22
|
+
*/
|
|
23
|
+
export type FieldSqlType =
|
|
24
|
+
| "text"
|
|
25
|
+
| "varchar"
|
|
26
|
+
| "char"
|
|
27
|
+
| "boolean"
|
|
28
|
+
| "smallint"
|
|
29
|
+
| "integer"
|
|
30
|
+
| "bigint"
|
|
31
|
+
| "serial"
|
|
32
|
+
| "smallserial"
|
|
33
|
+
| "bigserial"
|
|
34
|
+
| "numeric"
|
|
35
|
+
| "real"
|
|
36
|
+
| "doublePrecision"
|
|
37
|
+
| "json"
|
|
38
|
+
| "jsonb"
|
|
39
|
+
| "uuid"
|
|
40
|
+
| "time"
|
|
41
|
+
| "timestamp"
|
|
42
|
+
| "date"
|
|
43
|
+
| "interval"
|
|
44
|
+
| "point"
|
|
45
|
+
| "line"
|
|
46
|
+
| "bytea"
|
|
47
|
+
| "inet"
|
|
48
|
+
| "cidr"
|
|
49
|
+
| "macaddr"
|
|
50
|
+
| "macaddr8";
|
|
51
|
+
|
|
52
|
+
/** Timestamp / time precision — mirrors drizzle-orm rc.5 `Precision` union. */
|
|
53
|
+
export type FieldPrecision = 0 | 1 | 2 | 3 | 4 | 5 | 6;
|
|
54
|
+
|
|
55
|
+
/** JS mapping for temporal (`timestamp` / `date`) columns. */
|
|
56
|
+
export type TemporalMode = "date" | "string";
|
|
57
|
+
|
|
58
|
+
/** JS mapping for `bigint` columns. */
|
|
59
|
+
export type BigIntMode = "number" | "bigint" | "string";
|
|
60
|
+
|
|
61
|
+
/** JS mapping for `numeric` columns. */
|
|
62
|
+
export type NumericMode = "string" | "number" | "bigint";
|
|
63
|
+
|
|
64
|
+
/** Tuple vs object mapping for geometric `point` columns. */
|
|
65
|
+
export type PointMode = "tuple" | "xy";
|
|
66
|
+
|
|
67
|
+
/** Tuple vs equation-object mapping for geometric `line` columns. */
|
|
68
|
+
export type LineMode = "tuple" | "abc";
|
|
69
|
+
|
|
70
|
+
/** Postgres `interval` field subset — mirrors drizzle-orm rc.5 `IntervalConfig["fields"]`. */
|
|
71
|
+
export type IntervalField =
|
|
72
|
+
| "year"
|
|
73
|
+
| "month"
|
|
74
|
+
| "day"
|
|
75
|
+
| "hour"
|
|
76
|
+
| "minute"
|
|
77
|
+
| "second"
|
|
78
|
+
| "year to month"
|
|
79
|
+
| "day to hour"
|
|
80
|
+
| "day to minute"
|
|
81
|
+
| "day to second"
|
|
82
|
+
| "hour to minute"
|
|
83
|
+
| "hour to second"
|
|
84
|
+
| "minute to second";
|
|
85
|
+
|
|
86
|
+
/**
|
|
87
|
+
* Per-type options recorded on a column (`length`, `precision`, `scale`,
|
|
88
|
+
* `withTimezone`, `mode`, `fields`, `enumValues`). Only the keys relevant to
|
|
89
|
+
* {@link SchemaColumnDecl.sqlType} are populated.
|
|
90
|
+
*/
|
|
91
|
+
export interface FieldTypeOptions {
|
|
92
|
+
/** `varchar` / `char` max length. */
|
|
93
|
+
readonly length?: number;
|
|
94
|
+
/** `time` / `timestamp` fractional-second digits (0–6). */
|
|
95
|
+
readonly precision?: number;
|
|
96
|
+
/** `numeric(p, s)` scale digits. */
|
|
97
|
+
readonly scale?: number;
|
|
98
|
+
/** `time` / `timestamp` timezone flavor. */
|
|
99
|
+
readonly withTimezone?: boolean;
|
|
100
|
+
/** JS mapping selector (`TemporalMode` / `BigIntMode` / `NumericMode` / `PointMode` / `LineMode`). */
|
|
101
|
+
readonly mode?: string;
|
|
102
|
+
/** `interval` field qualifier. */
|
|
103
|
+
readonly fields?: IntervalField;
|
|
104
|
+
/** Allowed string values (`text` / `varchar` / `char` enum). */
|
|
105
|
+
readonly enumValues?: readonly string[];
|
|
106
|
+
}
|
|
15
107
|
|
|
16
108
|
/** Known `$defaultFn` helpers recognized by the Drizzle emitter. */
|
|
17
|
-
export type DefaultFnKind = "id" | "now" | "custom";
|
|
109
|
+
export type DefaultFnKind = "id" | "now" | "nowIso" | "nowDate" | "custom";
|
|
18
110
|
|
|
19
111
|
/** FK action — mirrors Drizzle `UpdateDeleteAction`. */
|
|
20
112
|
export type ReferenceAction = "cascade" | "restrict" | "no action" | "set null" | "set default";
|
|
@@ -62,7 +154,7 @@ export interface SchemaColumnDecl<
|
|
|
62
154
|
readonly $infer?: TData;
|
|
63
155
|
readonly unique: boolean;
|
|
64
156
|
/** Literal `.default(v)` when set. */
|
|
65
|
-
readonly defaultValue?: string | number | boolean | null;
|
|
157
|
+
readonly defaultValue?: string | number | boolean | Date | Buffer | null;
|
|
66
158
|
/** Runtime default applied on insert when the key is missing. */
|
|
67
159
|
readonly defaultFn?: () => unknown;
|
|
68
160
|
/** Emitter hint for `$defaultFn(id|now)`. */
|
|
@@ -71,6 +163,8 @@ export interface SchemaColumnDecl<
|
|
|
71
163
|
readonly tableName?: string;
|
|
72
164
|
/** Foreign key when `.references()` was used. */
|
|
73
165
|
readonly references?: ColumnReference;
|
|
166
|
+
/** Per-type options (`length`, `precision`, `mode`, `enumValues`, …). */
|
|
167
|
+
readonly typeOptions?: FieldTypeOptions;
|
|
74
168
|
/** Optional human description for Console / docs (falls back to the JS key). */
|
|
75
169
|
readonly description?: string;
|
|
76
170
|
/**
|
|
@@ -111,8 +205,22 @@ export interface SchemaTenantScopedDecl {
|
|
|
111
205
|
readonly tenantScoped: false;
|
|
112
206
|
}
|
|
113
207
|
|
|
208
|
+
/**
|
|
209
|
+
* Opt out of the project-wide `store.live` default for a schema table when
|
|
210
|
+
* `oke({ store: { live: true } })` is on. Mirrors {@link SchemaTenantScopedDecl}
|
|
211
|
+
* (`unscoped()`) — a per-table escape hatch for a project-level default flip.
|
|
212
|
+
*/
|
|
213
|
+
export interface SchemaLiveDecl {
|
|
214
|
+
readonly kind: "schema-live";
|
|
215
|
+
readonly live: false;
|
|
216
|
+
}
|
|
217
|
+
|
|
114
218
|
/** Third-arg extra for {@link schemaTable}. */
|
|
115
|
-
export type SchemaTableExtra =
|
|
219
|
+
export type SchemaTableExtra =
|
|
220
|
+
| SchemaPolicyDecl
|
|
221
|
+
| SchemaRlsEnableDecl
|
|
222
|
+
| SchemaTenantScopedDecl
|
|
223
|
+
| SchemaLiveDecl;
|
|
116
224
|
|
|
117
225
|
/** Options for {@link schemaPolicy} / Gate helpers. */
|
|
118
226
|
export interface SchemaPolicyOptions {
|
|
@@ -138,6 +246,8 @@ export interface SchemaTableDecl extends TableHandle {
|
|
|
138
246
|
readonly policies?: readonly SchemaPolicyDecl[];
|
|
139
247
|
/** When `false`, skip fail-loud tenant-policy requirement. */
|
|
140
248
|
readonly tenantScoped?: boolean;
|
|
249
|
+
/** When `false`, opt out of the project-wide `store.live` default. */
|
|
250
|
+
readonly live?: boolean;
|
|
141
251
|
}
|
|
142
252
|
|
|
143
253
|
/**
|
|
@@ -150,8 +260,42 @@ export interface FieldBuilder<TData = unknown, TNotNull extends boolean = false>
|
|
|
150
260
|
primaryKey(): FieldBuilder<TData, true>;
|
|
151
261
|
notNull(): FieldBuilder<TData, true>;
|
|
152
262
|
unique(): FieldBuilder<TData, TNotNull>;
|
|
153
|
-
default(value:
|
|
154
|
-
defaultFn(fn: () =>
|
|
263
|
+
default(value: TData | null): FieldBuilder<TData, TNotNull>;
|
|
264
|
+
defaultFn(fn: () => TData): FieldBuilder<TData, TNotNull>;
|
|
265
|
+
/**
|
|
266
|
+
* Prepared default — a fresh OKID string on every insert. Sugar for
|
|
267
|
+
* `defaultFn(okid)`; the emitter compiles it to Drizzle `$defaultFn(id)`.
|
|
268
|
+
*
|
|
269
|
+
* Only string-typed columns expose it (a non-string builder types the
|
|
270
|
+
* member as `undefined`, so misuse fails TypeScript with a "possibly
|
|
271
|
+
* undefined" error).
|
|
272
|
+
*
|
|
273
|
+
* ```ts
|
|
274
|
+
* id: field.text().primaryKey().okid(),
|
|
275
|
+
* ```
|
|
276
|
+
*/
|
|
277
|
+
okid: TData extends string ? () => FieldBuilder<TData, TNotNull> : undefined;
|
|
278
|
+
/**
|
|
279
|
+
* Prepared default — the current instant, shaped to this column.
|
|
280
|
+
*
|
|
281
|
+
* Sugar for `defaultFn(now | nowIso | nowDate)`; the emitter compiles it to
|
|
282
|
+
* Drizzle `$defaultFn(...)`. The resolved helper follows the column's SQL
|
|
283
|
+
* type and temporal mode:
|
|
284
|
+
*
|
|
285
|
+
* - number columns → epoch-ms (`now`)
|
|
286
|
+
* - `timestamp` / `date` (default `{ mode: "date" }`) → `Date` (`nowDate`)
|
|
287
|
+
* - `timestamp` / `date` with `{ mode: "string" }` → ISO-8601 string
|
|
288
|
+
* (`nowIso`)
|
|
289
|
+
*
|
|
290
|
+
* Non-temporal string columns also resolve to an ISO string.
|
|
291
|
+
*
|
|
292
|
+
* ```ts
|
|
293
|
+
* createdAt: field.integer().notNull().now(), // epoch-ms number
|
|
294
|
+
* bornAt: field.timestamp().notNull().now(), // Date object
|
|
295
|
+
* seenAt: field.timestamp({ mode: "string" }).now(), // ISO string
|
|
296
|
+
* ```
|
|
297
|
+
*/
|
|
298
|
+
now: TData extends number | Date | string ? () => FieldBuilder<TData, TNotNull> : undefined;
|
|
155
299
|
pii(): FieldBuilder<TData, TNotNull>;
|
|
156
300
|
sensitive(): FieldBuilder<TData, TNotNull>;
|
|
157
301
|
retain(duration: string): FieldBuilder<TData, TNotNull>;
|
|
@@ -159,6 +303,14 @@ export interface FieldBuilder<TData = unknown, TNotNull extends boolean = false>
|
|
|
159
303
|
as(sqlName: string): FieldBuilder<TData, TNotNull>;
|
|
160
304
|
/** Optional human description for Console / docs (falls back to the JS key). */
|
|
161
305
|
describe(description: string): FieldBuilder<TData, TNotNull>;
|
|
306
|
+
/**
|
|
307
|
+
* Override the inferred JS value type (mirrors Drizzle `$type<T>()`).
|
|
308
|
+
*
|
|
309
|
+
* ```ts
|
|
310
|
+
* field.uuid().type<UserId & { __brand: "user_id" }>()
|
|
311
|
+
* ```
|
|
312
|
+
*/
|
|
313
|
+
type<T>(): FieldBuilder<T, TNotNull>;
|
|
162
314
|
/**
|
|
163
315
|
* Declare a foreign key to another column (dialect-agnostic).
|
|
164
316
|
*
|
|
@@ -182,13 +334,14 @@ interface FieldState {
|
|
|
182
334
|
readonly primaryKey: boolean;
|
|
183
335
|
readonly notNull: boolean;
|
|
184
336
|
readonly unique: boolean;
|
|
185
|
-
readonly defaultValue?: string | number | boolean | null;
|
|
337
|
+
readonly defaultValue?: string | number | boolean | Date | Buffer | null;
|
|
186
338
|
readonly defaultFn?: () => unknown;
|
|
187
339
|
readonly defaultFnKind?: DefaultFnKind;
|
|
188
340
|
readonly classification?: ColumnClassification;
|
|
189
341
|
readonly sqlName?: string;
|
|
190
342
|
readonly description?: string;
|
|
191
343
|
readonly references?: ColumnReference;
|
|
344
|
+
readonly typeOptions?: FieldTypeOptions;
|
|
192
345
|
}
|
|
193
346
|
|
|
194
347
|
function camelToSnake(key: string): string {
|
|
@@ -201,9 +354,14 @@ function camelToSnake(key: string): string {
|
|
|
201
354
|
function defaultFnKindOf(fn: () => unknown): DefaultFnKind {
|
|
202
355
|
if (fn === idHelper) return "id";
|
|
203
356
|
if (fn === nowHelper) return "now";
|
|
357
|
+
if (fn === nowIsoHelper) return "nowIso";
|
|
358
|
+
if (fn === nowDateHelper) return "nowDate";
|
|
204
359
|
const name = typeof fn.name === "string" ? fn.name : "";
|
|
205
360
|
if (name === "id") return "id";
|
|
361
|
+
if (name === "okid") return "id";
|
|
206
362
|
if (name === "now") return "now";
|
|
363
|
+
if (name === "nowIso") return "nowIso";
|
|
364
|
+
if (name === "nowDate") return "nowDate";
|
|
207
365
|
return "custom";
|
|
208
366
|
}
|
|
209
367
|
|
|
@@ -224,12 +382,42 @@ function createBuilder<TData, TNotNull extends boolean>(
|
|
|
224
382
|
primaryKey: () => next<true>({ primaryKey: true, notNull: true }),
|
|
225
383
|
notNull: () => next<true>({ notNull: true }),
|
|
226
384
|
unique: () => next<TNotNull>({ unique: true }),
|
|
227
|
-
default: (value) => next<TNotNull>({ defaultValue: value }),
|
|
385
|
+
default: (value) => next<TNotNull>({ defaultValue: value as FieldState["defaultValue"] }),
|
|
228
386
|
defaultFn: (fn) =>
|
|
229
387
|
next<TNotNull>({
|
|
230
|
-
defaultFn: fn,
|
|
231
|
-
defaultFnKind: defaultFnKindOf(fn),
|
|
388
|
+
defaultFn: fn as () => unknown,
|
|
389
|
+
defaultFnKind: defaultFnKindOf(fn as () => unknown),
|
|
232
390
|
}),
|
|
391
|
+
okid: (() =>
|
|
392
|
+
next<TNotNull>({
|
|
393
|
+
defaultFn: idHelper,
|
|
394
|
+
defaultFnKind: "id",
|
|
395
|
+
})) as FieldBuilder<TData, TNotNull>["okid"],
|
|
396
|
+
now: (() => {
|
|
397
|
+
const sqlType = state.sqlType;
|
|
398
|
+
const mode = state.typeOptions?.mode;
|
|
399
|
+
// Number-typed columns — epoch-ms default.
|
|
400
|
+
const numericTypes = new Set<FieldSqlType>([
|
|
401
|
+
"smallint",
|
|
402
|
+
"integer",
|
|
403
|
+
"bigint",
|
|
404
|
+
"serial",
|
|
405
|
+
"smallserial",
|
|
406
|
+
"bigserial",
|
|
407
|
+
"numeric",
|
|
408
|
+
"real",
|
|
409
|
+
"doublePrecision",
|
|
410
|
+
]);
|
|
411
|
+
if (numericTypes.has(sqlType)) {
|
|
412
|
+
return next<TNotNull>({ defaultFn: nowHelper, defaultFnKind: "now" });
|
|
413
|
+
}
|
|
414
|
+
// Date-object temporal columns `{ mode: "date" }`.
|
|
415
|
+
if (mode === "date" && (sqlType === "timestamp" || sqlType === "date")) {
|
|
416
|
+
return next<TNotNull>({ defaultFn: nowDateHelper, defaultFnKind: "nowDate" });
|
|
417
|
+
}
|
|
418
|
+
// Everything else — ISO-8601 string default.
|
|
419
|
+
return next<TNotNull>({ defaultFn: nowIsoHelper, defaultFnKind: "nowIso" });
|
|
420
|
+
}) as FieldBuilder<TData, TNotNull>["now"],
|
|
233
421
|
pii: () =>
|
|
234
422
|
next<TNotNull>({ classification: mergeClassification(state.classification, { pii: true }) }),
|
|
235
423
|
sensitive: () =>
|
|
@@ -242,6 +430,7 @@ function createBuilder<TData, TNotNull extends boolean>(
|
|
|
242
430
|
}),
|
|
243
431
|
as: (sqlName) => next<TNotNull>({ sqlName }),
|
|
244
432
|
describe: (description) => next<TNotNull>({ description }),
|
|
433
|
+
type: () => createBuilder<unknown, TNotNull>(state) as FieldBuilder<never, TNotNull> as never,
|
|
245
434
|
references: (ref, actions) =>
|
|
246
435
|
next<TNotNull>({
|
|
247
436
|
references: actions ? { ref, actions } : { ref },
|
|
@@ -262,6 +451,9 @@ function createBuilder<TData, TNotNull extends boolean>(
|
|
|
262
451
|
...(state.classification ? { classification: state.classification } : {}),
|
|
263
452
|
...(state.description !== undefined ? { description: state.description } : {}),
|
|
264
453
|
...(state.references ? { references: state.references } : {}),
|
|
454
|
+
...(state.typeOptions !== undefined && Object.keys(state.typeOptions).length > 0
|
|
455
|
+
? { typeOptions: state.typeOptions }
|
|
456
|
+
: {}),
|
|
265
457
|
getSQL(): never {
|
|
266
458
|
throw new Error(
|
|
267
459
|
`SchemaColumnDecl.getSQL("${sqlName}") is a type bridge — OKE compiles columns directly`,
|
|
@@ -273,38 +465,321 @@ function createBuilder<TData, TNotNull extends boolean>(
|
|
|
273
465
|
}
|
|
274
466
|
|
|
275
467
|
/**
|
|
276
|
-
* Field builders —
|
|
468
|
+
* Field builders — the full Drizzle Postgres column-type surface.
|
|
469
|
+
*
|
|
470
|
+
* Factory names mirror `drizzle-orm/pg-core` exactly (`field.text()` ·
|
|
471
|
+
* `field.timestamp()` · …). Options bags carry the per-type knobs (`length`,
|
|
472
|
+
* `precision`, `mode`, …) and JS inference follows Drizzle's mapping rules —
|
|
473
|
+
* with OKE defaults: temporals infer `Date` unless `{ mode: "string" }`,
|
|
474
|
+
* and the serial family is NOT NULL by SQL physics.
|
|
277
475
|
*/
|
|
278
476
|
export const field = {
|
|
279
|
-
|
|
280
|
-
|
|
281
|
-
|
|
282
|
-
|
|
283
|
-
|
|
284
|
-
|
|
285
|
-
|
|
286
|
-
|
|
287
|
-
|
|
288
|
-
|
|
289
|
-
|
|
290
|
-
|
|
291
|
-
|
|
292
|
-
|
|
293
|
-
|
|
477
|
+
/**
|
|
478
|
+
* Text id column with the default generation id pre-applied.
|
|
479
|
+
*
|
|
480
|
+
* The "default generation id" currently resolves to OKID, so
|
|
481
|
+
* `field.id()` ≡ `field.text().okid()` — it infers `string` and emits a
|
|
482
|
+
* `$defaultFn(id)` to be minted on insert.
|
|
483
|
+
*
|
|
484
|
+
* ```ts
|
|
485
|
+
* id: field.id().primaryKey(),
|
|
486
|
+
* ```
|
|
487
|
+
*/
|
|
488
|
+
id: (() => fieldOf("text", { defaultFn: idHelper, defaultFnKind: "id" })) as FieldApi["id"],
|
|
489
|
+
|
|
490
|
+
/**
|
|
491
|
+
* Text id column explicitly generated with OK ID.
|
|
492
|
+
*
|
|
493
|
+
* Unlike {@link field.id} (which follows whatever the default generation id
|
|
494
|
+
* becomes), this pins OK ID. Today that is the same generator.
|
|
495
|
+
*
|
|
496
|
+
* ```ts
|
|
497
|
+
* id: field.okid().primaryKey(),
|
|
498
|
+
* ```
|
|
499
|
+
*/
|
|
500
|
+
okid: (() => fieldOf("text", { defaultFn: idHelper, defaultFnKind: "id" })) as FieldApi["okid"],
|
|
501
|
+
|
|
502
|
+
/** Variable-length string; `{ enum: [...] }` narrows to a literal union. */
|
|
503
|
+
text: ((options?: { readonly enum?: readonly string[] }) =>
|
|
504
|
+
fieldOf("text", mergeOptions({ enumValues: options?.enum }))) as FieldApi["text"],
|
|
505
|
+
|
|
506
|
+
/** Bounded string; `{ length }` emits `varchar(n)`, `{ enum }` narrows literals. */
|
|
507
|
+
varchar: ((options?: { readonly length?: number; readonly enum?: readonly string[] }) =>
|
|
508
|
+
fieldOf(
|
|
509
|
+
"varchar",
|
|
510
|
+
mergeOptions({ length: options?.length }, { enumValues: options?.enum }),
|
|
511
|
+
)) as FieldApi["varchar"],
|
|
512
|
+
|
|
513
|
+
/** Fixed-length blank-padded string; same knobs as {@link field.varchar}. */
|
|
514
|
+
char: ((options?: { readonly length?: number; readonly enum?: readonly string[] }) =>
|
|
515
|
+
fieldOf(
|
|
516
|
+
"char",
|
|
517
|
+
mergeOptions({ length: options?.length }, { enumValues: options?.enum }),
|
|
518
|
+
)) as FieldApi["char"],
|
|
519
|
+
|
|
520
|
+
boolean: (() => fieldOf("boolean")) as FieldApi["boolean"],
|
|
521
|
+
smallint: (() => fieldOf("smallint")) as FieldApi["smallint"],
|
|
522
|
+
integer: (() => fieldOf("integer")) as FieldApi["integer"],
|
|
523
|
+
|
|
524
|
+
/** `bigint` — defaults to JS `number`; rc.5 requires an explicit mode upstream, ours absorbs it. */
|
|
525
|
+
bigint: ((options?: { readonly mode?: "number" | "bigint" | "string" }) =>
|
|
526
|
+
fieldOf("bigint", mergeOptions({ mode: options?.mode ?? "number" }))) as FieldApi["bigint"],
|
|
527
|
+
|
|
528
|
+
/** Auto-incrementing int4 — NOT NULL by SQL physics. */
|
|
529
|
+
serial: (() => notNullFieldOf("serial")) as FieldApi["serial"],
|
|
530
|
+
/** Auto-incrementing int2 — NOT NULL by SQL physics. */
|
|
531
|
+
smallserial: (() => notNullFieldOf("smallserial")) as FieldApi["smallserial"],
|
|
532
|
+
|
|
533
|
+
/** Auto-incrementing int8 — NOT NULL; defaults to JS `number`. */
|
|
534
|
+
bigserial: ((options?: { readonly mode?: "number" | "bigint" }) =>
|
|
535
|
+
notNullFieldOf(
|
|
536
|
+
"bigserial",
|
|
537
|
+
mergeOptions({ mode: options?.mode ?? "number" }),
|
|
538
|
+
)) as FieldApi["bigserial"],
|
|
539
|
+
|
|
540
|
+
/**
|
|
541
|
+
* Exact-decimal `numeric(p, s)` — infers `string` by default (Drizzle
|
|
542
|
+
* physics: avoids float error); opt into `number` / `bigint` via `mode`.
|
|
543
|
+
*/
|
|
544
|
+
numeric: ((options?: {
|
|
545
|
+
readonly precision?: number;
|
|
546
|
+
readonly scale?: number;
|
|
547
|
+
readonly mode?: "string" | "number" | "bigint";
|
|
548
|
+
}) =>
|
|
549
|
+
fieldOf(
|
|
550
|
+
"numeric",
|
|
551
|
+
mergeOptions(
|
|
552
|
+
{ precision: options?.precision },
|
|
553
|
+
{ scale: options?.scale },
|
|
554
|
+
{ mode: options?.mode },
|
|
555
|
+
),
|
|
556
|
+
)) as FieldApi["numeric"],
|
|
557
|
+
|
|
558
|
+
/** Alias of {@link field.numeric} — mirrors drizzle-orm (`decimal === numeric`). */
|
|
559
|
+
decimal: undefined as unknown as FieldApi["decimal"],
|
|
560
|
+
|
|
561
|
+
/** Single-precision float4 (~6 significant digits). */
|
|
562
|
+
real: (() => fieldOf("real")) as FieldApi["real"],
|
|
563
|
+
/** Double-precision float8 (~15 significant digits). */
|
|
564
|
+
doublePrecision: (() => fieldOf("doublePrecision")) as FieldApi["doublePrecision"],
|
|
565
|
+
|
|
566
|
+
/** Textual JSON — narrow the payload with `field.json<MyShape>()`. */
|
|
567
|
+
json: (<T = unknown>() => fieldOf<T>("json")) as FieldApi["json"],
|
|
568
|
+
/** Binary JSON (decomposed, indexable) — narrow with `field.jsonb<MyShape>()`. */
|
|
569
|
+
jsonb: (<T = unknown>() => fieldOf<T>("jsonb")) as FieldApi["jsonb"],
|
|
570
|
+
|
|
571
|
+
uuid: (() => fieldOf("uuid")) as FieldApi["uuid"],
|
|
572
|
+
|
|
573
|
+
/** Time of day — always infers `string` (Drizzle `'string time'`). */
|
|
574
|
+
time: ((options?: { readonly precision?: FieldPrecision; readonly withTimezone?: boolean }) =>
|
|
575
|
+
fieldOf(
|
|
576
|
+
"time",
|
|
577
|
+
mergeOptions({ precision: options?.precision }, { withTimezone: options?.withTimezone }),
|
|
578
|
+
)) as FieldApi["time"],
|
|
579
|
+
|
|
580
|
+
/**
|
|
581
|
+
* Date + time — infers `Date` objects by default; `{ mode: "string" }`
|
|
582
|
+
* opts into ISO strings. OKE pins the mode explicitly when emitting
|
|
583
|
+
* Drizzle schemas.
|
|
584
|
+
*/
|
|
585
|
+
timestamp: ((options?: {
|
|
586
|
+
readonly mode?: TemporalMode;
|
|
587
|
+
readonly precision?: FieldPrecision;
|
|
588
|
+
readonly withTimezone?: boolean;
|
|
589
|
+
}) =>
|
|
590
|
+
fieldOf(
|
|
591
|
+
"timestamp",
|
|
592
|
+
mergeOptions(
|
|
593
|
+
{ mode: options?.mode ?? "date" },
|
|
594
|
+
{ precision: options?.precision },
|
|
595
|
+
{ withTimezone: options?.withTimezone },
|
|
596
|
+
),
|
|
597
|
+
)) as FieldApi["timestamp"],
|
|
598
|
+
|
|
599
|
+
/** Calendar date — infers `Date` by default; `{ mode: "string" }` opts into ISO `YYYY-MM-DD`. */
|
|
600
|
+
date: ((options?: { readonly mode?: TemporalMode }) =>
|
|
601
|
+
fieldOf("date", mergeOptions({ mode: options?.mode ?? "date" }))) as FieldApi["date"],
|
|
602
|
+
|
|
603
|
+
/** Time span — infers `string`. */
|
|
604
|
+
interval: ((options?: { readonly fields?: IntervalField; readonly precision?: number }) =>
|
|
605
|
+
fieldOf(
|
|
606
|
+
"interval",
|
|
607
|
+
mergeOptions({ fields: options?.fields }, { precision: options?.precision }),
|
|
608
|
+
)) as FieldApi["interval"],
|
|
609
|
+
|
|
610
|
+
/** Geometric point — `[number, number]` tuple by default; `{ mode: "xy" }` for `{ x, y }`. */
|
|
611
|
+
point: ((options?: { readonly mode?: PointMode }) =>
|
|
612
|
+
fieldOf("point", mergeOptions({ mode: options?.mode ?? "tuple" }))) as FieldApi["point"],
|
|
613
|
+
|
|
614
|
+
/** Geometric line — `[n, n, n]` tuple by default; `{ mode: "abc" }` for the equation object. */
|
|
615
|
+
line: ((options?: { readonly mode?: LineMode }) =>
|
|
616
|
+
fieldOf("line", mergeOptions({ mode: options?.mode ?? "tuple" }))) as FieldApi["line"],
|
|
617
|
+
|
|
618
|
+
/** Binary data — infers `Buffer` (a `Uint8Array`). */
|
|
619
|
+
bytea: (() => fieldOf("bytea")) as FieldApi["bytea"],
|
|
620
|
+
|
|
621
|
+
inet: (() => fieldOf("inet")) as FieldApi["inet"],
|
|
622
|
+
cidr: (() => fieldOf("cidr")) as FieldApi["cidr"],
|
|
623
|
+
macaddr: (() => fieldOf("macaddr")) as FieldApi["macaddr"],
|
|
624
|
+
macaddr8: (() => fieldOf("macaddr8")) as FieldApi["macaddr8"],
|
|
625
|
+
} as FieldApi;
|
|
626
|
+
|
|
627
|
+
// `decimal` is the exact alias drizzle-orm ships (`export const decimal = numeric`)
|
|
628
|
+
// — same runtime, same overloads. Assigned post-literal because the literal
|
|
629
|
+
// references `field.numeric` before `field` is initialized.
|
|
630
|
+
(field.decimal as FieldApi["numeric"]) = field.numeric;
|
|
631
|
+
|
|
632
|
+
/** Precise public signatures for {@link field} (overloads + inference). */
|
|
633
|
+
interface FieldApi {
|
|
634
|
+
/** Text id column with the default generation id (currently OK ID) pre-applied. */
|
|
635
|
+
readonly id: () => FieldBuilder<string, false>;
|
|
636
|
+
/** Text id column explicitly generated with OK ID. */
|
|
637
|
+
readonly okid: () => FieldBuilder<string, false>;
|
|
638
|
+
readonly text: {
|
|
639
|
+
(): FieldBuilder<string, false>;
|
|
640
|
+
<const E extends readonly [string, ...string[]]>(options: {
|
|
641
|
+
readonly enum: E;
|
|
642
|
+
}): FieldBuilder<E[number], false>;
|
|
643
|
+
};
|
|
644
|
+
readonly varchar: {
|
|
645
|
+
(): FieldBuilder<string, false>;
|
|
646
|
+
(options: { readonly length: number }): FieldBuilder<string, false>;
|
|
647
|
+
<const E extends readonly [string, ...string[]]>(options: {
|
|
648
|
+
readonly length?: number;
|
|
649
|
+
readonly enum: E;
|
|
650
|
+
}): FieldBuilder<E[number], false>;
|
|
651
|
+
};
|
|
652
|
+
readonly char: FieldApi["varchar"];
|
|
653
|
+
readonly boolean: () => FieldBuilder<boolean, false>;
|
|
654
|
+
readonly smallint: () => FieldBuilder<number, false>;
|
|
655
|
+
readonly integer: () => FieldBuilder<number, false>;
|
|
656
|
+
readonly bigint: {
|
|
657
|
+
(): FieldBuilder<number, false>;
|
|
658
|
+
(options: { readonly mode: "bigint" }): FieldBuilder<bigint, false>;
|
|
659
|
+
(options: { readonly mode: "string" }): FieldBuilder<string, false>;
|
|
660
|
+
};
|
|
661
|
+
readonly serial: () => FieldBuilder<number, true>;
|
|
662
|
+
readonly smallserial: () => FieldBuilder<number, true>;
|
|
663
|
+
readonly bigserial: {
|
|
664
|
+
(): FieldBuilder<number, true>;
|
|
665
|
+
(options: { readonly mode: "bigint" }): FieldBuilder<bigint, true>;
|
|
666
|
+
};
|
|
667
|
+
readonly numeric: {
|
|
668
|
+
(): FieldBuilder<string, false>;
|
|
669
|
+
(options: {
|
|
670
|
+
readonly precision?: number;
|
|
671
|
+
readonly scale?: number;
|
|
672
|
+
readonly mode?: "string";
|
|
673
|
+
}): FieldBuilder<string, false>;
|
|
674
|
+
(options: {
|
|
675
|
+
readonly precision?: number;
|
|
676
|
+
readonly scale?: number;
|
|
677
|
+
readonly mode: "number";
|
|
678
|
+
}): FieldBuilder<number, false>;
|
|
679
|
+
(options: {
|
|
680
|
+
readonly precision?: number;
|
|
681
|
+
readonly scale?: number;
|
|
682
|
+
readonly mode: "bigint";
|
|
683
|
+
}): FieldBuilder<bigint, false>;
|
|
684
|
+
};
|
|
685
|
+
readonly decimal: FieldApi["numeric"];
|
|
686
|
+
readonly real: () => FieldBuilder<number, false>;
|
|
687
|
+
readonly doublePrecision: () => FieldBuilder<number, false>;
|
|
688
|
+
readonly json: <T = unknown>() => FieldBuilder<T, false>;
|
|
689
|
+
readonly jsonb: <T = unknown>() => FieldBuilder<T, false>;
|
|
690
|
+
readonly uuid: () => FieldBuilder<string, false>;
|
|
691
|
+
readonly time: (options?: {
|
|
692
|
+
readonly precision?: FieldPrecision;
|
|
693
|
+
readonly withTimezone?: boolean;
|
|
694
|
+
}) => FieldBuilder<string, false>;
|
|
695
|
+
readonly timestamp: {
|
|
696
|
+
(options?: {
|
|
697
|
+
readonly mode?: "date";
|
|
698
|
+
readonly precision?: FieldPrecision;
|
|
699
|
+
readonly withTimezone?: boolean;
|
|
700
|
+
}): FieldBuilder<Date, false>;
|
|
701
|
+
(options: {
|
|
702
|
+
readonly mode: "string";
|
|
703
|
+
readonly precision?: FieldPrecision;
|
|
704
|
+
readonly withTimezone?: boolean;
|
|
705
|
+
}): FieldBuilder<string, false>;
|
|
706
|
+
};
|
|
707
|
+
readonly date: {
|
|
708
|
+
(options?: { readonly mode?: "date" }): FieldBuilder<Date, false>;
|
|
709
|
+
(options: { readonly mode: "string" }): FieldBuilder<string, false>;
|
|
710
|
+
};
|
|
711
|
+
readonly interval: (options?: {
|
|
712
|
+
readonly fields?: IntervalField;
|
|
713
|
+
readonly precision?: number;
|
|
714
|
+
}) => FieldBuilder<string, false>;
|
|
715
|
+
readonly point: {
|
|
716
|
+
(options?: { readonly mode?: "tuple" }): FieldBuilder<[number, number], false>;
|
|
717
|
+
(options: { readonly mode: "xy" }): FieldBuilder<{ x: number; y: number }, false>;
|
|
718
|
+
};
|
|
719
|
+
readonly line: {
|
|
720
|
+
(options?: { readonly mode?: "tuple" }): FieldBuilder<[number, number, number], false>;
|
|
721
|
+
(options: { readonly mode: "abc" }): FieldBuilder<{ a: number; b: number; c: number }, false>;
|
|
722
|
+
};
|
|
723
|
+
readonly bytea: () => FieldBuilder<Buffer, false>;
|
|
724
|
+
readonly inet: () => FieldBuilder<string, false>;
|
|
725
|
+
readonly cidr: () => FieldBuilder<string, false>;
|
|
726
|
+
readonly macaddr: () => FieldBuilder<string, false>;
|
|
727
|
+
readonly macaddr8: () => FieldBuilder<string, false>;
|
|
728
|
+
}
|
|
729
|
+
|
|
730
|
+
/**
|
|
731
|
+
* Merge defined per-type option entries into a {@link Partial<FieldState>}
|
|
732
|
+
* patch — empty when nothing was set.
|
|
733
|
+
*/
|
|
734
|
+
function mergeOptions(
|
|
735
|
+
...parts: readonly (Partial<FieldTypeOptions> | undefined)[]
|
|
736
|
+
): Partial<FieldState> {
|
|
737
|
+
const merged: Record<string, unknown> = {};
|
|
738
|
+
for (const part of parts) {
|
|
739
|
+
if (!part) continue;
|
|
740
|
+
for (const [key, value] of Object.entries(part)) {
|
|
741
|
+
if (value !== undefined) merged[key] = value;
|
|
742
|
+
}
|
|
743
|
+
}
|
|
744
|
+
return Object.keys(merged).length > 0 ? { typeOptions: merged as FieldTypeOptions } : {};
|
|
745
|
+
}
|
|
746
|
+
|
|
747
|
+
function fieldOf<TData>(
|
|
748
|
+
sqlType: FieldSqlType,
|
|
749
|
+
extra: Partial<FieldState> = {},
|
|
750
|
+
): FieldBuilder<TData, false> {
|
|
751
|
+
return createBuilder<TData, false>({
|
|
752
|
+
sqlType,
|
|
753
|
+
primaryKey: false,
|
|
754
|
+
notNull: false,
|
|
755
|
+
unique: false,
|
|
756
|
+
...extra,
|
|
757
|
+
});
|
|
758
|
+
}
|
|
759
|
+
|
|
760
|
+
function notNullFieldOf<TData>(
|
|
761
|
+
sqlType: FieldSqlType,
|
|
762
|
+
extra: Partial<FieldState> = {},
|
|
763
|
+
): FieldBuilder<TData, true> {
|
|
764
|
+
return createBuilder<TData, true>({
|
|
765
|
+
sqlType,
|
|
766
|
+
primaryKey: false,
|
|
767
|
+
notNull: true,
|
|
768
|
+
unique: false,
|
|
769
|
+
...extra,
|
|
770
|
+
});
|
|
771
|
+
}
|
|
294
772
|
|
|
295
773
|
/**
|
|
296
774
|
* Column map input for {@link schemaTable}.
|
|
297
775
|
*
|
|
298
|
-
*
|
|
299
|
-
*
|
|
300
|
-
*
|
|
776
|
+
* Structural over enumerated: methodful builders are invariant in their type
|
|
777
|
+
* parameters, so an explicit `string | number × false | true` union would
|
|
778
|
+
* reject the widened `field.*` surface (`Date`, `bigint`, tuples, generics).
|
|
779
|
+
* `FieldBuilder<unknown, boolean>` accepts every builder via covariance of
|
|
780
|
+
* the phantom `$infer` property.
|
|
301
781
|
*/
|
|
302
|
-
export type SchemaColumnInput =
|
|
303
|
-
| FieldBuilder<string, false>
|
|
304
|
-
| FieldBuilder<string, true>
|
|
305
|
-
| FieldBuilder<number, false>
|
|
306
|
-
| FieldBuilder<number, true>
|
|
307
|
-
| SchemaColumnDecl;
|
|
782
|
+
export type SchemaColumnInput = FieldBuilder<unknown, boolean> | SchemaColumnDecl;
|
|
308
783
|
|
|
309
784
|
/**
|
|
310
785
|
* Whether a value is a finalized {@link SchemaColumnDecl}.
|
|
@@ -436,6 +911,7 @@ export function schemaTable<C extends Record<string, SchemaColumnInput>>(
|
|
|
436
911
|
const unscoped = extras.some(
|
|
437
912
|
(extra) => extra.kind === "schema-tenant-scoped" && extra.tenantScoped === false,
|
|
438
913
|
);
|
|
914
|
+
const live = extras.some((extra) => extra.kind === "schema-live") ? false : undefined;
|
|
439
915
|
const table = {
|
|
440
916
|
name,
|
|
441
917
|
columns: stamped,
|
|
@@ -443,6 +919,7 @@ export function schemaTable<C extends Record<string, SchemaColumnInput>>(
|
|
|
443
919
|
...(rls ? { rls: true } : {}),
|
|
444
920
|
...(policies.length > 0 ? { policies } : {}),
|
|
445
921
|
...(unscoped ? { tenantScoped: false } : {}),
|
|
922
|
+
...(live !== undefined ? { live: false } : {}),
|
|
446
923
|
};
|
|
447
924
|
// Survive columns named `name` or `kind` (they would otherwise shadow
|
|
448
925
|
// the table discriminant / SQL name).
|
|
@@ -458,6 +935,22 @@ export function schemaRls(): SchemaRlsEnableDecl {
|
|
|
458
935
|
return { kind: "schema-rls" };
|
|
459
936
|
}
|
|
460
937
|
|
|
938
|
+
/**
|
|
939
|
+
* Opt a table out of the project-wide live-by-default posture
|
|
940
|
+
* (`oke({ store: { live: true } })`). The project flag makes every NEW
|
|
941
|
+
* `store.schema.table` declaration live by default; pass
|
|
942
|
+
* `store.schema.live(false)` in the table's extras array to declare a
|
|
943
|
+
* specific table as NOT live. No-op when the project flag is off (that
|
|
944
|
+
* path is already explicit-only).
|
|
945
|
+
*
|
|
946
|
+
* Mirrors `store.schema.unscoped()`, the per-table opt-out for the
|
|
947
|
+
* `gate.auth.tenant` default flip. Changes declaration defaulting only —
|
|
948
|
+
* never the underlying CDC / RLS-per-event cost model.
|
|
949
|
+
*/
|
|
950
|
+
export function schemaLive(live: false): SchemaLiveDecl {
|
|
951
|
+
return { kind: "schema-live", live };
|
|
952
|
+
}
|
|
953
|
+
|
|
461
954
|
/**
|
|
462
955
|
* Raw Drizzle-shaped policy extra.
|
|
463
956
|
*
|
|
@@ -546,18 +1039,22 @@ export function schemaPolicyOwner(
|
|
|
546
1039
|
/**
|
|
547
1040
|
* Scope policy — `oke.has_scope('booking:create')`.
|
|
548
1041
|
*
|
|
549
|
-
*
|
|
1042
|
+
* Accepts a bare scope string or a `gate.scope(...)` declaration; the decl
|
|
1043
|
+
* form keeps the scope string single-sourced (TS catches dangling gate refs).
|
|
1044
|
+
*
|
|
1045
|
+
* @param scope - Scope string or `PolicyGateDecl` from `gate.scope(...)`
|
|
550
1046
|
* @param options - Command (default `insert`)
|
|
551
1047
|
*/
|
|
552
1048
|
export function schemaPolicyScope(
|
|
553
|
-
scope: string,
|
|
1049
|
+
scope: string | PolicyGateDecl,
|
|
554
1050
|
options: Pick<SchemaPolicyOptions, "for" | "as" | "to"> = {},
|
|
555
1051
|
): SchemaPolicyDecl {
|
|
1052
|
+
const resolved = typeof scope === "string" ? scope : (scope.scopes?.[0] ?? scope.name);
|
|
556
1053
|
const command = options.for ?? "insert";
|
|
557
|
-
return schemaPolicy(helperPolicyName("scope",
|
|
1054
|
+
return schemaPolicy(helperPolicyName("scope", resolved, command), {
|
|
558
1055
|
...options,
|
|
559
1056
|
for: command,
|
|
560
|
-
...policyPredicates(command, `oke.has_scope(${sqlStringLiteral(
|
|
1057
|
+
...policyPredicates(command, `oke.has_scope(${sqlStringLiteral(resolved)})`),
|
|
561
1058
|
});
|
|
562
1059
|
}
|
|
563
1060
|
|
|
@@ -806,6 +1303,7 @@ export const schema = {
|
|
|
806
1303
|
rls: schemaRls,
|
|
807
1304
|
policy: schemaPolicyApi,
|
|
808
1305
|
unscoped: schemaUnscoped,
|
|
1306
|
+
live: schemaLive,
|
|
809
1307
|
} as const;
|
|
810
1308
|
|
|
811
1309
|
/**
|