okengine 0.17.2 → 0.18.4
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/AGENTS.md +5 -3
- package/README.md +14 -10
- package/manifest.v1.schema.json +61 -2
- package/package.json +19 -5
- package/site/content/docs/ai/index.mdx +1 -1
- package/site/content/docs/ai/mcp.mdx +10 -1
- package/site/content/docs/ai/skills.mdx +9 -9
- package/site/content/docs/elements/ai.mdx +1 -1
- package/site/content/docs/elements/clock.mdx +1 -1
- package/site/content/docs/elements/flow.mdx +25 -1
- package/site/content/docs/elements/gate.mdx +3 -2
- package/site/content/docs/elements/store.mdx +289 -341
- package/site/content/docs/elements/vault.mdx +5 -5
- package/site/content/docs/get-started/basic-usage.mdx +3 -10
- package/site/content/docs/get-started/index.mdx +1 -1
- package/site/content/docs/get-started/installation.mdx +2 -3
- package/site/content/docs/get-started/introduction.mdx +58 -121
- package/site/content/docs/get-started/meta.json +9 -1
- package/site/content/docs/get-started/project-structure.mdx +4 -11
- package/site/content/docs/get-started/testing.mdx +328 -0
- package/site/content/docs/get-started/why.mdx +93 -71
- package/site/content/docs/index.mdx +44 -11
- package/site/content/docs/meta.json +8 -5
- 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/providers/index.mdx +2 -0
- package/site/content/docs/recipes/index.mdx +2 -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-De7Lc2JC.js} +1 -1
- package/src/console/ui-next/dist/assets/{flows-page-BiZ4-6yQ.js → flows-page-RGy7VEA_.js} +1 -1
- package/src/console/ui-next/dist/assets/{index-C8NRK2R-.js → index-_rgpdVzo.js} +3 -3
- package/src/console/ui-next/dist/assets/{observability-page-CrB6vd1T.js → observability-page-Ds6pcnh-.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-4rHOePuE.js} +1 -1
- package/src/console/ui-next/dist/assets/{vault-page-C6Xxm9SA.js → vault-page-DISPgxLM.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 +5 -2
- 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
|
@@ -15,10 +15,12 @@ import { createSqlStoreHandle } from "./sql-session.ts";
|
|
|
15
15
|
function createTinyPoolClient(size = 2): PostgresClientLike & {
|
|
16
16
|
readonly reserveCalls: { n: number };
|
|
17
17
|
readonly rootBegins: { n: number };
|
|
18
|
+
readonly helperInstalls: { n: number };
|
|
18
19
|
} {
|
|
19
20
|
const slots = Array.from({ length: size }, () => ({ held: false }));
|
|
20
21
|
const reserveCalls = { n: 0 };
|
|
21
22
|
const rootBegins = { n: 0 };
|
|
23
|
+
const helperInstalls = { n: 0 };
|
|
22
24
|
|
|
23
25
|
function checkout(): { held: boolean } {
|
|
24
26
|
const idle = slots.find((slot) => !slot.held);
|
|
@@ -35,15 +37,18 @@ function createTinyPoolClient(size = 2): PostgresClientLike & {
|
|
|
35
37
|
const client: PostgresClientLike & {
|
|
36
38
|
readonly reserveCalls: { n: number };
|
|
37
39
|
readonly rootBegins: { n: number };
|
|
40
|
+
readonly helperInstalls: { n: number };
|
|
38
41
|
} = {
|
|
39
42
|
reserveCalls,
|
|
40
43
|
rootBegins,
|
|
44
|
+
helperInstalls,
|
|
41
45
|
async reserve() {
|
|
42
46
|
reserveCalls.n += 1;
|
|
43
47
|
const slot = checkout();
|
|
44
48
|
slot.held = true;
|
|
45
49
|
return {
|
|
46
50
|
async unsafe(sql) {
|
|
51
|
+
if (/CREATE OR REPLACE FUNCTION oke\.gate\(\)/.test(sql)) helperInstalls.n += 1;
|
|
47
52
|
return rowsFor(sql);
|
|
48
53
|
},
|
|
49
54
|
release() {
|
|
@@ -53,6 +58,7 @@ function createTinyPoolClient(size = 2): PostgresClientLike & {
|
|
|
53
58
|
},
|
|
54
59
|
async unsafe(sql) {
|
|
55
60
|
const slot = checkout();
|
|
61
|
+
if (/CREATE OR REPLACE FUNCTION oke\.gate\(\)/.test(sql)) helperInstalls.n += 1;
|
|
56
62
|
const head = sql.trim().split(/\s+/)[0]?.toUpperCase();
|
|
57
63
|
if (head === "BEGIN") {
|
|
58
64
|
rootBegins.n += 1;
|
|
@@ -114,4 +120,25 @@ describe("sql-session RLS stamp on pooled postgres", () => {
|
|
|
114
120
|
await expect(pool.unsafe("BEGIN")).resolves.toEqual([]);
|
|
115
121
|
await expect(pool.unsafe("SET LOCAL ROLE oke_app")).rejects.toThrow("checkout timeout");
|
|
116
122
|
});
|
|
123
|
+
|
|
124
|
+
test("concurrent identity bags on one connection install helpers exactly once (G8c fix)", async () => {
|
|
125
|
+
const pool = createTinyPoolClient(8);
|
|
126
|
+
const connection = await connectPostgres({ client: pool, role: "primary" });
|
|
127
|
+
// Fresh handles per identity — the pre-fix code installed per handle.
|
|
128
|
+
const mk = (userId: string) =>
|
|
129
|
+
createSqlStoreHandle("sql:app", {
|
|
130
|
+
connection,
|
|
131
|
+
classifications: new Map(),
|
|
132
|
+
routedRole: "primary",
|
|
133
|
+
domainDdl: "off",
|
|
134
|
+
rls: { gate: "member", userId, scopes: ["member"] },
|
|
135
|
+
});
|
|
136
|
+
const sessions = Array.from({ length: 6 }, (_, i) => mk(`user-${i}`));
|
|
137
|
+
|
|
138
|
+
await Promise.all(sessions.map((s) => s.raw(`SELECT id, owner FROM notes`)));
|
|
139
|
+
|
|
140
|
+
// One shared-conn install — concurrent CREATE OR REPLACE would otherwise
|
|
141
|
+
// race pg_proc (`tuple concurrently updated`).
|
|
142
|
+
expect(pool.helperInstalls.n).toBe(1);
|
|
143
|
+
});
|
|
117
144
|
});
|
|
@@ -14,6 +14,7 @@ import {
|
|
|
14
14
|
type RlsIdentity,
|
|
15
15
|
} from "../../drivers/pg-rls.ts";
|
|
16
16
|
import { throwOke } from "../../kernel/errors.ts";
|
|
17
|
+
import { AsyncLocalStorage } from "node:async_hooks";
|
|
17
18
|
import { maskRows, tableFromSql } from "./classify.ts";
|
|
18
19
|
import { isMissingDomainRelationError } from "./missing-relation.ts";
|
|
19
20
|
import {
|
|
@@ -46,6 +47,109 @@ export type InferSelectRow<T> = T extends { readonly $inferSelect: infer R } ? R
|
|
|
46
47
|
/** Serialize stamp frames on a shared connection so concurrent identities cannot interleave. */
|
|
47
48
|
const rlsStampTails = new WeakMap<SqlConnection, Promise<unknown>>();
|
|
48
49
|
|
|
50
|
+
/**
|
|
51
|
+
* Runtime CDC sink — set once by the store runtime at boot. Write hooks call
|
|
52
|
+
* {@link notifySqlCdc} after committed DML; the sink feeds LiveQueryRuntime
|
|
53
|
+
* and (when a driver owns an outbox) durable multi-host delivery.
|
|
54
|
+
*/
|
|
55
|
+
export interface SqlCdcSink {
|
|
56
|
+
/** Deliver one captured change. */
|
|
57
|
+
(event: {
|
|
58
|
+
readonly tableName: string;
|
|
59
|
+
readonly op: "insert" | "update" | "delete";
|
|
60
|
+
readonly before: Record<string, unknown> | null;
|
|
61
|
+
readonly after: Record<string, unknown> | null;
|
|
62
|
+
/** Echoed `X-Oke-Mutation-Id` when the write rode an HTTP request. */
|
|
63
|
+
readonly mutationId?: string;
|
|
64
|
+
}): void | Promise<void>;
|
|
65
|
+
}
|
|
66
|
+
|
|
67
|
+
let sqlCdcSink: SqlCdcSink | null = null;
|
|
68
|
+
|
|
69
|
+
/**
|
|
70
|
+
* Install the process-wide CDC sink. Idempotent — last install wins.
|
|
71
|
+
*
|
|
72
|
+
* @param sink - Store-runtime-owned delivery target
|
|
73
|
+
*/
|
|
74
|
+
export function setSqlCdcSink(sink: SqlCdcSink | null): void {
|
|
75
|
+
sqlCdcSink = sink;
|
|
76
|
+
}
|
|
77
|
+
|
|
78
|
+
/**
|
|
79
|
+
* True when `sql` starts with INSERT/UPDATE/DELETE on a domain table
|
|
80
|
+
* (excludes stamp preludes, txn control, DDL — see {@link isRlsStampExemptSql}).
|
|
81
|
+
*
|
|
82
|
+
* @param sql - Statement text with any placeholder style
|
|
83
|
+
*/
|
|
84
|
+
export function isCdcCandidateSql(sql: string): boolean {
|
|
85
|
+
return (
|
|
86
|
+
/^(insert|update|delete)\b/i.test(sql.trim()) && !/^(delete\s+from\s+oke_)/i.test(sql.trim())
|
|
87
|
+
);
|
|
88
|
+
}
|
|
89
|
+
|
|
90
|
+
/** Fire-and-forget CDC notification (never blocks or fails the write path). */
|
|
91
|
+
function notifySqlCdc(event: {
|
|
92
|
+
readonly tableName: string;
|
|
93
|
+
readonly op: "insert" | "update" | "delete";
|
|
94
|
+
readonly before: Record<string, unknown> | null;
|
|
95
|
+
readonly after: Record<string, unknown> | null;
|
|
96
|
+
}): void {
|
|
97
|
+
if (!sqlCdcSink) return;
|
|
98
|
+
const mutationId = currentCdcMutationId();
|
|
99
|
+
try {
|
|
100
|
+
const result = sqlCdcSink(mutationId !== undefined ? { ...event, mutationId } : event);
|
|
101
|
+
if (result instanceof Promise) void result.catch(() => undefined);
|
|
102
|
+
} catch {
|
|
103
|
+
// Telemetry must never break writes.
|
|
104
|
+
}
|
|
105
|
+
}
|
|
106
|
+
|
|
107
|
+
/**
|
|
108
|
+
* Ambient mutation id for the current async chain — read from the
|
|
109
|
+
* `X-Oke-Mutation-Id` request header by the HTTP layer and echoed onto CDC
|
|
110
|
+
* events so clients can dedupe their own writes (Realtime optimistic race
|
|
111
|
+
* contract).
|
|
112
|
+
*/
|
|
113
|
+
const cdcMutationStorage = new AsyncLocalStorage<{ readonly mutationId: string }>();
|
|
114
|
+
|
|
115
|
+
/**
|
|
116
|
+
* Read the ambient mutation id, or `undefined` outside a stamped request.
|
|
117
|
+
*/
|
|
118
|
+
export function currentCdcMutationId(): string | undefined {
|
|
119
|
+
return cdcMutationStorage.getStore()?.mutationId;
|
|
120
|
+
}
|
|
121
|
+
|
|
122
|
+
/**
|
|
123
|
+
* Run `fn` with `mutationId` as the ambient CDC mutation id.
|
|
124
|
+
*
|
|
125
|
+
* @param mutationId - Client-supplied mutation correlation id
|
|
126
|
+
* @param fn - Flow execution whose store writes carry the id
|
|
127
|
+
*/
|
|
128
|
+
export async function withCdcMutationId<T>(mutationId: string, fn: () => Promise<T>): Promise<T> {
|
|
129
|
+
return cdcMutationStorage.run({ mutationId }, fn);
|
|
130
|
+
}
|
|
131
|
+
|
|
132
|
+
/**
|
|
133
|
+
* One helper-installation per CONNECTION, not per session handle.
|
|
134
|
+
*
|
|
135
|
+
* Concurrent identity bags share `sharedSqlConn`; installing per handle races
|
|
136
|
+
* `CREATE OR REPLACE FUNCTION oke.*` on the same `pg_proc` tuple and fails
|
|
137
|
+
* with `tuple concurrently updated`. The install promise is shared so the
|
|
138
|
+
* first stamped op installs and everyone else awaits it.
|
|
139
|
+
*/
|
|
140
|
+
const rlsHelperInstalls = new WeakMap<SqlConnection, Promise<void>>();
|
|
141
|
+
|
|
142
|
+
async function ensureOkeRlsHelpers(connection: SqlConnection): Promise<void> {
|
|
143
|
+
let installing = rlsHelperInstalls.get(connection);
|
|
144
|
+
if (!installing) {
|
|
145
|
+
installing = installOkeRlsHelpers((sql) => connection.exec(sql));
|
|
146
|
+
rlsHelperInstalls.set(connection, installing);
|
|
147
|
+
// Failed install must not poison the cache — allow a retry.
|
|
148
|
+
void installing.catch(() => rlsHelperInstalls.delete(connection));
|
|
149
|
+
}
|
|
150
|
+
await installing;
|
|
151
|
+
}
|
|
152
|
+
|
|
49
153
|
/**
|
|
50
154
|
* True when SQL must not enter an RLS identity frame (txn control or DDL).
|
|
51
155
|
*
|
|
@@ -405,7 +509,6 @@ export function createSqlStoreHandle(
|
|
|
405
509
|
): SqlStoreHandle {
|
|
406
510
|
const { connection, classifications, revealPii, rls } = options;
|
|
407
511
|
const domainDdl: DomainDdlMode = options.domainDdl ?? "ensure";
|
|
408
|
-
let helpersReady = false;
|
|
409
512
|
|
|
410
513
|
function mask(rows: SqlRow[], table?: string): SqlRow[] {
|
|
411
514
|
return maskRows(rows, { classifications, table, revealPii });
|
|
@@ -448,10 +551,7 @@ export function createSqlStoreHandle(
|
|
|
448
551
|
|
|
449
552
|
async function applyRlsStamp<T>(fn: (conn: SqlConnection) => Promise<T>): Promise<T> {
|
|
450
553
|
if (!rls) return fn(connection);
|
|
451
|
-
|
|
452
|
-
await installOkeRlsHelpers((sql) => connection.exec(sql));
|
|
453
|
-
helpersReady = true;
|
|
454
|
-
}
|
|
554
|
+
await ensureOkeRlsHelpers(connection);
|
|
455
555
|
const frame = async (tx: SqlConnection): Promise<T> => {
|
|
456
556
|
for (const stmt of buildRlsIdentityPreludeSql(rls)) {
|
|
457
557
|
await tx.exec(stmt.sql, stmt.params ?? []);
|
|
@@ -470,13 +570,12 @@ export function createSqlStoreHandle(
|
|
|
470
570
|
if (cols.length === 0) return;
|
|
471
571
|
const name = resolveTableName(table);
|
|
472
572
|
const pk = cols.find((c) => c.primary)?.sqlName;
|
|
473
|
-
//
|
|
474
|
-
//
|
|
475
|
-
|
|
573
|
+
// ddlTypeOf already widens the int family to BIGINT; memory/SQLite accept
|
|
574
|
+
// it too (dynamic typing), so no per-driver remap is needed.
|
|
575
|
+
void connection.driverId;
|
|
476
576
|
const colSql = cols
|
|
477
577
|
.map((c) => {
|
|
478
|
-
const
|
|
479
|
-
const typ = pk && c.sqlName === pk ? `${base} PRIMARY KEY` : base;
|
|
578
|
+
const typ = pk && c.sqlName === pk ? `${c.sqlType} PRIMARY KEY` : c.sqlType;
|
|
480
579
|
return `${quoteIdent(c.sqlName)} ${typ}`;
|
|
481
580
|
})
|
|
482
581
|
.join(", ");
|
|
@@ -513,6 +612,30 @@ export function createSqlStoreHandle(
|
|
|
513
612
|
return compileWhere(where);
|
|
514
613
|
}
|
|
515
614
|
|
|
615
|
+
/**
|
|
616
|
+
* Postgres `row_to_json` images come back as JSON strings on pglite and
|
|
617
|
+
* objects on Bun.SQL — normalize to the JS row mapping (snake→camel).
|
|
618
|
+
*
|
|
619
|
+
* @param table - Table handle for column remapping
|
|
620
|
+
* @param image - Raw RETURNING cell
|
|
621
|
+
*/
|
|
622
|
+
function jsonImageToJs(table: TableHandle | unknown, image: unknown): SqlRow | null {
|
|
623
|
+
if (image === null || image === undefined) return null;
|
|
624
|
+
let obj: Record<string, unknown>;
|
|
625
|
+
if (typeof image === "string") {
|
|
626
|
+
try {
|
|
627
|
+
obj = JSON.parse(image) as Record<string, unknown>;
|
|
628
|
+
} catch {
|
|
629
|
+
return null;
|
|
630
|
+
}
|
|
631
|
+
} else if (typeof image === "object") {
|
|
632
|
+
obj = image as Record<string, unknown>;
|
|
633
|
+
} else {
|
|
634
|
+
return null;
|
|
635
|
+
}
|
|
636
|
+
return toJs(table, [obj])[0] ?? null;
|
|
637
|
+
}
|
|
638
|
+
|
|
516
639
|
/** Accumulated select state across the fluent chain. */
|
|
517
640
|
interface SelectPlan {
|
|
518
641
|
readonly where?: unknown;
|
|
@@ -641,8 +764,14 @@ export function createSqlStoreHandle(
|
|
|
641
764
|
const placeholders = cols.map(() => "?").join(", ");
|
|
642
765
|
const colList = cols.map(quoteIdent).join(", ");
|
|
643
766
|
const params = cols.map((c) => prepared[c]);
|
|
644
|
-
const sql = `INSERT INTO ${quoteIdent(name)} (${colList}) VALUES (${placeholders})
|
|
645
|
-
await
|
|
767
|
+
const sql = `INSERT INTO ${quoteIdent(name)} (${colList}) VALUES (${placeholders}) RETURNING *`;
|
|
768
|
+
const inserted = await query(sql, params);
|
|
769
|
+
notifySqlCdc({
|
|
770
|
+
tableName: name,
|
|
771
|
+
op: "insert",
|
|
772
|
+
before: null,
|
|
773
|
+
after: toJs(table, inserted)[0] ?? null,
|
|
774
|
+
});
|
|
646
775
|
};
|
|
647
776
|
return {
|
|
648
777
|
async returning() {
|
|
@@ -654,6 +783,12 @@ export function createSqlStoreHandle(
|
|
|
654
783
|
const params = cols.map((c) => prepared[c]);
|
|
655
784
|
const sql = `INSERT INTO ${quoteIdent(name)} (${colList}) VALUES (${placeholders}) RETURNING *`;
|
|
656
785
|
const rows = await query(sql, params);
|
|
786
|
+
notifySqlCdc({
|
|
787
|
+
tableName: name,
|
|
788
|
+
op: "insert",
|
|
789
|
+
before: null,
|
|
790
|
+
after: (toJs(table, rows)[0] as Record<string, unknown>) ?? null,
|
|
791
|
+
});
|
|
657
792
|
return mask(toJs(table, rows), name);
|
|
658
793
|
},
|
|
659
794
|
execute: runExecute,
|
|
@@ -680,11 +815,53 @@ export function createSqlStoreHandle(
|
|
|
680
815
|
if (!compiled.clause) {
|
|
681
816
|
throw new Error("update().set().where(): condition required");
|
|
682
817
|
}
|
|
683
|
-
const
|
|
818
|
+
const pk = resolvePkColumn(table);
|
|
819
|
+
const setParams = setEntries.map(([, v]) => v);
|
|
820
|
+
|
|
821
|
+
// Single-statement before capture (postgres / pglite): the
|
|
822
|
+
// `__oke_old` CTE snapshots matched pre-images FOR UPDATE,
|
|
823
|
+
// joins the UPDATE, and RETURNING emits both JSON images in
|
|
824
|
+
// one round-trip. Binding order follows marker appearance:
|
|
825
|
+
// CTE(where) params first, then SET values.
|
|
826
|
+
if (RLS_CONTEXT_DRIVERS.has(connection.driverId)) {
|
|
827
|
+
const sql =
|
|
828
|
+
`WITH __oke_old AS (` +
|
|
829
|
+
`SELECT * FROM ${quoteIdent(name)} WHERE ${compiled.clause} FOR UPDATE` +
|
|
830
|
+
`)` +
|
|
831
|
+
` UPDATE ${quoteIdent(name)} SET ${setSql}` +
|
|
832
|
+
` FROM __oke_old` +
|
|
833
|
+
` WHERE ${quoteIdent(name)}.${quoteIdent(pk)} = __oke_old.${quoteIdent(pk)}` +
|
|
834
|
+
` RETURNING row_to_json(__oke_old) AS __oke_before_data, row_to_json(${quoteIdent(name)}) AS __oke_after_data`;
|
|
835
|
+
const result = await query(sql, [...compiled.params, ...setParams]);
|
|
836
|
+
for (const rawRow of result) {
|
|
837
|
+
notifySqlCdc({
|
|
838
|
+
tableName: name,
|
|
839
|
+
op: "update",
|
|
840
|
+
before: jsonImageToJs(table, rawRow.__oke_before_data),
|
|
841
|
+
after: jsonImageToJs(table, rawRow.__oke_after_data),
|
|
842
|
+
});
|
|
843
|
+
}
|
|
844
|
+
return result.length;
|
|
845
|
+
}
|
|
846
|
+
|
|
847
|
+
// Fallback (memory): explicit pre-read then plain DML — the
|
|
848
|
+
// hook still emits true before-images, just via two statements.
|
|
849
|
+
const prior = await query(
|
|
850
|
+
`SELECT * FROM ${quoteIdent(name)} WHERE ${compiled.clause}`,
|
|
851
|
+
compiled.params,
|
|
852
|
+
);
|
|
684
853
|
const result = await exec(
|
|
685
854
|
`UPDATE ${quoteIdent(name)} SET ${setSql} WHERE ${compiled.clause}`,
|
|
686
|
-
params,
|
|
855
|
+
[...setParams, ...compiled.params],
|
|
687
856
|
);
|
|
857
|
+
for (const beforeRow of toJs(table, prior)) {
|
|
858
|
+
notifySqlCdc({
|
|
859
|
+
tableName: name,
|
|
860
|
+
op: "update",
|
|
861
|
+
before: beforeRow,
|
|
862
|
+
after: { ...beforeRow },
|
|
863
|
+
});
|
|
864
|
+
}
|
|
688
865
|
return result.changes;
|
|
689
866
|
},
|
|
690
867
|
};
|
|
@@ -709,10 +886,19 @@ export function createSqlStoreHandle(
|
|
|
709
886
|
await ensureFromMeta(table);
|
|
710
887
|
const name = resolveTableName(table);
|
|
711
888
|
const pk = resolvePkColumn(table);
|
|
712
|
-
|
|
713
|
-
|
|
714
|
-
|
|
715
|
-
|
|
889
|
+
// DELETE ... RETURNING * gives the full last image in one statement.
|
|
890
|
+
const rows = await query(
|
|
891
|
+
`DELETE FROM ${quoteIdent(name)} WHERE ${quoteIdent(pk)} = ? RETURNING *`,
|
|
892
|
+
[idValue],
|
|
893
|
+
);
|
|
894
|
+
if (rows.length === 0) return false;
|
|
895
|
+
notifySqlCdc({
|
|
896
|
+
tableName: name,
|
|
897
|
+
op: "delete",
|
|
898
|
+
before: toJs(table, rows)[0] ?? null,
|
|
899
|
+
after: null,
|
|
900
|
+
});
|
|
901
|
+
return true;
|
|
716
902
|
})();
|
|
717
903
|
}
|
|
718
904
|
const builder: DeleteBuilder = {
|
|
@@ -723,11 +909,14 @@ export function createSqlStoreHandle(
|
|
|
723
909
|
if (!compiled.clause) {
|
|
724
910
|
throw new Error("delete().where(): condition required");
|
|
725
911
|
}
|
|
726
|
-
const
|
|
727
|
-
`DELETE FROM ${quoteIdent(name)} WHERE ${compiled.clause}
|
|
912
|
+
const rows = await query(
|
|
913
|
+
`DELETE FROM ${quoteIdent(name)} WHERE ${compiled.clause} RETURNING *`,
|
|
728
914
|
compiled.params,
|
|
729
915
|
);
|
|
730
|
-
|
|
916
|
+
for (const beforeRow of toJs(table, rows)) {
|
|
917
|
+
notifySqlCdc({ tableName: name, op: "delete", before: beforeRow, after: null });
|
|
918
|
+
}
|
|
919
|
+
return rows.length;
|
|
731
920
|
},
|
|
732
921
|
};
|
|
733
922
|
return builder;
|
|
@@ -773,7 +962,16 @@ export function createSqlStoreHandle(
|
|
|
773
962
|
const placeholders = cols.map(() => "?").join(", ");
|
|
774
963
|
const colList = cols.map(quoteIdent).join(", ");
|
|
775
964
|
const params = cols.map((c) => prepared[c]);
|
|
776
|
-
|
|
965
|
+
const rows = await query(
|
|
966
|
+
`INSERT INTO ${quoteIdent(name)} (${colList}) VALUES (${placeholders}) RETURNING *`,
|
|
967
|
+
params,
|
|
968
|
+
);
|
|
969
|
+
notifySqlCdc({
|
|
970
|
+
tableName: name,
|
|
971
|
+
op: "insert",
|
|
972
|
+
before: null,
|
|
973
|
+
after: toJs(table, rows)[0] ?? null,
|
|
974
|
+
});
|
|
777
975
|
return { status: "upserted" as const };
|
|
778
976
|
}
|
|
779
977
|
if (upsertOptions?.onExisting !== "update") {
|
|
@@ -784,7 +982,45 @@ export function createSqlStoreHandle(
|
|
|
784
982
|
if (setEntries.length === 0) return { status: "changed" as const };
|
|
785
983
|
const setSql = setEntries.map(([col]) => `${quoteIdent(col)} = ?`).join(", ");
|
|
786
984
|
const params = [...setEntries.map(([, v]) => v), ...compiled.params];
|
|
985
|
+
if (RLS_CONTEXT_DRIVERS.has(connection.driverId)) {
|
|
986
|
+
// Single-statement before/after capture — same shape as update().
|
|
987
|
+
// SET may include the PK (upsert keys) — exclude it from the UPDATE
|
|
988
|
+
// list to keep param order and row identity stable.
|
|
989
|
+
const pkCol = resolvePkColumn(table);
|
|
990
|
+
const setEntriesNoPk = setEntries.filter(([col]) => col !== pkCol);
|
|
991
|
+
const setSqlNoPk = setEntriesNoPk.map(([col]) => `${quoteIdent(col)} = ?`).join(", ");
|
|
992
|
+
const sql =
|
|
993
|
+
`WITH __oke_old AS (` +
|
|
994
|
+
`SELECT * FROM ${quoteIdent(name)} WHERE ${compiled.clause} FOR UPDATE` +
|
|
995
|
+
`)` +
|
|
996
|
+
` UPDATE ${quoteIdent(name)} SET ${setSqlNoPk}` +
|
|
997
|
+
` FROM __oke_old` +
|
|
998
|
+
` WHERE ${quoteIdent(name)}.${quoteIdent(pkCol)} = __oke_old.${quoteIdent(pkCol)}` +
|
|
999
|
+
` RETURNING row_to_json(__oke_old) AS __oke_before_data, row_to_json(${quoteIdent(name)}) AS __oke_after_data`;
|
|
1000
|
+
const result = await query(sql, [...compiled.params, ...setEntriesNoPk.map(([, v]) => v)]);
|
|
1001
|
+
for (const rawRow of result) {
|
|
1002
|
+
notifySqlCdc({
|
|
1003
|
+
tableName: name,
|
|
1004
|
+
op: "update",
|
|
1005
|
+
before: jsonImageToJs(table, rawRow.__oke_before_data),
|
|
1006
|
+
after: jsonImageToJs(table, rawRow.__oke_after_data),
|
|
1007
|
+
});
|
|
1008
|
+
}
|
|
1009
|
+
return { status: "changed" as const };
|
|
1010
|
+
}
|
|
1011
|
+
const prior = await query(
|
|
1012
|
+
`SELECT * FROM ${quoteIdent(name)} WHERE ${compiled.clause}`,
|
|
1013
|
+
compiled.params,
|
|
1014
|
+
);
|
|
787
1015
|
await exec(`UPDATE ${quoteIdent(name)} SET ${setSql} WHERE ${compiled.clause}`, params);
|
|
1016
|
+
for (const beforeRow of toJs(table, prior)) {
|
|
1017
|
+
notifySqlCdc({
|
|
1018
|
+
tableName: name,
|
|
1019
|
+
op: "update",
|
|
1020
|
+
before: beforeRow,
|
|
1021
|
+
after: { ...beforeRow, ...values },
|
|
1022
|
+
});
|
|
1023
|
+
}
|
|
788
1024
|
return { status: "changed" as const };
|
|
789
1025
|
},
|
|
790
1026
|
|
|
@@ -796,6 +1032,38 @@ export function createSqlStoreHandle(
|
|
|
796
1032
|
const colMeta = cols.find((c) => c.key === column || c.sqlName === column);
|
|
797
1033
|
const sqlCol = colMeta?.sqlName ?? column;
|
|
798
1034
|
const col = quoteIdent(sqlCol);
|
|
1035
|
+
if (RLS_CONTEXT_DRIVERS.has(connection.driverId)) {
|
|
1036
|
+
// Single statement: the CTE locks the row and carries only the PK
|
|
1037
|
+
// (projecting more would make the bare SET target ambiguous against
|
|
1038
|
+
// __oke_old's columns), UPDATE returns the after-image and new value
|
|
1039
|
+
// in one round-trip.
|
|
1040
|
+
const sql =
|
|
1041
|
+
`WITH __oke_old AS (` +
|
|
1042
|
+
`SELECT ${quoteIdent(pk)} FROM ${quoteIdent(name)} WHERE ${quoteIdent(pk)} = ? FOR UPDATE` +
|
|
1043
|
+
`)` +
|
|
1044
|
+
` UPDATE ${quoteIdent(name)} SET ${col} = ${col} + ?` +
|
|
1045
|
+
` FROM __oke_old` +
|
|
1046
|
+
` WHERE ${quoteIdent(name)}.${quoteIdent(pk)} = __oke_old.${quoteIdent(pk)}` +
|
|
1047
|
+
` RETURNING row_to_json(${quoteIdent(name)}) AS __oke_after_data, ${col}`;
|
|
1048
|
+
const result = await query(sql, [idValue, by]);
|
|
1049
|
+
const row = result[0];
|
|
1050
|
+
if (!row) {
|
|
1051
|
+
throw new Error(`increment(): no row with ${pk} ${JSON.stringify(idValue)} in ${name}`);
|
|
1052
|
+
}
|
|
1053
|
+
notifySqlCdc({
|
|
1054
|
+
tableName: name,
|
|
1055
|
+
op: "update",
|
|
1056
|
+
// No full pre-image in this single statement — the CTE projects
|
|
1057
|
+
// just the PK. The runtime classifies a numeric-only mutation as
|
|
1058
|
+
// an upsert event; revocation never needs this before-image.
|
|
1059
|
+
before: null,
|
|
1060
|
+
after: jsonImageToJs(table, row.__oke_after_data),
|
|
1061
|
+
});
|
|
1062
|
+
return asNumber(row[sqlCol], sqlCol);
|
|
1063
|
+
}
|
|
1064
|
+
const prior = await query(`SELECT * FROM ${quoteIdent(name)} WHERE ${quoteIdent(pk)} = ?`, [
|
|
1065
|
+
idValue,
|
|
1066
|
+
]);
|
|
799
1067
|
const rows = await query(
|
|
800
1068
|
`UPDATE ${quoteIdent(name)} SET ${col} = ${col} + ? WHERE ${quoteIdent(pk)} = ? RETURNING ${col}`,
|
|
801
1069
|
[by, idValue],
|
|
@@ -804,6 +1072,12 @@ export function createSqlStoreHandle(
|
|
|
804
1072
|
if (!row) {
|
|
805
1073
|
throw new Error(`increment(): no row with ${pk} ${JSON.stringify(idValue)} in ${name}`);
|
|
806
1074
|
}
|
|
1075
|
+
notifySqlCdc({
|
|
1076
|
+
tableName: name,
|
|
1077
|
+
op: "update",
|
|
1078
|
+
before: toJs(table, prior)[0] ?? null,
|
|
1079
|
+
after: null,
|
|
1080
|
+
});
|
|
807
1081
|
return asNumber(row[sqlCol], sqlCol);
|
|
808
1082
|
},
|
|
809
1083
|
|
|
@@ -863,21 +1137,9 @@ export function createSqlStoreHandle(
|
|
|
863
1137
|
async ensureTable(table: TableHandle) {
|
|
864
1138
|
const cols = Object.values(table.columns);
|
|
865
1139
|
const pk = resolvePkColumn(table);
|
|
866
|
-
const intType =
|
|
867
|
-
connection.driverId === "postgres" || connection.driverId === "pglite"
|
|
868
|
-
? "BIGINT"
|
|
869
|
-
: "INTEGER";
|
|
870
1140
|
const colSql = cols
|
|
871
1141
|
.map((c) => {
|
|
872
|
-
const typ =
|
|
873
|
-
c.name === pk
|
|
874
|
-
? "TEXT PRIMARY KEY"
|
|
875
|
-
: c.name === "clicks" ||
|
|
876
|
-
c.name === "qty" ||
|
|
877
|
-
c.name === "createdAt" ||
|
|
878
|
-
c.name === "created_at"
|
|
879
|
-
? intType
|
|
880
|
-
: "TEXT";
|
|
1142
|
+
const typ = c.name === pk ? "TEXT PRIMARY KEY" : "TEXT";
|
|
881
1143
|
return `${quoteIdent(c.name)} ${typ}`;
|
|
882
1144
|
})
|
|
883
1145
|
.join(", ");
|
|
@@ -6,6 +6,7 @@
|
|
|
6
6
|
*/
|
|
7
7
|
|
|
8
8
|
import type { ColumnClassification } from "../../manifest/types.ts";
|
|
9
|
+
import { okid } from "../../okid.ts";
|
|
9
10
|
|
|
10
11
|
/** Column descriptor with optional classification. */
|
|
11
12
|
export interface ColumnDef {
|
|
@@ -59,8 +60,22 @@ export interface ResolvedColumn {
|
|
|
59
60
|
readonly sqlName: string;
|
|
60
61
|
/** Whether this column is the primary key. */
|
|
61
62
|
readonly primary: boolean;
|
|
62
|
-
/**
|
|
63
|
-
readonly sqlType:
|
|
63
|
+
/** Unparameterized Postgres DDL type for auto-DDL (`CREATE TABLE`). */
|
|
64
|
+
readonly sqlType:
|
|
65
|
+
| "TEXT"
|
|
66
|
+
| "INTEGER"
|
|
67
|
+
| "BIGINT"
|
|
68
|
+
| "REAL"
|
|
69
|
+
| "DOUBLE PRECISION"
|
|
70
|
+
| "NUMERIC"
|
|
71
|
+
| "BOOLEAN"
|
|
72
|
+
| "JSONB"
|
|
73
|
+
| "BYTEA"
|
|
74
|
+
| "POINT"
|
|
75
|
+
| "LINE"
|
|
76
|
+
| "TIMESTAMP"
|
|
77
|
+
| "DATE"
|
|
78
|
+
| "BLOB";
|
|
64
79
|
/** Optional `$defaultFn` from Drizzle. */
|
|
65
80
|
readonly defaultFn?: () => unknown;
|
|
66
81
|
}
|
|
@@ -119,6 +134,57 @@ export function isDrizzleTable(t: Record<string, unknown>): boolean {
|
|
|
119
134
|
return ("$" in t || "_" in t) && !("columns" in t);
|
|
120
135
|
}
|
|
121
136
|
|
|
137
|
+
/**
|
|
138
|
+
* Unparameterized Postgres DDL type for an abstract {@link FieldSqlType}.
|
|
139
|
+
*
|
|
140
|
+
* Mirrors each type's drizzle `getSQLType()` output minus parameters — the
|
|
141
|
+
* memory driver's `CREATE TABLE` parser comma-splits column defs, so
|
|
142
|
+
* `numeric(p, s)` parens are never emitted here. Precision/length remain
|
|
143
|
+
* migration-time concerns via drizzle-kit reading the emitted schema.
|
|
144
|
+
*
|
|
145
|
+
* @param sqlType - Declared SQL type primitive
|
|
146
|
+
*/
|
|
147
|
+
export function ddlTypeOf(sqlType: string): ResolvedColumn["sqlType"] {
|
|
148
|
+
switch (sqlType) {
|
|
149
|
+
case "smallint":
|
|
150
|
+
case "smallserial":
|
|
151
|
+
return "INTEGER";
|
|
152
|
+
case "integer":
|
|
153
|
+
case "bigint":
|
|
154
|
+
case "serial":
|
|
155
|
+
case "bigserial":
|
|
156
|
+
// Abstract integer (ms clocks) and int8 family widen to BIGINT.
|
|
157
|
+
return "BIGINT";
|
|
158
|
+
case "real":
|
|
159
|
+
return "REAL";
|
|
160
|
+
case "doublePrecision":
|
|
161
|
+
return "DOUBLE PRECISION";
|
|
162
|
+
case "numeric":
|
|
163
|
+
return "NUMERIC";
|
|
164
|
+
case "boolean":
|
|
165
|
+
return "BOOLEAN";
|
|
166
|
+
case "json":
|
|
167
|
+
case "jsonb":
|
|
168
|
+
// jsonb chosen for indexability; auto-DDL is a dev-only convenience.
|
|
169
|
+
return "JSONB";
|
|
170
|
+
case "bytea":
|
|
171
|
+
return "BYTEA";
|
|
172
|
+
case "point":
|
|
173
|
+
return "POINT";
|
|
174
|
+
case "line":
|
|
175
|
+
return "LINE";
|
|
176
|
+
case "timestamp":
|
|
177
|
+
return "TIMESTAMP";
|
|
178
|
+
case "date":
|
|
179
|
+
return "DATE";
|
|
180
|
+
case "BLOB":
|
|
181
|
+
return "BLOB";
|
|
182
|
+
default:
|
|
183
|
+
// text · varchar · char · uuid · time · interval · inet/cidr/mac* — text-shaped.
|
|
184
|
+
return "TEXT";
|
|
185
|
+
}
|
|
186
|
+
}
|
|
187
|
+
|
|
122
188
|
/**
|
|
123
189
|
* Introspect columns from a TableHandle or Drizzle table.
|
|
124
190
|
*
|
|
@@ -144,19 +210,11 @@ export function resolveColumns(table: unknown): ResolvedColumn[] {
|
|
|
144
210
|
typeof col.primaryKey === "boolean"
|
|
145
211
|
? col.primaryKey
|
|
146
212
|
: col.name === "id" || key === "id" || sqlName === "id";
|
|
147
|
-
const sqlType =
|
|
148
|
-
col.sqlType === "integer" || col.sqlType === "INTEGER"
|
|
149
|
-
? ("INTEGER" as const)
|
|
150
|
-
: col.sqlType === "real" || col.sqlType === "REAL"
|
|
151
|
-
? ("REAL" as const)
|
|
152
|
-
: col.sqlType === "blob" || col.sqlType === "BLOB"
|
|
153
|
-
? ("BLOB" as const)
|
|
154
|
-
: ("TEXT" as const);
|
|
155
213
|
return {
|
|
156
214
|
key,
|
|
157
215
|
sqlName,
|
|
158
216
|
primary,
|
|
159
|
-
sqlType,
|
|
217
|
+
sqlType: ddlTypeOf(col.sqlType ?? ""),
|
|
160
218
|
...(typeof col.defaultFn === "function" ? { defaultFn: col.defaultFn } : {}),
|
|
161
219
|
};
|
|
162
220
|
});
|
|
@@ -219,13 +277,19 @@ function drizzleSqlType(
|
|
|
219
277
|
): ResolvedColumn["sqlType"] {
|
|
220
278
|
const ct = (columnType ?? "").toLowerCase();
|
|
221
279
|
const dt = (dataType ?? "").toLowerCase();
|
|
222
|
-
if (ct.includes("
|
|
223
|
-
|
|
224
|
-
|
|
225
|
-
if (
|
|
226
|
-
|
|
227
|
-
|
|
228
|
-
if (ct.includes("
|
|
280
|
+
if (ct.includes("bigint") || dt.includes("bigint")) return "BIGINT";
|
|
281
|
+
if (ct.includes("smallint")) return "INTEGER";
|
|
282
|
+
if (ct.includes("int") || dt.includes("int") || dt.includes("number")) return "INTEGER";
|
|
283
|
+
if (dt.includes("boolean") || ct.includes("bool")) return "BOOLEAN";
|
|
284
|
+
if (ct.includes("jsonb")) return "JSONB";
|
|
285
|
+
if (ct.includes("json")) return "JSONB";
|
|
286
|
+
if (ct.includes("bytea") || dt.includes("buffer")) return "BYTEA";
|
|
287
|
+
if (ct.includes("double precision")) return "DOUBLE PRECISION";
|
|
288
|
+
if (ct.includes("real") || dt.includes("float")) return "REAL";
|
|
289
|
+
if (ct.includes("numeric")) return "NUMERIC";
|
|
290
|
+
if (ct.includes("timestamp")) return "TIMESTAMP";
|
|
291
|
+
if (ct.startsWith("date")) return "DATE";
|
|
292
|
+
if (ct.includes("blob")) return "BLOB";
|
|
229
293
|
return "TEXT";
|
|
230
294
|
}
|
|
231
295
|
|
|
@@ -330,15 +394,32 @@ export function classificationsFromTable(table: TableHandle): Record<string, Col
|
|
|
330
394
|
}
|
|
331
395
|
|
|
332
396
|
/**
|
|
333
|
-
* Generate a unique id (
|
|
397
|
+
* Generate a unique id (21-char OKID, 126-bit entropy). Suitable for
|
|
398
|
+
* `$defaultFn(id)` in schemas.
|
|
334
399
|
*/
|
|
335
400
|
export function id(): string {
|
|
336
|
-
return
|
|
401
|
+
return okid();
|
|
337
402
|
}
|
|
338
403
|
|
|
339
404
|
/**
|
|
340
|
-
* Current epoch-ms. Suitable for `$defaultFn(now)` in schemas.
|
|
405
|
+
* Current epoch-ms. Suitable for `$defaultFn(now)` in schemas (number columns).
|
|
341
406
|
*/
|
|
342
407
|
export function now(): number {
|
|
343
408
|
return Date.now();
|
|
344
409
|
}
|
|
410
|
+
|
|
411
|
+
/**
|
|
412
|
+
* Current instant as an ISO-8601 string. The resolved default for `.now()` on
|
|
413
|
+
* string-typed temporal columns (`timestamp` / `date` in string mode).
|
|
414
|
+
*/
|
|
415
|
+
export function nowIso(): string {
|
|
416
|
+
return new Date().toISOString();
|
|
417
|
+
}
|
|
418
|
+
|
|
419
|
+
/**
|
|
420
|
+
* Current instant as a `Date`. The resolved default for `.now()` on temporal
|
|
421
|
+
* columns declared with `{ mode: "date" }`.
|
|
422
|
+
*/
|
|
423
|
+
export function nowDate(): Date {
|
|
424
|
+
return new Date();
|
|
425
|
+
}
|