okengine 0.14.1 → 0.15.2
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 -4
- package/site/content/docs/elements/ai.mdx +2 -1
- package/site/content/docs/elements/channel.mdx +1 -0
- package/site/content/docs/elements/clock.mdx +3 -0
- package/site/content/docs/elements/flow.mdx +3 -0
- package/site/content/docs/elements/gate.mdx +25 -22
- package/site/content/docs/elements/signal.mdx +4 -4
- package/site/content/docs/elements/store.mdx +86 -27
- package/site/content/docs/elements/vault.mdx +1 -0
- package/site/content/docs/get-started/basic-usage.mdx +22 -13
- package/site/content/docs/get-started/index.mdx +5 -0
- package/site/content/docs/get-started/introduction.mdx +11 -3
- package/site/content/docs/get-started/meta.json +1 -1
- package/site/content/docs/get-started/project-structure.mdx +932 -0
- package/site/content/docs/reference/cli.md +13 -10
- package/site/content/docs/reference/client.mdx +25 -21
- package/site/content/docs/reference/security.md +1 -1
- package/src/auth/bindings.ts +14 -5
- package/src/auth/cross-plane.ts +10 -3
- package/src/cli/ai-setup/ai-setup.test.ts +62 -0
- package/src/cli/ai-setup/apply.ts +32 -1
- package/src/cli/attach-host-console.test.ts +3 -2
- package/src/cli/attach-host-console.ts +3 -0
- package/src/cli/build.test.ts +4 -2
- package/src/cli/db-seed.ts +48 -15
- package/src/cli/db.test.ts +104 -0
- package/src/cli/db.ts +122 -10
- package/src/cli/dev-controls.test.ts +20 -0
- package/src/cli/dev-controls.ts +13 -0
- package/src/cli/dev.test.ts +15 -4
- package/src/cli/dev.ts +102 -29
- package/src/cli/ensure-drizzle-config.ts +2 -1
- package/src/cli/tui/DevLive.tsx +12 -2
- package/src/cli/tui/keys.test.ts +2 -1
- package/src/cli/tui/keys.ts +3 -2
- package/src/compiler/aot.test.ts +1 -1
- package/src/compiler/extract.test.ts +409 -33
- package/src/compiler/extract.ts +318 -67
- package/src/compiler/flow-path.test.ts +96 -0
- package/src/compiler/flow-path.ts +173 -0
- package/src/compiler/generate-adopt.test.ts +112 -3
- package/src/compiler/generate-adopt.ts +280 -26
- package/src/compiler/schema-from-ast.ts +609 -0
- package/src/console/server/bind.ts +2 -2
- package/src/console/server/console.test.ts +3 -0
- package/src/console/server/flows.ts +120 -40
- package/src/console/server/invoke-user-flow.test.ts +3 -1
- package/src/console/server/invoke-user-flow.ts +17 -2
- package/src/console/server/iso-at.test.ts +9 -0
- package/src/console/server/iso-at.ts +11 -0
- package/src/console/server/security-headers.ts +6 -3
- package/src/console/server/spa-proxy.test.ts +1 -0
- package/src/console/server/sql-catalog.ts +147 -7
- package/src/console/server/sql-rls.test.ts +16 -0
- package/src/console/server/state.ts +8 -0
- package/src/console/server/store.test.ts +7 -4
- package/src/console/server/store.ts +142 -58
- package/src/console/ui-next/dist/assets/agent-disclosure-C0X1fbWF.js +1 -0
- package/src/console/ui-next/dist/assets/{cache-glyph-BanhLsEY.js → cache-glyph-F1FI122b.js} +1 -1
- package/src/console/ui-next/dist/assets/{call-pii-button-bqkxMrJH.js → call-pii-button-DEDCl_j3.js} +1 -1
- package/src/console/ui-next/dist/assets/collapsible-LPqGvfoz.js +1 -0
- package/src/console/ui-next/dist/assets/{confirm-sheet-DDCRmG62.js → confirm-sheet-3ptDzXbz.js} +1 -1
- package/src/console/ui-next/dist/assets/{duration-tone-oiRxPVsZ.js → duration-tone-sC3lGABz.js} +1 -1
- package/src/console/ui-next/dist/assets/{explorer-empty-HjTnVQoR.js → explorer-empty-BvYrygyO.js} +1 -1
- package/src/console/ui-next/dist/assets/flows-page-Dluo1Bd7.js +1 -0
- package/src/console/ui-next/dist/assets/{highlighted-json-CvDPvveV.js → highlighted-json-CS_O8L-r.js} +1 -1
- package/src/console/ui-next/dist/assets/http-method--sWDdXSB.js +1 -0
- package/src/console/ui-next/dist/assets/index-Ca3HZMVq.js +66 -0
- package/src/console/ui-next/dist/assets/index-UtTDRQpS.css +2 -0
- package/src/console/ui-next/dist/assets/{link-DF7SZ9Ek.js → link-COeyggt-.js} +1 -1
- package/src/console/ui-next/dist/assets/{observability-page-BEZDzyYh.js → observability-page-HK9-BO8z.js} +2 -2
- package/src/console/ui-next/dist/assets/react-D8E3mtu1.js +1 -0
- package/src/console/ui-next/dist/assets/replica-lag-DBIFFf7d.js +18 -0
- package/src/console/ui-next/dist/assets/sheet-form-Dnwa5oOR.js +1 -0
- package/src/console/ui-next/dist/assets/shortcut-keys-CoqwEIi0.js +1 -0
- package/src/console/ui-next/dist/assets/skeleton-BHcfDYcb.js +1 -0
- package/src/console/ui-next/dist/assets/store-page-BSohH3wM.js +41 -0
- package/src/console/ui-next/dist/assets/trace-detail-sheet-CFIGRnnA.js +2 -0
- package/src/console/ui-next/dist/assets/tree-expand-toggle-BgLl34w7.js +54 -0
- package/src/console/ui-next/dist/assets/units-page-B0cFE76A.js +1 -0
- package/src/console/ui-next/dist/assets/{use-vault-list-Cl79j_ku.js → use-vault-list-BLbZhzyF.js} +1 -1
- package/src/console/ui-next/dist/assets/vault-page-B1SbYe10.js +2 -0
- package/src/console/ui-next/dist/index.html +6 -6
- package/src/console/ui-next/src/client.ts +35 -7
- package/src/console/ui-next/src/components/motion/table/index.tsx +16 -0
- package/src/console/ui-next/src/components/motion/table/types.ts +4 -0
- package/src/console/ui-next/src/components/ui/kbd.tsx +2 -2
- package/src/console/ui-next/src/features/flows/traces/http-method.ts +19 -0
- package/src/console/ui-next/src/features/flows/traces/trace-detail.test.ts +19 -1
- package/src/console/ui-next/src/features/flows/traces/traces-pane.tsx +10 -3
- package/src/console/ui-next/src/features/store/detail/browse-section.tsx +45 -1
- package/src/console/ui-next/src/features/store/detail/resource-panel.tsx +78 -16
- package/src/console/ui-next/src/features/store/detail/store-row-detail-sheet.tsx +155 -14
- package/src/console/ui-next/src/features/store/explorer/store-tree.tsx +33 -133
- package/src/console/ui-next/src/features/store/files/file-preview.tsx +4 -1
- package/src/console/ui-next/src/features/store/grid/rls-policy-sheet.tsx +326 -165
- package/src/console/ui-next/src/features/store/grid/sql-insert-sheet.tsx +6 -0
- package/src/console/ui-next/src/features/store/grid/store-data-grid.tsx +50 -3
- package/src/console/ui-next/src/features/store/lib/files-meta.test.ts +3 -0
- package/src/console/ui-next/src/features/store/lib/files-meta.ts +12 -0
- package/src/console/ui-next/src/features/store/lib/query-defaults.test.ts +25 -0
- package/src/console/ui-next/src/features/store/lib/query-defaults.ts +85 -3
- package/src/console/ui-next/src/features/store/lib/query-gate.ts +30 -4
- package/src/console/ui-next/src/features/store/lib/rls-policy.test.ts +139 -0
- package/src/console/ui-next/src/features/store/lib/rls-policy.ts +223 -21
- package/src/console/ui-next/src/features/store/lib/schema-graph.ts +4 -0
- package/src/console/ui-next/src/features/store/lib/sql-catalog.test.ts +24 -0
- package/src/console/ui-next/src/features/store/lib/sql-catalog.ts +19 -0
- package/src/console/ui-next/src/features/store/lib/store-tree.test.ts +6 -0
- package/src/console/ui-next/src/features/store/lib/store-tree.ts +13 -0
- package/src/console/ui-next/src/features/store/query/query-console.tsx +48 -14
- package/src/console/ui-next/src/features/store/query/query-gate-menu.tsx +19 -20
- package/src/console/ui-next/src/features/store/query/query-gate-parts.tsx +2 -1
- package/src/console/ui-next/src/features/store/query/query-results.tsx +110 -83
- package/src/console/ui-next/src/features/store/schema/schema-table-node.tsx +17 -0
- package/src/console/ui-next/src/features/units/call/call-api-panel.tsx +112 -72
- package/src/console/ui-next/src/features/units/lib/contract-input.test.ts +206 -1
- package/src/console/ui-next/src/features/units/lib/contract-input.ts +55 -0
- package/src/console/ui-next/src/features/vault/lib/types.ts +1 -1
- package/src/docker/compose-health.test.ts +27 -0
- package/src/docker/compose-health.ts +19 -3
- package/src/docker/docker.test.ts +3 -0
- package/src/docker/recipes/pgdog.ts +2 -0
- package/src/drivers/clock-postgres.ts +13 -9
- package/src/drivers/instances-postgres.ts +4 -6
- package/src/drivers/journal-postgres.ts +13 -9
- package/src/drivers/pg-rls.test.ts +87 -0
- package/src/drivers/pg-rls.ts +233 -7
- package/src/drivers/pglite.ts +26 -3
- package/src/drivers/postgres.test.ts +23 -0
- package/src/drivers/postgres.ts +133 -9
- package/src/drivers/types.ts +10 -0
- package/src/elements/clock/declare.ts +26 -1
- package/src/elements/gate/boot.ts +5 -5
- package/src/elements/gate/declare.ts +36 -4
- package/src/elements/gate/flatten.ts +1 -1
- package/src/elements/gate/runtime.ts +1 -1
- package/src/elements/store/declare.ts +9 -6
- package/src/elements/store/emit-drizzle.ts +44 -8
- package/src/elements/store/resource.test.ts +1 -1
- package/src/elements/store/rls-identity.test.ts +95 -0
- package/src/elements/store/rls-identity.ts +116 -0
- package/src/elements/store/runtime.ts +4 -0
- package/src/elements/store/schema-decl.test.ts +37 -0
- package/src/elements/store/schema-decl.ts +161 -0
- package/src/elements/store/seed.ts +1 -1
- package/src/elements/store/sql-condition.test.ts +12 -0
- package/src/elements/store/sql-condition.ts +26 -2
- package/src/elements/store/sql-rls-isolation.test.ts +165 -0
- package/src/elements/store/sql-rls-stamp.test.ts +117 -0
- package/src/elements/store/sql-session.ts +81 -3
- package/src/elements/store/upsert-app.test.ts +1 -1
- package/src/elements/store.ts +14 -0
- package/src/http.ts +20 -3
- package/src/index.ts +10 -1
- package/src/kernel/adopt-barrel-fresh.test.ts +1 -1
- package/src/kernel/adopt-routes.ts +30 -2
- package/src/kernel/app.ts +131 -14
- package/src/kernel/auto-cache.test.ts +8 -8
- package/src/kernel/auto-registry.test.ts +49 -9
- package/src/kernel/boot.test.ts +4 -4
- package/src/kernel/boot.ts +2 -2
- package/src/kernel/effects-stamping.test.ts +3 -3
- package/src/kernel/element-registries.ts +12 -5
- package/src/kernel/errors.ts +24 -0
- package/src/kernel/flow-units.ts +64 -0
- package/src/kernel/flow.ts +29 -17
- package/src/kernel/fx.ts +23 -0
- package/src/kernel/horizontal-child.ts +4 -4
- package/src/kernel/http-path-pending.ts +20 -0
- package/src/kernel/http-resource.ts +4 -0
- package/src/kernel/http-stream.test.ts +4 -4
- package/src/kernel/pipeline.test.ts +2 -2
- package/src/kernel/plugin-elements.test.ts +1 -1
- package/src/kernel/plugin-needs.test.ts +1 -1
- package/src/kernel/ready.test.ts +2 -2
- package/src/kernel/resource-mount.test.ts +14 -0
- package/src/kernel/run-duration.test.ts +2 -2
- package/src/kernel/stamp-http.test.ts +79 -0
- package/src/kernel/stamp-http.ts +75 -0
- package/src/kernel/triggers.ts +52 -27
- package/src/manifest/types.ts +13 -0
- package/src/release/measure.ts +2 -2
- package/src/runtime/json-code-block.test.ts +2 -2
- package/src/runtime/serve.test.ts +4 -4
- package/src/term.test.ts +52 -4
- package/src/term.ts +125 -17
- package/src/test/reset-element-registries.ts +8 -3
- package/src/cli/ask-seed.test.ts +0 -96
- package/src/cli/ask-seed.ts +0 -82
- package/src/console/ui-next/dist/assets/collapsible-DYb0xU8C.js +0 -1
- package/src/console/ui-next/dist/assets/flows-page-DxDsOd4f.js +0 -1
- package/src/console/ui-next/dist/assets/http-method-CJCBYL2j.js +0 -1
- package/src/console/ui-next/dist/assets/index-Ce6WKWKM.js +0 -66
- package/src/console/ui-next/dist/assets/index-D4Ldtj79.css +0 -2
- package/src/console/ui-next/dist/assets/query-gate-parts-1m8m1iNp.js +0 -2
- package/src/console/ui-next/dist/assets/react-B1ML8gxg.js +0 -1
- package/src/console/ui-next/dist/assets/replica-lag-C8_BRt2x.js +0 -18
- package/src/console/ui-next/dist/assets/sheet-form-D-ata7jy.js +0 -1
- package/src/console/ui-next/dist/assets/shortcut-keys-DO4IsVqv.js +0 -1
- package/src/console/ui-next/dist/assets/skeleton-CL_X0GCj.js +0 -1
- package/src/console/ui-next/dist/assets/store-page-v3LXdYpr.js +0 -45
- package/src/console/ui-next/dist/assets/trace-detail-sheet-DFLFfUUX.js +0 -2
- package/src/console/ui-next/dist/assets/units-page-C0gW6Kdo.js +0 -1
- package/src/console/ui-next/dist/assets/vault-page-DuKzqwzW.js +0 -2
|
@@ -105,22 +105,24 @@ describe("extractManifest — fx.raw", () => {
|
|
|
105
105
|
});
|
|
106
106
|
|
|
107
107
|
describe("extractManifest — performance", () => {
|
|
108
|
-
test(
|
|
109
|
-
|
|
110
|
-
|
|
111
|
-
|
|
112
|
-
|
|
113
|
-
|
|
114
|
-
|
|
115
|
-
|
|
116
|
-
|
|
117
|
-
|
|
108
|
+
test(
|
|
109
|
+
"200-flow synthetic app extracts in under 2s",
|
|
110
|
+
async () => {
|
|
111
|
+
const sources: Record<string, string> = {};
|
|
112
|
+
const parts: string[] = [
|
|
113
|
+
`import { on, flow, http, store } from "okengine";`,
|
|
114
|
+
`export const db = store.sql("db");`,
|
|
115
|
+
`export const items = { name: "items" };`,
|
|
116
|
+
`export const app = { name: "synth" };`,
|
|
117
|
+
];
|
|
118
|
+
// Declare oke name via a tiny app file pattern
|
|
119
|
+
sources["src/app.ts"] = `
|
|
118
120
|
import { oke } from "okengine";
|
|
119
121
|
export const app = oke({ name: "synth200" });
|
|
120
122
|
`;
|
|
121
123
|
|
|
122
|
-
|
|
123
|
-
|
|
124
|
+
for (let i = 0; i < 200; i++) {
|
|
125
|
+
parts.push(`
|
|
124
126
|
export const flow_${i} = on(
|
|
125
127
|
http.get("/f/${i}"),
|
|
126
128
|
flow("synth.flow_${i}", {
|
|
@@ -132,19 +134,22 @@ export const flow_${i} = on(
|
|
|
132
134
|
}),
|
|
133
135
|
);
|
|
134
136
|
`);
|
|
135
|
-
|
|
136
|
-
|
|
137
|
-
|
|
138
|
-
|
|
139
|
-
|
|
140
|
-
|
|
141
|
-
|
|
142
|
-
|
|
143
|
-
|
|
144
|
-
|
|
145
|
-
|
|
146
|
-
|
|
147
|
-
|
|
137
|
+
}
|
|
138
|
+
sources["src/flows/all.ts"] = parts.join("\n");
|
|
139
|
+
|
|
140
|
+
await extractFromSources(sources);
|
|
141
|
+
const start = performance.now();
|
|
142
|
+
const manifest = await extractFromSources(sources);
|
|
143
|
+
const elapsed = performance.now() - start;
|
|
144
|
+
|
|
145
|
+
expect(Object.keys(manifest.flows ?? {}).length).toBe(200);
|
|
146
|
+
// Local ~400ms after warmup. GHA shared runners measured ~2.9s cold.
|
|
147
|
+
// Warmup + extract exceeds Bun's 5s default (v0.15.0 tag timed out at 5.6s).
|
|
148
|
+
const budgetMs = process.env.CI ? 4_000 : 2_000;
|
|
149
|
+
expect(elapsed).toBeLessThan(budgetMs);
|
|
150
|
+
},
|
|
151
|
+
{ timeout: 15_000 },
|
|
152
|
+
);
|
|
148
153
|
});
|
|
149
154
|
|
|
150
155
|
describe("serializeManifest stability", () => {
|
|
@@ -241,6 +246,32 @@ export const db = store.sql("app", { schema: { notes } });
|
|
|
241
246
|
});
|
|
242
247
|
});
|
|
243
248
|
|
|
249
|
+
test("extracts RLS helpers onto store.tables", async () => {
|
|
250
|
+
const source = `
|
|
251
|
+
import { store, field } from "okengine";
|
|
252
|
+
|
|
253
|
+
export const bookings = store.schema.table("bookings", {
|
|
254
|
+
id: field.text().primaryKey(),
|
|
255
|
+
owner: field.text().notNull(),
|
|
256
|
+
}, [
|
|
257
|
+
store.schema.policy.gate("member", { for: "select" }),
|
|
258
|
+
store.schema.policy.owner("owner"),
|
|
259
|
+
]);
|
|
260
|
+
|
|
261
|
+
export const db = store.sql("app", { schema: { bookings } });
|
|
262
|
+
`;
|
|
263
|
+
const manifest = await extractFromSources({
|
|
264
|
+
"src/schema.decl.ts": source,
|
|
265
|
+
});
|
|
266
|
+
const table = manifest.stores?.app?.tables?.bookings;
|
|
267
|
+
expect(table?.rls).toBe(true);
|
|
268
|
+
expect(table?.policies?.gate_member_select).toMatchObject({
|
|
269
|
+
for: "select",
|
|
270
|
+
using: "oke.gate() = 'member'",
|
|
271
|
+
});
|
|
272
|
+
expect(table?.policies?.owner_owner_all?.using).toContain("oke.user()");
|
|
273
|
+
});
|
|
274
|
+
|
|
244
275
|
test("extracts optional description fields additively", async () => {
|
|
245
276
|
const source = `
|
|
246
277
|
import { store, field, signal, channel, clock, gate, vault } from "okengine";
|
|
@@ -468,6 +499,27 @@ export const remove = mounted.remove;
|
|
|
468
499
|
expect(manifest.flows?.update?.live).toBeUndefined();
|
|
469
500
|
expect(manifest.flows?.remove?.live).toBeUndefined();
|
|
470
501
|
});
|
|
502
|
+
|
|
503
|
+
test("chains .public().live() onto the five verbs", async () => {
|
|
504
|
+
const source = `
|
|
505
|
+
import { on, http, store } from "okengine";
|
|
506
|
+
|
|
507
|
+
export const db = store.sql("notes", { schema: {} });
|
|
508
|
+
const notesR = store.resource(db, {}, {});
|
|
509
|
+
const mounted = on(http.resource("/notes", notesR.all()).public().live());
|
|
510
|
+
|
|
511
|
+
export const list = mounted.list;
|
|
512
|
+
export const create = mounted.create;
|
|
513
|
+
export const get = mounted.get;
|
|
514
|
+
export const update = mounted.update;
|
|
515
|
+
export const remove = mounted.remove;
|
|
516
|
+
`;
|
|
517
|
+
const manifest = await extractFromSources({ "src/flows/notes.ts": source });
|
|
518
|
+
expect(manifest.flows?.list?.gates).toEqual(["public"]);
|
|
519
|
+
expect(manifest.flows?.create?.gates).toEqual(["public"]);
|
|
520
|
+
expect(manifest.flows?.get?.live).toBe(true);
|
|
521
|
+
expect(manifest.flows?.create?.live).toBeUndefined();
|
|
522
|
+
});
|
|
471
523
|
});
|
|
472
524
|
|
|
473
525
|
describe("extractManifest — vault.config", () => {
|
|
@@ -512,7 +564,7 @@ import { on, flow, http, gate, store } from "okengine";
|
|
|
512
564
|
export const files = store.files("uploads");
|
|
513
565
|
|
|
514
566
|
export const attach = on(
|
|
515
|
-
http.post("/attach").
|
|
567
|
+
http.post("/attach").public(),
|
|
516
568
|
flow("notes.attach", {
|
|
517
569
|
do: async (input, fx) => {
|
|
518
570
|
await fx.store(files).put("key", input.text);
|
|
@@ -533,7 +585,7 @@ import { on, flow, http, gate, store } from "okengine";
|
|
|
533
585
|
export const files = store.files("uploads");
|
|
534
586
|
|
|
535
587
|
export const attach = on(
|
|
536
|
-
http.post("/attach").
|
|
588
|
+
http.post("/attach").public(),
|
|
537
589
|
flow("notes.attachImage", {
|
|
538
590
|
do: async (input, fx) => {
|
|
539
591
|
await fx.store(files).putImage("key", input.bytes);
|
|
@@ -609,7 +661,7 @@ import { on, flow, http, signal } from "okengine";
|
|
|
609
661
|
export const notify = signal("notify", { delivery: "once" });
|
|
610
662
|
|
|
611
663
|
export const failed = on(
|
|
612
|
-
http.get("/notifications/failed").
|
|
664
|
+
http.get("/notifications/failed").public(),
|
|
613
665
|
flow("notifications.failed", {
|
|
614
666
|
do: async (input, fx) => fx.json.withQuery(await fx.deadLetters(notify), input),
|
|
615
667
|
}),
|
|
@@ -632,7 +684,7 @@ export const noteCreatedMail = mail.template("note-created", {
|
|
|
632
684
|
});
|
|
633
685
|
|
|
634
686
|
export const onCreated = on(
|
|
635
|
-
http.post("/hook").
|
|
687
|
+
http.post("/hook").public(),
|
|
636
688
|
flow("notes.onCreated", {
|
|
637
689
|
do: async (payload, fx) => {
|
|
638
690
|
await fx.send(noteCreatedMail, { to: "you@localhost", data: payload });
|
|
@@ -655,7 +707,7 @@ const otp = channel.sms({});
|
|
|
655
707
|
export const otpTemplate = otp.template("otp-code", {});
|
|
656
708
|
|
|
657
709
|
export const send = on(
|
|
658
|
-
http.post("/otp").
|
|
710
|
+
http.post("/otp").public(),
|
|
659
711
|
flow("notes.sendOtp", {
|
|
660
712
|
do: async (payload, fx) => {
|
|
661
713
|
await fx.send(otpTemplate, { to: "+10000000000", data: payload });
|
|
@@ -738,19 +790,32 @@ export const decoy = flow("checkout.realName", {
|
|
|
738
790
|
});
|
|
739
791
|
});
|
|
740
792
|
|
|
741
|
-
describe("extractManifest — .
|
|
742
|
-
test("reads the public sentinel from
|
|
793
|
+
describe("extractManifest — .public()", () => {
|
|
794
|
+
test("reads the public sentinel from .public()", async () => {
|
|
743
795
|
const source = `
|
|
744
796
|
import { on, flow, http } from "okengine";
|
|
745
797
|
|
|
746
798
|
export const health = on(
|
|
747
|
-
http.get("/health").
|
|
799
|
+
http.get("/health").public(),
|
|
748
800
|
flow("health.check", { do: () => ({ ok: true }) }),
|
|
749
801
|
);
|
|
750
802
|
`;
|
|
751
803
|
const manifest = await extractFromSources({ "src/flows/health.ts": source });
|
|
752
804
|
expect(manifest.flows?.["health.check"]?.gates).toEqual(["public"]);
|
|
753
805
|
});
|
|
806
|
+
|
|
807
|
+
test("does not treat the removed .gate.public property as attach", async () => {
|
|
808
|
+
const source = `
|
|
809
|
+
import { on, flow, http } from "okengine";
|
|
810
|
+
|
|
811
|
+
export const health = on(
|
|
812
|
+
http.get("/health").gate.public,
|
|
813
|
+
flow("health.check", { do: () => ({ ok: true }) }),
|
|
814
|
+
);
|
|
815
|
+
`;
|
|
816
|
+
const manifest = await extractFromSources({ "src/flows/health.ts": source });
|
|
817
|
+
expect(manifest.flows?.["health.check"]?.gates).toBeUndefined();
|
|
818
|
+
});
|
|
754
819
|
});
|
|
755
820
|
|
|
756
821
|
describe("extractManifest — gate.all", () => {
|
|
@@ -829,3 +894,314 @@ export const triage = on(
|
|
|
829
894
|
expect(manifest.flows?.["support.triage"]?.effects?.calls).toEqual(["mcp:github/create_issue"]);
|
|
830
895
|
});
|
|
831
896
|
});
|
|
897
|
+
|
|
898
|
+
describe("extractManifest — file-tree stamps", () => {
|
|
899
|
+
test("pathless http.get() in notes/[id]/get.ts infers /notes/:id and notes.get", async () => {
|
|
900
|
+
const source = `
|
|
901
|
+
import { on, flow, http } from "okengine";
|
|
902
|
+
export const get = on(
|
|
903
|
+
http.get().public(),
|
|
904
|
+
flow({ do: (input) => input }),
|
|
905
|
+
);
|
|
906
|
+
`;
|
|
907
|
+
const manifest = await extractFromSources({ "src/flows/notes/[id]/get.ts": source });
|
|
908
|
+
expect(manifest.flows?.["notes.get"]?.trigger).toEqual({
|
|
909
|
+
http: { method: "GET", path: "/notes/:id" },
|
|
910
|
+
});
|
|
911
|
+
});
|
|
912
|
+
|
|
913
|
+
test("nameless flow({ do }) still appears on the Manifest", async () => {
|
|
914
|
+
const source = `
|
|
915
|
+
import { flow } from "okengine";
|
|
916
|
+
export const ping = flow({ do: () => ({ ok: true }) });
|
|
917
|
+
`;
|
|
918
|
+
const manifest = await extractFromSources({ "src/flows/notes/ping.ts": source });
|
|
919
|
+
expect(manifest.flows?.["notes.ping"]).toBeDefined();
|
|
920
|
+
});
|
|
921
|
+
|
|
922
|
+
test("effects are identical for the same do body in a tree file vs a barrel", async () => {
|
|
923
|
+
const doBody = `
|
|
924
|
+
const rows = await fx.store(db).select().from(notes);
|
|
925
|
+
await fx.emit(noteCreated, { id: "1" });
|
|
926
|
+
await fx.call("notes.ping");
|
|
927
|
+
return rows;
|
|
928
|
+
`;
|
|
929
|
+
const barrel = `
|
|
930
|
+
import { on, flow, http, store, signal } from "okengine";
|
|
931
|
+
export const db = store.sql("db");
|
|
932
|
+
export const notes = { name: "notes" };
|
|
933
|
+
export const noteCreated = signal("note-created");
|
|
934
|
+
export const get = on(
|
|
935
|
+
http.get("/notes/:id").public(),
|
|
936
|
+
flow("notes.get", {
|
|
937
|
+
do: async (_input, fx) => {${doBody}},
|
|
938
|
+
}),
|
|
939
|
+
);
|
|
940
|
+
`;
|
|
941
|
+
const tree = `
|
|
942
|
+
import { on, flow, http, store, signal } from "okengine";
|
|
943
|
+
export const db = store.sql("db");
|
|
944
|
+
export const notes = { name: "notes" };
|
|
945
|
+
export const noteCreated = signal("note-created");
|
|
946
|
+
export const get = on(
|
|
947
|
+
http.get().public(),
|
|
948
|
+
flow({
|
|
949
|
+
do: async (_input, fx) => {${doBody}},
|
|
950
|
+
}),
|
|
951
|
+
);
|
|
952
|
+
`;
|
|
953
|
+
const barrelManifest = await extractFromSources({ "src/flows/notes/index.ts": barrel });
|
|
954
|
+
const treeManifest = await extractFromSources({ "src/flows/notes/[id]/get.ts": tree });
|
|
955
|
+
const barrelFx = barrelManifest.flows?.["notes.get"]?.effects;
|
|
956
|
+
const treeFx = treeManifest.flows?.["notes.get"]?.effects;
|
|
957
|
+
expect(treeFx).toEqual(barrelFx);
|
|
958
|
+
expect(treeFx?.reads).toBeDefined();
|
|
959
|
+
expect(treeFx?.emits).toBeDefined();
|
|
960
|
+
expect(treeFx?.calls).toBeDefined();
|
|
961
|
+
expect(treeFx?.reads?.length).toBeGreaterThan(0);
|
|
962
|
+
expect(treeFx?.emits?.length).toBeGreaterThan(0);
|
|
963
|
+
expect(treeFx?.calls?.length).toBeGreaterThan(0);
|
|
964
|
+
});
|
|
965
|
+
});
|
|
966
|
+
|
|
967
|
+
describe("extractManifest — flow in/out schema expansion", () => {
|
|
968
|
+
test("expands z.object identifiers into JSON Schema fields", async () => {
|
|
969
|
+
const source = `
|
|
970
|
+
import { flow } from "okengine";
|
|
971
|
+
import { z } from "zod";
|
|
972
|
+
|
|
973
|
+
export const TaskCreateIn = z.object({
|
|
974
|
+
title: z.string().min(1).max(500),
|
|
975
|
+
spaceKey: z.string().min(1),
|
|
976
|
+
priority: z.number().int().min(0).max(4).optional(),
|
|
977
|
+
});
|
|
978
|
+
|
|
979
|
+
export const TaskCreateOut = z.object({
|
|
980
|
+
id: z.string(),
|
|
981
|
+
identifier: z.string(),
|
|
982
|
+
});
|
|
983
|
+
|
|
984
|
+
export const create = flow("tasks.create", {
|
|
985
|
+
in: TaskCreateIn,
|
|
986
|
+
out: TaskCreateOut,
|
|
987
|
+
do: async () => ({ id: "1", identifier: "ENG-1" }),
|
|
988
|
+
});
|
|
989
|
+
`;
|
|
990
|
+
const manifest = await extractFromSources({ "src/flows/tasks/shapes.ts": source });
|
|
991
|
+
expect(manifest.flows?.["tasks.create"]?.in).toEqual({
|
|
992
|
+
type: "object",
|
|
993
|
+
properties: {
|
|
994
|
+
title: { type: "string", minLength: 1, maxLength: 500 },
|
|
995
|
+
spaceKey: { type: "string", minLength: 1 },
|
|
996
|
+
priority: { type: "integer", minimum: 0, maximum: 4 },
|
|
997
|
+
},
|
|
998
|
+
required: ["title", "spaceKey"],
|
|
999
|
+
});
|
|
1000
|
+
expect(manifest.flows?.["tasks.create"]?.out).toEqual({
|
|
1001
|
+
type: "object",
|
|
1002
|
+
properties: {
|
|
1003
|
+
id: { type: "string" },
|
|
1004
|
+
identifier: { type: "string" },
|
|
1005
|
+
},
|
|
1006
|
+
required: ["id", "identifier"],
|
|
1007
|
+
});
|
|
1008
|
+
});
|
|
1009
|
+
|
|
1010
|
+
test("expands drizzle-orm/zod createSelect/Insert/UpdateSchema from table columns", async () => {
|
|
1011
|
+
const schema = `
|
|
1012
|
+
import { store, field, id, now } from "okengine";
|
|
1013
|
+
|
|
1014
|
+
export const users = store.schema.table("users", {
|
|
1015
|
+
id: field.text().primaryKey().defaultFn(id),
|
|
1016
|
+
name: field.text().notNull(),
|
|
1017
|
+
age: field.integer().notNull(),
|
|
1018
|
+
bio: field.text(),
|
|
1019
|
+
createdAt: field.integer().notNull().defaultFn(now),
|
|
1020
|
+
});
|
|
1021
|
+
`;
|
|
1022
|
+
const flows = `
|
|
1023
|
+
import { flow } from "okengine";
|
|
1024
|
+
import { createInsertSchema, createSelectSchema, createUpdateSchema } from "drizzle-orm/zod";
|
|
1025
|
+
import { users } from "../schema.decl.ts";
|
|
1026
|
+
|
|
1027
|
+
export const userSelect = createSelectSchema(users);
|
|
1028
|
+
export const userInsert = createInsertSchema(users);
|
|
1029
|
+
export const userUpdate = createUpdateSchema(users);
|
|
1030
|
+
|
|
1031
|
+
export const create = flow("users.create", {
|
|
1032
|
+
in: userInsert,
|
|
1033
|
+
out: userSelect,
|
|
1034
|
+
do: async () => ({}),
|
|
1035
|
+
});
|
|
1036
|
+
|
|
1037
|
+
export const patch = flow("users.patch", {
|
|
1038
|
+
in: userUpdate,
|
|
1039
|
+
do: async () => ({}),
|
|
1040
|
+
});
|
|
1041
|
+
`;
|
|
1042
|
+
const manifest = await extractFromSources({
|
|
1043
|
+
"src/schema.decl.ts": schema,
|
|
1044
|
+
"src/flows/users.ts": flows,
|
|
1045
|
+
});
|
|
1046
|
+
const insert = manifest.flows?.["users.create"]?.in as {
|
|
1047
|
+
properties?: Record<string, unknown>;
|
|
1048
|
+
required?: string[];
|
|
1049
|
+
};
|
|
1050
|
+
expect(insert?.properties).toMatchObject({
|
|
1051
|
+
name: { type: "string" },
|
|
1052
|
+
age: { type: "integer" },
|
|
1053
|
+
bio: { type: "string" },
|
|
1054
|
+
createdAt: { type: "integer" },
|
|
1055
|
+
});
|
|
1056
|
+
expect(insert?.properties?.id).toBeUndefined();
|
|
1057
|
+
expect(insert?.required).toEqual(["name", "age"]);
|
|
1058
|
+
|
|
1059
|
+
const select = manifest.flows?.["users.create"]?.out as {
|
|
1060
|
+
properties?: Record<string, unknown>;
|
|
1061
|
+
required?: string[];
|
|
1062
|
+
};
|
|
1063
|
+
expect(select?.properties?.id).toMatchObject({ type: "string", primaryKey: true });
|
|
1064
|
+
expect(select?.required).toEqual(expect.arrayContaining(["id", "name", "age", "createdAt"]));
|
|
1065
|
+
expect(select?.required).not.toContain("bio");
|
|
1066
|
+
|
|
1067
|
+
const update = manifest.flows?.["users.patch"]?.in as {
|
|
1068
|
+
properties?: Record<string, unknown>;
|
|
1069
|
+
required?: string[];
|
|
1070
|
+
};
|
|
1071
|
+
expect(update?.properties?.name).toMatchObject({ type: "string" });
|
|
1072
|
+
expect(update?.properties?.id).toBeUndefined();
|
|
1073
|
+
expect(update?.required).toBeUndefined();
|
|
1074
|
+
});
|
|
1075
|
+
|
|
1076
|
+
test("expands tableZod(table).select / .insert and .pick()", async () => {
|
|
1077
|
+
const schema = `
|
|
1078
|
+
import { store, field, id } from "okengine";
|
|
1079
|
+
export const spaces = store.schema.table("spaces", {
|
|
1080
|
+
id: field.text().primaryKey().defaultFn(id),
|
|
1081
|
+
key: field.text().notNull().unique(),
|
|
1082
|
+
name: field.text().notNull(),
|
|
1083
|
+
color: field.text(),
|
|
1084
|
+
});
|
|
1085
|
+
`;
|
|
1086
|
+
const zod = `
|
|
1087
|
+
import { createInsertSchema, createSelectSchema, createUpdateSchema } from "drizzle-orm/zod";
|
|
1088
|
+
export function tableZod(table) {
|
|
1089
|
+
return {
|
|
1090
|
+
select: createSelectSchema(table),
|
|
1091
|
+
insert: createInsertSchema(table),
|
|
1092
|
+
update: createUpdateSchema(table),
|
|
1093
|
+
};
|
|
1094
|
+
}
|
|
1095
|
+
export const spacesZod = tableZod(spaces);
|
|
1096
|
+
`;
|
|
1097
|
+
const flows = `
|
|
1098
|
+
import { flow } from "okengine";
|
|
1099
|
+
export const create = flow("spaces.create", {
|
|
1100
|
+
in: spacesZod.insert,
|
|
1101
|
+
out: spacesZod.select.pick({ id: true, key: true }),
|
|
1102
|
+
do: async () => ({}),
|
|
1103
|
+
});
|
|
1104
|
+
`;
|
|
1105
|
+
const manifest = await extractFromSources({
|
|
1106
|
+
"src/schema.decl.ts": schema,
|
|
1107
|
+
"src/db/zod.ts": zod,
|
|
1108
|
+
"src/flows/spaces.ts": flows,
|
|
1109
|
+
});
|
|
1110
|
+
expect(manifest.flows?.["spaces.create"]?.in).toMatchObject({
|
|
1111
|
+
type: "object",
|
|
1112
|
+
properties: {
|
|
1113
|
+
key: { type: "string", unique: true },
|
|
1114
|
+
name: { type: "string" },
|
|
1115
|
+
color: { type: "string" },
|
|
1116
|
+
},
|
|
1117
|
+
required: ["key", "name"],
|
|
1118
|
+
});
|
|
1119
|
+
expect(manifest.flows?.["spaces.create"]?.out).toEqual({
|
|
1120
|
+
type: "object",
|
|
1121
|
+
properties: {
|
|
1122
|
+
id: { type: "string", primaryKey: true },
|
|
1123
|
+
key: { type: "string", unique: true },
|
|
1124
|
+
},
|
|
1125
|
+
required: ["id", "key"],
|
|
1126
|
+
});
|
|
1127
|
+
});
|
|
1128
|
+
|
|
1129
|
+
test("bindCrud stamps createIn / out onto CRUD flows", async () => {
|
|
1130
|
+
const source = `
|
|
1131
|
+
import { store, field, id } from "okengine";
|
|
1132
|
+
import { z } from "zod";
|
|
1133
|
+
|
|
1134
|
+
export const notes = store.schema.table("notes", {
|
|
1135
|
+
id: field.text().primaryKey().defaultFn(id),
|
|
1136
|
+
title: field.text().notNull(),
|
|
1137
|
+
});
|
|
1138
|
+
|
|
1139
|
+
const createIn = z.object({
|
|
1140
|
+
title: z.string().min(1),
|
|
1141
|
+
});
|
|
1142
|
+
|
|
1143
|
+
export const { list, create, get, update, remove } = bindCrud({
|
|
1144
|
+
unit: "notes",
|
|
1145
|
+
path: "/notes",
|
|
1146
|
+
table: notes,
|
|
1147
|
+
createIn,
|
|
1148
|
+
out: createSelectSchema(notes),
|
|
1149
|
+
});
|
|
1150
|
+
`;
|
|
1151
|
+
const manifest = await extractFromSources({ "src/flows/notes/index.ts": source });
|
|
1152
|
+
expect(manifest.flows?.["notes.create"]?.in).toEqual({
|
|
1153
|
+
type: "object",
|
|
1154
|
+
properties: { title: { type: "string", minLength: 1 } },
|
|
1155
|
+
required: ["title"],
|
|
1156
|
+
});
|
|
1157
|
+
expect(manifest.flows?.["notes.get"]?.out).toMatchObject({
|
|
1158
|
+
type: "object",
|
|
1159
|
+
properties: {
|
|
1160
|
+
id: { type: "string", primaryKey: true },
|
|
1161
|
+
title: { type: "string" },
|
|
1162
|
+
},
|
|
1163
|
+
});
|
|
1164
|
+
expect(manifest.flows?.["notes.list"]?.in).toMatchObject({
|
|
1165
|
+
type: "object",
|
|
1166
|
+
properties: { q: { type: "string" }, limit: { type: "integer" } },
|
|
1167
|
+
});
|
|
1168
|
+
expect((manifest.flows?.["notes.update"]?.in as { required?: string[] }).required).toContain(
|
|
1169
|
+
"id",
|
|
1170
|
+
);
|
|
1171
|
+
});
|
|
1172
|
+
|
|
1173
|
+
test("same-named createIn stays file-local (does not leak across units)", async () => {
|
|
1174
|
+
const spaces = `
|
|
1175
|
+
import { flow } from "okengine";
|
|
1176
|
+
import { z } from "zod";
|
|
1177
|
+
const createIn = z.object({ key: z.string(), name: z.string() });
|
|
1178
|
+
export const create = flow("spaces.create", {
|
|
1179
|
+
in: createIn,
|
|
1180
|
+
do: async () => ({}),
|
|
1181
|
+
});
|
|
1182
|
+
`;
|
|
1183
|
+
const views = `
|
|
1184
|
+
import { flow } from "okengine";
|
|
1185
|
+
import { z } from "zod";
|
|
1186
|
+
const createIn = z.object({ kind: z.enum(["list", "board"]) });
|
|
1187
|
+
export const create = flow("views.create", {
|
|
1188
|
+
in: createIn,
|
|
1189
|
+
do: async () => ({}),
|
|
1190
|
+
});
|
|
1191
|
+
`;
|
|
1192
|
+
const manifest = await extractFromSources({
|
|
1193
|
+
"src/flows/spaces/index.ts": spaces,
|
|
1194
|
+
"src/flows/views/index.ts": views,
|
|
1195
|
+
});
|
|
1196
|
+
expect(manifest.flows?.["spaces.create"]?.in).toEqual({
|
|
1197
|
+
type: "object",
|
|
1198
|
+
properties: { key: { type: "string" }, name: { type: "string" } },
|
|
1199
|
+
required: ["key", "name"],
|
|
1200
|
+
});
|
|
1201
|
+
expect(manifest.flows?.["views.create"]?.in).toEqual({
|
|
1202
|
+
type: "object",
|
|
1203
|
+
properties: { kind: { type: "string", enum: ["list", "board"] } },
|
|
1204
|
+
required: ["kind"],
|
|
1205
|
+
});
|
|
1206
|
+
});
|
|
1207
|
+
});
|