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
|
@@ -47,7 +47,7 @@ const CreateOut = z.object({ ok: z.boolean() });
|
|
|
47
47
|
|
|
48
48
|
function buildUnannotatedCreateFlow(db: ReturnType<typeof store.sql>) {
|
|
49
49
|
return on(
|
|
50
|
-
http.post("/notes").
|
|
50
|
+
http.post("/notes").public(),
|
|
51
51
|
flow("notes.create", {
|
|
52
52
|
in: CreateIn,
|
|
53
53
|
out: CreateOut,
|
|
@@ -140,7 +140,7 @@ describe("boot-level: table-ref resolution stays backward compatible", () => {
|
|
|
140
140
|
resetFlowSeq();
|
|
141
141
|
const db = store.sql("app", { schema: { notes } });
|
|
142
142
|
const create = on(
|
|
143
|
-
http.post("/notes").
|
|
143
|
+
http.post("/notes").public(),
|
|
144
144
|
flow("notes.create", {
|
|
145
145
|
in: CreateIn,
|
|
146
146
|
out: CreateOut,
|
|
@@ -193,7 +193,7 @@ import { on, flow, http, gate } from "okengine";
|
|
|
193
193
|
const db = {} as any;
|
|
194
194
|
const notes = {} as any;
|
|
195
195
|
export const create = on(
|
|
196
|
-
http.post("/notes").
|
|
196
|
+
http.post("/notes").public(),
|
|
197
197
|
flow("notes.create", {
|
|
198
198
|
do: async (input, fx) => {
|
|
199
199
|
await fx.store(db).insert(notes).values({ id: input.id, title: input.title, createdAt: 1 });
|
|
@@ -1,8 +1,9 @@
|
|
|
1
1
|
/**
|
|
2
|
-
* Module-evaluation registries for `store
|
|
3
|
-
* `vault.env.required`, `signal()`, `
|
|
4
|
-
*
|
|
5
|
-
*
|
|
2
|
+
* Module-evaluation registries for `store.*`, `vault.secret`,
|
|
3
|
+
* `vault.env.required`, `signal()`, `clock()`, `gate.policy` / `.scope` /
|
|
4
|
+
* `.rate`, `channel.<medium>().template()`, and `ai.model` / `.prompt` /
|
|
5
|
+
* `ai.embed` / `ai.agent` / `ai.mcpServer` — the plain arrays behind each
|
|
6
|
+
* declare module's `listX()` / `resetX()` pair (mirrors `on.ts`'s
|
|
6
7
|
* trigger-drain `bindings` array).
|
|
7
8
|
*
|
|
8
9
|
* Lives here, not in the declare modules themselves, so {@link oke} can read
|
|
@@ -24,8 +25,10 @@ import type { StoreDecl } from "../elements/store/declare.ts";
|
|
|
24
25
|
import type { VaultSecretDecl } from "../elements/vault/declare.ts";
|
|
25
26
|
import type { SignalDecl } from "../elements/signal/declare.ts";
|
|
26
27
|
import type { ChannelTemplateDecl } from "../elements/channel/declare.ts";
|
|
28
|
+
import type { ClockDecl } from "../elements/clock/declare.ts";
|
|
29
|
+
import type { GateDecl } from "../elements/gate/declare.ts";
|
|
27
30
|
|
|
28
|
-
/** `store.sql` / `store.files` declarations since the last reset. */
|
|
31
|
+
/** `store.sql` / `store.kv` / `store.files` / `store.index` declarations since the last reset. */
|
|
29
32
|
export const storeRegistry: StoreDecl[] = [];
|
|
30
33
|
/** `vault.secret` declarations since the last reset. */
|
|
31
34
|
export const secretRegistry: VaultSecretDecl[] = [];
|
|
@@ -33,6 +36,10 @@ export const secretRegistry: VaultSecretDecl[] = [];
|
|
|
33
36
|
export const requiredEnvRegistry: string[] = [];
|
|
34
37
|
/** `signal()` declarations since the last reset. */
|
|
35
38
|
export const signalRegistry: SignalDecl[] = [];
|
|
39
|
+
/** `clock()` declarations since the last reset. */
|
|
40
|
+
export const clockRegistry: ClockDecl[] = [];
|
|
41
|
+
/** `gate.policy` / `gate.scope` / `gate.rate` declarations since the last reset. */
|
|
42
|
+
export const gateRegistry: GateDecl[] = [];
|
|
36
43
|
/** Medium-binder `.template()` declarations since the last reset. */
|
|
37
44
|
export const channelTemplateRegistry: ChannelTemplateDecl[] = [];
|
|
38
45
|
/** `ai.model` declarations since the last reset. */
|
package/src/kernel/errors.ts
CHANGED
|
@@ -183,6 +183,30 @@ export const OKE_ERRORS = {
|
|
|
183
183
|
cause: "src/flows/{unit} exists on disk but adopted no flows — the .adopt() barrel is stale.",
|
|
184
184
|
fix: "Run `oke dev` or `oke build` to regenerate `src/flows/generated.ts`.",
|
|
185
185
|
},
|
|
186
|
+
/**
|
|
187
|
+
* `http.get()` was never stamped from the file tree — refuse a silent `/`.
|
|
188
|
+
*/
|
|
189
|
+
HTTP_PATH_UNRESOLVED: {
|
|
190
|
+
code: 1010,
|
|
191
|
+
cause: 'Flow "{flow}" bound {method} with no path — the file-tree stamp never ran.',
|
|
192
|
+
fix: 'Put the file under `src/flows/<unit>/` and import `@/flows/generated`, or pass an explicit path to `http.{method}("/…")`.',
|
|
193
|
+
},
|
|
194
|
+
/**
|
|
195
|
+
* Two HTTP bindings share method + path — last-add-wins is the opposite of this DX.
|
|
196
|
+
*/
|
|
197
|
+
HTTP_ROUTE_DUPLICATE: {
|
|
198
|
+
code: 1011,
|
|
199
|
+
cause: '{method} {path} is bound twice (flow "{flow}").',
|
|
200
|
+
fix: "Give each HTTP flow a unique method + path.",
|
|
201
|
+
},
|
|
202
|
+
/**
|
|
203
|
+
* Adopted HTTP flow still has no name (`flow({ do })` outside a unit).
|
|
204
|
+
*/
|
|
205
|
+
HTTP_FLOW_UNNAMED: {
|
|
206
|
+
code: 1012,
|
|
207
|
+
cause: "An HTTP flow on {method} {path} has no name.",
|
|
208
|
+
fix: 'Use `flow("unit.export", {…})` or export it from a `src/flows/<unit>/` file so the tree can stamp `unit.export`.',
|
|
209
|
+
},
|
|
186
210
|
/**
|
|
187
211
|
* Emit target has no subscriber (unified-theory §21).
|
|
188
212
|
* Thrown at emit when `optional` is false and nobody is subscribed.
|
|
@@ -0,0 +1,64 @@
|
|
|
1
|
+
/**
|
|
2
|
+
* Module-evaluation registry for `src/flows/generated.ts`.
|
|
3
|
+
*
|
|
4
|
+
* Same consume/ignore posture as `on()` / `listBindings()`: generated.ts
|
|
5
|
+
* calls {@link registerFlowUnits}; {@link oke} drains the bag into `$routes`
|
|
6
|
+
* unless `registry: "ignore"`.
|
|
7
|
+
*/
|
|
8
|
+
|
|
9
|
+
import type { AnyFlowDef } from "./flow.ts";
|
|
10
|
+
import { isFlow } from "./flow.ts";
|
|
11
|
+
|
|
12
|
+
/** Unit name → export name → Flow. */
|
|
13
|
+
export type FlowUnitBag = {
|
|
14
|
+
readonly [exportName: string]: unknown;
|
|
15
|
+
};
|
|
16
|
+
|
|
17
|
+
const pending: Record<string, FlowUnitBag> = {};
|
|
18
|
+
|
|
19
|
+
/**
|
|
20
|
+
* Record generated flow units for the next {@link oke} construction.
|
|
21
|
+
*
|
|
22
|
+
* @param units - `{ notes, main, … }` from `generated.ts`
|
|
23
|
+
*/
|
|
24
|
+
export function registerFlowUnits(units: Record<string, FlowUnitBag>): void {
|
|
25
|
+
Object.assign(pending, units);
|
|
26
|
+
}
|
|
27
|
+
|
|
28
|
+
/**
|
|
29
|
+
* Drain registered units (consume). Clears the registry.
|
|
30
|
+
*/
|
|
31
|
+
export function consumeRegisteredFlowUnits(): Record<string, FlowUnitBag> {
|
|
32
|
+
const snap: Record<string, FlowUnitBag> = { ...pending };
|
|
33
|
+
resetRegisteredFlowUnits();
|
|
34
|
+
return snap;
|
|
35
|
+
}
|
|
36
|
+
|
|
37
|
+
/**
|
|
38
|
+
* Snapshot without clearing (keep / tests).
|
|
39
|
+
*/
|
|
40
|
+
export function peekRegisteredFlowUnits(): Record<string, FlowUnitBag> {
|
|
41
|
+
return { ...pending };
|
|
42
|
+
}
|
|
43
|
+
|
|
44
|
+
/**
|
|
45
|
+
* Clear the unit registry (tests / `registry: "consume"` after drain).
|
|
46
|
+
*/
|
|
47
|
+
export function resetRegisteredFlowUnits(): void {
|
|
48
|
+
for (const key of Object.keys(pending)) {
|
|
49
|
+
delete pending[key];
|
|
50
|
+
}
|
|
51
|
+
}
|
|
52
|
+
|
|
53
|
+
/**
|
|
54
|
+
* Collect FlowDefs from a unit bag (skip non-flow exports).
|
|
55
|
+
*
|
|
56
|
+
* @param bag - Namespace object
|
|
57
|
+
*/
|
|
58
|
+
export function flowsInUnitBag(bag: FlowUnitBag): AnyFlowDef[] {
|
|
59
|
+
const found: AnyFlowDef[] = [];
|
|
60
|
+
for (const value of Object.values(bag)) {
|
|
61
|
+
if (isFlow(value)) found.push(value);
|
|
62
|
+
}
|
|
63
|
+
return found;
|
|
64
|
+
}
|
package/src/kernel/flow.ts
CHANGED
|
@@ -239,13 +239,34 @@ export const flowBrand: unique symbol = Symbol("oke.flow");
|
|
|
239
239
|
* Input / output types are inferred from Standard Schema `in` / `out` when
|
|
240
240
|
* present; otherwise from the `do` handler signature.
|
|
241
241
|
*
|
|
242
|
+
* A nameless `flow({ do })` is stamped `unit.export` by the file-tree
|
|
243
|
+
* generator, {@link unit}, or `.adopt()`. Explicit `flow("notes.get", {…})`
|
|
244
|
+
* still wins.
|
|
245
|
+
*
|
|
246
|
+
* @param options - Contract and handler (name stamped later)
|
|
247
|
+
*/
|
|
248
|
+
export function flow<Opts extends FlowOptions<any, any, any>>(
|
|
249
|
+
options: Opts,
|
|
250
|
+
): FlowDef<InferFlowIn<Opts>, InferFlowOut<Opts>, InferFlowErrors<Opts>>;
|
|
251
|
+
/**
|
|
242
252
|
* @param name - Stable name (used by `fx.call` and the Manifest)
|
|
243
253
|
* @param options - Contract and handler
|
|
244
254
|
*/
|
|
245
255
|
export function flow<Opts extends FlowOptions<any, any, any>>(
|
|
246
256
|
name: string,
|
|
247
257
|
options: Opts & { readonly name?: never },
|
|
248
|
-
): FlowDef<InferFlowIn<Opts>, InferFlowOut<Opts>, InferFlowErrors<Opts
|
|
258
|
+
): FlowDef<InferFlowIn<Opts>, InferFlowOut<Opts>, InferFlowErrors<Opts>>;
|
|
259
|
+
export function flow(
|
|
260
|
+
nameOrOptions: string | FlowOptions<any, any, any>,
|
|
261
|
+
maybeOptions?: FlowOptions<any, any, any> & { readonly name?: never },
|
|
262
|
+
): FlowDef {
|
|
263
|
+
const named = typeof nameOrOptions === "string";
|
|
264
|
+
const name = named ? nameOrOptions : "";
|
|
265
|
+
const options = (named ? maybeOptions : nameOrOptions) as FlowOptions<any, any, any> | undefined;
|
|
266
|
+
if (!options || typeof options.do !== "function") {
|
|
267
|
+
throw new TypeError("flow() expected an options bag with a do handler");
|
|
268
|
+
}
|
|
269
|
+
|
|
249
270
|
const triggers: Trigger[] = [];
|
|
250
271
|
const hooks: Partial<Record<HookStage, HookFn[]>> = {};
|
|
251
272
|
const pendingPlugins: PluginDef[] = [];
|
|
@@ -253,13 +274,13 @@ export function flow<Opts extends FlowOptions<any, any, any>>(
|
|
|
253
274
|
const dot = name.indexOf(".");
|
|
254
275
|
const unit = dot > 0 ? name.slice(0, dot) : undefined;
|
|
255
276
|
|
|
256
|
-
const def: FlowDef
|
|
277
|
+
const def: FlowDef = {
|
|
257
278
|
[flowBrand]: true,
|
|
258
279
|
name,
|
|
259
280
|
unit,
|
|
260
281
|
in: options.in,
|
|
261
282
|
out: options.out,
|
|
262
|
-
errors: (options.errors ?? undefined) as
|
|
283
|
+
errors: (options.errors ?? undefined) as FlowErrorMap | undefined,
|
|
263
284
|
effects: options.effects,
|
|
264
285
|
durable: options.durable ?? false,
|
|
265
286
|
retry: options.retry,
|
|
@@ -268,17 +289,8 @@ export function flow<Opts extends FlowOptions<any, any, any>>(
|
|
|
268
289
|
slo: options.slo,
|
|
269
290
|
plane: options.plane,
|
|
270
291
|
breaking: options.breaking ?? false,
|
|
271
|
-
compensate: options.compensate as
|
|
272
|
-
|
|
273
|
-
ctx: {
|
|
274
|
-
readonly input: InferFlowIn<Opts>;
|
|
275
|
-
readonly error: unknown;
|
|
276
|
-
readonly completedSteps: readonly string[];
|
|
277
|
-
},
|
|
278
|
-
fx: import("./fx.ts").Fx,
|
|
279
|
-
) => unknown | Promise<unknown>)
|
|
280
|
-
| undefined,
|
|
281
|
-
do: options.do as FlowHandler<InferFlowIn<Opts>, InferFlowOut<Opts>>,
|
|
292
|
+
compensate: options.compensate as FlowDef["compensate"],
|
|
293
|
+
do: options.do as FlowHandler,
|
|
282
294
|
triggers,
|
|
283
295
|
$trigger: undefined,
|
|
284
296
|
hooks,
|
|
@@ -320,9 +332,9 @@ export function isFlow(value: unknown): value is AnyFlowDef {
|
|
|
320
332
|
}
|
|
321
333
|
|
|
322
334
|
/**
|
|
323
|
-
* No-op. `
|
|
324
|
-
*
|
|
325
|
-
*
|
|
335
|
+
* No-op. Nameless flows start as `""` and are stamped by unit / adopt /
|
|
336
|
+
* the file-tree generator — kept so existing test `beforeEach` blocks
|
|
337
|
+
* don't need touching.
|
|
326
338
|
*
|
|
327
339
|
* @internal
|
|
328
340
|
*/
|
package/src/kernel/fx.ts
CHANGED
|
@@ -27,6 +27,8 @@ import type {
|
|
|
27
27
|
StoreRuntime,
|
|
28
28
|
SqlStoreHandle,
|
|
29
29
|
} from "../elements/store.ts";
|
|
30
|
+
import { rlsIdentityFromAuth } from "../elements/store.ts";
|
|
31
|
+
import type { RlsIdentity } from "../drivers/pg-rls.ts";
|
|
30
32
|
import type { SqlRow } from "../drivers/types.ts";
|
|
31
33
|
import type { SignalDecl, SignalRuntime } from "../elements/signal.ts";
|
|
32
34
|
import type { DeadLetter, SignalEmitOptions } from "../drivers/signal-types.ts";
|
|
@@ -911,6 +913,12 @@ export interface CreateFxOptions {
|
|
|
911
913
|
readonly runId?: string;
|
|
912
914
|
/** Reveal PII through the store runtime (requires `pii:reveal` upstream). */
|
|
913
915
|
readonly revealPii?: boolean;
|
|
916
|
+
/** Trigger gate names for RLS (`oke.gate` = first policy/public). */
|
|
917
|
+
readonly rlsGateNames?: readonly string[];
|
|
918
|
+
/** Skip RLS stamp (Operator / `bypassGates` / operator-plane flows). */
|
|
919
|
+
readonly rlsBypass?: boolean;
|
|
920
|
+
/** Forced bag (Console / Call API). Wins over {@link rlsGateNames}. */
|
|
921
|
+
readonly rls?: import("../drivers/pg-rls.ts").RlsIdentity;
|
|
914
922
|
/**
|
|
915
923
|
* Active journal session when the flow is durable. Every `fx` call is
|
|
916
924
|
* recorded; `step` / `sleep` replay from the journal on resume.
|
|
@@ -1025,6 +1033,19 @@ export function createFxContext(options: CreateFxOptions): FxContext {
|
|
|
1025
1033
|
} satisfies FxPrincipal);
|
|
1026
1034
|
const journal = options.journal;
|
|
1027
1035
|
|
|
1036
|
+
function rlsInvokeContext(): { rls?: RlsIdentity } {
|
|
1037
|
+
if (options.rlsBypass === true) return {};
|
|
1038
|
+
if (options.rls) return { rls: options.rls };
|
|
1039
|
+
const identity = rlsIdentityFromAuth({
|
|
1040
|
+
userId: auth.userId,
|
|
1041
|
+
scopes: auth.scopes,
|
|
1042
|
+
gateNames: options.rlsGateNames ?? [],
|
|
1043
|
+
bypass: false,
|
|
1044
|
+
operator: false,
|
|
1045
|
+
});
|
|
1046
|
+
return identity ? { rls: identity } : {};
|
|
1047
|
+
}
|
|
1048
|
+
|
|
1028
1049
|
async function gated<T>(
|
|
1029
1050
|
kind: Parameters<CapabilityToken["assert"]>[0],
|
|
1030
1051
|
resource: string,
|
|
@@ -1342,6 +1363,7 @@ export function createFxContext(options: CreateFxOptions): FxContext {
|
|
|
1342
1363
|
cache.handle = await runtime.open(decl, {
|
|
1343
1364
|
effects: options.effects ?? {},
|
|
1344
1365
|
revealPii: options.revealPii,
|
|
1366
|
+
...rlsInvokeContext(),
|
|
1345
1367
|
});
|
|
1346
1368
|
}
|
|
1347
1369
|
return cache.handle;
|
|
@@ -1361,6 +1383,7 @@ export function createFxContext(options: CreateFxOptions): FxContext {
|
|
|
1361
1383
|
{
|
|
1362
1384
|
effects: options.effects ?? {},
|
|
1363
1385
|
revealPii: options.revealPii,
|
|
1386
|
+
...rlsInvokeContext(),
|
|
1364
1387
|
},
|
|
1365
1388
|
{
|
|
1366
1389
|
gate: gated,
|
|
@@ -190,11 +190,11 @@ const charge = flow("horizontal.charge", {
|
|
|
190
190
|
});
|
|
191
191
|
|
|
192
192
|
const bindings: Binding[] = [
|
|
193
|
-
{ trigger: http.get("/ping").
|
|
193
|
+
{ trigger: http.get("/ping").public(), flow: ping as AnyFlowDef },
|
|
194
194
|
{ trigger: http.get("/rate").gate(rateGate), flow: rate as AnyFlowDef },
|
|
195
|
-
{ trigger: http.post("/write").
|
|
196
|
-
{ trigger: http.post("/emit").
|
|
197
|
-
{ trigger: http.post("/charge").
|
|
195
|
+
{ trigger: http.post("/write").public(), flow: write as AnyFlowDef },
|
|
196
|
+
{ trigger: http.post("/emit").public(), flow: emit as AnyFlowDef },
|
|
197
|
+
{ trigger: http.post("/charge").public(), flow: charge as AnyFlowDef },
|
|
198
198
|
];
|
|
199
199
|
|
|
200
200
|
const app = oke({
|
|
@@ -0,0 +1,20 @@
|
|
|
1
|
+
/**
|
|
2
|
+
* Branded sentinel for `http.get()` with no path — never `"/"`.
|
|
3
|
+
* Lives in a leaf module so `oke()` can refuse leftovers without pulling
|
|
4
|
+
* the HTTP trigger constructors onto a Store-only graph.
|
|
5
|
+
*/
|
|
6
|
+
|
|
7
|
+
/** Unresolved path token. {@link stampHttpPath} replaces it from the file tree. */
|
|
8
|
+
export const HTTP_PATH_PENDING = "__oke_http_path_pending__" as const;
|
|
9
|
+
|
|
10
|
+
/** Type of {@link HTTP_PATH_PENDING}. */
|
|
11
|
+
export type HttpPathPending = typeof HTTP_PATH_PENDING;
|
|
12
|
+
|
|
13
|
+
/**
|
|
14
|
+
* True when an HTTP trigger still carries the unresolved path sentinel.
|
|
15
|
+
*
|
|
16
|
+
* @param path - Trigger path
|
|
17
|
+
*/
|
|
18
|
+
export function isPendingHttpPath(path: string): boolean {
|
|
19
|
+
return path === HTTP_PATH_PENDING;
|
|
20
|
+
}
|
|
@@ -3,6 +3,7 @@
|
|
|
3
3
|
* apps do not pay for the five-verb CRUD helper.
|
|
4
4
|
*/
|
|
5
5
|
|
|
6
|
+
import { GATE_PUBLIC_NAME } from "../elements/gate/flatten.ts";
|
|
6
7
|
import {
|
|
7
8
|
createGateAttach,
|
|
8
9
|
createHttpTrigger,
|
|
@@ -47,6 +48,9 @@ export function httpResource<P extends string>(
|
|
|
47
48
|
{ trigger: verb("DELETE", id, false), flow: ops.remove },
|
|
48
49
|
],
|
|
49
50
|
gate: createGateAttach((next) => httpResource(path, ops, next, isLive), gates),
|
|
51
|
+
public() {
|
|
52
|
+
return httpResource(path, ops, [...gates, GATE_PUBLIC_NAME], isLive);
|
|
53
|
+
},
|
|
50
54
|
live() {
|
|
51
55
|
return httpResource(path, ops, gates, true);
|
|
52
56
|
},
|
|
@@ -83,12 +83,12 @@ describe("fx.json.stream HTTP SSE", () => {
|
|
|
83
83
|
test("encodes token chunks as SSE and leaves JSON flows buffered", async () => {
|
|
84
84
|
const smart = ai.model("smart");
|
|
85
85
|
on(
|
|
86
|
-
http.get("/chat").
|
|
86
|
+
http.get("/chat").public(),
|
|
87
87
|
flow("chat.stream", {
|
|
88
88
|
do: (_input, fx) => fx.json.stream(fx.stream(smart, { prompt: "hi" })),
|
|
89
89
|
}),
|
|
90
90
|
);
|
|
91
|
-
on(http.get("/ping").
|
|
91
|
+
on(http.get("/ping").public(), flow("ping", { do: () => ({ ok: true as const }) }));
|
|
92
92
|
|
|
93
93
|
const app = oke({
|
|
94
94
|
name: "http-stream",
|
|
@@ -111,7 +111,7 @@ describe("fx.json.stream HTTP SSE", () => {
|
|
|
111
111
|
let providerAborted = false;
|
|
112
112
|
const smart = ai.model("smart");
|
|
113
113
|
on(
|
|
114
|
-
http.get("/slow").
|
|
114
|
+
http.get("/slow").public(),
|
|
115
115
|
flow("chat.slow", {
|
|
116
116
|
do: (_input, fx) => fx.json.stream(fx.stream(smart, { prompt: "long" })),
|
|
117
117
|
}),
|
|
@@ -139,7 +139,7 @@ describe("fx.json.stream HTTP SSE", () => {
|
|
|
139
139
|
await runs.open();
|
|
140
140
|
const smart = ai.model("smart");
|
|
141
141
|
on(
|
|
142
|
-
http.get("/chat").
|
|
142
|
+
http.get("/chat").public(),
|
|
143
143
|
flow("chat.stream", {
|
|
144
144
|
do: (_input, fx) => fx.json.stream(fx.stream(smart, { prompt: "hi" })),
|
|
145
145
|
}),
|
|
@@ -65,9 +65,9 @@ describe("gateDenialFailure", () => {
|
|
|
65
65
|
});
|
|
66
66
|
});
|
|
67
67
|
|
|
68
|
-
describe("http trigger — .
|
|
68
|
+
describe("http trigger — .public()", () => {
|
|
69
69
|
test("attaches the public sentinel without calling gate()", () => {
|
|
70
|
-
const trigger = http.get("/health").
|
|
70
|
+
const trigger = http.get("/health").public();
|
|
71
71
|
expect(trigger.gates.map((g) => (typeof g === "string" ? g : g.name))).toEqual(["public"]);
|
|
72
72
|
expect(trigger.live().isLive).toBe(true);
|
|
73
73
|
});
|
|
@@ -32,7 +32,7 @@ describe("plugin element contributions", () => {
|
|
|
32
32
|
.table("otp_codes")
|
|
33
33
|
.needs("store.sql");
|
|
34
34
|
|
|
35
|
-
on(http.get("/ok").
|
|
35
|
+
on(http.get("/ok").public(), flow("ok", { do: () => ({ ok: true }) }));
|
|
36
36
|
|
|
37
37
|
const app = oke({
|
|
38
38
|
name: "plugin-elements",
|
|
@@ -75,7 +75,7 @@ describe("plugin .needs() resolution", () => {
|
|
|
75
75
|
test("boot fails when plugin needs missing peer plugin", async () => {
|
|
76
76
|
resetBindings();
|
|
77
77
|
resetFlowSeq();
|
|
78
|
-
on(http.get("/x").
|
|
78
|
+
on(http.get("/x").public(), flow("x", { do: () => ({ ok: true }) }));
|
|
79
79
|
const dependent = plugin("needs-auth", { version: "0.0.1" }).needs("auth");
|
|
80
80
|
const app = oke({ name: "needs-boot", gate: { policies: [gate.public] } }).plug(dependent);
|
|
81
81
|
await expect(app.boot({ env: "test" })).rejects.toThrow(PluginNeedsError);
|
package/src/kernel/ready.test.ts
CHANGED
|
@@ -38,7 +38,7 @@ describe("GET /_/ready", () => {
|
|
|
38
38
|
});
|
|
39
39
|
const bindings: Binding[] = [
|
|
40
40
|
{
|
|
41
|
-
trigger: http.post("/charge").
|
|
41
|
+
trigger: http.post("/charge").public(),
|
|
42
42
|
flow: charge as AnyFlowDef,
|
|
43
43
|
},
|
|
44
44
|
];
|
|
@@ -82,7 +82,7 @@ describe("GET /_oke/client.json", () => {
|
|
|
82
82
|
startScheduler: false,
|
|
83
83
|
env: "test",
|
|
84
84
|
gate: { unguardedHttp: "allow" },
|
|
85
|
-
bindings: [{ trigger: http.get("/ping").
|
|
85
|
+
bindings: [{ trigger: http.get("/ping").public(), flow: ping as AnyFlowDef }],
|
|
86
86
|
});
|
|
87
87
|
await app.boot();
|
|
88
88
|
const res = await app.fetch(new Request("http://127.0.0.1/_oke/client.json"));
|
|
@@ -63,6 +63,20 @@ describe("http.resource — gate / live", () => {
|
|
|
63
63
|
expect(remove?.isLive).toBe(false);
|
|
64
64
|
});
|
|
65
65
|
|
|
66
|
+
test(".public().live() stamps the sentinel on every verb", () => {
|
|
67
|
+
resetBindings();
|
|
68
|
+
resetFlowSeq();
|
|
69
|
+
on(http.resource("/notes", bag()).public().live());
|
|
70
|
+
expect(httpOf("/notes", "GET")?.gates.map((g) => (typeof g === "string" ? g : g.name))).toEqual(
|
|
71
|
+
["public"],
|
|
72
|
+
);
|
|
73
|
+
expect(
|
|
74
|
+
httpOf("/notes", "POST")?.gates.map((g) => (typeof g === "string" ? g : g.name)),
|
|
75
|
+
).toEqual(["public"]);
|
|
76
|
+
expect(httpOf("/notes", "GET")?.isLive).toBe(true);
|
|
77
|
+
expect(httpOf("/notes", "POST")?.isLive).toBe(false);
|
|
78
|
+
});
|
|
79
|
+
|
|
66
80
|
test(".live().gate(member) order does not matter", () => {
|
|
67
81
|
resetBindings();
|
|
68
82
|
resetFlowSeq();
|
|
@@ -23,7 +23,7 @@ describe("execute run duration", () => {
|
|
|
23
23
|
await runs.open();
|
|
24
24
|
|
|
25
25
|
on(
|
|
26
|
-
http.get("/ping").
|
|
26
|
+
http.get("/ping").public(),
|
|
27
27
|
flow("demo.ping", {
|
|
28
28
|
do: () => ({ ok: true }),
|
|
29
29
|
}),
|
|
@@ -68,7 +68,7 @@ describe("execute run duration", () => {
|
|
|
68
68
|
await runs.open();
|
|
69
69
|
try {
|
|
70
70
|
on(
|
|
71
|
-
http.get("/ping").
|
|
71
|
+
http.get("/ping").public(),
|
|
72
72
|
flow("demo.ping", {
|
|
73
73
|
do: () => ({ ok: true }),
|
|
74
74
|
}),
|
|
@@ -0,0 +1,79 @@
|
|
|
1
|
+
/**
|
|
2
|
+
* In-place path / name stamps and fail-loud HTTP boot.
|
|
3
|
+
*/
|
|
4
|
+
|
|
5
|
+
import { describe, expect, test, beforeEach } from "bun:test";
|
|
6
|
+
import type { RuntimeRouteMap } from "./adopt-routes.ts";
|
|
7
|
+
import { oke } from "./app.ts";
|
|
8
|
+
import { flow, resetFlowSeq } from "./flow.ts";
|
|
9
|
+
import { resetRegisteredFlowUnits, registerFlowUnits } from "./flow-units.ts";
|
|
10
|
+
import { listBindings, on, resetBindings } from "./on.ts";
|
|
11
|
+
import { stampFlowName, stampHttpPath } from "./stamp-http.ts";
|
|
12
|
+
import { HTTP_PATH_PENDING } from "./http-path-pending.ts";
|
|
13
|
+
import { http } from "./triggers.ts";
|
|
14
|
+
|
|
15
|
+
beforeEach(() => {
|
|
16
|
+
resetBindings();
|
|
17
|
+
resetFlowSeq();
|
|
18
|
+
resetRegisteredFlowUnits();
|
|
19
|
+
});
|
|
20
|
+
|
|
21
|
+
describe("stampHttpPath", () => {
|
|
22
|
+
test("mutates the on() binding in place — listBindings sees the stamped path", () => {
|
|
23
|
+
const f = on(http.get().public(), flow({ do: () => ({ ok: true }) }));
|
|
24
|
+
expect(f.$trigger && f.$trigger.kind === "http" && f.$trigger.path).toBe(HTTP_PATH_PENDING);
|
|
25
|
+
stampHttpPath(f, "/notes/:id");
|
|
26
|
+
expect(f.$trigger && f.$trigger.kind === "http" && f.$trigger.path).toBe("/notes/:id");
|
|
27
|
+
const bound = listBindings()[0];
|
|
28
|
+
expect(bound?.trigger.kind === "http" && bound.trigger.path).toBe("/notes/:id");
|
|
29
|
+
});
|
|
30
|
+
|
|
31
|
+
test("explicit http.get('/x') wins — stamp does not overwrite", () => {
|
|
32
|
+
const f = on(http.get("/me/tasks").public(), flow("notes.get", { do: () => 1 }));
|
|
33
|
+
stampHttpPath(f, "/notes/:id");
|
|
34
|
+
expect(f.$trigger && f.$trigger.kind === "http" && f.$trigger.path).toBe("/me/tasks");
|
|
35
|
+
});
|
|
36
|
+
});
|
|
37
|
+
|
|
38
|
+
describe("stampFlowName", () => {
|
|
39
|
+
test("fills a nameless flow as unit.export", () => {
|
|
40
|
+
const f = flow({ do: () => 1 });
|
|
41
|
+
expect(f.name).toBe("");
|
|
42
|
+
stampFlowName(f, "notes.get");
|
|
43
|
+
expect(f.name).toBe("notes.get");
|
|
44
|
+
expect(f.unit).toBe("notes");
|
|
45
|
+
});
|
|
46
|
+
|
|
47
|
+
test("explicit flow('notes.get') wins", () => {
|
|
48
|
+
const f = flow("notes.get", { do: () => 1 });
|
|
49
|
+
stampFlowName(f, "notes.list");
|
|
50
|
+
expect(f.name).toBe("notes.get");
|
|
51
|
+
});
|
|
52
|
+
});
|
|
53
|
+
|
|
54
|
+
describe("oke — unresolved sentinel and duplicate routes", () => {
|
|
55
|
+
test("unresolved http.get() fails construction", () => {
|
|
56
|
+
on(http.get().public(), flow("notes.get", { do: () => 1 }));
|
|
57
|
+
expect(() => oke({ name: "t", autoBoot: false })).toThrow(/OKE1010/);
|
|
58
|
+
});
|
|
59
|
+
|
|
60
|
+
test("duplicate GET /notes fails construction", () => {
|
|
61
|
+
on(http.get("/notes").public(), flow("notes.list", { do: () => 1 }));
|
|
62
|
+
on(http.get("/notes").public(), flow("notes.also", { do: () => 1 }));
|
|
63
|
+
expect(() => oke({ name: "t", autoBoot: false })).toThrow(/OKE1011/);
|
|
64
|
+
});
|
|
65
|
+
|
|
66
|
+
test("nameless HTTP flow fails construction", () => {
|
|
67
|
+
on(http.get("/notes").public(), flow({ do: () => 1 }));
|
|
68
|
+
expect(() => oke({ name: "t", autoBoot: false })).toThrow(/OKE1012/);
|
|
69
|
+
});
|
|
70
|
+
|
|
71
|
+
test("registerFlowUnits drains into $routes; registry ignore does not", () => {
|
|
72
|
+
const get = on(http.get("/notes/:id").public(), flow("notes.get", { do: () => 1 }));
|
|
73
|
+
registerFlowUnits({ notes: { get } });
|
|
74
|
+
const ignored = oke({ name: "t", autoBoot: false, registry: "ignore" });
|
|
75
|
+
expect(ignored.$routes).toEqual({});
|
|
76
|
+
const app = oke({ name: "t", autoBoot: false });
|
|
77
|
+
expect((app.$routes as RuntimeRouteMap).notes?.get?.path).toBe("/notes/:id");
|
|
78
|
+
});
|
|
79
|
+
});
|
|
@@ -0,0 +1,75 @@
|
|
|
1
|
+
/**
|
|
2
|
+
* In-place HTTP path + Flow name stamps.
|
|
3
|
+
*
|
|
4
|
+
* `on()` already registered the trigger (possibly still on the pending-path
|
|
5
|
+
* sentinel). A wrapper that returned a new trigger would leave `listBindings()`
|
|
6
|
+
* on the sentinel — mutate the existing object.
|
|
7
|
+
*/
|
|
8
|
+
|
|
9
|
+
import { isFlow, type AnyFlowDef, type FlowDef } from "./flow.ts";
|
|
10
|
+
import { isPendingHttpPath, type HttpPathPending } from "./http-path-pending.ts";
|
|
11
|
+
import type { HttpTrigger, Trigger } from "./triggers.ts";
|
|
12
|
+
|
|
13
|
+
/**
|
|
14
|
+
* Replace a pending HTTP path on {@link FlowDef} with the file-tree stamp.
|
|
15
|
+
*
|
|
16
|
+
* Explicit `http.get("/x")` wins — the pending sentinel is the only path
|
|
17
|
+
* this function overwrites. Returns the same object with a tighter trigger
|
|
18
|
+
* type when the original path was pending.
|
|
19
|
+
*
|
|
20
|
+
* @param flow - Bound flow (already passed through `on()`)
|
|
21
|
+
* @param path - Inferred URL (`/notes/:id`)
|
|
22
|
+
*/
|
|
23
|
+
export function stampHttpPath<F extends AnyFlowDef, P extends string>(
|
|
24
|
+
flow: F,
|
|
25
|
+
path: P,
|
|
26
|
+
): StampHttpPath<F, P> {
|
|
27
|
+
if (!isFlow(flow)) return flow as StampHttpPath<F, P>;
|
|
28
|
+
const apply = (trigger: Trigger): void => {
|
|
29
|
+
if (trigger.kind !== "http") return;
|
|
30
|
+
if (!isPendingHttpPath(trigger.path)) return;
|
|
31
|
+
(trigger as { path: string }).path = path;
|
|
32
|
+
};
|
|
33
|
+
if (flow.$trigger) apply(flow.$trigger);
|
|
34
|
+
for (const trigger of flow.triggers) apply(trigger);
|
|
35
|
+
return flow as StampHttpPath<F, P>;
|
|
36
|
+
}
|
|
37
|
+
|
|
38
|
+
/**
|
|
39
|
+
* Stamp `unit.export` onto a nameless (or `flow_*`) Flow.
|
|
40
|
+
*
|
|
41
|
+
* Explicit `flow("notes.get")` wins. Fills `unit` when missing.
|
|
42
|
+
*
|
|
43
|
+
* @param flow - Flow definition
|
|
44
|
+
* @param name - `notes.get`
|
|
45
|
+
*/
|
|
46
|
+
export function stampFlowName<F extends AnyFlowDef>(flow: F, name: string): F {
|
|
47
|
+
if (!isFlow(flow)) return flow;
|
|
48
|
+
const f = flow as { name: string; unit: string | undefined };
|
|
49
|
+
if (!f.name || f.name.startsWith("flow_")) {
|
|
50
|
+
f.name = name;
|
|
51
|
+
}
|
|
52
|
+
if (!f.unit) {
|
|
53
|
+
const resolved = f.name || name;
|
|
54
|
+
const dot = resolved.indexOf(".");
|
|
55
|
+
f.unit = dot > 0 ? resolved.slice(0, dot) : resolved;
|
|
56
|
+
}
|
|
57
|
+
return flow;
|
|
58
|
+
}
|
|
59
|
+
|
|
60
|
+
/**
|
|
61
|
+
* Type-level path replacement — only when the bound trigger is still pending.
|
|
62
|
+
*
|
|
63
|
+
* @typeParam F - Flow definition
|
|
64
|
+
* @typeParam P - Stamped path
|
|
65
|
+
*/
|
|
66
|
+
export type StampHttpPath<F, P extends string> =
|
|
67
|
+
F extends FlowDef<infer I, infer O, infer E, infer D, infer T>
|
|
68
|
+
? T extends HttpTrigger<infer M, infer Cur>
|
|
69
|
+
? [Cur] extends [HttpPathPending]
|
|
70
|
+
? FlowDef<I, O, E, D, HttpTrigger<M, P>>
|
|
71
|
+
: F
|
|
72
|
+
: F
|
|
73
|
+
: F;
|
|
74
|
+
|
|
75
|
+
export { HTTP_PATH_PENDING, isPendingHttpPath } from "./http-path-pending.ts";
|