okengine 0.19.9 → 0.21.0
This diff represents the content of publicly available package versions that have been released to one of the supported registries. The information contained in this diff is provided for informational purposes only and reflects changes between package versions as they appear in their respective public registries.
- package/AGENTS.md +1 -1
- package/package.json +2 -1
- package/site/content/docs/client/auth.mdx +1 -2
- package/site/content/docs/client/calling.mdx +125 -37
- package/site/content/docs/client/index.mdx +7 -7
- package/site/content/docs/client/react.mdx +5 -0
- package/site/content/docs/elements/channel/email.mdx +25 -36
- package/site/content/docs/elements/channel/index.mdx +88 -46
- package/site/content/docs/elements/channel/push.mdx +7 -9
- package/site/content/docs/elements/channel/sms.mdx +6 -4
- package/site/content/docs/elements/channel/whatsapp.mdx +11 -9
- package/site/content/docs/elements/clock/index.mdx +14 -25
- package/site/content/docs/elements/flow/http.mdx +42 -23
- package/site/content/docs/elements/flow/index.mdx +41 -30
- package/site/content/docs/elements/flow/routing.mdx +166 -122
- package/site/content/docs/elements/gate/rls.mdx +2 -2
- package/site/content/docs/elements/gate/tenancy.mdx +1 -1
- package/site/content/docs/elements/store/files.mdx +11 -5
- package/site/content/docs/elements/store/index.mdx +13 -6
- package/site/content/docs/elements/store/kv.mdx +8 -2
- package/site/content/docs/elements/store/search.mdx +5 -5
- package/site/content/docs/elements/store/sql.mdx +100 -39
- package/site/content/docs/elements/vault/index.mdx +14 -13
- package/site/content/docs/elements/vault/secrets.mdx +6 -3
- package/site/content/docs/index.mdx +1 -1
- package/site/content/docs/plugins/magic-link.mdx +4 -3
- package/site/content/docs/plugins/otp.mdx +4 -3
- package/site/content/docs/plugins/two-factor.mdx +4 -0
- package/site/content/docs/providers/index.mdx +1 -1
- package/site/content/docs/recipes/index.mdx +1 -1
- package/site/content/docs/recipes/rustfs.mdx +1 -1
- package/site/content/docs/reference/cli.mdx +7 -4
- package/site/content/docs/reference/configuration.mdx +8 -8
- package/site/content/docs/reference/errors.mdx +229 -55
- package/site/content/docs/reference/fx.mdx +22 -9
- package/site/content/docs/reference/i18n.mdx +4 -4
- package/site/content/docs/reference/plugins.mdx +4 -4
- package/site/content/docs/understand/the-architecture.mdx +2 -2
- package/site/content/docs/understand/try-it.mdx +759 -24
- package/src/cli/ai-setup/ai-setup.test.ts +40 -0
- package/src/cli/ai-setup/apply.ts +28 -46
- package/src/cli/build.test.ts +3 -3
- package/src/cli/build.ts +5 -5
- package/src/cli/db-auto-push.test.ts +11 -0
- package/src/cli/db-auto-push.ts +6 -2
- package/src/cli/db.test.ts +1 -1
- package/src/cli/db.ts +6 -6
- package/src/cli/dev-app-runner.ts +2 -1
- package/src/cli/dev-db-push.test.ts +6 -2
- package/src/cli/dev-schema-sync.ts +1 -1
- package/src/cli/dev.test.ts +10 -7
- package/src/cli/dev.ts +13 -11
- package/src/cli/ensure-drizzle-config.ts +4 -3
- package/src/cli/start.ts +2 -1
- package/src/client/create.ts +3 -3
- package/src/client/explain.test.ts +252 -0
- package/src/client/explain.ts +272 -0
- package/src/client/live.test.ts +44 -0
- package/src/client/notes-contract.test.ts +10 -0
- package/src/client/sse.ts +6 -2
- package/src/client/transport.test.ts +67 -0
- package/src/client/transport.ts +24 -40
- package/src/client/types.ts +17 -6
- package/src/client-react/browser.test.ts +23 -0
- package/src/client-react/live-resource.ts +6 -2
- package/src/client-react/use-live-query.ts +1 -1
- package/src/compiler/flow-path.test.ts +1 -0
- package/src/compiler/flow-path.ts +1 -1
- package/src/compiler/generate-adopt.test.ts +55 -1
- package/src/compiler/generate-adopt.ts +111 -21
- package/src/compiler/response.ts +17 -27
- package/src/config/index.ts +6 -4
- package/src/console/server/invoke-user-flow.test.ts +8 -2
- package/src/console/server/invoke-user-flow.ts +12 -18
- package/src/console/server/security.gate.test.ts +1 -1
- package/src/console/ui-next/dist/assets/{access-page-BoC83Ubl.js → access-page-tIbsiphz.js} +1 -1
- package/src/console/ui-next/dist/assets/{agent-disclosure-CKjAEOqA.js → agent-disclosure-CSKumwS2.js} +1 -1
- package/src/console/ui-next/dist/assets/{cache-glyph-Ceaq9pYh.js → cache-glyph-BCC-DxKT.js} +1 -1
- package/src/console/ui-next/dist/assets/{call-pii-button-C4lmY7ck.js → call-pii-button-jOmYISdc.js} +1 -1
- package/src/console/ui-next/dist/assets/{collapsible-82y257sL.js → collapsible-DC2xNaAb.js} +1 -1
- package/src/console/ui-next/dist/assets/{duration-tone-W63jaMZ8.js → duration-tone-CwoV56jn.js} +1 -1
- package/src/console/ui-next/dist/assets/{flows-page-KFTFj2rK.js → flows-page-gT1lsWQK.js} +1 -1
- package/src/console/ui-next/dist/assets/{highlighted-json-yE9zNTqC.js → highlighted-json-C2GZEJNI.js} +1 -1
- package/src/console/ui-next/dist/assets/{http-method-CwFeFroN.js → http-method-BdYjcIrD.js} +1 -1
- package/src/console/ui-next/dist/assets/{index-r7xXt_VV.js → index-Cul17AcV.js} +3 -3
- package/src/console/ui-next/dist/assets/{observability-page-BDiliMiC.js → observability-page-oY9vdYBk.js} +1 -1
- package/src/console/ui-next/dist/assets/{replica-lag-DYDzWUFT.js → replica-lag-C4QdAF7J.js} +1 -1
- package/src/console/ui-next/dist/assets/{request-meta-CzrOfgiz.js → request-meta-C43DHyld.js} +1 -1
- package/src/console/ui-next/dist/assets/{store-page-CZC2cwaw.js → store-page-CL0D9dOq.js} +1 -1
- package/src/console/ui-next/dist/assets/{trace-detail-sheet-DgeeejW7.js → trace-detail-sheet-Dcpo4Us_.js} +1 -1
- package/src/console/ui-next/dist/assets/{tree-expand-toggle-CzGIyOPY.js → tree-expand-toggle-CKJTuv43.js} +1 -1
- package/src/console/ui-next/dist/assets/{units-page-DBiDCLIB.js → units-page-1PlT19ft.js} +1 -1
- package/src/console/ui-next/dist/assets/{vault-page-CcHsthPe.js → vault-page-BwZ9YjTW.js} +1 -1
- package/src/console/ui-next/dist/index.html +1 -1
- package/src/docker/docker.test.ts +3 -3
- package/src/docker/images-config.test.ts +4 -4
- package/src/docker/stack-id.test.ts +1 -1
- package/src/drivers/clock-postgres.test.ts +10 -2
- package/src/drivers/clock-postgres.ts +18 -2
- package/src/drivers/vault-driver-removal.test.ts +2 -2
- package/src/elements/channel/declare.ts +66 -3
- package/src/elements/channel/runtime.ts +9 -11
- package/src/elements/channel.test.ts +42 -0
- package/src/elements/channel.ts +4 -2
- package/src/elements/clock/reconcile.ts +45 -24
- package/src/elements/clock.test.ts +33 -0
- package/src/elements/store/emit-drizzle.ts +285 -65
- package/src/elements/store/files-errors.test.ts +149 -0
- package/src/elements/store/files-errors.ts +189 -0
- package/src/elements/store/kv-errors.test.ts +98 -0
- package/src/elements/store/kv-errors.ts +139 -0
- package/src/elements/store/load-plugin-tables.ts +1 -1
- package/src/elements/store/prepare-row.test.ts +57 -4
- package/src/elements/store/resource.ts +11 -7
- package/src/elements/store/runtime.ts +18 -13
- package/src/elements/store/schema-decl.test.ts +178 -0
- package/src/elements/store/sql-errors.test.ts +197 -0
- package/src/elements/store/sql-errors.ts +294 -0
- package/src/elements/store/sql-session.test.ts +52 -0
- package/src/elements/store/sql-session.ts +50 -2
- package/src/elements/store/store-errors.ts +47 -0
- package/src/elements/store/table.ts +8 -6
- package/src/http.ts +9 -1
- package/src/i18n/catalogs/ar.ts +18 -0
- package/src/i18n/catalogs/en.ts +18 -0
- package/src/index.ts +9 -1
- package/src/kernel/adopt-barrel-fresh.test.ts +1 -1
- package/src/kernel/app.ts +40 -34
- package/src/kernel/auto-registry.test.ts +26 -1
- package/src/kernel/boot.ts +2 -2
- package/src/kernel/boundary-contract.ts +6 -1
- package/src/kernel/builtin-errors.test.ts +117 -0
- package/src/kernel/builtin-errors.ts +129 -0
- package/src/kernel/call.test.ts +182 -0
- package/src/kernel/errors-vault.ts +16 -0
- package/src/kernel/errors.registry.test.ts +7 -0
- package/src/kernel/errors.ts +97 -24
- package/src/kernel/fail-helpers.ts +34 -0
- package/src/kernel/flow-units.ts +3 -3
- package/src/kernel/fx.test.ts +8 -0
- package/src/kernel/fx.ts +24 -8
- package/src/kernel/index.ts +12 -1
- package/src/kernel/mutation-id.ts +8 -0
- package/src/kernel/plugin.ts +4 -3
- package/src/kernel/project-out.test.ts +176 -0
- package/src/kernel/project-out.ts +91 -0
- package/src/kernel/realtime-bind.ts +2 -3
- package/src/kernel/router/linear.ts +12 -6
- package/src/kernel/router.test.ts +13 -0
- package/src/plugins/magic-link.ts +25 -24
- package/src/plugins/otp.ts +35 -24
- package/src/plugins/two-factor.ts +15 -0
- package/src/runs/duckdb.test.ts +2 -2
- package/src/runtime/dev-request-log.ts +29 -11
- package/src/term.test.ts +76 -0
- package/src/term.ts +166 -3
|
@@ -75,6 +75,14 @@ export interface CronStore {
|
|
|
75
75
|
* @param leaseMs - Lease TTL
|
|
76
76
|
*/
|
|
77
77
|
acquireLease(name: string, instanceId: string, now: number, leaseMs: number): Promise<boolean>;
|
|
78
|
+
/**
|
|
79
|
+
* Atomic read-modify-write. Reconcile uses this so a concurrent
|
|
80
|
+
* {@link CronStore.acquireLease} cannot be clobbered by a stale `get` + `put`.
|
|
81
|
+
*
|
|
82
|
+
* @param name - Cron name
|
|
83
|
+
* @param fn - Receives the current row (or `undefined`); return the next row
|
|
84
|
+
*/
|
|
85
|
+
update(name: string, fn: (prev: CronRow | undefined) => CronRow): Promise<void>;
|
|
78
86
|
}
|
|
79
87
|
|
|
80
88
|
/** Result of one reconciliation pass. */
|
|
@@ -200,30 +208,29 @@ export async function orphanPerTenantCronRows(
|
|
|
200
208
|
}
|
|
201
209
|
|
|
202
210
|
async function putActiveRow(store: CronStore, decl: ClockDecl, name: string): Promise<void> {
|
|
203
|
-
|
|
204
|
-
|
|
205
|
-
|
|
206
|
-
|
|
207
|
-
|
|
208
|
-
|
|
209
|
-
|
|
210
|
-
|
|
211
|
-
|
|
212
|
-
|
|
213
|
-
|
|
214
|
-
|
|
215
|
-
|
|
216
|
-
|
|
217
|
-
|
|
218
|
-
|
|
219
|
-
|
|
220
|
-
|
|
221
|
-
|
|
222
|
-
|
|
223
|
-
|
|
224
|
-
|
|
225
|
-
};
|
|
226
|
-
await store.put(row);
|
|
211
|
+
await store.update(name, (prev) => {
|
|
212
|
+
const overrideCron =
|
|
213
|
+
decl.overridable && prev?.overrideCron !== undefined ? prev.overrideCron : undefined;
|
|
214
|
+
const overrideEvery =
|
|
215
|
+
decl.overridable && prev?.overrideEvery !== undefined ? prev.overrideEvery : undefined;
|
|
216
|
+
return {
|
|
217
|
+
name,
|
|
218
|
+
declaredCron: decl.cron,
|
|
219
|
+
declaredEvery: decl.every,
|
|
220
|
+
overrideCron,
|
|
221
|
+
overrideEvery,
|
|
222
|
+
effectiveCron: overrideCron ?? decl.cron,
|
|
223
|
+
effectiveEvery: overrideEvery ?? decl.every,
|
|
224
|
+
timezone: decl.timezone,
|
|
225
|
+
overridable: decl.overridable,
|
|
226
|
+
status: "active",
|
|
227
|
+
leaderInstanceId: prev?.leaderInstanceId,
|
|
228
|
+
leaderLeaseUntil: prev?.leaderLeaseUntil,
|
|
229
|
+
lastRunAt: prev?.lastRunAt,
|
|
230
|
+
nextRunAt: prev?.nextRunAt,
|
|
231
|
+
dstAmbiguity: prev?.dstAmbiguity,
|
|
232
|
+
};
|
|
233
|
+
});
|
|
227
234
|
}
|
|
228
235
|
|
|
229
236
|
/**
|
|
@@ -305,6 +312,12 @@ export function createMemoryCronStore(seed?: readonly CronRow[]): CronStore {
|
|
|
305
312
|
return true;
|
|
306
313
|
});
|
|
307
314
|
},
|
|
315
|
+
async update(name, fn) {
|
|
316
|
+
return withLock(name, () => {
|
|
317
|
+
const prev = rows.get(name);
|
|
318
|
+
rows.set(name, structuredClone(fn(prev ? structuredClone(prev) : undefined)));
|
|
319
|
+
});
|
|
320
|
+
},
|
|
308
321
|
};
|
|
309
322
|
}
|
|
310
323
|
|
|
@@ -409,5 +422,13 @@ export function createFileCronStore(path: string): CronStore {
|
|
|
409
422
|
return true;
|
|
410
423
|
});
|
|
411
424
|
},
|
|
425
|
+
async update(name, fn) {
|
|
426
|
+
await withFileLock(async () => {
|
|
427
|
+
const map = await loadUnlocked();
|
|
428
|
+
const prev = map.get(name);
|
|
429
|
+
map.set(name, structuredClone(fn(prev ? structuredClone(prev) : undefined)));
|
|
430
|
+
await flushUnlocked(map);
|
|
431
|
+
});
|
|
432
|
+
},
|
|
412
433
|
};
|
|
413
434
|
}
|
|
@@ -25,6 +25,7 @@ import {
|
|
|
25
25
|
import {
|
|
26
26
|
clock,
|
|
27
27
|
createClockRuntime,
|
|
28
|
+
createFileCronStore,
|
|
28
29
|
createMemoryCronStore,
|
|
29
30
|
createTestClockRuntime,
|
|
30
31
|
createTimeTravel,
|
|
@@ -218,6 +219,38 @@ describe("leader election", () => {
|
|
|
218
219
|
expect(fired).toEqual([winner, "i-b"]);
|
|
219
220
|
});
|
|
220
221
|
|
|
222
|
+
test("file store: overlapping reconcile cannot double-fire a due tick", async () => {
|
|
223
|
+
const dir = await mkdtemp(join(tmpdir(), "oke-clock-file-race-"));
|
|
224
|
+
try {
|
|
225
|
+
const store = createFileCronStore(join(dir, "crons.json"));
|
|
226
|
+
const fired: string[] = [];
|
|
227
|
+
const mk = (instanceId: string) => {
|
|
228
|
+
const rt = createClockRuntime({ instanceId, store, leaseMs: 200 });
|
|
229
|
+
rt.register(clock("once", { every: "1h" }));
|
|
230
|
+
rt.onCron("once", () => {
|
|
231
|
+
fired.push(instanceId);
|
|
232
|
+
});
|
|
233
|
+
return rt;
|
|
234
|
+
};
|
|
235
|
+
const a = mk("a");
|
|
236
|
+
const b = mk("b");
|
|
237
|
+
const results = await Promise.all([
|
|
238
|
+
(async () => {
|
|
239
|
+
await a.reconcile();
|
|
240
|
+
return a.tick();
|
|
241
|
+
})(),
|
|
242
|
+
(async () => {
|
|
243
|
+
await b.reconcile();
|
|
244
|
+
return b.tick();
|
|
245
|
+
})(),
|
|
246
|
+
]);
|
|
247
|
+
expect(results.flatMap((r) => r.ran).filter((n) => n === "once")).toHaveLength(1);
|
|
248
|
+
expect(fired).toHaveLength(1);
|
|
249
|
+
} finally {
|
|
250
|
+
await rm(dir, { recursive: true, force: true });
|
|
251
|
+
}
|
|
252
|
+
});
|
|
253
|
+
|
|
221
254
|
test("tryAcquireLease is exclusive within the lease window", async () => {
|
|
222
255
|
const store = createMemoryCronStore([
|
|
223
256
|
{
|
|
@@ -6,8 +6,8 @@
|
|
|
6
6
|
*/
|
|
7
7
|
|
|
8
8
|
import { existsSync } from "node:fs";
|
|
9
|
-
import { mkdir, writeFile } from "node:fs/promises";
|
|
10
|
-
import { dirname, join } from "node:path";
|
|
9
|
+
import { mkdir, readdir, readFile, unlink, writeFile } from "node:fs/promises";
|
|
10
|
+
import { basename, dirname, join } from "node:path";
|
|
11
11
|
import type { TableContribution } from "../../kernel/plugin.ts";
|
|
12
12
|
import {
|
|
13
13
|
isFieldBuilder,
|
|
@@ -34,11 +34,35 @@ export type SqlDialect = "postgres";
|
|
|
34
34
|
/** Header written at the top of generated files. */
|
|
35
35
|
export const GENERATED_SCHEMA_HEADER = "// generated by oke — do not edit";
|
|
36
36
|
|
|
37
|
-
/** Default
|
|
38
|
-
export const DEFAULT_GENERATED_SCHEMA_REL = "src/db/
|
|
37
|
+
/** Default generated Drizzle barrel (relative to project root). */
|
|
38
|
+
export const DEFAULT_GENERATED_SCHEMA_REL = "src/db/drizzle/index.ts";
|
|
39
39
|
|
|
40
|
-
/**
|
|
41
|
-
|
|
40
|
+
/**
|
|
41
|
+
* Prior single-file emit (`src/db/drizzle.ts`). Folder emit deletes this
|
|
42
|
+
* sibling when it still carries the generated header.
|
|
43
|
+
*/
|
|
44
|
+
export const PREV_SINGLE_DRIZZLE_REL = "src/db/drizzle.ts";
|
|
45
|
+
|
|
46
|
+
/**
|
|
47
|
+
* Prior emit filename (`src/db/schema.drizzle.ts`) — still resolved when
|
|
48
|
+
* present and the new default does not exist yet.
|
|
49
|
+
*/
|
|
50
|
+
export const PREV_DRIZZLE_EMIT_REL = "src/db/schema.drizzle.ts";
|
|
51
|
+
|
|
52
|
+
/** Single-file declare module (takes priority when both layouts exist). */
|
|
53
|
+
export const DEFAULT_DECLARE_SCHEMA_REL = "src/db/schema.ts";
|
|
54
|
+
|
|
55
|
+
/**
|
|
56
|
+
* Prior single-file declare (`src/db/schema.decl.ts`) — still resolved when
|
|
57
|
+
* present and `schema.ts` does not exist yet.
|
|
58
|
+
*/
|
|
59
|
+
export const PREV_DECLARE_SCHEMA_REL = "src/db/schema.decl.ts";
|
|
60
|
+
|
|
61
|
+
/**
|
|
62
|
+
* Folder declare module — `src/db/schema/index.ts`.
|
|
63
|
+
* Used when {@link DEFAULT_DECLARE_SCHEMA_REL} is absent.
|
|
64
|
+
*/
|
|
65
|
+
export const DEFAULT_DECLARE_SCHEMA_DIR_REL = "src/db/schema/index.ts";
|
|
42
66
|
|
|
43
67
|
/**
|
|
44
68
|
* Prior `src/db/` emit filename — still resolved when present and the new
|
|
@@ -289,19 +313,26 @@ export interface EmitDrizzleSourceOptions {
|
|
|
289
313
|
readonly relations?: readonly SchemaRelationsDecl[];
|
|
290
314
|
}
|
|
291
315
|
|
|
292
|
-
|
|
293
|
-
|
|
294
|
-
|
|
295
|
-
|
|
296
|
-
|
|
297
|
-
|
|
298
|
-
|
|
299
|
-
|
|
316
|
+
function tableExportName(table: SchemaTableDecl): string {
|
|
317
|
+
return exportNameForTable(schemaTableSqlName(table) ?? table.name);
|
|
318
|
+
}
|
|
319
|
+
|
|
320
|
+
function referencedExportNames(table: SchemaTableDecl): string[] {
|
|
321
|
+
const self = tableExportName(table);
|
|
322
|
+
const names = new Set<string>();
|
|
323
|
+
for (const col of Object.values(table.columns)) {
|
|
324
|
+
if (!col.references) continue;
|
|
325
|
+
const target = col.references.ref();
|
|
326
|
+
const targetName = exportNameForTable(target.tableName ?? "unknown");
|
|
327
|
+
if (targetName !== self) names.add(targetName);
|
|
328
|
+
}
|
|
329
|
+
return [...names].sort();
|
|
330
|
+
}
|
|
331
|
+
|
|
332
|
+
function emitImportLines(
|
|
300
333
|
tables: readonly SchemaTableDecl[],
|
|
301
|
-
|
|
302
|
-
|
|
303
|
-
): string {
|
|
304
|
-
void dialect;
|
|
334
|
+
options: { readonly relations?: boolean } = {},
|
|
335
|
+
): string[] {
|
|
305
336
|
const usedFactories = new Set<string>();
|
|
306
337
|
for (const table of tables) {
|
|
307
338
|
for (const col of Object.values(table.columns)) {
|
|
@@ -316,8 +347,9 @@ export function emitDrizzleSource(
|
|
|
316
347
|
const coreNames = ["pgTable", ...pgIntImport, needsPolicy ? "pgPolicy" : null].filter(
|
|
317
348
|
(name): name is string => name !== null,
|
|
318
349
|
);
|
|
319
|
-
const
|
|
320
|
-
|
|
350
|
+
const lines = [`import { ${coreNames.join(", ")} } from "drizzle-orm/pg-core";`];
|
|
351
|
+
if (needsPolicy) lines.push(`import { sql } from "drizzle-orm";`);
|
|
352
|
+
if (options.relations) lines.push(`import { defineRelations } from "drizzle-orm";`);
|
|
321
353
|
|
|
322
354
|
const needsId = tables.some((t) =>
|
|
323
355
|
Object.values(t.columns).some((c) => c.defaultFnKind === "id"),
|
|
@@ -337,30 +369,146 @@ export function emitDrizzleSource(
|
|
|
337
369
|
needsNowIso ? "nowIso" : null,
|
|
338
370
|
needsNowDate ? "nowDate" : null,
|
|
339
371
|
].filter((x): x is string => x !== null);
|
|
340
|
-
|
|
341
|
-
|
|
372
|
+
if (helperNames.length > 0) {
|
|
373
|
+
lines.push(`import { ${helperNames.join(", ")} } from "okengine/store";`);
|
|
374
|
+
}
|
|
375
|
+
return lines;
|
|
376
|
+
}
|
|
342
377
|
|
|
343
|
-
|
|
344
|
-
|
|
345
|
-
|
|
378
|
+
function joinGeneratedModule(parts: readonly string[]): string {
|
|
379
|
+
return parts.filter((line, i, arr) => !(line === "" && arr[i - 1] === "")).join("\n");
|
|
380
|
+
}
|
|
346
381
|
|
|
382
|
+
/**
|
|
383
|
+
* Emit a full Drizzle schema module from abstract table decls.
|
|
384
|
+
*
|
|
385
|
+
* @param tables - App + plugin tables (already merged)
|
|
386
|
+
* @param dialect - Always `postgres` (`pgTable`); retained for call-site clarity
|
|
387
|
+
* @param options - Optional relations to emit as `defineRelations`
|
|
388
|
+
*/
|
|
389
|
+
export function emitDrizzleSource(
|
|
390
|
+
tables: readonly SchemaTableDecl[],
|
|
391
|
+
dialect: SqlDialect = "postgres",
|
|
392
|
+
options: EmitDrizzleSourceOptions = {},
|
|
393
|
+
): string {
|
|
394
|
+
const relations = options.relations ?? [];
|
|
347
395
|
const blocks = tables.map((table) => emitTableBlock(table, dialect));
|
|
348
|
-
|
|
349
396
|
const relationsBlock = emitRelationsSource(relations);
|
|
350
|
-
|
|
351
|
-
return [
|
|
397
|
+
return joinGeneratedModule([
|
|
352
398
|
GENERATED_SCHEMA_HEADER,
|
|
353
|
-
|
|
354
|
-
sqlImport.trimEnd(),
|
|
355
|
-
relationsImport.trimEnd(),
|
|
356
|
-
helperImport.trimEnd(),
|
|
399
|
+
...emitImportLines(tables, { relations: relations.length > 0 }),
|
|
357
400
|
"",
|
|
358
401
|
blocks.join("\n\n"),
|
|
359
402
|
relationsBlock ? `\n${relationsBlock}` : "",
|
|
360
403
|
"",
|
|
361
|
-
]
|
|
362
|
-
|
|
363
|
-
|
|
404
|
+
]);
|
|
405
|
+
}
|
|
406
|
+
|
|
407
|
+
/**
|
|
408
|
+
* One generated table module (`drizzle/<export>.ts`).
|
|
409
|
+
*
|
|
410
|
+
* @param table - Table declaration
|
|
411
|
+
* @param dialect - Always `postgres`
|
|
412
|
+
*/
|
|
413
|
+
export function emitDrizzleTableSource(
|
|
414
|
+
table: SchemaTableDecl,
|
|
415
|
+
dialect: SqlDialect = "postgres",
|
|
416
|
+
): string {
|
|
417
|
+
const refs = referencedExportNames(table).map(
|
|
418
|
+
(name) => `import { ${name} } from ${JSON.stringify(`./${name}.ts`)};`,
|
|
419
|
+
);
|
|
420
|
+
return joinGeneratedModule([
|
|
421
|
+
GENERATED_SCHEMA_HEADER,
|
|
422
|
+
...emitImportLines([table]),
|
|
423
|
+
...refs,
|
|
424
|
+
"",
|
|
425
|
+
emitTableBlock(table, dialect),
|
|
426
|
+
"",
|
|
427
|
+
]);
|
|
428
|
+
}
|
|
429
|
+
|
|
430
|
+
/**
|
|
431
|
+
* Generated `drizzle/relations.ts` when declare relations exist.
|
|
432
|
+
*
|
|
433
|
+
* @param relations - Relations declarations
|
|
434
|
+
*/
|
|
435
|
+
export function emitDrizzleRelationsFileSource(relations: readonly SchemaRelationsDecl[]): string {
|
|
436
|
+
if (relations.length === 0) return "";
|
|
437
|
+
const keys = new Set<string>();
|
|
438
|
+
for (const rel of relations) {
|
|
439
|
+
for (const k of rel.tableKeys) keys.add(k);
|
|
440
|
+
}
|
|
441
|
+
const imports = [...keys]
|
|
442
|
+
.sort()
|
|
443
|
+
.map((name) => `import { ${name} } from ${JSON.stringify(`./${name}.ts`)};`);
|
|
444
|
+
return joinGeneratedModule([
|
|
445
|
+
GENERATED_SCHEMA_HEADER,
|
|
446
|
+
`import { defineRelations } from "drizzle-orm";`,
|
|
447
|
+
...imports,
|
|
448
|
+
"",
|
|
449
|
+
emitRelationsSource(relations),
|
|
450
|
+
"",
|
|
451
|
+
]);
|
|
452
|
+
}
|
|
453
|
+
|
|
454
|
+
/**
|
|
455
|
+
* Generated `drizzle/index.ts` barrel.
|
|
456
|
+
*
|
|
457
|
+
* @param fileNames - Table (and relations) filenames to re-export
|
|
458
|
+
*/
|
|
459
|
+
export function emitDrizzleIndexSource(fileNames: readonly string[]): string {
|
|
460
|
+
const exports = fileNames.map((name) => `export * from ${JSON.stringify(`./${name}`)};`);
|
|
461
|
+
return `${GENERATED_SCHEMA_HEADER}\n${exports.join("\n")}\n`;
|
|
462
|
+
}
|
|
463
|
+
|
|
464
|
+
/**
|
|
465
|
+
* Filename → source for a generated `drizzle/` folder (one table per file).
|
|
466
|
+
*
|
|
467
|
+
* @param tables - App + plugin tables (already merged)
|
|
468
|
+
* @param dialect - Always `postgres`
|
|
469
|
+
* @param options - Optional relations file
|
|
470
|
+
*/
|
|
471
|
+
export function emitDrizzleFolderSources(
|
|
472
|
+
tables: readonly SchemaTableDecl[],
|
|
473
|
+
dialect: SqlDialect = "postgres",
|
|
474
|
+
options: EmitDrizzleSourceOptions = {},
|
|
475
|
+
): ReadonlyMap<string, string> {
|
|
476
|
+
const files = new Map<string, string>();
|
|
477
|
+
const tableFiles: string[] = [];
|
|
478
|
+
for (const table of tables) {
|
|
479
|
+
const name = `${tableExportName(table)}.ts`;
|
|
480
|
+
files.set(name, emitDrizzleTableSource(table, dialect));
|
|
481
|
+
tableFiles.push(name);
|
|
482
|
+
}
|
|
483
|
+
const relations = options.relations ?? [];
|
|
484
|
+
const extra: string[] = [];
|
|
485
|
+
if (relations.length > 0) {
|
|
486
|
+
files.set("relations.ts", emitDrizzleRelationsFileSource(relations));
|
|
487
|
+
extra.push("relations.ts");
|
|
488
|
+
}
|
|
489
|
+
files.set("index.ts", emitDrizzleIndexSource([...tableFiles, ...extra]));
|
|
490
|
+
return files;
|
|
491
|
+
}
|
|
492
|
+
|
|
493
|
+
/**
|
|
494
|
+
* Whether `db.generated` is the folder barrel (`drizzle/index.ts`).
|
|
495
|
+
*
|
|
496
|
+
* @param rel - Path relative to project root
|
|
497
|
+
*/
|
|
498
|
+
export function isFolderGeneratedRel(rel: string): boolean {
|
|
499
|
+
const n = rel.replace(/\\/g, "/").replace(/\/$/, "");
|
|
500
|
+
return /(?:^|\/)drizzle\/index\.tsx?$/.test(n) || /(?:^|\/)drizzle$/.test(n);
|
|
501
|
+
}
|
|
502
|
+
|
|
503
|
+
/**
|
|
504
|
+
* Normalize `db.generated` so a directory `drizzle` becomes `drizzle/index.ts`.
|
|
505
|
+
*
|
|
506
|
+
* @param rel - Path relative to project root
|
|
507
|
+
*/
|
|
508
|
+
export function normalizeGeneratedRel(rel: string): string {
|
|
509
|
+
const n = rel.replace(/\\/g, "/").replace(/\/$/, "");
|
|
510
|
+
if (n.endsWith("/drizzle") || n === "drizzle") return `${n}/index.ts`;
|
|
511
|
+
return n;
|
|
364
512
|
}
|
|
365
513
|
|
|
366
514
|
function emitTableBlock(table: SchemaTableDecl, dialect: SqlDialect): string {
|
|
@@ -534,6 +682,19 @@ export async function maybeEmitDomainSchema(
|
|
|
534
682
|
}
|
|
535
683
|
|
|
536
684
|
const tables = mergeSchemaTables(appTables, fromPlugins);
|
|
685
|
+
if (isFolderGeneratedRel(outRel)) {
|
|
686
|
+
const dirAbs = dirname(outAbs);
|
|
687
|
+
const files = emitDrizzleFolderSources(tables, options.dialect, {
|
|
688
|
+
relations: appRelations,
|
|
689
|
+
});
|
|
690
|
+
await writeGeneratedFolder(dirAbs, files);
|
|
691
|
+
await removeLegacySingleDrizzleFile(dirAbs);
|
|
692
|
+
options.write?.(
|
|
693
|
+
`oke db: emitted ${outRel} (${tables.length} table${tables.length === 1 ? "" : "s"}, ${options.dialect})\n`,
|
|
694
|
+
);
|
|
695
|
+
return { emitted: true, path: outAbs, tableCount: tables.length };
|
|
696
|
+
}
|
|
697
|
+
|
|
537
698
|
const source = emitDrizzleSource(tables, options.dialect, { relations: appRelations });
|
|
538
699
|
await mkdir(dirname(outAbs), { recursive: true });
|
|
539
700
|
await writeFile(outAbs, source, "utf8");
|
|
@@ -543,9 +704,95 @@ export async function maybeEmitDomainSchema(
|
|
|
543
704
|
return { emitted: true, path: outAbs, tableCount: tables.length };
|
|
544
705
|
}
|
|
545
706
|
|
|
707
|
+
async function writeGeneratedFolder(
|
|
708
|
+
dirAbs: string,
|
|
709
|
+
files: ReadonlyMap<string, string>,
|
|
710
|
+
): Promise<void> {
|
|
711
|
+
await mkdir(dirAbs, { recursive: true });
|
|
712
|
+
for (const [name, source] of files) {
|
|
713
|
+
await writeFile(join(dirAbs, name), source, "utf8");
|
|
714
|
+
}
|
|
715
|
+
let existing: string[] = [];
|
|
716
|
+
try {
|
|
717
|
+
existing = await readdir(dirAbs);
|
|
718
|
+
} catch {
|
|
719
|
+
return;
|
|
720
|
+
}
|
|
721
|
+
for (const name of existing) {
|
|
722
|
+
if (!name.endsWith(".ts") && !name.endsWith(".tsx")) continue;
|
|
723
|
+
if (files.has(name)) continue;
|
|
724
|
+
const abs = join(dirAbs, name);
|
|
725
|
+
try {
|
|
726
|
+
const content = await readFile(abs, "utf8");
|
|
727
|
+
if (content.startsWith(GENERATED_SCHEMA_HEADER)) await unlink(abs);
|
|
728
|
+
} catch {
|
|
729
|
+
// ignore prune failures
|
|
730
|
+
}
|
|
731
|
+
}
|
|
732
|
+
}
|
|
733
|
+
|
|
734
|
+
async function removeLegacySingleDrizzleFile(dirAbs: string): Promise<void> {
|
|
735
|
+
if (basename(dirAbs) !== "drizzle") return;
|
|
736
|
+
const sibling = join(dirname(dirAbs), basename(PREV_SINGLE_DRIZZLE_REL));
|
|
737
|
+
if (!existsSync(sibling)) return;
|
|
738
|
+
try {
|
|
739
|
+
const content = await readFile(sibling, "utf8");
|
|
740
|
+
if (content.startsWith(GENERATED_SCHEMA_HEADER)) await unlink(sibling);
|
|
741
|
+
} catch {
|
|
742
|
+
// ignore
|
|
743
|
+
}
|
|
744
|
+
}
|
|
745
|
+
|
|
746
|
+
/**
|
|
747
|
+
* Discover the declare module when `db.declare` is omitted.
|
|
748
|
+
*
|
|
749
|
+
* Order: `schema.ts` (single file) → `schema.decl.ts` (prior name) →
|
|
750
|
+
* `schema/index.ts` (folder) → legacy `src/schema.decl.ts` → default
|
|
751
|
+
* `schema.ts` (may not exist yet).
|
|
752
|
+
*
|
|
753
|
+
* @param cwd - Project root
|
|
754
|
+
*/
|
|
755
|
+
export function discoverDeclareRel(cwd: string): string {
|
|
756
|
+
if (existsSync(join(cwd, DEFAULT_DECLARE_SCHEMA_REL))) {
|
|
757
|
+
return DEFAULT_DECLARE_SCHEMA_REL;
|
|
758
|
+
}
|
|
759
|
+
if (existsSync(join(cwd, PREV_DECLARE_SCHEMA_REL))) {
|
|
760
|
+
return PREV_DECLARE_SCHEMA_REL;
|
|
761
|
+
}
|
|
762
|
+
if (existsSync(join(cwd, DEFAULT_DECLARE_SCHEMA_DIR_REL))) {
|
|
763
|
+
return DEFAULT_DECLARE_SCHEMA_DIR_REL;
|
|
764
|
+
}
|
|
765
|
+
if (existsSync(join(cwd, LEGACY_DECLARE_SCHEMA_REL))) {
|
|
766
|
+
return LEGACY_DECLARE_SCHEMA_REL;
|
|
767
|
+
}
|
|
768
|
+
return DEFAULT_DECLARE_SCHEMA_REL;
|
|
769
|
+
}
|
|
770
|
+
|
|
771
|
+
/**
|
|
772
|
+
* Discover the generated Drizzle emit path when `db.generated` is omitted.
|
|
773
|
+
*
|
|
774
|
+
* Order: `src/db/drizzle/index.ts`, except legacy `src/schema.generated.ts`
|
|
775
|
+
* when the declare module is still on the old `src/` layout. Older single
|
|
776
|
+
* files (`drizzle.ts`, `schema.drizzle.ts`) are not re-targeted — set
|
|
777
|
+
* `db.generated` to keep emitting them.
|
|
778
|
+
*
|
|
779
|
+
* @param cwd - Project root
|
|
780
|
+
* @param declareRel - Resolved declare relative path
|
|
781
|
+
*/
|
|
782
|
+
export function discoverGeneratedRel(cwd: string, declareRel: string): string {
|
|
783
|
+
const preferSrcLegacy =
|
|
784
|
+
declareRel === LEGACY_DECLARE_SCHEMA_REL ||
|
|
785
|
+
(!existsSync(join(cwd, DEFAULT_DECLARE_SCHEMA_REL)) &&
|
|
786
|
+
!existsSync(join(cwd, PREV_DECLARE_SCHEMA_REL)) &&
|
|
787
|
+
!existsSync(join(cwd, DEFAULT_DECLARE_SCHEMA_DIR_REL)) &&
|
|
788
|
+
existsSync(join(cwd, LEGACY_GENERATED_SCHEMA_REL)));
|
|
789
|
+
if (preferSrcLegacy) return LEGACY_GENERATED_SCHEMA_REL;
|
|
790
|
+
return DEFAULT_GENERATED_SCHEMA_REL;
|
|
791
|
+
}
|
|
792
|
+
|
|
546
793
|
/**
|
|
547
794
|
* Resolve declare/out paths from an optional db config slice.
|
|
548
|
-
* Prefers explicit
|
|
795
|
+
* Prefers explicit `db.declare` / `db.generated`, then discovery.
|
|
549
796
|
*
|
|
550
797
|
* @param cwd - Project root
|
|
551
798
|
* @param db - Config `db` block
|
|
@@ -554,35 +801,8 @@ export function resolveEmitPaths(
|
|
|
554
801
|
cwd: string,
|
|
555
802
|
db?: { readonly declare?: string; readonly generated?: string },
|
|
556
803
|
): { readonly declarePath: string; readonly outPath: string } {
|
|
557
|
-
|
|
558
|
-
|
|
559
|
-
if (existsSync(join(cwd, DEFAULT_DECLARE_SCHEMA_REL))) {
|
|
560
|
-
declareRel = DEFAULT_DECLARE_SCHEMA_REL;
|
|
561
|
-
} else if (existsSync(join(cwd, LEGACY_DECLARE_SCHEMA_REL))) {
|
|
562
|
-
declareRel = LEGACY_DECLARE_SCHEMA_REL;
|
|
563
|
-
} else {
|
|
564
|
-
declareRel = DEFAULT_DECLARE_SCHEMA_REL;
|
|
565
|
-
}
|
|
566
|
-
}
|
|
567
|
-
|
|
568
|
-
let outRel = db?.generated;
|
|
569
|
-
if (!outRel) {
|
|
570
|
-
const preferSrcLegacy =
|
|
571
|
-
declareRel === LEGACY_DECLARE_SCHEMA_REL ||
|
|
572
|
-
(!existsSync(join(cwd, DEFAULT_DECLARE_SCHEMA_REL)) &&
|
|
573
|
-
existsSync(join(cwd, LEGACY_GENERATED_SCHEMA_REL)));
|
|
574
|
-
if (preferSrcLegacy) {
|
|
575
|
-
outRel = LEGACY_GENERATED_SCHEMA_REL;
|
|
576
|
-
} else if (
|
|
577
|
-
!existsSync(join(cwd, DEFAULT_GENERATED_SCHEMA_REL)) &&
|
|
578
|
-
existsSync(join(cwd, PREV_GENERATED_SCHEMA_REL))
|
|
579
|
-
) {
|
|
580
|
-
outRel = PREV_GENERATED_SCHEMA_REL;
|
|
581
|
-
} else {
|
|
582
|
-
outRel = DEFAULT_GENERATED_SCHEMA_REL;
|
|
583
|
-
}
|
|
584
|
-
}
|
|
585
|
-
|
|
804
|
+
const declareRel = db?.declare ?? discoverDeclareRel(cwd);
|
|
805
|
+
const outRel = normalizeGeneratedRel(db?.generated ?? discoverGeneratedRel(cwd, declareRel));
|
|
586
806
|
return {
|
|
587
807
|
declarePath: join(cwd, declareRel),
|
|
588
808
|
outPath: join(cwd, outRel),
|