okengine 0.14.1 → 0.16.0
This diff represents the content of publicly available package versions that have been released to one of the supported registries. The information contained in this diff is provided for informational purposes only and reflects changes between package versions as they appear in their respective public registries.
- package/package.json +3 -4
- package/site/content/docs/elements/ai.mdx +2 -1
- package/site/content/docs/elements/channel.mdx +1 -0
- package/site/content/docs/elements/clock.mdx +3 -0
- package/site/content/docs/elements/flow.mdx +9 -0
- package/site/content/docs/elements/gate.mdx +80 -22
- package/site/content/docs/elements/signal.mdx +4 -4
- package/site/content/docs/elements/store.mdx +84 -27
- package/site/content/docs/elements/vault.mdx +4 -0
- package/site/content/docs/get-started/basic-usage.mdx +22 -13
- package/site/content/docs/get-started/index.mdx +5 -0
- package/site/content/docs/get-started/introduction.mdx +11 -3
- package/site/content/docs/get-started/meta.json +1 -1
- package/site/content/docs/get-started/project-structure.mdx +932 -0
- package/site/content/docs/reference/cli.md +13 -10
- package/site/content/docs/reference/client.mdx +28 -21
- package/site/content/docs/reference/fx.mdx +29 -18
- package/site/content/docs/reference/security.md +2 -2
- package/src/auth/api-key-sql.test.ts +47 -0
- package/src/auth/api-key-sql.ts +254 -0
- package/src/auth/api-keys.ts +273 -14
- package/src/auth/auth.test.ts +56 -0
- package/src/auth/bindings.ts +14 -5
- package/src/auth/config.ts +5 -5
- package/src/auth/cross-plane.ts +10 -3
- package/src/auth/index.ts +25 -1
- package/src/auth/schema.ts +8 -0
- package/src/cli/ai-setup/ai-setup.test.ts +62 -0
- package/src/cli/ai-setup/apply.ts +32 -1
- package/src/cli/attach-host-console.test.ts +40 -2
- package/src/cli/attach-host-console.ts +9 -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 +17 -2
- package/src/compiler/extract.test.ts +409 -33
- package/src/compiler/extract.ts +318 -67
- package/src/compiler/flow-path.test.ts +96 -0
- package/src/compiler/flow-path.ts +173 -0
- package/src/compiler/generate-adopt.test.ts +112 -3
- package/src/compiler/generate-adopt.ts +280 -26
- package/src/compiler/schema-from-ast.ts +609 -0
- package/src/console/server/access.test.ts +116 -8
- package/src/console/server/access.ts +70 -23
- package/src/console/server/app.ts +1 -0
- package/src/console/server/bind.ts +2 -2
- package/src/console/server/console.test.ts +3 -0
- package/src/console/server/dev-identities.test.ts +127 -0
- package/src/console/server/dev-identities.ts +229 -0
- package/src/console/server/flows-invoke.test.ts +21 -4
- package/src/console/server/flows.ts +233 -98
- package/src/console/server/gates.test.ts +24 -0
- package/src/console/server/gates.ts +8 -21
- package/src/console/server/index.ts +1 -0
- 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/live.test.ts +26 -1
- package/src/console/server/live.ts +4 -2
- package/src/console/server/operator-db.ts +2 -0
- package/src/console/server/security-headers.ts +6 -3
- package/src/console/server/spa-proxy.test.ts +1 -0
- package/src/console/server/sql-catalog.ts +147 -7
- package/src/console/server/sql-rls.test.ts +16 -0
- package/src/console/server/state.ts +37 -55
- package/src/console/server/store.test.ts +7 -4
- package/src/console/server/store.ts +142 -58
- package/src/console/ui-next/dist/assets/access-page-CAGHrA9H.js +4 -0
- package/src/console/ui-next/dist/assets/agent-disclosure-BP0Y0Sux.js +1 -0
- package/src/console/ui-next/dist/assets/cache-glyph-B5X-NM0-.js +1 -0
- package/src/console/ui-next/dist/assets/call-pii-button-ChAUiLo9.js +1 -0
- package/src/console/ui-next/dist/assets/collapsible-DGnOM2ph.js +1 -0
- package/src/console/ui-next/dist/assets/copy-inline-button-DBHhgHKP.js +1 -0
- package/src/console/ui-next/dist/assets/detail-header-DhHM1iaZ.js +1 -0
- package/src/console/ui-next/dist/assets/dropdown-menu-_NjTEo5_.js +1 -0
- package/src/console/ui-next/dist/assets/{duration-tone-oiRxPVsZ.js → duration-tone-sC3lGABz.js} +1 -1
- package/src/console/ui-next/dist/assets/element-icons-BVXtRyd3.js +1 -0
- package/src/console/ui-next/dist/assets/explorer-empty-2uIhBu0_.js +1 -0
- package/src/console/ui-next/dist/assets/flows-page-B-OUtiAu.js +1 -0
- package/src/console/ui-next/dist/assets/{highlighted-json-CvDPvveV.js → highlighted-json-Awq7gYdu.js} +1 -1
- package/src/console/ui-next/dist/assets/http-method-_UHM2ODJ.js +1 -0
- package/src/console/ui-next/dist/assets/index-CGoZkILK.js +66 -0
- package/src/console/ui-next/dist/assets/index-Ck88Jmv8.css +2 -0
- package/src/console/ui-next/dist/assets/link-BX6Vqztd.js +1 -0
- package/src/console/ui-next/dist/assets/observability-page-BDyXalNR.js +4 -0
- package/src/console/ui-next/dist/assets/react-D8E3mtu1.js +1 -0
- package/src/console/ui-next/dist/assets/replica-lag-DKRrbvdo.js +18 -0
- package/src/console/ui-next/dist/assets/request-meta-DV0ywz7t.js +1 -0
- package/src/console/ui-next/dist/assets/shortcut-JQIZlWfm.js +1 -0
- package/src/console/ui-next/dist/assets/shortcut-keys-DKxNTe_m.js +1 -0
- package/src/console/ui-next/dist/assets/{skeleton-CL_X0GCj.js → skeleton-D-czQJT6.js} +1 -1
- package/src/console/ui-next/dist/assets/store-page-Xh8Kn3rx.js +41 -0
- package/src/console/ui-next/dist/assets/trace-detail-sheet-B09O8rA6.js +2 -0
- package/src/console/ui-next/dist/assets/tree-expand-toggle-DkOXA12R.js +54 -0
- package/src/console/ui-next/dist/assets/units-page-Dpk40kOQ.js +1 -0
- package/src/console/ui-next/dist/assets/{use-vault-list-Cl79j_ku.js → use-vault-list-uk4WVboC.js} +1 -1
- package/src/console/ui-next/dist/assets/useMutation-B8EO02Ej.js +1 -0
- package/src/console/ui-next/dist/assets/useRender-BE2A9BWC.js +1 -0
- package/src/console/ui-next/dist/assets/vault-page-B1dB9Ft0.js +2 -0
- package/src/console/ui-next/dist/index.html +12 -10
- package/src/console/ui-next/index.html +3 -3
- package/src/console/ui-next/src/client.ts +202 -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/shell/app-sidebar.tsx +2 -0
- package/src/console/ui-next/src/components/shell/command-items.test.ts +1 -0
- package/src/console/ui-next/src/components/shell/command-items.ts +8 -0
- package/src/console/ui-next/src/components/shell/shell-layout.tsx +2 -1
- package/src/console/ui-next/src/components/ui/kbd.tsx +2 -2
- package/src/console/ui-next/src/components/ui/sheet-form.tsx +8 -3
- package/src/console/ui-next/src/features/access/access-page.tsx +347 -0
- package/src/console/ui-next/src/features/access/data/use-access.ts +23 -0
- package/src/console/ui-next/src/features/access/detail/access-detail.tsx +552 -0
- package/src/console/ui-next/src/features/access/explorer/access-list.tsx +125 -0
- package/src/console/ui-next/src/features/access/lib/format-when.test.ts +139 -0
- package/src/console/ui-next/src/features/access/lib/format-when.ts +296 -0
- package/src/console/ui-next/src/features/access/lib/scope-groups.test.ts +34 -0
- package/src/console/ui-next/src/features/access/lib/scope-groups.ts +40 -0
- package/src/console/ui-next/src/features/access/sheets/access-allow-fields.tsx +236 -0
- package/src/console/ui-next/src/features/access/sheets/access-create-sheet.tsx +316 -0
- package/src/console/ui-next/src/features/access/sheets/access-edit-sheet.tsx +240 -0
- package/src/console/ui-next/src/features/access/sheets/access-expiry-fields.tsx +66 -0
- package/src/console/ui-next/src/features/access/sheets/access-refresh-sheet.tsx +104 -0
- package/src/console/ui-next/src/features/access/sheets/access-scope-field.tsx +115 -0
- package/src/console/ui-next/src/features/access/sheets/access-secret-sheet.tsx +89 -0
- package/src/console/ui-next/src/features/access/sheets/access-sheet-search.tsx +49 -0
- package/src/console/ui-next/src/features/access/state/access-selection.ts +84 -0
- package/src/console/ui-next/src/features/auth/auth-redirect.test.ts +5 -0
- package/src/console/ui-next/src/features/auth/auth-redirect.ts +18 -2
- package/src/console/ui-next/src/features/flows/traces/http-method.ts +19 -0
- package/src/console/ui-next/src/features/flows/traces/trace-detail.test.ts +19 -1
- package/src/console/ui-next/src/features/flows/traces/traces-pane.tsx +10 -3
- package/src/console/ui-next/src/features/not-found/not-found-page.tsx +1 -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/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/console/ui-next/src/lib/document-meta.test.ts +1 -0
- package/src/console/ui-next/src/lib/document-meta.ts +2 -1
- package/src/console/ui-next/src/lib/last-module-search.test.ts +1 -0
- package/src/console/ui-next/src/lib/last-module-search.ts +2 -1
- package/src/console/ui-next/src/lib/shortcut.test.ts +2 -0
- package/src/console/ui-next/src/lib/shortcut.ts +3 -0
- package/src/console/ui-next/src/router.tsx +10 -1
- package/src/console/ui-next/ui-next-seed-app-schema.ts +88 -37
- package/src/console/ui-next/ui-next-seed-store.ts +43 -15
- package/src/console/xss.gate.test.ts +1 -0
- package/src/docker/compose-health.test.ts +27 -0
- package/src/docker/compose-health.ts +19 -3
- package/src/docker/docker.test.ts +3 -0
- package/src/docker/recipes/pgdog.ts +2 -0
- package/src/drivers/clock-postgres.ts +13 -9
- package/src/drivers/instances-postgres.ts +4 -6
- package/src/drivers/journal-postgres.ts +13 -9
- package/src/drivers/pg-rls.test.ts +87 -0
- package/src/drivers/pg-rls.ts +233 -7
- package/src/drivers/pglite.ts +26 -3
- package/src/drivers/postgres.test.ts +23 -0
- package/src/drivers/postgres.ts +133 -9
- package/src/drivers/types.ts +10 -0
- package/src/elements/clock/declare.ts +26 -1
- package/src/elements/gate/boot.ts +5 -5
- package/src/elements/gate/declare.ts +38 -4
- package/src/elements/gate/flatten.ts +1 -1
- package/src/elements/gate/runtime.ts +1 -1
- package/src/elements/store/cache.ts +1 -1
- package/src/elements/store/declare.ts +9 -6
- package/src/elements/store/emit-drizzle.ts +44 -8
- package/src/elements/store/resource.test.ts +1 -1
- package/src/elements/store/rls-identity.test.ts +95 -0
- package/src/elements/store/rls-identity.ts +116 -0
- package/src/elements/store/runtime.ts +18 -0
- package/src/elements/store/schema-decl.test.ts +37 -0
- package/src/elements/store/schema-decl.ts +260 -41
- 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-select.types.test.ts +71 -0
- package/src/elements/store/sql-session.ts +135 -35
- package/src/elements/store/upsert-app.test.ts +1 -1
- package/src/elements/store.ts +17 -0
- package/src/http.ts +20 -3
- package/src/index.ts +11 -1
- package/src/kernel/adopt-barrel-fresh.test.ts +1 -1
- package/src/kernel/adopt-routes.ts +30 -2
- package/src/kernel/api-key-host-persist.test.ts +143 -0
- package/src/kernel/app.ts +145 -16
- package/src/kernel/auth-resolve.ts +10 -5
- package/src/kernel/auto-cache.test.ts +8 -8
- package/src/kernel/auto-registry.test.ts +49 -9
- package/src/kernel/boot.test.ts +5 -5
- package/src/kernel/boot.ts +2 -2
- package/src/kernel/effects-stamping.test.ts +3 -3
- package/src/kernel/element-registries.ts +12 -5
- package/src/kernel/errors.ts +24 -0
- package/src/kernel/flow-units.ts +64 -0
- package/src/kernel/flow.ts +29 -17
- package/src/kernel/fx-auth-keys.test.ts +88 -0
- package/src/kernel/fx-auth-keys.ts +198 -0
- package/src/kernel/fx.test.ts +17 -1
- package/src/kernel/fx.ts +59 -18
- package/src/kernel/horizontal-child.ts +4 -4
- package/src/kernel/http-path-pending.ts +20 -0
- package/src/kernel/http-resource.ts +4 -0
- package/src/kernel/http-stream.test.ts +4 -4
- package/src/kernel/pipeline-api-key.test.ts +192 -7
- package/src/kernel/pipeline.test.ts +2 -2
- package/src/kernel/pipeline.ts +4 -2
- package/src/kernel/plugin-elements.test.ts +1 -1
- package/src/kernel/plugin-needs.test.ts +1 -1
- package/src/kernel/ready.test.ts +2 -2
- package/src/kernel/resource-mount.test.ts +14 -0
- package/src/kernel/run-duration.test.ts +2 -2
- package/src/kernel/stamp-http.test.ts +79 -0
- package/src/kernel/stamp-http.ts +75 -0
- package/src/kernel/triggers.ts +52 -27
- package/src/manifest/types.ts +22 -3
- package/src/release/build-lib.ts +1 -0
- package/src/release/measure.ts +2 -2
- package/src/runtime/json-code-block.test.ts +2 -2
- package/src/runtime/serve.test.ts +4 -4
- package/src/term.test.ts +52 -4
- package/src/term.ts +125 -17
- package/src/test/reset-element-registries.ts +8 -3
- package/src/cli/ask-seed.test.ts +0 -96
- package/src/cli/ask-seed.ts +0 -82
- package/src/console/ui-next/dist/assets/cache-glyph-BanhLsEY.js +0 -1
- package/src/console/ui-next/dist/assets/call-pii-button-bqkxMrJH.js +0 -1
- package/src/console/ui-next/dist/assets/collapsible-DYb0xU8C.js +0 -1
- package/src/console/ui-next/dist/assets/confirm-sheet-DDCRmG62.js +0 -1
- package/src/console/ui-next/dist/assets/explorer-empty-HjTnVQoR.js +0 -1
- package/src/console/ui-next/dist/assets/flows-page-DxDsOd4f.js +0 -1
- package/src/console/ui-next/dist/assets/http-method-CJCBYL2j.js +0 -1
- package/src/console/ui-next/dist/assets/index-Ce6WKWKM.js +0 -66
- package/src/console/ui-next/dist/assets/index-D4Ldtj79.css +0 -2
- package/src/console/ui-next/dist/assets/link-DF7SZ9Ek.js +0 -1
- package/src/console/ui-next/dist/assets/observability-page-BEZDzyYh.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-Cgzuv4k1.js +0 -1
- package/src/console/ui-next/dist/assets/shortcut-keys-DO4IsVqv.js +0 -1
- package/src/console/ui-next/dist/assets/store-page-v3LXdYpr.js +0 -45
- package/src/console/ui-next/dist/assets/trace-detail-sheet-DFLFfUUX.js +0 -2
- package/src/console/ui-next/dist/assets/units-page-C0gW6Kdo.js +0 -1
- package/src/console/ui-next/dist/assets/vault-page-DuKzqwzW.js +0 -2
|
@@ -2,15 +2,27 @@
|
|
|
2
2
|
* Console helpers for RLS policy create (SQL preview + templates).
|
|
3
3
|
*/
|
|
4
4
|
|
|
5
|
+
import type {
|
|
6
|
+
ColumnClassification,
|
|
7
|
+
DeclaredColumn,
|
|
8
|
+
Manifest,
|
|
9
|
+
Table,
|
|
10
|
+
} from "../../../../../../manifest/types.ts";
|
|
5
11
|
import {
|
|
6
12
|
buildCreatePolicySql,
|
|
7
13
|
buildRowSecuritySql,
|
|
8
14
|
parseSqlPolicySpec,
|
|
9
15
|
quotePgIdent,
|
|
16
|
+
formatPolicyRole,
|
|
17
|
+
emitPgPolicySource,
|
|
18
|
+
emitStoreSchemaPolicySource,
|
|
10
19
|
type SqlPolicyBehavior,
|
|
11
20
|
type SqlPolicyCommand,
|
|
12
21
|
type SqlPolicySpec,
|
|
13
22
|
} from "../../../../../../drivers/pg-rls.ts";
|
|
23
|
+
import { fkColumnStem } from "./schema-graph.ts";
|
|
24
|
+
|
|
25
|
+
export { emitPgPolicySource, emitStoreSchemaPolicySource };
|
|
14
26
|
|
|
15
27
|
export type { SqlPolicyBehavior, SqlPolicyCommand, SqlPolicySpec };
|
|
16
28
|
export { buildCreatePolicySql, buildRowSecuritySql, parseSqlPolicySpec };
|
|
@@ -28,7 +40,8 @@ export type RlsPolicyTemplate = {
|
|
|
28
40
|
readonly withCheck?: string;
|
|
29
41
|
};
|
|
30
42
|
|
|
31
|
-
const OKE_GATE = "
|
|
43
|
+
const OKE_GATE = "oke.gate()";
|
|
44
|
+
const OKE_USER = "oke.user()";
|
|
32
45
|
|
|
33
46
|
/** Common Postgres RLS starters — OKE Gate / owner / join, no vendor auth helpers. */
|
|
34
47
|
export const RLS_POLICY_TEMPLATES: readonly RlsPolicyTemplate[] = [
|
|
@@ -52,14 +65,14 @@ export const RLS_POLICY_TEMPLATES: readonly RlsPolicyTemplate[] = [
|
|
|
52
65
|
id: "owner-read",
|
|
53
66
|
name: "owner_read",
|
|
54
67
|
title: "Enable read for owners",
|
|
55
|
-
detail: "SELECT when a text owner column matches
|
|
68
|
+
detail: "SELECT when a text owner column matches oke.user().",
|
|
56
69
|
command: "SELECT",
|
|
57
|
-
using:
|
|
70
|
+
using: `owner = ${OKE_USER}`,
|
|
58
71
|
},
|
|
59
72
|
{
|
|
60
73
|
id: "gate-read",
|
|
61
74
|
name: "gate_read",
|
|
62
|
-
title: "Enable read when oke.gate matches",
|
|
75
|
+
title: "Enable read when oke.gate() matches",
|
|
63
76
|
detail: `SELECT when ${OKE_GATE} equals a Gate name (pair Policy & scope).`,
|
|
64
77
|
command: "SELECT",
|
|
65
78
|
using: `${OKE_GATE} = 'member'`,
|
|
@@ -70,8 +83,7 @@ export const RLS_POLICY_TEMPLATES: readonly RlsPolicyTemplate[] = [
|
|
|
70
83
|
title: "Enable read via a related table",
|
|
71
84
|
detail: "SELECT with EXISTS against members — membership joins, not a column on this table.",
|
|
72
85
|
command: "SELECT",
|
|
73
|
-
using:
|
|
74
|
-
"exists (select 1 from members m where m.user_id = current_user and m.team_id = team_id)",
|
|
86
|
+
using: "exists (select 1 from members m where m.user_id = oke.user() and m.team_id = team_id)",
|
|
75
87
|
},
|
|
76
88
|
{
|
|
77
89
|
id: "insert-all",
|
|
@@ -85,14 +97,14 @@ export const RLS_POLICY_TEMPLATES: readonly RlsPolicyTemplate[] = [
|
|
|
85
97
|
id: "insert-owner",
|
|
86
98
|
name: "insert_owner",
|
|
87
99
|
title: "Enable insert for owners",
|
|
88
|
-
detail: "INSERT WITH CHECK so new rows set owner to
|
|
100
|
+
detail: "INSERT WITH CHECK so new rows set owner to oke.user().",
|
|
89
101
|
command: "INSERT",
|
|
90
|
-
withCheck:
|
|
102
|
+
withCheck: `owner = ${OKE_USER}`,
|
|
91
103
|
},
|
|
92
104
|
{
|
|
93
105
|
id: "insert-gate",
|
|
94
106
|
name: "insert_gate",
|
|
95
|
-
title: "Enable insert when oke.gate matches",
|
|
107
|
+
title: "Enable insert when oke.gate() matches",
|
|
96
108
|
detail: `INSERT WITH CHECK when ${OKE_GATE} equals a Gate name.`,
|
|
97
109
|
command: "INSERT",
|
|
98
110
|
withCheck: `${OKE_GATE} = 'member'`,
|
|
@@ -101,15 +113,15 @@ export const RLS_POLICY_TEMPLATES: readonly RlsPolicyTemplate[] = [
|
|
|
101
113
|
id: "update-owner",
|
|
102
114
|
name: "update_owner",
|
|
103
115
|
title: "Enable update for owners",
|
|
104
|
-
detail: "UPDATE own rows. USING and WITH CHECK both require owner =
|
|
116
|
+
detail: "UPDATE own rows. USING and WITH CHECK both require owner = oke.user().",
|
|
105
117
|
command: "UPDATE",
|
|
106
|
-
using:
|
|
107
|
-
withCheck:
|
|
118
|
+
using: `owner = ${OKE_USER}`,
|
|
119
|
+
withCheck: `owner = ${OKE_USER}`,
|
|
108
120
|
},
|
|
109
121
|
{
|
|
110
122
|
id: "update-gate",
|
|
111
123
|
name: "update_gate",
|
|
112
|
-
title: "Enable update when oke.gate matches",
|
|
124
|
+
title: "Enable update when oke.gate() matches",
|
|
113
125
|
detail: `UPDATE when ${OKE_GATE} equals a Gate name.`,
|
|
114
126
|
command: "UPDATE",
|
|
115
127
|
using: `${OKE_GATE} = 'member'`,
|
|
@@ -119,9 +131,9 @@ export const RLS_POLICY_TEMPLATES: readonly RlsPolicyTemplate[] = [
|
|
|
119
131
|
id: "delete-owner",
|
|
120
132
|
name: "delete_owner",
|
|
121
133
|
title: "Enable delete for owners",
|
|
122
|
-
detail: "DELETE when a text owner column matches
|
|
134
|
+
detail: "DELETE when a text owner column matches oke.user().",
|
|
123
135
|
command: "DELETE",
|
|
124
|
-
using:
|
|
136
|
+
using: `owner = ${OKE_USER}`,
|
|
125
137
|
},
|
|
126
138
|
{
|
|
127
139
|
id: "deny-delete",
|
|
@@ -135,15 +147,15 @@ export const RLS_POLICY_TEMPLATES: readonly RlsPolicyTemplate[] = [
|
|
|
135
147
|
id: "all-owner",
|
|
136
148
|
name: "owner_all",
|
|
137
149
|
title: "Enable all commands for owners",
|
|
138
|
-
detail: "ALL commands when owner =
|
|
150
|
+
detail: "ALL commands when owner = oke.user() (USING and WITH CHECK).",
|
|
139
151
|
command: "ALL",
|
|
140
|
-
using:
|
|
141
|
-
withCheck:
|
|
152
|
+
using: `owner = ${OKE_USER}`,
|
|
153
|
+
withCheck: `owner = ${OKE_USER}`,
|
|
142
154
|
},
|
|
143
155
|
{
|
|
144
156
|
id: "all-gate",
|
|
145
157
|
name: "gate_all",
|
|
146
|
-
title: "Enable all commands when oke.gate matches",
|
|
158
|
+
title: "Enable all commands when oke.gate() matches",
|
|
147
159
|
detail: `ALL commands when ${OKE_GATE} equals a Gate name.`,
|
|
148
160
|
command: "ALL",
|
|
149
161
|
using: `${OKE_GATE} = 'member'`,
|
|
@@ -153,7 +165,7 @@ export const RLS_POLICY_TEMPLATES: readonly RlsPolicyTemplate[] = [
|
|
|
153
165
|
id: "restrictive-gate",
|
|
154
166
|
name: "restrictive_gate",
|
|
155
167
|
title: "Restrictive extra Gate check",
|
|
156
|
-
detail: "RESTRICTIVE policy ANDs with permissive ones. Requires oke.gate to be set.",
|
|
168
|
+
detail: "RESTRICTIVE policy ANDs with permissive ones. Requires oke.gate() to be set.",
|
|
157
169
|
command: "ALL",
|
|
158
170
|
behavior: "RESTRICTIVE",
|
|
159
171
|
using: `${OKE_GATE} is not null`,
|
|
@@ -262,17 +274,207 @@ export function rlsGateActionsForMode(mode: RlsGateMode): readonly string[] {
|
|
|
262
274
|
* @param spec - Policy fields
|
|
263
275
|
* @param enableRls - Also enable RLS on the table
|
|
264
276
|
*/
|
|
277
|
+
/**
|
|
278
|
+
* Code-dock sources: `store.schema.policy` plus Drizzle `pgPolicy`.
|
|
279
|
+
*
|
|
280
|
+
* @param spec - Policy fields
|
|
281
|
+
*/
|
|
282
|
+
export function rlsPolicyCodeSource(spec: SqlPolicySpec): string {
|
|
283
|
+
return `${emitStoreSchemaPolicySource(spec)}\n\n${emitPgPolicySource(spec)}`;
|
|
284
|
+
}
|
|
285
|
+
|
|
286
|
+
/**
|
|
287
|
+
* USING / WITH CHECK from Advanced Gate picks (never Postgres `TO`).
|
|
288
|
+
*
|
|
289
|
+
* @param gates - Selected policy / public / scope names
|
|
290
|
+
*/
|
|
291
|
+
export function rlsGatePredicateSql(gates: readonly string[]): string {
|
|
292
|
+
if (gates.length === 0) return "true";
|
|
293
|
+
return gates
|
|
294
|
+
.map((gate) => {
|
|
295
|
+
const lit = `'${gate.replaceAll("'", "''")}'`;
|
|
296
|
+
return gate.includes(":") ? `oke.has_scope(${lit})` : `oke.gate() = ${lit}`;
|
|
297
|
+
})
|
|
298
|
+
.join(" OR ");
|
|
299
|
+
}
|
|
300
|
+
|
|
265
301
|
export function rlsPolicyPreviewSql(spec: SqlPolicySpec, enableRls: boolean): string {
|
|
266
302
|
const create = formatCreatePolicyPreview(spec);
|
|
267
303
|
if (!enableRls) return `${create};`;
|
|
268
304
|
return `${buildRowSecuritySql(spec.table, true)};\n${create};`;
|
|
269
305
|
}
|
|
270
306
|
|
|
307
|
+
/** SQL names owner templates bind to, first match wins. */
|
|
308
|
+
const OWNER_SQL_NAMES = [
|
|
309
|
+
"owner",
|
|
310
|
+
"owner_id",
|
|
311
|
+
"owner_email",
|
|
312
|
+
"user_id",
|
|
313
|
+
"created_by",
|
|
314
|
+
"creator",
|
|
315
|
+
"creator_id",
|
|
316
|
+
"creator_email",
|
|
317
|
+
"author",
|
|
318
|
+
"author_id",
|
|
319
|
+
"author_email",
|
|
320
|
+
] as const;
|
|
321
|
+
|
|
322
|
+
/** One Manifest column for the identity select (PK / FK / unique marks). */
|
|
323
|
+
export type RlsTableColumn = {
|
|
324
|
+
readonly sqlName: string;
|
|
325
|
+
readonly primaryKey: boolean;
|
|
326
|
+
readonly foreignKey: boolean;
|
|
327
|
+
readonly unique: boolean;
|
|
328
|
+
/** FK inferred from `*_id` when Manifest omitted `.references()`. */
|
|
329
|
+
readonly inferred: boolean;
|
|
330
|
+
};
|
|
331
|
+
|
|
332
|
+
/**
|
|
333
|
+
* Manifest columns with PK / FK / unique marks (`sqlName`, else snake_case).
|
|
334
|
+
*
|
|
335
|
+
* @param manifest - Current Manifest, or null
|
|
336
|
+
* @param storeRef - Store effect ref (`sql:db`)
|
|
337
|
+
* @param table - Table name
|
|
338
|
+
*/
|
|
339
|
+
export function rlsTableColumns(
|
|
340
|
+
manifest: Manifest | null,
|
|
341
|
+
storeRef: string,
|
|
342
|
+
table: string,
|
|
343
|
+
): readonly RlsTableColumn[] {
|
|
344
|
+
const cut = storeRef.indexOf(":");
|
|
345
|
+
const storeName = cut >= 0 ? storeRef.slice(cut + 1) : storeRef;
|
|
346
|
+
const cols = manifest?.stores?.[storeName]?.tables?.[table]?.columns ?? {};
|
|
347
|
+
return Object.entries(cols).map(([key, col]) => {
|
|
348
|
+
const sqlName = sqlNameOf(key, col);
|
|
349
|
+
const declared = isDeclaredColumn(col) ? col : null;
|
|
350
|
+
const primaryKey = declared?.primaryKey === true;
|
|
351
|
+
const declaredFk = declared?.references?.table !== undefined;
|
|
352
|
+
const inferred = !declaredFk && !primaryKey && fkColumnStem(sqlName) !== null;
|
|
353
|
+
return {
|
|
354
|
+
sqlName,
|
|
355
|
+
primaryKey,
|
|
356
|
+
foreignKey: declaredFk || inferred,
|
|
357
|
+
unique: declared?.unique === true && !primaryKey,
|
|
358
|
+
inferred,
|
|
359
|
+
};
|
|
360
|
+
});
|
|
361
|
+
}
|
|
362
|
+
|
|
363
|
+
/**
|
|
364
|
+
* SQL column names on a Manifest table (`sqlName`, else snake_case key).
|
|
365
|
+
*
|
|
366
|
+
* @param manifest - Current Manifest, or null
|
|
367
|
+
* @param storeRef - Store effect ref (`sql:db`)
|
|
368
|
+
* @param table - Table name
|
|
369
|
+
*/
|
|
370
|
+
export function rlsTableSqlColumns(
|
|
371
|
+
manifest: Manifest | null,
|
|
372
|
+
storeRef: string,
|
|
373
|
+
table: string,
|
|
374
|
+
): readonly string[] {
|
|
375
|
+
return rlsTableColumns(manifest, storeRef, table).map((col) => col.sqlName);
|
|
376
|
+
}
|
|
377
|
+
|
|
378
|
+
function isDeclaredColumn(col: DeclaredColumn | ColumnClassification): col is DeclaredColumn {
|
|
379
|
+
return (
|
|
380
|
+
"type" in col ||
|
|
381
|
+
"nullable" in col ||
|
|
382
|
+
"primaryKey" in col ||
|
|
383
|
+
"unique" in col ||
|
|
384
|
+
"sqlName" in col ||
|
|
385
|
+
"description" in col ||
|
|
386
|
+
"references" in col ||
|
|
387
|
+
"default" in col
|
|
388
|
+
);
|
|
389
|
+
}
|
|
390
|
+
|
|
391
|
+
/**
|
|
392
|
+
* Best owner-like SQL column for owner templates, or null.
|
|
393
|
+
*
|
|
394
|
+
* @param sqlNames - Table SQL column names
|
|
395
|
+
*/
|
|
396
|
+
export function rlsOwnerColumn(sqlNames: readonly string[]): string | null {
|
|
397
|
+
const byLower = new Map(sqlNames.map((name) => [name.toLowerCase(), name]));
|
|
398
|
+
for (const name of OWNER_SQL_NAMES) {
|
|
399
|
+
const hit = byLower.get(name);
|
|
400
|
+
if (hit) return hit;
|
|
401
|
+
}
|
|
402
|
+
return null;
|
|
403
|
+
}
|
|
404
|
+
|
|
405
|
+
/**
|
|
406
|
+
* Replace the generic `owner` placeholder with the table's owner column.
|
|
407
|
+
*
|
|
408
|
+
* @param expr - USING / WITH CHECK body
|
|
409
|
+
* @param ownerCol - SQL column to bind
|
|
410
|
+
*/
|
|
411
|
+
export function rlsBindOwnerExpr(expr: string, ownerCol: string): string {
|
|
412
|
+
return expr.replace(/\bowner\b/g, ownerCol);
|
|
413
|
+
}
|
|
414
|
+
|
|
415
|
+
/**
|
|
416
|
+
* True when the expression still uses the unbound `owner` placeholder.
|
|
417
|
+
*
|
|
418
|
+
* @param expr - USING / WITH CHECK body
|
|
419
|
+
*/
|
|
420
|
+
export function rlsExprNeedsOwnerColumn(expr: string): boolean {
|
|
421
|
+
return /\bowner\b/.test(expr);
|
|
422
|
+
}
|
|
423
|
+
|
|
424
|
+
/**
|
|
425
|
+
* True when a template compares a row to `oke.user()` via `owner`.
|
|
426
|
+
*
|
|
427
|
+
* @param tpl - Policy template
|
|
428
|
+
*/
|
|
429
|
+
export function rlsTemplateUsesOwner(tpl: Pick<RlsPolicyTemplate, "using" | "withCheck">): boolean {
|
|
430
|
+
return rlsExprNeedsOwnerColumn(tpl.using ?? "") || rlsExprNeedsOwnerColumn(tpl.withCheck ?? "");
|
|
431
|
+
}
|
|
432
|
+
|
|
433
|
+
/**
|
|
434
|
+
* True when the expression is an owner / `oke.user()` identity check.
|
|
435
|
+
*
|
|
436
|
+
* @param expr - USING / WITH CHECK body
|
|
437
|
+
*/
|
|
438
|
+
export function rlsExprUsesUserIdentity(expr: string): boolean {
|
|
439
|
+
return rlsExprNeedsOwnerColumn(expr) || /oke\.user\(\)/.test(expr);
|
|
440
|
+
}
|
|
441
|
+
|
|
442
|
+
/**
|
|
443
|
+
* Rewrite `owner` or a previous identity column to `to`.
|
|
444
|
+
*
|
|
445
|
+
* @param expr - USING / WITH CHECK body
|
|
446
|
+
* @param from - Previous SQL column (or `owner`)
|
|
447
|
+
* @param to - Selected SQL column
|
|
448
|
+
*/
|
|
449
|
+
export function rlsRewriteIdentityColumn(expr: string, from: string, to: string): string {
|
|
450
|
+
if (!to || from === to) return expr;
|
|
451
|
+
let next = expr;
|
|
452
|
+
if (rlsExprNeedsOwnerColumn(next)) next = rlsBindOwnerExpr(next, to);
|
|
453
|
+
if (from !== "" && from !== "owner") {
|
|
454
|
+
next = next.replace(new RegExp(`\\b${escapeIdent(from)}\\b`, "g"), to);
|
|
455
|
+
}
|
|
456
|
+
return next;
|
|
457
|
+
}
|
|
458
|
+
|
|
459
|
+
function escapeIdent(name: string): string {
|
|
460
|
+
return name.replace(/[.*+?^${}()|[\]\\]/g, "\\$&");
|
|
461
|
+
}
|
|
462
|
+
|
|
463
|
+
function sqlNameOf(key: string, col: NonNullable<Table["columns"]>[string]): string {
|
|
464
|
+
if (col && typeof col === "object" && "sqlName" in col && typeof col.sqlName === "string") {
|
|
465
|
+
return col.sqlName;
|
|
466
|
+
}
|
|
467
|
+
return key
|
|
468
|
+
.replace(/([a-z0-9])([A-Z])/g, "$1_$2")
|
|
469
|
+
.replace(/([A-Z])([A-Z][a-z])/g, "$1_$2")
|
|
470
|
+
.toLowerCase();
|
|
471
|
+
}
|
|
472
|
+
|
|
271
473
|
function formatCreatePolicyPreview(spec: SqlPolicySpec): string {
|
|
272
474
|
const roles =
|
|
273
475
|
spec.roles.length === 0
|
|
274
476
|
? "public"
|
|
275
|
-
: spec.roles.map((role) => (role
|
|
477
|
+
: spec.roles.map((role) => formatPolicyRole(role)).join(", ");
|
|
276
478
|
const lines = [
|
|
277
479
|
`CREATE POLICY ${quotePgIdent(spec.name)}`,
|
|
278
480
|
` ON ${quotePgIdent(spec.table)}`,
|
|
@@ -35,6 +35,8 @@ export type SchemaGraphTable = {
|
|
|
35
35
|
readonly storeName: string;
|
|
36
36
|
readonly storeRef: string;
|
|
37
37
|
readonly columns: readonly SchemaGraphColumn[];
|
|
38
|
+
/** Manifest `rls` or any declared policies. */
|
|
39
|
+
readonly rls?: boolean;
|
|
38
40
|
};
|
|
39
41
|
|
|
40
42
|
/** Custom data on a schema-visualizer table node. */
|
|
@@ -138,11 +140,13 @@ export function schemaGraphTables(
|
|
|
138
140
|
if (store.facet !== "sql") continue;
|
|
139
141
|
for (const child of store.children) {
|
|
140
142
|
if (isSqlCatalogChild(child)) continue;
|
|
143
|
+
const declared = manifest?.stores?.[store.name]?.tables?.[child.name];
|
|
141
144
|
out.push({
|
|
142
145
|
id: child.effectRef,
|
|
143
146
|
name: child.name,
|
|
144
147
|
storeName: store.name,
|
|
145
148
|
storeRef: store.ref,
|
|
149
|
+
rls: declared?.rls === true || Object.keys(declared?.policies ?? {}).length > 0,
|
|
146
150
|
columns: columnsForChild(
|
|
147
151
|
store,
|
|
148
152
|
child.name,
|
|
@@ -6,6 +6,7 @@ import {
|
|
|
6
6
|
isSqlExtensionChild,
|
|
7
7
|
isSqlPolicyChild,
|
|
8
8
|
storeChildLabel,
|
|
9
|
+
storeChildRlsPolicyCount,
|
|
9
10
|
storeChildShowsRls,
|
|
10
11
|
} from "./sql-catalog.ts";
|
|
11
12
|
|
|
@@ -48,6 +49,29 @@ describe("sql catalog children", () => {
|
|
|
48
49
|
expect(storeChildShowsRls(child("policies", "policy"))).toBe(false);
|
|
49
50
|
});
|
|
50
51
|
|
|
52
|
+
test("RLS policy count prefers the store-list stamp", () => {
|
|
53
|
+
const stamped = { ...child("bookings", "table"), rls: true, rlsPolicyCount: 2 };
|
|
54
|
+
expect(storeChildRlsPolicyCount(stamped, null, "db")).toBe(2);
|
|
55
|
+
expect(
|
|
56
|
+
storeChildRlsPolicyCount(
|
|
57
|
+
child("bookings", "table"),
|
|
58
|
+
{
|
|
59
|
+
oke: "1.0",
|
|
60
|
+
app: "t",
|
|
61
|
+
stores: {
|
|
62
|
+
db: {
|
|
63
|
+
facet: "sql",
|
|
64
|
+
tables: {
|
|
65
|
+
bookings: { policies: { mine: { using: "true" }, team: { using: "true" } } },
|
|
66
|
+
},
|
|
67
|
+
},
|
|
68
|
+
},
|
|
69
|
+
},
|
|
70
|
+
"db",
|
|
71
|
+
),
|
|
72
|
+
).toBe(2);
|
|
73
|
+
});
|
|
74
|
+
|
|
51
75
|
test("splits tables from catalog folders", () => {
|
|
52
76
|
const grouped = groupSqlChildren([
|
|
53
77
|
child("bookings", "table"),
|
|
@@ -3,6 +3,7 @@
|
|
|
3
3
|
* extensions / RLS policies).
|
|
4
4
|
*/
|
|
5
5
|
|
|
6
|
+
import type { Manifest } from "../../../../../../manifest/types.ts";
|
|
6
7
|
import type { StoreListChild } from "@/client.ts";
|
|
7
8
|
|
|
8
9
|
/** Catalog kind on a SQL store child. */
|
|
@@ -64,6 +65,24 @@ export function storeChildShowsRls(child: StoreListChild): boolean {
|
|
|
64
65
|
return child.kind === "table" && typeof child.rls === "boolean";
|
|
65
66
|
}
|
|
66
67
|
|
|
68
|
+
/**
|
|
69
|
+
* Policy count for the resource-header RLS chip.
|
|
70
|
+
*
|
|
71
|
+
* Prefers the store-list stamp; falls back to Manifest `table.policies`.
|
|
72
|
+
*
|
|
73
|
+
* @param child - Store-list child
|
|
74
|
+
* @param manifest - Current Manifest, or null
|
|
75
|
+
* @param storeName - Manifest store key
|
|
76
|
+
*/
|
|
77
|
+
export function storeChildRlsPolicyCount(
|
|
78
|
+
child: StoreListChild,
|
|
79
|
+
manifest: Manifest | null,
|
|
80
|
+
storeName: string,
|
|
81
|
+
): number {
|
|
82
|
+
if (typeof child.rlsPolicyCount === "number") return child.rlsPolicyCount;
|
|
83
|
+
return Object.keys(manifest?.stores?.[storeName]?.tables?.[child.name]?.policies ?? {}).length;
|
|
84
|
+
}
|
|
85
|
+
|
|
67
86
|
/**
|
|
68
87
|
* Tree / header label for a child (`Indexes` instead of `indexes`).
|
|
69
88
|
*
|
|
@@ -18,6 +18,7 @@ import {
|
|
|
18
18
|
storeTreeIsOpen,
|
|
19
19
|
storeTreeOpenKeys,
|
|
20
20
|
isSingletonStoreLeaf,
|
|
21
|
+
resourceHeaderChildRef,
|
|
21
22
|
storeTreeTablesKey,
|
|
22
23
|
toggleHiddenFacet,
|
|
23
24
|
visibleFacetBands,
|
|
@@ -143,6 +144,11 @@ describe("store-tree", () => {
|
|
|
143
144
|
expect(storeTreeAncestorKeys(STORES, "index:comments")).toEqual(["facet:index"]);
|
|
144
145
|
});
|
|
145
146
|
|
|
147
|
+
test("resource header drops the child ref when it repeats the store", () => {
|
|
148
|
+
expect(resourceHeaderChildRef("kv:reminders", "kv:reminders")).toBeNull();
|
|
149
|
+
expect(resourceHeaderChildRef("sql:db", "sql:shipments")).toBe("sql:shipments");
|
|
150
|
+
});
|
|
151
|
+
|
|
146
152
|
test("ancestor keys open the facet, store, and Tables folder for a table", () => {
|
|
147
153
|
expect(storeTreeAncestorKeys(STORES, "sql:shipments")).toEqual([
|
|
148
154
|
"facet:sql",
|
|
@@ -6,6 +6,8 @@ import { DatabaseIcon, File01Icon, Key01Icon, Search01Icon } from "@hugeicons/co
|
|
|
6
6
|
import type { StoreFacet, StoreListChild, StoreListStore } from "@/client.ts";
|
|
7
7
|
import type { ElementHugeIcon } from "@/lib/element-icons.ts";
|
|
8
8
|
import { isStoreFacet, STORE_FACETS } from "./parse-resource-ref.ts";
|
|
9
|
+
|
|
10
|
+
export { STORE_FACETS };
|
|
9
11
|
import { isSqlCatalogChild, storeChildLabel } from "./sql-catalog.ts";
|
|
10
12
|
|
|
11
13
|
/** Facet band label for the left explorer. */
|
|
@@ -153,6 +155,17 @@ export function isSingletonStoreLeaf(store: StoreListStore): boolean {
|
|
|
153
155
|
return store.children.length === 1 && only !== undefined && only.name === store.name;
|
|
154
156
|
}
|
|
155
157
|
|
|
158
|
+
/**
|
|
159
|
+
* Child ref in the resource header. Null when it repeats the store
|
|
160
|
+
* (`kv:reminders · kv:reminders`).
|
|
161
|
+
*
|
|
162
|
+
* @param storeRef - Store identity (`kv:reminders`)
|
|
163
|
+
* @param effectRef - Selected child (`kv:reminders` or `sql:tasks`)
|
|
164
|
+
*/
|
|
165
|
+
export function resourceHeaderChildRef(storeRef: string, effectRef: string): string | null {
|
|
166
|
+
return storeRef === effectRef ? null : effectRef;
|
|
167
|
+
}
|
|
168
|
+
|
|
156
169
|
/**
|
|
157
170
|
* Open-state key for a facet band in {@link StoreTree}.
|
|
158
171
|
*
|
|
@@ -60,9 +60,10 @@ import { kvSetPatch, parseKvQuery } from "../lib/kv-query.ts";
|
|
|
60
60
|
import {
|
|
61
61
|
defaultKvQuery,
|
|
62
62
|
defaultSqlQuery,
|
|
63
|
-
|
|
63
|
+
randomChildName,
|
|
64
64
|
isConsoleAuthStore,
|
|
65
65
|
pickQueryStore,
|
|
66
|
+
reconcileDefaultQuery,
|
|
66
67
|
} from "../lib/query-defaults.ts";
|
|
67
68
|
import {
|
|
68
69
|
addQueryTab,
|
|
@@ -138,10 +139,12 @@ export function QueryConsole({
|
|
|
138
139
|
);
|
|
139
140
|
const store = ofFacet.find((s) => s.ref === storeRef) ?? pickQueryStore(ofFacet, facet, null);
|
|
140
141
|
const seed = seedEditor(store, facet, selectedEffectRef);
|
|
142
|
+
const childNames = store?.children.map((child) => child.name) ?? [];
|
|
141
143
|
|
|
142
|
-
const [tabs, setTabs] = useState<readonly QueryTab[]>(
|
|
143
|
-
|
|
144
|
-
|
|
144
|
+
const [tabs, setTabs] = useState<readonly QueryTab[]>(() => {
|
|
145
|
+
const loaded = loadQueryTabs(facet) ?? [{ id: "q_1", title: "Query 1", text: seed }];
|
|
146
|
+
return reconcileLoadedTabs(loaded, childNames, facet);
|
|
147
|
+
});
|
|
145
148
|
const [activeId, setActiveId] = useState(() => tabs[0]?.id ?? "q_1");
|
|
146
149
|
const active = tabs.find((t) => t.id === activeId) ?? tabs[0];
|
|
147
150
|
const text = active?.text ?? seed;
|
|
@@ -153,7 +156,10 @@ export function QueryConsole({
|
|
|
153
156
|
const [kvPending, setKvPending] = useState(false);
|
|
154
157
|
const [schemaOpen, setSchemaOpen] = useState(false);
|
|
155
158
|
const [piiMasked, setPiiMasked] = useState(true);
|
|
156
|
-
const [
|
|
159
|
+
const [rlsAs, setRlsAs] = useState<{ asGate: string | null; asUserId: string | null }>({
|
|
160
|
+
asGate: null,
|
|
161
|
+
asUserId: null,
|
|
162
|
+
});
|
|
157
163
|
const [resultsOpen, setResultsOpen] = useState(true);
|
|
158
164
|
const [history, setHistory] = useState<readonly QueryHistoryEntry[]>(() =>
|
|
159
165
|
loadQueryHistory(facet),
|
|
@@ -194,6 +200,13 @@ export function QueryConsole({
|
|
|
194
200
|
setStoreRef((prev) => (ofFacet.some((s) => s.ref === prev) ? prev : next.ref));
|
|
195
201
|
}, [stores, facet, selectedEffectRef, ofFacet]);
|
|
196
202
|
|
|
203
|
+
const childKey = childNames.join("\0");
|
|
204
|
+
useEffect(() => {
|
|
205
|
+
const names = childKey.length === 0 ? [] : childKey.split("\0");
|
|
206
|
+
if (names.length === 0) return;
|
|
207
|
+
setTabs((prev) => reconcileLoadedTabs(prev, names, facet));
|
|
208
|
+
}, [childKey, facet]);
|
|
209
|
+
|
|
197
210
|
useEffect(() => {
|
|
198
211
|
saveQueryTabs(facet, tabs);
|
|
199
212
|
}, [facet, tabs]);
|
|
@@ -256,7 +269,7 @@ export function QueryConsole({
|
|
|
256
269
|
setExecuted(set?.executed ?? next.map((row) => row.executed).join("\n") ?? null);
|
|
257
270
|
};
|
|
258
271
|
|
|
259
|
-
const runBatch = (texts: readonly string[], includePii = !piiMasked, gate =
|
|
272
|
+
const runBatch = (texts: readonly string[], includePii = !piiMasked, gate = rlsAs): void => {
|
|
260
273
|
if (!store) return;
|
|
261
274
|
setResultsOpen(true);
|
|
262
275
|
setError(null);
|
|
@@ -290,7 +303,8 @@ export function QueryConsole({
|
|
|
290
303
|
sql: sqlText,
|
|
291
304
|
...(writing ? { allowWrite: true } : {}),
|
|
292
305
|
...(includePii ? { revealPii: true } : {}),
|
|
293
|
-
...(gate ? { asGate: gate } : {}),
|
|
306
|
+
...(gate.asGate ? { asGate: gate.asGate } : {}),
|
|
307
|
+
...(gate.asUserId ? { asUserId: gate.asUserId } : {}),
|
|
294
308
|
...(tenant ? { tenant } : {}),
|
|
295
309
|
});
|
|
296
310
|
const changed = writeChanges(data.rows);
|
|
@@ -302,8 +316,13 @@ export function QueryConsole({
|
|
|
302
316
|
];
|
|
303
317
|
if (data.masked) bits.push("PII masked");
|
|
304
318
|
else if (includePii) bits.push("PII visible");
|
|
305
|
-
if (data.asGate) {
|
|
306
|
-
|
|
319
|
+
if (data.rls?.gate ?? data.asGate) {
|
|
320
|
+
const label = data.rls?.userId
|
|
321
|
+
? `${data.rls.gate} / ${data.rls.userId}`
|
|
322
|
+
: (data.rls?.gate ?? data.asGate);
|
|
323
|
+
bits.push(
|
|
324
|
+
data.gateApplied || data.rls?.applied ? `as ${label}` : `as ${label} (not applied)`,
|
|
325
|
+
);
|
|
307
326
|
}
|
|
308
327
|
nextSets.push({
|
|
309
328
|
label: sqlStatementLabel(sqlText),
|
|
@@ -351,7 +370,7 @@ export function QueryConsole({
|
|
|
351
370
|
})();
|
|
352
371
|
};
|
|
353
372
|
|
|
354
|
-
const runText = (sqlText: string, includePii = !piiMasked, gate =
|
|
373
|
+
const runText = (sqlText: string, includePii = !piiMasked, gate = rlsAs): void => {
|
|
355
374
|
if (!store) return;
|
|
356
375
|
setResultsOpen(true);
|
|
357
376
|
setError(null);
|
|
@@ -876,9 +895,9 @@ export function QueryConsole({
|
|
|
876
895
|
{isSql ? (
|
|
877
896
|
<QueryGateMenu
|
|
878
897
|
manifest={manifest}
|
|
879
|
-
|
|
898
|
+
value={rlsAs}
|
|
880
899
|
onChange={(next) => {
|
|
881
|
-
|
|
900
|
+
setRlsAs(next);
|
|
882
901
|
if (executed && !isSqlWrite(executed)) {
|
|
883
902
|
runText(executed, !piiMasked, next);
|
|
884
903
|
}
|
|
@@ -916,7 +935,6 @@ export function QueryConsole({
|
|
|
916
935
|
<HugeiconsIcon icon={PlayIcon} className="size-3.5" />
|
|
917
936
|
)}
|
|
918
937
|
{selected ? "Run selection" : "Run"}
|
|
919
|
-
<ShortcutKeys keys={runKeys} className="hidden sm:inline-flex" />
|
|
920
938
|
</Button>
|
|
921
939
|
</ToolbarTip>
|
|
922
940
|
{isSql ? (
|
|
@@ -1060,6 +1078,22 @@ function writeChanges(rows: readonly QueryResultRow[]): number | null {
|
|
|
1060
1078
|
return typeof changes === "number" && Number.isFinite(changes) ? changes : null;
|
|
1061
1079
|
}
|
|
1062
1080
|
|
|
1081
|
+
/** Rewrite leftover default seeds that name a table the live store dropped. */
|
|
1082
|
+
function reconcileLoadedTabs(
|
|
1083
|
+
tabs: readonly QueryTab[],
|
|
1084
|
+
names: readonly string[],
|
|
1085
|
+
facet: StoreQueryFacet,
|
|
1086
|
+
): readonly QueryTab[] {
|
|
1087
|
+
let changed = false;
|
|
1088
|
+
const next = tabs.map((tab) => {
|
|
1089
|
+
const text = reconcileDefaultQuery(tab.text, names, facet);
|
|
1090
|
+
if (text === tab.text) return tab;
|
|
1091
|
+
changed = true;
|
|
1092
|
+
return { ...tab, text };
|
|
1093
|
+
});
|
|
1094
|
+
return changed ? next : tabs;
|
|
1095
|
+
}
|
|
1096
|
+
|
|
1063
1097
|
function seedEditor(
|
|
1064
1098
|
store: StoreListStore | null | undefined,
|
|
1065
1099
|
facet: StoreQueryFacet,
|
|
@@ -1069,7 +1103,7 @@ function seedEditor(
|
|
|
1069
1103
|
const selected = selectedEffectRef
|
|
1070
1104
|
? store.children.find((c) => c.effectRef === selectedEffectRef)?.name
|
|
1071
1105
|
: undefined;
|
|
1072
|
-
const name = selected ??
|
|
1106
|
+
const name = selected ?? randomChildName(store);
|
|
1073
1107
|
return facet === "sql" ? defaultSqlQuery(name) : defaultKvQuery(name ? `${name}:` : undefined);
|
|
1074
1108
|
}
|
|
1075
1109
|
|