okengine 0.7.0 → 0.9.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 +3 -2
- package/site/content/docs/deployment/docker-swarm.mdx +228 -0
- package/site/content/docs/deployment/docker.mdx +212 -0
- package/site/content/docs/deployment/index.mdx +83 -0
- package/site/content/docs/deployment/kubernetes.mdx +176 -0
- package/site/content/docs/deployment/meta.json +5 -0
- package/site/content/docs/deployment/reverse-proxy.mdx +216 -0
- package/site/content/docs/elements/channel.mdx +25 -12
- package/site/content/docs/elements/clock.mdx +17 -15
- package/site/content/docs/elements/flow.mdx +6 -2
- package/site/content/docs/elements/signal.mdx +10 -8
- package/site/content/docs/elements/store.mdx +165 -0
- package/site/content/docs/get-started/index.mdx +5 -0
- package/site/content/docs/get-started/installation.mdx +18 -16
- package/site/content/docs/index.mdx +5 -0
- package/site/content/docs/meta.json +10 -1
- package/site/content/docs/plugins/index.mdx +1 -2
- package/site/content/docs/plugins/magic-link.mdx +44 -2
- package/site/content/docs/plugins/meta.json +1 -2
- package/site/content/docs/plugins/otp.mdx +202 -0
- package/site/content/docs/plugins/two-factor.mdx +2 -1
- package/site/content/docs/reference/cli.md +5 -2
- package/site/content/docs/reference/configuration.mdx +21 -3
- package/site/content/docs/reference/environment-variables.mdx +21 -8
- package/site/content/docs/reference/plugins.mdx +1 -1
- package/src/auth/auth.test.ts +36 -0
- package/src/auth/bindings.ts +3 -12
- package/src/auth/identity.ts +33 -0
- package/src/auth/index.ts +5 -0
- package/src/auth/otp-capability.ts +119 -0
- package/src/auth/otp-seal.test.ts +61 -0
- package/src/auth/otp-seal.ts +84 -0
- package/src/auth/schema.ts +3 -0
- package/src/auth/sessions.ts +26 -27
- package/src/auth/tables.ts +4 -0
- package/src/auth/verification.ts +61 -1
- 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/dev-app-runner.ts +4 -0
- package/src/cli/docker.ts +4 -1
- package/src/cli/load-config.images.test.ts +26 -0
- package/src/cli/load-config.ts +10 -2
- package/src/cli/registry.ts +38 -2
- package/src/compiler/effects-infer.ts +1 -0
- package/src/config/index.ts +4 -0
- package/src/console/server/operator-db.ts +34 -9
- package/src/docker/compose.ts +162 -6
- package/src/docker/derive.ts +60 -3
- package/src/docker/docker.test.ts +374 -1
- package/src/docker/helpers.ts +2 -0
- package/src/docker/index.ts +11 -0
- package/src/docker/recipes/caddy.ts +51 -0
- package/src/docker/recipes/dragonfly.ts +31 -0
- package/src/docker/recipes/index.ts +25 -2
- package/src/docker/recipes/pgdog.ts +84 -0
- package/src/docker/recipes/redis.ts +6 -3
- package/src/docker/recipes/traefik.ts +83 -0
- package/src/docker/recipes/valkey.ts +30 -0
- package/src/docker/stack-id.ts +5 -0
- package/src/docker/types.ts +18 -0
- package/src/drivers/channel-sently.test.ts +8 -0
- package/src/drivers/channel-taqnyat-mail.ts +34 -0
- package/src/drivers/channel-taqnyat-whatsapp.ts +94 -0
- package/src/drivers/channel-types.ts +72 -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/otp-delivery.test.ts +76 -0
- package/src/elements/channel/otp-delivery.ts +291 -0
- package/src/elements/channel/runtime.ts +203 -114
- package/src/elements/channel.test.ts +71 -0
- package/src/elements/channel.ts +12 -2
- 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 +18 -0
- package/src/kernel/app.ts +221 -14
- package/src/kernel/boot-bind/channel.test.ts +16 -0
- package/src/kernel/boot-bind/channel.ts +64 -0
- package/src/kernel/boot-bind/clock.ts +17 -6
- package/src/kernel/boot-bind/gate.ts +14 -19
- package/src/kernel/boot-bind/honor-config.test.ts +123 -4
- package/src/kernel/boot-bind/journal.ts +89 -0
- package/src/kernel/boot-bind/signal.ts +20 -0
- package/src/kernel/boot-bind/store.test.ts +82 -0
- package/src/kernel/boot-bind/store.ts +22 -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 +9 -0
- package/src/kernel/fx.ts +175 -5
- package/src/kernel/graceful-shutdown.test.ts +76 -0
- package/src/kernel/graceful-shutdown.ts +106 -0
- package/src/kernel/horizontal-child.ts +257 -0
- package/src/kernel/horizontal.integration.test.ts +229 -0
- package/src/kernel/index.ts +14 -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/kernel/ready.test.ts +76 -0
- package/src/plugins/auth-delivery.mailpit.integration.test.ts +5 -5
- package/src/plugins/auth-methods.security.test.ts +20 -27
- package/src/plugins/auth-methods.test.ts +7 -6
- package/src/plugins/index.ts +12 -8
- package/src/plugins/magic-link.ts +1 -23
- package/src/plugins/otp.test.ts +236 -0
- package/src/plugins/otp.ts +570 -0
- package/src/plugins/taqnyat.live.test.ts +172 -0
- package/src/release/official-plugins.ts +1 -2
- package/site/content/docs/plugins/email-otp.mdx +0 -117
- package/site/content/docs/plugins/phone-number.mdx +0 -111
- package/src/plugins/email-otp.ts +0 -214
- package/src/plugins/phone-number.ts +0 -149
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 {
|
|
@@ -17,6 +17,7 @@
|
|
|
17
17
|
|
|
18
18
|
import { resolve } from "node:path";
|
|
19
19
|
import { pathToFileURL } from "node:url";
|
|
20
|
+
import { installGracefulShutdown, type GracefulShutdownApp } from "../kernel/graceful-shutdown.ts";
|
|
20
21
|
import { createBunRuntime } from "../runtime/bun.ts";
|
|
21
22
|
import { APP_PORT, type FetchApp } from "../runtime/types.ts";
|
|
22
23
|
import { clearTerminalScreen, formatAppReadyLine, formatDevHero } from "../term.ts";
|
|
@@ -33,6 +34,8 @@ type DevRunnerGlobals = typeof globalThis & {
|
|
|
33
34
|
/** App entry shape — FetchApp plus boot before serve. */
|
|
34
35
|
type BootableApp = FetchApp & {
|
|
35
36
|
boot(): Promise<unknown>;
|
|
37
|
+
stop(): Promise<void>;
|
|
38
|
+
readonly bootResult?: GracefulShutdownApp["bootResult"];
|
|
36
39
|
};
|
|
37
40
|
|
|
38
41
|
const entry = Bun.env["OKE_ENTRY"];
|
|
@@ -68,6 +71,7 @@ const handle = createBunRuntime().serve(mod.app, {
|
|
|
68
71
|
hostname,
|
|
69
72
|
id: DEV_APP_SERVE_ID,
|
|
70
73
|
});
|
|
74
|
+
installGracefulShutdown({ app: mod.app, handle });
|
|
71
75
|
|
|
72
76
|
const appUrl = `http://${formatHostForUrl(hostname)}:${handle.port}`;
|
|
73
77
|
const g = globalThis as DevRunnerGlobals;
|
package/src/cli/docker.ts
CHANGED
|
@@ -105,7 +105,10 @@ export async function runDockerDerive(
|
|
|
105
105
|
write(
|
|
106
106
|
`compose merge order (cwd ${composeDir}/):\n${result.composeFiles.map((f) => ` -f ${f}`).join("\n")}\n`,
|
|
107
107
|
);
|
|
108
|
-
write(
|
|
108
|
+
write(
|
|
109
|
+
`(${composeDir}/compose.override.yml is layer 4 — user-owned, never written by oke)\n` +
|
|
110
|
+
`(${composeDir}/compose.all.yml is layers 1–3 merged — single-file alternative for Swarm / one \`-c\`)\n`,
|
|
111
|
+
);
|
|
109
112
|
return { code: 0, result };
|
|
110
113
|
} catch (err) {
|
|
111
114
|
console.error(err instanceof Error ? err.message : String(err));
|
|
@@ -21,6 +21,7 @@ describe("defaultImagesFromConfig", () => {
|
|
|
21
21
|
});
|
|
22
22
|
expect(images).toEqual({
|
|
23
23
|
"store.sql": "postgres:18-alpine",
|
|
24
|
+
pgdog: "ghcr.io/pgdogdev/pgdog:v0.1.51",
|
|
24
25
|
"store.kv": "redis:8-alpine",
|
|
25
26
|
});
|
|
26
27
|
});
|
|
@@ -35,6 +36,31 @@ describe("defaultImagesFromConfig", () => {
|
|
|
35
36
|
},
|
|
36
37
|
});
|
|
37
38
|
expect(images["store.sql"]).toBe("pgvector/pgvector:pg17");
|
|
39
|
+
expect(images.pgdog).toBe("ghcr.io/pgdogdev/pgdog:v0.1.51");
|
|
40
|
+
});
|
|
41
|
+
|
|
42
|
+
test("clock postgres alone pulls store.sql image", () => {
|
|
43
|
+
const images = defaultImagesFromConfig({
|
|
44
|
+
drivers: {
|
|
45
|
+
clock: { prod: "postgres" },
|
|
46
|
+
},
|
|
47
|
+
});
|
|
48
|
+
expect(images).toEqual({
|
|
49
|
+
"store.sql": "postgres:18-alpine",
|
|
50
|
+
pgdog: "ghcr.io/pgdogdev/pgdog:v0.1.51",
|
|
51
|
+
});
|
|
52
|
+
});
|
|
53
|
+
|
|
54
|
+
test("journal postgres alone pulls store.sql image", () => {
|
|
55
|
+
const images = defaultImagesFromConfig({
|
|
56
|
+
drivers: {
|
|
57
|
+
journal: { prod: "postgres" },
|
|
58
|
+
},
|
|
59
|
+
});
|
|
60
|
+
expect(images).toEqual({
|
|
61
|
+
"store.sql": "postgres:18-alpine",
|
|
62
|
+
pgdog: "ghcr.io/pgdogdev/pgdog:v0.1.51",
|
|
63
|
+
});
|
|
38
64
|
});
|
|
39
65
|
|
|
40
66
|
test("returns empty when no container drivers", () => {
|
package/src/cli/load-config.ts
CHANGED
|
@@ -9,6 +9,7 @@ import type { Manifest } from "../manifest/types.ts";
|
|
|
9
9
|
/** Default image pins when `images` is omitted but prod drivers need containers. */
|
|
10
10
|
const DEFAULT_SQL_IMAGE = "postgres:18-alpine";
|
|
11
11
|
const DEFAULT_PGVECTOR_IMAGE = "pgvector/pgvector:pg17";
|
|
12
|
+
const DEFAULT_PGDOG_IMAGE = "ghcr.io/pgdogdev/pgdog:v0.1.51";
|
|
12
13
|
const DEFAULT_KV_IMAGE = "redis:8-alpine";
|
|
13
14
|
const DEFAULT_VAULT_IMAGE = "openbao/openbao:2.6.1";
|
|
14
15
|
|
|
@@ -47,18 +48,25 @@ export function defaultImagesFromConfig(config: OkeConfig): Readonly<Record<stri
|
|
|
47
48
|
const index = dockerOrProdId(config.drivers?.store?.index);
|
|
48
49
|
const kv = dockerOrProdId(config.drivers?.store?.kv);
|
|
49
50
|
const signal = dockerOrProdId(config.drivers?.signal);
|
|
51
|
+
const clock = dockerOrProdId(config.drivers?.clock);
|
|
52
|
+
const journal = dockerOrProdId(config.drivers?.journal);
|
|
50
53
|
|
|
51
|
-
//
|
|
52
|
-
//
|
|
54
|
+
// SQL-consuming facets pull a SQL image. Signal postgres still needs a
|
|
55
|
+
// LISTEN/NOTIFY client (not Bun.SQL); clock + journal postgres are real
|
|
56
|
+
// SKIP LOCKED stores (CronStore / durable-run journal).
|
|
53
57
|
const needsSql =
|
|
54
58
|
sql === "postgres" ||
|
|
55
59
|
sql === "pgvector" ||
|
|
56
60
|
index === "pgvector" ||
|
|
61
|
+
clock === "postgres" ||
|
|
62
|
+
journal === "postgres" ||
|
|
57
63
|
(config.drivers?.prod ?? []).some((p) => p === "postgres" || p === "pgvector");
|
|
58
64
|
|
|
59
65
|
if (needsSql) {
|
|
60
66
|
out["store.sql"] =
|
|
61
67
|
sql === "pgvector" || index === "pgvector" ? DEFAULT_PGVECTOR_IMAGE : DEFAULT_SQL_IMAGE;
|
|
68
|
+
// PgDog in front of Postgres — caps real server connections under horizontal scale.
|
|
69
|
+
out.pgdog = DEFAULT_PGDOG_IMAGE;
|
|
62
70
|
}
|
|
63
71
|
|
|
64
72
|
const needsKv =
|