okengine 0.13.0 → 0.15.2
This diff represents the content of publicly available package versions that have been released to one of the supported registries. The information contained in this diff is provided for informational purposes only and reflects changes between package versions as they appear in their respective public registries.
- package/manifest.v1.schema.json +7 -1
- package/package.json +4 -5
- package/site/content/docs/ai/mcp.mdx +27 -2
- package/site/content/docs/elements/ai.mdx +59 -11
- package/site/content/docs/elements/channel.mdx +1 -0
- package/site/content/docs/elements/clock.mdx +30 -12
- package/site/content/docs/elements/flow.mdx +9 -3
- package/site/content/docs/elements/gate.mdx +25 -22
- package/site/content/docs/elements/signal.mdx +71 -25
- package/site/content/docs/elements/store.mdx +87 -28
- package/site/content/docs/elements/vault.mdx +1 -0
- package/site/content/docs/get-started/basic-usage.mdx +22 -13
- package/site/content/docs/get-started/index.mdx +5 -0
- package/site/content/docs/get-started/installation.mdx +2 -2
- package/site/content/docs/get-started/introduction.mdx +11 -3
- package/site/content/docs/get-started/meta.json +1 -1
- package/site/content/docs/get-started/project-structure.mdx +932 -0
- package/site/content/docs/reference/cli.md +13 -10
- package/site/content/docs/reference/client.mdx +25 -21
- package/site/content/docs/reference/fx.mdx +27 -22
- package/site/content/docs/reference/security.md +1 -1
- package/src/auth/bindings.ts +14 -5
- package/src/auth/cross-plane.ts +10 -3
- package/src/cli/ai-setup/ai-setup.test.ts +62 -0
- package/src/cli/ai-setup/apply.ts +32 -1
- package/src/cli/ai-setup/recommend.test.ts +25 -0
- package/src/cli/ai-setup/recommend.ts +8 -3
- package/src/cli/attach-host-console.test.ts +3 -2
- package/src/cli/attach-host-console.ts +3 -0
- package/src/cli/build.test.ts +4 -2
- package/src/cli/db-seed.ts +48 -15
- package/src/cli/db.test.ts +104 -0
- package/src/cli/db.ts +122 -10
- package/src/cli/dev-controls.test.ts +20 -0
- package/src/cli/dev-controls.ts +13 -0
- package/src/cli/dev.test.ts +15 -4
- package/src/cli/dev.ts +102 -29
- package/src/cli/ensure-drizzle-config.ts +2 -1
- package/src/cli/tui/DevLive.tsx +12 -2
- package/src/cli/tui/keys.test.ts +2 -1
- package/src/cli/tui/keys.ts +3 -2
- package/src/compiler/aot.test.ts +1 -1
- package/src/compiler/effects-infer.ts +58 -3
- package/src/compiler/extract.test.ts +458 -29
- package/src/compiler/extract.ts +387 -69
- package/src/compiler/flow-path.test.ts +96 -0
- package/src/compiler/flow-path.ts +173 -0
- package/src/compiler/generate-adopt.test.ts +112 -3
- package/src/compiler/generate-adopt.ts +280 -26
- package/src/compiler/response.ts +45 -1
- package/src/compiler/schema-from-ast.ts +609 -0
- package/src/console/server/ai.ts +5 -2
- package/src/console/server/bind.ts +2 -2
- package/src/console/server/console.test.ts +3 -0
- package/src/console/server/flows.ts +121 -40
- package/src/console/server/invoke-user-flow.test.ts +3 -1
- package/src/console/server/invoke-user-flow.ts +17 -2
- package/src/console/server/iso-at.test.ts +9 -0
- package/src/console/server/iso-at.ts +11 -0
- package/src/console/server/security-headers.ts +6 -3
- package/src/console/server/serve.ts +17 -2
- package/src/console/server/spa-proxy.test.ts +1 -0
- package/src/console/server/sql-catalog.ts +147 -7
- package/src/console/server/sql-rls.test.ts +16 -0
- package/src/console/server/state.ts +8 -0
- package/src/console/server/store.test.ts +7 -4
- package/src/console/server/store.ts +142 -58
- package/src/console/ui-next/dist/assets/agent-disclosure-C0X1fbWF.js +1 -0
- package/src/console/ui-next/dist/assets/cache-glyph-F1FI122b.js +1 -0
- package/src/console/ui-next/dist/assets/{call-pii-button-bqkxMrJH.js → call-pii-button-DEDCl_j3.js} +1 -1
- package/src/console/ui-next/dist/assets/collapsible-LPqGvfoz.js +1 -0
- package/src/console/ui-next/dist/assets/{confirm-sheet-DDCRmG62.js → confirm-sheet-3ptDzXbz.js} +1 -1
- package/src/console/ui-next/dist/assets/{duration-tone-oiRxPVsZ.js → duration-tone-sC3lGABz.js} +1 -1
- package/src/console/ui-next/dist/assets/{explorer-empty-HjTnVQoR.js → explorer-empty-BvYrygyO.js} +1 -1
- package/src/console/ui-next/dist/assets/flows-page-Dluo1Bd7.js +1 -0
- package/src/console/ui-next/dist/assets/{highlighted-json-CvDPvveV.js → highlighted-json-CS_O8L-r.js} +1 -1
- package/src/console/ui-next/dist/assets/http-method--sWDdXSB.js +1 -0
- package/src/console/ui-next/dist/assets/index-Ca3HZMVq.js +66 -0
- package/src/console/ui-next/dist/assets/index-UtTDRQpS.css +2 -0
- package/src/console/ui-next/dist/assets/{link-DF7SZ9Ek.js → link-COeyggt-.js} +1 -1
- package/src/console/ui-next/dist/assets/observability-page-HK9-BO8z.js +4 -0
- package/src/console/ui-next/dist/assets/react-D8E3mtu1.js +1 -0
- package/src/console/ui-next/dist/assets/replica-lag-DBIFFf7d.js +18 -0
- package/src/console/ui-next/dist/assets/sheet-form-Dnwa5oOR.js +1 -0
- package/src/console/ui-next/dist/assets/shortcut-keys-CoqwEIi0.js +1 -0
- package/src/console/ui-next/dist/assets/skeleton-BHcfDYcb.js +1 -0
- package/src/console/ui-next/dist/assets/store-page-BSohH3wM.js +41 -0
- package/src/console/ui-next/dist/assets/trace-detail-sheet-CFIGRnnA.js +2 -0
- package/src/console/ui-next/dist/assets/tree-expand-toggle-BgLl34w7.js +54 -0
- package/src/console/ui-next/dist/assets/units-page-B0cFE76A.js +1 -0
- package/src/console/ui-next/dist/assets/{use-vault-list-Cl79j_ku.js → use-vault-list-BLbZhzyF.js} +1 -1
- package/src/console/ui-next/dist/assets/vault-page-B1SbYe10.js +2 -0
- package/src/console/ui-next/dist/index.html +6 -6
- package/src/console/ui-next/seed-invoke-host.ts +2 -0
- package/src/console/ui-next/src/client.ts +35 -7
- package/src/console/ui-next/src/components/motion/table/index.tsx +16 -0
- package/src/console/ui-next/src/components/motion/table/types.ts +4 -0
- package/src/console/ui-next/src/components/ui/kbd.tsx +2 -2
- package/src/console/ui-next/src/features/flows/graph/build-flow-graph.test.ts +18 -0
- package/src/console/ui-next/src/features/flows/graph/build-flow-graph.ts +14 -1
- package/src/console/ui-next/src/features/flows/graph/neighborhood.test.ts +17 -0
- package/src/console/ui-next/src/features/flows/graph/neighborhood.ts +16 -3
- package/src/console/ui-next/src/features/flows/traces/effect-kind.ts +3 -1
- package/src/console/ui-next/src/features/flows/traces/effect-summary.ts +24 -0
- package/src/console/ui-next/src/features/flows/traces/http-method.ts +19 -0
- package/src/console/ui-next/src/features/flows/traces/trace-detail-sheet.tsx +9 -3
- package/src/console/ui-next/src/features/flows/traces/trace-detail.test.ts +29 -2
- package/src/console/ui-next/src/features/flows/traces/traces-pane.tsx +10 -3
- package/src/console/ui-next/src/features/observability/lib/ask-count.test.ts +25 -0
- package/src/console/ui-next/src/features/observability/lib/ask-count.ts +4 -1
- package/src/console/ui-next/src/features/store/detail/browse-section.tsx +45 -1
- package/src/console/ui-next/src/features/store/detail/resource-panel.tsx +78 -16
- package/src/console/ui-next/src/features/store/detail/store-row-detail-sheet.tsx +155 -14
- package/src/console/ui-next/src/features/store/explorer/store-tree.tsx +33 -133
- package/src/console/ui-next/src/features/store/files/file-preview.tsx +4 -1
- package/src/console/ui-next/src/features/store/grid/rls-policy-sheet.tsx +326 -165
- package/src/console/ui-next/src/features/store/grid/sql-insert-sheet.tsx +6 -0
- package/src/console/ui-next/src/features/store/grid/store-data-grid.tsx +50 -3
- package/src/console/ui-next/src/features/store/lib/files-meta.test.ts +3 -0
- package/src/console/ui-next/src/features/store/lib/files-meta.ts +12 -0
- package/src/console/ui-next/src/features/store/lib/query-defaults.test.ts +25 -0
- package/src/console/ui-next/src/features/store/lib/query-defaults.ts +85 -3
- package/src/console/ui-next/src/features/store/lib/query-gate.ts +30 -4
- package/src/console/ui-next/src/features/store/lib/rls-policy.test.ts +139 -0
- package/src/console/ui-next/src/features/store/lib/rls-policy.ts +223 -21
- package/src/console/ui-next/src/features/store/lib/schema-graph.ts +4 -0
- package/src/console/ui-next/src/features/store/lib/sql-catalog.test.ts +24 -0
- package/src/console/ui-next/src/features/store/lib/sql-catalog.ts +19 -0
- package/src/console/ui-next/src/features/store/lib/store-tree.test.ts +6 -0
- package/src/console/ui-next/src/features/store/lib/store-tree.ts +13 -0
- package/src/console/ui-next/src/features/store/query/query-console.tsx +48 -14
- package/src/console/ui-next/src/features/store/query/query-gate-menu.tsx +19 -20
- package/src/console/ui-next/src/features/store/query/query-gate-parts.tsx +2 -1
- package/src/console/ui-next/src/features/store/query/query-results.tsx +110 -83
- package/src/console/ui-next/src/features/store/schema/schema-table-node.tsx +17 -0
- package/src/console/ui-next/src/features/units/call/call-api-panel.tsx +112 -72
- package/src/console/ui-next/src/features/units/detail/effects-summary.tsx +12 -4
- package/src/console/ui-next/src/features/units/lib/contract-input.test.ts +206 -1
- package/src/console/ui-next/src/features/units/lib/contract-input.ts +55 -0
- package/src/console/ui-next/src/features/vault/lib/types.ts +1 -1
- package/src/docker/compose-health.test.ts +27 -0
- package/src/docker/compose-health.ts +19 -3
- package/src/docker/docker.test.ts +4 -1
- package/src/docker/dockerfile.ts +1 -1
- package/src/docker/recipes/pgdog.ts +2 -0
- package/src/drivers/ai-anthropic.ts +5 -0
- package/src/drivers/ai-ollama.ts +49 -30
- package/src/drivers/ai-openai-compatible.ts +57 -46
- package/src/drivers/ai-providers.test.ts +3 -0
- package/src/drivers/bun-native-completeness.test.ts +7 -9
- package/src/drivers/clock-postgres.ts +13 -9
- package/src/drivers/instances-postgres.ts +4 -6
- package/src/drivers/journal-postgres.ts +13 -9
- package/src/drivers/pg-rls.test.ts +87 -0
- package/src/drivers/pg-rls.ts +233 -7
- package/src/drivers/pglite.ts +26 -3
- package/src/drivers/postgres.test.ts +23 -0
- package/src/drivers/postgres.ts +133 -9
- package/src/drivers/redis.ts +11 -4
- package/src/drivers/signal-redis.ts +24 -14
- package/src/drivers/signal-types.ts +2 -1
- package/src/drivers/types.ts +10 -0
- package/src/elements/ai/declare.ts +109 -0
- package/src/elements/ai/errors.test.ts +5 -1
- package/src/elements/ai/errors.ts +30 -2
- package/src/elements/ai/eval.ts +4 -6
- package/src/elements/ai/mcp-client.test.ts +206 -0
- package/src/elements/ai/mcp-client.ts +362 -0
- package/src/elements/ai/mcp-http.ts +159 -0
- package/src/elements/ai/mcp-mock.ts +134 -0
- package/src/elements/ai/mcp-protocol.ts +234 -0
- package/src/elements/ai/mcp-stdio.test.ts +50 -0
- package/src/elements/ai/mcp-stdio.ts +212 -0
- package/src/elements/ai/mcp-transport.ts +70 -0
- package/src/elements/ai/runtime.ts +159 -29
- package/src/elements/ai.test.ts +139 -0
- package/src/elements/ai.ts +16 -0
- package/src/elements/clock/declare.ts +26 -1
- package/src/elements/clock/health.test.ts +43 -0
- package/src/elements/clock/runtime.ts +55 -2
- package/src/elements/clock/schedule.ts +71 -142
- package/src/elements/clock.test.ts +9 -40
- package/src/elements/clock.ts +6 -1
- package/src/elements/gate/boot.ts +5 -5
- package/src/elements/gate/declare.ts +36 -4
- package/src/elements/gate/flatten.ts +1 -1
- package/src/elements/gate/runtime.ts +1 -1
- package/src/elements/index.ts +2 -0
- package/src/elements/signal/declare.ts +2 -1
- package/src/elements/signal/runtime.ts +16 -1
- package/src/elements/signal.ts +1 -0
- package/src/elements/store/cache.test.ts +2 -0
- package/src/elements/store/cache.ts +3 -3
- package/src/elements/store/declare.ts +9 -6
- package/src/elements/store/emit-drizzle.ts +44 -8
- package/src/elements/store/resource.test.ts +1 -1
- package/src/elements/store/rls-identity.test.ts +95 -0
- package/src/elements/store/rls-identity.ts +116 -0
- package/src/elements/store/runtime.ts +4 -0
- package/src/elements/store/schema-decl.test.ts +37 -0
- package/src/elements/store/schema-decl.ts +161 -0
- package/src/elements/store/seed.ts +1 -1
- package/src/elements/store/sql-condition.test.ts +12 -0
- package/src/elements/store/sql-condition.ts +26 -2
- package/src/elements/store/sql-rls-isolation.test.ts +165 -0
- package/src/elements/store/sql-rls-stamp.test.ts +117 -0
- package/src/elements/store/sql-session.ts +81 -3
- package/src/elements/store/upsert-app.test.ts +1 -1
- package/src/elements/store.ts +14 -0
- package/src/elements/vault/builtin-adapter.ts +17 -0
- package/src/full.ts +2 -0
- package/src/http.ts +20 -3
- package/src/index.ts +14 -1
- package/src/kernel/adopt-barrel-fresh.test.ts +1 -1
- package/src/kernel/adopt-routes.ts +30 -2
- package/src/kernel/app.ts +227 -77
- package/src/kernel/auto-cache.test.ts +8 -8
- package/src/kernel/auto-registry.test.ts +54 -9
- package/src/kernel/boot-bind/ai.ts +24 -0
- package/src/kernel/boot-bind/clock.ts +2 -0
- package/src/kernel/boot.test.ts +4 -4
- package/src/kernel/boot.ts +5 -3
- package/src/kernel/effects-stamping.test.ts +3 -3
- package/src/kernel/element-registries.ts +15 -5
- package/src/kernel/errors.ts +24 -0
- package/src/kernel/flow-units.ts +64 -0
- package/src/kernel/flow.ts +29 -17
- package/src/kernel/fx-dead-letters.test.ts +77 -0
- package/src/kernel/fx.test.ts +22 -0
- package/src/kernel/fx.ts +135 -6
- package/src/kernel/horizontal-child.ts +4 -4
- package/src/kernel/http-path-pending.ts +20 -0
- package/src/kernel/http-resource.ts +4 -0
- package/src/kernel/http-stream.test.ts +174 -0
- package/src/kernel/index.ts +2 -0
- package/src/kernel/pipeline.test.ts +2 -2
- package/src/kernel/plugin-elements.test.ts +1 -1
- package/src/kernel/plugin-needs.test.ts +1 -1
- package/src/kernel/ready.test.ts +2 -2
- package/src/kernel/resource-mount.test.ts +14 -0
- package/src/kernel/run-duration.test.ts +2 -2
- package/src/kernel/stamp-http.test.ts +79 -0
- package/src/kernel/stamp-http.ts +75 -0
- package/src/kernel/triggers.ts +52 -27
- package/src/manifest/mcp-ref.ts +88 -0
- package/src/manifest/types.ts +41 -2
- package/src/manifest/validate.test.ts +20 -0
- package/src/mcp/docs-server.ts +1 -1
- package/src/mcp/server.ts +1 -1
- package/src/plugins/compression.test.ts +21 -0
- package/src/plugins/compression.ts +1 -0
- package/src/release/measure.ts +2 -2
- package/src/runtime/bun.ts +41 -4
- package/src/runtime/json-code-block.test.ts +2 -2
- package/src/runtime/serve.test.ts +4 -4
- package/src/term.test.ts +52 -4
- package/src/term.ts +125 -17
- package/src/test/reset-element-registries.ts +10 -3
- package/src/cli/ask-seed.test.ts +0 -96
- package/src/cli/ask-seed.ts +0 -82
- package/src/console/ui-next/dist/assets/cache-glyph-CLPBqZeb.js +0 -1
- package/src/console/ui-next/dist/assets/collapsible-DYb0xU8C.js +0 -1
- package/src/console/ui-next/dist/assets/flows-page-Dg8CTE29.js +0 -1
- package/src/console/ui-next/dist/assets/http-method-BJ92Z_ke.js +0 -1
- package/src/console/ui-next/dist/assets/index-Bp-R7jtM.js +0 -66
- package/src/console/ui-next/dist/assets/index-D4Ldtj79.css +0 -2
- package/src/console/ui-next/dist/assets/observability-page-DAnpEaq1.js +0 -4
- package/src/console/ui-next/dist/assets/query-gate-parts-1m8m1iNp.js +0 -2
- package/src/console/ui-next/dist/assets/react-B1ML8gxg.js +0 -1
- package/src/console/ui-next/dist/assets/replica-lag-C8_BRt2x.js +0 -18
- package/src/console/ui-next/dist/assets/sheet-form-D-ata7jy.js +0 -1
- package/src/console/ui-next/dist/assets/shortcut-keys-DO4IsVqv.js +0 -1
- package/src/console/ui-next/dist/assets/skeleton-CL_X0GCj.js +0 -1
- package/src/console/ui-next/dist/assets/store-page-v3LXdYpr.js +0 -45
- package/src/console/ui-next/dist/assets/trace-detail-sheet-D16lWQMt.js +0 -2
- package/src/console/ui-next/dist/assets/units-page-BtQ0bqMe.js +0 -1
- package/src/console/ui-next/dist/assets/vault-page-Ca-MvcmJ.js +0 -2
|
@@ -92,6 +92,7 @@ describe("autoCacheEligible", () => {
|
|
|
92
92
|
false,
|
|
93
93
|
);
|
|
94
94
|
expect(autoCacheEligible({ effects: { reads: ["runs"] } })).toBe(false);
|
|
95
|
+
expect(autoCacheEligible({ effects: { reads: ["signal:notify"] } })).toBe(false);
|
|
95
96
|
expect(autoCacheEligible({ effects: {} })).toBe(false);
|
|
96
97
|
});
|
|
97
98
|
|
|
@@ -112,6 +113,7 @@ describe("autoCacheEligible", () => {
|
|
|
112
113
|
{ kind: "read", resource: "sql:views" },
|
|
113
114
|
{ kind: "read", resource: "sql:views" },
|
|
114
115
|
{ kind: "read", resource: "runs" },
|
|
116
|
+
{ kind: "read", resource: "signal:notify" },
|
|
115
117
|
{ kind: "write", resource: "sql:views" },
|
|
116
118
|
{ kind: "ask", resource: "summarize" },
|
|
117
119
|
{ kind: "emit", resource: "view-changed" },
|
|
@@ -53,7 +53,7 @@ export function tier1KeysForReads(
|
|
|
53
53
|
effects: Effects,
|
|
54
54
|
dimsByResource?: Readonly<Record<string, readonly string[]>>,
|
|
55
55
|
): string[] {
|
|
56
|
-
const reads = (effects.reads ?? []).filter((r): r is ResourceRef => r
|
|
56
|
+
const reads = (effects.reads ?? []).filter((r): r is ResourceRef => isStoreResourceRef(r));
|
|
57
57
|
return reads.map((resource) => computedCacheKey(resource, dimsByResource?.[resource]));
|
|
58
58
|
}
|
|
59
59
|
|
|
@@ -223,7 +223,7 @@ export function resolveCacheEffects(
|
|
|
223
223
|
declared: Effects | undefined,
|
|
224
224
|
learnedReads: readonly ResourceRef[] | undefined,
|
|
225
225
|
): Effects {
|
|
226
|
-
const declaredReads = (declared?.reads ?? []).filter((r) => r
|
|
226
|
+
const declaredReads = (declared?.reads ?? []).filter((r) => isStoreResourceRef(r));
|
|
227
227
|
if (declaredReads.length > 0 || (declared?.writes?.length ?? 0) > 0) {
|
|
228
228
|
return declared ?? {};
|
|
229
229
|
}
|
|
@@ -275,7 +275,7 @@ export function autoCacheEligible(options: {
|
|
|
275
275
|
const effects = options.effects ?? {};
|
|
276
276
|
if ((effects.asks?.length ?? 0) > 0) return false;
|
|
277
277
|
if ((effects.writes?.length ?? 0) > 0) return false;
|
|
278
|
-
const reads = (effects.reads ?? []).filter((r) => r
|
|
278
|
+
const reads = (effects.reads ?? []).filter((r) => isStoreResourceRef(r));
|
|
279
279
|
return reads.length > 0;
|
|
280
280
|
}
|
|
281
281
|
|
|
@@ -122,13 +122,12 @@ export interface IndexStoreDecl extends StoreDeclBase {
|
|
|
122
122
|
export type StoreDecl = SqlStoreDecl | KvStoreDecl | FilesStoreDecl | IndexStoreDecl;
|
|
123
123
|
|
|
124
124
|
/**
|
|
125
|
-
* `store
|
|
125
|
+
* Every `store.*` facet pushes into the shared {@link storeRegistry}
|
|
126
126
|
* (`src/kernel/element-registries.ts`) so {@link oke} can auto-populate
|
|
127
127
|
* `stores` with zero explicit array — mirrors the {@link on} trigger-drain
|
|
128
|
-
* registry (`src/kernel/on.ts`).
|
|
129
|
-
* intentionally not auto-registered (out of scope).
|
|
128
|
+
* registry (`src/kernel/on.ts`).
|
|
130
129
|
*
|
|
131
|
-
* Snapshot of every
|
|
130
|
+
* Snapshot of every store declared since the last reset.
|
|
132
131
|
*/
|
|
133
132
|
export function listStores(): readonly StoreDecl[] {
|
|
134
133
|
return storeRegistry.slice();
|
|
@@ -180,13 +179,15 @@ export function sql(name: string, options: SqlStoreOptions = {}): SqlStoreDecl {
|
|
|
180
179
|
* @param options - Options
|
|
181
180
|
*/
|
|
182
181
|
export function kv(name: string, options: KvStoreOptions = {}): KvStoreDecl {
|
|
183
|
-
|
|
182
|
+
const decl: KvStoreDecl = {
|
|
184
183
|
facet: "kv",
|
|
185
184
|
name,
|
|
186
185
|
ref: `kv:${name}`,
|
|
187
186
|
...(options.description !== undefined ? { description: options.description } : {}),
|
|
188
187
|
...(options.durable === true ? { durable: true } : {}),
|
|
189
188
|
};
|
|
189
|
+
storeRegistry.push(decl);
|
|
190
|
+
return decl;
|
|
190
191
|
}
|
|
191
192
|
|
|
192
193
|
/**
|
|
@@ -213,13 +214,15 @@ export function files(name: string, options: FilesStoreOptions = {}): FilesStore
|
|
|
213
214
|
* @param options - Dimensions
|
|
214
215
|
*/
|
|
215
216
|
export function index(name: string, options: IndexStoreOptions = {}): IndexStoreDecl {
|
|
216
|
-
|
|
217
|
+
const decl: IndexStoreDecl = {
|
|
217
218
|
facet: "index",
|
|
218
219
|
name,
|
|
219
220
|
ref: `index:${name}`,
|
|
220
221
|
...(options.description !== undefined ? { description: options.description } : {}),
|
|
221
222
|
dims: options.dims,
|
|
222
223
|
};
|
|
224
|
+
storeRegistry.push(decl);
|
|
225
|
+
return decl;
|
|
223
226
|
}
|
|
224
227
|
|
|
225
228
|
/** Public `store` element — four facets + abstract schema declare + resource. */
|
|
@@ -21,9 +21,11 @@ import {
|
|
|
21
21
|
type SchemaColumnDecl,
|
|
22
22
|
type SchemaColumnInput,
|
|
23
23
|
type SchemaRelationEntry,
|
|
24
|
+
type SchemaPolicyDecl,
|
|
24
25
|
type SchemaRelationsDecl,
|
|
25
26
|
type SchemaTableDecl,
|
|
26
27
|
} from "./schema-decl.ts";
|
|
28
|
+
import { emitPgPolicySource, parseSqlPolicySpec } from "../../drivers/pg-rls.ts";
|
|
27
29
|
|
|
28
30
|
/** Supported Drizzle dialects for domain schema emit (Postgres-family only). */
|
|
29
31
|
export type SqlDialect = "postgres";
|
|
@@ -186,7 +188,12 @@ export function emitDrizzleSource(
|
|
|
186
188
|
Object.values(t.columns).some((c) => c.sqlType === "integer"),
|
|
187
189
|
);
|
|
188
190
|
const pgIntImport = needsInteger ? "bigint" : null;
|
|
189
|
-
const
|
|
191
|
+
const needsPolicy = tables.some((t) => (t.policies?.length ?? 0) > 0);
|
|
192
|
+
const coreNames = ["pgTable", "text", pgIntImport, needsPolicy ? "pgPolicy" : null].filter(
|
|
193
|
+
(name): name is string => name !== null,
|
|
194
|
+
);
|
|
195
|
+
const coreImport = `import { ${coreNames.join(", ")} } from "drizzle-orm/pg-core";`;
|
|
196
|
+
const sqlImport = needsPolicy ? `import { sql } from "drizzle-orm";\n` : "";
|
|
190
197
|
|
|
191
198
|
const needsId = tables.some((t) =>
|
|
192
199
|
Object.values(t.columns).some((c) => c.defaultFnKind === "id"),
|
|
@@ -204,19 +211,14 @@ export function emitDrizzleSource(
|
|
|
204
211
|
const relationsImport =
|
|
205
212
|
relations.length > 0 ? `import { defineRelations } from "drizzle-orm";\n` : "";
|
|
206
213
|
|
|
207
|
-
const blocks = tables.map((table) =>
|
|
208
|
-
const lines = Object.values(table.columns).map(
|
|
209
|
-
(col) => ` ${col.key}: ${emitColumnSource(col, dialect)},`,
|
|
210
|
-
);
|
|
211
|
-
const sqlName = schemaTableSqlName(table) ?? table.name;
|
|
212
|
-
return `export const ${exportNameForTable(sqlName)} = pgTable(${JSON.stringify(sqlName)}, {\n${lines.join("\n")}\n});`;
|
|
213
|
-
});
|
|
214
|
+
const blocks = tables.map((table) => emitTableBlock(table, dialect));
|
|
214
215
|
|
|
215
216
|
const relationsBlock = emitRelationsSource(relations);
|
|
216
217
|
|
|
217
218
|
return [
|
|
218
219
|
GENERATED_SCHEMA_HEADER,
|
|
219
220
|
coreImport,
|
|
221
|
+
sqlImport.trimEnd(),
|
|
220
222
|
relationsImport.trimEnd(),
|
|
221
223
|
helperImport.trimEnd(),
|
|
222
224
|
"",
|
|
@@ -228,6 +230,40 @@ export function emitDrizzleSource(
|
|
|
228
230
|
.join("\n");
|
|
229
231
|
}
|
|
230
232
|
|
|
233
|
+
function emitTableBlock(table: SchemaTableDecl, dialect: SqlDialect): string {
|
|
234
|
+
const lines = Object.values(table.columns).map(
|
|
235
|
+
(col) => ` ${col.key}: ${emitColumnSource(col, dialect)},`,
|
|
236
|
+
);
|
|
237
|
+
const sqlName = schemaTableSqlName(table) ?? table.name;
|
|
238
|
+
const exportName = exportNameForTable(sqlName);
|
|
239
|
+
const cols = `{\n${lines.join("\n")}\n}`;
|
|
240
|
+
const policies = table.policies ?? [];
|
|
241
|
+
if (policies.length === 0) {
|
|
242
|
+
const ctor = table.rls === true ? "pgTable.withRLS" : "pgTable";
|
|
243
|
+
return `export const ${exportName} = ${ctor}(${JSON.stringify(sqlName)}, ${cols});`;
|
|
244
|
+
}
|
|
245
|
+
const extras = policies.map((policy) => ` ${emitPolicyExtra(policy, sqlName)},`).join("\n");
|
|
246
|
+
return `export const ${exportName} = pgTable(${JSON.stringify(sqlName)}, ${cols}, (_t) => [\n${extras}\n]);`;
|
|
247
|
+
}
|
|
248
|
+
|
|
249
|
+
function emitPolicyExtra(policy: SchemaPolicyDecl, table: string): string {
|
|
250
|
+
const roles = Array.isArray(policy.to)
|
|
251
|
+
? policy.to.join(", ")
|
|
252
|
+
: typeof policy.to === "string"
|
|
253
|
+
? policy.to
|
|
254
|
+
: "public";
|
|
255
|
+
const spec = parseSqlPolicySpec({
|
|
256
|
+
name: policy.name,
|
|
257
|
+
table,
|
|
258
|
+
as: policy.as,
|
|
259
|
+
to: roles,
|
|
260
|
+
for: policy.for,
|
|
261
|
+
using: policy.using,
|
|
262
|
+
withCheck: policy.withCheck,
|
|
263
|
+
});
|
|
264
|
+
return emitPgPolicySource(spec).replaceAll("\n", "\n ");
|
|
265
|
+
}
|
|
266
|
+
|
|
231
267
|
function exportNameForTable(name: string): string {
|
|
232
268
|
// Keep simple SQL names as-is; camelCase odd names.
|
|
233
269
|
if (/^[a-zA-Z_][a-zA-Z0-9_]*$/.test(name)) return name;
|
|
@@ -7,7 +7,7 @@
|
|
|
7
7
|
|
|
8
8
|
import { afterAll, afterEach, beforeAll, beforeEach, describe, expect, test } from "bun:test";
|
|
9
9
|
import { integer, pgTable, text } from "drizzle-orm/pg-core";
|
|
10
|
-
import { createInsertSchema, createSelectSchema } from "drizzle-zod";
|
|
10
|
+
import { createInsertSchema, createSelectSchema } from "drizzle-orm/zod";
|
|
11
11
|
import { z } from "zod";
|
|
12
12
|
import { pgliteDriver } from "../../drivers/pglite.ts";
|
|
13
13
|
import type { SqlConnection } from "../../drivers/types.ts";
|
|
@@ -0,0 +1,95 @@
|
|
|
1
|
+
import { describe, expect, test } from "bun:test";
|
|
2
|
+
import type { Manifest } from "../../manifest/types.ts";
|
|
3
|
+
import { firstPolicyOrPublic, resolveRlsIdentity, rlsIdentityFromAuth } from "./rls-identity.ts";
|
|
4
|
+
|
|
5
|
+
const MANIFEST: Manifest = {
|
|
6
|
+
oke: "1.0",
|
|
7
|
+
app: "app",
|
|
8
|
+
gates: {
|
|
9
|
+
public: { kind: "policy" },
|
|
10
|
+
member: { kind: "policy", description: "Signed-in" },
|
|
11
|
+
"booking:create": { kind: "policy", scopes: ["booking:create"] },
|
|
12
|
+
"rate:x": { kind: "rate", max: 1, per: "1m" },
|
|
13
|
+
},
|
|
14
|
+
};
|
|
15
|
+
|
|
16
|
+
describe("resolveRlsIdentity", () => {
|
|
17
|
+
test("operator omit and bypass skip the stamp", () => {
|
|
18
|
+
expect(resolveRlsIdentity({})).toBeNull();
|
|
19
|
+
expect(resolveRlsIdentity({ asGate: "member", bypass: true })).toBeNull();
|
|
20
|
+
});
|
|
21
|
+
|
|
22
|
+
test("public and policy-only leave userId empty", () => {
|
|
23
|
+
expect(resolveRlsIdentity({ asGate: "public", manifest: MANIFEST })).toEqual({
|
|
24
|
+
gate: "public",
|
|
25
|
+
userId: "",
|
|
26
|
+
scopes: [],
|
|
27
|
+
});
|
|
28
|
+
expect(resolveRlsIdentity({ asGate: "member", manifest: MANIFEST })).toEqual({
|
|
29
|
+
gate: "member",
|
|
30
|
+
userId: "",
|
|
31
|
+
scopes: ["member"],
|
|
32
|
+
});
|
|
33
|
+
expect(resolveRlsIdentity({ asGate: "booking:create", manifest: MANIFEST })).toEqual({
|
|
34
|
+
gate: "booking:create",
|
|
35
|
+
userId: "",
|
|
36
|
+
scopes: ["booking:create"],
|
|
37
|
+
});
|
|
38
|
+
});
|
|
39
|
+
|
|
40
|
+
test("seeded user keeps real id and mapped gate", () => {
|
|
41
|
+
expect(
|
|
42
|
+
resolveRlsIdentity({
|
|
43
|
+
asUserId: "u1",
|
|
44
|
+
asGate: "member",
|
|
45
|
+
identities: [{ id: "u1", scopes: ["member"], status: "active" }],
|
|
46
|
+
manifest: MANIFEST,
|
|
47
|
+
}),
|
|
48
|
+
).toEqual({ gate: "member", userId: "u1", scopes: ["member"] });
|
|
49
|
+
});
|
|
50
|
+
|
|
51
|
+
test("refuses rate gates", () => {
|
|
52
|
+
expect(resolveRlsIdentity({ asGate: "rate:x", manifest: MANIFEST })).toBeNull();
|
|
53
|
+
});
|
|
54
|
+
});
|
|
55
|
+
|
|
56
|
+
describe("rlsIdentityFromAuth", () => {
|
|
57
|
+
test("uses the first policy/public and live userId", () => {
|
|
58
|
+
expect(
|
|
59
|
+
rlsIdentityFromAuth({
|
|
60
|
+
userId: "u1",
|
|
61
|
+
scopes: new Set(["member", "booking:create"]),
|
|
62
|
+
gateNames: ["member", "booking:create"],
|
|
63
|
+
}),
|
|
64
|
+
).toEqual({
|
|
65
|
+
gate: "member",
|
|
66
|
+
userId: "u1",
|
|
67
|
+
scopes: ["member", "booking:create"],
|
|
68
|
+
});
|
|
69
|
+
});
|
|
70
|
+
|
|
71
|
+
test("skips cron (no gates) and operator bypass", () => {
|
|
72
|
+
expect(
|
|
73
|
+
rlsIdentityFromAuth({
|
|
74
|
+
userId: null,
|
|
75
|
+
scopes: [],
|
|
76
|
+
gateNames: [],
|
|
77
|
+
}),
|
|
78
|
+
).toBeNull();
|
|
79
|
+
expect(
|
|
80
|
+
rlsIdentityFromAuth({
|
|
81
|
+
userId: "op",
|
|
82
|
+
scopes: [],
|
|
83
|
+
gateNames: ["member"],
|
|
84
|
+
operator: true,
|
|
85
|
+
}),
|
|
86
|
+
).toBeNull();
|
|
87
|
+
});
|
|
88
|
+
});
|
|
89
|
+
|
|
90
|
+
describe("firstPolicyOrPublic", () => {
|
|
91
|
+
test("skips rate ids", () => {
|
|
92
|
+
expect(firstPolicyOrPublic(["rate:x", "member"])).toBe("member");
|
|
93
|
+
expect(firstPolicyOrPublic(["public"])).toBe("public");
|
|
94
|
+
});
|
|
95
|
+
});
|
|
@@ -0,0 +1,116 @@
|
|
|
1
|
+
/**
|
|
2
|
+
* One Gate → RLS identity bag for Console, Call API, and `fx.store`.
|
|
3
|
+
*/
|
|
4
|
+
|
|
5
|
+
import type { Manifest } from "../../manifest/types.ts";
|
|
6
|
+
import type { RlsIdentity } from "../../drivers/pg-rls.ts";
|
|
7
|
+
|
|
8
|
+
export type { RlsIdentity };
|
|
9
|
+
|
|
10
|
+
/** Seeded Console identity used by invoke-as / Gate picker. */
|
|
11
|
+
export type RlsIdentityRow = {
|
|
12
|
+
readonly id: string;
|
|
13
|
+
readonly scopes: readonly string[];
|
|
14
|
+
readonly status?: string;
|
|
15
|
+
};
|
|
16
|
+
|
|
17
|
+
/** Input to {@link resolveRlsIdentity}. */
|
|
18
|
+
export type ResolveRlsIdentityInput = {
|
|
19
|
+
readonly asGate?: string | null;
|
|
20
|
+
readonly asUserId?: string | null;
|
|
21
|
+
readonly bypass?: boolean;
|
|
22
|
+
readonly identities?: readonly RlsIdentityRow[];
|
|
23
|
+
readonly manifest?: Manifest | null;
|
|
24
|
+
};
|
|
25
|
+
|
|
26
|
+
/**
|
|
27
|
+
* Console / Call API bag. Operator and `bypass` return `null` (no stamp).
|
|
28
|
+
*
|
|
29
|
+
* @param input - Picker + Manifest
|
|
30
|
+
*/
|
|
31
|
+
export function resolveRlsIdentity(input: ResolveRlsIdentityInput): RlsIdentity | null {
|
|
32
|
+
if (input.bypass === true) return null;
|
|
33
|
+
const asGate = input.asGate?.trim() || null;
|
|
34
|
+
const asUserId = input.asUserId?.trim() || null;
|
|
35
|
+
if (!asGate && !asUserId) return null;
|
|
36
|
+
|
|
37
|
+
if (asUserId) {
|
|
38
|
+
const identity = (input.identities ?? []).find(
|
|
39
|
+
(row) => row.id === asUserId && row.status !== "disabled",
|
|
40
|
+
);
|
|
41
|
+
if (!identity) return null;
|
|
42
|
+
const gate = asGate ?? policyGateForScopes(identity.scopes, input.manifest);
|
|
43
|
+
if (!gate) return null;
|
|
44
|
+
return { gate, userId: identity.id, scopes: [...identity.scopes] };
|
|
45
|
+
}
|
|
46
|
+
|
|
47
|
+
if (!asGate) return null;
|
|
48
|
+
if (asGate === "public") {
|
|
49
|
+
return { gate: "public", userId: "", scopes: [] };
|
|
50
|
+
}
|
|
51
|
+
|
|
52
|
+
const declared = input.manifest?.gates?.[asGate];
|
|
53
|
+
if (!declared || declared.kind === "rate" || asGate.startsWith("rate:")) return null;
|
|
54
|
+
const scopes = declared.scopes && declared.scopes.length > 0 ? declared.scopes : [asGate];
|
|
55
|
+
return { gate: asGate, userId: "", scopes: [...scopes] };
|
|
56
|
+
}
|
|
57
|
+
|
|
58
|
+
/**
|
|
59
|
+
* Live HTTP / resource-mount bag after Gate passes.
|
|
60
|
+
*
|
|
61
|
+
* @param input - `fx.auth` + trigger gate names
|
|
62
|
+
*/
|
|
63
|
+
export function rlsIdentityFromAuth(input: {
|
|
64
|
+
readonly userId: string | null;
|
|
65
|
+
readonly scopes: ReadonlySet<string> | readonly string[];
|
|
66
|
+
readonly gateNames: readonly string[];
|
|
67
|
+
readonly bypass?: boolean;
|
|
68
|
+
readonly operator?: boolean;
|
|
69
|
+
}): RlsIdentity | null {
|
|
70
|
+
if (input.bypass === true || input.operator === true) return null;
|
|
71
|
+
const gate = firstPolicyOrPublic(input.gateNames);
|
|
72
|
+
if (!gate) return null;
|
|
73
|
+
const scopes = Array.isArray(input.scopes) ? input.scopes : [...input.scopes];
|
|
74
|
+
return {
|
|
75
|
+
gate,
|
|
76
|
+
userId: input.userId ?? "",
|
|
77
|
+
scopes,
|
|
78
|
+
};
|
|
79
|
+
}
|
|
80
|
+
|
|
81
|
+
/**
|
|
82
|
+
* First policy/public name (rate ids skipped).
|
|
83
|
+
*
|
|
84
|
+
* @param names - Trigger gate names
|
|
85
|
+
*/
|
|
86
|
+
export function firstPolicyOrPublic(names: readonly string[]): string | null {
|
|
87
|
+
for (const name of names) {
|
|
88
|
+
if (name.startsWith("rate:")) continue;
|
|
89
|
+
if (name.length > 0) return name;
|
|
90
|
+
}
|
|
91
|
+
return null;
|
|
92
|
+
}
|
|
93
|
+
|
|
94
|
+
/**
|
|
95
|
+
* First Manifest policy implied by scopes (name, then declared roles).
|
|
96
|
+
*
|
|
97
|
+
* @param scopes - Identity scopes
|
|
98
|
+
* @param manifest - Current Manifest
|
|
99
|
+
*/
|
|
100
|
+
export function policyGateForScopes(
|
|
101
|
+
scopes: readonly string[],
|
|
102
|
+
manifest: Manifest | null | undefined,
|
|
103
|
+
): string | null {
|
|
104
|
+
const gates = manifest?.gates ?? {};
|
|
105
|
+
for (const scope of scopes) {
|
|
106
|
+
const gate = gates[scope];
|
|
107
|
+
if (gate && gate.kind !== "rate") return scope;
|
|
108
|
+
}
|
|
109
|
+
for (const scope of scopes) {
|
|
110
|
+
for (const [name, gate] of Object.entries(gates)) {
|
|
111
|
+
if (gate.kind === "rate") continue;
|
|
112
|
+
if ((gate.roles ?? []).includes(scope)) return name;
|
|
113
|
+
}
|
|
114
|
+
}
|
|
115
|
+
return null;
|
|
116
|
+
}
|
|
@@ -24,6 +24,7 @@ import type {
|
|
|
24
24
|
VectorIndexStore,
|
|
25
25
|
} from "../../drivers/types.ts";
|
|
26
26
|
import { indexDriverNeedsSql } from "../../drivers/types.ts";
|
|
27
|
+
import type { RlsIdentity } from "../../drivers/pg-rls.ts";
|
|
27
28
|
import { buildClassificationMap, type MaskRowsOptions } from "./classify.ts";
|
|
28
29
|
import {
|
|
29
30
|
createStoreCache,
|
|
@@ -112,6 +113,8 @@ export interface StoreInvokeContext {
|
|
|
112
113
|
readonly effects: Effects;
|
|
113
114
|
/** Reveal PII (gated elsewhere). */
|
|
114
115
|
readonly revealPii?: boolean;
|
|
116
|
+
/** Gate identity for RLS (`oke.gate` / `oke.user` / `oke.has_scope`). */
|
|
117
|
+
readonly rls?: RlsIdentity;
|
|
115
118
|
}
|
|
116
119
|
|
|
117
120
|
/** Unified handle returned for any facet. */
|
|
@@ -347,6 +350,7 @@ export function createStoreRuntime(options: CreateStoreRuntimeOptions): StoreRun
|
|
|
347
350
|
revealPii: ctx.revealPii,
|
|
348
351
|
routedRole: target.role,
|
|
349
352
|
domainDdl: options.domainDdl ?? "ensure",
|
|
353
|
+
...(ctx.rls ? { rls: ctx.rls } : {}),
|
|
350
354
|
});
|
|
351
355
|
}
|
|
352
356
|
|
|
@@ -260,3 +260,40 @@ describe("emitDrizzleSource — references + relations (Linkly-shaped)", () => {
|
|
|
260
260
|
expect(relations.config.daily?.link?.target).toBe("links");
|
|
261
261
|
});
|
|
262
262
|
});
|
|
263
|
+
|
|
264
|
+
describe("store.schema RLS extras", () => {
|
|
265
|
+
test("helpers stamp rls + named policies", () => {
|
|
266
|
+
const bookings = store.schema.table(
|
|
267
|
+
"bookings",
|
|
268
|
+
{
|
|
269
|
+
id: field.text().primaryKey(),
|
|
270
|
+
owner: field.text().notNull(),
|
|
271
|
+
},
|
|
272
|
+
[
|
|
273
|
+
store.schema.policy.gate("member", { for: "select" }),
|
|
274
|
+
store.schema.policy.owner("owner", { for: "all" }),
|
|
275
|
+
store.schema.policy.scope("booking:create", { for: "insert" }),
|
|
276
|
+
],
|
|
277
|
+
);
|
|
278
|
+
expect(bookings.rls).toBe(true);
|
|
279
|
+
expect(bookings.policies?.map((p) => p.name)).toEqual([
|
|
280
|
+
"gate_member_select",
|
|
281
|
+
"owner_owner_all",
|
|
282
|
+
"scope_booking_create_insert",
|
|
283
|
+
]);
|
|
284
|
+
const src = emitDrizzleSource([bookings], "postgres");
|
|
285
|
+
expect(src).toContain("pgPolicy");
|
|
286
|
+
expect(src).toContain("oke.gate() = ");
|
|
287
|
+
expect(src).toContain("oke.user()");
|
|
288
|
+
expect(src).toContain("oke.has_scope(");
|
|
289
|
+
});
|
|
290
|
+
|
|
291
|
+
test("rls() without policies emits pgTable.withRLS", () => {
|
|
292
|
+
const notes = store.schema.table("notes", { id: field.text().primaryKey() }, [
|
|
293
|
+
store.schema.rls(),
|
|
294
|
+
]);
|
|
295
|
+
expect(notes.rls).toBe(true);
|
|
296
|
+
expect(notes.policies).toBeUndefined();
|
|
297
|
+
expect(emitDrizzleSource([notes], "postgres")).toContain("pgTable.withRLS");
|
|
298
|
+
});
|
|
299
|
+
});
|
|
@@ -69,6 +69,42 @@ export interface SchemaColumnDecl extends ColumnDef {
|
|
|
69
69
|
getSQL(): never;
|
|
70
70
|
}
|
|
71
71
|
|
|
72
|
+
/** Policy command on a {@link SchemaPolicyDecl}. */
|
|
73
|
+
export type SchemaPolicyFor = "all" | "select" | "insert" | "update" | "delete";
|
|
74
|
+
|
|
75
|
+
/** Policy behavior on a {@link SchemaPolicyDecl}. */
|
|
76
|
+
export type SchemaPolicyAs = "permissive" | "restrictive";
|
|
77
|
+
|
|
78
|
+
/**
|
|
79
|
+
* Drizzle-shaped policy extra (`pgPolicy` emit).
|
|
80
|
+
*/
|
|
81
|
+
export interface SchemaPolicyDecl {
|
|
82
|
+
readonly kind: "schema-policy";
|
|
83
|
+
readonly name: string;
|
|
84
|
+
readonly as?: SchemaPolicyAs;
|
|
85
|
+
readonly to?: string | readonly string[];
|
|
86
|
+
readonly for?: SchemaPolicyFor;
|
|
87
|
+
readonly using?: string;
|
|
88
|
+
readonly withCheck?: string;
|
|
89
|
+
}
|
|
90
|
+
|
|
91
|
+
/** Enable RLS with no policies (`pgTable.withRLS`). */
|
|
92
|
+
export interface SchemaRlsEnableDecl {
|
|
93
|
+
readonly kind: "schema-rls";
|
|
94
|
+
}
|
|
95
|
+
|
|
96
|
+
/** Third-arg extra for {@link schemaTable}. */
|
|
97
|
+
export type SchemaTableExtra = SchemaPolicyDecl | SchemaRlsEnableDecl;
|
|
98
|
+
|
|
99
|
+
/** Options for {@link schemaPolicy} / Gate helpers. */
|
|
100
|
+
export interface SchemaPolicyOptions {
|
|
101
|
+
readonly as?: SchemaPolicyAs;
|
|
102
|
+
readonly to?: string | readonly string[];
|
|
103
|
+
readonly for?: SchemaPolicyFor;
|
|
104
|
+
readonly using?: string;
|
|
105
|
+
readonly withCheck?: string;
|
|
106
|
+
}
|
|
107
|
+
|
|
72
108
|
/** Table from {@link store.schema.table} — extends {@link TableHandle}. */
|
|
73
109
|
export interface SchemaTableDecl extends TableHandle {
|
|
74
110
|
readonly kind: "schema-table";
|
|
@@ -78,6 +114,10 @@ export interface SchemaTableDecl extends TableHandle {
|
|
|
78
114
|
* (spreading columns would otherwise shadow {@link TableHandle.name}).
|
|
79
115
|
*/
|
|
80
116
|
readonly tableName?: string;
|
|
117
|
+
/** `true` when {@link schemaRls} was declared or any policy is present. */
|
|
118
|
+
readonly rls?: boolean;
|
|
119
|
+
/** Declared policies (emit + Manifest). */
|
|
120
|
+
readonly policies?: readonly SchemaPolicyDecl[];
|
|
81
121
|
}
|
|
82
122
|
|
|
83
123
|
/** Fluent field builder before key finalization. */
|
|
@@ -310,20 +350,28 @@ export type SchemaTableWithColumns<
|
|
|
310
350
|
*
|
|
311
351
|
* @param name - SQL table name
|
|
312
352
|
* @param columns - Column map using {@link field} builders
|
|
353
|
+
* @param extras - RLS enable + {@link schemaPolicy} extras (Drizzle third arg)
|
|
313
354
|
*/
|
|
314
355
|
export function schemaTable<C extends Record<string, SchemaColumnInput>>(
|
|
315
356
|
name: string,
|
|
316
357
|
columns: C,
|
|
358
|
+
extras: readonly SchemaTableExtra[] = [],
|
|
317
359
|
): SchemaTableWithColumns<{ [K in keyof C]: SchemaColumnDecl }> {
|
|
318
360
|
const finalized = finalizeColumnMap(columns);
|
|
319
361
|
const stamped: Record<string, SchemaColumnDecl> = {};
|
|
320
362
|
for (const [key, col] of Object.entries(finalized)) {
|
|
321
363
|
stamped[key] = { ...col, tableName: name };
|
|
322
364
|
}
|
|
365
|
+
const policies = extras.filter(
|
|
366
|
+
(extra): extra is SchemaPolicyDecl => extra.kind === "schema-policy",
|
|
367
|
+
);
|
|
368
|
+
const rls = extras.some((extra) => extra.kind === "schema-rls") || policies.length > 0;
|
|
323
369
|
const table = {
|
|
324
370
|
name,
|
|
325
371
|
columns: stamped,
|
|
326
372
|
...stamped,
|
|
373
|
+
...(rls ? { rls: true } : {}),
|
|
374
|
+
...(policies.length > 0 ? { policies } : {}),
|
|
327
375
|
};
|
|
328
376
|
// Survive columns named `name` or `kind` (they would otherwise shadow
|
|
329
377
|
// the table discriminant / SQL name).
|
|
@@ -332,6 +380,117 @@ export function schemaTable<C extends Record<string, SchemaColumnInput>>(
|
|
|
332
380
|
return table as SchemaTableWithColumns<{ [K in keyof C]: SchemaColumnDecl }>;
|
|
333
381
|
}
|
|
334
382
|
|
|
383
|
+
/**
|
|
384
|
+
* Enable RLS with no policies (`pgTable.withRLS`).
|
|
385
|
+
*/
|
|
386
|
+
export function schemaRls(): SchemaRlsEnableDecl {
|
|
387
|
+
return { kind: "schema-rls" };
|
|
388
|
+
}
|
|
389
|
+
|
|
390
|
+
/**
|
|
391
|
+
* Raw Drizzle-shaped policy extra.
|
|
392
|
+
*
|
|
393
|
+
* @param name - Policy name
|
|
394
|
+
* @param options - `as` / `to` / `for` / `using` / `withCheck`
|
|
395
|
+
*/
|
|
396
|
+
export function schemaPolicy(name: string, options: SchemaPolicyOptions = {}): SchemaPolicyDecl {
|
|
397
|
+
return {
|
|
398
|
+
kind: "schema-policy",
|
|
399
|
+
name,
|
|
400
|
+
...(options.as !== undefined ? { as: options.as } : {}),
|
|
401
|
+
...(options.to !== undefined ? { to: options.to } : {}),
|
|
402
|
+
...(options.for !== undefined ? { for: options.for } : {}),
|
|
403
|
+
...(options.using !== undefined ? { using: options.using } : {}),
|
|
404
|
+
...(options.withCheck !== undefined ? { withCheck: options.withCheck } : {}),
|
|
405
|
+
};
|
|
406
|
+
}
|
|
407
|
+
|
|
408
|
+
function sqlStringLiteral(value: string): string {
|
|
409
|
+
return `'${value.replaceAll("'", "''")}'`;
|
|
410
|
+
}
|
|
411
|
+
|
|
412
|
+
function helperPolicyName(prefix: string, key: string, command: SchemaPolicyFor): string {
|
|
413
|
+
const slug = key.replace(/[^A-Za-z0-9]+/g, "_").replace(/^_|_$/g, "");
|
|
414
|
+
return `${prefix}_${slug}_${command}`;
|
|
415
|
+
}
|
|
416
|
+
|
|
417
|
+
function policyPredicates(
|
|
418
|
+
command: SchemaPolicyFor,
|
|
419
|
+
expr: string,
|
|
420
|
+
): { readonly using?: string; readonly withCheck?: string } {
|
|
421
|
+
if (command === "insert") return { withCheck: expr };
|
|
422
|
+
if (command === "update" || command === "all") return { using: expr, withCheck: expr };
|
|
423
|
+
return { using: expr };
|
|
424
|
+
}
|
|
425
|
+
|
|
426
|
+
/**
|
|
427
|
+
* Gate-name policy — `oke.gate() = 'member'`.
|
|
428
|
+
*
|
|
429
|
+
* @param gate - Policy / public Gate name
|
|
430
|
+
* @param options - Command (default `select`)
|
|
431
|
+
*/
|
|
432
|
+
export function schemaPolicyGate(
|
|
433
|
+
gate: string,
|
|
434
|
+
options: Pick<SchemaPolicyOptions, "for" | "as" | "to"> = {},
|
|
435
|
+
): SchemaPolicyDecl {
|
|
436
|
+
const command = options.for ?? "select";
|
|
437
|
+
return schemaPolicy(helperPolicyName("gate", gate, command), {
|
|
438
|
+
...options,
|
|
439
|
+
for: command,
|
|
440
|
+
...policyPredicates(command, `oke.gate() = ${sqlStringLiteral(gate)}`),
|
|
441
|
+
});
|
|
442
|
+
}
|
|
443
|
+
|
|
444
|
+
/**
|
|
445
|
+
* Owner-column policy — `owner = oke.user()`.
|
|
446
|
+
*
|
|
447
|
+
* @param column - SQL / JS column name
|
|
448
|
+
* @param options - Command (default `all`)
|
|
449
|
+
*/
|
|
450
|
+
export function schemaPolicyOwner(
|
|
451
|
+
column: string,
|
|
452
|
+
options: Pick<SchemaPolicyOptions, "for" | "as" | "to"> = {},
|
|
453
|
+
): SchemaPolicyDecl {
|
|
454
|
+
const command = options.for ?? "all";
|
|
455
|
+
return schemaPolicy(helperPolicyName("owner", column, command), {
|
|
456
|
+
...options,
|
|
457
|
+
for: command,
|
|
458
|
+
...policyPredicates(command, `${column} = oke.user()`),
|
|
459
|
+
});
|
|
460
|
+
}
|
|
461
|
+
|
|
462
|
+
/**
|
|
463
|
+
* Scope policy — `oke.has_scope('booking:create')`.
|
|
464
|
+
*
|
|
465
|
+
* @param scope - Scope string
|
|
466
|
+
* @param options - Command (default `insert`)
|
|
467
|
+
*/
|
|
468
|
+
export function schemaPolicyScope(
|
|
469
|
+
scope: string,
|
|
470
|
+
options: Pick<SchemaPolicyOptions, "for" | "as" | "to"> = {},
|
|
471
|
+
): SchemaPolicyDecl {
|
|
472
|
+
const command = options.for ?? "insert";
|
|
473
|
+
return schemaPolicy(helperPolicyName("scope", scope, command), {
|
|
474
|
+
...options,
|
|
475
|
+
for: command,
|
|
476
|
+
...policyPredicates(command, `oke.has_scope(${sqlStringLiteral(scope)})`),
|
|
477
|
+
});
|
|
478
|
+
}
|
|
479
|
+
|
|
480
|
+
/** Callable `store.schema.policy` plus Gate helpers. */
|
|
481
|
+
export type SchemaPolicyApi = typeof schemaPolicy & {
|
|
482
|
+
readonly gate: typeof schemaPolicyGate;
|
|
483
|
+
readonly owner: typeof schemaPolicyOwner;
|
|
484
|
+
readonly scope: typeof schemaPolicyScope;
|
|
485
|
+
};
|
|
486
|
+
|
|
487
|
+
/** `store.schema.policy` — raw + Gate helpers. */
|
|
488
|
+
export const schemaPolicyApi: SchemaPolicyApi = Object.assign(schemaPolicy, {
|
|
489
|
+
gate: schemaPolicyGate,
|
|
490
|
+
owner: schemaPolicyOwner,
|
|
491
|
+
scope: schemaPolicyScope,
|
|
492
|
+
});
|
|
493
|
+
|
|
335
494
|
// ─── Relations (mirrors drizzle-orm `defineRelations`) ───────────────────────
|
|
336
495
|
|
|
337
496
|
/** Column path recorded by relation helpers (`r.links.code`). */
|
|
@@ -527,6 +686,8 @@ export function schemaRelations<T extends Readonly<Record<string, SchemaTableDec
|
|
|
527
686
|
export const schema = {
|
|
528
687
|
table: schemaTable,
|
|
529
688
|
relations: schemaRelations,
|
|
689
|
+
rls: schemaRls,
|
|
690
|
+
policy: schemaPolicyApi,
|
|
530
691
|
} as const;
|
|
531
692
|
|
|
532
693
|
/**
|