okengine 0.17.2 → 0.18.4
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/AGENTS.md +5 -3
- package/README.md +14 -10
- package/manifest.v1.schema.json +61 -2
- package/package.json +19 -5
- package/site/content/docs/ai/index.mdx +1 -1
- package/site/content/docs/ai/mcp.mdx +10 -1
- package/site/content/docs/ai/skills.mdx +9 -9
- package/site/content/docs/elements/ai.mdx +1 -1
- package/site/content/docs/elements/clock.mdx +1 -1
- package/site/content/docs/elements/flow.mdx +25 -1
- package/site/content/docs/elements/gate.mdx +3 -2
- package/site/content/docs/elements/store.mdx +289 -341
- package/site/content/docs/elements/vault.mdx +5 -5
- package/site/content/docs/get-started/basic-usage.mdx +3 -10
- package/site/content/docs/get-started/index.mdx +1 -1
- package/site/content/docs/get-started/installation.mdx +2 -3
- package/site/content/docs/get-started/introduction.mdx +58 -121
- package/site/content/docs/get-started/meta.json +9 -1
- package/site/content/docs/get-started/project-structure.mdx +4 -11
- package/site/content/docs/get-started/testing.mdx +328 -0
- package/site/content/docs/get-started/why.mdx +93 -71
- package/site/content/docs/index.mdx +44 -11
- package/site/content/docs/meta.json +8 -5
- package/site/content/docs/plugins/apple.mdx +151 -0
- package/site/content/docs/plugins/discord.mdx +139 -0
- package/site/content/docs/plugins/facebook.mdx +134 -0
- package/site/content/docs/plugins/figma.mdx +138 -0
- package/site/content/docs/plugins/github.mdx +138 -0
- package/site/content/docs/plugins/google.mdx +153 -0
- package/site/content/docs/plugins/index.mdx +47 -1
- package/site/content/docs/plugins/meta.json +10 -0
- package/site/content/docs/plugins/microsoft.mdx +151 -0
- package/site/content/docs/plugins/oauth.mdx +188 -0
- package/site/content/docs/plugins/x.mdx +125 -0
- package/site/content/docs/providers/index.mdx +2 -0
- package/site/content/docs/recipes/index.mdx +2 -0
- package/site/content/docs/reference/cli.md +3 -2
- package/site/content/docs/reference/client.mdx +58 -1
- package/site/content/docs/reference/configuration.mdx +2 -4
- package/site/content/docs/reference/fx.mdx +3 -1
- package/site/content/docs/reference/index.mdx +0 -5
- package/site/content/docs/reference/meta.json +2 -2
- package/site/content/docs/reference/okid.mdx +137 -0
- package/src/auth/bindings.ts +1 -1
- package/src/auth/config.ts +9 -0
- package/src/auth/identity-sql.ts +314 -0
- package/src/auth/identity.ts +140 -2
- package/src/auth/index.ts +17 -1
- package/src/auth/method-context.ts +3 -0
- package/src/auth/oauth-as/cimd.ts +132 -0
- package/src/auth/oauth-as/crypto.test.ts +101 -0
- package/src/auth/oauth-as/crypto.ts +393 -0
- package/src/auth/oauth-as/errors.ts +68 -0
- package/src/auth/oauth-as/http.test.ts +419 -0
- package/src/auth/oauth-as/http.ts +842 -0
- package/src/auth/oauth-as/stores.ts +61 -0
- package/src/auth/oauth-as/tables.ts +142 -0
- package/src/auth/tables.ts +0 -11
- package/src/bench/README.md +83 -0
- package/src/bench/REPORT.md +176 -0
- package/src/bench/g01-rls-stamp.bench.ts +194 -0
- package/src/bench/g02-clock-per-tenant.bench.ts +158 -0
- package/src/bench/g03-signal-once.bench.ts +157 -0
- package/src/bench/g03-signal-reconnect.bench.ts +254 -0
- package/src/bench/g03-signal-sse-memory.bench.ts +191 -0
- package/src/bench/g04-auth-vault-hotpath.bench.ts +170 -0
- package/src/bench/g05-sustained-full.bench.ts +265 -0
- package/src/bench/g06-mixed-load.bench.ts +260 -0
- package/src/bench/g07-vault-crypto.bench.ts +100 -0
- package/src/bench/g07-vault-rotate-under-read.bench.ts +285 -0
- package/src/bench/g08-conn-oversubscribe.bench.ts +194 -0
- package/src/bench/g08-store-kv-durable.bench.ts +133 -0
- package/src/bench/g08-store-sql.bench.ts +178 -0
- package/src/bench/g09-journal-sustained.bench.ts +203 -0
- package/src/bench/g10-observability-contention.bench.ts +246 -0
- package/src/bench/g11-cold-start-cycle.bench.ts +164 -0
- package/src/bench/g13-elements.bench.ts +427 -0
- package/src/bench/g14-graceful-shutdown.bench.ts +244 -0
- package/src/bench/g15-postgres-degradation.bench.ts +264 -0
- package/src/bench/g16-live-query-fanout.bench.ts +206 -0
- package/src/bench/lib/event-loop-lag.ts +26 -0
- package/src/bench/lib/infra.ts +60 -0
- package/src/bench/lib/report.ts +52 -0
- package/src/bench/lib/rss-sampler.ts +61 -0
- package/src/bench/lib/signal-pg.ts +88 -0
- package/src/bench/load-app.ts +337 -0
- package/src/bench/load-child.ts +108 -0
- package/src/bench/smoke.bench.ts +43 -0
- package/src/cli/competitor-mention-removal.test.ts +28 -0
- package/src/cli/doctor-fd.ts +117 -0
- package/src/cli/doctor.test.ts +192 -0
- package/src/cli/doctor.ts +129 -1
- package/src/client/create.ts +95 -1
- package/src/client/index.ts +9 -2
- package/src/client/transport.ts +11 -4
- package/src/client/use-live-query.ts +154 -0
- package/src/client-react/index.ts +15 -1
- package/src/client-react/live-resource.ts +246 -0
- package/src/client-react/use-live-query.test.ts +475 -0
- package/src/client-react/use-live-query.ts +530 -0
- package/src/compiler/extract.test.ts +518 -0
- package/src/compiler/extract.ts +386 -19
- package/src/console/server/invoke-user-flow.ts +2 -1
- package/src/console/ui-next/dist/assets/{access-page-DnWbnGzq.js → access-page-De7Lc2JC.js} +1 -1
- package/src/console/ui-next/dist/assets/{flows-page-BiZ4-6yQ.js → flows-page-RGy7VEA_.js} +1 -1
- package/src/console/ui-next/dist/assets/{index-C8NRK2R-.js → index-_rgpdVzo.js} +3 -3
- package/src/console/ui-next/dist/assets/{observability-page-CrB6vd1T.js → observability-page-Ds6pcnh-.js} +1 -1
- package/src/console/ui-next/dist/assets/{store-page-CS5-aETQ.js → store-page-02xOiqIK.js} +3 -3
- package/src/console/ui-next/dist/assets/{units-page-CjtdlW8l.js → units-page-4rHOePuE.js} +1 -1
- package/src/console/ui-next/dist/assets/{vault-page-C6Xxm9SA.js → vault-page-DISPgxLM.js} +1 -1
- package/src/console/ui-next/dist/index.html +1 -1
- package/src/console/ui-next/src/features/store/lib/fields-from-table.ts +36 -2
- package/src/drivers/cdc-outbox.ts +389 -0
- package/src/drivers/memory.ts +20 -0
- package/src/drivers/oauth-apple.ts +156 -0
- package/src/drivers/oauth-discord.ts +79 -0
- package/src/drivers/oauth-facebook.ts +80 -0
- package/src/drivers/oauth-figma.ts +116 -0
- package/src/drivers/oauth-github.ts +92 -0
- package/src/drivers/oauth-google.ts +142 -0
- package/src/drivers/oauth-microsoft.ts +174 -0
- package/src/drivers/oauth-oidc.ts +293 -0
- package/src/drivers/oauth-shared.ts +326 -0
- package/src/drivers/oauth-types.ts +159 -0
- package/src/drivers/oauth-x.ts +77 -0
- package/src/drivers/oauth2-common.ts +95 -0
- package/src/drivers/oauth2-token.ts +61 -0
- package/src/drivers/pg-rls-row-passes.ts +251 -0
- package/src/drivers/pg-rls.ts +2 -0
- package/src/drivers/postgres.ts +45 -2
- package/src/drivers/signal-postgres.ts +2 -1
- package/src/elements/channel/runtime.ts +29 -2
- package/src/elements/channel.test.ts +52 -0
- package/src/elements/gate/boot.ts +29 -2
- package/src/elements/store/emit-drizzle.ts +147 -14
- package/src/elements/store/field-ddl.test.ts +118 -0
- package/src/elements/store/field-types.test.ts +455 -0
- package/src/elements/store/list-query.golden.json +777 -0
- package/src/elements/store/list-query.parity.test.ts +396 -0
- package/src/elements/store/list-query.ts +792 -0
- package/src/elements/store/live-default.test.ts +136 -0
- package/src/elements/store/live-http.test.ts +160 -0
- package/src/elements/store/live-isolation.test.ts +291 -0
- package/src/elements/store/live-query-runtime.test.ts +323 -0
- package/src/elements/store/live-query-runtime.ts +403 -0
- package/src/elements/store/live-query-server.test.ts +377 -0
- package/src/elements/store/live-query-server.ts +102 -0
- package/src/elements/store/live-query.ts +97 -0
- package/src/elements/store/resource.ts +189 -680
- package/src/elements/store/rls-row-passes-policies.parity.test.ts +665 -0
- package/src/elements/store/schema-decl.ts +539 -41
- package/src/elements/store/sql-rls-stamp.test.ts +27 -0
- package/src/elements/store/sql-session.ts +297 -35
- package/src/elements/store/table.ts +102 -21
- package/src/elements/store.test.ts +3 -1
- package/src/elements/store.ts +12 -1
- package/src/elements/vault/chaos-child.ts +74 -1
- package/src/elements/vault/chaos.test.ts +4 -2
- package/src/elements/vault/storage.ts +4 -2
- package/src/index.ts +5 -2
- package/src/kernel/app-auth.ts +1 -0
- package/src/kernel/app.ts +116 -2
- package/src/kernel/auth-sharing.test.ts +196 -0
- package/src/kernel/boot.test.ts +3 -3
- package/src/kernel/errors.ts +8 -0
- package/src/kernel/fx.test.ts +1 -0
- package/src/kernel/fx.ts +14 -2
- package/src/kernel/horizontal-child.ts +2 -1
- package/src/kernel/http-resource.ts +33 -7
- package/src/kernel/identity-host-persist.test.ts +119 -0
- package/src/kernel/instance-id.ts +4 -2
- package/src/kernel/journal.ts +2 -1
- package/src/kernel/mcp-tool.test.ts +95 -0
- package/src/kernel/on.ts +9 -0
- package/src/kernel/realtime-bind.ts +326 -0
- package/src/kernel/resource-live.ts +117 -0
- package/src/kernel/triggers.ts +86 -4
- package/src/manifest/diff.ts +37 -0
- package/src/manifest/types.ts +64 -2
- package/src/okid.bench.test.ts +64 -0
- package/src/okid.test.ts +338 -0
- package/src/okid.ts +245 -0
- package/src/plugins/anonymous.ts +19 -1
- package/src/plugins/auth/shared.ts +15 -0
- package/src/plugins/index.ts +2 -0
- package/src/plugins/magic-link.ts +10 -8
- package/src/plugins/mcp-oauth.ts +208 -0
- package/src/plugins/oauth/flow-store.ts +117 -0
- package/src/plugins/oauth/link.ts +69 -0
- package/src/plugins/oauth/shared.ts +108 -0
- package/src/plugins/oauth/token-vault.ts +100 -0
- package/src/plugins/oauth.security.test.ts +535 -0
- package/src/plugins/oauth.ts +532 -0
- package/src/plugins/otp.ts +48 -6
- package/src/plugins/passkey.ts +20 -1
- package/src/plugins/two-factor.ts +11 -0
- package/src/plugins/username.ts +40 -7
- package/src/release/build-lib.ts +7 -1
- package/src/release/measure.ts +1 -0
- package/src/release/official-plugins.ts +4 -1
- package/src/runs/collect.ts +2 -1
- package/src/runs/drivers/files.ts +2 -1
- package/src/test/create-test-app.ts +114 -5
- package/src/test/export-bundle.test.ts +33 -0
- package/src/test/live-signals.test.ts +83 -0
- package/src/test/tenant-isolation.test.ts +175 -0
- package/src/testing.ts +26 -0
- package/src/upgrade/codemods.ts +1 -1
- package/site/content/docs/reference/migrating-environments.mdx +0 -158
package/src/compiler/extract.ts
CHANGED
|
@@ -81,6 +81,8 @@ type DeclaredSchemaTable = {
|
|
|
81
81
|
readonly rls?: boolean;
|
|
82
82
|
readonly policies?: Record<string, TablePolicy>;
|
|
83
83
|
readonly tenantScoped?: boolean;
|
|
84
|
+
/** `false` when `store.schema.live(false)` opted out (project flag on). */
|
|
85
|
+
readonly live?: boolean;
|
|
84
86
|
};
|
|
85
87
|
|
|
86
88
|
interface ProjectScope {
|
|
@@ -111,11 +113,30 @@ interface ProjectScope {
|
|
|
111
113
|
i18n?: Manifest["i18n"];
|
|
112
114
|
topology?: Manifest["topology"];
|
|
113
115
|
images?: Record<string, string>;
|
|
116
|
+
/** Store-wide live defaulting — `oke({ store: { live: true } })`. */
|
|
117
|
+
store?: Manifest["store"];
|
|
114
118
|
flows: Record<string, Flow>;
|
|
115
119
|
/** Export name → flow id (for agent tools). */
|
|
116
120
|
flowExports: Map<string, string>;
|
|
117
121
|
/** Local binding name → store.resource declaration (for on(http.resource)). */
|
|
118
|
-
resources: Map<
|
|
122
|
+
resources: Map<
|
|
123
|
+
string,
|
|
124
|
+
{
|
|
125
|
+
storeName: string;
|
|
126
|
+
storeRef: string;
|
|
127
|
+
breaking?: boolean;
|
|
128
|
+
/** Physical table name (second argument or schema-table binding ref). */
|
|
129
|
+
tableName?: string;
|
|
130
|
+
/**
|
|
131
|
+
* Raw `live` option on `store.resource(…, { live })` — `undefined`
|
|
132
|
+
* when omitted. Resolved against the project-wide `store.live` flag
|
|
133
|
+
* and the table's `live: false` opt-out in pass 2.
|
|
134
|
+
*/
|
|
135
|
+
liveOpt?: boolean;
|
|
136
|
+
/** `true` when the resource carries a live query surface. */
|
|
137
|
+
live?: boolean;
|
|
138
|
+
}
|
|
139
|
+
>;
|
|
119
140
|
/** Local binding name → medium, for `const x = channel.email(…)` binders. */
|
|
120
141
|
channelMediumBindings: Map<string, ChannelMedium>;
|
|
121
142
|
}
|
|
@@ -209,12 +230,14 @@ export async function extractManifest(options: ExtractManifestOptions = {}): Pro
|
|
|
209
230
|
if (Object.keys(journeys).length > 0) manifest.journeys = journeys;
|
|
210
231
|
if (Object.keys(drivers).length > 0) manifest.drivers = drivers;
|
|
211
232
|
if (scope.tenancy) manifest.tenancy = scope.tenancy;
|
|
233
|
+
if (scope.store && scope.store.live === true) manifest.store = { live: true };
|
|
212
234
|
if (scope.i18n) manifest.i18n = scope.i18n;
|
|
213
235
|
if (scope.topology) manifest.topology = scope.topology;
|
|
214
236
|
if (scope.images) manifest.images = scope.images;
|
|
215
237
|
|
|
216
238
|
applyTenancyDefaults(manifest);
|
|
217
239
|
assertTenantSafeSchema(manifest);
|
|
240
|
+
applyLiveDefaults(manifest, scope);
|
|
218
241
|
|
|
219
242
|
return manifest;
|
|
220
243
|
}
|
|
@@ -271,6 +294,7 @@ function collectDeclarations(filePath: string, program: AstNode, scope: ProjectS
|
|
|
271
294
|
const name = stringProp(opts, "name");
|
|
272
295
|
if (name) scope.app = name;
|
|
273
296
|
applyOkeTenantConfig(opts, scope);
|
|
297
|
+
applyOkeStoreLiveConfig(opts, scope);
|
|
274
298
|
});
|
|
275
299
|
}
|
|
276
300
|
|
|
@@ -470,23 +494,26 @@ function tableFromDeclared(t: DeclaredSchemaTable): {
|
|
|
470
494
|
rls?: boolean;
|
|
471
495
|
policies?: Record<string, TablePolicy>;
|
|
472
496
|
tenantScoped?: boolean;
|
|
497
|
+
live?: boolean;
|
|
473
498
|
} {
|
|
474
499
|
return {
|
|
475
500
|
columns: t.columns,
|
|
476
501
|
...(t.rls ? { rls: true } : {}),
|
|
477
502
|
...(t.policies ? { policies: t.policies } : {}),
|
|
478
503
|
...(t.tenantScoped === false ? { tenantScoped: false } : {}),
|
|
504
|
+
...(t.live === false ? { live: false } : {}),
|
|
479
505
|
};
|
|
480
506
|
}
|
|
481
507
|
|
|
482
508
|
function parseSchemaTableExtras(
|
|
483
509
|
node: AstNode | undefined,
|
|
484
|
-
): Pick<DeclaredSchemaTable, "rls" | "policies" | "tenantScoped"> {
|
|
510
|
+
): Pick<DeclaredSchemaTable, "rls" | "policies" | "tenantScoped" | "live"> {
|
|
485
511
|
if (!node || node.type !== "ArrayExpression") return {};
|
|
486
512
|
const els = (node as AstNode & { elements?: AstNode[] }).elements ?? [];
|
|
487
513
|
const policies: Record<string, TablePolicy> = {};
|
|
488
514
|
let rls = false;
|
|
489
515
|
let tenantScoped: boolean | undefined;
|
|
516
|
+
let live: boolean | undefined;
|
|
490
517
|
for (const el of els) {
|
|
491
518
|
if (!el || el.type !== "CallExpression") continue;
|
|
492
519
|
const extra = parseSchemaTableExtra(el as CallExpression);
|
|
@@ -499,6 +526,10 @@ function parseSchemaTableExtras(
|
|
|
499
526
|
tenantScoped = false;
|
|
500
527
|
continue;
|
|
501
528
|
}
|
|
529
|
+
if (extra.kind === "live") {
|
|
530
|
+
live = false;
|
|
531
|
+
continue;
|
|
532
|
+
}
|
|
502
533
|
policies[extra.name] = extra.policy;
|
|
503
534
|
rls = true;
|
|
504
535
|
}
|
|
@@ -506,6 +537,7 @@ function parseSchemaTableExtras(
|
|
|
506
537
|
...(rls ? { rls: true } : {}),
|
|
507
538
|
...(Object.keys(policies).length > 0 ? { policies } : {}),
|
|
508
539
|
...(tenantScoped === false ? { tenantScoped: false } : {}),
|
|
540
|
+
...(live === false ? { live: false } : {}),
|
|
509
541
|
};
|
|
510
542
|
}
|
|
511
543
|
|
|
@@ -514,6 +546,7 @@ function parseSchemaTableExtra(
|
|
|
514
546
|
):
|
|
515
547
|
| { kind: "rls" }
|
|
516
548
|
| { kind: "unscoped" }
|
|
549
|
+
| { kind: "live" }
|
|
517
550
|
| { kind: "policy"; name: string; policy: TablePolicy }
|
|
518
551
|
| undefined {
|
|
519
552
|
const callee = call.callee;
|
|
@@ -526,6 +559,9 @@ function parseSchemaTableExtra(
|
|
|
526
559
|
if (method === "unscoped" && isStoreSchemaCallee(member.object)) {
|
|
527
560
|
return { kind: "unscoped" };
|
|
528
561
|
}
|
|
562
|
+
if (method === "live" && isStoreSchemaCallee(member.object)) {
|
|
563
|
+
return { kind: "live" };
|
|
564
|
+
}
|
|
529
565
|
if (method === "policy" && isStoreSchemaCallee(member.object)) {
|
|
530
566
|
const name = stringArg(call.arguments[0]);
|
|
531
567
|
if (!name) return undefined;
|
|
@@ -610,19 +646,63 @@ function parseDeclaredColumns(obj: AstNode): Record<string, DeclaredColumn> {
|
|
|
610
646
|
return out;
|
|
611
647
|
}
|
|
612
648
|
|
|
649
|
+
/**
|
|
650
|
+
* Abstract `field.*` SQL type heads recognized by the compiler.
|
|
651
|
+
*/
|
|
652
|
+
const FIELD_SQL_TYPES: ReadonlySet<string> = new Set([
|
|
653
|
+
"text",
|
|
654
|
+
"varchar",
|
|
655
|
+
"char",
|
|
656
|
+
"boolean",
|
|
657
|
+
"smallint",
|
|
658
|
+
"integer",
|
|
659
|
+
"bigint",
|
|
660
|
+
"serial",
|
|
661
|
+
"smallserial",
|
|
662
|
+
"bigserial",
|
|
663
|
+
"numeric",
|
|
664
|
+
"decimal",
|
|
665
|
+
"real",
|
|
666
|
+
"doublePrecision",
|
|
667
|
+
"json",
|
|
668
|
+
"jsonb",
|
|
669
|
+
"uuid",
|
|
670
|
+
"time",
|
|
671
|
+
"timestamp",
|
|
672
|
+
"date",
|
|
673
|
+
"interval",
|
|
674
|
+
"point",
|
|
675
|
+
"line",
|
|
676
|
+
"bytea",
|
|
677
|
+
"inet",
|
|
678
|
+
"cidr",
|
|
679
|
+
"macaddr",
|
|
680
|
+
"macaddr8",
|
|
681
|
+
]);
|
|
682
|
+
|
|
683
|
+
/** Manifest primitive for a field head (`decimal` collapses into `numeric`). */
|
|
684
|
+
function manifestFieldType(head: string): DeclaredColumn["type"] {
|
|
685
|
+
return head === "decimal" ? "numeric" : (head as DeclaredColumn["type"]);
|
|
686
|
+
}
|
|
687
|
+
|
|
613
688
|
/**
|
|
614
689
|
* Walk `field.text().notNull().pii()` (and similar) into a DeclaredColumn.
|
|
615
690
|
*
|
|
691
|
+
* Recognizes every {@link FIELD_SQL_TYPES} head plus the option bag passed to
|
|
692
|
+
* the factory call (`field.varchar({ length: 256 })`,
|
|
693
|
+
* `field.timestamp({ mode: "date", precision: 6 })`, …).
|
|
694
|
+
*
|
|
616
695
|
* @param node - AST node for the column value
|
|
617
696
|
* @param key - JS key (for default sqlName)
|
|
618
697
|
*/
|
|
619
698
|
function parseFieldChain(node: AstNode, key: string): DeclaredColumn | undefined {
|
|
620
699
|
const chain: string[] = [];
|
|
621
|
-
let sqlType: "
|
|
700
|
+
let sqlType: DeclaredColumn["type"];
|
|
622
701
|
let sqlName: string | undefined;
|
|
623
702
|
let description: string | undefined;
|
|
624
703
|
let defaultValue: string | number | boolean | null | undefined;
|
|
625
704
|
let hasDefault = false;
|
|
705
|
+
let optionsNode: AstNode | undefined;
|
|
626
706
|
let cur: AstNode | undefined = node;
|
|
627
707
|
|
|
628
708
|
while (cur && cur.type === "CallExpression") {
|
|
@@ -639,7 +719,7 @@ function parseFieldChain(node: AstNode, key: string): DeclaredColumn | undefined
|
|
|
639
719
|
if (method === "describe") {
|
|
640
720
|
description = stringArg(call.arguments[0]) ?? description;
|
|
641
721
|
}
|
|
642
|
-
if (method === "defaultFn") {
|
|
722
|
+
if (method === "defaultFn" || method === "okid" || method === "now") {
|
|
643
723
|
hasDefault = true;
|
|
644
724
|
defaultValue = defaultValue ?? null;
|
|
645
725
|
}
|
|
@@ -669,30 +749,29 @@ function parseFieldChain(node: AstNode, key: string): DeclaredColumn | undefined
|
|
|
669
749
|
break;
|
|
670
750
|
}
|
|
671
751
|
|
|
672
|
-
// Remaining should be field
|
|
752
|
+
// Remaining should be field.<type> — bare MemberExpression or factory call
|
|
753
|
+
// with an options object (field.varchar({ length: 256 })).
|
|
673
754
|
if (cur && cur.type === "MemberExpression") {
|
|
674
755
|
const member = cur as AstNode & { object: AstNode; property: AstNode };
|
|
675
756
|
if (identifierName(member.object) === "field") {
|
|
676
757
|
const t = identifierName(member.property);
|
|
677
|
-
if (t
|
|
758
|
+
if (t && FIELD_SQL_TYPES.has(t)) sqlType = manifestFieldType(t);
|
|
678
759
|
}
|
|
679
760
|
}
|
|
680
761
|
|
|
681
762
|
// Also: field.text() ends as CallExpression with callee MemberExpression field.text
|
|
682
763
|
if (!sqlType && node.type === "CallExpression") {
|
|
683
|
-
// Re-walk to find field.text() / field.integer() as a call
|
|
684
764
|
let probe: AstNode | undefined = node;
|
|
685
765
|
while (probe && probe.type === "CallExpression") {
|
|
686
766
|
const call = probe as CallExpression;
|
|
687
767
|
const callee = call.callee;
|
|
688
768
|
if (callee.type === "MemberExpression") {
|
|
689
769
|
const member = callee as AstNode & { object: AstNode; property: AstNode };
|
|
690
|
-
|
|
691
|
-
|
|
692
|
-
(
|
|
693
|
-
|
|
694
|
-
|
|
695
|
-
sqlType = identifierName(member.property) as "text" | "integer";
|
|
770
|
+
const head = identifierName(member.property);
|
|
771
|
+
if (identifierName(member.object) === "field" && head && FIELD_SQL_TYPES.has(head)) {
|
|
772
|
+
sqlType = manifestFieldType(head);
|
|
773
|
+
const arg0 = call.arguments[0];
|
|
774
|
+
if (arg0?.type === "ObjectExpression") optionsNode = arg0;
|
|
696
775
|
break;
|
|
697
776
|
}
|
|
698
777
|
probe = member.object;
|
|
@@ -715,6 +794,7 @@ function parseFieldChain(node: AstNode, key: string): DeclaredColumn | undefined
|
|
|
715
794
|
...(description !== undefined ? { description } : {}),
|
|
716
795
|
...(methods.has("pii") ? { pii: true } : {}),
|
|
717
796
|
...(methods.has("sensitive") ? { sensitive: true } : {}),
|
|
797
|
+
...parseFieldOptions(optionsNode),
|
|
718
798
|
};
|
|
719
799
|
if (methods.has("retain")) {
|
|
720
800
|
// retain("7y") — find the call in the original chain by re-walk
|
|
@@ -780,6 +860,49 @@ function parseReferencesArg(
|
|
|
780
860
|
return {};
|
|
781
861
|
}
|
|
782
862
|
|
|
863
|
+
/**
|
|
864
|
+
* Extract per-type options (`length`, `precision`, `scale`, `withTimezone`,
|
|
865
|
+
* `mode`, `fields`, `enumValues`) from a factory call's options object.
|
|
866
|
+
*
|
|
867
|
+
* @param node - Options object AST node (may be undefined)
|
|
868
|
+
*/
|
|
869
|
+
function parseFieldOptions(node: AstNode | undefined): Partial<DeclaredColumn> | undefined {
|
|
870
|
+
if (!node || node.type !== "ObjectExpression") return undefined;
|
|
871
|
+
const length = numberProp(node, "length");
|
|
872
|
+
const precision = numberProp(node, "precision");
|
|
873
|
+
const scale = numberProp(node, "scale");
|
|
874
|
+
const withTimezone = boolProp(node, "withTimezone");
|
|
875
|
+
const mode = stringProp(node, "mode");
|
|
876
|
+
const fields = stringProp(node, "fields");
|
|
877
|
+
const enumNode = objectProp(node, "enum");
|
|
878
|
+
const enumValues =
|
|
879
|
+
enumNode?.type === "ArrayExpression"
|
|
880
|
+
? ((enumNode as AstNode & { elements?: AstNode[] }).elements ?? [])
|
|
881
|
+
.map((el) => stringArg(el))
|
|
882
|
+
.filter((v): v is string => v !== undefined)
|
|
883
|
+
: undefined;
|
|
884
|
+
if (
|
|
885
|
+
length === undefined &&
|
|
886
|
+
precision === undefined &&
|
|
887
|
+
scale === undefined &&
|
|
888
|
+
withTimezone === undefined &&
|
|
889
|
+
mode === undefined &&
|
|
890
|
+
fields === undefined &&
|
|
891
|
+
(enumValues === undefined || enumValues.length === 0)
|
|
892
|
+
) {
|
|
893
|
+
return undefined;
|
|
894
|
+
}
|
|
895
|
+
return {
|
|
896
|
+
...(length !== undefined ? { length } : {}),
|
|
897
|
+
...(precision !== undefined ? { precision } : {}),
|
|
898
|
+
...(scale !== undefined ? { scale } : {}),
|
|
899
|
+
...(withTimezone !== undefined ? { withTimezone } : {}),
|
|
900
|
+
...(mode !== undefined ? { mode } : {}),
|
|
901
|
+
...(fields !== undefined ? { fields } : {}),
|
|
902
|
+
...(enumValues !== undefined && enumValues.length > 0 ? { enumValues } : {}),
|
|
903
|
+
};
|
|
904
|
+
}
|
|
905
|
+
|
|
783
906
|
function camelToSnakeKey(key: string): string {
|
|
784
907
|
return key
|
|
785
908
|
.replace(/([a-z0-9])([A-Z])/g, "$1_$2")
|
|
@@ -852,11 +975,27 @@ function visitDeclarationCall(call: CallExpression, program: AstNode, scope: Pro
|
|
|
852
975
|
const storeName = ref.split(":")[1] ?? dbName ?? "store";
|
|
853
976
|
const opts = objectArg(call.arguments[2]);
|
|
854
977
|
const breaking = opts ? boolProp(opts, "breaking") === true : false;
|
|
978
|
+
// Table name: the second argument may be a schema-table binding
|
|
979
|
+
// (resolve to its declared name) or a bare string literal.
|
|
980
|
+
const tableArgName = identifierName(call.arguments[1]);
|
|
981
|
+
const tableName =
|
|
982
|
+
(tableArgName
|
|
983
|
+
? scope.bindings.get(tableArgName)?.kind === "table"
|
|
984
|
+
? scope.bindings.get(tableArgName)?.ref
|
|
985
|
+
: undefined
|
|
986
|
+
: undefined) ?? stringArg(call.arguments[1]);
|
|
855
987
|
if (bindingName) {
|
|
988
|
+
// Raw declaration: `{ live: true }` opts in, `{ live: false }`
|
|
989
|
+
// opts out of the project-wide default, omitted defers to the
|
|
990
|
+
// project flag resolved in pass 2 (scope/store live stamp complete
|
|
991
|
+
// and `schemaTables` populated regardless of file order).
|
|
992
|
+
const liveOpt = opts ? boolProp(opts, "live") : undefined;
|
|
856
993
|
scope.resources.set(bindingName, {
|
|
857
994
|
storeName,
|
|
858
995
|
storeRef: ref,
|
|
859
996
|
...(breaking ? { breaking: true } : {}),
|
|
997
|
+
...(tableName !== undefined ? { tableName } : {}),
|
|
998
|
+
...(liveOpt !== undefined ? { liveOpt } : {}),
|
|
860
999
|
});
|
|
861
1000
|
}
|
|
862
1001
|
}
|
|
@@ -1584,7 +1723,21 @@ function registerFlow(args: {
|
|
|
1584
1723
|
flow.source = `${args.file.path}:${line}`;
|
|
1585
1724
|
|
|
1586
1725
|
if (boolProp(opts, "durable")) flow.durable = true;
|
|
1587
|
-
if (typeof liveFromTrigger === "string")
|
|
1726
|
+
if (typeof liveFromTrigger === "string") {
|
|
1727
|
+
flow.live = liveFromTrigger;
|
|
1728
|
+
// Manual live surface — `.live(tableBinding)` on the trigger. Synthesize
|
|
1729
|
+
// the internal signal exactly like `store.resource({ live: true })` so
|
|
1730
|
+
// the Manifest stays a faithful mirror of what booted, and enforce the
|
|
1731
|
+
// same guardrails (PK required; updatedAt / RLS warnings).
|
|
1732
|
+
const liveTable = trigger?.liveTable;
|
|
1733
|
+
if (liveTable !== undefined) {
|
|
1734
|
+
assertLiveGuardrails(args.scope, liveTable, `sql:${liveTable}`);
|
|
1735
|
+
args.scope.signals[`oke/live/sql:${liveTable}`] = {
|
|
1736
|
+
delivery: "live",
|
|
1737
|
+
description: `Live query CDC fan-out for ${liveTable} (synthesized by .live(table))`,
|
|
1738
|
+
};
|
|
1739
|
+
}
|
|
1740
|
+
}
|
|
1588
1741
|
if (boolProp(opts, "breaking")) flow.breaking = true;
|
|
1589
1742
|
|
|
1590
1743
|
const slo = parseSlo(objectProp(opts, "slo"));
|
|
@@ -1787,6 +1940,19 @@ function registerResourceMount(
|
|
|
1787
1940
|
}
|
|
1788
1941
|
const resource = baseName ? scope.resources.get(baseName) : undefined;
|
|
1789
1942
|
|
|
1943
|
+
// Resolve the table's live posture once: an explicit `{ live: false }`
|
|
1944
|
+
// opts out, an explicit `{ live: true }` opts in, and an omitted `live`
|
|
1945
|
+
// defers to the project-wide `store.live` flag unless the table itself
|
|
1946
|
+
// declared `store.schema.live(false)`. The raw option stays on the
|
|
1947
|
+
// resource; this pass-2 resolution relies on the fully-populated scope
|
|
1948
|
+
// (project flag stamped in pass 1, `schemaTables` regardless of file order).
|
|
1949
|
+
const resourceLive =
|
|
1950
|
+
resource?.liveOpt !== undefined
|
|
1951
|
+
? resource.liveOpt
|
|
1952
|
+
: resource?.tableName !== undefined &&
|
|
1953
|
+
scope.store?.live === true &&
|
|
1954
|
+
scope.schemaTables.get(resource.tableName)?.live !== false;
|
|
1955
|
+
|
|
1790
1956
|
const storeRef: ResourceRef | undefined =
|
|
1791
1957
|
resource?.storeRef !== undefined ? (resource.storeRef as ResourceRef) : undefined;
|
|
1792
1958
|
const effects: Effects | undefined = storeRef
|
|
@@ -1823,6 +1989,40 @@ function registerResourceMount(
|
|
|
1823
1989
|
scope.bindings.set(name, { kind: "flow", ref: name });
|
|
1824
1990
|
}
|
|
1825
1991
|
|
|
1992
|
+
// Live query surface — synthesize GET <path>/live + the internal signal
|
|
1993
|
+
// exactly like `store.resource({ live: true })` does at runtime so the
|
|
1994
|
+
// Manifest stays a faithful mirror of what booted. Gate is the resolved
|
|
1995
|
+
// `resourceLive` (explicit opt-in/opt-out, else the project-wide
|
|
1996
|
+
// `store.live` flag minus the table's `live: false` opt-out). Requires a
|
|
1997
|
+
// statically known table name; runtime `resolveTableName` throws for
|
|
1998
|
+
// anything else, so an unresolvable table never boots a live surface either.
|
|
1999
|
+
if (resourceLive === true && resource !== undefined) {
|
|
2000
|
+
const tableName = resource.tableName;
|
|
2001
|
+
if (tableName !== undefined) {
|
|
2002
|
+
const resourceStoreRef = resource.storeRef;
|
|
2003
|
+
assertLiveGuardrails(scope, tableName, resourceStoreRef);
|
|
2004
|
+
const signalName = `oke/live/sql:${tableName}`;
|
|
2005
|
+
scope.signals[signalName] = {
|
|
2006
|
+
delivery: "live",
|
|
2007
|
+
description: `Live query CDC fan-out for ${tableName} (synthesized by store.resource live)`,
|
|
2008
|
+
};
|
|
2009
|
+
const flowName = `_live_${tableName}`;
|
|
2010
|
+
scope.flows[flowName] = {
|
|
2011
|
+
trigger: { http: { method: "GET", path: `${path}/live` } },
|
|
2012
|
+
effects: {
|
|
2013
|
+
reads: [
|
|
2014
|
+
(resourceStoreRef ?? sqlTableRef(tableName)) as ResourceRef,
|
|
2015
|
+
`signal:${signalName}` as ResourceRef,
|
|
2016
|
+
],
|
|
2017
|
+
},
|
|
2018
|
+
...(gates && gates.length > 0 ? { gates } : {}),
|
|
2019
|
+
...(resource.breaking ? { breaking: true } : {}),
|
|
2020
|
+
source: `${file.path}:${line}`,
|
|
2021
|
+
};
|
|
2022
|
+
scope.bindings.set(flowName, { kind: "flow", ref: flowName });
|
|
2023
|
+
}
|
|
2024
|
+
}
|
|
2025
|
+
|
|
1826
2026
|
const exportName = enclosingConstName(onCall, program);
|
|
1827
2027
|
if (exportName) {
|
|
1828
2028
|
scope.bindings.set(exportName, { kind: "flow", ref: baseName ?? exportName });
|
|
@@ -1942,8 +2142,10 @@ function registerNamedTableCrud(call: CallExpression, file: SourceFile, scope: P
|
|
|
1942
2142
|
interface ParsedTrigger {
|
|
1943
2143
|
trigger: Trigger;
|
|
1944
2144
|
gates?: string[];
|
|
1945
|
-
/** Live signal name from `.live(signal)` / `http.live(signal)`. */
|
|
2145
|
+
/** Live signal name from `.live(signal)` / `http.live(signal)` / `.live(table)`. */
|
|
1946
2146
|
live?: string;
|
|
2147
|
+
/** Physical table name when `.live(tableBinding)` — manual live surface. */
|
|
2148
|
+
liveTable?: string;
|
|
1947
2149
|
}
|
|
1948
2150
|
|
|
1949
2151
|
function parseTrigger(
|
|
@@ -1963,6 +2165,10 @@ function parseTrigger(
|
|
|
1963
2165
|
return { trigger: {} };
|
|
1964
2166
|
}
|
|
1965
2167
|
|
|
2168
|
+
// mcp.tool("bookings.create").gate(...) / mcp.tool(name)
|
|
2169
|
+
const mcpTool = parseMcpTrigger(call, scope);
|
|
2170
|
+
if (mcpTool) return mcpTool;
|
|
2171
|
+
|
|
1966
2172
|
// http.post("/x").gate(...) / http.get("/x").live(signal) / http.live(signal)
|
|
1967
2173
|
const http = parseHttpTrigger(call, scope, filePath);
|
|
1968
2174
|
if (http) return http;
|
|
@@ -1977,6 +2183,8 @@ function parseTrigger(
|
|
|
1977
2183
|
if (node.type === "MemberExpression") {
|
|
1978
2184
|
const http = parseHttpTrigger(node, scope, filePath);
|
|
1979
2185
|
if (http) return http;
|
|
2186
|
+
const mcp = parseMcpTrigger(node, scope);
|
|
2187
|
+
if (mcp) return mcp;
|
|
1980
2188
|
}
|
|
1981
2189
|
|
|
1982
2190
|
if (node.type === "Identifier") {
|
|
@@ -2004,6 +2212,19 @@ function resolveSignalName(node: AstNode | undefined, scope: ProjectScope): stri
|
|
|
2004
2212
|
return id;
|
|
2005
2213
|
}
|
|
2006
2214
|
|
|
2215
|
+
/**
|
|
2216
|
+
* Resolve a `.live(tableBinding)` argument — a `store.schema.table(...)`
|
|
2217
|
+
* (or `table(...)`) binding — to the internal live-signal name
|
|
2218
|
+
* `oke/live/sql:<table>`. Returns `undefined` for anything that is not a
|
|
2219
|
+
* table binding (signal handles resolve through {@link resolveSignalName}).
|
|
2220
|
+
*/
|
|
2221
|
+
function resolveLiveTableName(node: AstNode | undefined, scope: ProjectScope): string | undefined {
|
|
2222
|
+
const id = identifierName(node);
|
|
2223
|
+
if (!id) return undefined;
|
|
2224
|
+
const binding = scope.bindings.get(id);
|
|
2225
|
+
return binding?.kind === "table" ? binding.ref : undefined;
|
|
2226
|
+
}
|
|
2227
|
+
|
|
2007
2228
|
function parseHttpTrigger(
|
|
2008
2229
|
leaf: AstNode,
|
|
2009
2230
|
scope: ProjectScope,
|
|
@@ -2014,6 +2235,8 @@ function parseHttpTrigger(
|
|
|
2014
2235
|
let method: string | undefined;
|
|
2015
2236
|
let path: string | undefined;
|
|
2016
2237
|
let live: string | undefined;
|
|
2238
|
+
/** Table name when `.live(tableBinding)` — a manual live query surface. */
|
|
2239
|
+
let liveTable: string | undefined;
|
|
2017
2240
|
const gateNames: string[] = [];
|
|
2018
2241
|
|
|
2019
2242
|
for (const node of chain) {
|
|
@@ -2028,10 +2251,20 @@ function parseHttpTrigger(
|
|
|
2028
2251
|
if (obj.type === "Identifier" && (obj as Identifier).name === "http" && prop) {
|
|
2029
2252
|
if (prop === "live") {
|
|
2030
2253
|
method = "GET";
|
|
2031
|
-
const
|
|
2254
|
+
const tableName = resolveLiveTableName(c.arguments[0], scope);
|
|
2255
|
+
const signalName =
|
|
2256
|
+
tableName !== undefined
|
|
2257
|
+
? `oke/live/sql:${tableName}`
|
|
2258
|
+
: resolveSignalName(c.arguments[0], scope);
|
|
2032
2259
|
if (signalName) {
|
|
2033
2260
|
live = signalName;
|
|
2034
|
-
|
|
2261
|
+
if (tableName !== undefined) liveTable = tableName;
|
|
2262
|
+
// Keep any explicit GET path chained so far (e.g.
|
|
2263
|
+
// `http.get("/tasks/live").live(tasks)`); only synthesize the
|
|
2264
|
+
// firehose path for the bare `http.live(signal)` form.
|
|
2265
|
+
if (path === undefined) {
|
|
2266
|
+
path = `/_oke/live/${encodeURIComponent(signalName)}`;
|
|
2267
|
+
}
|
|
2035
2268
|
}
|
|
2036
2269
|
continue;
|
|
2037
2270
|
}
|
|
@@ -2049,8 +2282,15 @@ function parseHttpTrigger(
|
|
|
2049
2282
|
|
|
2050
2283
|
if (prop === "public") gateNames.push("public");
|
|
2051
2284
|
if (prop === "live") {
|
|
2052
|
-
const
|
|
2053
|
-
|
|
2285
|
+
const tableName = resolveLiveTableName(c.arguments[0], scope);
|
|
2286
|
+
const signalName =
|
|
2287
|
+
tableName !== undefined
|
|
2288
|
+
? `oke/live/sql:${tableName}`
|
|
2289
|
+
: resolveSignalName(c.arguments[0], scope);
|
|
2290
|
+
if (signalName) {
|
|
2291
|
+
live = signalName;
|
|
2292
|
+
if (tableName !== undefined) liveTable = tableName;
|
|
2293
|
+
}
|
|
2054
2294
|
}
|
|
2055
2295
|
}
|
|
2056
2296
|
}
|
|
@@ -2082,6 +2322,7 @@ function parseHttpTrigger(
|
|
|
2082
2322
|
},
|
|
2083
2323
|
gates: gateNames.length > 0 ? gateNames : undefined,
|
|
2084
2324
|
...(live !== undefined ? { live } : {}),
|
|
2325
|
+
...(liveTable !== undefined ? { liveTable } : {}),
|
|
2085
2326
|
};
|
|
2086
2327
|
}
|
|
2087
2328
|
|
|
@@ -2119,6 +2360,45 @@ function stripTriggerExtras(trigger: Trigger): Trigger {
|
|
|
2119
2360
|
return trigger;
|
|
2120
2361
|
}
|
|
2121
2362
|
|
|
2363
|
+
/**
|
|
2364
|
+
* `mcp.tool("name").gate(...)` — per-flow MCP tool opt-in. The Manifest
|
|
2365
|
+
* records `trigger.mcp: { name }` plus any chained gates; absence of the
|
|
2366
|
+
* trigger means the flow is not an MCP tool.
|
|
2367
|
+
*/
|
|
2368
|
+
function parseMcpTrigger(leaf: AstNode, scope: ProjectScope): ParsedTrigger | undefined {
|
|
2369
|
+
const chain = flattenMemberCallChain(leaf);
|
|
2370
|
+
let toolName: string | undefined;
|
|
2371
|
+
const gateNames: string[] = [];
|
|
2372
|
+
|
|
2373
|
+
for (const node of chain) {
|
|
2374
|
+
if (node.type !== "CallExpression") continue;
|
|
2375
|
+
const c = node as CallExpression;
|
|
2376
|
+
const callee = c.callee;
|
|
2377
|
+
if (callee.type !== "MemberExpression") continue;
|
|
2378
|
+
const member = callee as AstNode & { object: AstNode; property: AstNode };
|
|
2379
|
+
const prop = identifierName(member.property);
|
|
2380
|
+
if (
|
|
2381
|
+
prop === "tool" &&
|
|
2382
|
+
member.object.type === "Identifier" &&
|
|
2383
|
+
(member.object as Identifier).name === "mcp"
|
|
2384
|
+
) {
|
|
2385
|
+
toolName = stringArg(c.arguments[0]);
|
|
2386
|
+
continue;
|
|
2387
|
+
}
|
|
2388
|
+
if (prop === "gate") {
|
|
2389
|
+
for (const arg of c.arguments) {
|
|
2390
|
+
gateNames.push(...gateNamesFromArg(arg, scope));
|
|
2391
|
+
}
|
|
2392
|
+
}
|
|
2393
|
+
}
|
|
2394
|
+
|
|
2395
|
+
if (!toolName) return undefined;
|
|
2396
|
+
return {
|
|
2397
|
+
trigger: { mcp: { name: toolName } },
|
|
2398
|
+
gates: gateNames.length > 0 ? gateNames : undefined,
|
|
2399
|
+
};
|
|
2400
|
+
}
|
|
2401
|
+
|
|
2122
2402
|
function parseEffectsObject(node: AstNode | undefined): Effects | undefined {
|
|
2123
2403
|
if (!node) return undefined;
|
|
2124
2404
|
const effects: Effects = {};
|
|
@@ -2493,6 +2773,93 @@ function applyTenancyDefaults(manifest: Manifest): void {
|
|
|
2493
2773
|
}
|
|
2494
2774
|
}
|
|
2495
2775
|
|
|
2776
|
+
/**
|
|
2777
|
+
* Stamp the project-wide store-live default from `oke({ store: { live: true } })`.
|
|
2778
|
+
*
|
|
2779
|
+
* Mirrors {@link applyOkeTenantConfig} — the proven "opt-in flag flips the
|
|
2780
|
+
* default posture for NEW declarations" pattern. Only `live: true` flips the
|
|
2781
|
+
* default; `false` / omitted leaves today's explicit-only behavior untouched.
|
|
2782
|
+
*/
|
|
2783
|
+
function applyOkeStoreLiveConfig(opts: AstNode | undefined, scope: ProjectScope): void {
|
|
2784
|
+
const store = objectProp(opts, "store");
|
|
2785
|
+
const live = boolProp(store, "live");
|
|
2786
|
+
if (live !== true) return;
|
|
2787
|
+
scope.store = { ...scope.store, live: true };
|
|
2788
|
+
}
|
|
2789
|
+
|
|
2790
|
+
/**
|
|
2791
|
+
* Default every SQL table to live when `oke({ store: { live: true } })` is
|
|
2792
|
+
* on, unless the table explicitly opted out with `store.schema.live(false)`.
|
|
2793
|
+
*
|
|
2794
|
+
* The default flip is declaration ergonomics only: each live table is
|
|
2795
|
+
* recorded in the Manifest and its DX Pack A guardrails run through the exact
|
|
2796
|
+
* same {@link assertLiveGuardrails} path as an explicit `live: true` — no
|
|
2797
|
+
* separate, weaker code path. RLS-per-event fan-out cost is unchanged.
|
|
2798
|
+
*/
|
|
2799
|
+
function applyLiveDefaults(manifest: Manifest, scope: ProjectScope): void {
|
|
2800
|
+
if (manifest.store?.live !== true) return;
|
|
2801
|
+
for (const [storeName, store] of Object.entries(manifest.stores ?? {})) {
|
|
2802
|
+
if (store.facet !== "sql" || !store.tables) continue;
|
|
2803
|
+
for (const [tableName, table] of Object.entries(store.tables)) {
|
|
2804
|
+
if (table.live === false) continue;
|
|
2805
|
+
table.live = true;
|
|
2806
|
+
const signalName = `oke/live/sql:${tableName}`;
|
|
2807
|
+
if (manifest.signals?.[signalName] === undefined) {
|
|
2808
|
+
manifest.signals = manifest.signals ?? {};
|
|
2809
|
+
manifest.signals[signalName] = {
|
|
2810
|
+
delivery: "live",
|
|
2811
|
+
description: `Live query CDC fan-out for ${tableName} (synthesized by store.live project default)`,
|
|
2812
|
+
};
|
|
2813
|
+
}
|
|
2814
|
+
assertLiveGuardrails(scope, tableName, `sql:${storeName}`);
|
|
2815
|
+
}
|
|
2816
|
+
}
|
|
2817
|
+
}
|
|
2818
|
+
|
|
2819
|
+
/**
|
|
2820
|
+
* DX Pack A guardrails for live query surfaces (Realtime Hardening plan) —
|
|
2821
|
+
* enforced identically for `store.resource({ live: true })` and manual
|
|
2822
|
+
* `.live(table)` flows:
|
|
2823
|
+
*
|
|
2824
|
+
* - **error** — table has no primary key column (upsert/revoked/delete are
|
|
2825
|
+
* PK-addressed; without one classification cannot address rows).
|
|
2826
|
+
* - **warn** — no `updatedAt`/`updated_at` timestamp column (`rowVersion`
|
|
2827
|
+
* LWW guard unavailable; optimistic races rely on `mutationId` only).
|
|
2828
|
+
* - **warn** — table carries no RLS policies (live without RLS is
|
|
2829
|
+
* operator-bypass territory — confirm intent).
|
|
2830
|
+
*
|
|
2831
|
+
* Column information lives on the declared schema table; when the compiler
|
|
2832
|
+
* could not resolve columns statically (bare string table), checks skip.
|
|
2833
|
+
*
|
|
2834
|
+
* @param scope - Project scope (declared tables + resolved drivers)
|
|
2835
|
+
* @param tableName - Physical SQL table name
|
|
2836
|
+
* @param storeRef - ResourceRef of the owning store (`sql:<name>`)
|
|
2837
|
+
*/
|
|
2838
|
+
function assertLiveGuardrails(scope: ProjectScope, tableName: string, storeRef: string): void {
|
|
2839
|
+
const declared = scope.schemaTables.get(tableName);
|
|
2840
|
+
if (!declared) return;
|
|
2841
|
+
const columns = Object.values(declared.columns);
|
|
2842
|
+
if (columns.length > 0 && !columns.some((c) => c.primaryKey === true)) {
|
|
2843
|
+
throw new Error(
|
|
2844
|
+
`extract: live: true on table "${tableName}" requires a primary key column (upsert/revoked/delete address rows by PK)`,
|
|
2845
|
+
);
|
|
2846
|
+
}
|
|
2847
|
+
const hasUpdatedAt = columns.some((c) => c.sqlName === "updated_at" || c.sqlName === "updatedAt");
|
|
2848
|
+
if (columns.length > 0 && !hasUpdatedAt) {
|
|
2849
|
+
console.warn(
|
|
2850
|
+
`[oke extract] warn: live: true on "${tableName}" has no updatedAt/updated_at column — rowVersion LWW guard unavailable; optimistic races rely on mutationId`,
|
|
2851
|
+
);
|
|
2852
|
+
}
|
|
2853
|
+
if (
|
|
2854
|
+
(declared.rls !== true || Object.keys(declared.policies ?? {}).length === 0) &&
|
|
2855
|
+
storeRef.startsWith("sql:")
|
|
2856
|
+
) {
|
|
2857
|
+
console.warn(
|
|
2858
|
+
`[oke extract] warn: live: true on "${tableName}" without RLS policies — every gated subscriber sees every row; confirm this is intended`,
|
|
2859
|
+
);
|
|
2860
|
+
}
|
|
2861
|
+
}
|
|
2862
|
+
|
|
2496
2863
|
function assertTenantSafeSchema(manifest: Manifest): void {
|
|
2497
2864
|
if (manifest.tenancy?.enabled !== true) return;
|
|
2498
2865
|
for (const [storeName, store] of Object.entries(manifest.stores ?? {})) {
|
|
@@ -16,6 +16,7 @@ import { isFlowFailure } from "../../kernel/hooks.ts";
|
|
|
16
16
|
import type { Trigger } from "../../kernel/triggers.ts";
|
|
17
17
|
import type { RlsIdentity } from "../../drivers/pg-rls.ts";
|
|
18
18
|
import { resolveRlsIdentity } from "../../elements/store.ts";
|
|
19
|
+
import { okid } from "../../okid.ts";
|
|
19
20
|
import type { Manifest } from "../../manifest/types.ts";
|
|
20
21
|
import type { ConsoleIdentity } from "./state.ts";
|
|
21
22
|
|
|
@@ -361,7 +362,7 @@ export function bindHostInvokeUserFlow(app: InvokeHostApp): ConsoleInvokeUserFlo
|
|
|
361
362
|
...(input.body !== undefined ? { body: input.body } : {}),
|
|
362
363
|
});
|
|
363
364
|
|
|
364
|
-
const runId =
|
|
365
|
+
const runId = okid();
|
|
365
366
|
const result = await app.execute(flowDef, assembled, trigger, {
|
|
366
367
|
trustedInvoke: true,
|
|
367
368
|
runId,
|