okengine 0.7.0 → 0.8.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/package.json +2 -2
- package/site/content/docs/elements/channel.mdx +23 -12
- package/site/content/docs/elements/clock.mdx +17 -15
- package/site/content/docs/elements/flow.mdx +6 -2
- package/site/content/docs/elements/store.mdx +131 -0
- package/site/content/docs/get-started/installation.mdx +18 -16
- package/site/content/docs/plugins/magic-link.mdx +42 -0
- package/site/content/docs/plugins/phone-number.mdx +78 -17
- package/site/content/docs/plugins/two-factor.mdx +1 -0
- package/site/content/docs/reference/cli.md +2 -0
- package/site/content/docs/reference/configuration.mdx +5 -3
- package/site/content/docs/reference/environment-variables.mdx +20 -8
- package/src/cli/db-seed.ts +359 -0
- package/src/cli/db.test.ts +341 -3
- package/src/cli/db.ts +75 -8
- package/src/cli/load-config.images.test.ts +22 -0
- package/src/cli/load-config.ts +7 -2
- package/src/cli/registry.ts +37 -1
- package/src/compiler/effects-infer.ts +1 -0
- package/src/config/index.ts +4 -0
- package/src/drivers/channel-sently.test.ts +8 -0
- package/src/drivers/channel-taqnyat-mail.ts +34 -0
- package/src/drivers/channel-types.ts +71 -0
- package/src/drivers/clock-postgres.test.ts +258 -0
- package/src/drivers/clock-postgres.ts +410 -0
- package/src/drivers/index.ts +18 -0
- package/src/drivers/journal-postgres.test.ts +175 -0
- package/src/drivers/journal-postgres.ts +492 -0
- package/src/elements/channel/runtime.ts +51 -0
- package/src/elements/channel.test.ts +71 -0
- package/src/elements/clock/chaos-child.ts +280 -41
- package/src/elements/clock/durable.ts +7 -0
- package/src/elements/clock/reconcile.ts +2 -2
- package/src/elements/clock/runtime.ts +5 -3
- package/src/elements/clock.ts +1 -1
- package/src/elements/store/seed.test.ts +27 -0
- package/src/elements/store/seed.ts +68 -0
- package/src/elements/store/sql-session.test.ts +39 -0
- package/src/elements/store/sql-session.ts +55 -0
- package/src/elements/store/upsert-app.test.ts +103 -0
- package/src/elements/store.ts +5 -0
- package/src/index.ts +15 -0
- package/src/kernel/app.ts +165 -14
- package/src/kernel/boot-bind/channel.test.ts +16 -0
- package/src/kernel/boot-bind/channel.ts +13 -0
- package/src/kernel/boot-bind/clock.ts +17 -6
- package/src/kernel/boot-bind/honor-config.test.ts +105 -4
- package/src/kernel/boot-bind/journal.ts +89 -0
- package/src/kernel/boot.test.ts +6 -4
- package/src/kernel/boot.ts +53 -13
- package/src/kernel/concurrency.ts +1 -1
- package/src/kernel/fx.test.ts +6 -0
- package/src/kernel/fx.ts +126 -5
- package/src/kernel/index.ts +6 -0
- package/src/kernel/journal-boot.test.ts +397 -0
- package/src/kernel/journal-suspend.ts +35 -0
- package/src/kernel/journal.test.ts +142 -0
- package/src/kernel/journal.ts +202 -27
- package/src/plugins/auth-methods.security.test.ts +10 -7
- package/src/plugins/phone-number.ts +67 -10
- package/src/plugins/taqnyat.live.test.ts +174 -0
package/src/cli/db.test.ts
CHANGED
|
@@ -3,15 +3,74 @@
|
|
|
3
3
|
*/
|
|
4
4
|
|
|
5
5
|
import { describe, expect, test } from "bun:test";
|
|
6
|
-
import {
|
|
6
|
+
import { Database } from "bun:sqlite";
|
|
7
|
+
import { mkdir, mkdtemp, readdir, readFile, rename, rm, writeFile } from "node:fs/promises";
|
|
7
8
|
import { tmpdir } from "node:os";
|
|
8
9
|
import { join, resolve } from "node:path";
|
|
9
|
-
import {
|
|
10
|
-
import {
|
|
10
|
+
import { createFx } from "../kernel/fx.ts";
|
|
11
|
+
import { defineSeed, type SeedFn } from "../elements/store/seed.ts";
|
|
12
|
+
import { EXIT_OK, EXIT_RUNTIME, EXIT_USAGE } from "./exit.ts";
|
|
13
|
+
import {
|
|
14
|
+
dbCli,
|
|
15
|
+
emitAbstractSchemaPrestep,
|
|
16
|
+
executeSeedDef,
|
|
17
|
+
runDb,
|
|
18
|
+
runGenerate,
|
|
19
|
+
runPush,
|
|
20
|
+
runSeed,
|
|
21
|
+
} from "./db.ts";
|
|
11
22
|
|
|
12
23
|
/** Repo public entry — absolute import so temp apps need no install. */
|
|
13
24
|
const OKE_INDEX = resolve(import.meta.dir, "../index.ts");
|
|
14
25
|
const CONFIG_MOD = resolve(import.meta.dir, "../config/index.ts");
|
|
26
|
+
const DB_MOD = resolve(import.meta.dir, "./db.ts");
|
|
27
|
+
|
|
28
|
+
/**
|
|
29
|
+
* Run `oke db generate|migrate` in a fresh Bun process.
|
|
30
|
+
*
|
|
31
|
+
* drizzle-kit's in-process `generate` caches the schema module — a second
|
|
32
|
+
* generate in the same process after rewriting the schema file returns
|
|
33
|
+
* `no_changes`. Real CLI invocations are one process each; the adversarial
|
|
34
|
+
* multi-migration test must match that.
|
|
35
|
+
*
|
|
36
|
+
* Temp projects live under {@link import.meta.dir} so `bunx drizzle-kit`
|
|
37
|
+
* (migrate's default) walks up to the repo `node_modules`.
|
|
38
|
+
*/
|
|
39
|
+
async function runDbFresh(
|
|
40
|
+
sub: "generate" | "migrate",
|
|
41
|
+
cwd: string,
|
|
42
|
+
): Promise<{ readonly code: number; readonly out: string }> {
|
|
43
|
+
const proc = Bun.spawn(
|
|
44
|
+
[
|
|
45
|
+
"bun",
|
|
46
|
+
"-e",
|
|
47
|
+
`import { runDb } from ${JSON.stringify(DB_MOD)};
|
|
48
|
+
const cwd = ${JSON.stringify(cwd)};
|
|
49
|
+
const prev = process.cwd();
|
|
50
|
+
process.chdir(cwd);
|
|
51
|
+
try {
|
|
52
|
+
process.exit(await runDb(${JSON.stringify(sub)}, { cwd, skipEmit: true }));
|
|
53
|
+
} finally {
|
|
54
|
+
process.chdir(prev);
|
|
55
|
+
}
|
|
56
|
+
`,
|
|
57
|
+
],
|
|
58
|
+
{ cwd, stdout: "pipe", stderr: "pipe", env: process.env as Record<string, string> },
|
|
59
|
+
);
|
|
60
|
+
const [stdout, stderr, code] = await Promise.all([
|
|
61
|
+
new Response(proc.stdout).text(),
|
|
62
|
+
new Response(proc.stderr).text(),
|
|
63
|
+
proc.exited,
|
|
64
|
+
]);
|
|
65
|
+
return { code, out: `${stdout}${stderr}` };
|
|
66
|
+
}
|
|
67
|
+
|
|
68
|
+
async function migrationFolders(drizzleDir: string): Promise<string[]> {
|
|
69
|
+
return (await readdir(drizzleDir, { withFileTypes: true }))
|
|
70
|
+
.filter((d) => d.isDirectory())
|
|
71
|
+
.map((d) => d.name)
|
|
72
|
+
.sort();
|
|
73
|
+
}
|
|
15
74
|
|
|
16
75
|
describe("oke db", () => {
|
|
17
76
|
test("help lists subcommands", async () => {
|
|
@@ -26,6 +85,7 @@ describe("oke db", () => {
|
|
|
26
85
|
expect(lines.join("\n")).toContain("push");
|
|
27
86
|
expect(lines.join("\n")).toContain("generate");
|
|
28
87
|
expect(lines.join("\n")).toContain("migrate");
|
|
88
|
+
expect(lines.join("\n")).toContain("seed");
|
|
29
89
|
expect(lines.join("\n")).toContain("oke schema generate");
|
|
30
90
|
} finally {
|
|
31
91
|
console.log = orig;
|
|
@@ -187,3 +247,281 @@ export const app = oke({ name: "plugin-only" }).plug(
|
|
|
187
247
|
expect(generated).toContain('integer("hits")');
|
|
188
248
|
});
|
|
189
249
|
});
|
|
250
|
+
|
|
251
|
+
describe("oke db multi-migration catch-up", () => {
|
|
252
|
+
/**
|
|
253
|
+
* Real drizzle-kit generate/migrate through `oke db` — proves multi-file
|
|
254
|
+
* versioned migrations and that re-migrate / environment lag only apply
|
|
255
|
+
* previously-unapplied files (drizzle-kit `__drizzle_migrations` history).
|
|
256
|
+
* The CLI wrapper must not interfere with that bookkeeping.
|
|
257
|
+
*/
|
|
258
|
+
test("second migrate skips applied files; lag DB catches up with only the new one", async () => {
|
|
259
|
+
const dir = await mkdtemp(join(import.meta.dir, ".tmp-db-mig-"));
|
|
260
|
+
try {
|
|
261
|
+
await mkdir(join(dir, "src"), { recursive: true });
|
|
262
|
+
await writeFile(
|
|
263
|
+
join(dir, "src", "schema.ts"),
|
|
264
|
+
`import { sqliteTable, text } from "drizzle-orm/sqlite-core";
|
|
265
|
+
export const notes = sqliteTable("notes", {
|
|
266
|
+
id: text("id").primaryKey(),
|
|
267
|
+
title: text("title").notNull(),
|
|
268
|
+
});
|
|
269
|
+
`,
|
|
270
|
+
);
|
|
271
|
+
await writeFile(
|
|
272
|
+
join(dir, "drizzle.config.ts"),
|
|
273
|
+
`import { defineConfig } from "drizzle-kit";
|
|
274
|
+
export default defineConfig({
|
|
275
|
+
dialect: "sqlite",
|
|
276
|
+
schema: "./src/schema.ts",
|
|
277
|
+
out: "./drizzle",
|
|
278
|
+
dbCredentials: { url: "file:./app.db" },
|
|
279
|
+
});
|
|
280
|
+
`,
|
|
281
|
+
);
|
|
282
|
+
|
|
283
|
+
const gen1 = await runDbFresh("generate", dir);
|
|
284
|
+
expect(gen1.code).toBe(EXIT_OK);
|
|
285
|
+
expect(gen1.out).toMatch(/oke db generate: wrote /);
|
|
286
|
+
const migsAfterFirst = await migrationFolders(join(dir, "drizzle"));
|
|
287
|
+
expect(migsAfterFirst.length).toBe(1);
|
|
288
|
+
const initialMig = migsAfterFirst[0];
|
|
289
|
+
expect(initialMig).toBeDefined();
|
|
290
|
+
if (initialMig === undefined) throw new Error("expected initial migration folder");
|
|
291
|
+
|
|
292
|
+
const mig1 = await runDbFresh("migrate", dir);
|
|
293
|
+
expect(mig1.code).toBe(EXIT_OK);
|
|
294
|
+
expect(mig1.out).toContain("oke db migrate: applied");
|
|
295
|
+
|
|
296
|
+
let db = new Database(join(dir, "app.db"));
|
|
297
|
+
expect(
|
|
298
|
+
db.query("SELECT name FROM sqlite_master WHERE type='table' AND name='notes'").get(),
|
|
299
|
+
).toBeTruthy();
|
|
300
|
+
let history = db.query("SELECT name FROM __drizzle_migrations ORDER BY id").all() as Array<{
|
|
301
|
+
name: string;
|
|
302
|
+
}>;
|
|
303
|
+
expect(history.map((r) => r.name)).toEqual([initialMig]);
|
|
304
|
+
db.close();
|
|
305
|
+
|
|
306
|
+
await writeFile(
|
|
307
|
+
join(dir, "src", "schema.ts"),
|
|
308
|
+
`import { sqliteTable, text, integer } from "drizzle-orm/sqlite-core";
|
|
309
|
+
export const notes = sqliteTable("notes", {
|
|
310
|
+
id: text("id").primaryKey(),
|
|
311
|
+
title: text("title").notNull(),
|
|
312
|
+
pinned: integer("pinned").notNull().default(0),
|
|
313
|
+
});
|
|
314
|
+
`,
|
|
315
|
+
);
|
|
316
|
+
// Distinct timestamp folder names (drizzle-kit uses YYYYMMDDHHmmss).
|
|
317
|
+
await Bun.sleep(1100);
|
|
318
|
+
|
|
319
|
+
const gen2 = await runDbFresh("generate", dir);
|
|
320
|
+
expect(gen2.code).toBe(EXIT_OK);
|
|
321
|
+
expect(gen2.out).toMatch(/oke db generate: wrote /);
|
|
322
|
+
const migs = await migrationFolders(join(dir, "drizzle"));
|
|
323
|
+
expect(migs.length).toBe(2);
|
|
324
|
+
const firstMig = migs[0];
|
|
325
|
+
const secondMig = migs[1];
|
|
326
|
+
expect(firstMig).toBeDefined();
|
|
327
|
+
expect(secondMig).toBeDefined();
|
|
328
|
+
if (firstMig === undefined || secondMig === undefined) {
|
|
329
|
+
throw new Error("expected two migration folders");
|
|
330
|
+
}
|
|
331
|
+
expect(firstMig).toBe(initialMig);
|
|
332
|
+
|
|
333
|
+
const mig2 = await runDbFresh("migrate", dir);
|
|
334
|
+
expect(mig2.code).toBe(EXIT_OK);
|
|
335
|
+
|
|
336
|
+
db = new Database(join(dir, "app.db"));
|
|
337
|
+
history = db.query("SELECT name FROM __drizzle_migrations ORDER BY id").all() as Array<{
|
|
338
|
+
name: string;
|
|
339
|
+
}>;
|
|
340
|
+
expect(history.map((r) => r.name)).toEqual([firstMig, secondMig]);
|
|
341
|
+
const cols = db.query("PRAGMA table_info(notes)").all() as Array<{ name: string }>;
|
|
342
|
+
expect(cols.map((c) => c.name)).toContain("pinned");
|
|
343
|
+
db.close();
|
|
344
|
+
|
|
345
|
+
// Idempotent re-migrate: history unchanged, no error.
|
|
346
|
+
const migNoop = await runDbFresh("migrate", dir);
|
|
347
|
+
expect(migNoop.code).toBe(EXIT_OK);
|
|
348
|
+
db = new Database(join(dir, "app.db"));
|
|
349
|
+
const historyNoop = db
|
|
350
|
+
.query("SELECT name FROM __drizzle_migrations ORDER BY id")
|
|
351
|
+
.all() as Array<{ name: string }>;
|
|
352
|
+
expect(historyNoop.map((r) => r.name)).toEqual([firstMig, secondMig]);
|
|
353
|
+
db.close();
|
|
354
|
+
|
|
355
|
+
// Staging/prod lag: fresh DB with only the first migration applied,
|
|
356
|
+
// then both files present — catch up applies only the second.
|
|
357
|
+
await rm(join(dir, "app.db"), { force: true });
|
|
358
|
+
const stash = join(dir, "_stash_second");
|
|
359
|
+
await rm(stash, { recursive: true, force: true });
|
|
360
|
+
await rename(join(dir, "drizzle", secondMig), stash);
|
|
361
|
+
|
|
362
|
+
const lagFirst = await runDbFresh("migrate", dir);
|
|
363
|
+
expect(lagFirst.code).toBe(EXIT_OK);
|
|
364
|
+
db = new Database(join(dir, "app.db"));
|
|
365
|
+
const lagHist1 = db
|
|
366
|
+
.query("SELECT name FROM __drizzle_migrations ORDER BY id")
|
|
367
|
+
.all() as Array<{ name: string }>;
|
|
368
|
+
expect(lagHist1.map((r) => r.name)).toEqual([firstMig]);
|
|
369
|
+
const lagCols1 = db.query("PRAGMA table_info(notes)").all() as Array<{ name: string }>;
|
|
370
|
+
expect(lagCols1.map((c) => c.name)).not.toContain("pinned");
|
|
371
|
+
db.close();
|
|
372
|
+
|
|
373
|
+
await rename(stash, join(dir, "drizzle", secondMig));
|
|
374
|
+
const lagCatchUp = await runDbFresh("migrate", dir);
|
|
375
|
+
expect(lagCatchUp.code).toBe(EXIT_OK);
|
|
376
|
+
db = new Database(join(dir, "app.db"));
|
|
377
|
+
const lagHist2 = db
|
|
378
|
+
.query("SELECT name FROM __drizzle_migrations ORDER BY id")
|
|
379
|
+
.all() as Array<{ name: string }>;
|
|
380
|
+
expect(lagHist2.map((r) => r.name)).toEqual([firstMig, secondMig]);
|
|
381
|
+
const lagCols2 = db.query("PRAGMA table_info(notes)").all() as Array<{ name: string }>;
|
|
382
|
+
expect(lagCols2.map((c) => c.name)).toContain("pinned");
|
|
383
|
+
db.close();
|
|
384
|
+
} finally {
|
|
385
|
+
await rm(dir, { recursive: true, force: true });
|
|
386
|
+
}
|
|
387
|
+
}, 120_000);
|
|
388
|
+
});
|
|
389
|
+
|
|
390
|
+
describe("oke db seed", () => {
|
|
391
|
+
function trackingDef(order: string[]) {
|
|
392
|
+
const a: SeedFn = async function a() {
|
|
393
|
+
order.push("a");
|
|
394
|
+
};
|
|
395
|
+
const b: SeedFn = async function b() {
|
|
396
|
+
order.push("b");
|
|
397
|
+
};
|
|
398
|
+
const dev: SeedFn = async function seedDev() {
|
|
399
|
+
order.push("dev");
|
|
400
|
+
};
|
|
401
|
+
const prod: SeedFn = async function seedProd() {
|
|
402
|
+
order.push("prod");
|
|
403
|
+
};
|
|
404
|
+
return defineSeed({
|
|
405
|
+
essential: [a, b],
|
|
406
|
+
dev,
|
|
407
|
+
prod,
|
|
408
|
+
});
|
|
409
|
+
}
|
|
410
|
+
|
|
411
|
+
async function seedWith(
|
|
412
|
+
env: "local" | "docker" | "test" | "prod",
|
|
413
|
+
extras: Parameters<typeof runSeed>[0] = {},
|
|
414
|
+
) {
|
|
415
|
+
const order: string[] = [];
|
|
416
|
+
const out: string[] = [];
|
|
417
|
+
const code = await runSeed({
|
|
418
|
+
env,
|
|
419
|
+
seedDef: trackingDef(order),
|
|
420
|
+
createFx: async () => ({
|
|
421
|
+
fx: createFx({ flow: "oke.db.seed.test" }),
|
|
422
|
+
stop: async () => {},
|
|
423
|
+
}),
|
|
424
|
+
write: (t) => out.push(t),
|
|
425
|
+
force: true,
|
|
426
|
+
...extras,
|
|
427
|
+
});
|
|
428
|
+
return { code, order, out: out.join("") };
|
|
429
|
+
}
|
|
430
|
+
|
|
431
|
+
test("essential array order is execution order", async () => {
|
|
432
|
+
const { code, order, out } = await seedWith("local");
|
|
433
|
+
expect(code).toBe(EXIT_OK);
|
|
434
|
+
expect(order.slice(0, 2)).toEqual(["a", "b"]);
|
|
435
|
+
expect(out).toContain("oke db seed: essential a");
|
|
436
|
+
expect(out).toContain("oke db seed: ok");
|
|
437
|
+
});
|
|
438
|
+
|
|
439
|
+
test("local runs essential + dev, never prod", async () => {
|
|
440
|
+
const { order } = await seedWith("local");
|
|
441
|
+
expect(order).toEqual(["a", "b", "dev"]);
|
|
442
|
+
});
|
|
443
|
+
|
|
444
|
+
test("docker runs essential + dev, never prod", async () => {
|
|
445
|
+
const { order } = await seedWith("docker");
|
|
446
|
+
expect(order).toEqual(["a", "b", "dev"]);
|
|
447
|
+
});
|
|
448
|
+
|
|
449
|
+
test("prod runs essential + prod, never dev", async () => {
|
|
450
|
+
const { order } = await seedWith("prod");
|
|
451
|
+
expect(order).toEqual(["a", "b", "prod"]);
|
|
452
|
+
});
|
|
453
|
+
|
|
454
|
+
test("test runs essential only", async () => {
|
|
455
|
+
const { order, out } = await seedWith("test");
|
|
456
|
+
expect(order).toEqual(["a", "b"]);
|
|
457
|
+
expect(out).toContain("essential only");
|
|
458
|
+
});
|
|
459
|
+
|
|
460
|
+
test("prod without --force requires confirmEnv", async () => {
|
|
461
|
+
const out: string[] = [];
|
|
462
|
+
let asked = false;
|
|
463
|
+
const code = await runSeed({
|
|
464
|
+
env: "prod",
|
|
465
|
+
force: false,
|
|
466
|
+
stdinIsTTY: true,
|
|
467
|
+
seedDef: defineSeed({ essential: async () => {} }),
|
|
468
|
+
createFx: async () => ({
|
|
469
|
+
fx: createFx({ flow: "oke.db.seed.test" }),
|
|
470
|
+
stop: async () => {},
|
|
471
|
+
}),
|
|
472
|
+
confirmEnv: async () => {
|
|
473
|
+
asked = true;
|
|
474
|
+
return false;
|
|
475
|
+
},
|
|
476
|
+
write: (t) => out.push(t),
|
|
477
|
+
});
|
|
478
|
+
expect(asked).toBe(true);
|
|
479
|
+
expect(code).toBe(EXIT_USAGE);
|
|
480
|
+
expect(out.join("")).toContain("cancelled");
|
|
481
|
+
});
|
|
482
|
+
|
|
483
|
+
test("prod with --force skips confirm", async () => {
|
|
484
|
+
let asked = false;
|
|
485
|
+
const { code } = await seedWith("prod", {
|
|
486
|
+
force: true,
|
|
487
|
+
confirmEnv: async () => {
|
|
488
|
+
asked = true;
|
|
489
|
+
return false;
|
|
490
|
+
},
|
|
491
|
+
});
|
|
492
|
+
expect(asked).toBe(false);
|
|
493
|
+
expect(code).toBe(EXIT_OK);
|
|
494
|
+
});
|
|
495
|
+
|
|
496
|
+
test("docker confirm proceeds when confirmEnv returns true", async () => {
|
|
497
|
+
const out: string[] = [];
|
|
498
|
+
const order: string[] = [];
|
|
499
|
+
const code = await runSeed({
|
|
500
|
+
env: "docker",
|
|
501
|
+
force: false,
|
|
502
|
+
stdinIsTTY: true,
|
|
503
|
+
seedDef: trackingDef(order),
|
|
504
|
+
createFx: async () => ({
|
|
505
|
+
fx: createFx({ flow: "oke.db.seed.test" }),
|
|
506
|
+
stop: async () => {},
|
|
507
|
+
}),
|
|
508
|
+
confirmEnv: async (env, target) => {
|
|
509
|
+
expect(env).toBe("docker");
|
|
510
|
+
expect(target.length).toBeGreaterThan(0);
|
|
511
|
+
return true;
|
|
512
|
+
},
|
|
513
|
+
write: (t) => out.push(t),
|
|
514
|
+
});
|
|
515
|
+
expect(code).toBe(EXIT_OK);
|
|
516
|
+
expect(order).toEqual(["a", "b", "dev"]);
|
|
517
|
+
expect(out.join("")).toContain("oke db seed:");
|
|
518
|
+
});
|
|
519
|
+
|
|
520
|
+
test("executeSeedDef is the category source of truth", async () => {
|
|
521
|
+
const order: string[] = [];
|
|
522
|
+
const out: string[] = [];
|
|
523
|
+
const fx = createFx({ flow: "x" });
|
|
524
|
+
await executeSeedDef(trackingDef(order), "prod", fx, (t) => out.push(t));
|
|
525
|
+
expect(order).toEqual(["a", "b", "prod"]);
|
|
526
|
+
});
|
|
527
|
+
});
|
package/src/cli/db.ts
CHANGED
|
@@ -1,5 +1,6 @@
|
|
|
1
1
|
/**
|
|
2
|
-
* `oke db push|generate|migrate` — domain schema sync via drizzle-kit
|
|
2
|
+
* `oke db push|generate|migrate|seed` — domain schema sync via drizzle-kit,
|
|
3
|
+
* plus explicit seed runs.
|
|
3
4
|
*
|
|
4
5
|
* Distinct from `oke schema generate` (core/plugin stub tables).
|
|
5
6
|
* When abstract decls exist (`src/schema.decl.ts`), emits Drizzle first —
|
|
@@ -15,11 +16,24 @@ import {
|
|
|
15
16
|
import { loadPluginTablesFromAppEntry } from "../elements/store/load-plugin-tables.ts";
|
|
16
17
|
import { resolveDriverId, type ConfigEnv, type OkeConfig } from "../config/index.ts";
|
|
17
18
|
import type { TableContribution } from "../kernel/plugin.ts";
|
|
19
|
+
import { runSeed, type SeedOptions } from "./db-seed.ts";
|
|
18
20
|
import { resolveDrizzleKitEnv } from "./drizzle-env.ts";
|
|
19
21
|
import { resolveDevSqlEnv } from "./resolve-dev-sql-env.ts";
|
|
20
22
|
import { EXIT_OK, EXIT_RUNTIME, EXIT_USAGE } from "./exit.ts";
|
|
21
23
|
import { loadOkeConfig } from "./load-config.ts";
|
|
22
24
|
|
|
25
|
+
export {
|
|
26
|
+
executeSeedDef,
|
|
27
|
+
formatSeedTarget,
|
|
28
|
+
isSeedDef,
|
|
29
|
+
loadSeedDef,
|
|
30
|
+
redactConnectionTarget,
|
|
31
|
+
runSeed,
|
|
32
|
+
runSeedFns,
|
|
33
|
+
type SeedOptions,
|
|
34
|
+
type SeedTally,
|
|
35
|
+
} from "./db-seed.ts";
|
|
36
|
+
|
|
23
37
|
/**
|
|
24
38
|
* Apply a drizzle-kit env overlay to `process.env` for the in-process SDK,
|
|
25
39
|
* returning a restore function. drizzle-kit's `push`/`generate` read
|
|
@@ -67,7 +81,7 @@ export async function withDrizzleKitEnv<T>(
|
|
|
67
81
|
}
|
|
68
82
|
|
|
69
83
|
/** Subcommands under `oke db`. */
|
|
70
|
-
export type DbSubcommand = "push" | "generate" | "migrate";
|
|
84
|
+
export type DbSubcommand = "push" | "generate" | "migrate" | "seed";
|
|
71
85
|
|
|
72
86
|
/** Options for {@link runDb}. */
|
|
73
87
|
export interface DbOptions {
|
|
@@ -104,6 +118,15 @@ export interface DbOptions {
|
|
|
104
118
|
readonly pluginTables?: readonly TableContribution[];
|
|
105
119
|
/** Override app entry path for plugin-table discovery. */
|
|
106
120
|
readonly entry?: string;
|
|
121
|
+
/** Skip docker/prod confirm for seed (`--force`). */
|
|
122
|
+
readonly force?: boolean;
|
|
123
|
+
/** Seed module path override. */
|
|
124
|
+
readonly seedPath?: string;
|
|
125
|
+
/** Seed-only injectables forwarded to {@link runSeed}. */
|
|
126
|
+
readonly seedDef?: SeedOptions["seedDef"];
|
|
127
|
+
readonly createFx?: SeedOptions["createFx"];
|
|
128
|
+
readonly confirmEnv?: SeedOptions["confirmEnv"];
|
|
129
|
+
readonly stdinIsTTY?: boolean;
|
|
107
130
|
}
|
|
108
131
|
|
|
109
132
|
/** Minimal drizzle-kit envelope we handle. */
|
|
@@ -135,7 +158,7 @@ export async function resolveDrizzleConfigPath(
|
|
|
135
158
|
/**
|
|
136
159
|
* Run `oke db <subcommand>`.
|
|
137
160
|
*
|
|
138
|
-
* @param sub - push | generate | migrate
|
|
161
|
+
* @param sub - push | generate | migrate | seed
|
|
139
162
|
* @param options - Paths / injectables
|
|
140
163
|
*/
|
|
141
164
|
export async function runDb(sub: DbSubcommand, options: DbOptions = {}): Promise<number> {
|
|
@@ -143,6 +166,22 @@ export async function runDb(sub: DbSubcommand, options: DbOptions = {}): Promise
|
|
|
143
166
|
const cwd = options.cwd ?? process.cwd();
|
|
144
167
|
const loaded = await loadOkeConfig(cwd).catch(() => null);
|
|
145
168
|
const env = options.env ?? (await resolveDevSqlEnv(cwd));
|
|
169
|
+
|
|
170
|
+
if (sub === "seed") {
|
|
171
|
+
return runSeed({
|
|
172
|
+
cwd,
|
|
173
|
+
write,
|
|
174
|
+
env,
|
|
175
|
+
force: options.force,
|
|
176
|
+
seedPath: options.seedPath,
|
|
177
|
+
entry: options.entry,
|
|
178
|
+
seedDef: options.seedDef,
|
|
179
|
+
createFx: options.createFx,
|
|
180
|
+
confirmEnv: options.confirmEnv,
|
|
181
|
+
stdinIsTTY: options.stdinIsTTY,
|
|
182
|
+
});
|
|
183
|
+
}
|
|
184
|
+
|
|
146
185
|
if (!options.skipEmit) {
|
|
147
186
|
await emitAbstractSchemaPrestep(cwd, loaded?.config, write, env, {
|
|
148
187
|
pluginTables: options.pluginTables,
|
|
@@ -399,16 +438,16 @@ export async function detectDbDrift(options: DbOptions = {}): Promise<{
|
|
|
399
438
|
}
|
|
400
439
|
|
|
401
440
|
/**
|
|
402
|
-
* CLI entry: parse `oke db <push|generate|migrate> [
|
|
441
|
+
* CLI entry: parse `oke db <push|generate|migrate|seed> […]`.
|
|
403
442
|
*
|
|
404
443
|
* @param args - Args after `db`
|
|
405
444
|
*/
|
|
406
445
|
export async function dbCli(args: readonly string[]): Promise<number> {
|
|
407
446
|
const sub = args[0];
|
|
408
447
|
if (!sub || sub === "--help" || sub === "-h") {
|
|
409
|
-
console.log(`oke db push|generate|migrate [--config|-c
|
|
448
|
+
console.log(`oke db push|generate|migrate|seed [--config|-c] [--env name] [--force]
|
|
410
449
|
|
|
411
|
-
Domain schema sync via drizzle-kit.
|
|
450
|
+
Domain schema sync via drizzle-kit, plus explicit seed.
|
|
412
451
|
When src/schema.decl.ts and/or a plugged app entry exists, emits
|
|
413
452
|
schema.generated.ts from store.schema.table + live plugin .table()
|
|
414
453
|
contributions — then runs drizzle-kit.
|
|
@@ -418,24 +457,52 @@ Not the same as \`oke schema generate\` (core/plugin stub tables).
|
|
|
418
457
|
push Apply schema to the live local DB (dev; no migration files)
|
|
419
458
|
generate Write versioned SQL under drizzle/ for review
|
|
420
459
|
migrate Apply generated migrations (explicit; never automatic in prod)
|
|
460
|
+
seed Run defineSeed (essential + env category); never at boot
|
|
461
|
+
|
|
462
|
+
--env Override config env (local|docker|test|prod)
|
|
463
|
+
--force Skip docker/prod confirmation prompt (CI)
|
|
464
|
+
--entry App entry for seed boot / plugin table discovery
|
|
421
465
|
`);
|
|
422
466
|
return sub ? EXIT_OK : EXIT_USAGE;
|
|
423
467
|
}
|
|
424
|
-
if (sub !== "push" && sub !== "generate" && sub !== "migrate") {
|
|
468
|
+
if (sub !== "push" && sub !== "generate" && sub !== "migrate" && sub !== "seed") {
|
|
425
469
|
console.error(`oke db: unknown subcommand "${sub}"`);
|
|
426
470
|
return EXIT_USAGE;
|
|
427
471
|
}
|
|
428
472
|
|
|
429
473
|
let config: string | undefined;
|
|
474
|
+
let env: ConfigEnv | undefined;
|
|
475
|
+
let force = false;
|
|
476
|
+
let entry: string | undefined;
|
|
430
477
|
for (let i = 1; i < args.length; i++) {
|
|
431
478
|
const a = args[i]!;
|
|
432
479
|
if (a === "--config" || a === "-c") config = args[++i];
|
|
480
|
+
else if (a === "--env") {
|
|
481
|
+
const value = args[++i];
|
|
482
|
+
const parsed = parseConfigEnv(value);
|
|
483
|
+
if (!parsed) {
|
|
484
|
+
console.error(`oke db: invalid --env ${JSON.stringify(value)} (local|docker|test|prod)`);
|
|
485
|
+
return EXIT_USAGE;
|
|
486
|
+
}
|
|
487
|
+
env = parsed;
|
|
488
|
+
} else if (a === "--force") force = true;
|
|
489
|
+
else if (a === "--entry" || a === "-e") entry = args[++i];
|
|
433
490
|
else if (a === "--help" || a === "-h") {
|
|
434
491
|
return dbCli(["--help"]);
|
|
492
|
+
} else if (a.startsWith("-")) {
|
|
493
|
+
console.error(`oke db ${sub}: unknown flag ${a}`);
|
|
494
|
+
return EXIT_USAGE;
|
|
435
495
|
}
|
|
436
496
|
}
|
|
437
497
|
|
|
438
|
-
return runDb(sub, { config });
|
|
498
|
+
return runDb(sub, { config, env, force, entry });
|
|
499
|
+
}
|
|
500
|
+
|
|
501
|
+
function parseConfigEnv(value: string | undefined): ConfigEnv | undefined {
|
|
502
|
+
if (value === "local" || value === "docker" || value === "test" || value === "prod") {
|
|
503
|
+
return value;
|
|
504
|
+
}
|
|
505
|
+
return undefined;
|
|
439
506
|
}
|
|
440
507
|
|
|
441
508
|
function reportKitResult(verb: string, result: DbKitResult, write: (text: string) => void): number {
|
|
@@ -37,6 +37,28 @@ describe("defaultImagesFromConfig", () => {
|
|
|
37
37
|
expect(images["store.sql"]).toBe("pgvector/pgvector:pg17");
|
|
38
38
|
});
|
|
39
39
|
|
|
40
|
+
test("clock postgres alone pulls store.sql image", () => {
|
|
41
|
+
const images = defaultImagesFromConfig({
|
|
42
|
+
drivers: {
|
|
43
|
+
clock: { prod: "postgres" },
|
|
44
|
+
},
|
|
45
|
+
});
|
|
46
|
+
expect(images).toEqual({
|
|
47
|
+
"store.sql": "postgres:18-alpine",
|
|
48
|
+
});
|
|
49
|
+
});
|
|
50
|
+
|
|
51
|
+
test("journal postgres alone pulls store.sql image", () => {
|
|
52
|
+
const images = defaultImagesFromConfig({
|
|
53
|
+
drivers: {
|
|
54
|
+
journal: { prod: "postgres" },
|
|
55
|
+
},
|
|
56
|
+
});
|
|
57
|
+
expect(images).toEqual({
|
|
58
|
+
"store.sql": "postgres:18-alpine",
|
|
59
|
+
});
|
|
60
|
+
});
|
|
61
|
+
|
|
40
62
|
test("returns empty when no container drivers", () => {
|
|
41
63
|
expect(defaultImagesFromConfig({})).toEqual({});
|
|
42
64
|
expect(
|
package/src/cli/load-config.ts
CHANGED
|
@@ -47,13 +47,18 @@ export function defaultImagesFromConfig(config: OkeConfig): Readonly<Record<stri
|
|
|
47
47
|
const index = dockerOrProdId(config.drivers?.store?.index);
|
|
48
48
|
const kv = dockerOrProdId(config.drivers?.store?.kv);
|
|
49
49
|
const signal = dockerOrProdId(config.drivers?.signal);
|
|
50
|
+
const clock = dockerOrProdId(config.drivers?.clock);
|
|
51
|
+
const journal = dockerOrProdId(config.drivers?.journal);
|
|
50
52
|
|
|
51
|
-
//
|
|
52
|
-
//
|
|
53
|
+
// SQL-consuming facets pull a SQL image. Signal postgres still needs a
|
|
54
|
+
// LISTEN/NOTIFY client (not Bun.SQL); clock + journal postgres are real
|
|
55
|
+
// SKIP LOCKED stores (CronStore / durable-run journal).
|
|
53
56
|
const needsSql =
|
|
54
57
|
sql === "postgres" ||
|
|
55
58
|
sql === "pgvector" ||
|
|
56
59
|
index === "pgvector" ||
|
|
60
|
+
clock === "postgres" ||
|
|
61
|
+
journal === "postgres" ||
|
|
57
62
|
(config.drivers?.prod ?? []).some((p) => p === "postgres" || p === "pgvector");
|
|
58
63
|
|
|
59
64
|
if (needsSql) {
|
package/src/cli/registry.ts
CHANGED
|
@@ -202,7 +202,7 @@ export const OKE_COMMANDS: readonly CliCommand[] = [
|
|
|
202
202
|
},
|
|
203
203
|
{
|
|
204
204
|
name: "db",
|
|
205
|
-
summary: "domain schema — push · generate · migrate",
|
|
205
|
+
summary: "domain schema — push · generate · migrate · seed",
|
|
206
206
|
subcommands: [
|
|
207
207
|
{
|
|
208
208
|
name: "push",
|
|
@@ -215,6 +215,12 @@ export const OKE_COMMANDS: readonly CliCommand[] = [
|
|
|
215
215
|
valueName: "path",
|
|
216
216
|
summary: "drizzle.config.ts path",
|
|
217
217
|
},
|
|
218
|
+
{
|
|
219
|
+
long: "--env",
|
|
220
|
+
takesValue: true,
|
|
221
|
+
valueName: "name",
|
|
222
|
+
summary: "Config env (local|docker|test|prod)",
|
|
223
|
+
},
|
|
218
224
|
HELP,
|
|
219
225
|
],
|
|
220
226
|
},
|
|
@@ -229,6 +235,12 @@ export const OKE_COMMANDS: readonly CliCommand[] = [
|
|
|
229
235
|
valueName: "path",
|
|
230
236
|
summary: "drizzle.config.ts path",
|
|
231
237
|
},
|
|
238
|
+
{
|
|
239
|
+
long: "--env",
|
|
240
|
+
takesValue: true,
|
|
241
|
+
valueName: "name",
|
|
242
|
+
summary: "Config env (local|docker|test|prod)",
|
|
243
|
+
},
|
|
232
244
|
HELP,
|
|
233
245
|
],
|
|
234
246
|
},
|
|
@@ -243,6 +255,30 @@ export const OKE_COMMANDS: readonly CliCommand[] = [
|
|
|
243
255
|
valueName: "path",
|
|
244
256
|
summary: "drizzle.config.ts path",
|
|
245
257
|
},
|
|
258
|
+
{
|
|
259
|
+
long: "--env",
|
|
260
|
+
takesValue: true,
|
|
261
|
+
valueName: "name",
|
|
262
|
+
summary: "Config env (local|docker|test|prod)",
|
|
263
|
+
},
|
|
264
|
+
HELP,
|
|
265
|
+
],
|
|
266
|
+
},
|
|
267
|
+
{
|
|
268
|
+
name: "seed",
|
|
269
|
+
summary: "Run defineSeed (essential + env category); never at boot",
|
|
270
|
+
flags: [
|
|
271
|
+
{
|
|
272
|
+
long: "--env",
|
|
273
|
+
takesValue: true,
|
|
274
|
+
valueName: "name",
|
|
275
|
+
summary: "Config env (local|docker|test|prod)",
|
|
276
|
+
},
|
|
277
|
+
{
|
|
278
|
+
long: "--force",
|
|
279
|
+
summary: "Skip docker/prod confirmation prompt",
|
|
280
|
+
},
|
|
281
|
+
ENTRY,
|
|
246
282
|
HELP,
|
|
247
283
|
],
|
|
248
284
|
},
|
package/src/config/index.ts
CHANGED
|
@@ -73,6 +73,8 @@ export interface DriversConfig {
|
|
|
73
73
|
readonly store?: StoreDriversConfig;
|
|
74
74
|
readonly signal?: EnvDriverMap;
|
|
75
75
|
readonly clock?: EnvDriverMap;
|
|
76
|
+
/** Durable-run journal: `memory` · `file` · `postgres`. */
|
|
77
|
+
readonly journal?: EnvDriverMap;
|
|
76
78
|
readonly vault?: EnvDriverMap;
|
|
77
79
|
readonly channel?: ChannelDriversConfig;
|
|
78
80
|
readonly ai?: EnvDriverMap;
|
|
@@ -283,6 +285,7 @@ export function normalizeDriversConfig(
|
|
|
283
285
|
...(store !== undefined ? { store } : {}),
|
|
284
286
|
signal: normalizeEnvDriverMap(drivers.signal as RawEnvDriverMap),
|
|
285
287
|
clock: normalizeEnvDriverMap(drivers.clock as RawEnvDriverMap),
|
|
288
|
+
journal: normalizeEnvDriverMap(drivers.journal as RawEnvDriverMap),
|
|
286
289
|
vault: normalizeEnvDriverMap(drivers.vault as RawEnvDriverMap),
|
|
287
290
|
...(channel !== undefined ? { channel } : {}),
|
|
288
291
|
ai: normalizeEnvDriverMap(drivers.ai as RawEnvDriverMap),
|
|
@@ -333,6 +336,7 @@ export function fillDriversDockerFromProd(
|
|
|
333
336
|
...(store !== undefined ? { store } : {}),
|
|
334
337
|
signal: fillDockerFromProd(drivers.signal),
|
|
335
338
|
clock: fillDockerFromProd(drivers.clock),
|
|
339
|
+
journal: fillDockerFromProd(drivers.journal),
|
|
336
340
|
vault: fillDockerFromProd(drivers.vault),
|
|
337
341
|
...(channel !== undefined ? { channel } : {}),
|
|
338
342
|
ai: fillDockerFromProd(drivers.ai),
|