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
|
@@ -60,6 +60,7 @@ import { modChord, modShiftChord } from "@/lib/shortcut.ts";
|
|
|
60
60
|
import { cn } from "@/lib/utils.ts";
|
|
61
61
|
import { useStoreEdit } from "../data/use-store-edit.ts";
|
|
62
62
|
import { JsonValueSheet } from "../detail/json-value-sheet.tsx";
|
|
63
|
+
import { StoreRowDetailSheet } from "../detail/store-row-detail-sheet.tsx";
|
|
63
64
|
import { RevealCell } from "../detail/reveal-cell.tsx";
|
|
64
65
|
import { asInspectableJson, jsonValueEqual } from "../lib/json-value.ts";
|
|
65
66
|
import { parseKvTtlDraft } from "../lib/kv-meta.ts";
|
|
@@ -132,12 +133,22 @@ export interface StoreDataGridProps {
|
|
|
132
133
|
readonly insertOpen?: boolean;
|
|
133
134
|
readonly onInsertOpenChange?: (open: boolean) => void;
|
|
134
135
|
readonly onDeleteRows?: (rows: readonly StoreGridRow[]) => void;
|
|
136
|
+
/** Gate identity for row DML (`oke.*` stamp). Catalog DDL ignores this. */
|
|
137
|
+
readonly asGate?: string | null;
|
|
138
|
+
readonly asUserId?: string | null;
|
|
139
|
+
/** Two-finger / right-click empty grid (Create policy / insert). */
|
|
140
|
+
readonly onViewportContextMenu?: () => void;
|
|
135
141
|
}
|
|
136
142
|
|
|
137
143
|
/** Row height presets (px) for the height menu. */
|
|
138
144
|
const ROW_HEIGHTS = { short: 26, medium: 34, tall: 44, "extra-tall": 56 } as const;
|
|
139
145
|
type RowHeight = keyof typeof ROW_HEIGHTS;
|
|
140
146
|
|
|
147
|
+
/** Payload columns absorb leftover table width (SQL `comment`, KV `value`). */
|
|
148
|
+
function isFillColumn(key: string): boolean {
|
|
149
|
+
return key === "comment" || key === "value";
|
|
150
|
+
}
|
|
151
|
+
|
|
141
152
|
/** Fixed column width (px) by cell variant. */
|
|
142
153
|
function columnWidthPx(type: StoreGridColumn["type"], key: string): number {
|
|
143
154
|
if (key === "id" || key === "key" || key === "name") return 192;
|
|
@@ -231,6 +242,9 @@ export function StoreDataGrid({
|
|
|
231
242
|
insertOpen = false,
|
|
232
243
|
onInsertOpenChange,
|
|
233
244
|
onDeleteRows,
|
|
245
|
+
asGate = null,
|
|
246
|
+
asUserId = null,
|
|
247
|
+
onViewportContextMenu,
|
|
234
248
|
}: StoreDataGridProps): JSX.Element {
|
|
235
249
|
const [findText, setFindText] = useState("");
|
|
236
250
|
const [selected, setSelected] = useState<readonly string[]>([]);
|
|
@@ -250,6 +264,8 @@ export function StoreDataGrid({
|
|
|
250
264
|
readonly rowId: string;
|
|
251
265
|
readonly column: string;
|
|
252
266
|
} | null>(null);
|
|
267
|
+
const [detailRow, setDetailRow] = useState<StoreGridRow | null>(null);
|
|
268
|
+
const [detailOpen, setDetailOpen] = useState(false);
|
|
253
269
|
const [addOpen, setAddOpen] = useState(false);
|
|
254
270
|
const [saving, setSaving] = useState(false);
|
|
255
271
|
const [viewportHeight, setViewportHeight] = useState(440);
|
|
@@ -533,6 +549,8 @@ export function StoreDataGrid({
|
|
|
533
549
|
ref: storeRef,
|
|
534
550
|
...(facet === "sql" ? { child: childName, id: rowId } : { key: rowId }),
|
|
535
551
|
...(tenant ? { tenant } : {}),
|
|
552
|
+
...(asGate ? { asGate } : {}),
|
|
553
|
+
...(asUserId ? { asUserId } : {}),
|
|
536
554
|
patch: sanitizeStorePatch(patch),
|
|
537
555
|
commit: true,
|
|
538
556
|
}),
|
|
@@ -758,8 +776,8 @@ export function StoreDataGrid({
|
|
|
758
776
|
header: <StoreColumnHeader col={col} />,
|
|
759
777
|
sortable: true,
|
|
760
778
|
align: isNumericType(col.type) ? "right" : "left",
|
|
761
|
-
width: col.key
|
|
762
|
-
fill: col.key
|
|
779
|
+
width: isFillColumn(col.key) ? undefined : `${columnWidthPx(col.type, col.key)}px`,
|
|
780
|
+
fill: isFillColumn(col.key),
|
|
763
781
|
sortValue: (row) => {
|
|
764
782
|
const raw = overlayValue(row, col.key);
|
|
765
783
|
if (typeof raw === "number") return raw;
|
|
@@ -1260,6 +1278,11 @@ export function StoreDataGrid({
|
|
|
1260
1278
|
}
|
|
1261
1279
|
: undefined
|
|
1262
1280
|
}
|
|
1281
|
+
onRowContextMenu={(row) => {
|
|
1282
|
+
setDetailRow(row);
|
|
1283
|
+
setDetailOpen(true);
|
|
1284
|
+
}}
|
|
1285
|
+
onViewportContextMenu={onViewportContextMenu}
|
|
1263
1286
|
cellRange={cellRange}
|
|
1264
1287
|
onCellRangeChange={
|
|
1265
1288
|
canInlineEdit
|
|
@@ -1324,6 +1347,8 @@ export function StoreDataGrid({
|
|
|
1324
1347
|
childName={childName}
|
|
1325
1348
|
tenant={tenant}
|
|
1326
1349
|
columns={model.columns}
|
|
1350
|
+
asGate={asGate}
|
|
1351
|
+
asUserId={asUserId}
|
|
1327
1352
|
/>
|
|
1328
1353
|
) : null}
|
|
1329
1354
|
|
|
@@ -1338,6 +1363,28 @@ export function StoreDataGrid({
|
|
|
1338
1363
|
/>
|
|
1339
1364
|
) : null}
|
|
1340
1365
|
|
|
1366
|
+
<StoreRowDetailSheet
|
|
1367
|
+
open={detailOpen}
|
|
1368
|
+
onOpenChange={setDetailOpen}
|
|
1369
|
+
model={model}
|
|
1370
|
+
row={detailRow ? (model.rows.find((row) => row.id === detailRow.id) ?? detailRow) : null}
|
|
1371
|
+
facet={facet}
|
|
1372
|
+
storeRef={storeRef}
|
|
1373
|
+
childName={childName}
|
|
1374
|
+
tenant={tenant}
|
|
1375
|
+
masked={masked}
|
|
1376
|
+
asGate={asGate}
|
|
1377
|
+
asUserId={asUserId}
|
|
1378
|
+
onDeleteRow={
|
|
1379
|
+
onDeleteRows
|
|
1380
|
+
? (row) => {
|
|
1381
|
+
setDetailOpen(false);
|
|
1382
|
+
onDeleteRows([row]);
|
|
1383
|
+
}
|
|
1384
|
+
: undefined
|
|
1385
|
+
}
|
|
1386
|
+
/>
|
|
1387
|
+
|
|
1341
1388
|
{inspect && inspectRow && inspectCol ? (
|
|
1342
1389
|
<JsonValueSheet
|
|
1343
1390
|
open
|
|
@@ -1443,7 +1490,7 @@ export function StoreDataGrid({
|
|
|
1443
1490
|
</span>
|
|
1444
1491
|
) : (
|
|
1445
1492
|
<span className="ml-auto opacity-60">
|
|
1446
|
-
Click to select · drag a range ·
|
|
1493
|
+
Click to select · two-finger click opens sheet · drag a range · type to edit
|
|
1447
1494
|
</span>
|
|
1448
1495
|
)}
|
|
1449
1496
|
</div>
|
|
@@ -8,6 +8,7 @@ import {
|
|
|
8
8
|
fileNameFromKey,
|
|
9
9
|
filePreviewMode,
|
|
10
10
|
formatFilePreviewText,
|
|
11
|
+
previewBlobType,
|
|
11
12
|
inferContentType,
|
|
12
13
|
isAsciiObjectKey,
|
|
13
14
|
safeFileObjectKey,
|
|
@@ -66,6 +67,8 @@ describe("fileKind helpers", () => {
|
|
|
66
67
|
expect(fileKindIsText("code")).toBe(true);
|
|
67
68
|
expect(fileKindIsText("image")).toBe(false);
|
|
68
69
|
expect(fileKindIsImage("image")).toBe(true);
|
|
70
|
+
expect(previewBlobType("pdf", "application/octet-stream")).toBe("application/pdf");
|
|
71
|
+
expect(previewBlobType("image", "image/png")).toBe("image/png");
|
|
69
72
|
expect(filePreviewMode("pdf")).toBe("pdf");
|
|
70
73
|
expect(filePreviewMode("video")).toBe("video");
|
|
71
74
|
expect(filePreviewMode("archive")).toBe("none");
|
|
@@ -219,6 +219,18 @@ export function fileKindIsImage(kind: FileKind): boolean {
|
|
|
219
219
|
return kind === "image";
|
|
220
220
|
}
|
|
221
221
|
|
|
222
|
+
/**
|
|
223
|
+
* MIME type for a files-preview blob.
|
|
224
|
+
* PDF is always `application/pdf` so `nosniff` does not hide the viewer.
|
|
225
|
+
*
|
|
226
|
+
* @param mode - Inspector surface
|
|
227
|
+
* @param contentType - Catalog / inferred type
|
|
228
|
+
*/
|
|
229
|
+
export function previewBlobType(mode: FilePreviewMode, contentType: string): string {
|
|
230
|
+
if (mode === "pdf") return "application/pdf";
|
|
231
|
+
return contentType;
|
|
232
|
+
}
|
|
233
|
+
|
|
222
234
|
/**
|
|
223
235
|
* Inspector surface for a kind (`none` → download fallback).
|
|
224
236
|
*
|
|
@@ -5,7 +5,10 @@ import {
|
|
|
5
5
|
defaultKvQuery,
|
|
6
6
|
defaultSqlQuery,
|
|
7
7
|
isConsoleAuthStore,
|
|
8
|
+
pickChildName,
|
|
8
9
|
pickQueryStore,
|
|
10
|
+
reconcileDefaultKv,
|
|
11
|
+
reconcileDefaultSql,
|
|
9
12
|
} from "./query-defaults.ts";
|
|
10
13
|
|
|
11
14
|
const emptyWillNot = { writerFlowIds: [], signals: [], channels: [] };
|
|
@@ -53,6 +56,28 @@ describe("default queries", () => {
|
|
|
53
56
|
expect(defaultKvQuery("holds:")).toContain("list holds:");
|
|
54
57
|
expect(defaultKvQuery()).toContain("list");
|
|
55
58
|
});
|
|
59
|
+
|
|
60
|
+
test("picks a random live child", () => {
|
|
61
|
+
expect(pickChildName(["activity", "tasks"], () => 0)).toBe("activity");
|
|
62
|
+
expect(pickChildName(["activity", "tasks"], () => 0.99)).toBe("tasks");
|
|
63
|
+
expect(pickChildName([])).toBeUndefined();
|
|
64
|
+
});
|
|
65
|
+
|
|
66
|
+
test("rewrites a leftover default select when the table is gone", () => {
|
|
67
|
+
const stale = defaultSqlQuery("issues");
|
|
68
|
+
expect(reconcileDefaultSql(stale, ["activity", "tasks"], () => 0.99)).toBe(
|
|
69
|
+
defaultSqlQuery("tasks"),
|
|
70
|
+
);
|
|
71
|
+
expect(reconcileDefaultSql(stale, ["issues"])).toBe(stale);
|
|
72
|
+
expect(reconcileDefaultSql("SELECT 1 FROM issues", ["activity"])).toBe("SELECT 1 FROM issues");
|
|
73
|
+
});
|
|
74
|
+
|
|
75
|
+
test("rewrites a leftover default kv list when the namespace is gone", () => {
|
|
76
|
+
const stale = defaultKvQuery("sessions:");
|
|
77
|
+
expect(reconcileDefaultKv(stale, ["holds", "locks"], () => 0)).toBe(defaultKvQuery("holds:"));
|
|
78
|
+
expect(reconcileDefaultKv(stale, ["sessions"])).toBe(stale);
|
|
79
|
+
expect(reconcileDefaultKv("get sessions:a", ["holds"])).toBe("get sessions:a");
|
|
80
|
+
});
|
|
56
81
|
});
|
|
57
82
|
|
|
58
83
|
describe("pickQueryStore", () => {
|
|
@@ -17,6 +17,66 @@ export function defaultSqlQuery(table?: string): string {
|
|
|
17
17
|
return `SELECT *\nFROM "${table}"\nLIMIT 50;`;
|
|
18
18
|
}
|
|
19
19
|
|
|
20
|
+
const DEFAULT_SQL = /^SELECT \*\nFROM "([^"]+)"\nLIMIT 50;$/;
|
|
21
|
+
const DEFAULT_KV = /^\/\/ list · get · set · delete · ttl\nlist(?: (.+))?$/;
|
|
22
|
+
|
|
23
|
+
/**
|
|
24
|
+
* Replace a leftover default `SELECT` when that table is gone.
|
|
25
|
+
* Custom SQL is left alone.
|
|
26
|
+
*
|
|
27
|
+
* @param text - Editor buffer
|
|
28
|
+
* @param tables - Live table names
|
|
29
|
+
* @param random - Unit interval used to pick a replacement table
|
|
30
|
+
*/
|
|
31
|
+
export function reconcileDefaultSql(
|
|
32
|
+
text: string,
|
|
33
|
+
tables: readonly string[],
|
|
34
|
+
random: () => number = Math.random,
|
|
35
|
+
): string {
|
|
36
|
+
const match = DEFAULT_SQL.exec(text);
|
|
37
|
+
if (!match) return text;
|
|
38
|
+
const table = match[1];
|
|
39
|
+
if (table && tables.includes(table)) return text;
|
|
40
|
+
return defaultSqlQuery(pickChildName(tables, random));
|
|
41
|
+
}
|
|
42
|
+
|
|
43
|
+
/**
|
|
44
|
+
* Replace a leftover default `list ns:` when that namespace is gone.
|
|
45
|
+
*
|
|
46
|
+
* @param text - Editor buffer
|
|
47
|
+
* @param namespaces - Live namespace names
|
|
48
|
+
* @param random - Unit interval used to pick a replacement namespace
|
|
49
|
+
*/
|
|
50
|
+
export function reconcileDefaultKv(
|
|
51
|
+
text: string,
|
|
52
|
+
namespaces: readonly string[],
|
|
53
|
+
random: () => number = Math.random,
|
|
54
|
+
): string {
|
|
55
|
+
const match = DEFAULT_KV.exec(text);
|
|
56
|
+
if (!match) return text;
|
|
57
|
+
const prefix = match[1];
|
|
58
|
+
if (!prefix) return text;
|
|
59
|
+
const name = prefix.endsWith(":") ? prefix.slice(0, -1) : prefix;
|
|
60
|
+
if (namespaces.includes(name)) return text;
|
|
61
|
+
const next = pickChildName(namespaces, random);
|
|
62
|
+
return defaultKvQuery(next ? `${next}:` : undefined);
|
|
63
|
+
}
|
|
64
|
+
|
|
65
|
+
/**
|
|
66
|
+
* Re-seed a default buffer against the live store children.
|
|
67
|
+
*
|
|
68
|
+
* @param text - Editor buffer
|
|
69
|
+
* @param names - Table or namespace names
|
|
70
|
+
* @param facet - SQL or KV
|
|
71
|
+
*/
|
|
72
|
+
export function reconcileDefaultQuery(
|
|
73
|
+
text: string,
|
|
74
|
+
names: readonly string[],
|
|
75
|
+
facet: Extract<StoreFacet, "sql" | "kv">,
|
|
76
|
+
): string {
|
|
77
|
+
return facet === "sql" ? reconcileDefaultSql(text, names) : reconcileDefaultKv(text, names);
|
|
78
|
+
}
|
|
79
|
+
|
|
20
80
|
/**
|
|
21
81
|
* Seed KV command for a namespace prefix.
|
|
22
82
|
*
|
|
@@ -59,12 +119,34 @@ export function pickQueryStore(
|
|
|
59
119
|
}
|
|
60
120
|
|
|
61
121
|
/**
|
|
62
|
-
*
|
|
122
|
+
* One live child name, picked at random.
|
|
123
|
+
*
|
|
124
|
+
* @param names - Table or namespace names
|
|
125
|
+
* @param random - Unit interval in `[0, 1)`
|
|
126
|
+
*/
|
|
127
|
+
export function pickChildName(
|
|
128
|
+
names: readonly string[],
|
|
129
|
+
random: () => number = Math.random,
|
|
130
|
+
): string | undefined {
|
|
131
|
+
if (names.length === 0) return undefined;
|
|
132
|
+
const index = Math.min(names.length - 1, Math.floor(random() * names.length));
|
|
133
|
+
return names[index];
|
|
134
|
+
}
|
|
135
|
+
|
|
136
|
+
/**
|
|
137
|
+
* Default child name under a store (random table / namespace).
|
|
63
138
|
*
|
|
64
139
|
* @param store - Store row
|
|
140
|
+
* @param random - Unit interval used to pick
|
|
65
141
|
*/
|
|
66
|
-
export function
|
|
67
|
-
|
|
142
|
+
export function randomChildName(
|
|
143
|
+
store: StoreListStore,
|
|
144
|
+
random: () => number = Math.random,
|
|
145
|
+
): string | undefined {
|
|
146
|
+
return pickChildName(
|
|
147
|
+
store.children.map((child) => child.name),
|
|
148
|
+
random,
|
|
149
|
+
);
|
|
68
150
|
}
|
|
69
151
|
|
|
70
152
|
/**
|
|
@@ -1,9 +1,8 @@
|
|
|
1
1
|
/**
|
|
2
2
|
* Query-console "view as Gate" choices — Operator, public, user, or policy.
|
|
3
3
|
*
|
|
4
|
-
* Gates are OKE authorization, not Postgres `TO` roles. Selecting one
|
|
5
|
-
* `oke.gate`
|
|
6
|
-
* `current_setting('oke.gate', true)`.
|
|
4
|
+
* Gates are OKE authorization, not Postgres `TO` roles. Selecting one stamps
|
|
5
|
+
* `oke.gate()` / `oke.user()` / `oke.has_scope()` on postgres / pglite.
|
|
7
6
|
*/
|
|
8
7
|
|
|
9
8
|
import type { RlsCatalogGate, RlsGateCatalog } from "./rls-gate-catalog.ts";
|
|
@@ -36,6 +35,12 @@ export type QueryGateUserChoice = {
|
|
|
36
35
|
readonly gate: string;
|
|
37
36
|
};
|
|
38
37
|
|
|
38
|
+
/** Picker value shared by SQL console, browse, and grid edit. */
|
|
39
|
+
export type RlsPickerValue = {
|
|
40
|
+
readonly asGate: string | null;
|
|
41
|
+
readonly asUserId: string | null;
|
|
42
|
+
};
|
|
43
|
+
|
|
39
44
|
/** Identity slice used to build As-card user rows. */
|
|
40
45
|
export type QueryGateIdentity = {
|
|
41
46
|
readonly id: string;
|
|
@@ -175,10 +180,31 @@ export function filterQueryGateAsChoices<
|
|
|
175
180
|
*
|
|
176
181
|
* @param asGate - Selected gate name, or null for Operator
|
|
177
182
|
*/
|
|
178
|
-
export function queryGateToolbarLabel(
|
|
183
|
+
export function queryGateToolbarLabel(
|
|
184
|
+
asGate: string | null,
|
|
185
|
+
asUserId?: string | null,
|
|
186
|
+
userLabel?: string | null,
|
|
187
|
+
): string {
|
|
188
|
+
if (asUserId && userLabel) return `User · ${userLabel}`;
|
|
189
|
+
if (asUserId) return `User · ${asUserId}`;
|
|
179
190
|
return asGate && asGate.length > 0 ? `Gate · ${asGate}` : "Gate";
|
|
180
191
|
}
|
|
181
192
|
|
|
193
|
+
/**
|
|
194
|
+
* Request fields for store query / edit / SQL.
|
|
195
|
+
*
|
|
196
|
+
* @param value - Picker bag
|
|
197
|
+
*/
|
|
198
|
+
export function rlsPickerPayload(value: RlsPickerValue): {
|
|
199
|
+
readonly asGate?: string;
|
|
200
|
+
readonly asUserId?: string;
|
|
201
|
+
} {
|
|
202
|
+
return {
|
|
203
|
+
...(value.asGate ? { asGate: value.asGate } : {}),
|
|
204
|
+
...(value.asUserId ? { asUserId: value.asUserId } : {}),
|
|
205
|
+
};
|
|
206
|
+
}
|
|
207
|
+
|
|
182
208
|
function gateChoiceDetail(gate: RlsCatalogGate): string {
|
|
183
209
|
if (gate.description) return gate.description;
|
|
184
210
|
if (gate.roles.length > 0) return `Roles ${gate.roles.join(", ")}`;
|
|
@@ -9,7 +9,17 @@ import {
|
|
|
9
9
|
rlsGateModeFromCommand,
|
|
10
10
|
rlsPolicyPredicates,
|
|
11
11
|
rlsPolicyPreviewSql,
|
|
12
|
+
rlsPolicyCodeSource,
|
|
13
|
+
rlsGatePredicateSql,
|
|
12
14
|
rlsStoreGateActions,
|
|
15
|
+
rlsBindOwnerExpr,
|
|
16
|
+
rlsExprNeedsOwnerColumn,
|
|
17
|
+
rlsExprUsesUserIdentity,
|
|
18
|
+
rlsOwnerColumn,
|
|
19
|
+
rlsRewriteIdentityColumn,
|
|
20
|
+
rlsTableColumns,
|
|
21
|
+
rlsTableSqlColumns,
|
|
22
|
+
rlsTemplateUsesOwner,
|
|
13
23
|
} from "./rls-policy.ts";
|
|
14
24
|
|
|
15
25
|
describe("rlsPolicyPreviewSql", () => {
|
|
@@ -62,6 +72,35 @@ describe("RLS_POLICY_TEMPLATES", () => {
|
|
|
62
72
|
expect(filterRlsPolicyTemplates(RLS_POLICY_TEMPLATES, "oke.gate").length).toBeGreaterThan(0);
|
|
63
73
|
expect(filterRlsPolicyTemplates(RLS_POLICY_TEMPLATES, "owner").length).toBeGreaterThan(0);
|
|
64
74
|
expect(filterRlsPolicyTemplates(RLS_POLICY_TEMPLATES, "missing")).toEqual([]);
|
|
75
|
+
expect(
|
|
76
|
+
RLS_POLICY_TEMPLATES.some((tpl) => /\bcurrent_user\b/.test(`${tpl.using} ${tpl.withCheck}`)),
|
|
77
|
+
).toBe(false);
|
|
78
|
+
expect(
|
|
79
|
+
RLS_POLICY_TEMPLATES.some((tpl) => /oke\.user\(\)/.test(`${tpl.using} ${tpl.withCheck}`)),
|
|
80
|
+
).toBe(true);
|
|
81
|
+
});
|
|
82
|
+
});
|
|
83
|
+
|
|
84
|
+
describe("rlsGatePredicateSql", () => {
|
|
85
|
+
test("fills oke.gate / oke.has_scope — not Postgres TO", () => {
|
|
86
|
+
expect(rlsGatePredicateSql([])).toBe("true");
|
|
87
|
+
expect(rlsGatePredicateSql(["member"])).toBe("oke.gate() = 'member'");
|
|
88
|
+
expect(rlsGatePredicateSql(["booking:create"])).toBe("oke.has_scope('booking:create')");
|
|
89
|
+
});
|
|
90
|
+
});
|
|
91
|
+
|
|
92
|
+
describe("rlsPolicyCodeSource", () => {
|
|
93
|
+
test("emits store.schema.policy and pgPolicy", () => {
|
|
94
|
+
const spec = parseSqlPolicySpec({
|
|
95
|
+
name: "gate_member_select",
|
|
96
|
+
table: "bookings",
|
|
97
|
+
command: "SELECT",
|
|
98
|
+
using: "oke.gate() = 'member'",
|
|
99
|
+
});
|
|
100
|
+
const code = rlsPolicyCodeSource(spec);
|
|
101
|
+
expect(code).toContain("store.schema.policy");
|
|
102
|
+
expect(code).toContain("pgPolicy");
|
|
103
|
+
expect(code).toContain("oke.gate()");
|
|
65
104
|
});
|
|
66
105
|
});
|
|
67
106
|
|
|
@@ -75,6 +114,106 @@ describe("rlsPolicyPredicates", () => {
|
|
|
75
114
|
});
|
|
76
115
|
});
|
|
77
116
|
|
|
117
|
+
describe("rls owner column bind", () => {
|
|
118
|
+
test("picks creator_email when owner is missing", () => {
|
|
119
|
+
expect(rlsOwnerColumn(["id", "title", "creator_email"])).toBe("creator_email");
|
|
120
|
+
expect(rlsOwnerColumn(["id", "owner_email"])).toBe("owner_email");
|
|
121
|
+
expect(rlsOwnerColumn(["id", "title"])).toBeNull();
|
|
122
|
+
expect(rlsBindOwnerExpr("owner = oke.user()", "creator_email")).toBe(
|
|
123
|
+
"creator_email = oke.user()",
|
|
124
|
+
);
|
|
125
|
+
expect(rlsExprNeedsOwnerColumn("owner = oke.user()")).toBe(true);
|
|
126
|
+
expect(rlsExprNeedsOwnerColumn("creator_email = oke.user()")).toBe(false);
|
|
127
|
+
});
|
|
128
|
+
|
|
129
|
+
test("reads Manifest sqlName / snake_case keys", () => {
|
|
130
|
+
const cols = rlsTableSqlColumns(
|
|
131
|
+
{
|
|
132
|
+
oke: "1.0",
|
|
133
|
+
app: "keel",
|
|
134
|
+
stores: {
|
|
135
|
+
db: {
|
|
136
|
+
facet: "sql",
|
|
137
|
+
tables: {
|
|
138
|
+
tasks: {
|
|
139
|
+
columns: {
|
|
140
|
+
creatorEmail: { type: "text", sqlName: "creator_email" },
|
|
141
|
+
title: { type: "text" },
|
|
142
|
+
},
|
|
143
|
+
},
|
|
144
|
+
},
|
|
145
|
+
},
|
|
146
|
+
},
|
|
147
|
+
},
|
|
148
|
+
"sql:db",
|
|
149
|
+
"tasks",
|
|
150
|
+
);
|
|
151
|
+
expect(cols).toEqual(["creator_email", "title"]);
|
|
152
|
+
expect(rlsOwnerColumn(cols)).toBe("creator_email");
|
|
153
|
+
});
|
|
154
|
+
|
|
155
|
+
test("marks PK / declared FK / inferred *_id", () => {
|
|
156
|
+
const cols = rlsTableColumns(
|
|
157
|
+
{
|
|
158
|
+
oke: "1.0",
|
|
159
|
+
app: "keel",
|
|
160
|
+
stores: {
|
|
161
|
+
db: {
|
|
162
|
+
facet: "sql",
|
|
163
|
+
tables: {
|
|
164
|
+
tasks: {
|
|
165
|
+
columns: {
|
|
166
|
+
id: { type: "text", primaryKey: true, sqlName: "id" },
|
|
167
|
+
identifier: { type: "text", unique: true, sqlName: "identifier" },
|
|
168
|
+
spaceId: {
|
|
169
|
+
type: "text",
|
|
170
|
+
sqlName: "space_id",
|
|
171
|
+
references: { table: "spaces", column: "id" },
|
|
172
|
+
},
|
|
173
|
+
parentId: { type: "text", sqlName: "parent_id" },
|
|
174
|
+
title: { type: "text", sqlName: "title" },
|
|
175
|
+
},
|
|
176
|
+
},
|
|
177
|
+
},
|
|
178
|
+
},
|
|
179
|
+
},
|
|
180
|
+
},
|
|
181
|
+
"sql:db",
|
|
182
|
+
"tasks",
|
|
183
|
+
);
|
|
184
|
+
expect(cols.find((c) => c.sqlName === "id")).toMatchObject({
|
|
185
|
+
primaryKey: true,
|
|
186
|
+
foreignKey: false,
|
|
187
|
+
});
|
|
188
|
+
expect(cols.find((c) => c.sqlName === "identifier")).toMatchObject({ unique: true });
|
|
189
|
+
expect(cols.find((c) => c.sqlName === "space_id")).toMatchObject({
|
|
190
|
+
foreignKey: true,
|
|
191
|
+
inferred: false,
|
|
192
|
+
});
|
|
193
|
+
expect(cols.find((c) => c.sqlName === "parent_id")).toMatchObject({
|
|
194
|
+
foreignKey: true,
|
|
195
|
+
inferred: true,
|
|
196
|
+
});
|
|
197
|
+
expect(cols.find((c) => c.sqlName === "title")).toMatchObject({
|
|
198
|
+
foreignKey: false,
|
|
199
|
+
primaryKey: false,
|
|
200
|
+
});
|
|
201
|
+
});
|
|
202
|
+
|
|
203
|
+
test("rewrites owner or a previous identity column", () => {
|
|
204
|
+
expect(rlsRewriteIdentityColumn("owner = oke.user()", "owner", "creator_email")).toBe(
|
|
205
|
+
"creator_email = oke.user()",
|
|
206
|
+
);
|
|
207
|
+
expect(
|
|
208
|
+
rlsRewriteIdentityColumn("creator_email = oke.user()", "creator_email", "owner_email"),
|
|
209
|
+
).toBe("owner_email = oke.user()");
|
|
210
|
+
expect(rlsExprUsesUserIdentity("creator_email = oke.user()")).toBe(true);
|
|
211
|
+
expect(rlsExprUsesUserIdentity("true")).toBe(false);
|
|
212
|
+
expect(rlsTemplateUsesOwner({ using: "owner = oke.user()" })).toBe(true);
|
|
213
|
+
expect(rlsTemplateUsesOwner({ withCheck: "true" })).toBe(false);
|
|
214
|
+
});
|
|
215
|
+
});
|
|
216
|
+
|
|
78
217
|
describe("rls gate posture", () => {
|
|
79
218
|
test("maps commands to Module:Action pairs", () => {
|
|
80
219
|
expect(rlsStoreGateActions("SELECT")).toEqual(["store.sql:read"]);
|