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
|
@@ -14,7 +14,7 @@
|
|
|
14
14
|
|
|
15
15
|
import { flow, type AnyFlowDef, type FlowDef, type FlowErrorMap } from "../../kernel/flow.ts";
|
|
16
16
|
import { applyBoundaryContract, stampBoundaryContract } from "../../kernel/boundary-contract.ts";
|
|
17
|
-
import { fail } from "../../kernel/
|
|
17
|
+
import { fail } from "../../kernel/fail-helpers.ts";
|
|
18
18
|
import type { Fx } from "../../kernel/fx.ts";
|
|
19
19
|
import {
|
|
20
20
|
openLiveStream,
|
|
@@ -53,7 +53,11 @@ export interface ResourceOptions {
|
|
|
53
53
|
* used to extend `update` with the path id (wire sends `{ id, ...patch }`).
|
|
54
54
|
*/
|
|
55
55
|
readonly idSchema?: unknown;
|
|
56
|
-
/**
|
|
56
|
+
/**
|
|
57
|
+
* Extra typed errors for get / update / remove.
|
|
58
|
+
*
|
|
59
|
+
* Built-in `NotFound` is always on — omit unless tightening the payload.
|
|
60
|
+
*/
|
|
57
61
|
readonly errors?: FlowErrorMap;
|
|
58
62
|
/** `:id` column; defaults to the table primary key. */
|
|
59
63
|
readonly id?: unknown;
|
|
@@ -191,7 +195,7 @@ export function resource(db: SqlStoreDecl, table: unknown, options: ResourceOpti
|
|
|
191
195
|
const { mode, limit, maxLimit, cursorColumns: resolvedCursor } = scope;
|
|
192
196
|
const countMode = scope.count;
|
|
193
197
|
|
|
194
|
-
const errors = options.errors
|
|
198
|
+
const errors = options.errors;
|
|
195
199
|
const breaking = options.breaking === true;
|
|
196
200
|
|
|
197
201
|
/** Run the list query and shape rows + meta. */
|
|
@@ -309,7 +313,7 @@ export function resource(db: SqlStoreDecl, table: unknown, options: ResourceOpti
|
|
|
309
313
|
findById(t: unknown, id: string): Promise<SqlRow | null>;
|
|
310
314
|
};
|
|
311
315
|
const row = await store.findById(table, String(id));
|
|
312
|
-
if (!row) return fail(
|
|
316
|
+
if (!row) return fail.notFound();
|
|
313
317
|
return row;
|
|
314
318
|
},
|
|
315
319
|
});
|
|
@@ -341,7 +345,7 @@ export function resource(db: SqlStoreDecl, table: unknown, options: ResourceOpti
|
|
|
341
345
|
update(t: unknown): { set(row: SqlRow): { where(w: unknown): Promise<number> } };
|
|
342
346
|
};
|
|
343
347
|
const existing = await store.findById(table, String(id));
|
|
344
|
-
if (!existing) return fail(
|
|
348
|
+
if (!existing) return fail.notFound();
|
|
345
349
|
if (Object.keys(patch).length > 0) {
|
|
346
350
|
await store
|
|
347
351
|
.update(table)
|
|
@@ -349,7 +353,7 @@ export function resource(db: SqlStoreDecl, table: unknown, options: ResourceOpti
|
|
|
349
353
|
.where(leafOp(idDrizzleCol, "=", String(id)));
|
|
350
354
|
}
|
|
351
355
|
const row = await store.findById(table, String(id));
|
|
352
|
-
if (!row) return fail(
|
|
356
|
+
if (!row) return fail.notFound();
|
|
353
357
|
return row;
|
|
354
358
|
},
|
|
355
359
|
});
|
|
@@ -371,7 +375,7 @@ export function resource(db: SqlStoreDecl, table: unknown, options: ResourceOpti
|
|
|
371
375
|
delete(t: unknown, id: string): Promise<boolean>;
|
|
372
376
|
};
|
|
373
377
|
const deleted = await store.delete(table, String(id));
|
|
374
|
-
if (!deleted) return fail(
|
|
378
|
+
if (!deleted) return fail.notFound();
|
|
375
379
|
return fx.json.empty();
|
|
376
380
|
},
|
|
377
381
|
});
|
|
@@ -44,6 +44,7 @@ import type {
|
|
|
44
44
|
StoreDecl,
|
|
45
45
|
} from "./declare.ts";
|
|
46
46
|
import { openSqlKvNamespace } from "./kv-sql.ts";
|
|
47
|
+
import { withStoreErrorMap } from "./store-errors.ts";
|
|
47
48
|
import { createGatedFilesStoreHandle, type GatedFilesFxBridge } from "./files-fx.ts";
|
|
48
49
|
import {
|
|
49
50
|
createFilesImagePipeline,
|
|
@@ -434,14 +435,15 @@ export function createStoreRuntime(options: CreateStoreRuntimeOptions): StoreRun
|
|
|
434
435
|
kvNs.set(decl.name, ns);
|
|
435
436
|
}
|
|
436
437
|
const driverId = ns.driverId;
|
|
438
|
+
const handle = ns;
|
|
437
439
|
return {
|
|
438
440
|
ref: `kv:${decl.name}`,
|
|
439
441
|
driverId,
|
|
440
|
-
get: (key) =>
|
|
441
|
-
set: (key, value, ttl) =>
|
|
442
|
-
delete: (key) =>
|
|
443
|
-
list: (prefix) =>
|
|
444
|
-
ttlMs: (key) =>
|
|
442
|
+
get: (key) => withStoreErrorMap(() => handle.get(key)),
|
|
443
|
+
set: (key, value, ttl) => withStoreErrorMap(() => handle.set(key, value, ttl)),
|
|
444
|
+
delete: (key) => withStoreErrorMap(() => handle.delete(key)),
|
|
445
|
+
list: (prefix) => withStoreErrorMap(() => handle.list(prefix)),
|
|
446
|
+
ttlMs: (key) => withStoreErrorMap(() => handle.ttlMs(key)),
|
|
445
447
|
};
|
|
446
448
|
}
|
|
447
449
|
|
|
@@ -458,19 +460,22 @@ export function createStoreRuntime(options: CreateStoreRuntimeOptions): StoreRun
|
|
|
458
460
|
});
|
|
459
461
|
fileBuckets.set(decl.name, bucket);
|
|
460
462
|
}
|
|
463
|
+
const handle = bucket;
|
|
461
464
|
const access = {
|
|
462
|
-
get: (key: string) =>
|
|
463
|
-
put: (key: string, data: Uint8Array | string) =>
|
|
465
|
+
get: (key: string) => withStoreErrorMap(() => handle.get(key)),
|
|
466
|
+
put: (key: string, data: Uint8Array | string) =>
|
|
467
|
+
withStoreErrorMap(() => handle.put(key, data)),
|
|
464
468
|
};
|
|
465
469
|
return {
|
|
466
470
|
ref: `files:${decl.name}`,
|
|
467
|
-
driverId:
|
|
468
|
-
put: (key, data) =>
|
|
469
|
-
get: (key) =>
|
|
470
|
-
delete: (key) =>
|
|
471
|
-
list: (prefix) =>
|
|
471
|
+
driverId: handle.driverId,
|
|
472
|
+
put: (key, data) => withStoreErrorMap(() => handle.put(key, data)),
|
|
473
|
+
get: (key) => withStoreErrorMap(() => handle.get(key)),
|
|
474
|
+
delete: (key) => withStoreErrorMap(() => handle.delete(key)),
|
|
475
|
+
list: (prefix) => withStoreErrorMap(() => handle.list(prefix)),
|
|
472
476
|
image: (source, imageOpts) => createFilesImagePipeline(access, source, imageOpts),
|
|
473
|
-
putImage: (key, data, putOpts) =>
|
|
477
|
+
putImage: (key, data, putOpts) =>
|
|
478
|
+
withStoreErrorMap(() => putImageToBucket(access, key, data, putOpts)),
|
|
474
479
|
};
|
|
475
480
|
}
|
|
476
481
|
|
|
@@ -3,6 +3,9 @@
|
|
|
3
3
|
*/
|
|
4
4
|
|
|
5
5
|
import { describe, expect, test } from "bun:test";
|
|
6
|
+
import { mkdir, mkdtemp, readFile, rm, writeFile } from "node:fs/promises";
|
|
7
|
+
import { tmpdir } from "node:os";
|
|
8
|
+
import { join, resolve } from "node:path";
|
|
6
9
|
import { plugin } from "../../kernel/plugin.ts";
|
|
7
10
|
import { createRecordingApi } from "../../kernel/registry.ts";
|
|
8
11
|
import {
|
|
@@ -18,9 +21,13 @@ import {
|
|
|
18
21
|
type SqlStoreHandle,
|
|
19
22
|
} from "../store.ts";
|
|
20
23
|
import {
|
|
24
|
+
discoverDeclareRel,
|
|
25
|
+
discoverGeneratedRel,
|
|
21
26
|
emitDrizzleSource,
|
|
22
27
|
GENERATED_SCHEMA_HEADER,
|
|
28
|
+
maybeEmitDomainSchema,
|
|
23
29
|
mergeSchemaTables,
|
|
30
|
+
resolveEmitPaths,
|
|
24
31
|
tablesFromPluginContributions,
|
|
25
32
|
} from "./emit-drizzle.ts";
|
|
26
33
|
|
|
@@ -297,3 +304,174 @@ describe("store.schema RLS extras", () => {
|
|
|
297
304
|
expect(emitDrizzleSource([notes], "postgres")).toContain("pgTable.withRLS");
|
|
298
305
|
});
|
|
299
306
|
});
|
|
307
|
+
|
|
308
|
+
describe("declare path discovery", () => {
|
|
309
|
+
test("schema.ts wins over schema/index.ts", async () => {
|
|
310
|
+
const dir = await mkdtemp(join(tmpdir(), "oke-decl-priority-"));
|
|
311
|
+
await mkdir(join(dir, "src", "db", "schema"), { recursive: true });
|
|
312
|
+
await writeFile(join(dir, "src", "db", "schema.ts"), "export {}\n");
|
|
313
|
+
await writeFile(join(dir, "src", "db", "schema", "index.ts"), "export {}\n");
|
|
314
|
+
expect(discoverDeclareRel(dir)).toBe("src/db/schema.ts");
|
|
315
|
+
expect(resolveEmitPaths(dir).declarePath).toBe(join(dir, "src/db/schema.ts"));
|
|
316
|
+
await rm(dir, { recursive: true, force: true });
|
|
317
|
+
});
|
|
318
|
+
|
|
319
|
+
test("schema.decl.ts when schema.ts is absent", async () => {
|
|
320
|
+
const dir = await mkdtemp(join(tmpdir(), "oke-decl-prev-"));
|
|
321
|
+
await mkdir(join(dir, "src", "db", "schema"), { recursive: true });
|
|
322
|
+
await writeFile(join(dir, "src", "db", "schema.decl.ts"), "export {}\n");
|
|
323
|
+
await writeFile(join(dir, "src", "db", "schema", "index.ts"), "export {}\n");
|
|
324
|
+
expect(discoverDeclareRel(dir)).toBe("src/db/schema.decl.ts");
|
|
325
|
+
await rm(dir, { recursive: true, force: true });
|
|
326
|
+
});
|
|
327
|
+
|
|
328
|
+
test("schema/index.ts when single-file declare is absent", async () => {
|
|
329
|
+
const dir = await mkdtemp(join(tmpdir(), "oke-decl-folder-"));
|
|
330
|
+
await mkdir(join(dir, "src", "db", "schema"), { recursive: true });
|
|
331
|
+
await writeFile(join(dir, "src", "db", "schema", "index.ts"), "export {}\n");
|
|
332
|
+
expect(discoverDeclareRel(dir)).toBe("src/db/schema/index.ts");
|
|
333
|
+
await rm(dir, { recursive: true, force: true });
|
|
334
|
+
});
|
|
335
|
+
|
|
336
|
+
test("db.declare overrides discovery", async () => {
|
|
337
|
+
const dir = await mkdtemp(join(tmpdir(), "oke-decl-override-"));
|
|
338
|
+
await mkdir(join(dir, "src", "db", "schema"), { recursive: true });
|
|
339
|
+
await writeFile(join(dir, "src", "db", "schema.ts"), "export {}\n");
|
|
340
|
+
const resolved = resolveEmitPaths(dir, { declare: "src/db/schema/index.ts" });
|
|
341
|
+
expect(resolved.declarePath).toBe(join(dir, "src/db/schema/index.ts"));
|
|
342
|
+
await rm(dir, { recursive: true, force: true });
|
|
343
|
+
});
|
|
344
|
+
|
|
345
|
+
test("drizzle/index.ts wins over drizzle.ts", async () => {
|
|
346
|
+
const dir = await mkdtemp(join(tmpdir(), "oke-gen-folder-"));
|
|
347
|
+
await mkdir(join(dir, "src", "db", "drizzle"), { recursive: true });
|
|
348
|
+
await writeFile(join(dir, "src", "db", "drizzle", "index.ts"), "export {}\n");
|
|
349
|
+
await writeFile(join(dir, "src", "db", "drizzle.ts"), "export {}\n");
|
|
350
|
+
expect(discoverGeneratedRel(dir, "src/db/schema.ts")).toBe("src/db/drizzle/index.ts");
|
|
351
|
+
await rm(dir, { recursive: true, force: true });
|
|
352
|
+
});
|
|
353
|
+
|
|
354
|
+
test("omitted generated is drizzle/index.ts even if older files exist", async () => {
|
|
355
|
+
const dir = await mkdtemp(join(tmpdir(), "oke-gen-default-"));
|
|
356
|
+
await mkdir(join(dir, "src", "db"), { recursive: true });
|
|
357
|
+
await writeFile(join(dir, "src", "db", "drizzle.ts"), "export {}\n");
|
|
358
|
+
await writeFile(join(dir, "src", "db", "schema.drizzle.ts"), "export {}\n");
|
|
359
|
+
expect(discoverGeneratedRel(dir, "src/db/schema.ts")).toBe("src/db/drizzle/index.ts");
|
|
360
|
+
await rm(dir, { recursive: true, force: true });
|
|
361
|
+
});
|
|
362
|
+
});
|
|
363
|
+
|
|
364
|
+
describe("maybeEmitDomainSchema — barrel declare", () => {
|
|
365
|
+
test("emits from schema/index.ts without schema.ts", async () => {
|
|
366
|
+
const dir = await mkdtemp(join(tmpdir(), "oke-schema-folder-"));
|
|
367
|
+
const schemaDir = join(dir, "src", "db", "schema");
|
|
368
|
+
await mkdir(schemaDir, { recursive: true });
|
|
369
|
+
const oke = resolve(import.meta.dir, "../../index.ts");
|
|
370
|
+
await writeFile(
|
|
371
|
+
join(schemaDir, "posts.ts"),
|
|
372
|
+
`import { store, field } from ${JSON.stringify(oke)};
|
|
373
|
+
export const posts = store.schema.table("posts", {
|
|
374
|
+
id: field.id().primaryKey(),
|
|
375
|
+
title: field.text().notNull(),
|
|
376
|
+
});
|
|
377
|
+
`,
|
|
378
|
+
);
|
|
379
|
+
await writeFile(join(schemaDir, "index.ts"), `export * from "./posts.ts";\n`);
|
|
380
|
+
const result = await maybeEmitDomainSchema({
|
|
381
|
+
cwd: dir,
|
|
382
|
+
dialect: "postgres",
|
|
383
|
+
});
|
|
384
|
+
expect(result.emitted).toBe(true);
|
|
385
|
+
expect(result.tableCount).toBe(1);
|
|
386
|
+
const generated = await readFile(join(dir, "src", "db", "drizzle", "posts.ts"), "utf8");
|
|
387
|
+
expect(generated).toContain('pgTable("posts"');
|
|
388
|
+
const barrel = await readFile(join(dir, "src", "db", "drizzle", "index.ts"), "utf8");
|
|
389
|
+
expect(barrel).toContain('export * from "./posts.ts"');
|
|
390
|
+
await rm(dir, { recursive: true, force: true });
|
|
391
|
+
});
|
|
392
|
+
|
|
393
|
+
test("writes one file per table with FK import + relations", async () => {
|
|
394
|
+
const dir = await mkdtemp(join(tmpdir(), "oke-drizzle-split-"));
|
|
395
|
+
const schemaDir = join(dir, "src", "db", "schema");
|
|
396
|
+
await mkdir(schemaDir, { recursive: true });
|
|
397
|
+
const oke = resolve(import.meta.dir, "../../index.ts");
|
|
398
|
+
await writeFile(
|
|
399
|
+
join(schemaDir, "links.ts"),
|
|
400
|
+
`import { store, field } from ${JSON.stringify(oke)};
|
|
401
|
+
export const links = store.schema.table("links", {
|
|
402
|
+
id: field.id().primaryKey(),
|
|
403
|
+
code: field.text().notNull().unique(),
|
|
404
|
+
});
|
|
405
|
+
`,
|
|
406
|
+
);
|
|
407
|
+
await writeFile(
|
|
408
|
+
join(schemaDir, "daily.ts"),
|
|
409
|
+
`import { store, field } from ${JSON.stringify(oke)};
|
|
410
|
+
import { links } from "./links.ts";
|
|
411
|
+
export const daily = store.schema.table("daily", {
|
|
412
|
+
id: field.id().primaryKey(),
|
|
413
|
+
code: field.text().notNull().references(() => links.code, { onDelete: "cascade" }),
|
|
414
|
+
});
|
|
415
|
+
`,
|
|
416
|
+
);
|
|
417
|
+
await writeFile(
|
|
418
|
+
join(schemaDir, "relations.ts"),
|
|
419
|
+
`import { store } from ${JSON.stringify(oke)};
|
|
420
|
+
import { daily } from "./daily.ts";
|
|
421
|
+
import { links } from "./links.ts";
|
|
422
|
+
export const relations = store.schema.relations({ links, daily }, (r) => ({
|
|
423
|
+
links: { daily: r.many.daily({ from: r.links.code, to: r.daily.code }) },
|
|
424
|
+
daily: { link: r.one.links({ from: r.daily.code, to: r.links.code, optional: false }) },
|
|
425
|
+
}));
|
|
426
|
+
`,
|
|
427
|
+
);
|
|
428
|
+
await writeFile(
|
|
429
|
+
join(schemaDir, "index.ts"),
|
|
430
|
+
`export * from "./links.ts";
|
|
431
|
+
export * from "./daily.ts";
|
|
432
|
+
export * from "./relations.ts";
|
|
433
|
+
`,
|
|
434
|
+
);
|
|
435
|
+
const result = await maybeEmitDomainSchema({ cwd: dir, dialect: "postgres" });
|
|
436
|
+
expect(result.emitted).toBe(true);
|
|
437
|
+
expect(result.tableCount).toBe(2);
|
|
438
|
+
const daily = await readFile(join(dir, "src", "db", "drizzle", "daily.ts"), "utf8");
|
|
439
|
+
expect(daily).toContain('import { links } from "./links.ts"');
|
|
440
|
+
expect(daily).toContain(".references(() => links.code");
|
|
441
|
+
const rel = await readFile(join(dir, "src", "db", "drizzle", "relations.ts"), "utf8");
|
|
442
|
+
expect(rel).toContain("defineRelations({ links, daily }");
|
|
443
|
+
const barrel = await readFile(join(dir, "src", "db", "drizzle", "index.ts"), "utf8");
|
|
444
|
+
expect(barrel).toContain('export * from "./relations.ts"');
|
|
445
|
+
await rm(dir, { recursive: true, force: true });
|
|
446
|
+
});
|
|
447
|
+
|
|
448
|
+
test("prunes stale generated files and drops sibling drizzle.ts", async () => {
|
|
449
|
+
const dir = await mkdtemp(join(tmpdir(), "oke-drizzle-prune-"));
|
|
450
|
+
const schemaDir = join(dir, "src", "db", "schema");
|
|
451
|
+
await mkdir(schemaDir, { recursive: true });
|
|
452
|
+
await mkdir(join(dir, "src", "db", "drizzle"), { recursive: true });
|
|
453
|
+
const oke = resolve(import.meta.dir, "../../index.ts");
|
|
454
|
+
await writeFile(
|
|
455
|
+
join(schemaDir, "posts.ts"),
|
|
456
|
+
`import { store, field } from ${JSON.stringify(oke)};
|
|
457
|
+
export const posts = store.schema.table("posts", {
|
|
458
|
+
id: field.id().primaryKey(),
|
|
459
|
+
});
|
|
460
|
+
`,
|
|
461
|
+
);
|
|
462
|
+
await writeFile(join(schemaDir, "index.ts"), `export * from "./posts.ts";\n`);
|
|
463
|
+
await writeFile(
|
|
464
|
+
join(dir, "src", "db", "drizzle.ts"),
|
|
465
|
+
`// generated by oke — do not edit\nexport const stale = true;\n`,
|
|
466
|
+
);
|
|
467
|
+
await writeFile(
|
|
468
|
+
join(dir, "src", "db", "drizzle", "gone.ts"),
|
|
469
|
+
`// generated by oke — do not edit\nexport const gone = true;\n`,
|
|
470
|
+
);
|
|
471
|
+
await maybeEmitDomainSchema({ cwd: dir, dialect: "postgres" });
|
|
472
|
+
expect(await Bun.file(join(dir, "src", "db", "drizzle.ts")).exists()).toBe(false);
|
|
473
|
+
expect(await Bun.file(join(dir, "src", "db", "drizzle", "gone.ts")).exists()).toBe(false);
|
|
474
|
+
expect(await Bun.file(join(dir, "src", "db", "drizzle", "posts.ts")).exists()).toBe(true);
|
|
475
|
+
await rm(dir, { recursive: true, force: true });
|
|
476
|
+
});
|
|
477
|
+
});
|
|
@@ -0,0 +1,197 @@
|
|
|
1
|
+
/**
|
|
2
|
+
* SQLSTATE / SQLite constraint → typed FlowFailure.
|
|
3
|
+
*/
|
|
4
|
+
|
|
5
|
+
import { describe, expect, test } from "bun:test";
|
|
6
|
+
import { isFlowFailure } from "../../kernel/hooks.ts";
|
|
7
|
+
import { isRetryableSqlError, sqlErrorToFailure } from "./sql-errors.ts";
|
|
8
|
+
|
|
9
|
+
describe("sqlErrorToFailure", () => {
|
|
10
|
+
test("unique / exclusion → Conflict without copying message", () => {
|
|
11
|
+
const unique = sqlErrorToFailure({
|
|
12
|
+
code: "23505",
|
|
13
|
+
message: 'duplicate key value violates unique constraint "users_email_key"',
|
|
14
|
+
detail: "Key (email)=(a@b.c) already exists.",
|
|
15
|
+
constraint: "users_email_key",
|
|
16
|
+
table: "users",
|
|
17
|
+
column: "email",
|
|
18
|
+
});
|
|
19
|
+
expect(unique?.error.code).toBe("Conflict");
|
|
20
|
+
expect(unique?.error.data).toEqual({
|
|
21
|
+
sqlstate: "23505",
|
|
22
|
+
constraint: "users_email_key",
|
|
23
|
+
table: "users",
|
|
24
|
+
column: "email",
|
|
25
|
+
});
|
|
26
|
+
expect(JSON.stringify(unique)).not.toContain("already exists");
|
|
27
|
+
expect(JSON.stringify(unique)).not.toContain("a@b.c");
|
|
28
|
+
|
|
29
|
+
const exclusion = sqlErrorToFailure({ code: "23P01", constraint: "no_overlap" });
|
|
30
|
+
expect(exclusion?.error.code).toBe("Conflict");
|
|
31
|
+
});
|
|
32
|
+
|
|
33
|
+
test("restrict / restrict_violation → ForeignKey", () => {
|
|
34
|
+
expect(sqlErrorToFailure({ code: "23001", table: "notes" })?.error.code).toBe("ForeignKey");
|
|
35
|
+
});
|
|
36
|
+
|
|
37
|
+
test("invalid / too_long / out_of_range → DatabaseError 422 reasons", () => {
|
|
38
|
+
expect(sqlErrorToFailure({ code: "22P02" })?.error.data).toMatchObject({
|
|
39
|
+
reason: "invalid",
|
|
40
|
+
sqlstate: "22P02",
|
|
41
|
+
});
|
|
42
|
+
expect(sqlErrorToFailure({ code: "22007" })?.error.data).toMatchObject({ reason: "invalid" });
|
|
43
|
+
expect(sqlErrorToFailure({ code: "22023" })?.error.data).toMatchObject({ reason: "invalid" });
|
|
44
|
+
expect(sqlErrorToFailure({ code: "42804" })?.error.data).toMatchObject({ reason: "invalid" });
|
|
45
|
+
expect(sqlErrorToFailure({ code: "22001", column: "title" })?.error.data).toMatchObject({
|
|
46
|
+
reason: "too_long",
|
|
47
|
+
column: "title",
|
|
48
|
+
});
|
|
49
|
+
expect(sqlErrorToFailure({ code: "22003" })?.error.data).toMatchObject({
|
|
50
|
+
reason: "out_of_range",
|
|
51
|
+
});
|
|
52
|
+
expect(sqlErrorToFailure({ code: "22008" })?.error.data).toMatchObject({
|
|
53
|
+
reason: "out_of_range",
|
|
54
|
+
});
|
|
55
|
+
});
|
|
56
|
+
|
|
57
|
+
test("walks Drizzle .cause for unique SQLSTATE", () => {
|
|
58
|
+
const wrapped = {
|
|
59
|
+
message: "Failed query: insert into users constraint users_email_key",
|
|
60
|
+
cause: {
|
|
61
|
+
code: "23505",
|
|
62
|
+
constraint: "users_email_key",
|
|
63
|
+
message: 'duplicate key value violates unique constraint "users_email_key"',
|
|
64
|
+
},
|
|
65
|
+
};
|
|
66
|
+
const mapped = sqlErrorToFailure(wrapped);
|
|
67
|
+
expect(mapped?.error.code).toBe("Conflict");
|
|
68
|
+
expect(mapped?.error.data).toMatchObject({ constraint: "users_email_key", sqlstate: "23505" });
|
|
69
|
+
expect(JSON.stringify(mapped)).not.toContain("duplicate key");
|
|
70
|
+
});
|
|
71
|
+
|
|
72
|
+
test("foreign key → ForeignKey", () => {
|
|
73
|
+
const fk = sqlErrorToFailure({
|
|
74
|
+
code: "23503",
|
|
75
|
+
table: "notes",
|
|
76
|
+
constraint: "notes_user_id_fkey",
|
|
77
|
+
message: 'insert or update on table "notes" violates foreign key constraint',
|
|
78
|
+
});
|
|
79
|
+
expect(fk?.error.code).toBe("ForeignKey");
|
|
80
|
+
expect(fk?.error.data).toMatchObject({
|
|
81
|
+
sqlstate: "23503",
|
|
82
|
+
table: "notes",
|
|
83
|
+
constraint: "notes_user_id_fkey",
|
|
84
|
+
});
|
|
85
|
+
expect(JSON.stringify(fk)).not.toContain("violates");
|
|
86
|
+
});
|
|
87
|
+
|
|
88
|
+
test("not_null / check → DatabaseError reasons", () => {
|
|
89
|
+
const notNull = sqlErrorToFailure({ code: "23502", column: "title", table: "posts" });
|
|
90
|
+
expect(notNull?.error.code).toBe("DatabaseError");
|
|
91
|
+
expect(notNull?.error.data).toMatchObject({ reason: "not_null", column: "title" });
|
|
92
|
+
|
|
93
|
+
const check = sqlErrorToFailure({ code: "23514", constraint: "positive_clicks" });
|
|
94
|
+
expect(check?.error.code).toBe("DatabaseError");
|
|
95
|
+
expect(check?.error.data).toMatchObject({ reason: "check", constraint: "positive_clicks" });
|
|
96
|
+
});
|
|
97
|
+
|
|
98
|
+
test("40001 / 40P01 left thrown until retryable: unavailable", () => {
|
|
99
|
+
const ser = { code: "40001", message: "could not serialize access" };
|
|
100
|
+
expect(isRetryableSqlError(ser)).toBe(true);
|
|
101
|
+
expect(sqlErrorToFailure(ser, { retryable: "leave" })).toBeUndefined();
|
|
102
|
+
const exhausted = sqlErrorToFailure(ser, { retryable: "unavailable" });
|
|
103
|
+
expect(exhausted?.error.code).toBe("ServiceUnavailable");
|
|
104
|
+
expect(JSON.stringify(exhausted)).not.toContain("serialize");
|
|
105
|
+
|
|
106
|
+
expect(sqlErrorToFailure({ code: "40P01" }, { retryable: "leave" })).toBeUndefined();
|
|
107
|
+
expect(sqlErrorToFailure({ code: "40P01" }, { retryable: "unavailable" })?.error.code).toBe(
|
|
108
|
+
"ServiceUnavailable",
|
|
109
|
+
);
|
|
110
|
+
expect(isRetryableSqlError({ code: "55P03" })).toBe(true);
|
|
111
|
+
expect(sqlErrorToFailure({ code: "55P03" }, { retryable: "leave" })).toBeUndefined();
|
|
112
|
+
expect(sqlErrorToFailure({ code: "55P03" }, { retryable: "unavailable" })?.error.code).toBe(
|
|
113
|
+
"ServiceUnavailable",
|
|
114
|
+
);
|
|
115
|
+
});
|
|
116
|
+
|
|
117
|
+
test("connection / paused → ServiceUnavailable", () => {
|
|
118
|
+
expect(sqlErrorToFailure({ code: "08006" })?.error.code).toBe("ServiceUnavailable");
|
|
119
|
+
expect(sqlErrorToFailure({ code: "53300" })?.error.code).toBe("ServiceUnavailable");
|
|
120
|
+
expect(sqlErrorToFailure({ code: "53100" })?.error.code).toBe("ServiceUnavailable");
|
|
121
|
+
expect(sqlErrorToFailure({ code: "57P01" })?.error.code).toBe("ServiceUnavailable");
|
|
122
|
+
expect(sqlErrorToFailure({ code: "57014" })?.error.code).toBe("ServiceUnavailable");
|
|
123
|
+
expect(sqlErrorToFailure({ code: "25P03" })?.error.code).toBe("ServiceUnavailable");
|
|
124
|
+
expect(sqlErrorToFailure({ code: "ERR_POSTGRES_CONNECTION_CLOSED" })?.error.code).toBe(
|
|
125
|
+
"ServiceUnavailable",
|
|
126
|
+
);
|
|
127
|
+
expect(
|
|
128
|
+
sqlErrorToFailure({ name: "SharedPostgresPausedError", code: "ERR_OKE_POSTGRES_PAUSED" })
|
|
129
|
+
?.error.code,
|
|
130
|
+
).toBe("ServiceUnavailable");
|
|
131
|
+
});
|
|
132
|
+
|
|
133
|
+
test("sqlite UNIQUE / FK / NOT NULL / CHECK", () => {
|
|
134
|
+
const unique = sqlErrorToFailure({
|
|
135
|
+
name: "SQLiteError",
|
|
136
|
+
message: "UNIQUE constraint failed: users.email",
|
|
137
|
+
errno: 2067,
|
|
138
|
+
});
|
|
139
|
+
expect(unique?.error.code).toBe("Conflict");
|
|
140
|
+
expect(unique?.error.data).toMatchObject({ table: "users", column: "email" });
|
|
141
|
+
|
|
142
|
+
const fk = sqlErrorToFailure({
|
|
143
|
+
name: "SQLiteError",
|
|
144
|
+
message: "FOREIGN KEY constraint failed",
|
|
145
|
+
errno: 787,
|
|
146
|
+
});
|
|
147
|
+
expect(fk?.error.code).toBe("ForeignKey");
|
|
148
|
+
|
|
149
|
+
const notNull = sqlErrorToFailure({
|
|
150
|
+
name: "SQLiteError",
|
|
151
|
+
message: "NOT NULL constraint failed: posts.title",
|
|
152
|
+
});
|
|
153
|
+
expect(notNull?.error.data).toMatchObject({
|
|
154
|
+
reason: "not_null",
|
|
155
|
+
table: "posts",
|
|
156
|
+
column: "title",
|
|
157
|
+
});
|
|
158
|
+
|
|
159
|
+
const check = sqlErrorToFailure({
|
|
160
|
+
name: "SQLiteError",
|
|
161
|
+
message: "CHECK constraint failed: positive_clicks",
|
|
162
|
+
});
|
|
163
|
+
expect(check?.error.data).toMatchObject({ reason: "check", constraint: "positive_clicks" });
|
|
164
|
+
|
|
165
|
+
const pk = sqlErrorToFailure({
|
|
166
|
+
name: "SQLiteError",
|
|
167
|
+
message: "PRIMARY KEY constraint failed: users.id",
|
|
168
|
+
errno: 1555,
|
|
169
|
+
});
|
|
170
|
+
expect(pk?.error.code).toBe("Conflict");
|
|
171
|
+
expect(pk?.error.data).toMatchObject({ table: "users", column: "id" });
|
|
172
|
+
});
|
|
173
|
+
|
|
174
|
+
test("sqlite busy left thrown until retryable: unavailable", () => {
|
|
175
|
+
const busy = {
|
|
176
|
+
name: "SQLiteError",
|
|
177
|
+
code: "SQLITE_BUSY",
|
|
178
|
+
errno: 5,
|
|
179
|
+
message: "database is locked",
|
|
180
|
+
};
|
|
181
|
+
expect(isRetryableSqlError(busy)).toBe(true);
|
|
182
|
+
expect(sqlErrorToFailure(busy, { retryable: "leave" })).toBeUndefined();
|
|
183
|
+
expect(sqlErrorToFailure(busy, { retryable: "unavailable" })?.error.code).toBe(
|
|
184
|
+
"ServiceUnavailable",
|
|
185
|
+
);
|
|
186
|
+
});
|
|
187
|
+
|
|
188
|
+
test("unknown SQLSTATE → DatabaseError unknown; non-SQL stays undefined", () => {
|
|
189
|
+
const other = sqlErrorToFailure({ code: "22012", message: "division by zero" });
|
|
190
|
+
expect(other?.error.code).toBe("DatabaseError");
|
|
191
|
+
expect(other?.error.data).toMatchObject({ reason: "unknown", sqlstate: "22012" });
|
|
192
|
+
expect(JSON.stringify(other)).not.toContain("division");
|
|
193
|
+
|
|
194
|
+
expect(sqlErrorToFailure(new Error("boom"))).toBeUndefined();
|
|
195
|
+
expect(isFlowFailure(sqlErrorToFailure({ code: "23505" }))).toBe(true);
|
|
196
|
+
});
|
|
197
|
+
});
|