okengine 0.13.0 → 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/manifest.v1.schema.json +7 -1
- package/package.json +4 -5
- package/site/content/docs/ai/mcp.mdx +27 -2
- package/site/content/docs/elements/ai.mdx +59 -11
- package/site/content/docs/elements/channel.mdx +1 -0
- package/site/content/docs/elements/clock.mdx +30 -12
- package/site/content/docs/elements/flow.mdx +9 -3
- package/site/content/docs/elements/gate.mdx +25 -22
- package/site/content/docs/elements/signal.mdx +71 -25
- package/site/content/docs/elements/store.mdx +87 -28
- 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/installation.mdx +2 -2
- 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/fx.mdx +27 -22
- 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/ai-setup/recommend.test.ts +25 -0
- package/src/cli/ai-setup/recommend.ts +8 -3
- 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/effects-infer.ts +58 -3
- package/src/compiler/extract.test.ts +458 -29
- package/src/compiler/extract.ts +387 -69
- 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/response.ts +45 -1
- package/src/compiler/schema-from-ast.ts +609 -0
- package/src/console/server/ai.ts +5 -2
- package/src/console/server/bind.ts +2 -2
- package/src/console/server/console.test.ts +3 -0
- package/src/console/server/flows.ts +121 -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/serve.ts +17 -2
- 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-F1FI122b.js +1 -0
- 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-HK9-BO8z.js +4 -0
- 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/seed-invoke-host.ts +2 -0
- 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/graph/build-flow-graph.test.ts +18 -0
- package/src/console/ui-next/src/features/flows/graph/build-flow-graph.ts +14 -1
- package/src/console/ui-next/src/features/flows/graph/neighborhood.test.ts +17 -0
- package/src/console/ui-next/src/features/flows/graph/neighborhood.ts +16 -3
- package/src/console/ui-next/src/features/flows/traces/effect-kind.ts +3 -1
- package/src/console/ui-next/src/features/flows/traces/effect-summary.ts +24 -0
- 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-sheet.tsx +9 -3
- package/src/console/ui-next/src/features/flows/traces/trace-detail.test.ts +29 -2
- package/src/console/ui-next/src/features/flows/traces/traces-pane.tsx +10 -3
- package/src/console/ui-next/src/features/observability/lib/ask-count.test.ts +25 -0
- package/src/console/ui-next/src/features/observability/lib/ask-count.ts +4 -1
- 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/detail/effects-summary.tsx +12 -4
- 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 +4 -1
- package/src/docker/dockerfile.ts +1 -1
- package/src/docker/recipes/pgdog.ts +2 -0
- package/src/drivers/ai-anthropic.ts +5 -0
- package/src/drivers/ai-ollama.ts +49 -30
- package/src/drivers/ai-openai-compatible.ts +57 -46
- package/src/drivers/ai-providers.test.ts +3 -0
- package/src/drivers/bun-native-completeness.test.ts +7 -9
- 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/redis.ts +11 -4
- package/src/drivers/signal-redis.ts +24 -14
- package/src/drivers/signal-types.ts +2 -1
- package/src/drivers/types.ts +10 -0
- package/src/elements/ai/declare.ts +109 -0
- package/src/elements/ai/errors.test.ts +5 -1
- package/src/elements/ai/errors.ts +30 -2
- package/src/elements/ai/eval.ts +4 -6
- package/src/elements/ai/mcp-client.test.ts +206 -0
- package/src/elements/ai/mcp-client.ts +362 -0
- package/src/elements/ai/mcp-http.ts +159 -0
- package/src/elements/ai/mcp-mock.ts +134 -0
- package/src/elements/ai/mcp-protocol.ts +234 -0
- package/src/elements/ai/mcp-stdio.test.ts +50 -0
- package/src/elements/ai/mcp-stdio.ts +212 -0
- package/src/elements/ai/mcp-transport.ts +70 -0
- package/src/elements/ai/runtime.ts +159 -29
- package/src/elements/ai.test.ts +139 -0
- package/src/elements/ai.ts +16 -0
- package/src/elements/clock/declare.ts +26 -1
- package/src/elements/clock/health.test.ts +43 -0
- package/src/elements/clock/runtime.ts +55 -2
- package/src/elements/clock/schedule.ts +71 -142
- package/src/elements/clock.test.ts +9 -40
- package/src/elements/clock.ts +6 -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/index.ts +2 -0
- package/src/elements/signal/declare.ts +2 -1
- package/src/elements/signal/runtime.ts +16 -1
- package/src/elements/signal.ts +1 -0
- package/src/elements/store/cache.test.ts +2 -0
- package/src/elements/store/cache.ts +3 -3
- 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/elements/vault/builtin-adapter.ts +17 -0
- package/src/full.ts +2 -0
- package/src/http.ts +20 -3
- package/src/index.ts +14 -1
- package/src/kernel/adopt-barrel-fresh.test.ts +1 -1
- package/src/kernel/adopt-routes.ts +30 -2
- package/src/kernel/app.ts +227 -77
- package/src/kernel/auto-cache.test.ts +8 -8
- package/src/kernel/auto-registry.test.ts +54 -9
- package/src/kernel/boot-bind/ai.ts +24 -0
- package/src/kernel/boot-bind/clock.ts +2 -0
- package/src/kernel/boot.test.ts +4 -4
- package/src/kernel/boot.ts +5 -3
- package/src/kernel/effects-stamping.test.ts +3 -3
- package/src/kernel/element-registries.ts +15 -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-dead-letters.test.ts +77 -0
- package/src/kernel/fx.test.ts +22 -0
- package/src/kernel/fx.ts +135 -6
- 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 +174 -0
- package/src/kernel/index.ts +2 -0
- 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/mcp-ref.ts +88 -0
- package/src/manifest/types.ts +41 -2
- package/src/manifest/validate.test.ts +20 -0
- package/src/mcp/docs-server.ts +1 -1
- package/src/mcp/server.ts +1 -1
- package/src/plugins/compression.test.ts +21 -0
- package/src/plugins/compression.ts +1 -0
- package/src/release/measure.ts +2 -2
- package/src/runtime/bun.ts +41 -4
- 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 +10 -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/cache-glyph-CLPBqZeb.js +0 -1
- package/src/console/ui-next/dist/assets/collapsible-DYb0xU8C.js +0 -1
- package/src/console/ui-next/dist/assets/flows-page-Dg8CTE29.js +0 -1
- package/src/console/ui-next/dist/assets/http-method-BJ92Z_ke.js +0 -1
- package/src/console/ui-next/dist/assets/index-Bp-R7jtM.js +0 -66
- package/src/console/ui-next/dist/assets/index-D4Ldtj79.css +0 -2
- package/src/console/ui-next/dist/assets/observability-page-DAnpEaq1.js +0 -4
- 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-D16lWQMt.js +0 -2
- package/src/console/ui-next/dist/assets/units-page-BtQ0bqMe.js +0 -1
- package/src/console/ui-next/dist/assets/vault-page-Ca-MvcmJ.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,16 +134,22 @@ export const flow_${i} = on(
|
|
|
132
134
|
}),
|
|
133
135
|
);
|
|
134
136
|
`);
|
|
135
|
-
|
|
136
|
-
|
|
137
|
-
|
|
138
|
-
|
|
139
|
-
|
|
140
|
-
|
|
141
|
-
|
|
142
|
-
|
|
143
|
-
|
|
144
|
-
|
|
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
|
+
);
|
|
145
153
|
});
|
|
146
154
|
|
|
147
155
|
describe("serializeManifest stability", () => {
|
|
@@ -238,6 +246,32 @@ export const db = store.sql("app", { schema: { notes } });
|
|
|
238
246
|
});
|
|
239
247
|
});
|
|
240
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
|
+
|
|
241
275
|
test("extracts optional description fields additively", async () => {
|
|
242
276
|
const source = `
|
|
243
277
|
import { store, field, signal, channel, clock, gate, vault } from "okengine";
|
|
@@ -465,6 +499,27 @@ export const remove = mounted.remove;
|
|
|
465
499
|
expect(manifest.flows?.update?.live).toBeUndefined();
|
|
466
500
|
expect(manifest.flows?.remove?.live).toBeUndefined();
|
|
467
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
|
+
});
|
|
468
523
|
});
|
|
469
524
|
|
|
470
525
|
describe("extractManifest — vault.config", () => {
|
|
@@ -509,7 +564,7 @@ import { on, flow, http, gate, store } from "okengine";
|
|
|
509
564
|
export const files = store.files("uploads");
|
|
510
565
|
|
|
511
566
|
export const attach = on(
|
|
512
|
-
http.post("/attach").
|
|
567
|
+
http.post("/attach").public(),
|
|
513
568
|
flow("notes.attach", {
|
|
514
569
|
do: async (input, fx) => {
|
|
515
570
|
await fx.store(files).put("key", input.text);
|
|
@@ -530,7 +585,7 @@ import { on, flow, http, gate, store } from "okengine";
|
|
|
530
585
|
export const files = store.files("uploads");
|
|
531
586
|
|
|
532
587
|
export const attach = on(
|
|
533
|
-
http.post("/attach").
|
|
588
|
+
http.post("/attach").public(),
|
|
534
589
|
flow("notes.attachImage", {
|
|
535
590
|
do: async (input, fx) => {
|
|
536
591
|
await fx.store(files).putImage("key", input.bytes);
|
|
@@ -598,6 +653,25 @@ export const expire = on(
|
|
|
598
653
|
});
|
|
599
654
|
});
|
|
600
655
|
|
|
656
|
+
describe("extractManifest — fx.deadLetters", () => {
|
|
657
|
+
test("fx.deadLetters(signal) infers reads: [signal:<name>]", async () => {
|
|
658
|
+
const source = `
|
|
659
|
+
import { on, flow, http, signal } from "okengine";
|
|
660
|
+
|
|
661
|
+
export const notify = signal("notify", { delivery: "once" });
|
|
662
|
+
|
|
663
|
+
export const failed = on(
|
|
664
|
+
http.get("/notifications/failed").public(),
|
|
665
|
+
flow("notifications.failed", {
|
|
666
|
+
do: async (input, fx) => fx.json.withQuery(await fx.deadLetters(notify), input),
|
|
667
|
+
}),
|
|
668
|
+
);
|
|
669
|
+
`;
|
|
670
|
+
const manifest = await extractFromSources({ "src/flows/failed.ts": source });
|
|
671
|
+
expect(manifest.flows?.["notifications.failed"]?.effects?.reads).toEqual(["signal:notify"]);
|
|
672
|
+
});
|
|
673
|
+
});
|
|
674
|
+
|
|
601
675
|
describe("extractManifest — channel medium binder aliasing", () => {
|
|
602
676
|
test("mail.template(...) resolves through `const mail = channel.email(...)`", async () => {
|
|
603
677
|
const source = `
|
|
@@ -610,7 +684,7 @@ export const noteCreatedMail = mail.template("note-created", {
|
|
|
610
684
|
});
|
|
611
685
|
|
|
612
686
|
export const onCreated = on(
|
|
613
|
-
http.post("/hook").
|
|
687
|
+
http.post("/hook").public(),
|
|
614
688
|
flow("notes.onCreated", {
|
|
615
689
|
do: async (payload, fx) => {
|
|
616
690
|
await fx.send(noteCreatedMail, { to: "you@localhost", data: payload });
|
|
@@ -633,7 +707,7 @@ const otp = channel.sms({});
|
|
|
633
707
|
export const otpTemplate = otp.template("otp-code", {});
|
|
634
708
|
|
|
635
709
|
export const send = on(
|
|
636
|
-
http.post("/otp").
|
|
710
|
+
http.post("/otp").public(),
|
|
637
711
|
flow("notes.sendOtp", {
|
|
638
712
|
do: async (payload, fx) => {
|
|
639
713
|
await fx.send(otpTemplate, { to: "+10000000000", data: payload });
|
|
@@ -716,19 +790,32 @@ export const decoy = flow("checkout.realName", {
|
|
|
716
790
|
});
|
|
717
791
|
});
|
|
718
792
|
|
|
719
|
-
describe("extractManifest — .
|
|
720
|
-
test("reads the public sentinel from
|
|
793
|
+
describe("extractManifest — .public()", () => {
|
|
794
|
+
test("reads the public sentinel from .public()", async () => {
|
|
721
795
|
const source = `
|
|
722
796
|
import { on, flow, http } from "okengine";
|
|
723
797
|
|
|
724
798
|
export const health = on(
|
|
725
|
-
http.get("/health").
|
|
799
|
+
http.get("/health").public(),
|
|
726
800
|
flow("health.check", { do: () => ({ ok: true }) }),
|
|
727
801
|
);
|
|
728
802
|
`;
|
|
729
803
|
const manifest = await extractFromSources({ "src/flows/health.ts": source });
|
|
730
804
|
expect(manifest.flows?.["health.check"]?.gates).toEqual(["public"]);
|
|
731
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
|
+
});
|
|
732
819
|
});
|
|
733
820
|
|
|
734
821
|
describe("extractManifest — gate.all", () => {
|
|
@@ -776,3 +863,345 @@ export const create = on(
|
|
|
776
863
|
expect(manifest.gates?.WRITE).toBeUndefined();
|
|
777
864
|
});
|
|
778
865
|
});
|
|
866
|
+
|
|
867
|
+
describe("extractManifest — ai.mcpServer", () => {
|
|
868
|
+
test("stamps mcpServers and infers mcp: calls from .tool()", async () => {
|
|
869
|
+
const source = `
|
|
870
|
+
import { ai, flow, on, http, vault } from "okengine";
|
|
871
|
+
|
|
872
|
+
export const token = vault.secret("GITHUB_TOKEN");
|
|
873
|
+
export const github = ai.mcpServer("github", {
|
|
874
|
+
url: "https://mcp.example/github",
|
|
875
|
+
auth: { bearer: token },
|
|
876
|
+
tools: ["create_issue"],
|
|
877
|
+
});
|
|
878
|
+
|
|
879
|
+
export const triage = on(
|
|
880
|
+
http.post("/triage"),
|
|
881
|
+
flow("support.triage", {
|
|
882
|
+
do: async (input, fx) => {
|
|
883
|
+
await fx.ask("ticket-triage", input, { tools: [github.tool("create_issue")] });
|
|
884
|
+
},
|
|
885
|
+
}),
|
|
886
|
+
);
|
|
887
|
+
`;
|
|
888
|
+
const manifest = await extractFromSources({ "src/ai.ts": source });
|
|
889
|
+
expect(manifest.ai?.mcpServers?.github).toEqual({
|
|
890
|
+
url: "https://mcp.example/github",
|
|
891
|
+
auth: "GITHUB_TOKEN",
|
|
892
|
+
tools: ["create_issue"],
|
|
893
|
+
});
|
|
894
|
+
expect(manifest.flows?.["support.triage"]?.effects?.calls).toEqual(["mcp:github/create_issue"]);
|
|
895
|
+
});
|
|
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
|
+
});
|