@rebasepro/studio 0.22.0 → 0.24.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.
Files changed (62) hide show
  1. package/dist/{ApiExplorer-D-jLWAaY.js → ApiExplorer-CCtczj3F.js} +16 -32
  2. package/dist/ApiExplorer-CCtczj3F.js.map +1 -0
  3. package/dist/ApiKeysView-B66TdzhH.js +1738 -0
  4. package/dist/ApiKeysView-B66TdzhH.js.map +1 -0
  5. package/dist/BackupsView-DmSMzAbT.js +368 -0
  6. package/dist/BackupsView-DmSMzAbT.js.map +1 -0
  7. package/dist/{BranchesView-D6Sz1a5f.js → BranchesView-DP1a87E-.js} +12 -8
  8. package/dist/BranchesView-DP1a87E-.js.map +1 -0
  9. package/dist/{CronJobsView-JwRjQCde.js → CronJobsView-TDErHTuK.js} +143 -68
  10. package/dist/CronJobsView-TDErHTuK.js.map +1 -0
  11. package/dist/{JSEditor-qiuwmcvk.js → JSEditor-B59pwg4c.js} +120 -74
  12. package/dist/JSEditor-B59pwg4c.js.map +1 -0
  13. package/dist/{LogsExplorer-Xq8K4oo6.js → LogsExplorer-BKM4yfP8.js} +48 -22
  14. package/dist/LogsExplorer-BKM4yfP8.js.map +1 -0
  15. package/dist/{MonacoEditor-D0_QljcX.js → MonacoEditor-CUxy5_k5.js} +11 -9
  16. package/dist/MonacoEditor-CUxy5_k5.js.map +1 -0
  17. package/dist/{RLSEditor-B-qrgdHy.js → RLSEditor-BlLLV6xs.js} +350 -98
  18. package/dist/RLSEditor-BlLLV6xs.js.map +1 -0
  19. package/dist/{SQLEditor-sdtoZv5F.js → SQLEditor-1An7w0j6.js} +496 -224
  20. package/dist/SQLEditor-1An7w0j6.js.map +1 -0
  21. package/dist/{SchemaVisualizer-DtDzIUNy.js → SchemaVisualizer-DCLVfA5t.js} +20 -24
  22. package/dist/SchemaVisualizer-DCLVfA5t.js.map +1 -0
  23. package/dist/{StorageView-C52dSZNf.js → StorageView-CbsHuCBf.js} +382 -228
  24. package/dist/StorageView-CbsHuCBf.js.map +1 -0
  25. package/dist/components/ApiExplorer/TryItPanel.d.ts +6 -1
  26. package/dist/components/ApiKeys/CreateApiKeyDialog.d.ts +21 -2
  27. package/dist/components/ApiKeys/scope-words.d.ts +37 -0
  28. package/dist/components/ApiKeys/scopes.d.ts +113 -0
  29. package/dist/components/AuthSimulationSelector.d.ts +5 -1
  30. package/dist/components/LogsExplorer/useLogTail.d.ts +9 -0
  31. package/dist/components/RLSEditor/policyListing.d.ts +44 -0
  32. package/dist/components/RLSEditor/policyRules.d.ts +49 -0
  33. package/dist/components/SQLEditor/MonacoEditor.d.ts +13 -1
  34. package/dist/components/SchemaVisualizer/schema-visualizer.utils.d.ts +0 -1
  35. package/dist/components/SchemaVisualizer/useSchemaGraph.d.ts +6 -0
  36. package/dist/components/load-failure.d.ts +1 -1
  37. package/dist/components/useRunAsUsers.d.ts +25 -0
  38. package/dist/index.es.js +11 -11
  39. package/dist/{load-failure-C6OMJSGC.js → load-failure-Bd8lFIO7.js} +2 -2
  40. package/dist/load-failure-Bd8lFIO7.js.map +1 -0
  41. package/dist/useRunAsUsers-CLaAtXt6.js +122 -0
  42. package/dist/useRunAsUsers-CLaAtXt6.js.map +1 -0
  43. package/dist/utils/sql_utils.d.ts +126 -36
  44. package/package.json +9 -9
  45. package/dist/ApiExplorer-D-jLWAaY.js.map +0 -1
  46. package/dist/ApiKeysView-OfJLH2x7.js +0 -1239
  47. package/dist/ApiKeysView-OfJLH2x7.js.map +0 -1
  48. package/dist/AuthSimulationSelector-BewmhMBw.js +0 -56
  49. package/dist/AuthSimulationSelector-BewmhMBw.js.map +0 -1
  50. package/dist/BackupsView-DYTAD6aW.js +0 -253
  51. package/dist/BackupsView-DYTAD6aW.js.map +0 -1
  52. package/dist/BranchesView-D6Sz1a5f.js.map +0 -1
  53. package/dist/CronJobsView-JwRjQCde.js.map +0 -1
  54. package/dist/JSEditor-qiuwmcvk.js.map +0 -1
  55. package/dist/LogsExplorer-Xq8K4oo6.js.map +0 -1
  56. package/dist/MonacoEditor-D0_QljcX.js.map +0 -1
  57. package/dist/RLSEditor-B-qrgdHy.js.map +0 -1
  58. package/dist/SQLEditor-sdtoZv5F.js.map +0 -1
  59. package/dist/SchemaVisualizer-DtDzIUNy.js.map +0 -1
  60. package/dist/StorageView-C52dSZNf.js.map +0 -1
  61. package/dist/components/ApiKeys/permissions.d.ts +0 -66
  62. package/dist/load-failure-C6OMJSGC.js.map +0 -1
@@ -0,0 +1 @@
1
+ {"version":3,"file":"MonacoEditor-CUxy5_k5.js","names":[],"sources":["../src/components/SQLEditor/MonacoEditor.tsx"],"sourcesContent":["import React, { useImperativeHandle, useRef } from \"react\";\nimport Editor, { Monaco, OnMount } from \"@monaco-editor/react\";\nimport type { editor, Position, IRange } from \"monaco-editor\";\nimport { cls, defaultBorderMixin } from \"@rebasepro/ui\";\nimport { useModeController } from \"@rebasepro/app\";\n\n/**\n * What a host can ask of the editor outside its own key bindings.\n *\n * The toolbar's buttons need the selection that Cmd+Enter already reads: a\n * button that runs the whole buffer while the shortcut beside it runs only the\n * highlighted statement is two answers to one question.\n */\nexport interface MonacoEditorHandle {\n /** The selected text, trimmed — `undefined` when nothing is selected. */\n getSelectedText: () => string | undefined;\n}\n\n/** The editor's selection, trimmed — `undefined` when there is none. */\nfunction selectedTextOf(instance: editor.IStandaloneCodeEditor): string | undefined {\n const selection = instance.getSelection();\n if (!selection || selection.isEmpty()) return undefined;\n return instance.getModel()?.getValueInRange(selection)?.trim() || undefined;\n}\n\nexport type MonacoEditorProps = {\n ref?: React.Ref<MonacoEditorHandle>;\n value: string;\n onChange: (value: string | undefined) => void;\n onRun?: (selectedText?: string) => void;\n className?: string;\n readOnly?: boolean;\n autoFocus?: boolean;\n schemas?: Record<string, { schemaName: string, tableName: string, columns: { name: string, dataType: string }[] }[]>;\n};\n\nexport const MonacoEditor = ({\n ref,\n value,\n onChange,\n onRun,\n className,\n readOnly = false,\n autoFocus = true,\n schemas\n}: MonacoEditorProps) => {\n const { mode } = useModeController();\n const editorRef = useRef<editor.IStandaloneCodeEditor | null>(null);\n const monacoRef = useRef<Monaco | null>(null);\n const onRunRef = useRef(onRun);\n onRunRef.current = onRun;\n\n const schemasRef = useRef(schemas);\n schemasRef.current = schemas;\n\n useImperativeHandle(ref, () => ({\n getSelectedText: () => (editorRef.current ? selectedTextOf(editorRef.current) : undefined)\n }), []);\n\n const handleEditorOnMount: OnMount = (editor, monaco) => {\n editorRef.current = editor;\n monacoRef.current = monaco;\n\n editor.addAction({\n id: \"run-query\",\n label: \"Run Query\",\n keybindings: [monaco.KeyMod.CtrlCmd + monaco.KeyCode.Enter],\n contextMenuGroupId: \"operation\",\n contextMenuOrder: 0,\n run: () => {\n onRunRef.current?.(selectedTextOf(editor));\n }\n });\n\n // Register custom autocomplete for tables, columns, and SQL keywords\n const sqlKeywords = [\n \"SELECT\", \"FROM\", \"WHERE\", \"INSERT INTO\", \"VALUES\", \"UPDATE\", \"SET\",\n \"DELETE FROM\", \"JOIN\", \"INNER JOIN\", \"LEFT JOIN\", \"RIGHT JOIN\", \"FULL JOIN\",\n \"ON\", \"GROUP BY\", \"HAVING\", \"ORDER BY\", \"ASC\", \"DESC\", \"LIMIT\", \"OFFSET\",\n \"AS\", \"AND\", \"OR\", \"NOT\", \"IN\", \"BETWEEN\", \"LIKE\", \"IS NULL\", \"IS NOT NULL\",\n \"CREATE TABLE\", \"ALTER TABLE\", \"DROP TABLE\", \"PRIMARY KEY\", \"FOREIGN KEY\",\n \"COUNT\", \"SUM\", \"AVG\", \"MIN\", \"MAX\", \"DISTINCT\", \"UNION\", \"ALL\", \"EXISTS\",\n \"CASE\", \"WHEN\", \"THEN\", \"ELSE\", \"END\", \"CAST\", \"COALESCE\"\n ];\n\n monaco.languages.registerCompletionItemProvider(\"pgsql\", {\n triggerCharacters: [\".\", \" \", \"A\", \"B\", \"C\", \"D\", \"E\", \"F\", \"G\", \"H\", \"I\", \"J\", \"K\", \"L\", \"M\", \"N\", \"O\", \"P\", \"Q\", \"R\", \"S\", \"T\", \"U\", \"V\", \"W\", \"X\", \"Y\", \"Z\", \"a\", \"b\", \"c\", \"d\", \"e\", \"f\", \"g\", \"h\", \"i\", \"j\", \"k\", \"l\", \"m\", \"n\", \"o\", \"p\", \"q\", \"r\", \"s\", \"t\", \"u\", \"v\", \"w\", \"x\", \"y\", \"z\"],\n provideCompletionItems: (model: editor.ITextModel, position: Position) => {\n const word = model.getWordUntilPosition(position);\n const range = {\n startLineNumber: position.lineNumber,\n endLineNumber: position.lineNumber,\n startColumn: word.startColumn,\n endColumn: word.endColumn\n };\n\n const textUntilPosition = model.getValueInRange({\n startLineNumber: position.lineNumber,\n startColumn: 1,\n endLineNumber: position.lineNumber,\n endColumn: position.column\n });\n\n // Check if user is typing `tableName.`\n const match = textUntilPosition.match(/([a-zA-Z0-9_]+)\\.$/);\n const tableNameMatch = match ? match[1] : null;\n\n const suggestions: { label: string; kind: number; insertText: string; range: IRange; detail: string; sortText: string }[] = [];\n const allSchemas = schemasRef.current || {};\n\n if (tableNameMatch) {\n // Provide columns for the matched table\n for (const [, tables] of Object.entries(allSchemas)) {\n const table = tables.find(t => t.tableName === tableNameMatch);\n if (table) {\n table.columns.forEach(col => {\n suggestions.push({\n label: col.name,\n kind: monaco.languages.CompletionItemKind.Field,\n insertText: col.name,\n range,\n detail: \"Column\",\n sortText: \"1\"\n });\n });\n }\n }\n } else {\n // Provide table names\n for (const [sName, tables] of Object.entries(allSchemas)) {\n tables.forEach(table => {\n suggestions.push({\n label: table.tableName,\n kind: monaco.languages.CompletionItemKind.Class,\n insertText: table.tableName,\n range,\n detail: `Table (${sName})`,\n sortText: \"2\"\n });\n });\n }\n\n // Provide SQL keywords\n sqlKeywords.forEach(keyword => {\n suggestions.push({\n label: keyword,\n kind: monaco.languages.CompletionItemKind.Keyword,\n insertText: keyword + \" \",\n range,\n detail: \"SQL Keyword\",\n sortText: \"3\"\n });\n });\n\n // Heuristic: Suggest columns for tables that are mentioned in the current editor text\n const fullText = model.getValue();\n const activeTables = new Set<string>();\n for (const [, tables] of Object.entries(allSchemas)) {\n tables.forEach(table => {\n const regex = new RegExp(`\\\\b${table.tableName}\\\\b`, \"i\");\n if (regex.test(fullText)) {\n activeTables.add(table.tableName);\n }\n });\n }\n\n const addedColumns = new Set<string>();\n for (const [, tables] of Object.entries(allSchemas)) {\n tables.forEach(table => {\n if (activeTables.has(table.tableName)) {\n table.columns.forEach(col => {\n if (!addedColumns.has(col.name)) {\n addedColumns.add(col.name);\n suggestions.push({\n label: col.name,\n kind: monaco.languages.CompletionItemKind.Field,\n insertText: col.name,\n range,\n detail: `Column (${table.tableName})`,\n sortText: \"1\" // Prioritize columns from active tables\n });\n }\n });\n }\n });\n }\n }\n\n return { suggestions };\n\n }\n });\n\n if (autoFocus) {\n editor.focus();\n }\n };\n\n return (\n <div className={cls(\"relative w-full h-full overflow-hidden\", className)}>\n <Editor\n height=\"100%\"\n defaultLanguage=\"pgsql\"\n value={value}\n onChange={onChange}\n onMount={handleEditorOnMount}\n theme={mode === \"dark\" ? \"vs-dark\" : \"vs\"}\n options={{\n minimap: { enabled: false },\n fontSize: 14,\n lineNumbers: \"on\",\n scrollBeyondLastLine: false,\n automaticLayout: true,\n readOnly,\n tabSize: 2,\n wordWrap: \"on\"\n }}\n />\n </div>\n );\n};\n"],"mappings":"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"}
@@ -1,11 +1,11 @@
1
- import { t as MonacoEditor } from "./MonacoEditor-D0_QljcX.js";
1
+ import { t as MonacoEditor } from "./MonacoEditor-CUxy5_k5.js";
2
2
  import { ConfirmationDialog, ErrorView, useApiBase, useApiConfig, useRebaseContext, useSnackbarController, useStudioCapabilities, useStudioCollectionRegistry, useStudioSchemaEditing, useTranslation } from "@rebasepro/app";
3
3
  import { useCallback, useEffect, useMemo, useState } from "react";
4
4
  import { Alert, AlertTriangleIcon, Button, Chip, CircularProgress, DatabaseIcon, Dialog, DialogActions, DialogContent, DialogTitle, HelpCircleIcon, IconButton, KeyIcon, Link2Icon, LockIcon, MultiSelect, MultiSelectItem, Paper, RefreshCwIcon, ResizablePanels, Select, SelectItem, ShieldIcon, Tab, Tabs, TextField, Tooltip, Trash2Icon, Typography, cls, defaultBorderMixin, iconSize } from "@rebasepro/ui";
5
5
  import { Fragment, jsx, jsxs } from "react/jsx-runtime";
6
6
  import { RLS_JWT_SQL, RLS_ROLES_SQL, RLS_UID_SQL, isPostgresCollectionConfig, policy } from "@rebasepro/types";
7
7
  import { getPolicyNamesForRule, getPolicyNamesForRules, getPolicyOperations } from "@rebasepro/utils";
8
- import { JUNCTION_TABLES_SQL, REBASE_INTERNAL_PREFIXES, REBASE_INTERNAL_SCHEMAS, REBASE_USER_ROLE, getEffectiveSecurityRules, getJunctionSecurityRules, policyToPostgres, resolveJunctionSpecs } from "@rebasepro/common";
8
+ import { JUNCTION_TABLES_SQL, REBASE_INTERNAL_PREFIXES, REBASE_INTERNAL_SCHEMAS, REBASE_USER_ROLE, compileRulePolicies, getEffectiveSecurityRules, getInjectedSecurityRules, getJunctionSecurityRules, getTableName, policyToPostgres, policyToSecurityRule, resolveJunctionSpecs } from "@rebasepro/common";
9
9
  //#region src/components/RLSEditor/policy-presets.ts
10
10
  /**
11
11
  * What the RLS editor offers before the user types anything: the roles a `TO`
@@ -506,23 +506,20 @@ var PolicyEditor = ({ policy, schema, table, roleOptions, onSave, onCancel }) =>
506
506
  }),
507
507
  /* @__PURE__ */ jsxs("ul", {
508
508
  className: "list-disc pl-5 space-y-1 text-sm text-text-secondary dark:text-text-secondary-dark",
509
- children: [
510
- /* @__PURE__ */ jsxs("li", { children: [
511
- /* @__PURE__ */ jsx("strong", { children: "public" }),
512
- ": ",
513
- t("studio_policy_help_role_public")
514
- ] }),
515
- /* @__PURE__ */ jsxs("li", { children: [
516
- /* @__PURE__ */ jsx("strong", { children: "authenticated" }),
517
- ": ",
518
- t("studio_policy_help_role_authenticated")
519
- ] }),
520
- /* @__PURE__ */ jsxs("li", { children: [
521
- /* @__PURE__ */ jsx("strong", { children: "anon" }),
522
- ": ",
523
- t("studio_policy_help_role_anon")
524
- ] })
525
- ]
509
+ children: [/* @__PURE__ */ jsxs("li", { children: [
510
+ /* @__PURE__ */ jsx("strong", { children: "public" }),
511
+ ": ",
512
+ t("studio_policy_help_role_public")
513
+ ] }), /* @__PURE__ */ jsxs("li", { children: [
514
+ /* @__PURE__ */ jsx("strong", { children: REBASE_USER_ROLE }),
515
+ ": ",
516
+ t("studio_policy_help_role_rebase_user")
517
+ ] })]
518
+ }),
519
+ /* @__PURE__ */ jsx(Typography, {
520
+ variant: "body2",
521
+ className: "text-text-secondary dark:text-text-secondary-dark mt-1",
522
+ children: t("studio_policy_help_roles_not_users")
526
523
  })
527
524
  ]
528
525
  }),
@@ -760,7 +757,215 @@ function isCancellation(err) {
760
757
  return err instanceof Error && err.name === "SchemaChangeCancelled";
761
758
  }
762
759
  //#endregion
760
+ //#region src/components/RLSEditor/policyRules.ts
761
+ /**
762
+ * Write an edit of one policy back to the declared rule it compiles from.
763
+ *
764
+ * The rule is found by the policy names it compiles to, not by `rule.name`:
765
+ * an unnamed rule's policy is `<table>_<op>_<hash>`, and a rule over several
766
+ * operations names each `<name>_<op>`. Matching on `name` missed both, saved
767
+ * the rules unchanged and reported success.
768
+ *
769
+ * A rule over several operations is refused rather than split: taking one
770
+ * operation out renames every policy the rule compiles to.
771
+ *
772
+ * @param declared The collection's `securityRules`, as written.
773
+ * @param effective Every rule the generator compiles for the table — the
774
+ * declared ones plus what Rebase adds — to tell a generated
775
+ * policy from one that exists only in the database.
776
+ */
777
+ function applyPolicyEdit(declared, effective, tableName, original, edited) {
778
+ const compilesTo = (rule) => getPolicyNamesForRule(rule, tableName).includes(original.policyname);
779
+ const index = declared.findIndex(compilesTo);
780
+ if (index === -1) return {
781
+ ok: false,
782
+ reason: effective.some(compilesTo) ? "generated" : "not_declared"
783
+ };
784
+ const rule = declared[index];
785
+ if (getPolicyOperations(rule).length > 1) return {
786
+ ok: false,
787
+ reason: "several_operations"
788
+ };
789
+ return {
790
+ ok: true,
791
+ rules: declared.map((r, i) => i === index ? editedRule(r, original, edited) : r)
792
+ };
793
+ }
794
+ /**
795
+ * The rule with what the editor changed, and nothing else.
796
+ *
797
+ * The editor shows a rule as a policy — name, command, mode, `TO` list and raw
798
+ * USING / WITH CHECK — and an `ownerField` or `access` rule has no raw clause
799
+ * to show. So a field is only written when it differs from what the editor was
800
+ * opened with: renaming an `access: "public"` rule keeps it public, and an
801
+ * unnamed rule stays unnamed unless the name was changed. A condition typed in
802
+ * replaces the rule's condition whole — the forms are exclusive.
803
+ */
804
+ function editedRule(rule, original, edited) {
805
+ const next = policyToSecurityRule(edited);
806
+ const result = { ...rule };
807
+ if ((edited.policyname ?? "") !== original.policyname) if (next.name) result.name = next.name;
808
+ else delete result.name;
809
+ if (edited.cmd !== original.cmd) {
810
+ delete result.operations;
811
+ result.operation = next.operation;
812
+ }
813
+ if (edited.permissive !== original.permissive) result.mode = next.mode;
814
+ if (!sameRoles$1(edited.roles, original.roles)) if (next.pgRoles) result.pgRoles = next.pgRoles;
815
+ else delete result.pgRoles;
816
+ if ((edited.qual || null) !== (original.qual || null) || (edited.with_check || null) !== (original.with_check || null)) {
817
+ delete result.ownerField;
818
+ delete result.access;
819
+ delete result.condition;
820
+ delete result.check;
821
+ delete result.using;
822
+ delete result.withCheck;
823
+ if (next.using) result.using = next.using;
824
+ if (next.withCheck) result.withCheck = next.withCheck;
825
+ }
826
+ return result;
827
+ }
828
+ function sameRoles$1(a, b) {
829
+ const left = [...a ?? []].sort();
830
+ const right = [...b ?? []].sort();
831
+ return left.length === right.length && left.every((role, i) => role === right[i]);
832
+ }
833
+ //#endregion
834
+ //#region src/components/RLSEditor/policyListing.ts
835
+ var COMMANDS = {
836
+ select: "SELECT",
837
+ insert: "INSERT",
838
+ update: "UPDATE",
839
+ delete: "DELETE",
840
+ all: "ALL"
841
+ };
842
+ /** A compiled policy, in the shape `pg_policies` describes one. */
843
+ function asPostgresPolicy(compiled, tableName) {
844
+ return {
845
+ policyname: compiled.name,
846
+ tablename: tableName,
847
+ permissive: compiled.mode === "restrictive" ? "RESTRICTIVE" : "PERMISSIVE",
848
+ cmd: COMMANDS[compiled.operation],
849
+ roles: compiled.roles,
850
+ qual: compiled.using,
851
+ with_check: compiled.withCheck
852
+ };
853
+ }
854
+ /**
855
+ * The rule's own clauses, for a rule the compiler could not compile — an
856
+ * `existsIn` naming a collection the registry does not hold, say. Only ever a
857
+ * `code_only` row's content: nothing is compared against it.
858
+ */
859
+ function uncompiledPolicy(rule, operation, name, tableName) {
860
+ return {
861
+ policyname: name,
862
+ tablename: tableName,
863
+ permissive: rule.mode === "restrictive" ? "RESTRICTIVE" : "PERMISSIVE",
864
+ cmd: COMMANDS[operation],
865
+ roles: [...rule.pgRoles ?? ["public"]],
866
+ qual: rule.using || null,
867
+ with_check: rule.withCheck || null
868
+ };
869
+ }
870
+ function sameRoles(a, b) {
871
+ const left = [...a].sort();
872
+ const right = [...b].sort();
873
+ return left.length === right.length && left.every((role, i) => role === right[i]);
874
+ }
875
+ /**
876
+ * Where a live policy differs from the one the code compiles to.
877
+ *
878
+ * The same comparison `rebase doctor` makes: the `TO` list, the command, the
879
+ * mode, and whether each clause is there at all. Never the clause text —
880
+ * Postgres rewrites an expression when it stores it, so the text of a policy
881
+ * that matches its declaration exactly still differs from the declaration's.
882
+ * A clause that is missing is not a rewrite, though: a `USING` that is gone
883
+ * changes who the policy admits.
884
+ */
885
+ function policyDrift(declared, live) {
886
+ const drift = [];
887
+ if (!sameRoles(declared.roles, live.roles)) drift.push("roles");
888
+ if (declared.cmd !== live.cmd) drift.push("command");
889
+ if (declared.permissive !== live.permissive) drift.push("mode");
890
+ if (declared.qual != null !== (live.qual != null)) drift.push("USING");
891
+ if (declared.with_check != null !== (live.with_check != null)) drift.push("WITH CHECK");
892
+ return drift;
893
+ }
894
+ /**
895
+ * The policies of one table: every policy the database has, as the database
896
+ * has it, and every policy the code declares for the table that it does not.
897
+ *
898
+ * A policy in both keeps the database's fields — this list is what Postgres
899
+ * enforces — and carries the declaration beside them, with where the two
900
+ * differ.
901
+ *
902
+ * @param live The table's `pg_policies` rows.
903
+ * @param collection The collection the table belongs to, if any.
904
+ * @param collections Every collection, to resolve the ones a rule refers to.
905
+ */
906
+ function listPolicies(live, collection, tableName, collections) {
907
+ const listed = /* @__PURE__ */ new Map();
908
+ for (const policy of live) listed.set(policy.policyname, {
909
+ ...policy,
910
+ status: "live"
911
+ });
912
+ if (!collection || !isPostgresCollectionConfig(collection)) return [...listed.values()];
913
+ const resolveCollection = (slug) => collections.find((c) => c.slug === slug || getTableName(c) === slug);
914
+ const injectedNames = getPolicyNamesForRules(getInjectedSecurityRules(collection), tableName);
915
+ const livePolicies = new Map(live.map((policy) => [policy.policyname, policy]));
916
+ for (const rule of getEffectiveSecurityRules(collection)) {
917
+ const operations = getPolicyOperations(rule);
918
+ let compiled;
919
+ try {
920
+ compiled = compileRulePolicies(collection, rule, { resolveCollection });
921
+ } catch {
922
+ compiled = void 0;
923
+ }
924
+ getPolicyNamesForRule(rule, tableName).forEach((name, index) => {
925
+ const declared = compiled ? asPostgresPolicy(compiled[index], tableName) : void 0;
926
+ const generated = injectedNames.has(name);
927
+ const existing = livePolicies.get(name);
928
+ if (existing) listed.set(name, {
929
+ ...existing,
930
+ status: "both",
931
+ declared,
932
+ drift: declared ? policyDrift(declared, existing) : void 0,
933
+ generated
934
+ });
935
+ else listed.set(name, {
936
+ ...declared ?? uncompiledPolicy(rule, operations[index] ?? "all", name, tableName),
937
+ status: "code_only",
938
+ generated
939
+ });
940
+ });
941
+ }
942
+ return [...listed.values()];
943
+ }
944
+ //#endregion
763
945
  //#region src/components/RLSEditor/RLSEditor.tsx
946
+ /**
947
+ * Whether `collection` is the one `db push` writes `table` for: the table
948
+ * `getTableName` derives, in the schema the collection declares or `public` —
949
+ * the planner's own resolution.
950
+ *
951
+ * This matched a table to any collection whose `id`, `path`, `table`, `slug`
952
+ * or `collectionId` equalled its name, in any schema. So an unmapped table
953
+ * named like another collection's slug, or a same-named table in another
954
+ * schema, was taken for that collection: it listed the collection's policies
955
+ * as missing from the table, and offered to import the table's own policies
956
+ * into the collection. And a collection that leaves `table` to a slug that is
957
+ * not already snake case mapped nothing.
958
+ */
959
+ function mapsTable(collection, table) {
960
+ if (!isPostgresCollectionConfig(collection)) return false;
961
+ return getTableName(collection) === table.tableName && (collection.schema || "public") === table.schemaName;
962
+ }
963
+ /** What the editor says when an edit has no declared rule to go back to. */
964
+ var POLICY_EDIT_REFUSALS = {
965
+ generated: "studio_rls_edit_generated",
966
+ not_declared: "studio_rls_edit_not_declared",
967
+ several_operations: "studio_rls_edit_several_operations"
968
+ };
764
969
  function classifyTableClient(tableName, schemaName, junctionTableNames, isMappedToCollection) {
765
970
  if (isMappedToCollection) return "collection";
766
971
  if (REBASE_INTERNAL_SCHEMAS.includes(schemaName) || REBASE_INTERNAL_PREFIXES.some((prefix) => tableName.startsWith(prefix))) return "internal";
@@ -1092,7 +1297,7 @@ var RLSEditor = ({ apiUrl = "" }) => {
1092
1297
  other: []
1093
1298
  };
1094
1299
  tables.forEach((table) => {
1095
- const isMapped = !!collectionRegistry.collections?.find((c) => c.id === table.tableName || c.path === table.tableName || c.table === table.tableName || c.slug === table.tableName || c.collectionId === table.tableName);
1300
+ const isMapped = !!collectionRegistry.collections?.some((c) => mapsTable(c, table));
1096
1301
  const cat = classifyTableClient(table.tableName, table.schemaName, junctionTableNames, isMapped);
1097
1302
  groups[cat].push(table);
1098
1303
  });
@@ -1104,7 +1309,7 @@ var RLSEditor = ({ apiUrl = "" }) => {
1104
1309
  ]);
1105
1310
  const activeCollection = useMemo(() => {
1106
1311
  if (!activeTableData) return null;
1107
- return collectionRegistry.collections?.find((c) => c.id === activeTableData.tableName || c.path === activeTableData.tableName || c.table === activeTableData.tableName || c.slug === activeTableData.tableName || c.collectionId === activeTableData.tableName) || null;
1312
+ return collectionRegistry.collections?.find((c) => mapsTable(c, activeTableData)) || null;
1108
1313
  }, [activeTableData, collectionRegistry.collections]);
1109
1314
  /** The category of the currently selected table. */
1110
1315
  const activeTableCategory = useMemo(() => {
@@ -1116,41 +1321,55 @@ var RLSEditor = ({ apiUrl = "" }) => {
1116
1321
  activeCollection,
1117
1322
  junctionTableNames
1118
1323
  ]);
1324
+ /**
1325
+ * The junction a relation derives this table as, when it is one. Its
1326
+ * policies are Rebase's — derived from the endpoints' relations — and the
1327
+ * server re-creates them on every start, like a collection's. `null` when
1328
+ * the registry's collections carry no resolvable relations (serialized
1329
+ * configs without relation closures).
1330
+ */
1331
+ const activeJunctionSpec = useMemo(() => {
1332
+ if (!activeTableData || activeCollection) return null;
1333
+ try {
1334
+ return resolveJunctionSpecs(collectionRegistry.collections ?? []).get(activeTableData.tableName) ?? null;
1335
+ } catch {
1336
+ return null;
1337
+ }
1338
+ }, [
1339
+ activeTableData,
1340
+ activeCollection,
1341
+ collectionRegistry.collections
1342
+ ]);
1343
+ /**
1344
+ * Whether the server re-asserts this table's RLS on every start: it switches
1345
+ * RLS back on and re-creates every policy the project declares for it.
1346
+ */
1347
+ const activeTableManaged = Boolean(activeCollection || activeJunctionSpec);
1348
+ /**
1349
+ * What Postgres enforces on the selected table, and what the code declares
1350
+ * that it does not have yet.
1351
+ *
1352
+ * A policy in both is listed with the *database's* fields, and the
1353
+ * declaration rides beside it with where the two differ (`listPolicies`).
1354
+ * The declaration's fields in their place would make a policy changed in
1355
+ * SQL look exactly like its declaration, and would show every rule written
1356
+ * as a structured condition — all of Rebase's own policies — with no USING.
1357
+ */
1119
1358
  const mergedPolicies = useMemo(() => {
1120
1359
  if (!activeTableData) return [];
1121
- const policiesMap = {};
1122
- (activeTableData.policies || []).forEach((p) => {
1123
- policiesMap[p.policyname] = {
1124
- ...p,
1125
- status: "live"
1126
- };
1127
- });
1128
- if (activeCollection && isPostgresCollectionConfig(activeCollection)) getEffectiveSecurityRules(activeCollection).forEach((rule) => {
1129
- const ops = getPolicyOperations(rule);
1130
- getPolicyNamesForRule(rule, activeTableData.tableName).forEach((policyName, opIdx) => {
1131
- policiesMap[policyName] = {
1132
- policyname: policyName,
1133
- tablename: activeTableData.tableName,
1134
- permissive: (rule.mode || "permissive").toUpperCase(),
1135
- cmd: (ops[opIdx] ?? rule.operation ?? "ALL").toUpperCase(),
1136
- roles: [...rule.pgRoles ?? ["public"]],
1137
- qual: rule.using || null,
1138
- with_check: rule.withCheck || null,
1139
- status: policiesMap[policyName] ? "both" : "code_only"
1140
- };
1141
- });
1142
- });
1143
- if (!activeCollection) try {
1144
- const spec = resolveJunctionSpecs(collectionRegistry.collections ?? []).get(activeTableData.tableName);
1145
- if (spec) {
1146
- const generatedNames = getPolicyNamesForRules(getJunctionSecurityRules(spec), spec.table);
1147
- for (const p of Object.values(policiesMap)) if (generatedNames.has(p.policyname)) p.status = "both";
1360
+ const listed = listPolicies(activeTableData.policies || [], activeCollection, activeTableData.tableName, collectionRegistry.collections ?? []);
1361
+ if (activeJunctionSpec) {
1362
+ const generatedNames = getPolicyNamesForRules(getJunctionSecurityRules(activeJunctionSpec), activeJunctionSpec.table);
1363
+ for (const p of listed) if (generatedNames.has(p.policyname)) {
1364
+ p.status = "both";
1365
+ p.generated = true;
1148
1366
  }
1149
- } catch {}
1150
- return Object.values(policiesMap).sort((a, b) => a.policyname.localeCompare(b.policyname));
1367
+ }
1368
+ return listed.sort((a, b) => a.policyname.localeCompare(b.policyname));
1151
1369
  }, [
1152
1370
  activeTableData,
1153
1371
  activeCollection,
1372
+ activeJunctionSpec,
1154
1373
  collectionRegistry.collections
1155
1374
  ]);
1156
1375
  const rlsStats = useMemo(() => {
@@ -1451,7 +1670,8 @@ var RLSEditor = ({ apiUrl = "" }) => {
1451
1670
  onClick: () => setConfirmToggleRls({
1452
1671
  table: activeTableData.tableName,
1453
1672
  schema: activeTableData.schemaName,
1454
- enabled: activeTableData.rlsEnabled
1673
+ enabled: activeTableData.rlsEnabled,
1674
+ managed: activeTableManaged
1455
1675
  }),
1456
1676
  children: activeTableData.rlsEnabled ? t("studio_rls_disable_rls") : t("studio_rls_enable_rls")
1457
1677
  }),
@@ -1509,20 +1729,29 @@ var RLSEditor = ({ apiUrl = "" }) => {
1509
1729
  roleOptions: pgRoleOptions,
1510
1730
  onSave: async (newPolicy) => {
1511
1731
  if (activeCollection && hasCodebase) {
1512
- const rule = {
1513
- name: newPolicy.policyname,
1514
- operation: newPolicy.cmd?.toLowerCase(),
1515
- mode: newPolicy.permissive?.toLowerCase(),
1516
- using: newPolicy.qual || void 0,
1517
- withCheck: newPolicy.with_check || void 0,
1518
- ...newPolicy.roles && !(newPolicy.roles.length === 1 && newPolicy.roles[0] === "public") ? { pgRoles: newPolicy.roles } : {}
1519
- };
1520
1732
  const existingRules = (isPostgresCollectionConfig(activeCollection) ? activeCollection.securityRules : void 0) || [];
1521
1733
  let newRules;
1522
- if (editingPolicy === "new") newRules = [...existingRules, rule];
1523
- else newRules = existingRules.map((r) => r.name === editingPolicy.policyname ? rule : r);
1734
+ if (editingPolicy === "new") newRules = [...existingRules, policyToSecurityRule(newPolicy)];
1735
+ else {
1736
+ const edit = applyPolicyEdit(existingRules, getEffectiveSecurityRules(activeCollection), activeTableData.tableName, editingPolicy, newPolicy);
1737
+ if (!edit.ok) {
1738
+ snackbarController.open({
1739
+ type: "error",
1740
+ message: t(POLICY_EDIT_REFUSALS[edit.reason], { policy: editingPolicy.policyname })
1741
+ });
1742
+ return;
1743
+ }
1744
+ newRules = edit.rules;
1745
+ if (JSON.stringify(newRules) === JSON.stringify(existingRules)) {
1746
+ snackbarController.open({
1747
+ type: "info",
1748
+ message: t("studio_rls_nothing_changed")
1749
+ });
1750
+ return;
1751
+ }
1752
+ }
1524
1753
  try {
1525
- await saveSecurityRules(activeCollection.id || activeCollection.path || activeCollection.alias || activeTableData.tableName, newRules);
1754
+ await saveSecurityRules(activeCollection.slug, newRules);
1526
1755
  snackbarController.open({
1527
1756
  type: "success",
1528
1757
  message: "Policy saved successfully"
@@ -1532,31 +1761,40 @@ var RLSEditor = ({ apiUrl = "" }) => {
1532
1761
  } catch (e) {
1533
1762
  reportSaveFailure(e);
1534
1763
  }
1535
- } else try {
1536
- const qualifiedTable = `${sanitizeSqlIdentifier(activeTableData.schemaName)}.${sanitizeSqlIdentifier(activeTableData.tableName)}`;
1537
- const policyName = sanitizeSqlIdentifier(newPolicy.policyname || "unnamed_policy");
1538
- const cmd = newPolicy.cmd || "ALL";
1539
- const permissive = (newPolicy.permissive || "PERMISSIVE") === "PERMISSIVE" ? "PERMISSIVE" : "RESTRICTIVE";
1540
- const roles = newPolicy.roles && newPolicy.roles.length > 0 ? newPolicy.roles.map((r) => sanitizeSqlIdentifier(r)).join(", ") : "public";
1541
- if (editingPolicy !== "new") await databaseAdmin.executeSql(`DROP POLICY IF EXISTS ${policyName} ON ${qualifiedTable}`);
1542
- let sql = `CREATE POLICY ${policyName} ON ${qualifiedTable}`;
1543
- sql += ` AS ${permissive}`;
1544
- sql += ` FOR ${cmd}`;
1545
- sql += ` TO ${roles}`;
1546
- if (newPolicy.qual) sql += ` USING (${newPolicy.qual})`;
1547
- if (newPolicy.with_check) sql += ` WITH CHECK (${newPolicy.with_check})`;
1548
- await databaseAdmin.executeSql(sql);
1549
- snackbarController.open({
1550
- type: "success",
1551
- message: `Policy "${newPolicy.policyname}" applied to the database`
1552
- });
1553
- setEditingPolicy(null);
1554
- fetchRLSData();
1555
- } catch (e) {
1556
- snackbarController.open({
1557
- type: "error",
1558
- message: e instanceof Error ? e.message : String(e)
1559
- });
1764
+ } else {
1765
+ if (editingPolicy !== "new" && editingPolicy.generated) {
1766
+ snackbarController.open({
1767
+ type: "error",
1768
+ message: t(POLICY_EDIT_REFUSALS.generated, { policy: editingPolicy.policyname })
1769
+ });
1770
+ return;
1771
+ }
1772
+ try {
1773
+ const qualifiedTable = `${sanitizeSqlIdentifier(activeTableData.schemaName)}.${sanitizeSqlIdentifier(activeTableData.tableName)}`;
1774
+ const policyName = sanitizeSqlIdentifier(newPolicy.policyname || "unnamed_policy");
1775
+ const cmd = newPolicy.cmd || "ALL";
1776
+ const permissive = (newPolicy.permissive || "PERMISSIVE") === "PERMISSIVE" ? "PERMISSIVE" : "RESTRICTIVE";
1777
+ const roles = newPolicy.roles && newPolicy.roles.length > 0 ? newPolicy.roles.map((r) => sanitizeSqlIdentifier(r)).join(", ") : "public";
1778
+ let sql = `CREATE POLICY ${policyName} ON ${qualifiedTable}`;
1779
+ sql += ` AS ${permissive}`;
1780
+ sql += ` FOR ${cmd}`;
1781
+ sql += ` TO ${roles}`;
1782
+ if (newPolicy.qual) sql += ` USING (${newPolicy.qual})`;
1783
+ if (newPolicy.with_check) sql += ` WITH CHECK (${newPolicy.with_check})`;
1784
+ if (editingPolicy !== "new") sql = `DROP POLICY IF EXISTS ${sanitizeSqlIdentifier(editingPolicy.policyname)} ON ${qualifiedTable}; ${sql}`;
1785
+ await databaseAdmin.executeSql(sql);
1786
+ snackbarController.open({
1787
+ type: "success",
1788
+ message: `Policy "${newPolicy.policyname}" applied to the database`
1789
+ });
1790
+ setEditingPolicy(null);
1791
+ fetchRLSData();
1792
+ } catch (e) {
1793
+ snackbarController.open({
1794
+ type: "error",
1795
+ message: e instanceof Error ? e.message : String(e)
1796
+ });
1797
+ }
1560
1798
  }
1561
1799
  },
1562
1800
  onCancel: () => setEditingPolicy(null)
@@ -1567,6 +1805,19 @@ var RLSEditor = ({ apiUrl = "" }) => {
1567
1805
  children: /* @__PURE__ */ jsxs("div", {
1568
1806
  className: "max-w-4xl mx-auto flex flex-col gap-6",
1569
1807
  children: [
1808
+ activeTableData && activeTableManaged && !(activeCollection && hasCodebase) && /* @__PURE__ */ jsx(Alert, {
1809
+ color: "info",
1810
+ children: /* @__PURE__ */ jsxs("div", {
1811
+ className: "flex items-start gap-2",
1812
+ children: [/* @__PURE__ */ jsx(RefreshCwIcon, {
1813
+ size: 16,
1814
+ className: "shrink-0 mt-0.5"
1815
+ }), /* @__PURE__ */ jsx(Typography, {
1816
+ variant: "caption",
1817
+ children: t("studio_rls_reapplied_on_start", { table: activeTableData.tableName })
1818
+ })]
1819
+ })
1820
+ }),
1570
1821
  activeTableData && activeTableCategory === "internal" && /* @__PURE__ */ jsx(Alert, {
1571
1822
  color: "info",
1572
1823
  children: /* @__PURE__ */ jsxs("div", {
@@ -1676,6 +1927,13 @@ var RLSEditor = ({ apiUrl = "" }) => {
1676
1927
  children: t("studio_rls_unapplied")
1677
1928
  })
1678
1929
  }),
1930
+ policy.drift && policy.drift.length > 0 && /* @__PURE__ */ jsx(Tooltip, {
1931
+ title: t("studio_rls_drift_tooltip", { fields: policy.drift.join(", ") }),
1932
+ children: /* @__PURE__ */ jsx("div", {
1933
+ className: "px-1.5 py-0.5 rounded text-[10px] bg-orange-500/10 text-orange-600 border border-orange-500/20 shrink-0",
1934
+ children: `${t("studio_rls_drift")}: ${policy.drift.join(", ")}`
1935
+ })
1936
+ }),
1679
1937
  policy.status === "live" && hasCodebase && /* @__PURE__ */ jsx(Tooltip, {
1680
1938
  title: "This policy is live in the database but missing from your codebase schema.",
1681
1939
  children: /* @__PURE__ */ jsx("div", {
@@ -1696,17 +1954,10 @@ var RLSEditor = ({ apiUrl = "" }) => {
1696
1954
  variant: "outlined",
1697
1955
  color: "primary",
1698
1956
  onClick: async () => {
1699
- const rule = {
1700
- name: policy.policyname,
1701
- operation: policy.cmd?.toLowerCase(),
1702
- mode: policy.permissive?.toLowerCase(),
1703
- using: policy.qual || void 0,
1704
- withCheck: policy.with_check || void 0,
1705
- roles: policy.roles
1706
- };
1957
+ const rule = policyToSecurityRule(policy);
1707
1958
  const newRules = [...(isPostgresCollectionConfig(activeCollection) ? activeCollection.securityRules : void 0) || [], rule];
1708
1959
  try {
1709
- await saveSecurityRules(activeCollection.id || activeCollection.path || activeCollection.alias || activeTableData.tableName, newRules);
1960
+ await saveSecurityRules(activeCollection.slug, newRules);
1710
1961
  snackbarController.open({
1711
1962
  type: "success",
1712
1963
  message: "Policy imported successfully"
@@ -1729,6 +1980,7 @@ var RLSEditor = ({ apiUrl = "" }) => {
1729
1980
  title: t("studio_rls_delete"),
1730
1981
  asChild: true,
1731
1982
  children: /* @__PURE__ */ jsx(IconButton, {
1983
+ "aria-label": t("studio_rls_delete"),
1732
1984
  size: "small",
1733
1985
  onClick: () => setConfirmDropPolicy({
1734
1986
  policyName: policy.policyname,
@@ -1799,7 +2051,7 @@ var RLSEditor = ({ apiUrl = "" }) => {
1799
2051
  onAccept: applyToggleRls,
1800
2052
  onCancel: () => setConfirmToggleRls(null),
1801
2053
  title: confirmToggleRls?.enabled ? t("studio_rls_disable_confirm_title") : t("studio_rls_enable_confirm_title"),
1802
- body: confirmToggleRls?.enabled ? t("studio_rls_disable_confirm_body", { table: confirmToggleRls?.table ?? "" }) : t("studio_rls_enable_confirm_body", { table: confirmToggleRls?.table ?? "" })
2054
+ body: confirmToggleRls?.enabled ? [t("studio_rls_disable_confirm_body", { table: confirmToggleRls.table }), confirmToggleRls.managed ? t("studio_rls_disable_reapplied", { table: confirmToggleRls.table }) : ""].filter(Boolean).join(" ") : t("studio_rls_enable_confirm_body", { table: confirmToggleRls?.table ?? "" })
1803
2055
  }),
1804
2056
  /* @__PURE__ */ jsx(ConfirmationDialog, {
1805
2057
  open: confirmDropPolicy !== null,
@@ -1818,4 +2070,4 @@ var RLSEditor = ({ apiUrl = "" }) => {
1818
2070
  //#endregion
1819
2071
  export { RLSEditor };
1820
2072
 
1821
- //# sourceMappingURL=RLSEditor-B-qrgdHy.js.map
2073
+ //# sourceMappingURL=RLSEditor-BlLLV6xs.js.map