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
package/src/compiler/extract.ts
CHANGED
|
@@ -22,6 +22,7 @@ import type {
|
|
|
22
22
|
Flow,
|
|
23
23
|
Gate,
|
|
24
24
|
Journey,
|
|
25
|
+
JsonSchema,
|
|
25
26
|
Manifest,
|
|
26
27
|
RateStrategy,
|
|
27
28
|
ResourceRef,
|
|
@@ -30,6 +31,7 @@ import type {
|
|
|
30
31
|
SignalDelivery,
|
|
31
32
|
Slo,
|
|
32
33
|
Store,
|
|
34
|
+
TablePolicy,
|
|
33
35
|
Trigger,
|
|
34
36
|
} from "../manifest/types.ts";
|
|
35
37
|
import { sqlTableRef } from "../manifest/sql-resource.ts";
|
|
@@ -45,6 +47,12 @@ import {
|
|
|
45
47
|
type InferBinding,
|
|
46
48
|
type Literal,
|
|
47
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";
|
|
48
56
|
|
|
49
57
|
/** One source file for extraction. */
|
|
50
58
|
export interface SourceFile {
|
|
@@ -67,16 +75,24 @@ export interface ExtractManifestOptions {
|
|
|
67
75
|
}
|
|
68
76
|
|
|
69
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
|
+
|
|
70
85
|
interface ProjectScope {
|
|
71
86
|
app: string;
|
|
72
87
|
bindings: Map<string, InferBinding>;
|
|
73
88
|
signals: Record<string, Signal>;
|
|
74
89
|
stores: Record<string, Store>;
|
|
75
90
|
/** Binding or table name → declared columns from `store.schema.table`. */
|
|
76
|
-
schemaTables: Map<
|
|
77
|
-
|
|
78
|
-
|
|
79
|
-
|
|
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 }>;
|
|
80
96
|
clocks: Record<string, Clock>;
|
|
81
97
|
gates: Record<string, Gate>;
|
|
82
98
|
/** Local binding name → gate manifest id (policy name or rate expression). */
|
|
@@ -119,6 +135,8 @@ export async function extractManifest(options: ExtractManifestOptions = {}): Pro
|
|
|
119
135
|
signals: {},
|
|
120
136
|
stores: {},
|
|
121
137
|
schemaTables: new Map(),
|
|
138
|
+
constInitsByFile: new Map(),
|
|
139
|
+
uniqueConstInits: new Map(),
|
|
122
140
|
clocks: {},
|
|
123
141
|
gates: {},
|
|
124
142
|
gateIds: new Map(),
|
|
@@ -144,8 +162,8 @@ export async function extractManifest(options: ExtractManifestOptions = {}): Pro
|
|
|
144
162
|
});
|
|
145
163
|
|
|
146
164
|
// Pass 1 — declarations (elements, config, named bindings).
|
|
147
|
-
for (const { program } of parsed) {
|
|
148
|
-
collectDeclarations(program, scope);
|
|
165
|
+
for (const { file, program } of parsed) {
|
|
166
|
+
collectDeclarations(file.path, program, scope);
|
|
149
167
|
}
|
|
150
168
|
|
|
151
169
|
// Pass 2 — flows / on() bindings.
|
|
@@ -230,13 +248,13 @@ async function readSources(rootDir: string, pattern: string): Promise<SourceFile
|
|
|
230
248
|
return files;
|
|
231
249
|
}
|
|
232
250
|
|
|
233
|
-
function collectDeclarations(program: AstNode, scope: ProjectScope): void {
|
|
251
|
+
function collectDeclarations(filePath: string, program: AstNode, scope: ProjectScope): void {
|
|
234
252
|
walk(program, (node) => {
|
|
235
253
|
if (node.type === "CallExpression") {
|
|
236
254
|
visitDeclarationCall(node as CallExpression, program, scope);
|
|
237
255
|
}
|
|
238
256
|
if (node.type === "VariableDeclarator") {
|
|
239
|
-
visitDeclarator(node as AstNode, scope);
|
|
257
|
+
visitDeclarator(node as AstNode, filePath, scope);
|
|
240
258
|
}
|
|
241
259
|
});
|
|
242
260
|
|
|
@@ -251,12 +269,13 @@ function collectDeclarations(program: AstNode, scope: ProjectScope): void {
|
|
|
251
269
|
});
|
|
252
270
|
}
|
|
253
271
|
|
|
254
|
-
function visitDeclarator(decl: AstNode, scope: ProjectScope): void {
|
|
272
|
+
function visitDeclarator(decl: AstNode, filePath: string, scope: ProjectScope): void {
|
|
255
273
|
const id = (decl as AstNode & { id?: AstNode }).id;
|
|
256
274
|
const init = (decl as AstNode & { init?: AstNode }).init;
|
|
257
275
|
if (!id || !init) return;
|
|
258
276
|
const name = identifierName(id);
|
|
259
277
|
if (!name) return;
|
|
278
|
+
recordConstInit(scope, filePath, name, unwrapTsExpr(init));
|
|
260
279
|
|
|
261
280
|
// export const x = on(...) / flow(...) — flow export alias
|
|
262
281
|
if (init.type === "CallExpression") {
|
|
@@ -326,7 +345,7 @@ function finalizeRefs(scope: ProjectScope): void {
|
|
|
326
345
|
if (!declared) continue;
|
|
327
346
|
const existing = table.columns ?? {};
|
|
328
347
|
if (Object.keys(existing).length === 0) {
|
|
329
|
-
store.tables[name] = { ...table,
|
|
348
|
+
store.tables[name] = { ...table, ...tableFromDeclared(declared) };
|
|
330
349
|
}
|
|
331
350
|
}
|
|
332
351
|
}
|
|
@@ -338,7 +357,7 @@ function finalizeRefs(scope: ProjectScope): void {
|
|
|
338
357
|
if (!store.tables) continue;
|
|
339
358
|
for (const name of Object.keys(store.tables)) attachedNames.add(name);
|
|
340
359
|
}
|
|
341
|
-
const orphanByName = new Map<string,
|
|
360
|
+
const orphanByName = new Map<string, DeclaredSchemaTable>();
|
|
342
361
|
for (const t of scope.schemaTables.values()) {
|
|
343
362
|
if (!attachedNames.has(t.name)) orphanByName.set(t.name, t);
|
|
344
363
|
}
|
|
@@ -351,7 +370,7 @@ function finalizeRefs(scope: ProjectScope): void {
|
|
|
351
370
|
target.tables = target.tables ?? {};
|
|
352
371
|
for (const t of orphanByName.values()) {
|
|
353
372
|
if (target.tables[t.name]) continue;
|
|
354
|
-
target.tables[t.name] =
|
|
373
|
+
target.tables[t.name] = tableFromDeclared(t);
|
|
355
374
|
}
|
|
356
375
|
}
|
|
357
376
|
}
|
|
@@ -361,7 +380,8 @@ function collectSchemaTable(call: CallExpression, program: AstNode, scope: Proje
|
|
|
361
380
|
if (!tableName) return;
|
|
362
381
|
const colsNode = objectArg(call.arguments[1]);
|
|
363
382
|
const columns = colsNode ? parseDeclaredColumns(colsNode) : {};
|
|
364
|
-
const
|
|
383
|
+
const extras = parseSchemaTableExtras(call.arguments[2]);
|
|
384
|
+
const entry = { name: tableName, columns, ...extras };
|
|
365
385
|
scope.schemaTables.set(tableName, entry);
|
|
366
386
|
const bindingName = enclosingConstName(call, program);
|
|
367
387
|
if (bindingName) {
|
|
@@ -396,7 +416,7 @@ function attachSchemaOption(
|
|
|
396
416
|
const id = (value as Identifier).name;
|
|
397
417
|
const declared = scope.schemaTables.get(id) ?? scope.schemaTables.get(key);
|
|
398
418
|
if (declared) {
|
|
399
|
-
store.tables[declared.name] =
|
|
419
|
+
store.tables[declared.name] = tableFromDeclared(declared);
|
|
400
420
|
} else {
|
|
401
421
|
store.tables[key] = store.tables[key] ?? {};
|
|
402
422
|
}
|
|
@@ -406,7 +426,7 @@ function attachSchemaOption(
|
|
|
406
426
|
if (value.type === "CallExpression") {
|
|
407
427
|
const inline = parseInlineSchemaTable(value as CallExpression);
|
|
408
428
|
if (inline) {
|
|
409
|
-
store.tables[inline.name] =
|
|
429
|
+
store.tables[inline.name] = tableFromDeclared(inline);
|
|
410
430
|
scope.schemaTables.set(inline.name, inline);
|
|
411
431
|
}
|
|
412
432
|
}
|
|
@@ -420,9 +440,7 @@ function attachSchemaOption(
|
|
|
420
440
|
}
|
|
421
441
|
}
|
|
422
442
|
|
|
423
|
-
function parseInlineSchemaTable(
|
|
424
|
-
call: CallExpression,
|
|
425
|
-
): { name: string; columns: Record<string, DeclaredColumn> } | undefined {
|
|
443
|
+
function parseInlineSchemaTable(call: CallExpression): DeclaredSchemaTable | undefined {
|
|
426
444
|
const callee = call.callee;
|
|
427
445
|
if (callee.type !== "MemberExpression") return undefined;
|
|
428
446
|
const member = callee as AstNode & { object: AstNode; property: AstNode };
|
|
@@ -438,6 +456,121 @@ function parseInlineSchemaTable(
|
|
|
438
456
|
return {
|
|
439
457
|
name: tableName,
|
|
440
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 } : {}),
|
|
441
574
|
};
|
|
442
575
|
}
|
|
443
576
|
|
|
@@ -482,6 +615,10 @@ function parseFieldChain(node: AstNode, key: string): DeclaredColumn | undefined
|
|
|
482
615
|
if (method === "describe") {
|
|
483
616
|
description = stringArg(call.arguments[0]) ?? description;
|
|
484
617
|
}
|
|
618
|
+
if (method === "defaultFn") {
|
|
619
|
+
hasDefault = true;
|
|
620
|
+
defaultValue = defaultValue ?? null;
|
|
621
|
+
}
|
|
485
622
|
if (method === "default") {
|
|
486
623
|
const lit = call.arguments[0];
|
|
487
624
|
if (lit && lit.type === "Literal") {
|
|
@@ -1300,7 +1437,7 @@ function registerFlow(args: {
|
|
|
1300
1437
|
scope: ProjectScope;
|
|
1301
1438
|
exportName: string | undefined;
|
|
1302
1439
|
}): void {
|
|
1303
|
-
const opts =
|
|
1440
|
+
const opts = flowOptionsArg(args.flowCall);
|
|
1304
1441
|
if (!opts) return;
|
|
1305
1442
|
|
|
1306
1443
|
// `flow(\`${unit}.list\`)` inside a helper — name is only known at the
|
|
@@ -1309,6 +1446,7 @@ function registerFlow(args: {
|
|
|
1309
1446
|
|
|
1310
1447
|
const name =
|
|
1311
1448
|
stringArg(args.flowCall.arguments[0]) ??
|
|
1449
|
+
nameFromFlowFile(args.file.path, args.exportName) ??
|
|
1312
1450
|
args.exportName ??
|
|
1313
1451
|
`flow_${Object.keys(args.scope.flows).length + 1}`;
|
|
1314
1452
|
|
|
@@ -1356,7 +1494,9 @@ function registerFlow(args: {
|
|
|
1356
1494
|
}
|
|
1357
1495
|
}
|
|
1358
1496
|
|
|
1359
|
-
const trigger = args.triggerNode
|
|
1497
|
+
const trigger = args.triggerNode
|
|
1498
|
+
? parseTrigger(args.triggerNode, args.scope, args.file.path)
|
|
1499
|
+
: undefined;
|
|
1360
1500
|
|
|
1361
1501
|
const gates = trigger?.gates;
|
|
1362
1502
|
const liveFromTrigger = trigger?.live;
|
|
@@ -1369,9 +1509,9 @@ function registerFlow(args: {
|
|
|
1369
1509
|
}
|
|
1370
1510
|
if (gates && gates.length > 0) flow.gates = gates;
|
|
1371
1511
|
|
|
1372
|
-
const inSchema = schemaProp(opts, "in");
|
|
1512
|
+
const inSchema = schemaProp(opts, "in", args.scope, args.file.path);
|
|
1373
1513
|
if (inSchema !== undefined) flow.in = inSchema;
|
|
1374
|
-
const outSchema = schemaProp(opts, "out");
|
|
1514
|
+
const outSchema = schemaProp(opts, "out", args.scope, args.file.path);
|
|
1375
1515
|
if (outSchema !== undefined) flow.out = outSchema;
|
|
1376
1516
|
|
|
1377
1517
|
const errors = parseErrors(objectProp(opts, "errors"));
|
|
@@ -1460,8 +1600,8 @@ function registerFlow(args: {
|
|
|
1460
1600
|
}
|
|
1461
1601
|
|
|
1462
1602
|
/**
|
|
1463
|
-
* `http.resource(path, bag).gate(...).live()` — find the resource call
|
|
1464
|
-
* 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.
|
|
1465
1605
|
*
|
|
1466
1606
|
* @param node - First argument to `on(…)`
|
|
1467
1607
|
* @param scope - Project scope (gate name expansion)
|
|
@@ -1492,19 +1632,8 @@ function unwrapResourceMount(
|
|
|
1492
1632
|
}
|
|
1493
1633
|
continue;
|
|
1494
1634
|
}
|
|
1635
|
+
if (prop === "public") gateNames.push("public");
|
|
1495
1636
|
if (prop === "live") live = true;
|
|
1496
|
-
continue;
|
|
1497
|
-
}
|
|
1498
|
-
if (item.type === "MemberExpression") {
|
|
1499
|
-
const member = item as AstNode & { object: AstNode; property: AstNode };
|
|
1500
|
-
if (identifierName(member.property) !== "public") continue;
|
|
1501
|
-
const obj = member.object;
|
|
1502
|
-
if (
|
|
1503
|
-
obj.type === "MemberExpression" &&
|
|
1504
|
-
identifierName((obj as AstNode & { property: AstNode }).property) === "gate"
|
|
1505
|
-
) {
|
|
1506
|
-
gateNames.push("public");
|
|
1507
|
-
}
|
|
1508
1637
|
}
|
|
1509
1638
|
}
|
|
1510
1639
|
if (!httpCall) return undefined;
|
|
@@ -1625,32 +1754,86 @@ function registerNamedTableCrud(call: CallExpression, file: SourceFile, scope: P
|
|
|
1625
1754
|
const idPath = `${path}/:id`;
|
|
1626
1755
|
const line = lineAt(file.source, call.start ?? 0);
|
|
1627
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
|
+
};
|
|
1628
1772
|
|
|
1629
|
-
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
|
+
}[] = [
|
|
1630
1782
|
{
|
|
1631
1783
|
op: "list",
|
|
1632
1784
|
method: "GET",
|
|
1633
1785
|
p: path,
|
|
1634
1786
|
eff: storeRef ? { reads: [storeRef] } : undefined,
|
|
1635
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,
|
|
1636
1818
|
},
|
|
1637
|
-
{ op: "create", method: "POST", p: path, eff: storeRef ? { writes: [storeRef] } : undefined },
|
|
1638
|
-
{ op: "get", method: "GET", p: idPath, eff: storeRef ? { reads: [storeRef] } : undefined },
|
|
1639
|
-
{ op: "update", method: "PATCH", p: idPath, eff: both },
|
|
1640
1819
|
{
|
|
1641
1820
|
op: "delete",
|
|
1642
1821
|
method: "DELETE",
|
|
1643
1822
|
p: idPath,
|
|
1644
1823
|
eff: storeRef ? { reads: [storeRef], writes: [storeRef] } : undefined,
|
|
1824
|
+
in: idSchema,
|
|
1825
|
+
out: okSchema,
|
|
1645
1826
|
},
|
|
1646
|
-
]
|
|
1827
|
+
];
|
|
1647
1828
|
|
|
1648
1829
|
for (const v of verbs) {
|
|
1649
1830
|
const name = `${unit}.${v.op}`;
|
|
1650
1831
|
const flow: Flow = {
|
|
1651
1832
|
trigger: { http: { method: v.method as never, path: v.p } },
|
|
1652
1833
|
...(v.eff ? { effects: v.eff as Effects } : {}),
|
|
1653
|
-
...(
|
|
1834
|
+
...(v.live ? { live: true } : {}),
|
|
1835
|
+
...(v.in !== undefined ? { in: v.in } : {}),
|
|
1836
|
+
...(v.out !== undefined ? { out: v.out } : {}),
|
|
1654
1837
|
source: `${file.path}:${line}`,
|
|
1655
1838
|
};
|
|
1656
1839
|
scope.flows[name] = flow;
|
|
@@ -1664,7 +1847,11 @@ interface ParsedTrigger {
|
|
|
1664
1847
|
live?: boolean;
|
|
1665
1848
|
}
|
|
1666
1849
|
|
|
1667
|
-
function parseTrigger(
|
|
1850
|
+
function parseTrigger(
|
|
1851
|
+
node: AstNode,
|
|
1852
|
+
scope: ProjectScope,
|
|
1853
|
+
filePath?: string,
|
|
1854
|
+
): ParsedTrigger | undefined {
|
|
1668
1855
|
// every("10m")
|
|
1669
1856
|
if (node.type === "CallExpression") {
|
|
1670
1857
|
const call = node as CallExpression;
|
|
@@ -1677,8 +1864,8 @@ function parseTrigger(node: AstNode, scope: ProjectScope): ParsedTrigger | undef
|
|
|
1677
1864
|
return { trigger: {} };
|
|
1678
1865
|
}
|
|
1679
1866
|
|
|
1680
|
-
// http.post("/x").gate(...).live() / http.get("/x").
|
|
1681
|
-
const http = parseHttpTrigger(call, scope);
|
|
1867
|
+
// http.post("/x").gate(...).live() / http.get("/x").public()
|
|
1868
|
+
const http = parseHttpTrigger(call, scope, filePath);
|
|
1682
1869
|
if (http) return http;
|
|
1683
1870
|
|
|
1684
1871
|
// table("orders").changed("status") / db.table(orders).changed(...)
|
|
@@ -1689,7 +1876,7 @@ function parseTrigger(node: AstNode, scope: ProjectScope): ParsedTrigger | undef
|
|
|
1689
1876
|
}
|
|
1690
1877
|
|
|
1691
1878
|
if (node.type === "MemberExpression") {
|
|
1692
|
-
const http = parseHttpTrigger(node, scope);
|
|
1879
|
+
const http = parseHttpTrigger(node, scope, filePath);
|
|
1693
1880
|
if (http) return http;
|
|
1694
1881
|
}
|
|
1695
1882
|
|
|
@@ -1707,8 +1894,12 @@ function parseTrigger(node: AstNode, scope: ProjectScope): ParsedTrigger | undef
|
|
|
1707
1894
|
return undefined;
|
|
1708
1895
|
}
|
|
1709
1896
|
|
|
1710
|
-
function parseHttpTrigger(
|
|
1711
|
-
|
|
1897
|
+
function parseHttpTrigger(
|
|
1898
|
+
leaf: AstNode,
|
|
1899
|
+
scope: ProjectScope,
|
|
1900
|
+
filePath?: string,
|
|
1901
|
+
): ParsedTrigger | undefined {
|
|
1902
|
+
// Walk: http.METHOD(path).gate(...).live() / .public()
|
|
1712
1903
|
const chain = flattenMemberCallChain(leaf);
|
|
1713
1904
|
let method: string | undefined;
|
|
1714
1905
|
let path: string | undefined;
|
|
@@ -1737,24 +1928,16 @@ function parseHttpTrigger(leaf: AstNode, scope: ProjectScope): ParsedTrigger | u
|
|
|
1737
1928
|
continue;
|
|
1738
1929
|
}
|
|
1739
1930
|
|
|
1931
|
+
if (prop === "public") gateNames.push("public");
|
|
1740
1932
|
if (prop === "live") live = true;
|
|
1741
|
-
continue;
|
|
1742
|
-
}
|
|
1743
|
-
|
|
1744
|
-
if (node.type === "MemberExpression") {
|
|
1745
|
-
const member = node as AstNode & { object: AstNode; property: AstNode };
|
|
1746
|
-
if (identifierName(member.property) !== "public") continue;
|
|
1747
|
-
const obj = member.object;
|
|
1748
|
-
if (
|
|
1749
|
-
obj.type === "MemberExpression" &&
|
|
1750
|
-
identifierName((obj as AstNode & { property: AstNode }).property) === "gate"
|
|
1751
|
-
) {
|
|
1752
|
-
gateNames.push("public");
|
|
1753
|
-
}
|
|
1754
1933
|
}
|
|
1755
1934
|
}
|
|
1756
1935
|
|
|
1757
|
-
if (!method
|
|
1936
|
+
if (!method) return undefined;
|
|
1937
|
+
if (!path) {
|
|
1938
|
+
path = filePath ? pathFromFlowFile(filePath) : undefined;
|
|
1939
|
+
}
|
|
1940
|
+
if (!path) return undefined;
|
|
1758
1941
|
|
|
1759
1942
|
const httpMethods = [
|
|
1760
1943
|
"GET",
|
|
@@ -1873,14 +2056,69 @@ function parseSlo(node: AstNode | undefined): Slo | undefined {
|
|
|
1873
2056
|
};
|
|
1874
2057
|
}
|
|
1875
2058
|
|
|
1876
|
-
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 {
|
|
1877
2110
|
const node = objectProp(obj, key);
|
|
1878
2111
|
if (!node) return undefined;
|
|
1879
|
-
|
|
1880
|
-
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
|
+
) {
|
|
1881
2120
|
return "…";
|
|
1882
2121
|
}
|
|
1883
|
-
if (node.type === "CallExpression") return "…";
|
|
1884
2122
|
const lit = stringArg(node);
|
|
1885
2123
|
if (lit) return lit;
|
|
1886
2124
|
return "…";
|
|
@@ -1888,7 +2126,7 @@ function schemaProp(obj: AstNode | undefined, key: string): string | undefined {
|
|
|
1888
2126
|
|
|
1889
2127
|
// ── AST helpers ────────────────────────────────────────────────────────────
|
|
1890
2128
|
|
|
1891
|
-
/** Leaf-to-root walk of `http.get("/x").
|
|
2129
|
+
/** Leaf-to-root walk of `http.get("/x").public().live()`. */
|
|
1892
2130
|
function flattenMemberCallChain(leaf: AstNode): AstNode[] {
|
|
1893
2131
|
const chain: AstNode[] = [];
|
|
1894
2132
|
let current: AstNode = leaf;
|
|
@@ -2008,6 +2246,19 @@ function objectArg(node: AstNode | undefined): AstNode | undefined {
|
|
|
2008
2246
|
return undefined;
|
|
2009
2247
|
}
|
|
2010
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
|
+
|
|
2011
2262
|
function objectProp(obj: AstNode | undefined, key: string): AstNode | undefined {
|
|
2012
2263
|
if (!obj || obj.type !== "ObjectExpression") return undefined;
|
|
2013
2264
|
for (const prop of objectProperties(obj)) {
|