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.
Files changed (205) hide show
  1. package/package.json +3 -4
  2. package/site/content/docs/elements/ai.mdx +2 -1
  3. package/site/content/docs/elements/channel.mdx +1 -0
  4. package/site/content/docs/elements/clock.mdx +3 -0
  5. package/site/content/docs/elements/flow.mdx +3 -0
  6. package/site/content/docs/elements/gate.mdx +25 -22
  7. package/site/content/docs/elements/signal.mdx +4 -4
  8. package/site/content/docs/elements/store.mdx +86 -27
  9. package/site/content/docs/elements/vault.mdx +1 -0
  10. package/site/content/docs/get-started/basic-usage.mdx +22 -13
  11. package/site/content/docs/get-started/index.mdx +5 -0
  12. package/site/content/docs/get-started/introduction.mdx +11 -3
  13. package/site/content/docs/get-started/meta.json +1 -1
  14. package/site/content/docs/get-started/project-structure.mdx +932 -0
  15. package/site/content/docs/reference/cli.md +13 -10
  16. package/site/content/docs/reference/client.mdx +25 -21
  17. package/site/content/docs/reference/security.md +1 -1
  18. package/src/auth/bindings.ts +14 -5
  19. package/src/auth/cross-plane.ts +10 -3
  20. package/src/cli/ai-setup/ai-setup.test.ts +62 -0
  21. package/src/cli/ai-setup/apply.ts +32 -1
  22. package/src/cli/attach-host-console.test.ts +3 -2
  23. package/src/cli/attach-host-console.ts +3 -0
  24. package/src/cli/build.test.ts +4 -2
  25. package/src/cli/db-seed.ts +48 -15
  26. package/src/cli/db.test.ts +104 -0
  27. package/src/cli/db.ts +122 -10
  28. package/src/cli/dev-controls.test.ts +20 -0
  29. package/src/cli/dev-controls.ts +13 -0
  30. package/src/cli/dev.test.ts +15 -4
  31. package/src/cli/dev.ts +102 -29
  32. package/src/cli/ensure-drizzle-config.ts +2 -1
  33. package/src/cli/tui/DevLive.tsx +12 -2
  34. package/src/cli/tui/keys.test.ts +2 -1
  35. package/src/cli/tui/keys.ts +3 -2
  36. package/src/compiler/aot.test.ts +1 -1
  37. package/src/compiler/extract.test.ts +409 -33
  38. package/src/compiler/extract.ts +318 -67
  39. package/src/compiler/flow-path.test.ts +96 -0
  40. package/src/compiler/flow-path.ts +173 -0
  41. package/src/compiler/generate-adopt.test.ts +112 -3
  42. package/src/compiler/generate-adopt.ts +280 -26
  43. package/src/compiler/schema-from-ast.ts +609 -0
  44. package/src/console/server/bind.ts +2 -2
  45. package/src/console/server/console.test.ts +3 -0
  46. package/src/console/server/flows.ts +120 -40
  47. package/src/console/server/invoke-user-flow.test.ts +3 -1
  48. package/src/console/server/invoke-user-flow.ts +17 -2
  49. package/src/console/server/iso-at.test.ts +9 -0
  50. package/src/console/server/iso-at.ts +11 -0
  51. package/src/console/server/security-headers.ts +6 -3
  52. package/src/console/server/spa-proxy.test.ts +1 -0
  53. package/src/console/server/sql-catalog.ts +147 -7
  54. package/src/console/server/sql-rls.test.ts +16 -0
  55. package/src/console/server/state.ts +8 -0
  56. package/src/console/server/store.test.ts +7 -4
  57. package/src/console/server/store.ts +142 -58
  58. package/src/console/ui-next/dist/assets/agent-disclosure-C0X1fbWF.js +1 -0
  59. package/src/console/ui-next/dist/assets/{cache-glyph-BanhLsEY.js → cache-glyph-F1FI122b.js} +1 -1
  60. package/src/console/ui-next/dist/assets/{call-pii-button-bqkxMrJH.js → call-pii-button-DEDCl_j3.js} +1 -1
  61. package/src/console/ui-next/dist/assets/collapsible-LPqGvfoz.js +1 -0
  62. package/src/console/ui-next/dist/assets/{confirm-sheet-DDCRmG62.js → confirm-sheet-3ptDzXbz.js} +1 -1
  63. package/src/console/ui-next/dist/assets/{duration-tone-oiRxPVsZ.js → duration-tone-sC3lGABz.js} +1 -1
  64. package/src/console/ui-next/dist/assets/{explorer-empty-HjTnVQoR.js → explorer-empty-BvYrygyO.js} +1 -1
  65. package/src/console/ui-next/dist/assets/flows-page-Dluo1Bd7.js +1 -0
  66. package/src/console/ui-next/dist/assets/{highlighted-json-CvDPvveV.js → highlighted-json-CS_O8L-r.js} +1 -1
  67. package/src/console/ui-next/dist/assets/http-method--sWDdXSB.js +1 -0
  68. package/src/console/ui-next/dist/assets/index-Ca3HZMVq.js +66 -0
  69. package/src/console/ui-next/dist/assets/index-UtTDRQpS.css +2 -0
  70. package/src/console/ui-next/dist/assets/{link-DF7SZ9Ek.js → link-COeyggt-.js} +1 -1
  71. package/src/console/ui-next/dist/assets/{observability-page-BEZDzyYh.js → observability-page-HK9-BO8z.js} +2 -2
  72. package/src/console/ui-next/dist/assets/react-D8E3mtu1.js +1 -0
  73. package/src/console/ui-next/dist/assets/replica-lag-DBIFFf7d.js +18 -0
  74. package/src/console/ui-next/dist/assets/sheet-form-Dnwa5oOR.js +1 -0
  75. package/src/console/ui-next/dist/assets/shortcut-keys-CoqwEIi0.js +1 -0
  76. package/src/console/ui-next/dist/assets/skeleton-BHcfDYcb.js +1 -0
  77. package/src/console/ui-next/dist/assets/store-page-BSohH3wM.js +41 -0
  78. package/src/console/ui-next/dist/assets/trace-detail-sheet-CFIGRnnA.js +2 -0
  79. package/src/console/ui-next/dist/assets/tree-expand-toggle-BgLl34w7.js +54 -0
  80. package/src/console/ui-next/dist/assets/units-page-B0cFE76A.js +1 -0
  81. package/src/console/ui-next/dist/assets/{use-vault-list-Cl79j_ku.js → use-vault-list-BLbZhzyF.js} +1 -1
  82. package/src/console/ui-next/dist/assets/vault-page-B1SbYe10.js +2 -0
  83. package/src/console/ui-next/dist/index.html +6 -6
  84. package/src/console/ui-next/src/client.ts +35 -7
  85. package/src/console/ui-next/src/components/motion/table/index.tsx +16 -0
  86. package/src/console/ui-next/src/components/motion/table/types.ts +4 -0
  87. package/src/console/ui-next/src/components/ui/kbd.tsx +2 -2
  88. package/src/console/ui-next/src/features/flows/traces/http-method.ts +19 -0
  89. package/src/console/ui-next/src/features/flows/traces/trace-detail.test.ts +19 -1
  90. package/src/console/ui-next/src/features/flows/traces/traces-pane.tsx +10 -3
  91. package/src/console/ui-next/src/features/store/detail/browse-section.tsx +45 -1
  92. package/src/console/ui-next/src/features/store/detail/resource-panel.tsx +78 -16
  93. package/src/console/ui-next/src/features/store/detail/store-row-detail-sheet.tsx +155 -14
  94. package/src/console/ui-next/src/features/store/explorer/store-tree.tsx +33 -133
  95. package/src/console/ui-next/src/features/store/files/file-preview.tsx +4 -1
  96. package/src/console/ui-next/src/features/store/grid/rls-policy-sheet.tsx +326 -165
  97. package/src/console/ui-next/src/features/store/grid/sql-insert-sheet.tsx +6 -0
  98. package/src/console/ui-next/src/features/store/grid/store-data-grid.tsx +50 -3
  99. package/src/console/ui-next/src/features/store/lib/files-meta.test.ts +3 -0
  100. package/src/console/ui-next/src/features/store/lib/files-meta.ts +12 -0
  101. package/src/console/ui-next/src/features/store/lib/query-defaults.test.ts +25 -0
  102. package/src/console/ui-next/src/features/store/lib/query-defaults.ts +85 -3
  103. package/src/console/ui-next/src/features/store/lib/query-gate.ts +30 -4
  104. package/src/console/ui-next/src/features/store/lib/rls-policy.test.ts +139 -0
  105. package/src/console/ui-next/src/features/store/lib/rls-policy.ts +223 -21
  106. package/src/console/ui-next/src/features/store/lib/schema-graph.ts +4 -0
  107. package/src/console/ui-next/src/features/store/lib/sql-catalog.test.ts +24 -0
  108. package/src/console/ui-next/src/features/store/lib/sql-catalog.ts +19 -0
  109. package/src/console/ui-next/src/features/store/lib/store-tree.test.ts +6 -0
  110. package/src/console/ui-next/src/features/store/lib/store-tree.ts +13 -0
  111. package/src/console/ui-next/src/features/store/query/query-console.tsx +48 -14
  112. package/src/console/ui-next/src/features/store/query/query-gate-menu.tsx +19 -20
  113. package/src/console/ui-next/src/features/store/query/query-gate-parts.tsx +2 -1
  114. package/src/console/ui-next/src/features/store/query/query-results.tsx +110 -83
  115. package/src/console/ui-next/src/features/store/schema/schema-table-node.tsx +17 -0
  116. package/src/console/ui-next/src/features/units/call/call-api-panel.tsx +112 -72
  117. package/src/console/ui-next/src/features/units/lib/contract-input.test.ts +206 -1
  118. package/src/console/ui-next/src/features/units/lib/contract-input.ts +55 -0
  119. package/src/console/ui-next/src/features/vault/lib/types.ts +1 -1
  120. package/src/docker/compose-health.test.ts +27 -0
  121. package/src/docker/compose-health.ts +19 -3
  122. package/src/docker/docker.test.ts +3 -0
  123. package/src/docker/recipes/pgdog.ts +2 -0
  124. package/src/drivers/clock-postgres.ts +13 -9
  125. package/src/drivers/instances-postgres.ts +4 -6
  126. package/src/drivers/journal-postgres.ts +13 -9
  127. package/src/drivers/pg-rls.test.ts +87 -0
  128. package/src/drivers/pg-rls.ts +233 -7
  129. package/src/drivers/pglite.ts +26 -3
  130. package/src/drivers/postgres.test.ts +23 -0
  131. package/src/drivers/postgres.ts +133 -9
  132. package/src/drivers/types.ts +10 -0
  133. package/src/elements/clock/declare.ts +26 -1
  134. package/src/elements/gate/boot.ts +5 -5
  135. package/src/elements/gate/declare.ts +36 -4
  136. package/src/elements/gate/flatten.ts +1 -1
  137. package/src/elements/gate/runtime.ts +1 -1
  138. package/src/elements/store/declare.ts +9 -6
  139. package/src/elements/store/emit-drizzle.ts +44 -8
  140. package/src/elements/store/resource.test.ts +1 -1
  141. package/src/elements/store/rls-identity.test.ts +95 -0
  142. package/src/elements/store/rls-identity.ts +116 -0
  143. package/src/elements/store/runtime.ts +4 -0
  144. package/src/elements/store/schema-decl.test.ts +37 -0
  145. package/src/elements/store/schema-decl.ts +161 -0
  146. package/src/elements/store/seed.ts +1 -1
  147. package/src/elements/store/sql-condition.test.ts +12 -0
  148. package/src/elements/store/sql-condition.ts +26 -2
  149. package/src/elements/store/sql-rls-isolation.test.ts +165 -0
  150. package/src/elements/store/sql-rls-stamp.test.ts +117 -0
  151. package/src/elements/store/sql-session.ts +81 -3
  152. package/src/elements/store/upsert-app.test.ts +1 -1
  153. package/src/elements/store.ts +14 -0
  154. package/src/http.ts +20 -3
  155. package/src/index.ts +10 -1
  156. package/src/kernel/adopt-barrel-fresh.test.ts +1 -1
  157. package/src/kernel/adopt-routes.ts +30 -2
  158. package/src/kernel/app.ts +131 -14
  159. package/src/kernel/auto-cache.test.ts +8 -8
  160. package/src/kernel/auto-registry.test.ts +49 -9
  161. package/src/kernel/boot.test.ts +4 -4
  162. package/src/kernel/boot.ts +2 -2
  163. package/src/kernel/effects-stamping.test.ts +3 -3
  164. package/src/kernel/element-registries.ts +12 -5
  165. package/src/kernel/errors.ts +24 -0
  166. package/src/kernel/flow-units.ts +64 -0
  167. package/src/kernel/flow.ts +29 -17
  168. package/src/kernel/fx.ts +23 -0
  169. package/src/kernel/horizontal-child.ts +4 -4
  170. package/src/kernel/http-path-pending.ts +20 -0
  171. package/src/kernel/http-resource.ts +4 -0
  172. package/src/kernel/http-stream.test.ts +4 -4
  173. package/src/kernel/pipeline.test.ts +2 -2
  174. package/src/kernel/plugin-elements.test.ts +1 -1
  175. package/src/kernel/plugin-needs.test.ts +1 -1
  176. package/src/kernel/ready.test.ts +2 -2
  177. package/src/kernel/resource-mount.test.ts +14 -0
  178. package/src/kernel/run-duration.test.ts +2 -2
  179. package/src/kernel/stamp-http.test.ts +79 -0
  180. package/src/kernel/stamp-http.ts +75 -0
  181. package/src/kernel/triggers.ts +52 -27
  182. package/src/manifest/types.ts +13 -0
  183. package/src/release/measure.ts +2 -2
  184. package/src/runtime/json-code-block.test.ts +2 -2
  185. package/src/runtime/serve.test.ts +4 -4
  186. package/src/term.test.ts +52 -4
  187. package/src/term.ts +125 -17
  188. package/src/test/reset-element-registries.ts +8 -3
  189. package/src/cli/ask-seed.test.ts +0 -96
  190. package/src/cli/ask-seed.ts +0 -82
  191. package/src/console/ui-next/dist/assets/collapsible-DYb0xU8C.js +0 -1
  192. package/src/console/ui-next/dist/assets/flows-page-DxDsOd4f.js +0 -1
  193. package/src/console/ui-next/dist/assets/http-method-CJCBYL2j.js +0 -1
  194. package/src/console/ui-next/dist/assets/index-Ce6WKWKM.js +0 -66
  195. package/src/console/ui-next/dist/assets/index-D4Ldtj79.css +0 -2
  196. package/src/console/ui-next/dist/assets/query-gate-parts-1m8m1iNp.js +0 -2
  197. package/src/console/ui-next/dist/assets/react-B1ML8gxg.js +0 -1
  198. package/src/console/ui-next/dist/assets/replica-lag-C8_BRt2x.js +0 -18
  199. package/src/console/ui-next/dist/assets/sheet-form-D-ata7jy.js +0 -1
  200. package/src/console/ui-next/dist/assets/shortcut-keys-DO4IsVqv.js +0 -1
  201. package/src/console/ui-next/dist/assets/skeleton-CL_X0GCj.js +0 -1
  202. package/src/console/ui-next/dist/assets/store-page-v3LXdYpr.js +0 -45
  203. package/src/console/ui-next/dist/assets/trace-detail-sheet-DFLFfUUX.js +0 -2
  204. package/src/console/ui-next/dist/assets/units-page-C0gW6Kdo.js +0 -1
  205. package/src/console/ui-next/dist/assets/vault-page-DuKzqwzW.js +0 -2
@@ -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
- string,
78
- { readonly name: string; readonly columns: Record<string, DeclaredColumn> }
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, columns: declared.columns };
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, { name: string; columns: Record<string, DeclaredColumn> }>();
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] = { columns: t.columns };
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 entry = { name: tableName, columns };
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] = { columns: declared.columns };
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] = { columns: inline.columns };
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 = objectArg(args.flowCall.arguments[1]);
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 ? parseTrigger(args.triggerNode, args.scope) : undefined;
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 and
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
- ] as const;
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
- ...("live" in v && v.live ? { live: true } : {}),
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(node: AstNode, scope: ProjectScope): ParsedTrigger | undefined {
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").gate.public
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(leaf: AstNode, scope: ProjectScope): ParsedTrigger | undefined {
1711
- // Walk: http.METHOD(path).gate(...).live() / .gate.public
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 || !path) return undefined;
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 schemaProp(obj: AstNode | undefined, key: string): string | undefined {
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
- // Identifier / member schemas → opaque placeholder matching the spec excerpt.
1880
- if (node.type === "Identifier" || node.type === "MemberExpression") {
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").gate.public.live()`. */
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)) {