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
package/src/compiler/extract.ts
CHANGED
|
@@ -11,6 +11,7 @@ import { parseSync } from "oxc-parser";
|
|
|
11
11
|
import type {
|
|
12
12
|
Ai,
|
|
13
13
|
AiAgent,
|
|
14
|
+
AiMcpServer,
|
|
14
15
|
AiModel,
|
|
15
16
|
AiPrompt,
|
|
16
17
|
Channel,
|
|
@@ -21,6 +22,7 @@ import type {
|
|
|
21
22
|
Flow,
|
|
22
23
|
Gate,
|
|
23
24
|
Journey,
|
|
25
|
+
JsonSchema,
|
|
24
26
|
Manifest,
|
|
25
27
|
RateStrategy,
|
|
26
28
|
ResourceRef,
|
|
@@ -29,12 +31,14 @@ import type {
|
|
|
29
31
|
SignalDelivery,
|
|
30
32
|
Slo,
|
|
31
33
|
Store,
|
|
34
|
+
TablePolicy,
|
|
32
35
|
Trigger,
|
|
33
36
|
} from "../manifest/types.ts";
|
|
34
37
|
import { sqlTableRef } from "../manifest/sql-resource.ts";
|
|
35
38
|
import {
|
|
36
39
|
identifierName,
|
|
37
40
|
inferEffects,
|
|
41
|
+
resolveCallTarget,
|
|
38
42
|
stringArg,
|
|
39
43
|
walk,
|
|
40
44
|
type AstNode,
|
|
@@ -43,6 +47,12 @@ import {
|
|
|
43
47
|
type InferBinding,
|
|
44
48
|
type Literal,
|
|
45
49
|
} from "./effects-infer.ts";
|
|
50
|
+
import { nameFromFlowFile, pathFromFlowFile } from "./flow-path.ts";
|
|
51
|
+
import {
|
|
52
|
+
defaultListInSchema,
|
|
53
|
+
jsonSchemaFromAst,
|
|
54
|
+
type SchemaExpandContext,
|
|
55
|
+
} from "./schema-from-ast.ts";
|
|
46
56
|
|
|
47
57
|
/** One source file for extraction. */
|
|
48
58
|
export interface SourceFile {
|
|
@@ -65,16 +75,24 @@ export interface ExtractManifestOptions {
|
|
|
65
75
|
}
|
|
66
76
|
|
|
67
77
|
/** Internal project scope accumulated across files. */
|
|
78
|
+
type DeclaredSchemaTable = {
|
|
79
|
+
readonly name: string;
|
|
80
|
+
readonly columns: Record<string, DeclaredColumn>;
|
|
81
|
+
readonly rls?: boolean;
|
|
82
|
+
readonly policies?: Record<string, TablePolicy>;
|
|
83
|
+
};
|
|
84
|
+
|
|
68
85
|
interface ProjectScope {
|
|
69
86
|
app: string;
|
|
70
87
|
bindings: Map<string, InferBinding>;
|
|
71
88
|
signals: Record<string, Signal>;
|
|
72
89
|
stores: Record<string, Store>;
|
|
73
90
|
/** Binding or table name → declared columns from `store.schema.table`. */
|
|
74
|
-
schemaTables: Map<
|
|
75
|
-
|
|
76
|
-
|
|
77
|
-
|
|
91
|
+
schemaTables: Map<string, DeclaredSchemaTable>;
|
|
92
|
+
/** `const Name = <init>` per file — used to expand flow `in` / `out`. */
|
|
93
|
+
constInitsByFile: Map<string, Map<string, AstNode>>;
|
|
94
|
+
/** Unique project-wide const names (ambiguous names omitted). */
|
|
95
|
+
uniqueConstInits: Map<string, { node: AstNode; file: string }>;
|
|
78
96
|
clocks: Record<string, Clock>;
|
|
79
97
|
gates: Record<string, Gate>;
|
|
80
98
|
/** Local binding name → gate manifest id (policy name or rate expression). */
|
|
@@ -117,6 +135,8 @@ export async function extractManifest(options: ExtractManifestOptions = {}): Pro
|
|
|
117
135
|
signals: {},
|
|
118
136
|
stores: {},
|
|
119
137
|
schemaTables: new Map(),
|
|
138
|
+
constInitsByFile: new Map(),
|
|
139
|
+
uniqueConstInits: new Map(),
|
|
120
140
|
clocks: {},
|
|
121
141
|
gates: {},
|
|
122
142
|
gateIds: new Map(),
|
|
@@ -142,8 +162,8 @@ export async function extractManifest(options: ExtractManifestOptions = {}): Pro
|
|
|
142
162
|
});
|
|
143
163
|
|
|
144
164
|
// Pass 1 — declarations (elements, config, named bindings).
|
|
145
|
-
for (const { program } of parsed) {
|
|
146
|
-
collectDeclarations(program, scope);
|
|
165
|
+
for (const { file, program } of parsed) {
|
|
166
|
+
collectDeclarations(file.path, program, scope);
|
|
147
167
|
}
|
|
148
168
|
|
|
149
169
|
// Pass 2 — flows / on() bindings.
|
|
@@ -177,11 +197,12 @@ export async function extractManifest(options: ExtractManifestOptions = {}): Pro
|
|
|
177
197
|
if (Object.keys(gates).length > 0) manifest.gates = gates;
|
|
178
198
|
if (Object.keys(vault).length > 0) manifest.vault = vault;
|
|
179
199
|
if (Object.keys(channels).length > 0) manifest.channels = channels;
|
|
180
|
-
if (scope.ai.models || scope.ai.prompts || scope.ai.agents) {
|
|
200
|
+
if (scope.ai.models || scope.ai.prompts || scope.ai.agents || scope.ai.mcpServers) {
|
|
181
201
|
manifest.ai = {
|
|
182
202
|
...(scope.ai.models ? { models: sortRecord(scope.ai.models) } : {}),
|
|
183
203
|
...(scope.ai.prompts ? { prompts: sortRecord(scope.ai.prompts) } : {}),
|
|
184
204
|
...(scope.ai.agents ? { agents: sortRecord(scope.ai.agents) } : {}),
|
|
205
|
+
...(scope.ai.mcpServers ? { mcpServers: sortRecord(scope.ai.mcpServers) } : {}),
|
|
185
206
|
};
|
|
186
207
|
}
|
|
187
208
|
if (Object.keys(journeys).length > 0) manifest.journeys = journeys;
|
|
@@ -227,13 +248,13 @@ async function readSources(rootDir: string, pattern: string): Promise<SourceFile
|
|
|
227
248
|
return files;
|
|
228
249
|
}
|
|
229
250
|
|
|
230
|
-
function collectDeclarations(program: AstNode, scope: ProjectScope): void {
|
|
251
|
+
function collectDeclarations(filePath: string, program: AstNode, scope: ProjectScope): void {
|
|
231
252
|
walk(program, (node) => {
|
|
232
253
|
if (node.type === "CallExpression") {
|
|
233
254
|
visitDeclarationCall(node as CallExpression, program, scope);
|
|
234
255
|
}
|
|
235
256
|
if (node.type === "VariableDeclarator") {
|
|
236
|
-
visitDeclarator(node as AstNode, scope);
|
|
257
|
+
visitDeclarator(node as AstNode, filePath, scope);
|
|
237
258
|
}
|
|
238
259
|
});
|
|
239
260
|
|
|
@@ -248,12 +269,13 @@ function collectDeclarations(program: AstNode, scope: ProjectScope): void {
|
|
|
248
269
|
});
|
|
249
270
|
}
|
|
250
271
|
|
|
251
|
-
function visitDeclarator(decl: AstNode, scope: ProjectScope): void {
|
|
272
|
+
function visitDeclarator(decl: AstNode, filePath: string, scope: ProjectScope): void {
|
|
252
273
|
const id = (decl as AstNode & { id?: AstNode }).id;
|
|
253
274
|
const init = (decl as AstNode & { init?: AstNode }).init;
|
|
254
275
|
if (!id || !init) return;
|
|
255
276
|
const name = identifierName(id);
|
|
256
277
|
if (!name) return;
|
|
278
|
+
recordConstInit(scope, filePath, name, unwrapTsExpr(init));
|
|
257
279
|
|
|
258
280
|
// export const x = on(...) / flow(...) — flow export alias
|
|
259
281
|
if (init.type === "CallExpression") {
|
|
@@ -301,6 +323,13 @@ function finalizeRefs(scope: ProjectScope): void {
|
|
|
301
323
|
);
|
|
302
324
|
}
|
|
303
325
|
}
|
|
326
|
+
if (scope.ai.mcpServers) {
|
|
327
|
+
for (const server of Object.values(scope.ai.mcpServers)) {
|
|
328
|
+
if (!server.auth) continue;
|
|
329
|
+
const binding = scope.bindings.get(server.auth);
|
|
330
|
+
if (binding?.kind === "secret") server.auth = binding.ref;
|
|
331
|
+
}
|
|
332
|
+
}
|
|
304
333
|
for (const journey of Object.values(scope.journeys)) {
|
|
305
334
|
if (!journey.flows) continue;
|
|
306
335
|
journey.flows = journey.flows.map(
|
|
@@ -316,7 +345,7 @@ function finalizeRefs(scope: ProjectScope): void {
|
|
|
316
345
|
if (!declared) continue;
|
|
317
346
|
const existing = table.columns ?? {};
|
|
318
347
|
if (Object.keys(existing).length === 0) {
|
|
319
|
-
store.tables[name] = { ...table,
|
|
348
|
+
store.tables[name] = { ...table, ...tableFromDeclared(declared) };
|
|
320
349
|
}
|
|
321
350
|
}
|
|
322
351
|
}
|
|
@@ -328,7 +357,7 @@ function finalizeRefs(scope: ProjectScope): void {
|
|
|
328
357
|
if (!store.tables) continue;
|
|
329
358
|
for (const name of Object.keys(store.tables)) attachedNames.add(name);
|
|
330
359
|
}
|
|
331
|
-
const orphanByName = new Map<string,
|
|
360
|
+
const orphanByName = new Map<string, DeclaredSchemaTable>();
|
|
332
361
|
for (const t of scope.schemaTables.values()) {
|
|
333
362
|
if (!attachedNames.has(t.name)) orphanByName.set(t.name, t);
|
|
334
363
|
}
|
|
@@ -341,7 +370,7 @@ function finalizeRefs(scope: ProjectScope): void {
|
|
|
341
370
|
target.tables = target.tables ?? {};
|
|
342
371
|
for (const t of orphanByName.values()) {
|
|
343
372
|
if (target.tables[t.name]) continue;
|
|
344
|
-
target.tables[t.name] =
|
|
373
|
+
target.tables[t.name] = tableFromDeclared(t);
|
|
345
374
|
}
|
|
346
375
|
}
|
|
347
376
|
}
|
|
@@ -351,7 +380,8 @@ function collectSchemaTable(call: CallExpression, program: AstNode, scope: Proje
|
|
|
351
380
|
if (!tableName) return;
|
|
352
381
|
const colsNode = objectArg(call.arguments[1]);
|
|
353
382
|
const columns = colsNode ? parseDeclaredColumns(colsNode) : {};
|
|
354
|
-
const
|
|
383
|
+
const extras = parseSchemaTableExtras(call.arguments[2]);
|
|
384
|
+
const entry = { name: tableName, columns, ...extras };
|
|
355
385
|
scope.schemaTables.set(tableName, entry);
|
|
356
386
|
const bindingName = enclosingConstName(call, program);
|
|
357
387
|
if (bindingName) {
|
|
@@ -386,7 +416,7 @@ function attachSchemaOption(
|
|
|
386
416
|
const id = (value as Identifier).name;
|
|
387
417
|
const declared = scope.schemaTables.get(id) ?? scope.schemaTables.get(key);
|
|
388
418
|
if (declared) {
|
|
389
|
-
store.tables[declared.name] =
|
|
419
|
+
store.tables[declared.name] = tableFromDeclared(declared);
|
|
390
420
|
} else {
|
|
391
421
|
store.tables[key] = store.tables[key] ?? {};
|
|
392
422
|
}
|
|
@@ -396,7 +426,7 @@ function attachSchemaOption(
|
|
|
396
426
|
if (value.type === "CallExpression") {
|
|
397
427
|
const inline = parseInlineSchemaTable(value as CallExpression);
|
|
398
428
|
if (inline) {
|
|
399
|
-
store.tables[inline.name] =
|
|
429
|
+
store.tables[inline.name] = tableFromDeclared(inline);
|
|
400
430
|
scope.schemaTables.set(inline.name, inline);
|
|
401
431
|
}
|
|
402
432
|
}
|
|
@@ -410,9 +440,7 @@ function attachSchemaOption(
|
|
|
410
440
|
}
|
|
411
441
|
}
|
|
412
442
|
|
|
413
|
-
function parseInlineSchemaTable(
|
|
414
|
-
call: CallExpression,
|
|
415
|
-
): { name: string; columns: Record<string, DeclaredColumn> } | undefined {
|
|
443
|
+
function parseInlineSchemaTable(call: CallExpression): DeclaredSchemaTable | undefined {
|
|
416
444
|
const callee = call.callee;
|
|
417
445
|
if (callee.type !== "MemberExpression") return undefined;
|
|
418
446
|
const member = callee as AstNode & { object: AstNode; property: AstNode };
|
|
@@ -428,6 +456,121 @@ function parseInlineSchemaTable(
|
|
|
428
456
|
return {
|
|
429
457
|
name: tableName,
|
|
430
458
|
columns: colsNode ? parseDeclaredColumns(colsNode) : {},
|
|
459
|
+
...parseSchemaTableExtras(call.arguments[2]),
|
|
460
|
+
};
|
|
461
|
+
}
|
|
462
|
+
|
|
463
|
+
function tableFromDeclared(t: DeclaredSchemaTable): {
|
|
464
|
+
columns: Record<string, DeclaredColumn>;
|
|
465
|
+
rls?: boolean;
|
|
466
|
+
policies?: Record<string, TablePolicy>;
|
|
467
|
+
} {
|
|
468
|
+
return {
|
|
469
|
+
columns: t.columns,
|
|
470
|
+
...(t.rls ? { rls: true } : {}),
|
|
471
|
+
...(t.policies ? { policies: t.policies } : {}),
|
|
472
|
+
};
|
|
473
|
+
}
|
|
474
|
+
|
|
475
|
+
function parseSchemaTableExtras(
|
|
476
|
+
node: AstNode | undefined,
|
|
477
|
+
): Pick<DeclaredSchemaTable, "rls" | "policies"> {
|
|
478
|
+
if (!node || node.type !== "ArrayExpression") return {};
|
|
479
|
+
const els = (node as AstNode & { elements?: AstNode[] }).elements ?? [];
|
|
480
|
+
const policies: Record<string, TablePolicy> = {};
|
|
481
|
+
let rls = false;
|
|
482
|
+
for (const el of els) {
|
|
483
|
+
if (!el || el.type !== "CallExpression") continue;
|
|
484
|
+
const extra = parseSchemaTableExtra(el as CallExpression);
|
|
485
|
+
if (!extra) continue;
|
|
486
|
+
if (extra.kind === "rls") {
|
|
487
|
+
rls = true;
|
|
488
|
+
continue;
|
|
489
|
+
}
|
|
490
|
+
policies[extra.name] = extra.policy;
|
|
491
|
+
rls = true;
|
|
492
|
+
}
|
|
493
|
+
return {
|
|
494
|
+
...(rls ? { rls: true } : {}),
|
|
495
|
+
...(Object.keys(policies).length > 0 ? { policies } : {}),
|
|
496
|
+
};
|
|
497
|
+
}
|
|
498
|
+
|
|
499
|
+
function parseSchemaTableExtra(
|
|
500
|
+
call: CallExpression,
|
|
501
|
+
): { kind: "rls" } | { kind: "policy"; name: string; policy: TablePolicy } | undefined {
|
|
502
|
+
const callee = call.callee;
|
|
503
|
+
if (callee.type !== "MemberExpression") return undefined;
|
|
504
|
+
const member = callee as AstNode & { object: AstNode; property: AstNode };
|
|
505
|
+
const method = identifierName(member.property);
|
|
506
|
+
if (method === "rls" && isStoreSchemaCallee(member.object)) {
|
|
507
|
+
return { kind: "rls" };
|
|
508
|
+
}
|
|
509
|
+
if (method === "policy" && isStoreSchemaCallee(member.object)) {
|
|
510
|
+
const name = stringArg(call.arguments[0]);
|
|
511
|
+
if (!name) return undefined;
|
|
512
|
+
return { kind: "policy", name, policy: parseTablePolicyOptions(objectArg(call.arguments[1])) };
|
|
513
|
+
}
|
|
514
|
+
if (
|
|
515
|
+
(method === "gate" || method === "owner" || method === "scope") &&
|
|
516
|
+
isStoreSchemaPolicyCallee(member.object)
|
|
517
|
+
) {
|
|
518
|
+
const key = stringArg(call.arguments[0]);
|
|
519
|
+
if (!key) return undefined;
|
|
520
|
+
const opts = objectArg(call.arguments[1]);
|
|
521
|
+
const command = (stringProp(opts, "for") ??
|
|
522
|
+
(method === "owner"
|
|
523
|
+
? "all"
|
|
524
|
+
: method === "scope"
|
|
525
|
+
? "insert"
|
|
526
|
+
: "select")) as TablePolicy["for"];
|
|
527
|
+
const slug = key.replace(/[^A-Za-z0-9]+/g, "_").replace(/^_|_$/g, "");
|
|
528
|
+
const name = `${method}_${slug}_${command}`;
|
|
529
|
+
const lit = `'${key.replaceAll("'", "''")}'`;
|
|
530
|
+
const expr =
|
|
531
|
+
method === "gate"
|
|
532
|
+
? `oke.gate() = ${lit}`
|
|
533
|
+
: method === "owner"
|
|
534
|
+
? `${key} = oke.user()`
|
|
535
|
+
: `oke.has_scope(${lit})`;
|
|
536
|
+
const policy: TablePolicy = {
|
|
537
|
+
for: command,
|
|
538
|
+
...(command === "insert"
|
|
539
|
+
? { withCheck: expr }
|
|
540
|
+
: command === "update" || command === "all"
|
|
541
|
+
? { using: expr, withCheck: expr }
|
|
542
|
+
: { using: expr }),
|
|
543
|
+
};
|
|
544
|
+
return { kind: "policy", name, policy };
|
|
545
|
+
}
|
|
546
|
+
return undefined;
|
|
547
|
+
}
|
|
548
|
+
|
|
549
|
+
function isStoreSchemaCallee(node: AstNode): boolean {
|
|
550
|
+
if (node.type !== "MemberExpression") return false;
|
|
551
|
+
const member = node as AstNode & { object: AstNode; property: AstNode };
|
|
552
|
+
return identifierName(member.object) === "store" && identifierName(member.property) === "schema";
|
|
553
|
+
}
|
|
554
|
+
|
|
555
|
+
function isStoreSchemaPolicyCallee(node: AstNode): boolean {
|
|
556
|
+
if (node.type !== "MemberExpression") return false;
|
|
557
|
+
const member = node as AstNode & { object: AstNode; property: AstNode };
|
|
558
|
+
return identifierName(member.property) === "policy" && isStoreSchemaCallee(member.object);
|
|
559
|
+
}
|
|
560
|
+
|
|
561
|
+
function parseTablePolicyOptions(opts: AstNode | undefined): TablePolicy {
|
|
562
|
+
const as = stringProp(opts, "as") as TablePolicy["as"];
|
|
563
|
+
const forCmd = stringProp(opts, "for") as TablePolicy["for"];
|
|
564
|
+
const using = stringProp(opts, "using");
|
|
565
|
+
const withCheck = stringProp(opts, "withCheck");
|
|
566
|
+
const toNode = objectProp(opts, "to");
|
|
567
|
+
const to = stringArg(toNode) ?? stringArrayProp(opts, "to");
|
|
568
|
+
return {
|
|
569
|
+
...(as ? { as } : {}),
|
|
570
|
+
...(to ? { to } : {}),
|
|
571
|
+
...(forCmd ? { for: forCmd } : {}),
|
|
572
|
+
...(using ? { using } : {}),
|
|
573
|
+
...(withCheck ? { withCheck } : {}),
|
|
431
574
|
};
|
|
432
575
|
}
|
|
433
576
|
|
|
@@ -472,6 +615,10 @@ function parseFieldChain(node: AstNode, key: string): DeclaredColumn | undefined
|
|
|
472
615
|
if (method === "describe") {
|
|
473
616
|
description = stringArg(call.arguments[0]) ?? description;
|
|
474
617
|
}
|
|
618
|
+
if (method === "defaultFn") {
|
|
619
|
+
hasDefault = true;
|
|
620
|
+
defaultValue = defaultValue ?? null;
|
|
621
|
+
}
|
|
475
622
|
if (method === "default") {
|
|
476
623
|
const lit = call.arguments[0];
|
|
477
624
|
if (lit && lit.type === "Literal") {
|
|
@@ -816,6 +963,7 @@ function visitDeclarationCall(call: CallExpression, program: AstNode, scope: Pro
|
|
|
816
963
|
const model: AiModel = {
|
|
817
964
|
...(stringProp(opts, "provider") ? { provider: stringProp(opts, "provider") } : {}),
|
|
818
965
|
...(stringProp(opts, "tier") ? { tier: stringProp(opts, "tier") } : {}),
|
|
966
|
+
...(stringProp(opts, "driverId") ? { driverId: stringProp(opts, "driverId") } : {}),
|
|
819
967
|
};
|
|
820
968
|
scope.ai.models = scope.ai.models ?? {};
|
|
821
969
|
scope.ai.models[modelName] = model;
|
|
@@ -833,6 +981,24 @@ function visitDeclarationCall(call: CallExpression, program: AstNode, scope: Pro
|
|
|
833
981
|
collectAgent(call, scope);
|
|
834
982
|
}
|
|
835
983
|
|
|
984
|
+
if (obj === "ai" && prop === "mcpServer") {
|
|
985
|
+
collectMcpServer(call, program, scope);
|
|
986
|
+
}
|
|
987
|
+
|
|
988
|
+
if (prop === "tool" && obj) {
|
|
989
|
+
const toolName = stringArg(call.arguments[0]);
|
|
990
|
+
const server = scope.bindings.get(obj);
|
|
991
|
+
if (server?.kind === "mcp-server" && toolName) {
|
|
992
|
+
const bindingName = enclosingConstName(call, program);
|
|
993
|
+
if (bindingName) {
|
|
994
|
+
scope.bindings.set(bindingName, {
|
|
995
|
+
kind: "mcp-tool",
|
|
996
|
+
ref: `mcp:${server.ref}/${toolName}`,
|
|
997
|
+
});
|
|
998
|
+
}
|
|
999
|
+
}
|
|
1000
|
+
}
|
|
1001
|
+
|
|
836
1002
|
if (obj === "ai" && prop === "embed") {
|
|
837
1003
|
const embedName = stringArg(call.arguments[0]);
|
|
838
1004
|
if (embedName) {
|
|
@@ -1026,7 +1192,11 @@ function collectAgent(call: CallExpression, scope: ProjectScope): void {
|
|
|
1026
1192
|
if (!agentName || !opts) return;
|
|
1027
1193
|
const toolsArr = arrayProp(opts, "tools");
|
|
1028
1194
|
const tools = toolsArr
|
|
1029
|
-
?.map((el) =>
|
|
1195
|
+
?.map((el) => {
|
|
1196
|
+
const resolved = resolveCallTarget(el, scope.bindings);
|
|
1197
|
+
if (resolved) return resolved;
|
|
1198
|
+
return identifierName(el) ?? stringArg(el);
|
|
1199
|
+
})
|
|
1030
1200
|
.filter((x): x is string => typeof x === "string")
|
|
1031
1201
|
.map((id) => scope.flowExports.get(id) ?? scope.bindings.get(id)?.ref ?? id);
|
|
1032
1202
|
const agent: AiAgent = {
|
|
@@ -1039,6 +1209,40 @@ function collectAgent(call: CallExpression, scope: ProjectScope): void {
|
|
|
1039
1209
|
scope.ai.agents[agentName] = agent;
|
|
1040
1210
|
}
|
|
1041
1211
|
|
|
1212
|
+
function collectMcpServer(call: CallExpression, program: AstNode, scope: ProjectScope): void {
|
|
1213
|
+
const serverName = stringArg(call.arguments[0]);
|
|
1214
|
+
const opts = objectArg(call.arguments[1]);
|
|
1215
|
+
if (!serverName || !opts) return;
|
|
1216
|
+
const tools = stringArrayProp(opts, "tools") ?? [];
|
|
1217
|
+
const url = stringProp(opts, "url");
|
|
1218
|
+
const command = stringProp(opts, "command");
|
|
1219
|
+
const args = stringArrayProp(opts, "args");
|
|
1220
|
+
const authObj = objectProp(opts, "auth");
|
|
1221
|
+
const bearerNode = authObj ? objectProp(authObj, "bearer") : undefined;
|
|
1222
|
+
const bearerLit = stringArg(bearerNode);
|
|
1223
|
+
const bearerId = identifierName(bearerNode);
|
|
1224
|
+
const auth =
|
|
1225
|
+
bearerLit ??
|
|
1226
|
+
(bearerId
|
|
1227
|
+
? scope.bindings.get(bearerId)?.kind === "secret"
|
|
1228
|
+
? scope.bindings.get(bearerId)?.ref
|
|
1229
|
+
: bearerId
|
|
1230
|
+
: undefined);
|
|
1231
|
+
const server: AiMcpServer = {
|
|
1232
|
+
tools,
|
|
1233
|
+
...(url ? { url } : {}),
|
|
1234
|
+
...(command ? { command } : {}),
|
|
1235
|
+
...(args && args.length > 0 ? { args } : {}),
|
|
1236
|
+
...(auth ? { auth } : {}),
|
|
1237
|
+
};
|
|
1238
|
+
scope.ai.mcpServers = scope.ai.mcpServers ?? {};
|
|
1239
|
+
scope.ai.mcpServers[serverName] = server;
|
|
1240
|
+
const bindingName = enclosingConstName(call, program);
|
|
1241
|
+
if (bindingName) {
|
|
1242
|
+
scope.bindings.set(bindingName, { kind: "mcp-server", ref: serverName });
|
|
1243
|
+
}
|
|
1244
|
+
}
|
|
1245
|
+
|
|
1042
1246
|
function collectConfig(opts: AstNode | undefined, scope: ProjectScope): void {
|
|
1043
1247
|
if (!opts) return;
|
|
1044
1248
|
|
|
@@ -1233,7 +1437,7 @@ function registerFlow(args: {
|
|
|
1233
1437
|
scope: ProjectScope;
|
|
1234
1438
|
exportName: string | undefined;
|
|
1235
1439
|
}): void {
|
|
1236
|
-
const opts =
|
|
1440
|
+
const opts = flowOptionsArg(args.flowCall);
|
|
1237
1441
|
if (!opts) return;
|
|
1238
1442
|
|
|
1239
1443
|
// `flow(\`${unit}.list\`)` inside a helper — name is only known at the
|
|
@@ -1242,6 +1446,7 @@ function registerFlow(args: {
|
|
|
1242
1446
|
|
|
1243
1447
|
const name =
|
|
1244
1448
|
stringArg(args.flowCall.arguments[0]) ??
|
|
1449
|
+
nameFromFlowFile(args.file.path, args.exportName) ??
|
|
1245
1450
|
args.exportName ??
|
|
1246
1451
|
`flow_${Object.keys(args.scope.flows).length + 1}`;
|
|
1247
1452
|
|
|
@@ -1289,7 +1494,9 @@ function registerFlow(args: {
|
|
|
1289
1494
|
}
|
|
1290
1495
|
}
|
|
1291
1496
|
|
|
1292
|
-
const trigger = args.triggerNode
|
|
1497
|
+
const trigger = args.triggerNode
|
|
1498
|
+
? parseTrigger(args.triggerNode, args.scope, args.file.path)
|
|
1499
|
+
: undefined;
|
|
1293
1500
|
|
|
1294
1501
|
const gates = trigger?.gates;
|
|
1295
1502
|
const liveFromTrigger = trigger?.live;
|
|
@@ -1302,9 +1509,9 @@ function registerFlow(args: {
|
|
|
1302
1509
|
}
|
|
1303
1510
|
if (gates && gates.length > 0) flow.gates = gates;
|
|
1304
1511
|
|
|
1305
|
-
const inSchema = schemaProp(opts, "in");
|
|
1512
|
+
const inSchema = schemaProp(opts, "in", args.scope, args.file.path);
|
|
1306
1513
|
if (inSchema !== undefined) flow.in = inSchema;
|
|
1307
|
-
const outSchema = schemaProp(opts, "out");
|
|
1514
|
+
const outSchema = schemaProp(opts, "out", args.scope, args.file.path);
|
|
1308
1515
|
if (outSchema !== undefined) flow.out = outSchema;
|
|
1309
1516
|
|
|
1310
1517
|
const errors = parseErrors(objectProp(opts, "errors"));
|
|
@@ -1393,8 +1600,8 @@ function registerFlow(args: {
|
|
|
1393
1600
|
}
|
|
1394
1601
|
|
|
1395
1602
|
/**
|
|
1396
|
-
* `http.resource(path, bag).gate(...).live()` — find the resource call
|
|
1397
|
-
* collect the same chain {@link parseHttpTrigger} reads on a single verb.
|
|
1603
|
+
* `http.resource(path, bag).gate(...).public().live()` — find the resource call
|
|
1604
|
+
* and collect the same chain {@link parseHttpTrigger} reads on a single verb.
|
|
1398
1605
|
*
|
|
1399
1606
|
* @param node - First argument to `on(…)`
|
|
1400
1607
|
* @param scope - Project scope (gate name expansion)
|
|
@@ -1425,19 +1632,8 @@ function unwrapResourceMount(
|
|
|
1425
1632
|
}
|
|
1426
1633
|
continue;
|
|
1427
1634
|
}
|
|
1635
|
+
if (prop === "public") gateNames.push("public");
|
|
1428
1636
|
if (prop === "live") live = true;
|
|
1429
|
-
continue;
|
|
1430
|
-
}
|
|
1431
|
-
if (item.type === "MemberExpression") {
|
|
1432
|
-
const member = item as AstNode & { object: AstNode; property: AstNode };
|
|
1433
|
-
if (identifierName(member.property) !== "public") continue;
|
|
1434
|
-
const obj = member.object;
|
|
1435
|
-
if (
|
|
1436
|
-
obj.type === "MemberExpression" &&
|
|
1437
|
-
identifierName((obj as AstNode & { property: AstNode }).property) === "gate"
|
|
1438
|
-
) {
|
|
1439
|
-
gateNames.push("public");
|
|
1440
|
-
}
|
|
1441
1637
|
}
|
|
1442
1638
|
}
|
|
1443
1639
|
if (!httpCall) return undefined;
|
|
@@ -1558,32 +1754,86 @@ function registerNamedTableCrud(call: CallExpression, file: SourceFile, scope: P
|
|
|
1558
1754
|
const idPath = `${path}/:id`;
|
|
1559
1755
|
const line = lineAt(file.source, call.start ?? 0);
|
|
1560
1756
|
const live = boolProp(opts, "liveList") === true;
|
|
1757
|
+
const skipCreate = boolProp(opts, "skipCreate") === true;
|
|
1758
|
+
const ctx = schemaExpandContext(scope, file.path);
|
|
1759
|
+
const createIn = jsonSchemaFromAst(objectProp(opts, "createIn"), ctx);
|
|
1760
|
+
const itemOut = jsonSchemaFromAst(objectProp(opts, "out"), ctx);
|
|
1761
|
+
const updateIn = jsonSchemaFromAst(objectProp(opts, "updateIn"), ctx) ?? createIn;
|
|
1762
|
+
const idSchema: JsonSchema = {
|
|
1763
|
+
type: "object",
|
|
1764
|
+
properties: { id: { type: "string" } },
|
|
1765
|
+
required: ["id"],
|
|
1766
|
+
};
|
|
1767
|
+
const okSchema: JsonSchema = {
|
|
1768
|
+
type: "object",
|
|
1769
|
+
properties: { ok: { const: true, type: "boolean" } },
|
|
1770
|
+
required: ["ok"],
|
|
1771
|
+
};
|
|
1561
1772
|
|
|
1562
|
-
const verbs
|
|
1773
|
+
const verbs: readonly {
|
|
1774
|
+
readonly op: string;
|
|
1775
|
+
readonly method: string;
|
|
1776
|
+
readonly p: string;
|
|
1777
|
+
readonly eff: Effects | undefined;
|
|
1778
|
+
readonly live?: boolean;
|
|
1779
|
+
readonly in?: JsonSchema;
|
|
1780
|
+
readonly out?: JsonSchema;
|
|
1781
|
+
}[] = [
|
|
1563
1782
|
{
|
|
1564
1783
|
op: "list",
|
|
1565
1784
|
method: "GET",
|
|
1566
1785
|
p: path,
|
|
1567
1786
|
eff: storeRef ? { reads: [storeRef] } : undefined,
|
|
1568
1787
|
live,
|
|
1788
|
+
in: defaultListInSchema(),
|
|
1789
|
+
...(itemOut ? { out: { type: "array", items: itemOut } } : {}),
|
|
1790
|
+
},
|
|
1791
|
+
...(skipCreate
|
|
1792
|
+
? []
|
|
1793
|
+
: [
|
|
1794
|
+
{
|
|
1795
|
+
op: "create",
|
|
1796
|
+
method: "POST",
|
|
1797
|
+
p: path,
|
|
1798
|
+
eff: storeRef ? { writes: [storeRef] } : undefined,
|
|
1799
|
+
...(createIn ? { in: createIn } : {}),
|
|
1800
|
+
out: idSchema,
|
|
1801
|
+
},
|
|
1802
|
+
]),
|
|
1803
|
+
{
|
|
1804
|
+
op: "get",
|
|
1805
|
+
method: "GET",
|
|
1806
|
+
p: idPath,
|
|
1807
|
+
eff: storeRef ? { reads: [storeRef] } : undefined,
|
|
1808
|
+
in: idSchema,
|
|
1809
|
+
...(itemOut ? { out: itemOut } : {}),
|
|
1810
|
+
},
|
|
1811
|
+
{
|
|
1812
|
+
op: "update",
|
|
1813
|
+
method: "PATCH",
|
|
1814
|
+
p: idPath,
|
|
1815
|
+
eff: both,
|
|
1816
|
+
in: updateIn ? mergeIdIntoSchema(updateIn) : idSchema,
|
|
1817
|
+
out: idSchema,
|
|
1569
1818
|
},
|
|
1570
|
-
{ op: "create", method: "POST", p: path, eff: storeRef ? { writes: [storeRef] } : undefined },
|
|
1571
|
-
{ op: "get", method: "GET", p: idPath, eff: storeRef ? { reads: [storeRef] } : undefined },
|
|
1572
|
-
{ op: "update", method: "PATCH", p: idPath, eff: both },
|
|
1573
1819
|
{
|
|
1574
1820
|
op: "delete",
|
|
1575
1821
|
method: "DELETE",
|
|
1576
1822
|
p: idPath,
|
|
1577
1823
|
eff: storeRef ? { reads: [storeRef], writes: [storeRef] } : undefined,
|
|
1824
|
+
in: idSchema,
|
|
1825
|
+
out: okSchema,
|
|
1578
1826
|
},
|
|
1579
|
-
]
|
|
1827
|
+
];
|
|
1580
1828
|
|
|
1581
1829
|
for (const v of verbs) {
|
|
1582
1830
|
const name = `${unit}.${v.op}`;
|
|
1583
1831
|
const flow: Flow = {
|
|
1584
1832
|
trigger: { http: { method: v.method as never, path: v.p } },
|
|
1585
1833
|
...(v.eff ? { effects: v.eff as Effects } : {}),
|
|
1586
|
-
...(
|
|
1834
|
+
...(v.live ? { live: true } : {}),
|
|
1835
|
+
...(v.in !== undefined ? { in: v.in } : {}),
|
|
1836
|
+
...(v.out !== undefined ? { out: v.out } : {}),
|
|
1587
1837
|
source: `${file.path}:${line}`,
|
|
1588
1838
|
};
|
|
1589
1839
|
scope.flows[name] = flow;
|
|
@@ -1597,7 +1847,11 @@ interface ParsedTrigger {
|
|
|
1597
1847
|
live?: boolean;
|
|
1598
1848
|
}
|
|
1599
1849
|
|
|
1600
|
-
function parseTrigger(
|
|
1850
|
+
function parseTrigger(
|
|
1851
|
+
node: AstNode,
|
|
1852
|
+
scope: ProjectScope,
|
|
1853
|
+
filePath?: string,
|
|
1854
|
+
): ParsedTrigger | undefined {
|
|
1601
1855
|
// every("10m")
|
|
1602
1856
|
if (node.type === "CallExpression") {
|
|
1603
1857
|
const call = node as CallExpression;
|
|
@@ -1610,8 +1864,8 @@ function parseTrigger(node: AstNode, scope: ProjectScope): ParsedTrigger | undef
|
|
|
1610
1864
|
return { trigger: {} };
|
|
1611
1865
|
}
|
|
1612
1866
|
|
|
1613
|
-
// http.post("/x").gate(...).live() / http.get("/x").
|
|
1614
|
-
const http = parseHttpTrigger(call, scope);
|
|
1867
|
+
// http.post("/x").gate(...).live() / http.get("/x").public()
|
|
1868
|
+
const http = parseHttpTrigger(call, scope, filePath);
|
|
1615
1869
|
if (http) return http;
|
|
1616
1870
|
|
|
1617
1871
|
// table("orders").changed("status") / db.table(orders).changed(...)
|
|
@@ -1622,7 +1876,7 @@ function parseTrigger(node: AstNode, scope: ProjectScope): ParsedTrigger | undef
|
|
|
1622
1876
|
}
|
|
1623
1877
|
|
|
1624
1878
|
if (node.type === "MemberExpression") {
|
|
1625
|
-
const http = parseHttpTrigger(node, scope);
|
|
1879
|
+
const http = parseHttpTrigger(node, scope, filePath);
|
|
1626
1880
|
if (http) return http;
|
|
1627
1881
|
}
|
|
1628
1882
|
|
|
@@ -1640,8 +1894,12 @@ function parseTrigger(node: AstNode, scope: ProjectScope): ParsedTrigger | undef
|
|
|
1640
1894
|
return undefined;
|
|
1641
1895
|
}
|
|
1642
1896
|
|
|
1643
|
-
function parseHttpTrigger(
|
|
1644
|
-
|
|
1897
|
+
function parseHttpTrigger(
|
|
1898
|
+
leaf: AstNode,
|
|
1899
|
+
scope: ProjectScope,
|
|
1900
|
+
filePath?: string,
|
|
1901
|
+
): ParsedTrigger | undefined {
|
|
1902
|
+
// Walk: http.METHOD(path).gate(...).live() / .public()
|
|
1645
1903
|
const chain = flattenMemberCallChain(leaf);
|
|
1646
1904
|
let method: string | undefined;
|
|
1647
1905
|
let path: string | undefined;
|
|
@@ -1670,24 +1928,16 @@ function parseHttpTrigger(leaf: AstNode, scope: ProjectScope): ParsedTrigger | u
|
|
|
1670
1928
|
continue;
|
|
1671
1929
|
}
|
|
1672
1930
|
|
|
1931
|
+
if (prop === "public") gateNames.push("public");
|
|
1673
1932
|
if (prop === "live") live = true;
|
|
1674
|
-
continue;
|
|
1675
|
-
}
|
|
1676
|
-
|
|
1677
|
-
if (node.type === "MemberExpression") {
|
|
1678
|
-
const member = node as AstNode & { object: AstNode; property: AstNode };
|
|
1679
|
-
if (identifierName(member.property) !== "public") continue;
|
|
1680
|
-
const obj = member.object;
|
|
1681
|
-
if (
|
|
1682
|
-
obj.type === "MemberExpression" &&
|
|
1683
|
-
identifierName((obj as AstNode & { property: AstNode }).property) === "gate"
|
|
1684
|
-
) {
|
|
1685
|
-
gateNames.push("public");
|
|
1686
|
-
}
|
|
1687
1933
|
}
|
|
1688
1934
|
}
|
|
1689
1935
|
|
|
1690
|
-
if (!method
|
|
1936
|
+
if (!method) return undefined;
|
|
1937
|
+
if (!path) {
|
|
1938
|
+
path = filePath ? pathFromFlowFile(filePath) : undefined;
|
|
1939
|
+
}
|
|
1940
|
+
if (!path) return undefined;
|
|
1691
1941
|
|
|
1692
1942
|
const httpMethods = [
|
|
1693
1943
|
"GET",
|
|
@@ -1806,14 +2056,69 @@ function parseSlo(node: AstNode | undefined): Slo | undefined {
|
|
|
1806
2056
|
};
|
|
1807
2057
|
}
|
|
1808
2058
|
|
|
1809
|
-
function
|
|
2059
|
+
function recordConstInit(scope: ProjectScope, filePath: string, name: string, node: AstNode): void {
|
|
2060
|
+
const byFile = scope.constInitsByFile.get(filePath) ?? new Map<string, AstNode>();
|
|
2061
|
+
byFile.set(name, node);
|
|
2062
|
+
scope.constInitsByFile.set(filePath, byFile);
|
|
2063
|
+
const existing = scope.uniqueConstInits.get(name);
|
|
2064
|
+
if (existing === undefined) {
|
|
2065
|
+
scope.uniqueConstInits.set(name, { node, file: filePath });
|
|
2066
|
+
return;
|
|
2067
|
+
}
|
|
2068
|
+
if (existing.file !== filePath) {
|
|
2069
|
+
scope.uniqueConstInits.delete(name);
|
|
2070
|
+
}
|
|
2071
|
+
}
|
|
2072
|
+
|
|
2073
|
+
function schemaExpandContext(scope: ProjectScope, filePath: string): SchemaExpandContext {
|
|
2074
|
+
return {
|
|
2075
|
+
filePath,
|
|
2076
|
+
resolveConst: (name, fromFile) => {
|
|
2077
|
+
const local = scope.constInitsByFile.get(fromFile)?.get(name);
|
|
2078
|
+
if (local) return { node: local, file: fromFile };
|
|
2079
|
+
return scope.uniqueConstInits.get(name);
|
|
2080
|
+
},
|
|
2081
|
+
bindings: scope.bindings,
|
|
2082
|
+
tableColumns: (name) => scope.schemaTables.get(name)?.columns,
|
|
2083
|
+
};
|
|
2084
|
+
}
|
|
2085
|
+
|
|
2086
|
+
function mergeIdIntoSchema(schema: JsonSchema): JsonSchema {
|
|
2087
|
+
if (typeof schema === "string") {
|
|
2088
|
+
return {
|
|
2089
|
+
type: "object",
|
|
2090
|
+
properties: { id: { type: "string" } },
|
|
2091
|
+
required: ["id"],
|
|
2092
|
+
};
|
|
2093
|
+
}
|
|
2094
|
+
const properties = {
|
|
2095
|
+
...((schema.properties as Record<string, unknown> | undefined) ?? {}),
|
|
2096
|
+
id: { type: "string" },
|
|
2097
|
+
};
|
|
2098
|
+
const required = [
|
|
2099
|
+
...new Set([...(Array.isArray(schema.required) ? (schema.required as string[]) : []), "id"]),
|
|
2100
|
+
];
|
|
2101
|
+
return { ...schema, type: "object", properties, required };
|
|
2102
|
+
}
|
|
2103
|
+
|
|
2104
|
+
function schemaProp(
|
|
2105
|
+
obj: AstNode | undefined,
|
|
2106
|
+
key: string,
|
|
2107
|
+
scope: ProjectScope,
|
|
2108
|
+
filePath: string,
|
|
2109
|
+
): JsonSchema | undefined {
|
|
1810
2110
|
const node = objectProp(obj, key);
|
|
1811
2111
|
if (!node) return undefined;
|
|
1812
|
-
|
|
1813
|
-
if (
|
|
2112
|
+
const expanded = jsonSchemaFromAst(node, schemaExpandContext(scope, filePath));
|
|
2113
|
+
if (expanded) return expanded;
|
|
2114
|
+
// Identifier / member / call that we could not expand — spec excerpt placeholder.
|
|
2115
|
+
if (
|
|
2116
|
+
node.type === "Identifier" ||
|
|
2117
|
+
node.type === "MemberExpression" ||
|
|
2118
|
+
node.type === "CallExpression"
|
|
2119
|
+
) {
|
|
1814
2120
|
return "…";
|
|
1815
2121
|
}
|
|
1816
|
-
if (node.type === "CallExpression") return "…";
|
|
1817
2122
|
const lit = stringArg(node);
|
|
1818
2123
|
if (lit) return lit;
|
|
1819
2124
|
return "…";
|
|
@@ -1821,7 +2126,7 @@ function schemaProp(obj: AstNode | undefined, key: string): string | undefined {
|
|
|
1821
2126
|
|
|
1822
2127
|
// ── AST helpers ────────────────────────────────────────────────────────────
|
|
1823
2128
|
|
|
1824
|
-
/** Leaf-to-root walk of `http.get("/x").
|
|
2129
|
+
/** Leaf-to-root walk of `http.get("/x").public().live()`. */
|
|
1825
2130
|
function flattenMemberCallChain(leaf: AstNode): AstNode[] {
|
|
1826
2131
|
const chain: AstNode[] = [];
|
|
1827
2132
|
let current: AstNode = leaf;
|
|
@@ -1941,6 +2246,19 @@ function objectArg(node: AstNode | undefined): AstNode | undefined {
|
|
|
1941
2246
|
return undefined;
|
|
1942
2247
|
}
|
|
1943
2248
|
|
|
2249
|
+
/**
|
|
2250
|
+
* `flow("name", { do })` options live in arg1; `flow({ do })` in arg0.
|
|
2251
|
+
*
|
|
2252
|
+
* @param flowCall - `flow(...)` AST node
|
|
2253
|
+
*/
|
|
2254
|
+
function flowOptionsArg(flowCall: CallExpression): AstNode | undefined {
|
|
2255
|
+
const named = objectArg(flowCall.arguments[1]);
|
|
2256
|
+
if (named) return named;
|
|
2257
|
+
if (stringArg(flowCall.arguments[0]) !== undefined) return undefined;
|
|
2258
|
+
if (isInterpolatedTemplate(flowCall.arguments[0])) return undefined;
|
|
2259
|
+
return objectArg(flowCall.arguments[0]);
|
|
2260
|
+
}
|
|
2261
|
+
|
|
1944
2262
|
function objectProp(obj: AstNode | undefined, key: string): AstNode | undefined {
|
|
1945
2263
|
if (!obj || obj.type !== "ObjectExpression") return undefined;
|
|
1946
2264
|
for (const prop of objectProperties(obj)) {
|