@rebasepro/server-postgres 0.25.0-canary.ge06257e → 0.25.1
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/dist/{BranchService-DNg4txyf.js → BranchService-ClklxyAQ.js} +283 -20
- package/dist/BranchService-ClklxyAQ.js.map +1 -0
- package/dist/backup/backup-cli.d.ts +70 -1
- package/dist/backup/backup-service.d.ts +79 -0
- package/dist/backup/pg-binary.d.ts +2 -2
- package/dist/backup/pg-tools.d.ts +60 -1
- package/dist/backup-cli-gijeXe0u.js +1961 -0
- package/dist/backup-cli-gijeXe0u.js.map +1 -0
- package/dist/{boot-ddl-Dnvpmm_T.js → boot-ddl-DYH95Qeg.js} +3 -3
- package/dist/{boot-ddl-Dnvpmm_T.js.map → boot-ddl-DYH95Qeg.js.map} +1 -1
- package/dist/branch-argv.d.ts +15 -0
- package/dist/branch-prune.d.ts +18 -0
- package/dist/{cli-console-C_ISGwJo.js → cli-console-CQrQ0lAY.js} +19 -13
- package/dist/cli-console-CQrQ0lAY.js.map +1 -0
- package/dist/cli-console.d.ts +4 -3
- package/dist/{cli-errors-mx4sSjB8.js → cli-errors-DiA0XFIS.js} +110 -14
- package/dist/{cli-errors-mx4sSjB8.js.map → cli-errors-DiA0XFIS.js.map} +1 -1
- package/dist/cli-errors.d.ts +26 -1
- package/dist/cli-flags.d.ts +51 -0
- package/dist/{cli-prompt-egtNKkZl.js → cli-prompt-DXG0yk-h.js} +39 -4
- package/dist/{cli-prompt-egtNKkZl.js.map → cli-prompt-DXG0yk-h.js.map} +1 -1
- package/dist/cli.js +120 -184
- package/dist/cli.js.map +1 -1
- package/dist/collections/validate-relations.d.ts +8 -2
- package/dist/{column-plan-helpers-D_V4oaZh.js → column-plan-helpers-IG6uFb1s.js} +2 -2
- package/dist/{column-plan-helpers-D_V4oaZh.js.map → column-plan-helpers-IG6uFb1s.js.map} +1 -1
- package/dist/{connection-C938nR3E.js → connection-czvI7TZL.js} +57 -10
- package/dist/connection-czvI7TZL.js.map +1 -0
- package/dist/{databasePoolManager-Yqdw14wY.js → databasePoolManager-3stMNFL-.js} +4 -6
- package/dist/databasePoolManager-3stMNFL-.js.map +1 -0
- package/dist/db-command-line.d.ts +14 -0
- package/dist/{rls-bootstrap-sql-Bd-hJk08.js → db-subcommands-BPoHCxJR.js} +455 -3
- package/dist/db-subcommands-BPoHCxJR.js.map +1 -0
- package/dist/db-subcommands.d.ts +11 -0
- package/dist/{deferred-index-build-Ca6OqKyR.js → deferred-index-build-BE0I6sYU.js} +8 -8
- package/dist/{deferred-index-build-Ca6OqKyR.js.map → deferred-index-build-BE0I6sYU.js.map} +1 -1
- package/dist/{doctor-CJyuhV1n.js → doctor-BT--1kq3.js} +107 -11
- package/dist/doctor-BT--1kq3.js.map +1 -0
- package/dist/{ensure-collection-policies-C1ux3V_z.js → ensure-collection-policies-BnGOURpG.js} +60 -11
- package/dist/ensure-collection-policies-BnGOURpG.js.map +1 -0
- package/dist/{ensure-collection-tables-Do-Bvb64.js → ensure-collection-tables-Cet4650m.js} +9 -9
- package/dist/{ensure-collection-tables-Do-Bvb64.js.map → ensure-collection-tables-Cet4650m.js.map} +1 -1
- package/dist/{ensure-tables-Dl_Eh9ow.js → ensure-tables-l_OgFQjN.js} +5 -5
- package/dist/{ensure-tables-Dl_Eh9ow.js.map → ensure-tables-l_OgFQjN.js.map} +1 -1
- package/dist/{enum-labels-kept-D9eTIWCz.js → enum-labels-kept-nheku1pB.js} +3 -3
- package/dist/{enum-labels-kept-D9eTIWCz.js.map → enum-labels-kept-nheku1pB.js.map} +1 -1
- package/dist/{generate-drizzle-schema-Bf8canaR.js → generate-drizzle-schema-DC0BOUpn.js} +3 -3
- package/dist/{generate-drizzle-schema-Bf8canaR.js.map → generate-drizzle-schema-DC0BOUpn.js.map} +1 -1
- package/dist/{generate-drizzle-schema-logic-BaT0H7Gq.js → generate-drizzle-schema-logic-CPzCRD-8.js} +4 -4
- package/dist/{generate-drizzle-schema-logic-BaT0H7Gq.js.map → generate-drizzle-schema-logic-CPzCRD-8.js.map} +1 -1
- package/dist/{generate-files-D50Pxw1G.js → generate-files-BP-4W_gm.js} +6 -6
- package/dist/{generate-files-D50Pxw1G.js.map → generate-files-BP-4W_gm.js.map} +1 -1
- package/dist/{generate-next-step-DCd7yiJ2.js → generate-next-step-DgK5BlBn.js} +22 -14
- package/dist/{generate-next-step-DCd7yiJ2.js.map → generate-next-step-DgK5BlBn.js.map} +1 -1
- package/dist/{generate-postgres-ddl-logic-B4k9djAV.js → generate-postgres-ddl-logic-DPl1oH_z.js} +4 -4
- package/dist/{generate-postgres-ddl-logic-B4k9djAV.js.map → generate-postgres-ddl-logic-DPl1oH_z.js.map} +1 -1
- package/dist/generated-schema-diff-eZCklRZQ.js +1025 -0
- package/dist/generated-schema-diff-eZCklRZQ.js.map +1 -0
- package/dist/{generated-schema-staleness-FlxM2i4i.js → generated-schema-staleness-BvNF3i-s.js} +2 -2
- package/dist/{generated-schema-staleness-FlxM2i4i.js.map → generated-schema-staleness-BvNF3i-s.js.map} +1 -1
- package/dist/index.d.ts +1 -0
- package/dist/index.es.js +70 -11
- package/dist/index.es.js.map +1 -1
- package/dist/{introspect-db-logic-j2vYhmdP.js → introspect-db-logic-DgQVzccW.js} +2 -2
- package/dist/{introspect-db-logic-j2vYhmdP.js.map → introspect-db-logic-DgQVzccW.js.map} +1 -1
- package/dist/migrate-policy-stamps-3eqfhes6.js +89 -0
- package/dist/migrate-policy-stamps-3eqfhes6.js.map +1 -0
- package/dist/{open-rebase-D8rytTP0.js → open-rebase-EmqVmS14.js} +78 -1027
- package/dist/open-rebase-EmqVmS14.js.map +1 -0
- package/dist/{pg-error-utils-CdsGYMTm.js → pg-error-utils-CFEnm2m9.js} +11 -4
- package/dist/pg-error-utils-CFEnm2m9.js.map +1 -0
- package/dist/{plan-schema-CsNgK0qi.js → plan-schema-Czf3q4-Q.js} +4 -4
- package/dist/{plan-schema-CsNgK0qi.js.map → plan-schema-Czf3q4-Q.js.map} +1 -1
- package/dist/{policy-drift-CY5P1ZRu.js → policy-drift-X4A-xAzv.js} +3 -3
- package/dist/{policy-drift-CY5P1ZRu.js.map → policy-drift-X4A-xAzv.js.map} +1 -1
- package/dist/pool-disconnects.d.ts +28 -0
- package/dist/{push-index-prebuild-BDFcfRPo.js → push-index-prebuild-COd7i6wm.js} +4 -4
- package/dist/{push-index-prebuild-BDFcfRPo.js.map → push-index-prebuild-COd7i6wm.js.map} +1 -1
- package/dist/{push-legacy-renames-D4bvMWAb.js → push-legacy-renames-CLgYYwPE.js} +4 -4
- package/dist/{push-legacy-renames-D4bvMWAb.js.map → push-legacy-renames-CLgYYwPE.js.map} +1 -1
- package/dist/{push-policy-plan-DTXE6aVa.js → push-policy-plan-D2gpQsdv.js} +23 -7
- package/dist/push-policy-plan-D2gpQsdv.js.map +1 -0
- package/dist/{render-ddl-CrfEe__B.js → render-ddl-4v88y-Gv.js} +4 -4
- package/dist/{render-ddl-CrfEe__B.js.map → render-ddl-4v88y-Gv.js.map} +1 -1
- package/dist/{rls-enforcement-D7P4-ZYX.js → rls-enforcement-DDyNoOSy.js} +2 -2
- package/dist/{rls-enforcement-D7P4-ZYX.js.map → rls-enforcement-DDyNoOSy.js.map} +1 -1
- package/dist/schema/atlas-argv.d.ts +3 -1
- package/dist/schema/doctor-cli.js +5 -5
- package/dist/schema/doctor.d.ts +20 -2
- package/dist/schema/ensure-collection-policies.d.ts +28 -1
- package/dist/schema/generate-drizzle-schema.js +1 -1
- package/dist/schema/generate-next-step.d.ts +20 -8
- package/dist/schema/generate-postgres-ddl.js +2 -2
- package/dist/schema/generated-schema-diff.d.ts +21 -0
- package/dist/schema/introspect-db.js +6 -6
- package/dist/schema/introspect-db.js.map +1 -1
- package/dist/schema/migrate-policy-stamps.d.ts +23 -0
- package/dist/schema/push-policy-plan.d.ts +10 -2
- package/dist/services/BranchService.d.ts +85 -0
- package/dist/services/pg-notify-listener.d.ts +2 -0
- package/dist/{source-DB6xJv3p.js → source-fZb92pN8.js} +46 -3
- package/dist/source-fZb92pN8.js.map +1 -0
- package/dist/{vector-index-BFhyEaiz.js → vector-index-DsQ3V2d2.js} +2 -2
- package/dist/{vector-index-BFhyEaiz.js.map → vector-index-DsQ3V2d2.js.map} +1 -1
- package/dist/{websocket-DMeXOoyv.js → websocket-BKZwsjmf.js} +18 -5
- package/dist/websocket-BKZwsjmf.js.map +1 -0
- package/package.json +7 -7
- package/dist/BranchService-DNg4txyf.js.map +0 -1
- package/dist/backup-cli-HCEkPcQj.js +0 -556
- package/dist/backup-cli-HCEkPcQj.js.map +0 -1
- package/dist/backup-service-D-UpT7DH.js +0 -857
- package/dist/backup-service-D-UpT7DH.js.map +0 -1
- package/dist/cli-console-C_ISGwJo.js.map +0 -1
- package/dist/cli-flags-DUozBFyK.js +0 -205
- package/dist/cli-flags-DUozBFyK.js.map +0 -1
- package/dist/connection-C938nR3E.js.map +0 -1
- package/dist/databasePoolManager-Yqdw14wY.js.map +0 -1
- package/dist/doctor-CJyuhV1n.js.map +0 -1
- package/dist/ensure-collection-policies-C1ux3V_z.js.map +0 -1
- package/dist/open-rebase-D8rytTP0.js.map +0 -1
- package/dist/pg-error-utils-CdsGYMTm.js.map +0 -1
- package/dist/push-policy-plan-DTXE6aVa.js.map +0 -1
- package/dist/rls-bootstrap-sql-Bd-hJk08.js.map +0 -1
- package/dist/rolldown-runtime-V7rFbMRe.js +0 -39
- package/dist/source-DB6xJv3p.js.map +0 -1
- package/dist/websocket-DMeXOoyv.js.map +0 -1
|
@@ -1 +1 @@
|
|
|
1
|
-
{"version":3,"file":"policy-drift-CY5P1ZRu.js","names":[],"sources":["../src/security/policy-drift.ts"],"sourcesContent":["/**\n * Compare the RLS policies a database actually has against the ones the\n * collections describe.\n *\n * Policies live in Postgres; the collection config is only their *source*.\n * Nothing reconciled the two, which is how the demo database served empty\n * collections indefinitely: its policies granted `TO authenticated` (a Supabase\n * role name) while requests run as `rebase_user`, so RLS filtered every row.\n * The config was later corrected and the database never noticed — an empty\n * table is indistinguishable from a table with no data.\n *\n * Expected policies are parsed from `generatePostgresPoliciesDdl`, the same\n * function `db push` uses to write `.rebase/sql/policies.sql`, so this compares\n * against exactly what would be applied rather than a reimplementation.\n */\nimport { RLS_UID_SQL, type CollectionConfig } from \"@rebasepro/types\";\n\nimport { generatePostgresPoliciesDdl } from \"../schema/generate-postgres-ddl-logic\";\n\nexport interface PolicyRef {\n schema: string;\n table: string;\n name: string;\n /** Roles in the TO clause. */\n roles: string[];\n /** SELECT / INSERT / UPDATE / DELETE / ALL. */\n command: string;\n /** Whether a USING clause is present at all (not what it says). */\n hasUsing: boolean;\n /** Whether a WITH CHECK clause is present at all (not what it says). */\n hasWithCheck: boolean;\n /**\n * PERMISSIVE or RESTRICTIVE — the `AS` clause.\n *\n * An exact catalogue value on both sides, so it belongs with roles and\n * command rather than with the expression text. It matters more than either:\n * permissive policies are ORed together and restrictive ones ANDed, so a rule\n * declared `mode: \"restrictive\"` whose live policy is PERMISSIVE has had its\n * gate turned from a requirement into an alternative — the maximally\n * permissive way for this to be wrong.\n *\n * The DDL regex captured this from the start and the destructuring threw it\n * away; `pg_policies.permissive` was never selected.\n */\n mode?: \"PERMISSIVE\" | \"RESTRICTIVE\";\n /**\n * The live clause text, when read from `pg_policies`. Present only for live\n * policies (the expected side is parsed from DDL and does not carry it).\n * Used solely for the insecure-tautology scan, not for divergence — Postgres\n * rewrites this text, so it is not safe to diff against expected.\n */\n qual?: string | null;\n withCheck?: string | null;\n}\n\nexport interface PolicyDrift {\n /** Described by the collections, absent from the database. */\n missing: PolicyRef[];\n /** In the database, described by no collection — stale pushes live here. */\n orphaned: PolicyRef[];\n /** Same policy name, different roles or command. */\n diverged: { expected: PolicyRef; actual: PolicyRef; differences: string[] }[];\n /**\n * A live policy whose expression is the known-permissive tautology\n * `rebase.uid() IS NOT NULL` — true for anonymous visitors too, because the\n * user path coerces a blank id to the `'anonymous'` sentinel. This is what\n * `policy.authenticated()` used to compile to, so a database pushed before\n * that fix carries it, and neither the name, roles, command nor clause\n * *presence* differs from the corrected policy — the only thing that changed\n * is the expression text, which this checker otherwise (correctly) ignores.\n * So it is the one drift that hides from every other check here.\n *\n * @see reason a sentence naming the clause and what to do.\n */\n insecure: { policy: PolicyRef; reason: string }[];\n /**\n * A table the collections describe whose RLS switch is off.\n *\n * `ALTER TABLE posts DISABLE ROW LEVEL SECURITY` leaves every row in\n * `pg_policies` untouched, so before this category every expected policy\n * still matched on name, roles, command and clause presence and the checker\n * reported clean — on a table Postgres was applying no filter to at all.\n * Requests run as `rebase_user`, which holds full DML, so the table is wide\n * open while `doctor` certifies it.\n *\n * `forced` reports `relforcerowsecurity`, which is what also subjects the\n * table's *owner* to its policies. Its absence is not drift on its own —\n * Rebase does not connect as the owner in the request path — so it is\n * reported for context rather than raised as a failure.\n */\n rlsDisabled: { schema: string; table: string; forced: boolean }[];\n}\n\nexport interface Queryable {\n query<R>(text: string, values?: unknown[]): Promise<{ rows: R[] }>;\n}\n\n// The trailing group captures whichever clause follows the TO list, which is\n// what tells us the clause is present.\nconst CREATE_POLICY = /CREATE POLICY \"([^\"]+)\" ON \"([^\"]+)\"\\.\"([^\"]+)\"\\s+AS (\\w+)\\s+FOR (\\w+)\\s+TO ([^\\n]+?)(\\s+USING\\s*\\(|\\s+WITH CHECK\\s*\\(|;)/gi;\n\nconst WITH_CHECK_NEXT = /^\\s+WITH CHECK\\s*\\(/i;\n\n/**\n * Index just past the `)` closing a clause whose `(` ends at `open`.\n *\n * Needed because a policy expression nests parens and can contain a quoted\n * literal holding either character, so \"find the next `)`\" would stop early and\n * miss the `WITH CHECK` that follows.\n */\nfunction clauseEnd(ddl: string, open: number): number {\n let depth = 1;\n let inQuote = false;\n for (let i = open; i < ddl.length; i++) {\n const c = ddl[i];\n if (inQuote) {\n // '' is an escaped quote inside a string, not a close.\n if (c === \"'\") {\n if (ddl[i + 1] === \"'\") i++;\n else inQuote = false;\n }\n continue;\n }\n if (c === \"'\") inQuote = true;\n else if (c === \"(\") depth++;\n else if (c === \")\" && --depth === 0) return i + 1;\n }\n return ddl.length;\n}\n\n/**\n * `AS PERMISSIVE` / `AS RESTRICTIVE` from either side, or undefined.\n *\n * The DDL spells it as a bare word; `pg_policies.permissive` is the string\n * `\"PERMISSIVE\"`/`\"RESTRICTIVE\"` on modern Postgres but a boolean on some\n * drivers and older servers, so both are accepted. Anything unrecognised\n * becomes `undefined` and the comparison below skips it rather than inventing\n * drift from a shape we did not anticipate.\n */\nfunction normalizeMode(raw: unknown): \"PERMISSIVE\" | \"RESTRICTIVE\" | undefined {\n if (typeof raw === \"boolean\") return raw ? \"PERMISSIVE\" : \"RESTRICTIVE\";\n if (typeof raw !== \"string\") return undefined;\n const upper = raw.trim().toUpperCase();\n if (upper === \"PERMISSIVE\" || upper === \"TRUE\" || upper === \"T\") return \"PERMISSIVE\";\n if (upper === \"RESTRICTIVE\" || upper === \"FALSE\" || upper === \"F\") return \"RESTRICTIVE\";\n return undefined;\n}\n\n/** Parse the generated DDL rather than rebuilding the shape by hand. */\nexport function parseExpectedPolicies(ddl: string): PolicyRef[] {\n const found: PolicyRef[] = [];\n for (const m of ddl.matchAll(CREATE_POLICY)) {\n const [, name, schema, table, modeRaw, command, rolesRaw, clause] = m;\n const roles = rolesRaw\n .split(\",\")\n .map((r) => r.trim().replace(/^\"|\"$/g, \"\"))\n .filter(Boolean);\n\n // The generator emits USING before WITH CHECK, so what follows the TO\n // list settles USING; WITH CHECK is then whatever follows that clause.\n const hasUsing = /USING/i.test(clause);\n const hasWithCheck = hasUsing\n ? WITH_CHECK_NEXT.test(ddl.slice(clauseEnd(ddl, m.index + m[0].length)))\n : /WITH CHECK/i.test(clause);\n\n found.push({\n schema, table, name, roles,\n command: command.toUpperCase(),\n hasUsing, hasWithCheck,\n mode: normalizeMode(modeRaw)\n });\n }\n return found;\n}\n\nasync function readLivePolicies(client: Queryable, schemas: string[]): Promise<PolicyRef[]> {\n const { rows } = await client.query<{\n schemaname: string; tablename: string; policyname: string; roles: string[] | string; cmd: string;\n qual: string | null; with_check: string | null; permissive: string | boolean | null;\n }>(\n `SELECT schemaname, tablename, policyname, roles, cmd, qual, with_check, permissive\n FROM pg_policies\n WHERE schemaname = ANY($1)`,\n [schemas]\n );\n\n return rows.map((r) => ({\n schema: r.schemaname,\n table: r.tablename,\n name: r.policyname,\n // node-postgres yields text[] as an array; some drivers hand back \"{a,b}\".\n roles: Array.isArray(r.roles)\n ? r.roles\n : String(r.roles ?? \"\").replace(/^\\{|\\}$/g, \"\").split(\",\").filter(Boolean),\n command: (r.cmd ?? \"ALL\").toUpperCase(),\n // Presence only. Postgres rewrites the text, but it does not invent or\n // drop a clause: NULL here means the policy genuinely has none.\n hasUsing: r.qual != null,\n hasWithCheck: r.with_check != null,\n mode: normalizeMode(r.permissive),\n qual: r.qual,\n withCheck: r.with_check\n }));\n}\n\n/**\n * Tables the collections describe whose RLS switch is off.\n *\n * Read from `pg_class` because `pg_policies` cannot answer it: disabling RLS\n * leaves the policy rows in place, so every other comparison in this file keeps\n * matching while Postgres applies none of them.\n *\n * Only tables that expect at least one policy are considered. A table with no\n * declared rules is not asserting anything about RLS, and reporting it would\n * make the check noisy on exactly the projects least able to judge the noise.\n */\nasync function readTablesWithRlsOff(\n client: Queryable,\n tables: { schema: string; table: string }[]\n): Promise<{ schema: string; table: string; forced: boolean }[]> {\n if (tables.length === 0) return [];\n const { rows } = await client.query<{\n schemaname: string; tablename: string; relrowsecurity: boolean; relforcerowsecurity: boolean;\n }>(\n `SELECT n.nspname AS schemaname,\n c.relname AS tablename,\n c.relrowsecurity,\n c.relforcerowsecurity\n FROM pg_class c\n JOIN pg_namespace n ON n.oid = c.relnamespace\n WHERE c.relkind = 'r'\n AND (n.nspname || '.' || c.relname) = ANY($1)`,\n [tables.map((t) => `${t.schema}.${t.table}`)]\n );\n\n return rows\n .filter((r) => !r.relrowsecurity)\n .map((r) => ({\n schema: r.schemaname,\n table: r.tablename,\n forced: !!r.relforcerowsecurity\n }));\n}\n\n/**\n * The permissive tautology `rebase.uid() IS NOT NULL`, without the\n * `<> 'anonymous'` guard that makes it mean \"signed in\".\n *\n * Whitespace varies with Postgres's rewrite, so match on a collapsed form. The\n * guard clause ({@link EXCLUDES_ANONYMOUS}, in any spelling) is what\n * distinguishes the corrected policy from the stale one, so its presence\n * clears the text.\n *\n * Both schema spellings are matched. The helpers moved from `auth` to `rebase`\n * in 1.0, and this check reads policies *as the database stored them*: a\n * database pushed before the move still holds `auth.uid()` until it is\n * recompiled, while everything written since — including raw `securityRules`\n * SQL, which the compiler rewrites on the way in — stores `rebase.uid()`.\n * Matching only the pre-1.0 name made the check blind to every policy a current\n * release could write, which is precisely the set still worth scanning.\n */\nfunction isPermissiveAuthTautology(clause: string | null | undefined): boolean {\n if (!clause) return false;\n const flat = clause.toLowerCase().replace(/\\s+/g, \" \");\n // The call bare, or wrapped as an initPlan, which Postgres stores as\n // `( SELECT rebase.uid() AS uid)`.\n if (!/\\b(?:rebase|auth)\\.uid\\s*\\(\\s*\\)(?:\\s+as\\s+\\w+\\s*\\))?\\s*is not null/.test(flat)) return false;\n return !EXCLUDES_ANONYMOUS.some((guard) => guard.test(flat));\n}\n\n/**\n * The guard that makes the null test mean \"signed in\": the anonymous\n * sentinel excluded, in every spelling a policy reaches the catalogue in.\n *\n * `policy.authenticated()` compiles to `NOT IN ('anonymous', 'anon')`, which\n * Postgres stores as `<> ALL (ARRAY['anonymous'::text, 'anon'::text])`. Only\n * `<> 'anonymous'` was recognised, so `doctor` and every `db push` called each\n * `authenticated()` and `registered()` policy the insecure tautology. A list\n * that leaves out `'anonymous'` excludes nobody a signed-out request arrives\n * as, and does not clear it.\n */\nconst EXCLUDES_ANONYMOUS: RegExp[] = [\n /(?:<>|!=)\\s*'anonymous'/,\n /(?:<>|!=)\\s*all\\s*\\(\\s*array\\s*\\[[^\\]]*'anonymous'/,\n /\\bnot in\\s*\\([^)]*'anonymous'/\n];\n\nconst keyOf = (p: PolicyRef) => `${p.schema}.${p.table}.${p.name}`;\nconst sameRoles = (a: string[], b: string[]) =>\n a.length === b.length && [...a].sort().join(\",\") === [...b].sort().join(\",\");\n\n/**\n * Diff expected against live.\n *\n * Compares names, roles, command, and whether each clause exists — all exact\n * values. Policy expression *text* is deliberately not compared: Postgres\n * rewrites `qual`/`with_check` when storing them (parenthesising, casting,\n * schema-qualifying), so text comparison reports drift that does not exist, and\n * a check that cries wolf gets ignored.\n *\n * Presence is not text, though. A NULL `qual` is not a rewrite of an\n * expression, it is the absence of one, and absence has no false-positive risk:\n * either the generator emitted a clause or it did not. That distinction is worth\n * the extra comparison — a production database was found with a SELECT policy\n * whose `qual` was NULL, matching on every field this checked and denying 100%\n * of reads. The same blindness would hide a policy that fails open.\n */\nexport async function checkPolicyDrift(\n client: Queryable,\n collections: CollectionConfig[]\n): Promise<PolicyDrift> {\n const expected = parseExpectedPolicies(generatePostgresPoliciesDdl(collections));\n const schemas = [...new Set(expected.map((p) => p.schema))];\n // Nothing expected means nothing to reconcile against; scanning every\n // schema would report the whole database as orphaned.\n if (schemas.length === 0) return { missing: [], orphaned: [], diverged: [], insecure: [], rlsDisabled: [] };\n\n const live = await readLivePolicies(client, schemas);\n const liveByKey = new Map(live.map((p) => [keyOf(p), p]));\n const expectedByKey = new Map(expected.map((p) => [keyOf(p), p]));\n\n const drift: PolicyDrift = { missing: [], orphaned: [], diverged: [], insecure: [], rlsDisabled: [] };\n\n // Before comparing policy against policy: is the switch even on? A table\n // with RLS disabled matches every expected policy on every field compared\n // below, so this has to be asked separately or it cannot be asked at all.\n const expectedTables = [...new Map(\n expected.map((p) => [`${p.schema}.${p.table}`, { schema: p.schema, table: p.table }])\n ).values()];\n drift.rlsDisabled = await readTablesWithRlsOff(client, expectedTables);\n\n // Scan every live policy for the permissive tautology. This is deliberately\n // independent of the name-keyed diff below: a database pushed before the\n // `authenticated()` fix matches its expected policy on name, roles, command\n // and clause presence, so nothing else here would flag it.\n for (const p of live) {\n const clause = isPermissiveAuthTautology(p.qual)\n ? \"USING\"\n : isPermissiveAuthTautology(p.withCheck) ? \"WITH CHECK\" : null;\n if (clause) {\n drift.insecure.push({\n policy: p,\n reason: `${clause} is \\`${RLS_UID_SQL} IS NOT NULL\\`, which is true for anonymous ` +\n `visitors too — this grants access to signed-out requests. It predates the ` +\n `\\`policy.authenticated()\\` fix; re-run \\`rebase db push\\` to tighten it.`\n });\n }\n }\n\n for (const [key, want] of expectedByKey) {\n const got = liveByKey.get(key);\n if (!got) {\n drift.missing.push(want);\n continue;\n }\n const differences: string[] = [];\n if (!sameRoles(want.roles, got.roles)) {\n differences.push(`roles: expected [${want.roles.join(\", \")}], database has [${got.roles.join(\", \")}]`);\n }\n if (want.command !== got.command) {\n differences.push(`command: expected ${want.command}, database has ${got.command}`);\n }\n // Skipped when either side is unknown: an unrecognised catalogue shape\n // should not manufacture drift. See `normalizeMode`.\n if (want.mode && got.mode && want.mode !== got.mode) {\n differences.push(\n `mode: expected ${want.mode}, database has ${got.mode}` +\n (got.mode === \"PERMISSIVE\"\n ? \" — a RESTRICTIVE rule is ANDed with the others, a PERMISSIVE one is ORed, so this policy now widens access instead of narrowing it\"\n : \" — a PERMISSIVE rule is ORed with the others, a RESTRICTIVE one is ANDed, so this policy now narrows access instead of widening it\")\n );\n }\n for (const clause of [\"USING\", \"WITH CHECK\"] as const) {\n const key = clause === \"USING\" ? \"hasUsing\" : \"hasWithCheck\";\n if (want[key] === got[key]) continue;\n differences.push(want[key]\n ? `${clause}: expected an expression, database has none — this policy matches no rows`\n : `${clause}: expected none, database has an expression`);\n }\n if (differences.length > 0) drift.diverged.push({ expected: want, actual: got, differences });\n }\n\n for (const [key, got] of liveByKey) {\n if (!expectedByKey.has(key)) drift.orphaned.push(got);\n }\n\n return drift;\n}\n\n/**\n * Does this name look like one the generator produced for this table?\n *\n * Unnamed rules compile to `<table>_<op>_<sha1[0:7]>` (plus `_<idx>` when one\n * rule spans several operations), and the hash covers the rule's semantics — so\n * *editing* a rule renames its policy. The policy under the old name is left\n * behind by `db push`, which only DROPs the names it is about to CREATE, and\n * Postgres ORs PERMISSIVE policies together: a superseded `USING (true)` keeps\n * granting everything no matter how tight its replacement is.\n *\n * Matching the shape is what makes dropping them safe. A hand-written policy\n * would have to collide with a 7-hex digest to be mistaken for generated one;\n * a policy named anything else is left alone and merely reported, because a\n * custom name is indistinguishable from one someone wrote in SQL on purpose.\n */\nexport function isGeneratedPolicyName(name: string, table: string): boolean {\n return new RegExp(`^${table.replace(/[.*+?^${}()|[\\]\\\\]/g, \"\\\\$&\")}_(select|insert|update|delete|all)_[0-9a-f]{7}(_\\\\d+)?$`)\n .test(name);\n}\n\nexport interface OrphanCleanup {\n /** Superseded generated policies that were dropped. */\n dropped: PolicyRef[];\n /** Orphans left in place because their names are not generator-shaped. */\n kept: PolicyRef[];\n}\n\n/**\n * Drop the policies an earlier push superseded but never removed.\n *\n * Only touches tables the collections describe — a table with no expected\n * policy is not ours to reconcile, and scanning by schema alone would sweep up\n * policies belonging to something else sharing the database.\n */\nexport async function dropOrphanedPolicies(\n client: Queryable,\n drift: PolicyDrift,\n collections: CollectionConfig[]\n): Promise<OrphanCleanup> {\n const { dropped, kept } = partitionOrphans(drift, collections);\n for (const p of dropped) {\n // Identifiers are quoted, and the name came from pg_policies rather than\n // from user input, so it is already a valid identifier.\n await client.query(`DROP POLICY IF EXISTS \"${p.name}\" ON \"${p.schema}\".\"${p.table}\"`);\n }\n return { dropped, kept };\n}\n\n/**\n * Which orphans {@link dropOrphanedPolicies} drops and which it keeps, decided\n * without dropping anything: what `db push --dry-run` lists.\n */\nexport function partitionOrphans(drift: PolicyDrift, collections: CollectionConfig[]): OrphanCleanup {\n const expected = parseExpectedPolicies(generatePostgresPoliciesDdl(collections));\n const managed = new Set(expected.map((p) => `${p.schema}.${p.table}`));\n\n const cleanup: OrphanCleanup = { dropped: [], kept: [] };\n for (const p of drift.orphaned) {\n if (managed.has(`${p.schema}.${p.table}`) && isGeneratedPolicyName(p.name, p.table)) cleanup.dropped.push(p);\n else cleanup.kept.push(p);\n }\n return cleanup;\n}\n\nexport const hasDrift = (d: PolicyDrift): boolean =>\n d.missing.length > 0 || d.orphaned.length > 0 || d.diverged.length > 0 || d.insecure.length > 0\n || d.rlsDisabled.length > 0;\n\n/** Human-readable report; empty string when the database matches the config. */\nexport function formatPolicyDrift(drift: PolicyDrift): string {\n if (!hasDrift(drift)) return \"\";\n const lines: string[] = [];\n\n // First, because it subsumes every other finding on the same table: if RLS\n // is off, the policies listed below are not being applied at all.\n if (drift.rlsDisabled.length > 0) {\n lines.push(\" RLS DISABLED — the table has policies, and Postgres is applying none of them:\");\n for (const t of drift.rlsDisabled) {\n lines.push(` • ${t.schema}.${t.table}${t.forced ? \"\" : \" (and FORCE is off)\"}`);\n }\n lines.push(\" Every row is readable and writable by any request that reaches the table.\");\n lines.push(` Fix: ALTER TABLE \"<schema>\".\"<table>\" ENABLE ROW LEVEL SECURITY; or re-run \\`rebase db push\\`.`);\n }\n if (drift.missing.length > 0) {\n lines.push(\" Missing — described by your collections, absent from the database:\");\n for (const p of drift.missing) lines.push(` • ${p.schema}.${p.table} → \"${p.name}\" (${p.command} TO ${p.roles.join(\", \")})`);\n lines.push(\" Run `rebase db push` to apply them.\");\n }\n if (drift.orphaned.length > 0) {\n // Two different things share this list, and they want opposite\n // advice. A name of the shape an unnamed rule compiles to is a rule\n // edited since: the next push drops it. Any other name was written by\n // hand or by another tool, and may be the table's whole access model —\n // \"left behind by an earlier push\" was false of it, and telling its\n // author to drop it would take that access away.\n const superseded = drift.orphaned.filter((p) => isGeneratedPolicyName(p.name, p.table));\n const unowned = drift.orphaned.filter((p) => !isGeneratedPolicyName(p.name, p.table));\n lines.push(\" Orphaned — in the database, described by no collection:\");\n for (const p of drift.orphaned) lines.push(` • ${p.schema}.${p.table} → \"${p.name}\" (${p.command} TO ${p.roles.join(\", \")})`);\n if (superseded.length > 0) {\n lines.push(` ${superseded.map((p) => `\"${p.name}\"`).join(\", \")}: left behind by an earlier push, when its rule changed. The next \\`rebase db push\\` drops it.`);\n }\n if (unowned.length > 0) {\n lines.push(` ${unowned.map((p) => `\"${p.name}\"`).join(\", \")}: written by hand or by another tool, and still in force.`);\n lines.push(\" To keep one, describe it in its collection's `securityRules` (Studio's RLS view imports it);\");\n lines.push(\" if it is stale, drop it by hand.\");\n }\n }\n if (drift.diverged.length > 0) {\n lines.push(\" Diverged — same policy, different definition:\");\n for (const d of drift.diverged) {\n lines.push(` • ${d.expected.schema}.${d.expected.table} → \"${d.expected.name}\"`);\n for (const diff of d.differences) lines.push(` ${diff}`);\n }\n }\n if (drift.insecure.length > 0) {\n lines.push(\" Insecure — a live policy grants access it should not:\");\n for (const i of drift.insecure) {\n lines.push(` • ${i.policy.schema}.${i.policy.table} → \"${i.policy.name}\"`);\n lines.push(` ${i.reason}`);\n }\n }\n return lines.join(\"\\n\");\n}\n"],"mappings":"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"}
|
|
1
|
+
{"version":3,"file":"policy-drift-X4A-xAzv.js","names":[],"sources":["../src/security/policy-drift.ts"],"sourcesContent":["/**\n * Compare the RLS policies a database actually has against the ones the\n * collections describe.\n *\n * Policies live in Postgres; the collection config is only their *source*.\n * Nothing reconciled the two, which is how the demo database served empty\n * collections indefinitely: its policies granted `TO authenticated` (a Supabase\n * role name) while requests run as `rebase_user`, so RLS filtered every row.\n * The config was later corrected and the database never noticed — an empty\n * table is indistinguishable from a table with no data.\n *\n * Expected policies are parsed from `generatePostgresPoliciesDdl`, the same\n * function `db push` uses to write `.rebase/sql/policies.sql`, so this compares\n * against exactly what would be applied rather than a reimplementation.\n */\nimport { RLS_UID_SQL, type CollectionConfig } from \"@rebasepro/types\";\n\nimport { generatePostgresPoliciesDdl } from \"../schema/generate-postgres-ddl-logic\";\n\nexport interface PolicyRef {\n schema: string;\n table: string;\n name: string;\n /** Roles in the TO clause. */\n roles: string[];\n /** SELECT / INSERT / UPDATE / DELETE / ALL. */\n command: string;\n /** Whether a USING clause is present at all (not what it says). */\n hasUsing: boolean;\n /** Whether a WITH CHECK clause is present at all (not what it says). */\n hasWithCheck: boolean;\n /**\n * PERMISSIVE or RESTRICTIVE — the `AS` clause.\n *\n * An exact catalogue value on both sides, so it belongs with roles and\n * command rather than with the expression text. It matters more than either:\n * permissive policies are ORed together and restrictive ones ANDed, so a rule\n * declared `mode: \"restrictive\"` whose live policy is PERMISSIVE has had its\n * gate turned from a requirement into an alternative — the maximally\n * permissive way for this to be wrong.\n *\n * The DDL regex captured this from the start and the destructuring threw it\n * away; `pg_policies.permissive` was never selected.\n */\n mode?: \"PERMISSIVE\" | \"RESTRICTIVE\";\n /**\n * The live clause text, when read from `pg_policies`. Present only for live\n * policies (the expected side is parsed from DDL and does not carry it).\n * Used solely for the insecure-tautology scan, not for divergence — Postgres\n * rewrites this text, so it is not safe to diff against expected.\n */\n qual?: string | null;\n withCheck?: string | null;\n}\n\nexport interface PolicyDrift {\n /** Described by the collections, absent from the database. */\n missing: PolicyRef[];\n /** In the database, described by no collection — stale pushes live here. */\n orphaned: PolicyRef[];\n /** Same policy name, different roles or command. */\n diverged: { expected: PolicyRef; actual: PolicyRef; differences: string[] }[];\n /**\n * A live policy whose expression is the known-permissive tautology\n * `rebase.uid() IS NOT NULL` — true for anonymous visitors too, because the\n * user path coerces a blank id to the `'anonymous'` sentinel. This is what\n * `policy.authenticated()` used to compile to, so a database pushed before\n * that fix carries it, and neither the name, roles, command nor clause\n * *presence* differs from the corrected policy — the only thing that changed\n * is the expression text, which this checker otherwise (correctly) ignores.\n * So it is the one drift that hides from every other check here.\n *\n * @see reason a sentence naming the clause and what to do.\n */\n insecure: { policy: PolicyRef; reason: string }[];\n /**\n * A table the collections describe whose RLS switch is off.\n *\n * `ALTER TABLE posts DISABLE ROW LEVEL SECURITY` leaves every row in\n * `pg_policies` untouched, so before this category every expected policy\n * still matched on name, roles, command and clause presence and the checker\n * reported clean — on a table Postgres was applying no filter to at all.\n * Requests run as `rebase_user`, which holds full DML, so the table is wide\n * open while `doctor` certifies it.\n *\n * `forced` reports `relforcerowsecurity`, which is what also subjects the\n * table's *owner* to its policies. Its absence is not drift on its own —\n * Rebase does not connect as the owner in the request path — so it is\n * reported for context rather than raised as a failure.\n */\n rlsDisabled: { schema: string; table: string; forced: boolean }[];\n}\n\nexport interface Queryable {\n query<R>(text: string, values?: unknown[]): Promise<{ rows: R[] }>;\n}\n\n// The trailing group captures whichever clause follows the TO list, which is\n// what tells us the clause is present.\nconst CREATE_POLICY = /CREATE POLICY \"([^\"]+)\" ON \"([^\"]+)\"\\.\"([^\"]+)\"\\s+AS (\\w+)\\s+FOR (\\w+)\\s+TO ([^\\n]+?)(\\s+USING\\s*\\(|\\s+WITH CHECK\\s*\\(|;)/gi;\n\nconst WITH_CHECK_NEXT = /^\\s+WITH CHECK\\s*\\(/i;\n\n/**\n * Index just past the `)` closing a clause whose `(` ends at `open`.\n *\n * Needed because a policy expression nests parens and can contain a quoted\n * literal holding either character, so \"find the next `)`\" would stop early and\n * miss the `WITH CHECK` that follows.\n */\nfunction clauseEnd(ddl: string, open: number): number {\n let depth = 1;\n let inQuote = false;\n for (let i = open; i < ddl.length; i++) {\n const c = ddl[i];\n if (inQuote) {\n // '' is an escaped quote inside a string, not a close.\n if (c === \"'\") {\n if (ddl[i + 1] === \"'\") i++;\n else inQuote = false;\n }\n continue;\n }\n if (c === \"'\") inQuote = true;\n else if (c === \"(\") depth++;\n else if (c === \")\" && --depth === 0) return i + 1;\n }\n return ddl.length;\n}\n\n/**\n * `AS PERMISSIVE` / `AS RESTRICTIVE` from either side, or undefined.\n *\n * The DDL spells it as a bare word; `pg_policies.permissive` is the string\n * `\"PERMISSIVE\"`/`\"RESTRICTIVE\"` on modern Postgres but a boolean on some\n * drivers and older servers, so both are accepted. Anything unrecognised\n * becomes `undefined` and the comparison below skips it rather than inventing\n * drift from a shape we did not anticipate.\n */\nfunction normalizeMode(raw: unknown): \"PERMISSIVE\" | \"RESTRICTIVE\" | undefined {\n if (typeof raw === \"boolean\") return raw ? \"PERMISSIVE\" : \"RESTRICTIVE\";\n if (typeof raw !== \"string\") return undefined;\n const upper = raw.trim().toUpperCase();\n if (upper === \"PERMISSIVE\" || upper === \"TRUE\" || upper === \"T\") return \"PERMISSIVE\";\n if (upper === \"RESTRICTIVE\" || upper === \"FALSE\" || upper === \"F\") return \"RESTRICTIVE\";\n return undefined;\n}\n\n/** Parse the generated DDL rather than rebuilding the shape by hand. */\nexport function parseExpectedPolicies(ddl: string): PolicyRef[] {\n const found: PolicyRef[] = [];\n for (const m of ddl.matchAll(CREATE_POLICY)) {\n const [, name, schema, table, modeRaw, command, rolesRaw, clause] = m;\n const roles = rolesRaw\n .split(\",\")\n .map((r) => r.trim().replace(/^\"|\"$/g, \"\"))\n .filter(Boolean);\n\n // The generator emits USING before WITH CHECK, so what follows the TO\n // list settles USING; WITH CHECK is then whatever follows that clause.\n const hasUsing = /USING/i.test(clause);\n const hasWithCheck = hasUsing\n ? WITH_CHECK_NEXT.test(ddl.slice(clauseEnd(ddl, m.index + m[0].length)))\n : /WITH CHECK/i.test(clause);\n\n found.push({\n schema, table, name, roles,\n command: command.toUpperCase(),\n hasUsing, hasWithCheck,\n mode: normalizeMode(modeRaw)\n });\n }\n return found;\n}\n\nasync function readLivePolicies(client: Queryable, schemas: string[]): Promise<PolicyRef[]> {\n const { rows } = await client.query<{\n schemaname: string; tablename: string; policyname: string; roles: string[] | string; cmd: string;\n qual: string | null; with_check: string | null; permissive: string | boolean | null;\n }>(\n `SELECT schemaname, tablename, policyname, roles, cmd, qual, with_check, permissive\n FROM pg_policies\n WHERE schemaname = ANY($1)`,\n [schemas]\n );\n\n return rows.map((r) => ({\n schema: r.schemaname,\n table: r.tablename,\n name: r.policyname,\n // node-postgres yields text[] as an array; some drivers hand back \"{a,b}\".\n roles: Array.isArray(r.roles)\n ? r.roles\n : String(r.roles ?? \"\").replace(/^\\{|\\}$/g, \"\").split(\",\").filter(Boolean),\n command: (r.cmd ?? \"ALL\").toUpperCase(),\n // Presence only. Postgres rewrites the text, but it does not invent or\n // drop a clause: NULL here means the policy genuinely has none.\n hasUsing: r.qual != null,\n hasWithCheck: r.with_check != null,\n mode: normalizeMode(r.permissive),\n qual: r.qual,\n withCheck: r.with_check\n }));\n}\n\n/**\n * Tables the collections describe whose RLS switch is off.\n *\n * Read from `pg_class` because `pg_policies` cannot answer it: disabling RLS\n * leaves the policy rows in place, so every other comparison in this file keeps\n * matching while Postgres applies none of them.\n *\n * Only tables that expect at least one policy are considered. A table with no\n * declared rules is not asserting anything about RLS, and reporting it would\n * make the check noisy on exactly the projects least able to judge the noise.\n */\nasync function readTablesWithRlsOff(\n client: Queryable,\n tables: { schema: string; table: string }[]\n): Promise<{ schema: string; table: string; forced: boolean }[]> {\n if (tables.length === 0) return [];\n const { rows } = await client.query<{\n schemaname: string; tablename: string; relrowsecurity: boolean; relforcerowsecurity: boolean;\n }>(\n `SELECT n.nspname AS schemaname,\n c.relname AS tablename,\n c.relrowsecurity,\n c.relforcerowsecurity\n FROM pg_class c\n JOIN pg_namespace n ON n.oid = c.relnamespace\n WHERE c.relkind = 'r'\n AND (n.nspname || '.' || c.relname) = ANY($1)`,\n [tables.map((t) => `${t.schema}.${t.table}`)]\n );\n\n return rows\n .filter((r) => !r.relrowsecurity)\n .map((r) => ({\n schema: r.schemaname,\n table: r.tablename,\n forced: !!r.relforcerowsecurity\n }));\n}\n\n/**\n * The permissive tautology `rebase.uid() IS NOT NULL`, without the\n * `<> 'anonymous'` guard that makes it mean \"signed in\".\n *\n * Whitespace varies with Postgres's rewrite, so match on a collapsed form. The\n * guard clause ({@link EXCLUDES_ANONYMOUS}, in any spelling) is what\n * distinguishes the corrected policy from the stale one, so its presence\n * clears the text.\n *\n * Both schema spellings are matched. The helpers moved from `auth` to `rebase`\n * in 1.0, and this check reads policies *as the database stored them*: a\n * database pushed before the move still holds `auth.uid()` until it is\n * recompiled, while everything written since — including raw `securityRules`\n * SQL, which the compiler rewrites on the way in — stores `rebase.uid()`.\n * Matching only the pre-1.0 name made the check blind to every policy a current\n * release could write, which is precisely the set still worth scanning.\n */\nfunction isPermissiveAuthTautology(clause: string | null | undefined): boolean {\n if (!clause) return false;\n const flat = clause.toLowerCase().replace(/\\s+/g, \" \");\n // The call bare, or wrapped as an initPlan, which Postgres stores as\n // `( SELECT rebase.uid() AS uid)`.\n if (!/\\b(?:rebase|auth)\\.uid\\s*\\(\\s*\\)(?:\\s+as\\s+\\w+\\s*\\))?\\s*is not null/.test(flat)) return false;\n return !EXCLUDES_ANONYMOUS.some((guard) => guard.test(flat));\n}\n\n/**\n * The guard that makes the null test mean \"signed in\": the anonymous\n * sentinel excluded, in every spelling a policy reaches the catalogue in.\n *\n * `policy.authenticated()` compiles to `NOT IN ('anonymous', 'anon')`, which\n * Postgres stores as `<> ALL (ARRAY['anonymous'::text, 'anon'::text])`. Only\n * `<> 'anonymous'` was recognised, so `doctor` and every `db push` called each\n * `authenticated()` and `registered()` policy the insecure tautology. A list\n * that leaves out `'anonymous'` excludes nobody a signed-out request arrives\n * as, and does not clear it.\n */\nconst EXCLUDES_ANONYMOUS: RegExp[] = [\n /(?:<>|!=)\\s*'anonymous'/,\n /(?:<>|!=)\\s*all\\s*\\(\\s*array\\s*\\[[^\\]]*'anonymous'/,\n /\\bnot in\\s*\\([^)]*'anonymous'/\n];\n\nconst keyOf = (p: PolicyRef) => `${p.schema}.${p.table}.${p.name}`;\nconst sameRoles = (a: string[], b: string[]) =>\n a.length === b.length && [...a].sort().join(\",\") === [...b].sort().join(\",\");\n\n/**\n * Diff expected against live.\n *\n * Compares names, roles, command, and whether each clause exists — all exact\n * values. Policy expression *text* is deliberately not compared: Postgres\n * rewrites `qual`/`with_check` when storing them (parenthesising, casting,\n * schema-qualifying), so text comparison reports drift that does not exist, and\n * a check that cries wolf gets ignored.\n *\n * Presence is not text, though. A NULL `qual` is not a rewrite of an\n * expression, it is the absence of one, and absence has no false-positive risk:\n * either the generator emitted a clause or it did not. That distinction is worth\n * the extra comparison — a production database was found with a SELECT policy\n * whose `qual` was NULL, matching on every field this checked and denying 100%\n * of reads. The same blindness would hide a policy that fails open.\n */\nexport async function checkPolicyDrift(\n client: Queryable,\n collections: CollectionConfig[]\n): Promise<PolicyDrift> {\n const expected = parseExpectedPolicies(generatePostgresPoliciesDdl(collections));\n const schemas = [...new Set(expected.map((p) => p.schema))];\n // Nothing expected means nothing to reconcile against; scanning every\n // schema would report the whole database as orphaned.\n if (schemas.length === 0) return { missing: [], orphaned: [], diverged: [], insecure: [], rlsDisabled: [] };\n\n const live = await readLivePolicies(client, schemas);\n const liveByKey = new Map(live.map((p) => [keyOf(p), p]));\n const expectedByKey = new Map(expected.map((p) => [keyOf(p), p]));\n\n const drift: PolicyDrift = { missing: [], orphaned: [], diverged: [], insecure: [], rlsDisabled: [] };\n\n // Before comparing policy against policy: is the switch even on? A table\n // with RLS disabled matches every expected policy on every field compared\n // below, so this has to be asked separately or it cannot be asked at all.\n const expectedTables = [...new Map(\n expected.map((p) => [`${p.schema}.${p.table}`, { schema: p.schema, table: p.table }])\n ).values()];\n drift.rlsDisabled = await readTablesWithRlsOff(client, expectedTables);\n\n // Scan every live policy for the permissive tautology. This is deliberately\n // independent of the name-keyed diff below: a database pushed before the\n // `authenticated()` fix matches its expected policy on name, roles, command\n // and clause presence, so nothing else here would flag it.\n for (const p of live) {\n const clause = isPermissiveAuthTautology(p.qual)\n ? \"USING\"\n : isPermissiveAuthTautology(p.withCheck) ? \"WITH CHECK\" : null;\n if (clause) {\n drift.insecure.push({\n policy: p,\n reason: `${clause} is \\`${RLS_UID_SQL} IS NOT NULL\\`, which is true for anonymous ` +\n `visitors too — this grants access to signed-out requests. It predates the ` +\n `\\`policy.authenticated()\\` fix; re-run \\`rebase db push\\` to tighten it.`\n });\n }\n }\n\n for (const [key, want] of expectedByKey) {\n const got = liveByKey.get(key);\n if (!got) {\n drift.missing.push(want);\n continue;\n }\n const differences: string[] = [];\n if (!sameRoles(want.roles, got.roles)) {\n differences.push(`roles: expected [${want.roles.join(\", \")}], database has [${got.roles.join(\", \")}]`);\n }\n if (want.command !== got.command) {\n differences.push(`command: expected ${want.command}, database has ${got.command}`);\n }\n // Skipped when either side is unknown: an unrecognised catalogue shape\n // should not manufacture drift. See `normalizeMode`.\n if (want.mode && got.mode && want.mode !== got.mode) {\n differences.push(\n `mode: expected ${want.mode}, database has ${got.mode}` +\n (got.mode === \"PERMISSIVE\"\n ? \" — a RESTRICTIVE rule is ANDed with the others, a PERMISSIVE one is ORed, so this policy now widens access instead of narrowing it\"\n : \" — a PERMISSIVE rule is ORed with the others, a RESTRICTIVE one is ANDed, so this policy now narrows access instead of widening it\")\n );\n }\n for (const clause of [\"USING\", \"WITH CHECK\"] as const) {\n const key = clause === \"USING\" ? \"hasUsing\" : \"hasWithCheck\";\n if (want[key] === got[key]) continue;\n differences.push(want[key]\n ? `${clause}: expected an expression, database has none — this policy matches no rows`\n : `${clause}: expected none, database has an expression`);\n }\n if (differences.length > 0) drift.diverged.push({ expected: want, actual: got, differences });\n }\n\n for (const [key, got] of liveByKey) {\n if (!expectedByKey.has(key)) drift.orphaned.push(got);\n }\n\n return drift;\n}\n\n/**\n * Does this name look like one the generator produced for this table?\n *\n * Unnamed rules compile to `<table>_<op>_<sha1[0:7]>` (plus `_<idx>` when one\n * rule spans several operations), and the hash covers the rule's semantics — so\n * *editing* a rule renames its policy. The policy under the old name is left\n * behind by `db push`, which only DROPs the names it is about to CREATE, and\n * Postgres ORs PERMISSIVE policies together: a superseded `USING (true)` keeps\n * granting everything no matter how tight its replacement is.\n *\n * Matching the shape is what makes dropping them safe. A hand-written policy\n * would have to collide with a 7-hex digest to be mistaken for generated one;\n * a policy named anything else is left alone and merely reported, because a\n * custom name is indistinguishable from one someone wrote in SQL on purpose.\n */\nexport function isGeneratedPolicyName(name: string, table: string): boolean {\n return new RegExp(`^${table.replace(/[.*+?^${}()|[\\]\\\\]/g, \"\\\\$&\")}_(select|insert|update|delete|all)_[0-9a-f]{7}(_\\\\d+)?$`)\n .test(name);\n}\n\nexport interface OrphanCleanup {\n /** Superseded generated policies that were dropped. */\n dropped: PolicyRef[];\n /** Orphans left in place because their names are not generator-shaped. */\n kept: PolicyRef[];\n}\n\n/**\n * Drop the policies an earlier push superseded but never removed.\n *\n * Only touches tables the collections describe — a table with no expected\n * policy is not ours to reconcile, and scanning by schema alone would sweep up\n * policies belonging to something else sharing the database.\n */\nexport async function dropOrphanedPolicies(\n client: Queryable,\n drift: PolicyDrift,\n collections: CollectionConfig[]\n): Promise<OrphanCleanup> {\n const { dropped, kept } = partitionOrphans(drift, collections);\n for (const p of dropped) {\n // Identifiers are quoted, and the name came from pg_policies rather than\n // from user input, so it is already a valid identifier.\n await client.query(`DROP POLICY IF EXISTS \"${p.name}\" ON \"${p.schema}\".\"${p.table}\"`);\n }\n return { dropped, kept };\n}\n\n/**\n * Which orphans {@link dropOrphanedPolicies} drops and which it keeps, decided\n * without dropping anything: what `db push --dry-run` lists.\n */\nexport function partitionOrphans(drift: PolicyDrift, collections: CollectionConfig[]): OrphanCleanup {\n const expected = parseExpectedPolicies(generatePostgresPoliciesDdl(collections));\n const managed = new Set(expected.map((p) => `${p.schema}.${p.table}`));\n\n const cleanup: OrphanCleanup = { dropped: [], kept: [] };\n for (const p of drift.orphaned) {\n if (managed.has(`${p.schema}.${p.table}`) && isGeneratedPolicyName(p.name, p.table)) cleanup.dropped.push(p);\n else cleanup.kept.push(p);\n }\n return cleanup;\n}\n\nexport const hasDrift = (d: PolicyDrift): boolean =>\n d.missing.length > 0 || d.orphaned.length > 0 || d.diverged.length > 0 || d.insecure.length > 0\n || d.rlsDisabled.length > 0;\n\n/** Human-readable report; empty string when the database matches the config. */\nexport function formatPolicyDrift(drift: PolicyDrift): string {\n if (!hasDrift(drift)) return \"\";\n const lines: string[] = [];\n\n // First, because it subsumes every other finding on the same table: if RLS\n // is off, the policies listed below are not being applied at all.\n if (drift.rlsDisabled.length > 0) {\n lines.push(\" RLS DISABLED — the table has policies, and Postgres is applying none of them:\");\n for (const t of drift.rlsDisabled) {\n lines.push(` • ${t.schema}.${t.table}${t.forced ? \"\" : \" (and FORCE is off)\"}`);\n }\n lines.push(\" Every row is readable and writable by any request that reaches the table.\");\n lines.push(` Fix: ALTER TABLE \"<schema>\".\"<table>\" ENABLE ROW LEVEL SECURITY; or re-run \\`rebase db push\\`.`);\n }\n if (drift.missing.length > 0) {\n lines.push(\" Missing — described by your collections, absent from the database:\");\n for (const p of drift.missing) lines.push(` • ${p.schema}.${p.table} → \"${p.name}\" (${p.command} TO ${p.roles.join(\", \")})`);\n lines.push(\" Run `rebase db push` to apply them.\");\n }\n if (drift.orphaned.length > 0) {\n // Two different things share this list, and they want opposite\n // advice. A name of the shape an unnamed rule compiles to is a rule\n // edited since: the next push drops it. Any other name was written by\n // hand or by another tool, and may be the table's whole access model —\n // \"left behind by an earlier push\" was false of it, and telling its\n // author to drop it would take that access away.\n const superseded = drift.orphaned.filter((p) => isGeneratedPolicyName(p.name, p.table));\n const unowned = drift.orphaned.filter((p) => !isGeneratedPolicyName(p.name, p.table));\n lines.push(\" Orphaned — in the database, described by no collection:\");\n for (const p of drift.orphaned) lines.push(` • ${p.schema}.${p.table} → \"${p.name}\" (${p.command} TO ${p.roles.join(\", \")})`);\n if (superseded.length > 0) {\n lines.push(` ${superseded.map((p) => `\"${p.name}\"`).join(\", \")}: left behind by an earlier push, when its rule changed. The next \\`rebase db push\\` drops it.`);\n }\n if (unowned.length > 0) {\n lines.push(` ${unowned.map((p) => `\"${p.name}\"`).join(\", \")}: written by hand or by another tool, and still in force.`);\n lines.push(\" To keep one, describe it in its collection's `securityRules` (Studio's RLS view imports it);\");\n lines.push(\" if it is stale, drop it by hand.\");\n }\n }\n if (drift.diverged.length > 0) {\n lines.push(\" Diverged — same policy, different definition:\");\n for (const d of drift.diverged) {\n lines.push(` • ${d.expected.schema}.${d.expected.table} → \"${d.expected.name}\"`);\n for (const diff of d.differences) lines.push(` ${diff}`);\n }\n }\n if (drift.insecure.length > 0) {\n lines.push(\" Insecure — a live policy grants access it should not:\");\n for (const i of drift.insecure) {\n lines.push(` • ${i.policy.schema}.${i.policy.table} → \"${i.policy.name}\"`);\n lines.push(` ${i.reason}`);\n }\n }\n return lines.join(\"\\n\");\n}\n"],"mappings":"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"}
|
|
@@ -0,0 +1,28 @@
|
|
|
1
|
+
/**
|
|
2
|
+
* What a pool says when the server ends its idle connections.
|
|
3
|
+
*
|
|
4
|
+
* `pg_terminate_backend` ends every connection to a database at once, and an
|
|
5
|
+
* idle pool client hears it as an `error` event: one per connection, from
|
|
6
|
+
* every pool the process holds. `rebase db branch create --force` does exactly
|
|
7
|
+
* that to the database it copies, so fifteen forced creates beside a running
|
|
8
|
+
* `rebase dev` printed 220 `❌ [ERROR] [pg-pool] Unexpected pool error
|
|
9
|
+
* {"detail":"terminating connection due to administrator command"}` lines. It
|
|
10
|
+
* read as an outage, and nothing was wrong: the pool opens new connections on
|
|
11
|
+
* the next query. A database restart looks the same.
|
|
12
|
+
*/
|
|
13
|
+
import { logger } from "@rebasepro/server";
|
|
14
|
+
/** Did the server end this connection, rather than something go wrong with it? */
|
|
15
|
+
export declare function serverEndedConnection(err: Error): boolean;
|
|
16
|
+
/** How long one line stands for every server-ended connection after it, across all pools. */
|
|
17
|
+
export declare const DISCONNECT_BURST_MS = 10000;
|
|
18
|
+
/** The logger methods a pool's error listener writes through. */
|
|
19
|
+
export type PoolErrorLog = Pick<typeof logger, "warn" | "error" | "debug">;
|
|
20
|
+
/**
|
|
21
|
+
* Say a pool's `error` event: a connection the server ended is one WARN line
|
|
22
|
+
* per burst for the whole process, and the rest of the burst is at debug;
|
|
23
|
+
* anything else is the ERROR it always was.
|
|
24
|
+
*
|
|
25
|
+
* The window is fixed from the first disconnect, not extended by each, so a
|
|
26
|
+
* second burst ten seconds later is said again.
|
|
27
|
+
*/
|
|
28
|
+
export declare function reportPoolError(label: string, err: Error, log?: PoolErrorLog, now?: () => number): void;
|
|
@@ -1,8 +1,8 @@
|
|
|
1
1
|
import { createRequire as __createRequire } from "module";
|
|
2
2
|
__createRequire(import.meta.url);
|
|
3
|
-
import { n as planSchema } from "./plan-schema-
|
|
4
|
-
import { s as diffPlanAgainstCatalogue } from "./deferred-index-build-
|
|
5
|
-
import { i as readExistingSchema } from "./ensure-collection-tables-
|
|
3
|
+
import { n as planSchema } from "./plan-schema-Czf3q4-Q.js";
|
|
4
|
+
import { s as diffPlanAgainstCatalogue } from "./deferred-index-build-BE0I6sYU.js";
|
|
5
|
+
import { i as readExistingSchema } from "./ensure-collection-tables-Cet4650m.js";
|
|
6
6
|
import { declaredDatabaseExtensions } from "@rebasepro/types";
|
|
7
7
|
//#region src/schema/push-index-prebuild.ts
|
|
8
8
|
/**
|
|
@@ -42,4 +42,4 @@ async function missingForeignKeyIndexes(client, collections) {
|
|
|
42
42
|
//#endregion
|
|
43
43
|
export { missingForeignKeyIndexes };
|
|
44
44
|
|
|
45
|
-
//# sourceMappingURL=push-index-prebuild-
|
|
45
|
+
//# sourceMappingURL=push-index-prebuild-COd7i6wm.js.map
|
|
@@ -1 +1 @@
|
|
|
1
|
-
{"version":3,"file":"push-index-prebuild-
|
|
1
|
+
{"version":3,"file":"push-index-prebuild-COd7i6wm.js","names":[],"sources":["../src/schema/push-index-prebuild.ts"],"sourcesContent":["/**\n * The foreign-key indexes `rebase db push` builds before Atlas plans.\n *\n * Rebase derives a btree for every `belongsTo` key and every junction's target\n * column (15b00afcd). On a table that already exists — every table of a\n * project upgraded across that release — the index is missing, and a push left\n * it to Atlas, which builds it inside the push's one transaction: a plain\n * `CREATE INDEX`, holding writes to the table for the whole build, about 0.4 s\n * at a million rows and 6 s at ten million. Atlas's own concurrent mode would\n * run the entire apply outside a transaction, which the push relies on.\n *\n * So the push builds those first, `CONCURRENTLY`, with the builder the boot\n * uses for the same indexes (`deferred-index-build.ts`: one at a time, a lock\n * budget for getting the table, an INVALID leftover dropped and rebuilt, a\n * claim shared with booting instances). Atlas then finds them and plans\n * nothing for them. One it could not build is still Atlas's, as before.\n *\n * Only an index whose columns are already there: a column this push adds is\n * created by Atlas, and its index with it, on a column that holds no values.\n */\nimport { declaredDatabaseExtensions, type CollectionConfig } from \"@rebasepro/types\";\nimport { planSchema } from \"./plan/plan-schema\";\nimport { diffPlanAgainstCatalogue, type DeferredIndexBuild } from \"./plan/diff-plan\";\nimport { readExistingSchema, type Queryable } from \"./ensure-collection-tables\";\n\n/** The derived foreign-key indexes a live table lacks (or holds INVALID) whose columns it already has. */\nexport async function missingForeignKeyIndexes(\n client: Queryable,\n collections: CollectionConfig[]\n): Promise<DeferredIndexBuild[]> {\n const schema = planSchema(collections, { databaseExtensions: declaredDatabaseExtensions() });\n const schemas = Array.from(new Set(schema.tables.map(table => table.schema)));\n // No row probes: whether a table holds rows decides only constraints on a\n // column being added, which this does not plan.\n const existing = await readExistingSchema(client, schemas, { probeTables: () => [] });\n const plan = diffPlanAgainstCatalogue(schema, existing);\n\n const specs = new Map(schema.tables.flatMap(table => table.indexes)\n .map(spec => [`${spec.schema}.${spec.indexName}`, spec]));\n return plan.deferredIndexes.filter(build => {\n const spec = specs.get(`${build.schema}.${build.indexName}`);\n if (spec?.derived !== \"foreign-key\") return false;\n const columns = existing.tables.get(`${build.schema}.${build.table}`);\n if (!columns) return false;\n return [...spec.keys.map(key => key.column), ...spec.include].every(column => columns.has(column));\n });\n}\n"],"mappings":";;;;;;;;;;;;;;;;;;;;;;;;;;;;AA0BA,eAAsB,yBAClB,QACA,aAC6B;CAC7B,MAAM,SAAS,WAAW,aAAa,EAAE,oBAAoB,2BAA2B,EAAE,CAAC;CAI3F,MAAM,WAAW,MAAM,mBAAmB,QAH1B,MAAM,KAAK,IAAI,IAAI,OAAO,OAAO,KAAI,UAAS,MAAM,MAAM,CAAC,CAGzB,GAAS,EAAE,mBAAmB,CAAC,EAAE,CAAC;CACpF,MAAM,OAAO,yBAAyB,QAAQ,QAAQ;CAEtD,MAAM,QAAQ,IAAI,IAAI,OAAO,OAAO,SAAQ,UAAS,MAAM,OAAO,CAAC,CAC9D,KAAI,SAAQ,CAAC,GAAG,KAAK,OAAO,GAAG,KAAK,aAAa,IAAI,CAAC,CAAC;CAC5D,OAAO,KAAK,gBAAgB,QAAO,UAAS;EACxC,MAAM,OAAO,MAAM,IAAI,GAAG,MAAM,OAAO,GAAG,MAAM,WAAW;EAC3D,IAAI,MAAM,YAAY,eAAe,OAAO;EAC5C,MAAM,UAAU,SAAS,OAAO,IAAI,GAAG,MAAM,OAAO,GAAG,MAAM,OAAO;EACpE,IAAI,CAAC,SAAS,OAAO;EACrB,OAAO,CAAC,GAAG,KAAK,KAAK,KAAI,QAAO,IAAI,MAAM,GAAG,GAAG,KAAK,OAAO,CAAC,CAAC,OAAM,WAAU,QAAQ,IAAI,MAAM,CAAC;CACrG,CAAC;AACL"}
|
|
@@ -1,8 +1,8 @@
|
|
|
1
1
|
import { createRequire as __createRequire } from "module";
|
|
2
2
|
__createRequire(import.meta.url);
|
|
3
|
-
import { n as planSchema } from "./plan-schema-
|
|
4
|
-
import { s as diffPlanAgainstCatalogue } from "./deferred-index-build-
|
|
5
|
-
import { i as readExistingSchema } from "./ensure-collection-tables-
|
|
3
|
+
import { n as planSchema } from "./plan-schema-Czf3q4-Q.js";
|
|
4
|
+
import { s as diffPlanAgainstCatalogue } from "./deferred-index-build-BE0I6sYU.js";
|
|
5
|
+
import { i as readExistingSchema } from "./ensure-collection-tables-Cet4650m.js";
|
|
6
6
|
import { declaredDatabaseExtensions } from "@rebasepro/types";
|
|
7
7
|
//#region src/schema/push-legacy-renames.ts
|
|
8
8
|
/**
|
|
@@ -31,4 +31,4 @@ async function legacyForeignKeyRenames(client, collections) {
|
|
|
31
31
|
//#endregion
|
|
32
32
|
export { legacyForeignKeyRenames };
|
|
33
33
|
|
|
34
|
-
//# sourceMappingURL=push-legacy-renames-
|
|
34
|
+
//# sourceMappingURL=push-legacy-renames-CLgYYwPE.js.map
|
|
@@ -1 +1 @@
|
|
|
1
|
-
{"version":3,"file":"push-legacy-renames-
|
|
1
|
+
{"version":3,"file":"push-legacy-renames-CLgYYwPE.js","names":[],"sources":["../src/schema/push-legacy-renames.ts"],"sourcesContent":["/**\n * The relation columns an aged database still holds under the name Rebase\n * derived before 0.13 (`categorie_id`, `people_id`), for `db push` to rename\n * before Atlas plans.\n *\n * Boot renames them losslessly (`diff-plan.ts`, `renameLegacyColumn`). Push\n * runs Atlas, which sees only \"drop `categorie_id`, add `category_id` NOT\n * NULL\": it listed the links as destructive changes to approve, and with\n * `--allow-destructive` dropped them. A pipeline that runs only `db push`\n * could not upgrade an aged database at all. Renaming first — the same\n * metadata-only statement boot runs — leaves Atlas nothing to plan.\n */\nimport { declaredDatabaseExtensions, type CollectionConfig } from \"@rebasepro/types\";\nimport { planSchema } from \"./plan/plan-schema\";\nimport { diffPlanAgainstCatalogue, type LegacyForeignKey } from \"./plan/diff-plan\";\nimport { readExistingSchema, type Queryable } from \"./ensure-collection-tables\";\n\nexport interface LegacyRename extends LegacyForeignKey {\n /** `ALTER TABLE … RENAME COLUMN …;` */\n sql: string;\n}\n\nexport async function legacyForeignKeyRenames(client: Queryable, collections: CollectionConfig[]): Promise<LegacyRename[]> {\n const schema = planSchema(collections, { databaseExtensions: declaredDatabaseExtensions() });\n const schemas = Array.from(new Set(schema.tables.map(table => table.schema)));\n const existing = await readExistingSchema(client, schemas, { probeTables: () => [] });\n const plan = diffPlanAgainstCatalogue(schema, existing);\n return plan.legacyForeignKeys.map(legacy => {\n const action = plan.actions.find(a => a.kind === \"rename-column\" && a.target === `${legacy.table}.${legacy.expected}`);\n return { ...legacy, sql: action?.sql ?? \"\" };\n }).filter(rename => rename.sql !== \"\");\n}\n"],"mappings":";;;;;;;;;;;;;;;;;;;AAsBA,eAAsB,wBAAwB,QAAmB,aAA0D;CACvH,MAAM,SAAS,WAAW,aAAa,EAAE,oBAAoB,2BAA2B,EAAE,CAAC;CAG3F,MAAM,OAAO,yBAAyB,QAAQ,MADvB,mBAAmB,QAD1B,MAAM,KAAK,IAAI,IAAI,OAAO,OAAO,KAAI,UAAS,MAAM,MAAM,CAAC,CACzB,GAAS,EAAE,mBAAmB,CAAC,EAAE,CAAC,CAC9B;CACtD,OAAO,KAAK,kBAAkB,KAAI,WAAU;EACxC,MAAM,SAAS,KAAK,QAAQ,MAAK,MAAK,EAAE,SAAS,mBAAmB,EAAE,WAAW,GAAG,OAAO,MAAM,GAAG,OAAO,UAAU;EACrH,OAAO;GAAE,GAAG;GAAQ,KAAK,QAAQ,OAAO;EAAG;CAC/C,CAAC,CAAC,CAAC,QAAO,WAAU,OAAO,QAAQ,EAAE;AACzC"}
|
|
@@ -1,12 +1,14 @@
|
|
|
1
1
|
import { createRequire as __createRequire } from "module";
|
|
2
2
|
__createRequire(import.meta.url);
|
|
3
|
-
import { n as generatePostgresPoliciesDdl } from "./generate-postgres-ddl-logic-
|
|
4
|
-
import { a as parseExpectedPolicies, o as partitionOrphans, t as checkPolicyDrift } from "./policy-drift-
|
|
5
|
-
import { readPolicyStamps, tablesRlsWouldEnable } from "./ensure-collection-policies-
|
|
3
|
+
import { n as generatePostgresPoliciesDdl } from "./generate-postgres-ddl-logic-DPl1oH_z.js";
|
|
4
|
+
import { a as parseExpectedPolicies, o as partitionOrphans, t as checkPolicyDrift } from "./policy-drift-X4A-xAzv.js";
|
|
5
|
+
import { probeRenderedPolicies, readPolicyStamps, tablesRlsWouldEnable } from "./ensure-collection-policies-BnGOURpG.js";
|
|
6
6
|
//#region src/schema/push-policy-plan.ts
|
|
7
7
|
var keyOf = (p) => `${p.schema}.${p.table}.${p.name}`;
|
|
8
8
|
/** What a stamp that no longer matches says. */
|
|
9
9
|
var STALE = "its rule, or the policy itself, has changed since Rebase wrote it";
|
|
10
|
+
/** What a policy with no stamp, whose rule stores something else, says. */
|
|
11
|
+
var UNSTAMPED_DIFFERS = "it carries no stamp of Rebase's, and its rule defines it otherwise";
|
|
10
12
|
/**
|
|
11
13
|
* Read the plan.
|
|
12
14
|
*
|
|
@@ -24,15 +26,29 @@ async function planPolicyPush(client, collections, options = {}) {
|
|
|
24
26
|
why: d.differences
|
|
25
27
|
}));
|
|
26
28
|
const settled = new Set([...drift.missing, ...drift.diverged.map((d) => d.expected)].map(keyOf));
|
|
27
|
-
const
|
|
29
|
+
const unsure = [];
|
|
28
30
|
for (const policy of parseExpectedPolicies(generatePostgresPoliciesDdl(collections))) {
|
|
29
31
|
if (settled.has(keyOf(policy))) continue;
|
|
30
32
|
const state = stamps.get(`${policy.schema}.${policy.table}`)?.get(policy.name);
|
|
33
|
+
if (state === "stale" || state === "unstamped") unsure.push({
|
|
34
|
+
policy,
|
|
35
|
+
state
|
|
36
|
+
});
|
|
37
|
+
}
|
|
38
|
+
const rendersAsStored = unsure.length === 0 ? /* @__PURE__ */ new Map() : await probeRenderedPolicies(client, collections, unsure.map((entry) => entry.policy), { searchPath: options.searchPath });
|
|
39
|
+
const unstamped = [];
|
|
40
|
+
for (const { policy, state } of unsure) {
|
|
41
|
+
const same = rendersAsStored.get(keyOf(policy));
|
|
42
|
+
if (same === true) continue;
|
|
31
43
|
if (state === "stale") replace.push({
|
|
32
44
|
policy,
|
|
33
45
|
why: [STALE]
|
|
34
46
|
});
|
|
35
|
-
else if (
|
|
47
|
+
else if (same === false) replace.push({
|
|
48
|
+
policy,
|
|
49
|
+
why: [UNSTAMPED_DIFFERS]
|
|
50
|
+
});
|
|
51
|
+
else unstamped.push(policy);
|
|
36
52
|
}
|
|
37
53
|
return {
|
|
38
54
|
enableRls,
|
|
@@ -106,6 +122,6 @@ async function underSearchPath(client, searchPath, read) {
|
|
|
106
122
|
}
|
|
107
123
|
}
|
|
108
124
|
//#endregion
|
|
109
|
-
export { formatPolicyPushPlan, planPolicyPush, summarizePolicyPushPlan };
|
|
125
|
+
export { formatPolicyPushPlan, planPolicyPush, summarizePolicyPushPlan, underSearchPath };
|
|
110
126
|
|
|
111
|
-
//# sourceMappingURL=push-policy-plan-
|
|
127
|
+
//# sourceMappingURL=push-policy-plan-D2gpQsdv.js.map
|
|
@@ -0,0 +1 @@
|
|
|
1
|
+
{"version":3,"file":"push-policy-plan-D2gpQsdv.js","names":[],"sources":["../src/schema/push-policy-plan.ts"],"sourcesContent":["/**\n * What `rebase db push` will do to row-level security, read before it does it.\n *\n * Atlas has no model for a policy: its free tier neither plans nor reads one.\n * So its plan, which was everything `db push --dry-run` showed, never named\n * one. After `schema introspect` on a database whose tables had policies of\n * their own and no admin baseline, the dry run said \"No changes: the database\n * already matches these collections\", and the push then created eight\n * `<table>_default_admin_*` policies that let an admin read and write every\n * row.\n *\n * The plan is the push's own steps, read instead of run:\n *\n * - **RLS switched on:** the tables `lockCollectionTables` switches on\n * ({@link tablesRlsWouldEnable}).\n * - **Policies created and replaced:** `policies.sql`, which DROPs and CREATEs\n * every declared policy, against the database as `checkPolicyDrift` reads\n * it: by name, `TO` list, command, mode and which clauses there are. Text\n * is compared through the stamp the push and every boot write\n * ({@link readPolicyStamps}), which is what catches a rule whose SQL changed\n * under the same name. Where the stamp does not match, or there is none (a\n * policy `db migrate` or a hand made), the policy is made again under a probe\n * name and rolled back ({@link probeRenderedPolicies}): one Postgres would\n * store exactly as it is now is no change. Only one that could not be probed\n * is listed as unstamped.\n * - **Policies dropped and kept:** {@link partitionOrphans}, which is how the\n * push's reconcile step decides.\n */\nimport type { CollectionConfig } from \"@rebasepro/types\";\nimport {\n checkPolicyDrift,\n parseExpectedPolicies,\n partitionOrphans,\n type PolicyRef,\n type Queryable\n} from \"../security/policy-drift\";\nimport { generatePostgresPoliciesDdl } from \"./generate-postgres-ddl-logic\";\nimport { probeRenderedPolicies, readPolicyStamps, tablesRlsWouldEnable } from \"./ensure-collection-policies\";\n\nexport interface PolicyPushPlan {\n /** Declared tables in the database with row-level security off, as `schema.table`: the push switches it on. */\n enableRls: string[];\n /** Declared policies the database does not have: the push creates them. */\n create: PolicyRef[];\n /** Declared policies the database has under the same name, defined otherwise: the push replaces them. */\n replace: { policy: PolicyRef; why: string[] }[];\n /** Policies an earlier push generated from a rule edited since: the push drops them. */\n drop: PolicyRef[];\n /** Policies no collection describes, which the push leaves in force. */\n keep: PolicyRef[];\n /** Declared policies the database has with no stamp of Rebase's: re-created from their rules, changed or not. */\n unstamped: PolicyRef[];\n}\n\nconst keyOf = (p: PolicyRef): string => `${p.schema}.${p.table}.${p.name}`;\n\n/** What a stamp that no longer matches says. */\nconst STALE = \"its rule, or the policy itself, has changed since Rebase wrote it\";\n\n/** What a policy with no stamp, whose rule stores something else, says. */\nconst UNSTAMPED_DIFFERS = \"it carries no stamp of Rebase's, and its rule defines it otherwise\";\n\n/**\n * Read the plan.\n *\n * @param options.searchPath the `search_path` the stamps were written under\n * when the connection's own differs — the one `db push` stamps under.\n * See {@link readPolicyStamps}.\n */\nexport async function planPolicyPush(\n client: Queryable,\n collections: CollectionConfig[],\n options: { searchPath?: string } = {}\n): Promise<PolicyPushPlan> {\n const drift = await checkPolicyDrift(client, collections);\n const { dropped, kept } = partitionOrphans(drift, collections);\n const enableRls = await tablesRlsWouldEnable(client, collections);\n const stamps = await underSearchPath(client, options.searchPath, () => readPolicyStamps(client, collections));\n\n const replace = drift.diverged.map(d => ({ policy: d.expected, why: d.differences }));\n const settled = new Set([...drift.missing, ...drift.diverged.map(d => d.expected)].map(keyOf));\n const unsure: { policy: PolicyRef; state: \"stale\" | \"unstamped\" }[] = [];\n for (const policy of parseExpectedPolicies(generatePostgresPoliciesDdl(collections))) {\n if (settled.has(keyOf(policy))) continue;\n const state = stamps.get(`${policy.schema}.${policy.table}`)?.get(policy.name);\n if (state === \"stale\" || state === \"unstamped\") unsure.push({ policy, state });\n }\n // What the stamp cannot say, asked of Postgres: would the push store the\n // definition that is there? A policy it would re-create exactly is no change.\n const rendersAsStored = unsure.length === 0\n ? new Map<string, boolean>()\n : await probeRenderedPolicies(client, collections, unsure.map(entry => entry.policy), { searchPath: options.searchPath });\n const unstamped: PolicyRef[] = [];\n for (const { policy, state } of unsure) {\n const same = rendersAsStored.get(keyOf(policy));\n if (same === true) continue;\n if (state === \"stale\") replace.push({ policy, why: [STALE] });\n else if (same === false) replace.push({ policy, why: [UNSTAMPED_DIFFERS] });\n else unstamped.push(policy);\n }\n\n return { enableRls, create: drift.missing, replace, drop: dropped, keep: kept, unstamped };\n}\n\n/** Whether the push would change any row-level security. Kept and unstamped policies are not changes. */\nexport function policyPlanChanges(plan: PolicyPushPlan): boolean {\n return plan.enableRls.length + plan.create.length + plan.replace.length + plan.drop.length > 0;\n}\n\nconst describe = (p: PolicyRef): string => `${p.schema}.${p.table} → \"${p.name}\" (${p.command} TO ${p.roles.join(\", \")})`;\n\n/**\n * The plan as `db push --dry-run` prints it: `changes`, what the push does,\n * and `notes`, what it leaves or cannot tell. Both empty when there is\n * nothing to say.\n */\nexport function formatPolicyPushPlan(plan: PolicyPushPlan): { changes: string[]; notes: string[] } {\n const changes: string[] = [];\n if (policyPlanChanges(plan)) {\n changes.push(\" Planned row-level security changes:\");\n for (const table of plan.enableRls) changes.push(` switch RLS on ${table}`);\n for (const p of plan.create) changes.push(` create policy ${describe(p)}`);\n for (const { policy, why } of plan.replace) changes.push(` replace policy ${describe(policy)}: ${why.join(\"; \")}`);\n for (const p of plan.drop) changes.push(` drop policy ${describe(p)}, left by a rule edited since`);\n }\n\n const notes: string[] = [];\n if (plan.unstamped.length > 0) {\n notes.push(` ${plan.unstamped.length} declared ${plan.unstamped.length === 1 ? \"policy carries\" : \"policies carry\"} ` +\n \"no stamp of Rebase's, so the push re-creates each from its rule\");\n notes.push(\" and cannot tell beforehand whether that changes it:\");\n for (const p of plan.unstamped) notes.push(` ${describe(p)}`);\n }\n if (plan.keep.length > 0) {\n notes.push(\" Policies in the database that no collection describes, which the push leaves in force:\");\n for (const p of plan.keep) notes.push(` ${describe(p)}`);\n }\n return { changes, notes };\n}\n\n/** One line for the push itself: the counts, or `null` when it changes no row-level security. */\nexport function summarizePolicyPushPlan(plan: PolicyPushPlan): string | null {\n if (!policyPlanChanges(plan)) return null;\n const count = (n: number, one: string, many: string): string => `${n} ${n === 1 ? one : many}`;\n const parts = [\n plan.create.length > 0 ? count(plan.create.length, \"policy to create\", \"policies to create\") : \"\",\n plan.replace.length > 0 ? `${plan.replace.length} to replace` : \"\",\n plan.drop.length > 0 ? `${plan.drop.length} to drop` : \"\",\n plan.enableRls.length > 0 ? `RLS to switch on for ${count(plan.enableRls.length, \"table\", \"tables\")}` : \"\"\n ].filter(Boolean);\n return ` Row-level security: ${parts.join(\", \")}. \\`rebase db push --dry-run\\` lists them.`;\n}\n\n/**\n * Run `read` under `searchPath`, set for one transaction and gone after it, or\n * as the connection is when there is none to set.\n */\nexport async function underSearchPath<T>(client: Queryable, searchPath: string | undefined, read: () => Promise<T>): Promise<T> {\n if (!searchPath) return read();\n await client.query(\"BEGIN\");\n try {\n await client.query(`SET LOCAL search_path TO ${searchPath}`);\n const result = await read();\n await client.query(\"COMMIT\");\n return result;\n } catch (err) {\n await client.query(\"ROLLBACK\").catch(() => undefined);\n throw err;\n }\n}\n"],"mappings":"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"}
|
|
@@ -1,9 +1,9 @@
|
|
|
1
1
|
import { createRequire as __createRequire } from "module";
|
|
2
2
|
__createRequire(import.meta.url);
|
|
3
3
|
import { c as searchColumnDefinition, f as searchExtensionStatements, g as searchStampGuards, h as searchIndexStatements, m as searchIndexNames, o as fuzzyColumnDefinition, p as searchHelperFunctions, u as searchColumnStamps } from "./search-column-BM-GV6vH.js";
|
|
4
|
-
import { a as vectorDimensionGuard, c as vectorExtensionStatement, d as vectorIndexStatements, i as vectorColumnDefinition, l as vectorIndexNames, o as vectorExtensionDeclared, t as VECTOR_EXTENSION_OPT_IN } from "./vector-index-
|
|
5
|
-
import { o as atlasExcludePattern, u as collectionIndexStatements } from "./source-
|
|
6
|
-
import { a as triggerStatements, r as quoteSqlLiteral } from "./plan-schema-
|
|
4
|
+
import { a as vectorDimensionGuard, c as vectorExtensionStatement, d as vectorIndexStatements, i as vectorColumnDefinition, l as vectorIndexNames, o as vectorExtensionDeclared, t as VECTOR_EXTENSION_OPT_IN } from "./vector-index-DsQ3V2d2.js";
|
|
5
|
+
import { o as atlasExcludePattern, u as collectionIndexStatements } from "./source-fZb92pN8.js";
|
|
6
|
+
import { a as triggerStatements, r as quoteSqlLiteral } from "./plan-schema-Czf3q4-Q.js";
|
|
7
7
|
//#region src/schema/plan/render-ddl.ts
|
|
8
8
|
/**
|
|
9
9
|
* The SQL files, rendered from a {@link SchemaPlan}.
|
|
@@ -339,4 +339,4 @@ var triggerExcludePatternsOf = (plan) => plan.tables.flatMap((table) => table.tr
|
|
|
339
339
|
//#endregion
|
|
340
340
|
export { renderPolicyStatements as a, renderSearchDdl as c, searchExcludePatternsOf as d, triggerExcludePatternsOf as f, renderPoliciesDdl as i, renderTriggersDdl as l, renderEnableRls as n, renderPostgresDdl as o, vectorExcludePatternsOf as p, renderPgType as r, renderPrimaryKeyConstraint as s, renderColumnDefinition as t, renderVectorDdl as u };
|
|
341
341
|
|
|
342
|
-
//# sourceMappingURL=render-ddl-
|
|
342
|
+
//# sourceMappingURL=render-ddl-4v88y-Gv.js.map
|
|
@@ -1 +1 @@
|
|
|
1
|
-
{"version":3,"file":"render-ddl-CrfEe__B.js","names":[],"sources":["../src/schema/plan/render-ddl.ts"],"sourcesContent":["/**\n * The SQL files, rendered from a {@link SchemaPlan}.\n *\n * Four files, because Atlas owns exactly one of them:\n *\n * - `schema.sql` — the desired state Atlas diffs the database against.\n * - `policies.sql` — the RLS policies. Atlas has no model for a policy.\n * - `search.sql` — the generated `tsvector` column, its GIN index and the\n * IMMUTABLE helpers its expression calls. Atlas's free tier refuses to\n * *parse* a file containing a function, and it wipes the dev database it\n * diffs against, so a helper seeded there is gone by the time the plan runs.\n * - `vector.sql` — the `vector` columns and their ANN indexes. Atlas cannot\n * *execute* these: it materialises `schema.sql` in a dev database that is\n * created empty and emptied again on every run, so `VECTOR(384)` resolves\n * against a database that structurally cannot have pgvector.\n * - `triggers.sql` — `autoValue: \"on_update\"`, which is a function plus a\n * binding, so it is out for the same reason search is.\n *\n * Everything not in `schema.sql` is excluded from Atlas's view by pattern\n * (`…ExcludePatterns` below) and applied by Rebase — at push time by the CLI,\n * at boot by the schema ensure. Without the excludes a desired state that omits\n * an object reads to Atlas as an instruction to drop it.\n */\nimport {\n fuzzyColumnDefinition,\n searchColumnDefinition,\n searchColumnStamps,\n searchExtensionStatements,\n searchHelperFunctions,\n searchIndexNames,\n searchIndexStatements,\n searchStampGuards\n} from \"../search-column\";\nimport {\n VECTOR_EXTENSION_OPT_IN,\n vectorColumnDefinition,\n vectorDimensionGuard,\n vectorExtensionDeclared,\n vectorExtensionStatement,\n vectorIndexNames,\n vectorIndexStatements\n} from \"../vector-index\";\nimport { atlasExcludePattern } from \"../atlas-exclude\";\nimport { collectionIndexStatements } from \"../collection-index\";\nimport { quoteSqlLiteral } from \"./plan-schema\";\nimport { triggerStatements } from \"./updated-at-trigger\";\nimport type { ColumnPlan, PgType, PolicyPlan, SchemaPlan, TablePlan } from \"./types\";\n\n/** What `schema.sql` may leave out, because another file owns it. */\nexport interface DdlRenderOptions {\n /** RLS policies. `false` puts them in `policies.sql` alone. */\n includePolicies?: boolean;\n /** The search column, its helpers and its index. See `search.sql`. */\n includeSearch?: boolean;\n /** The vector columns and their ANN indexes. See `vector.sql`. */\n includeVector?: boolean;\n}\n\n/**\n * A {@link PgType} as Postgres spells it.\n *\n * The one place. `getSqlColumnType` and `getDrizzleColumn` each carried their\n * own ladder and disagreed about `VARCHAR` widths, about `geopoint`, and about\n * whether an `increment` id honours `columnType`.\n */\nexport const renderPgType = (type: PgType): string => {\n switch (type.kind) {\n case \"text\": return \"TEXT\";\n case \"varchar\": return `VARCHAR(${type.length})`;\n case \"char\": return `CHAR(${type.length})`;\n case \"uuid\": return \"UUID\";\n case \"enum\": return `\"${type.schema}\".\"${type.name}\"`;\n case \"smallint\": return \"SMALLINT\";\n case \"integer\": return \"INTEGER\";\n case \"bigint\": return \"BIGINT\";\n case \"smallserial\": return \"SMALLSERIAL\";\n case \"serial\": return \"SERIAL\";\n case \"bigserial\": return \"BIGSERIAL\";\n case \"real\": return \"REAL\";\n case \"doublePrecision\": return \"DOUBLE PRECISION\";\n case \"numeric\":\n if (type.precision === undefined) return \"NUMERIC\";\n return type.scale === undefined\n ? `NUMERIC(${type.precision})`\n : `NUMERIC(${type.precision}, ${type.scale})`;\n case \"boolean\": return \"BOOLEAN\";\n case \"timestamptz\": return \"TIMESTAMP WITH TIME ZONE\";\n case \"date\": return \"DATE\";\n case \"time\": return \"TIME\";\n case \"json\": return \"JSON\";\n case \"jsonb\": return \"JSONB\";\n case \"vector\": return `VECTOR(${type.dimensions})`;\n case \"bytea\": return \"BYTEA\";\n case \"tsvector\": return \"tsvector\";\n case \"array\": return `${renderPgType(type.of)}[]`;\n }\n};\n\n/**\n * Everything after the column name, in one order for every SQL emitter.\n *\n * `CREATE TABLE` and `ADD COLUMN` used to assemble this separately, which is\n * how a required relation column came out `NOT NULL` after `db push` and\n * nullable after a boot-ensure. `identity` is part of the type rather than a\n * DEFAULT, which is why it is rendered here and not in the DEFAULT slot.\n */\nexport const renderColumnDefinition = (column: ColumnPlan): string => {\n // `auth-users-columns` is the single description of what an auth user\n // table's columns must be, and it hands over a rendered definition.\n if (column.sqlDefinition) return column.sqlDefinition;\n\n if (column.generated) {\n return `${renderPgType(column.type)} GENERATED ALWAYS AS (${column.generated.expression}) STORED`;\n }\n\n let definition = renderPgType(column.type);\n if (column.default?.kind === \"identity\") definition += \" GENERATED BY DEFAULT AS IDENTITY\";\n if (column.primaryKey) definition += \" PRIMARY KEY\";\n if (column.unique) definition += \" UNIQUE\";\n if (column.default && column.default.kind !== \"identity\") {\n definition += ` DEFAULT ${column.default.kind === \"sql\" ? column.default.expression : column.default.sql}`;\n }\n if (!column.nullable && !column.primaryKey) definition += \" NOT NULL\";\n return definition;\n};\n\n/**\n * `PRIMARY KEY (\"a\", \"b\")`, the clause a composite key takes inside a\n * `CREATE TABLE`. Shared by `schema.sql` and boot-ensure's `CREATE TABLE`, so\n * the two cannot list the columns differently.\n */\nexport const renderPrimaryKeyConstraint = (table: Pick<TablePlan, \"primaryKey\">): string =>\n `PRIMARY KEY (${table.primaryKey.map(c => `\"${c}\"`).join(\", \")})`;\n\n/** `\"col\" TYPE …`, as it appears inside a `CREATE TABLE`. */\nconst renderColumn = (column: ColumnPlan): string =>\n `\"${column.column}\" ${renderColumnDefinition(column)}`;\n\n// ── Policies ─────────────────────────────────────────────────────────────────\n\n/**\n * One policy as its `DROP` / `CREATE` pair — each a complete statement.\n *\n * This is the primitive the boot-time RLS applier runs one statement at a time\n * (the runtime's DB handle speaks the extended query protocol, which forbids\n * multiple commands in one execute), while `db push` writes the joined string.\n */\nexport const renderPolicyStatements = (\n table: Pick<TablePlan, \"schema\" | \"table\">,\n policy: PolicyPlan\n): string[] => {\n const target = `\"${table.schema}\".\"${table.table}\"`;\n let create = `CREATE POLICY \"${policy.name}\" ON ${target} AS ${policy.mode.toUpperCase()} ` +\n `FOR ${policy.operation.toUpperCase()} TO ${policy.roles.map(r => `\"${r}\"`).join(\", \")}`;\n if (policy.using) create += ` USING (${policy.using})`;\n if (policy.withCheck) create += ` WITH CHECK (${policy.withCheck})`;\n // The one policy an older naming stored this rule's operations under —\n // see `PolicyPlan.supersedes`. In the same per-table transaction, so a\n // request sees the old policy or the new ones, never neither.\n const superseded = (policy.supersedes ?? []).map(name => `DROP POLICY IF EXISTS \"${name}\" ON ${target};`);\n return [...superseded, `DROP POLICY IF EXISTS \"${policy.name}\" ON ${target};`, `${create};`];\n};\n\n/** `ALTER TABLE … ENABLE ROW LEVEL SECURITY;` — every generated table is locked. */\nexport const renderEnableRls = (table: Pick<TablePlan, \"schema\" | \"table\">): string =>\n `ALTER TABLE \"${table.schema}\".\"${table.table}\" ENABLE ROW LEVEL SECURITY;`;\n\nconst statementsToDdl = (statements: string[]): string => statements.map(s => `${s}\\n`).join(\"\");\n\n// ── schema.sql ───────────────────────────────────────────────────────────────\n\nexport function renderPostgresDdl(plan: SchemaPlan, options: DdlRenderOptions = {}): string {\n const includePolicies = options.includePolicies !== false;\n const includeSearch = options.includeSearch !== false;\n const includeVector = options.includeVector !== false;\n\n let ddl = \"-- This file is auto-generated by the Rebase DDL generator. Do not edit manually.\\n\\n\";\n\n // 1. Custom schemas.\n //\n // `rebase` is unconditional and load-bearing. The RLS helper functions live\n // in it, so the migration preamble creates it — which puts it in Atlas's\n // replayed state, and anything in that state but absent from this desired\n // schema gets a `DROP SCHEMA … CASCADE` planned against it. That would take\n // the auth tables.\n plan.schemas.forEach(schema => { ddl += `CREATE SCHEMA IF NOT EXISTS \"${schema}\";\\n`; });\n if (plan.schemas.length > 0) ddl += \"\\n\";\n\n // 1b. Search support, for collections that opted in.\n //\n // Extensions and helper functions come before every CREATE TABLE because a\n // generated column's expression is resolved at creation time: a table whose\n // search column calls `rebase_search_text` cannot be created before that\n // function exists. Both are `IF NOT EXISTS` / `OR REPLACE`, so a file\n // replayed against a live database is a no-op here.\n const searchTables = plan.tables.filter(t => t.search);\n if (searchTables.length > 0) {\n if (!includeSearch) {\n ddl += \"-- Full-text search support lives in `search.sql`, applied separately.\\n\\n\";\n } else {\n const extensions = Array.from(new Set(searchTables.flatMap(t => searchExtensionStatements(t.search!))));\n const helpers = Array.from(new Set(searchTables.flatMap(t => searchHelperFunctions(t.search!))));\n ddl += \"-- Full-text search support (collections declaring a `search` block)\\n\";\n extensions.forEach(s => { ddl += `${s}\\n`; });\n if (extensions.length > 0) ddl += \"\\n\";\n helpers.forEach(s => { ddl += `${s}\\n\\n`; });\n }\n }\n\n // 1c. Vector support. Only ever a note, never a statement — unlike 1b.\n // `CREATE EXTENSION vector` is the one thing this file must never carry\n // whichever way the flag goes: Atlas rejects an extension in a desired\n // state as a paid feature.\n if (!includeVector && plan.tables.some(t => t.vectorColumns.length > 0)) {\n ddl += \"-- Vector columns and their ANN indexes live in `vector.sql`, applied separately.\\n\\n\";\n }\n\n // 1d. `autoValue: \"on_update\"`. A trigger is a function plus a binding, so\n // it is out of Atlas's sight for the reason search is — and out\n // unconditionally, because `schema.sql` has no flag that would make Atlas\n // able to read one.\n if (plan.tables.some(t => t.triggers.length > 0)) {\n ddl += \"-- `autoValue: \\\"on_update\\\"` triggers live in `triggers.sql`, applied separately.\\n\\n\";\n }\n\n // 2. Enum types.\n plan.enums.forEach(enumPlan => {\n ddl += `CREATE TYPE \"${enumPlan.schema}\".\"${enumPlan.name}\" AS ENUM (${enumPlan.labels.map(quoteSqlLiteral).join(\", \")});\\n`;\n });\n if (ddl.endsWith(\";\\n\")) ddl += \"\\n\";\n\n // 3. Tables, in the plan's order — each collection, then any junction its\n // relations imply.\n const fkStatements: string[] = [];\n // Indexes follow the tables for the same reason the FK constraints do: the\n // table has to exist first.\n const indexStatements: string[] = [];\n // Policies are emitted after every CREATE TABLE, like the FK constraints: a\n // policy may reference other tables (a junction's derived policies always\n // reference both endpoints; `policy.existsIn` references a join table), and\n // CREATE POLICY validates those relations at creation time.\n const policyStatements: string[] = [];\n\n for (const table of plan.tables) {\n const emitted = table.columns.filter(column => {\n // A vector column is not Atlas's — `vector.sql` adds it. Filtered\n // here rather than upstream so the column keeps its place in the\n // generator's order when it *is* included, which is what the\n // derived-names contract renders.\n if (!includeVector && column.type.kind === \"vector\") return false;\n if (!includeSearch && column.source.kind === \"search\") return false;\n // A declared property already emits this column (an explicit\n // `postId` beside the `belongsTo` that uses it); the relation\n // contributes only the constraint.\n if (column.columnOwnedByProperty) return false;\n return true;\n });\n\n ddl += `CREATE TABLE \"${table.schema}\".\"${table.table}\" (\\n`;\n const lines = emitted.map(column => ` ${renderColumn(column)}`);\n // A key over several columns — a junction's two endpoints, or a\n // collection with several `isId` properties — is one table constraint;\n // a single-column key is already inline on its column.\n if (table.primaryKey.length > 1) {\n lines.push(` ${renderPrimaryKeyConstraint(table)}`);\n }\n ddl += lines.join(\",\\n\");\n ddl += \"\\n);\\n\\n\";\n\n for (const column of table.columns) {\n if (column.foreignKey) fkStatements.push(`${column.foreignKey.sql};`);\n }\n\n if (includeSearch && table.search) {\n indexStatements.push(...searchIndexStatements(table.search));\n }\n // ANN indexes are emitted with the other indexes rather than inline,\n // because `CREATE INDEX` is a statement and a column definition is not\n // — and because a column too wide for pgvector to index still needs its\n // column. Absent alongside their column when vector is carved out.\n if (includeVector && table.vector) {\n indexStatements.push(...vectorIndexStatements(table.vector));\n for (const skip of table.vector.skipped) {\n indexStatements.push(`-- No ANN index on \"${skip.schema}\".\"${skip.table}\".\"${skip.column}\": ${skip.reason}`);\n }\n }\n // The collection's own `indexes:` block. Last of the three index\n // producers, and the only one the developer wrote deliberately rather\n // than getting as a side effect of another feature.\n indexStatements.push(...collectionIndexStatements(table.indexes));\n\n if (includePolicies) {\n // No FORCE: authenticated requests run as the non-owner\n // `rebase_user` role, which plain ENABLE already binds. The owner\n // (server context) must bypass — it is the trusted plane.\n ddl += `${renderEnableRls(table)}\\n`;\n ddl += \"\\n\";\n for (const policy of table.policies) {\n policyStatements.push(statementsToDdl(renderPolicyStatements(table, policy)));\n }\n }\n }\n\n // Indexes before foreign keys. Today every FK targets a primary key created\n // inline, so the order is safe by accident; the moment a declared\n // `unique: true` index can back an FK target, a constraint emitted first\n // would reference an index that does not exist yet.\n if (indexStatements.length > 0) {\n ddl += \"-- Indexes\\n\";\n ddl += indexStatements.join(\"\\n\") + \"\\n\\n\";\n }\n\n if (fkStatements.length > 0) {\n ddl += \"-- Foreign Key Constraints\\n\";\n ddl += fkStatements.join(\"\\n\") + \"\\n\\n\";\n }\n\n if (policyStatements.length > 0) {\n ddl += \"-- Row Level Security Policies\\n\";\n ddl += policyStatements.join(\"\");\n ddl += \"\\n\";\n }\n\n return ddl;\n}\n\n// ── policies.sql ─────────────────────────────────────────────────────────────\n\nexport function renderPoliciesDdl(plan: SchemaPlan): string {\n let ddl = \"-- This file contains RLS policies generated by Rebase. Applied separately from migrations.\\n\\n\";\n\n // Collections first, then the junctions no collection declares — the order\n // the boot-time applier plans them in.\n for (const table of plan.tables.filter(t => t.kind === \"collection\")) {\n ddl += `${renderEnableRls(table)}\\n`;\n ddl += \"\\n\";\n if (table.policies.length === 0) continue;\n let announced: string | undefined;\n for (const policy of table.policies) {\n // Say which policies the author did not write. They are permissive,\n // so they OR with the declared rules and widen the final ACL beyond\n // what `securityRules` reads like — and re-appear after any manual\n // DROP, because a push asserts the declared state. Once per *rule*:\n // one rule with four operations is one thing the author did not\n // write, not four.\n if (policy.injected && policy.ruleKey !== announced) {\n ddl += \"-- Injected by Rebase (not from this collection's securityRules).\\n\";\n ddl += `-- Set \\`disableDefaultPolicies: true\\` on \"${table.slug}\" to drop these and own its RLS outright.\\n`;\n }\n announced = policy.ruleKey;\n ddl += statementsToDdl(renderPolicyStatements(table, policy));\n }\n ddl += \"\\n\";\n }\n\n for (const table of plan.tables.filter(t => t.kind === \"junction\")) {\n ddl += `${renderEnableRls(table)}\\n`;\n ddl += \"\\n\";\n if (table.policies.length === 0) continue;\n const declaringSlugs = (table.declaringSlugs ?? []).join('\", \"');\n ddl += `-- Derived by Rebase for the junction \"${table.table}\" (no collection declares it).\\n`;\n ddl += \"-- Reads require both endpoint rows to be visible; writes follow the update\\n\";\n ddl += `-- rules of \"${declaringSlugs}\". Set \\`disableDefaultPolicies: true\\` on the\\n`;\n ddl += \"-- declaring collection(s) to drop these and police the junction yourself.\\n\";\n for (const policy of table.policies) {\n ddl += statementsToDdl(renderPolicyStatements(table, policy));\n }\n ddl += \"\\n\";\n }\n\n return ddl;\n}\n\n// ── search.sql ───────────────────────────────────────────────────────────────\n\nexport function renderSearchDdl(plan: SchemaPlan): string {\n const tables = plan.tables.filter(t => t.search);\n if (tables.length === 0) return \"\";\n\n const extensions = Array.from(new Set(tables.flatMap(t => searchExtensionStatements(t.search!))));\n const helpers = Array.from(new Set(tables.flatMap(t => searchHelperFunctions(t.search!))));\n\n let ddl = \"-- This file is auto-generated by the Rebase DDL generator. Do not edit manually.\\n\";\n ddl += \"--\\n\";\n ddl += \"-- Full-text search for the collections declaring a `search` block.\\n\";\n ddl += \"-- Applied by Rebase, not by Atlas — see generatePostgresSearchDdl.\\n\\n\";\n\n extensions.forEach(s => { ddl += `${s}\\n`; });\n if (extensions.length > 0) ddl += \"\\n\";\n helpers.forEach(s => { ddl += `${s}\\n\\n`; });\n\n for (const table of tables) {\n const spec = table.search!;\n const target = `\"${table.schema}\".\"${table.table}\"`;\n // ADD COLUMN IF NOT EXISTS rather than the inline definition the\n // CREATE TABLE would use: by the time this runs the table exists,\n // whether Atlas just created it or it has been live for a year. The\n // guard first, because `ADD COLUMN IF NOT EXISTS` is a no-op against an\n // existing column — without it the file would report success, leave the\n // old expression in place, and then stamp it as current.\n searchStampGuards(spec).forEach(s => { ddl += `${s}\\n`; });\n ddl += `ALTER TABLE ${target} ADD COLUMN IF NOT EXISTS ${searchColumnDefinition(spec)};\\n`;\n const fuzzy = fuzzyColumnDefinition(spec);\n if (fuzzy) ddl += `ALTER TABLE ${target} ADD COLUMN IF NOT EXISTS ${fuzzy};\\n`;\n searchColumnStamps(spec).forEach(s => { ddl += `${s.sql}\\n`; });\n searchIndexStatements(spec).forEach(s => { ddl += `${s}\\n`; });\n ddl += \"\\n\";\n }\n\n return ddl;\n}\n\n/**\n * Glob patterns telling Atlas to leave an object alone.\n *\n * Fully qualified, `schema.table.object`, matching the include list. The\n * two-part form is what Atlas wants when the connection URL scopes it to one\n * schema and is *silently ignored* otherwise: it reads `posts.search_vector` as\n * a table named `search_vector` in a schema named `posts`, matches nothing, and\n * reports no error for the pattern that never fired. Each name is spelled by\n * `atlasExcludePattern`, so one holding a `.` or glob syntax still matches.\n */\nexport const searchExcludePatternsOf = (plan: SchemaPlan): string[] => {\n const patterns: string[] = [];\n for (const table of plan.tables) {\n if (!table.search) continue;\n for (const column of table.columns) {\n if (column.source.kind !== \"search\") continue;\n patterns.push(atlasExcludePattern(table.schema, table.table, column.column));\n }\n for (const index of searchIndexNames(table.search)) {\n patterns.push(atlasExcludePattern(table.schema, table.table, index));\n }\n }\n return patterns;\n};\n\n// ── vector.sql ───────────────────────────────────────────────────────────────\n\nexport function renderVectorDdl(plan: SchemaPlan): string {\n const tables = plan.tables.filter(t => t.vectorColumns.length > 0);\n if (tables.length === 0) return \"\";\n\n let ddl = \"-- This file is auto-generated by the Rebase DDL generator. Do not edit manually.\\n\";\n ddl += \"--\\n\";\n ddl += \"-- Vector columns and their ANN indexes, for the collections declaring a\\n\";\n ddl += \"-- `vector` property. Applied by Rebase, not by Atlas — see\\n\";\n ddl += \"-- generatePostgresVectorDdl.\\n\\n\";\n // The note is not decoration. Someone reading this file after a failed push\n // is looking for the reason the type does not exist, and \"no CREATE\n // EXTENSION here\" is invisible unless it is written down.\n ddl += vectorExtensionDeclared(plan.options.databaseExtensions)\n ? `${vectorExtensionStatement()}\\n\\n`\n : \"-- pgvector is not installed from here: no database declared it. To let Rebase\\n\" +\n `-- install it, add ${VECTOR_EXTENSION_OPT_IN} in config/resources.ts.\\n` +\n \"-- Otherwise install it once by hand; the column below needs the type either way.\\n\\n\";\n\n for (const table of tables) {\n const target = `\"${table.schema}\".\"${table.table}\"`;\n for (const spec of table.vectorColumns) {\n // Guard before the ADD, so a changed `dimensions` is reported\n // rather than skipped over — see `vectorDimensionGuard`.\n ddl += `${vectorDimensionGuard(spec)}\\n`;\n ddl += `ALTER TABLE ${target} ADD COLUMN IF NOT EXISTS ${vectorColumnDefinition(spec)};\\n`;\n }\n if (table.vector) {\n vectorIndexStatements(table.vector).forEach(s => { ddl += `${s}\\n`; });\n for (const skip of table.vector.skipped) {\n ddl += `-- No ANN index on \"${skip.schema}\".\"${skip.table}\".\"${skip.column}\": ${skip.reason}\\n`;\n }\n }\n ddl += \"\\n\";\n }\n\n return ddl;\n}\n\n/** @see searchExcludePatternsOf */\nexport const vectorExcludePatternsOf = (plan: SchemaPlan): string[] => {\n const patterns: string[] = [];\n for (const table of plan.tables) {\n const names = [\n ...table.vectorColumns.map(spec => spec.column),\n ...(table.vector ? vectorIndexNames(table.vector) : [])\n ];\n for (const name of names) patterns.push(atlasExcludePattern(table.schema, table.table, name));\n }\n return patterns;\n};\n\n// ── triggers.sql ─────────────────────────────────────────────────────────────\n\n/**\n * `autoValue: \"on_update\"`, as the file Rebase applies itself.\n *\n * Empty when nothing declares one — the caller writes no file then, and removes\n * a stale one.\n */\nexport function renderTriggersDdl(plan: SchemaPlan): string {\n const tables = plan.tables.filter(t => t.triggers.length > 0);\n if (tables.length === 0) return \"\";\n\n let ddl = \"-- This file is auto-generated by the Rebase DDL generator. Do not edit manually.\\n\";\n ddl += \"--\\n\";\n ddl += \"-- `updated_at` triggers, for the properties declaring `autoValue: \\\"on_update\\\"`.\\n\";\n ddl += \"-- Applied by Rebase, not by Atlas — a trigger is a function plus a binding,\\n\";\n ddl += \"-- and Atlas's free tier refuses to parse a desired state containing one.\\n\\n\";\n\n for (const statement of plan.functions.filter(isTriggerFunction)) {\n ddl += `${statement}\\n\\n`;\n }\n for (const table of tables) {\n for (const trigger of table.triggers) {\n triggerStatements(trigger).forEach(s => { ddl += `${s}\\n`; });\n }\n ddl += \"\\n\";\n }\n return ddl;\n}\n\nconst isTriggerFunction = (statement: string): boolean => statement.includes(\"RETURNS trigger\");\n\n/** @see searchExcludePatternsOf */\nexport const triggerExcludePatternsOf = (plan: SchemaPlan): string[] =>\n plan.tables.flatMap(table => table.triggers.map(t => atlasExcludePattern(table.schema, table.table, t.name)));\n"],"mappings":"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"}
|
|
1
|
+
{"version":3,"file":"render-ddl-4v88y-Gv.js","names":[],"sources":["../src/schema/plan/render-ddl.ts"],"sourcesContent":["/**\n * The SQL files, rendered from a {@link SchemaPlan}.\n *\n * Four files, because Atlas owns exactly one of them:\n *\n * - `schema.sql` — the desired state Atlas diffs the database against.\n * - `policies.sql` — the RLS policies. Atlas has no model for a policy.\n * - `search.sql` — the generated `tsvector` column, its GIN index and the\n * IMMUTABLE helpers its expression calls. Atlas's free tier refuses to\n * *parse* a file containing a function, and it wipes the dev database it\n * diffs against, so a helper seeded there is gone by the time the plan runs.\n * - `vector.sql` — the `vector` columns and their ANN indexes. Atlas cannot\n * *execute* these: it materialises `schema.sql` in a dev database that is\n * created empty and emptied again on every run, so `VECTOR(384)` resolves\n * against a database that structurally cannot have pgvector.\n * - `triggers.sql` — `autoValue: \"on_update\"`, which is a function plus a\n * binding, so it is out for the same reason search is.\n *\n * Everything not in `schema.sql` is excluded from Atlas's view by pattern\n * (`…ExcludePatterns` below) and applied by Rebase — at push time by the CLI,\n * at boot by the schema ensure. Without the excludes a desired state that omits\n * an object reads to Atlas as an instruction to drop it.\n */\nimport {\n fuzzyColumnDefinition,\n searchColumnDefinition,\n searchColumnStamps,\n searchExtensionStatements,\n searchHelperFunctions,\n searchIndexNames,\n searchIndexStatements,\n searchStampGuards\n} from \"../search-column\";\nimport {\n VECTOR_EXTENSION_OPT_IN,\n vectorColumnDefinition,\n vectorDimensionGuard,\n vectorExtensionDeclared,\n vectorExtensionStatement,\n vectorIndexNames,\n vectorIndexStatements\n} from \"../vector-index\";\nimport { atlasExcludePattern } from \"../atlas-exclude\";\nimport { collectionIndexStatements } from \"../collection-index\";\nimport { quoteSqlLiteral } from \"./plan-schema\";\nimport { triggerStatements } from \"./updated-at-trigger\";\nimport type { ColumnPlan, PgType, PolicyPlan, SchemaPlan, TablePlan } from \"./types\";\n\n/** What `schema.sql` may leave out, because another file owns it. */\nexport interface DdlRenderOptions {\n /** RLS policies. `false` puts them in `policies.sql` alone. */\n includePolicies?: boolean;\n /** The search column, its helpers and its index. See `search.sql`. */\n includeSearch?: boolean;\n /** The vector columns and their ANN indexes. See `vector.sql`. */\n includeVector?: boolean;\n}\n\n/**\n * A {@link PgType} as Postgres spells it.\n *\n * The one place. `getSqlColumnType` and `getDrizzleColumn` each carried their\n * own ladder and disagreed about `VARCHAR` widths, about `geopoint`, and about\n * whether an `increment` id honours `columnType`.\n */\nexport const renderPgType = (type: PgType): string => {\n switch (type.kind) {\n case \"text\": return \"TEXT\";\n case \"varchar\": return `VARCHAR(${type.length})`;\n case \"char\": return `CHAR(${type.length})`;\n case \"uuid\": return \"UUID\";\n case \"enum\": return `\"${type.schema}\".\"${type.name}\"`;\n case \"smallint\": return \"SMALLINT\";\n case \"integer\": return \"INTEGER\";\n case \"bigint\": return \"BIGINT\";\n case \"smallserial\": return \"SMALLSERIAL\";\n case \"serial\": return \"SERIAL\";\n case \"bigserial\": return \"BIGSERIAL\";\n case \"real\": return \"REAL\";\n case \"doublePrecision\": return \"DOUBLE PRECISION\";\n case \"numeric\":\n if (type.precision === undefined) return \"NUMERIC\";\n return type.scale === undefined\n ? `NUMERIC(${type.precision})`\n : `NUMERIC(${type.precision}, ${type.scale})`;\n case \"boolean\": return \"BOOLEAN\";\n case \"timestamptz\": return \"TIMESTAMP WITH TIME ZONE\";\n case \"date\": return \"DATE\";\n case \"time\": return \"TIME\";\n case \"json\": return \"JSON\";\n case \"jsonb\": return \"JSONB\";\n case \"vector\": return `VECTOR(${type.dimensions})`;\n case \"bytea\": return \"BYTEA\";\n case \"tsvector\": return \"tsvector\";\n case \"array\": return `${renderPgType(type.of)}[]`;\n }\n};\n\n/**\n * Everything after the column name, in one order for every SQL emitter.\n *\n * `CREATE TABLE` and `ADD COLUMN` used to assemble this separately, which is\n * how a required relation column came out `NOT NULL` after `db push` and\n * nullable after a boot-ensure. `identity` is part of the type rather than a\n * DEFAULT, which is why it is rendered here and not in the DEFAULT slot.\n */\nexport const renderColumnDefinition = (column: ColumnPlan): string => {\n // `auth-users-columns` is the single description of what an auth user\n // table's columns must be, and it hands over a rendered definition.\n if (column.sqlDefinition) return column.sqlDefinition;\n\n if (column.generated) {\n return `${renderPgType(column.type)} GENERATED ALWAYS AS (${column.generated.expression}) STORED`;\n }\n\n let definition = renderPgType(column.type);\n if (column.default?.kind === \"identity\") definition += \" GENERATED BY DEFAULT AS IDENTITY\";\n if (column.primaryKey) definition += \" PRIMARY KEY\";\n if (column.unique) definition += \" UNIQUE\";\n if (column.default && column.default.kind !== \"identity\") {\n definition += ` DEFAULT ${column.default.kind === \"sql\" ? column.default.expression : column.default.sql}`;\n }\n if (!column.nullable && !column.primaryKey) definition += \" NOT NULL\";\n return definition;\n};\n\n/**\n * `PRIMARY KEY (\"a\", \"b\")`, the clause a composite key takes inside a\n * `CREATE TABLE`. Shared by `schema.sql` and boot-ensure's `CREATE TABLE`, so\n * the two cannot list the columns differently.\n */\nexport const renderPrimaryKeyConstraint = (table: Pick<TablePlan, \"primaryKey\">): string =>\n `PRIMARY KEY (${table.primaryKey.map(c => `\"${c}\"`).join(\", \")})`;\n\n/** `\"col\" TYPE …`, as it appears inside a `CREATE TABLE`. */\nconst renderColumn = (column: ColumnPlan): string =>\n `\"${column.column}\" ${renderColumnDefinition(column)}`;\n\n// ── Policies ─────────────────────────────────────────────────────────────────\n\n/**\n * One policy as its `DROP` / `CREATE` pair — each a complete statement.\n *\n * This is the primitive the boot-time RLS applier runs one statement at a time\n * (the runtime's DB handle speaks the extended query protocol, which forbids\n * multiple commands in one execute), while `db push` writes the joined string.\n */\nexport const renderPolicyStatements = (\n table: Pick<TablePlan, \"schema\" | \"table\">,\n policy: PolicyPlan\n): string[] => {\n const target = `\"${table.schema}\".\"${table.table}\"`;\n let create = `CREATE POLICY \"${policy.name}\" ON ${target} AS ${policy.mode.toUpperCase()} ` +\n `FOR ${policy.operation.toUpperCase()} TO ${policy.roles.map(r => `\"${r}\"`).join(\", \")}`;\n if (policy.using) create += ` USING (${policy.using})`;\n if (policy.withCheck) create += ` WITH CHECK (${policy.withCheck})`;\n // The one policy an older naming stored this rule's operations under —\n // see `PolicyPlan.supersedes`. In the same per-table transaction, so a\n // request sees the old policy or the new ones, never neither.\n const superseded = (policy.supersedes ?? []).map(name => `DROP POLICY IF EXISTS \"${name}\" ON ${target};`);\n return [...superseded, `DROP POLICY IF EXISTS \"${policy.name}\" ON ${target};`, `${create};`];\n};\n\n/** `ALTER TABLE … ENABLE ROW LEVEL SECURITY;` — every generated table is locked. */\nexport const renderEnableRls = (table: Pick<TablePlan, \"schema\" | \"table\">): string =>\n `ALTER TABLE \"${table.schema}\".\"${table.table}\" ENABLE ROW LEVEL SECURITY;`;\n\nconst statementsToDdl = (statements: string[]): string => statements.map(s => `${s}\\n`).join(\"\");\n\n// ── schema.sql ───────────────────────────────────────────────────────────────\n\nexport function renderPostgresDdl(plan: SchemaPlan, options: DdlRenderOptions = {}): string {\n const includePolicies = options.includePolicies !== false;\n const includeSearch = options.includeSearch !== false;\n const includeVector = options.includeVector !== false;\n\n let ddl = \"-- This file is auto-generated by the Rebase DDL generator. Do not edit manually.\\n\\n\";\n\n // 1. Custom schemas.\n //\n // `rebase` is unconditional and load-bearing. The RLS helper functions live\n // in it, so the migration preamble creates it — which puts it in Atlas's\n // replayed state, and anything in that state but absent from this desired\n // schema gets a `DROP SCHEMA … CASCADE` planned against it. That would take\n // the auth tables.\n plan.schemas.forEach(schema => { ddl += `CREATE SCHEMA IF NOT EXISTS \"${schema}\";\\n`; });\n if (plan.schemas.length > 0) ddl += \"\\n\";\n\n // 1b. Search support, for collections that opted in.\n //\n // Extensions and helper functions come before every CREATE TABLE because a\n // generated column's expression is resolved at creation time: a table whose\n // search column calls `rebase_search_text` cannot be created before that\n // function exists. Both are `IF NOT EXISTS` / `OR REPLACE`, so a file\n // replayed against a live database is a no-op here.\n const searchTables = plan.tables.filter(t => t.search);\n if (searchTables.length > 0) {\n if (!includeSearch) {\n ddl += \"-- Full-text search support lives in `search.sql`, applied separately.\\n\\n\";\n } else {\n const extensions = Array.from(new Set(searchTables.flatMap(t => searchExtensionStatements(t.search!))));\n const helpers = Array.from(new Set(searchTables.flatMap(t => searchHelperFunctions(t.search!))));\n ddl += \"-- Full-text search support (collections declaring a `search` block)\\n\";\n extensions.forEach(s => { ddl += `${s}\\n`; });\n if (extensions.length > 0) ddl += \"\\n\";\n helpers.forEach(s => { ddl += `${s}\\n\\n`; });\n }\n }\n\n // 1c. Vector support. Only ever a note, never a statement — unlike 1b.\n // `CREATE EXTENSION vector` is the one thing this file must never carry\n // whichever way the flag goes: Atlas rejects an extension in a desired\n // state as a paid feature.\n if (!includeVector && plan.tables.some(t => t.vectorColumns.length > 0)) {\n ddl += \"-- Vector columns and their ANN indexes live in `vector.sql`, applied separately.\\n\\n\";\n }\n\n // 1d. `autoValue: \"on_update\"`. A trigger is a function plus a binding, so\n // it is out of Atlas's sight for the reason search is — and out\n // unconditionally, because `schema.sql` has no flag that would make Atlas\n // able to read one.\n if (plan.tables.some(t => t.triggers.length > 0)) {\n ddl += \"-- `autoValue: \\\"on_update\\\"` triggers live in `triggers.sql`, applied separately.\\n\\n\";\n }\n\n // 2. Enum types.\n plan.enums.forEach(enumPlan => {\n ddl += `CREATE TYPE \"${enumPlan.schema}\".\"${enumPlan.name}\" AS ENUM (${enumPlan.labels.map(quoteSqlLiteral).join(\", \")});\\n`;\n });\n if (ddl.endsWith(\";\\n\")) ddl += \"\\n\";\n\n // 3. Tables, in the plan's order — each collection, then any junction its\n // relations imply.\n const fkStatements: string[] = [];\n // Indexes follow the tables for the same reason the FK constraints do: the\n // table has to exist first.\n const indexStatements: string[] = [];\n // Policies are emitted after every CREATE TABLE, like the FK constraints: a\n // policy may reference other tables (a junction's derived policies always\n // reference both endpoints; `policy.existsIn` references a join table), and\n // CREATE POLICY validates those relations at creation time.\n const policyStatements: string[] = [];\n\n for (const table of plan.tables) {\n const emitted = table.columns.filter(column => {\n // A vector column is not Atlas's — `vector.sql` adds it. Filtered\n // here rather than upstream so the column keeps its place in the\n // generator's order when it *is* included, which is what the\n // derived-names contract renders.\n if (!includeVector && column.type.kind === \"vector\") return false;\n if (!includeSearch && column.source.kind === \"search\") return false;\n // A declared property already emits this column (an explicit\n // `postId` beside the `belongsTo` that uses it); the relation\n // contributes only the constraint.\n if (column.columnOwnedByProperty) return false;\n return true;\n });\n\n ddl += `CREATE TABLE \"${table.schema}\".\"${table.table}\" (\\n`;\n const lines = emitted.map(column => ` ${renderColumn(column)}`);\n // A key over several columns — a junction's two endpoints, or a\n // collection with several `isId` properties — is one table constraint;\n // a single-column key is already inline on its column.\n if (table.primaryKey.length > 1) {\n lines.push(` ${renderPrimaryKeyConstraint(table)}`);\n }\n ddl += lines.join(\",\\n\");\n ddl += \"\\n);\\n\\n\";\n\n for (const column of table.columns) {\n if (column.foreignKey) fkStatements.push(`${column.foreignKey.sql};`);\n }\n\n if (includeSearch && table.search) {\n indexStatements.push(...searchIndexStatements(table.search));\n }\n // ANN indexes are emitted with the other indexes rather than inline,\n // because `CREATE INDEX` is a statement and a column definition is not\n // — and because a column too wide for pgvector to index still needs its\n // column. Absent alongside their column when vector is carved out.\n if (includeVector && table.vector) {\n indexStatements.push(...vectorIndexStatements(table.vector));\n for (const skip of table.vector.skipped) {\n indexStatements.push(`-- No ANN index on \"${skip.schema}\".\"${skip.table}\".\"${skip.column}\": ${skip.reason}`);\n }\n }\n // The collection's own `indexes:` block. Last of the three index\n // producers, and the only one the developer wrote deliberately rather\n // than getting as a side effect of another feature.\n indexStatements.push(...collectionIndexStatements(table.indexes));\n\n if (includePolicies) {\n // No FORCE: authenticated requests run as the non-owner\n // `rebase_user` role, which plain ENABLE already binds. The owner\n // (server context) must bypass — it is the trusted plane.\n ddl += `${renderEnableRls(table)}\\n`;\n ddl += \"\\n\";\n for (const policy of table.policies) {\n policyStatements.push(statementsToDdl(renderPolicyStatements(table, policy)));\n }\n }\n }\n\n // Indexes before foreign keys. Today every FK targets a primary key created\n // inline, so the order is safe by accident; the moment a declared\n // `unique: true` index can back an FK target, a constraint emitted first\n // would reference an index that does not exist yet.\n if (indexStatements.length > 0) {\n ddl += \"-- Indexes\\n\";\n ddl += indexStatements.join(\"\\n\") + \"\\n\\n\";\n }\n\n if (fkStatements.length > 0) {\n ddl += \"-- Foreign Key Constraints\\n\";\n ddl += fkStatements.join(\"\\n\") + \"\\n\\n\";\n }\n\n if (policyStatements.length > 0) {\n ddl += \"-- Row Level Security Policies\\n\";\n ddl += policyStatements.join(\"\");\n ddl += \"\\n\";\n }\n\n return ddl;\n}\n\n// ── policies.sql ─────────────────────────────────────────────────────────────\n\nexport function renderPoliciesDdl(plan: SchemaPlan): string {\n let ddl = \"-- This file contains RLS policies generated by Rebase. Applied separately from migrations.\\n\\n\";\n\n // Collections first, then the junctions no collection declares — the order\n // the boot-time applier plans them in.\n for (const table of plan.tables.filter(t => t.kind === \"collection\")) {\n ddl += `${renderEnableRls(table)}\\n`;\n ddl += \"\\n\";\n if (table.policies.length === 0) continue;\n let announced: string | undefined;\n for (const policy of table.policies) {\n // Say which policies the author did not write. They are permissive,\n // so they OR with the declared rules and widen the final ACL beyond\n // what `securityRules` reads like — and re-appear after any manual\n // DROP, because a push asserts the declared state. Once per *rule*:\n // one rule with four operations is one thing the author did not\n // write, not four.\n if (policy.injected && policy.ruleKey !== announced) {\n ddl += \"-- Injected by Rebase (not from this collection's securityRules).\\n\";\n ddl += `-- Set \\`disableDefaultPolicies: true\\` on \"${table.slug}\" to drop these and own its RLS outright.\\n`;\n }\n announced = policy.ruleKey;\n ddl += statementsToDdl(renderPolicyStatements(table, policy));\n }\n ddl += \"\\n\";\n }\n\n for (const table of plan.tables.filter(t => t.kind === \"junction\")) {\n ddl += `${renderEnableRls(table)}\\n`;\n ddl += \"\\n\";\n if (table.policies.length === 0) continue;\n const declaringSlugs = (table.declaringSlugs ?? []).join('\", \"');\n ddl += `-- Derived by Rebase for the junction \"${table.table}\" (no collection declares it).\\n`;\n ddl += \"-- Reads require both endpoint rows to be visible; writes follow the update\\n\";\n ddl += `-- rules of \"${declaringSlugs}\". Set \\`disableDefaultPolicies: true\\` on the\\n`;\n ddl += \"-- declaring collection(s) to drop these and police the junction yourself.\\n\";\n for (const policy of table.policies) {\n ddl += statementsToDdl(renderPolicyStatements(table, policy));\n }\n ddl += \"\\n\";\n }\n\n return ddl;\n}\n\n// ── search.sql ───────────────────────────────────────────────────────────────\n\nexport function renderSearchDdl(plan: SchemaPlan): string {\n const tables = plan.tables.filter(t => t.search);\n if (tables.length === 0) return \"\";\n\n const extensions = Array.from(new Set(tables.flatMap(t => searchExtensionStatements(t.search!))));\n const helpers = Array.from(new Set(tables.flatMap(t => searchHelperFunctions(t.search!))));\n\n let ddl = \"-- This file is auto-generated by the Rebase DDL generator. Do not edit manually.\\n\";\n ddl += \"--\\n\";\n ddl += \"-- Full-text search for the collections declaring a `search` block.\\n\";\n ddl += \"-- Applied by Rebase, not by Atlas — see generatePostgresSearchDdl.\\n\\n\";\n\n extensions.forEach(s => { ddl += `${s}\\n`; });\n if (extensions.length > 0) ddl += \"\\n\";\n helpers.forEach(s => { ddl += `${s}\\n\\n`; });\n\n for (const table of tables) {\n const spec = table.search!;\n const target = `\"${table.schema}\".\"${table.table}\"`;\n // ADD COLUMN IF NOT EXISTS rather than the inline definition the\n // CREATE TABLE would use: by the time this runs the table exists,\n // whether Atlas just created it or it has been live for a year. The\n // guard first, because `ADD COLUMN IF NOT EXISTS` is a no-op against an\n // existing column — without it the file would report success, leave the\n // old expression in place, and then stamp it as current.\n searchStampGuards(spec).forEach(s => { ddl += `${s}\\n`; });\n ddl += `ALTER TABLE ${target} ADD COLUMN IF NOT EXISTS ${searchColumnDefinition(spec)};\\n`;\n const fuzzy = fuzzyColumnDefinition(spec);\n if (fuzzy) ddl += `ALTER TABLE ${target} ADD COLUMN IF NOT EXISTS ${fuzzy};\\n`;\n searchColumnStamps(spec).forEach(s => { ddl += `${s.sql}\\n`; });\n searchIndexStatements(spec).forEach(s => { ddl += `${s}\\n`; });\n ddl += \"\\n\";\n }\n\n return ddl;\n}\n\n/**\n * Glob patterns telling Atlas to leave an object alone.\n *\n * Fully qualified, `schema.table.object`, matching the include list. The\n * two-part form is what Atlas wants when the connection URL scopes it to one\n * schema and is *silently ignored* otherwise: it reads `posts.search_vector` as\n * a table named `search_vector` in a schema named `posts`, matches nothing, and\n * reports no error for the pattern that never fired. Each name is spelled by\n * `atlasExcludePattern`, so one holding a `.` or glob syntax still matches.\n */\nexport const searchExcludePatternsOf = (plan: SchemaPlan): string[] => {\n const patterns: string[] = [];\n for (const table of plan.tables) {\n if (!table.search) continue;\n for (const column of table.columns) {\n if (column.source.kind !== \"search\") continue;\n patterns.push(atlasExcludePattern(table.schema, table.table, column.column));\n }\n for (const index of searchIndexNames(table.search)) {\n patterns.push(atlasExcludePattern(table.schema, table.table, index));\n }\n }\n return patterns;\n};\n\n// ── vector.sql ───────────────────────────────────────────────────────────────\n\nexport function renderVectorDdl(plan: SchemaPlan): string {\n const tables = plan.tables.filter(t => t.vectorColumns.length > 0);\n if (tables.length === 0) return \"\";\n\n let ddl = \"-- This file is auto-generated by the Rebase DDL generator. Do not edit manually.\\n\";\n ddl += \"--\\n\";\n ddl += \"-- Vector columns and their ANN indexes, for the collections declaring a\\n\";\n ddl += \"-- `vector` property. Applied by Rebase, not by Atlas — see\\n\";\n ddl += \"-- generatePostgresVectorDdl.\\n\\n\";\n // The note is not decoration. Someone reading this file after a failed push\n // is looking for the reason the type does not exist, and \"no CREATE\n // EXTENSION here\" is invisible unless it is written down.\n ddl += vectorExtensionDeclared(plan.options.databaseExtensions)\n ? `${vectorExtensionStatement()}\\n\\n`\n : \"-- pgvector is not installed from here: no database declared it. To let Rebase\\n\" +\n `-- install it, add ${VECTOR_EXTENSION_OPT_IN} in config/resources.ts.\\n` +\n \"-- Otherwise install it once by hand; the column below needs the type either way.\\n\\n\";\n\n for (const table of tables) {\n const target = `\"${table.schema}\".\"${table.table}\"`;\n for (const spec of table.vectorColumns) {\n // Guard before the ADD, so a changed `dimensions` is reported\n // rather than skipped over — see `vectorDimensionGuard`.\n ddl += `${vectorDimensionGuard(spec)}\\n`;\n ddl += `ALTER TABLE ${target} ADD COLUMN IF NOT EXISTS ${vectorColumnDefinition(spec)};\\n`;\n }\n if (table.vector) {\n vectorIndexStatements(table.vector).forEach(s => { ddl += `${s}\\n`; });\n for (const skip of table.vector.skipped) {\n ddl += `-- No ANN index on \"${skip.schema}\".\"${skip.table}\".\"${skip.column}\": ${skip.reason}\\n`;\n }\n }\n ddl += \"\\n\";\n }\n\n return ddl;\n}\n\n/** @see searchExcludePatternsOf */\nexport const vectorExcludePatternsOf = (plan: SchemaPlan): string[] => {\n const patterns: string[] = [];\n for (const table of plan.tables) {\n const names = [\n ...table.vectorColumns.map(spec => spec.column),\n ...(table.vector ? vectorIndexNames(table.vector) : [])\n ];\n for (const name of names) patterns.push(atlasExcludePattern(table.schema, table.table, name));\n }\n return patterns;\n};\n\n// ── triggers.sql ─────────────────────────────────────────────────────────────\n\n/**\n * `autoValue: \"on_update\"`, as the file Rebase applies itself.\n *\n * Empty when nothing declares one — the caller writes no file then, and removes\n * a stale one.\n */\nexport function renderTriggersDdl(plan: SchemaPlan): string {\n const tables = plan.tables.filter(t => t.triggers.length > 0);\n if (tables.length === 0) return \"\";\n\n let ddl = \"-- This file is auto-generated by the Rebase DDL generator. Do not edit manually.\\n\";\n ddl += \"--\\n\";\n ddl += \"-- `updated_at` triggers, for the properties declaring `autoValue: \\\"on_update\\\"`.\\n\";\n ddl += \"-- Applied by Rebase, not by Atlas — a trigger is a function plus a binding,\\n\";\n ddl += \"-- and Atlas's free tier refuses to parse a desired state containing one.\\n\\n\";\n\n for (const statement of plan.functions.filter(isTriggerFunction)) {\n ddl += `${statement}\\n\\n`;\n }\n for (const table of tables) {\n for (const trigger of table.triggers) {\n triggerStatements(trigger).forEach(s => { ddl += `${s}\\n`; });\n }\n ddl += \"\\n\";\n }\n return ddl;\n}\n\nconst isTriggerFunction = (statement: string): boolean => statement.includes(\"RETURNS trigger\");\n\n/** @see searchExcludePatternsOf */\nexport const triggerExcludePatternsOf = (plan: SchemaPlan): string[] =>\n plan.tables.flatMap(table => table.triggers.map(t => atlasExcludePattern(table.schema, table.table, t.name)));\n"],"mappings":"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"}
|
|
@@ -1,6 +1,6 @@
|
|
|
1
1
|
import { createRequire as __createRequire } from "module";
|
|
2
2
|
__createRequire(import.meta.url);
|
|
3
|
-
import {
|
|
3
|
+
import { i as __exportAll } from "./cli-prompt-DXG0yk-h.js";
|
|
4
4
|
import { logger } from "@rebasepro/server";
|
|
5
5
|
import { sql } from "drizzle-orm";
|
|
6
6
|
import { ANONYMOUS_USER_ID, REBASE_SCHEMA, usesLegacyRlsFunctions } from "@rebasepro/types";
|
|
@@ -460,4 +460,4 @@ async function validatePolicyPgRoles(run, collections, requestRole = REBASE_USER
|
|
|
460
460
|
//#endregion
|
|
461
461
|
export { ensureAppRoleGrants as a, rls_enforcement_exports as c, warnOnLegacyRlsFunctions as d, warnOnRoleSchemaCollision as f, ensureAppRole as i, validatePolicyPgRoles as l, applyAuthContext as n, internalTablesGrantedToUserRole as o, detectConnectionPosture as r, revokeUserRoleSql as s, REBASE_USER_ROLE$1 as t, warnOnAnonymousGrants as u };
|
|
462
462
|
|
|
463
|
-
//# sourceMappingURL=rls-enforcement-
|
|
463
|
+
//# sourceMappingURL=rls-enforcement-DDyNoOSy.js.map
|