@rebasepro/server-postgres 0.19.1 → 0.19.2-canary.g08eed46

This diff represents the content of publicly available package versions that have been released to one of the supported registries. The information contained in this diff is provided for informational purposes only and reflects changes between package versions as they appear in their respective public registries.
Files changed (96) hide show
  1. package/dist/{BranchService-ByLfM_Tt.js → BranchService-W3DMfcZZ.js} +2 -2
  2. package/dist/{BranchService-ByLfM_Tt.js.map → BranchService-W3DMfcZZ.js.map} +1 -1
  3. package/dist/PostgresBackendDriver.d.ts +81 -7
  4. package/dist/PostgresBootstrapper.d.ts +10 -0
  5. package/dist/{backup-cli-oM9WQVlV.js → backup-cli-DSpQyqcG.js} +2 -2
  6. package/dist/{backup-cli-oM9WQVlV.js.map → backup-cli-DSpQyqcG.js.map} +1 -1
  7. package/dist/{backup-service-Br3tqwsy.js → backup-service-DA7a6SUV.js} +2 -2
  8. package/dist/{backup-service-Br3tqwsy.js.map → backup-service-DA7a6SUV.js.map} +1 -1
  9. package/dist/{cli-errors-DxN4yzdH.js → cli-errors-C3g_kHBw.js} +137 -18
  10. package/dist/cli-errors-C3g_kHBw.js.map +1 -0
  11. package/dist/cli-errors.d.ts +19 -0
  12. package/dist/cli-helpers.d.ts +47 -0
  13. package/dist/cli.js +233 -19
  14. package/dist/cli.js.map +1 -1
  15. package/dist/{collection-index-BHgT_TKd.js → collection-index-DdnsxiJ_.js} +14 -4
  16. package/dist/{collection-index-BHgT_TKd.js.map → collection-index-DdnsxiJ_.js.map} +1 -1
  17. package/dist/collections/validate-relations.d.ts +7 -0
  18. package/dist/column-plan-helpers-CpILzHJS.js +145 -0
  19. package/dist/column-plan-helpers-CpILzHJS.js.map +1 -0
  20. package/dist/{doctor-D7HWmGzs.js → doctor-D5SrGJ5P.js} +175 -61
  21. package/dist/doctor-D5SrGJ5P.js.map +1 -0
  22. package/dist/{ensure-collection-policies-2c2NV8zX.js → ensure-collection-policies-CHO0moXQ.js} +5 -5
  23. package/dist/{ensure-collection-policies-2c2NV8zX.js.map → ensure-collection-policies-CHO0moXQ.js.map} +1 -1
  24. package/dist/{ensure-collection-tables-D4wJzM4y.js → ensure-collection-tables-Bkvehxdm.js} +318 -312
  25. package/dist/ensure-collection-tables-Bkvehxdm.js.map +1 -0
  26. package/dist/{ensure-tables-CIojK-Rx.js → ensure-tables-CYMtuuOd.js} +2 -2
  27. package/dist/{ensure-tables-CIojK-Rx.js.map → ensure-tables-CYMtuuOd.js.map} +1 -1
  28. package/dist/generate-drizzle-schema-logic-Dfyf_MRu.js +228 -0
  29. package/dist/generate-drizzle-schema-logic-Dfyf_MRu.js.map +1 -0
  30. package/dist/{generate-drizzle-schema-CwEgVyjO.js → generate-drizzle-schema-vSK-VdYT.js} +3 -3
  31. package/dist/generate-drizzle-schema-vSK-VdYT.js.map +1 -0
  32. package/dist/generate-postgres-ddl-logic-CR2xcS7e.js +475 -0
  33. package/dist/generate-postgres-ddl-logic-CR2xcS7e.js.map +1 -0
  34. package/dist/{generated-schema-staleness-DQdfJl2A.js → generated-schema-staleness-DRQe2BpC.js} +81 -3
  35. package/dist/{generated-schema-staleness-DQdfJl2A.js.map → generated-schema-staleness-DRQe2BpC.js.map} +1 -1
  36. package/dist/history/HistoryService.d.ts +23 -4
  37. package/dist/index.es.js +1991 -580
  38. package/dist/index.es.js.map +1 -1
  39. package/dist/introspect-db-logic-C6LQdTxj.js.map +1 -1
  40. package/dist/plan-schema-Hgl62S-w.js +1531 -0
  41. package/dist/plan-schema-Hgl62S-w.js.map +1 -0
  42. package/dist/{policy-drift-kNJ6X2Uj.js → policy-drift-B0GDRh7_.js} +2 -2
  43. package/dist/{policy-drift-kNJ6X2Uj.js.map → policy-drift-B0GDRh7_.js.map} +1 -1
  44. package/dist/{rls-bootstrap-sql-DLDnUjtx.js → rls-bootstrap-sql-BlzsOUtz.js} +111 -8
  45. package/dist/{rls-bootstrap-sql-DLDnUjtx.js.map → rls-bootstrap-sql-BlzsOUtz.js.map} +1 -1
  46. package/dist/{rls-enforcement-DeYEjYLt.js → rls-enforcement-BV12vBwQ.js} +2 -1
  47. package/dist/rls-enforcement-BV12vBwQ.js.map +1 -0
  48. package/dist/schema/catalogue-schema.d.ts +97 -0
  49. package/dist/schema/collection-index.d.ts +9 -0
  50. package/dist/schema/column-plan-helpers.d.ts +125 -0
  51. package/dist/schema/config-relations.d.ts +32 -0
  52. package/dist/schema/doctor-cli.js +4 -4
  53. package/dist/schema/doctor.d.ts +9 -0
  54. package/dist/schema/dynamic-tables.d.ts +27 -6
  55. package/dist/schema/ensure-collection-tables.d.ts +31 -268
  56. package/dist/schema/generate-drizzle-schema-logic.d.ts +26 -8
  57. package/dist/schema/generate-drizzle-schema.js +1 -1
  58. package/dist/schema/generate-postgres-ddl-logic.d.ts +84 -168
  59. package/dist/schema/generate-postgres-ddl.js +8 -2
  60. package/dist/schema/generate-postgres-ddl.js.map +1 -1
  61. package/dist/schema/generated-column-conflicts.d.ts +84 -0
  62. package/dist/schema/generated-schema-diff.d.ts +56 -0
  63. package/dist/schema/generated-schema-staleness.d.ts +33 -0
  64. package/dist/schema/introspect-db-logic.d.ts +2 -0
  65. package/dist/schema/plan/diff-plan.d.ts +220 -0
  66. package/dist/schema/plan/plan-schema.d.ts +84 -0
  67. package/dist/schema/plan/render-ddl.d.ts +62 -0
  68. package/dist/schema/plan/render-drizzle.d.ts +31 -0
  69. package/dist/schema/plan/types.d.ts +410 -0
  70. package/dist/schema/plan/updated-at-trigger.d.ts +22 -0
  71. package/dist/schema/relation-names.d.ts +28 -0
  72. package/dist/schema/search-column.d.ts +9 -0
  73. package/dist/security/rls-enforcement.d.ts +17 -0
  74. package/dist/services/FetchService.d.ts +176 -36
  75. package/dist/services/PersistService.d.ts +17 -4
  76. package/dist/services/RelationService.d.ts +11 -3
  77. package/dist/services/collection-helpers.d.ts +16 -2
  78. package/dist/services/dataService.d.ts +29 -2
  79. package/dist/services/field-op-sql.d.ts +16 -0
  80. package/dist/services/field-viewer.d.ts +19 -0
  81. package/dist/services/realtimeService.d.ts +35 -1
  82. package/dist/services/row-pipeline.d.ts +29 -5
  83. package/dist/services/soft-delete.d.ts +73 -0
  84. package/dist/utils/pg-error-utils.d.ts +41 -1
  85. package/dist/{vector-index-DkWxm2de.js → vector-index-CMJKb3C9.js} +3 -14
  86. package/dist/{vector-index-DkWxm2de.js.map → vector-index-CMJKb3C9.js.map} +1 -1
  87. package/package.json +18 -10
  88. package/dist/cli-errors-DxN4yzdH.js.map +0 -1
  89. package/dist/doctor-D7HWmGzs.js.map +0 -1
  90. package/dist/ensure-collection-tables-D4wJzM4y.js.map +0 -1
  91. package/dist/generate-drizzle-schema-CwEgVyjO.js.map +0 -1
  92. package/dist/generate-drizzle-schema-logic-so2Z_lyC.js +0 -528
  93. package/dist/generate-drizzle-schema-logic-so2Z_lyC.js.map +0 -1
  94. package/dist/generate-postgres-ddl-logic-CtnpIBVv.js +0 -1312
  95. package/dist/generate-postgres-ddl-logic-CtnpIBVv.js.map +0 -1
  96. package/dist/rls-enforcement-DeYEjYLt.js.map +0 -1
@@ -214,6 +214,7 @@ async function applyAuthContext(tx, auth, userRole) {
214
214
  set_config('app.user_roles', ${normalizedRoles.join(",")}, true),
215
215
  set_config('app.is_anonymous', ${isAnonymous}, true),
216
216
  set_config('app.jwt', ${JSON.stringify({
217
+ ...auth.claims ?? {},
217
218
  sub: uid,
218
219
  roles: auth.roles,
219
220
  is_anonymous: auth.isAnonymous === true
@@ -339,4 +340,4 @@ async function validatePolicyPgRoles(run, collections, requestRole = REBASE_USER
339
340
  //#endregion
340
341
  export { rls_enforcement_exports as a, warnOnLegacyRlsFunctions as c, ensureAppRole as i, warnOnRoleSchemaCollision as l, applyAuthContext as n, validatePolicyPgRoles as o, detectConnectionPosture as r, warnOnAnonymousGrants as s, REBASE_USER_ROLE as t };
341
342
 
342
- //# sourceMappingURL=rls-enforcement-DeYEjYLt.js.map
343
+ //# sourceMappingURL=rls-enforcement-BV12vBwQ.js.map
@@ -0,0 +1 @@
1
+ {"version":3,"file":"rls-enforcement-BV12vBwQ.js","names":[],"sources":["../src/security/rls-enforcement.ts"],"sourcesContent":["import { sql as drizzleSql, SQL } from \"drizzle-orm\";\nimport { ANONYMOUS_USER_ID, PolicyExpression, SecurityRule } from \"@rebasepro/types\";\nimport {\n AnonymousGrantRisk,\n findAnonymousGrants,\n REBASE_USER_ROLE,\n revokeInternalTableAccess,\n securityRuleToConditions\n} from \"@rebasepro/common\";\nimport { REBASE_SCHEMA, usesLegacyRlsFunctions } from \"@rebasepro/types\";\nimport { logger } from \"@rebasepro/server\";\n\n/**\n * Unified RLS enforcement — the \"user context vs server context\" model.\n *\n * Every operation runs in one of two contexts:\n *\n * - **User context** — a request authenticated (or anonymous) via\n * `driver.withAuth(user)`. Runs as the restricted `rebase_user` role: a\n * non-owner, NOSUPERUSER, NOBYPASSRLS role, so Postgres RLS binds *every*\n * statement (SELECT, INSERT, UPDATE, DELETE). The collection's\n * `securityRules` are the whole authorization model; app-layer callbacks\n * are validation/side-effects, not a security boundary.\n *\n * - **Server context** — the base (owner) connection: auth flows, migrations,\n * and raw `rebase.sql`. As table owner it bypasses RLS. This is the trusted\n * plane, equivalent to Supabase's `service_role`.\n *\n * `rebase.dataAsAdmin` is **not** in it, despite the name. `init.ts` scopes\n * that driver with `withAuth(SERVICE_IDENTITY)`, so it arrives as user\n * context above — `rebase_user`, `app.uid = 'service'`, policies evaluated —\n * and clears the default policies through their admin arm rather than the\n * `rebase.uid() IS NULL` one.\n *\n * This module provides the three pieces:\n *\n * 1. {@link detectConnectionPosture} — is the connection subject to RLS at\n * all? (superuser / BYPASSRLS / table owner ⇒ no)\n * 2. {@link ensureAppRole} — idempotently provision `rebase_user` with\n * SELECT/INSERT/UPDATE/DELETE grants (+ default privileges so future\n * tables stay covered).\n * 3. {@link applyAuthContext} — per-transaction: set the `app.*` GUCs the\n * policies read (`rebase.uid()` etc.) and `SET LOCAL ROLE rebase_user` so\n * RLS binds. Transaction-scoped, so it composes with poolers.\n *\n * Provisioning runs from the framework's own bootstrap/migrate (which already\n * self-creates the `auth` schema and functions) — enforcement is default-on,\n * not an operator opt-in.\n */\n\n/**\n * The restricted role every authenticated (user-context) request runs as.\n *\n * Re-exported, not re-declared: the same name is needed by\n * `@rebasepro/common`'s internal-table revokes, and two spellings of a role name\n * fail as a silent no-op rather than an error.\n */\nexport { REBASE_USER_ROLE };\n\n/** Minimal SQL runner so callers can adapt drizzle or pg.Client. */\nexport type RawSqlRunner = (sqlText: string) => Promise<Record<string, unknown>[]>;\n\n/** Minimal transaction surface needed by {@link applyAuthContext}. */\nexport interface SqlTx {\n execute(query: SQL): Promise<unknown>;\n}\n\nexport interface ConnectionPosture {\n /** The connection's `current_user`. */\n role: string;\n superuser: boolean;\n bypassRLS: boolean;\n /**\n * Owns at least one user table — owners bypass non-FORCE RLS.\n *\n * \"Owns\" in Postgres's sense, not by name: a role that INHERITs the owner is\n * the owner for every ownership test the planner makes, including the one\n * that decides whether a policy applies at all.\n */\n ownsTables: boolean;\n /**\n * May create tables somewhere the server writes — and would then own them.\n *\n * The reason this is part of the posture rather than a separate question:\n * the posture is decided ONCE, at boot, and on a fresh database the\n * catalogue has nothing to report yet. The process that asks then goes on to\n * create every table in the schema and owns all of them for the rest of its\n * life. Asking only what is already there answers about the wrong instant.\n */\n canCreateTables: boolean;\n /** True when RLS would NOT constrain this connection. */\n privileged: boolean;\n}\n\nexport interface AuthContext {\n uid: string;\n /** Raw roles as carried on the user (strings or `{ id }` objects). */\n roles: unknown[];\n /**\n * Whether this session came from anonymous sign-in rather than an account.\n *\n * Anonymous sign-in mints a real user row and a real uid, so a guest was\n * indistinguishable from a registered account inside a policy: same\n * `rebase.uid()`, same default role. Every rule meaning \"a signed-in\n * person\" was therefore also a rule about anybody who had called\n * `POST /auth/anonymous`, which asks for no email, no password and no\n * agreement to anything.\n *\n * Optional, and absent reads as `false`: a caller that predates this — a\n * realtime subscription, a custom validator — keeps the behaviour it had\n * rather than having every one of its users reclassified as guests.\n */\n isAnonymous?: boolean;\n /**\n * The **custom** claims on the caller's token, as `rebase.jwt()` reports\n * them.\n *\n * This is how a policy reaches a fact about the session that is neither the\n * uid nor the roles — which, before this, it could not: `app.jwt` was built\n * from three fields the driver already had, so `rebase.jwt()`, documented\n * as \"full JWT claims as JSONB\", returned a three-key object that never had\n * a claim in it.\n *\n * Multi-tenancy's `claim` form is the first caller — `policy.authClaim(…)`\n * compiles to `rebase.jwt() ->> '<name>'`. Identity claims are stripped out\n * before they reach here (`AccessTokenPayload.claims`) and the identity\n * fields are written *over* them below, so nothing a hook put on a token\n * can assert a uid, roles or guest status.\n */\n claims?: Record<string, unknown>;\n}\n\nconst quoteIdent = (name: string): string => `\"${name.replace(/\"/g, \"\\\"\\\"\")}\"`;\n\n/** DML the user role holds on managed tables (RLS still filters per row). */\nconst USER_TABLE_PRIVILEGES = \"SELECT, INSERT, UPDATE, DELETE\";\n\n/**\n * Warn when the connection role shares its name with an existing schema.\n *\n * Postgres resolves unqualified names through `search_path`, which defaults to\n * `\"$user\", public` — and `$user` is the connection ROLE. When a schema of that\n * name exists it sits ahead of `public`, so every unqualified statement\n * silently operates on it instead:\n *\n * CREATE TABLE posts (...); -- you meant public.posts; you got <role>.posts\n *\n * Nothing errors. You get a second table of the same name in the wrong schema,\n * and reads that pin `public` cannot see it — which reads as \"missing table\" and\n * sends people to re-run a push that creates a *third* copy. The bootstrapper\n * has a whole branch dedicated to recognising the symptom after the fact.\n *\n * Rebase shipped straight into this: it creates a schema named `rebase` while\n * every template named the database role `rebase` too. The scaffold uses\n * `rebase_app` now, and every pool Rebase opens pins `search_path=public`\n * (`pinSearchPath`), which covers the paths the framework controls. This covers\n * the ones it does not — `psql`, `pg_dump`, drizzle-kit, a colleague's script,\n * a hand-written migration — because the hazard is a property of the two NAMES,\n * not of any one connection.\n *\n * A warning rather than a boot failure: the database works, the framework's own\n * traffic is pinned, and refusing to start over a naming choice a user may have\n * inherited would be worse than the risk.\n */\nexport async function warnOnRoleSchemaCollision(run: RawSqlRunner): Promise<void> {\n try {\n const rows = await run(`\n SELECT current_user AS role,\n EXISTS (\n SELECT 1 FROM pg_namespace n WHERE n.nspname = current_user\n ) AS collides\n `);\n if (rows[0]?.collides !== true) return;\n const role = String(rows[0]?.role ?? \"the connection role\");\n logger.warn(\n `⚠️ The database role \"${role}\" has the same name as a schema. Postgres resolves unqualified ` +\n `names through \\`search_path\\`, which defaults to \\`\"$user\", public\\` — so \"${role}\" is searched ` +\n `BEFORE public, and any unqualified \\`CREATE TABLE\\`/\\`SELECT\\` from a tool that does not pin the ` +\n `path (psql, pg_dump, drizzle-kit, a hand-written migration) silently lands in \"${role}\" instead. ` +\n `Rebase's own connections pin \\`search_path=public\\`, so the server is unaffected. To remove the ` +\n `hazard entirely, connect as a role whose name is not also a schema — the scaffold uses ` +\n `\"rebase_app\".`\n );\n } catch {\n // A diagnostic must never be the reason a boot fails.\n }\n}\n\n/**\n * Would RLS actually constrain this connection?\n *\n * Four ways the answer is no, and only two of them are obvious. Superuser and\n * BYPASSRLS announce themselves in `pg_roles`. The other two are about\n * ownership, and both used to read as \"no bypass here\":\n *\n * - **Ownership through membership.** The old query compared\n * `pg_tables.tableowner` to `current_user` as a string. Postgres does not: a\n * role that INHERITs the owner passes every ownership test the planner makes,\n * including the one that decides whether a policy applies. So a perfectly\n * ordinary \"app role that is a member of the owner role\" setup reported\n * unprivileged and bypassed every policy — permanently, since no restart\n * changes a string comparison's answer.\n *\n * - **Ownership that has not happened yet.** The posture is decided once, at\n * boot, and on a fresh database there is nothing in the catalogue to find. The\n * same process then creates every table and owns all of them. The first boot\n * of a new deployment therefore served every request unconstrained, the log\n * said RLS was fine, and the SECOND boot silently fixed it — which is the\n * worst possible shape for a security check, because the state that reproduces\n * it is gone by the time anyone looks. Asking \"may this role create tables\n * where the server provisions?\" answers about the process's whole life rather\n * than about one instant of it.\n *\n * `schemas` narrows the CREATE question to where the server actually writes.\n * Omitted, it asks about every schema the role can see, which is the safe\n * direction: over-reporting costs a role switch that was not strictly needed,\n * under-reporting costs the entire authorization model.\n */\nexport async function detectConnectionPosture(\n run: RawSqlRunner,\n schemas?: string[]\n): Promise<ConnectionPosture> {\n // Identifiers, and they reach SQL as literals in a catalogue comparison —\n // but they arrive from collection configs, so they are quoted as string\n // literals rather than interpolated as names, and a quote inside one is\n // escaped rather than trusted.\n const schemaFilter = schemas?.length\n ? `AND n.nspname IN (${Array.from(new Set(schemas))\n .map((s) => `'${s.replace(/'/g, \"''\")}'`)\n .join(\", \")})`\n : \"\";\n\n const rows = await run(`\n SELECT current_user AS role,\n r.rolsuper AS superuser,\n r.rolbypassrls AS bypassrls,\n EXISTS (\n SELECT 1 FROM pg_tables t\n WHERE pg_has_role(current_user, t.tableowner, 'USAGE')\n AND t.schemaname NOT IN ('pg_catalog', 'information_schema')\n ) AS owns_tables,\n EXISTS (\n SELECT 1 FROM pg_namespace n\n WHERE n.nspname NOT IN ('pg_catalog', 'information_schema')\n AND n.nspname NOT LIKE 'pg\\\\_toast%'\n AND n.nspname NOT LIKE 'pg\\\\_temp%'\n ${schemaFilter}\n AND has_schema_privilege(current_user, n.oid, 'CREATE')\n ) AS can_create\n FROM pg_roles r\n WHERE r.rolname = current_user\n `);\n const row = rows[0] ?? {};\n const superuser = row.superuser === true;\n const bypassRLS = row.bypassrls === true;\n const ownsTables = row.owns_tables === true;\n const canCreateTables = row.can_create === true;\n return {\n role: String(row.role ?? \"unknown\"),\n superuser,\n bypassRLS,\n ownsTables,\n canCreateTables,\n privileged: superuser || bypassRLS || ownsTables || canCreateTables\n };\n}\n\n/**\n * Human-actionable instructions for when the connection cannot provision the\n * user role itself (no CREATEROLE and role not pre-created by the platform).\n */\nexport function appRoleSetupInstructions(connectionRole: string, schemas: string[]): string {\n const grants = schemas.map((s) =>\n `GRANT USAGE ON SCHEMA ${quoteIdent(s)} TO ${REBASE_USER_ROLE};\\n` +\n `GRANT ${USER_TABLE_PRIVILEGES} ON ALL TABLES IN SCHEMA ${quoteIdent(s)} TO ${REBASE_USER_ROLE};\\n` +\n `GRANT USAGE, SELECT ON ALL SEQUENCES IN SCHEMA ${quoteIdent(s)} TO ${REBASE_USER_ROLE};`\n ).join(\"\\n\");\n return (\n `Rebase enforces row-level security by running authenticated requests as ` +\n `the restricted role \"${REBASE_USER_ROLE}\", but the connection role ` +\n `\"${connectionRole}\" bypasses RLS and cannot create that role itself.\\n` +\n `Run the following as a database administrator, then restart:\\n\\n` +\n `CREATE ROLE ${REBASE_USER_ROLE} NOLOGIN NOSUPERUSER NOBYPASSRLS NOINHERIT;\\n` +\n `GRANT ${REBASE_USER_ROLE} TO ${quoteIdent(connectionRole)};\\n` +\n grants\n );\n}\n\n/**\n * Idempotently provision the `rebase_user` role, membership for the current\n * connection role, and DML grants (+ default privileges for future tables)\n * on every existing schema in `schemas`.\n *\n * Split into privilege tiers so it works both when the connection is a\n * superuser (creates everything) and when the platform pre-created the role\n * and membership (e.g. CNPG `postInitApplicationSQL`) and the connection is\n * merely the table owner — owners can always run the grant tier themselves.\n *\n * RLS still filters every row: these grants only make the tables *reachable*\n * by the role; the policies decide which rows/commands actually pass.\n *\n * Throws with precise setup instructions when the role is missing and the\n * connection cannot create it.\n */\nexport async function ensureAppRole(run: RawSqlRunner, schemas: string[]): Promise<void> {\n const uniqueSchemas = Array.from(new Set(schemas.filter(Boolean)));\n\n // Tier 1 — role existence.\n const roleRows = await run(`SELECT 1 FROM pg_roles WHERE rolname = '${REBASE_USER_ROLE}'`);\n if (roleRows.length === 0) {\n try {\n await run(`CREATE ROLE ${REBASE_USER_ROLE} NOLOGIN NOSUPERUSER NOBYPASSRLS NOINHERIT`);\n } catch (err) {\n throw new Error(\n `Failed to create the \"${REBASE_USER_ROLE}\" role: ${err instanceof Error ? err.message : String(err)}\\n\\n` +\n appRoleSetupInstructions(\"current connection role\", uniqueSchemas)\n );\n }\n }\n\n // Tier 2 — membership, so a non-superuser connection may SET ROLE to it.\n const memberRows = await run(`\n SELECT (pg_has_role(current_user, '${REBASE_USER_ROLE}', 'MEMBER')\n OR (SELECT rolsuper FROM pg_roles WHERE rolname = current_user)) AS can_set,\n current_user AS role\n `);\n if (memberRows[0]?.can_set !== true) {\n try {\n await run(`GRANT ${REBASE_USER_ROLE} TO CURRENT_USER`);\n } catch (err) {\n throw new Error(\n `The connection role is not a member of \"${REBASE_USER_ROLE}\" and cannot grant itself membership: ` +\n `${err instanceof Error ? err.message : String(err)}\\n\\n` +\n appRoleSetupInstructions(String(memberRows[0]?.role ?? \"current connection role\"), uniqueSchemas)\n );\n }\n }\n\n // Tier 3 — grants. Table owners (the expected non-superuser posture) can\n // always grant on their own objects, so this tier needs no extra privilege.\n const nspRows = await run(\"SELECT nspname FROM pg_namespace\");\n const existing = new Set(nspRows.map((r) => String(r.nspname)));\n for (const schema of uniqueSchemas) {\n if (!existing.has(schema)) continue;\n const s = quoteIdent(schema);\n await run(`GRANT USAGE ON SCHEMA ${s} TO ${REBASE_USER_ROLE}`);\n await run(`GRANT ${USER_TABLE_PRIVILEGES} ON ALL TABLES IN SCHEMA ${s} TO ${REBASE_USER_ROLE}`);\n await run(`GRANT USAGE, SELECT ON ALL SEQUENCES IN SCHEMA ${s} TO ${REBASE_USER_ROLE}`);\n // Cover objects created later by the CURRENT role (the role that runs\n // migrations), so a migrate can never strand the user role.\n await run(`ALTER DEFAULT PRIVILEGES IN SCHEMA ${s} GRANT ${USER_TABLE_PRIVILEGES} ON TABLES TO ${REBASE_USER_ROLE}`);\n await run(`ALTER DEFAULT PRIVILEGES IN SCHEMA ${s} GRANT USAGE, SELECT ON SEQUENCES TO ${REBASE_USER_ROLE}`);\n\n // The grants above are deliberately schema-wide — a project's own\n // collections may live in `rebase`, and future tables must be reachable\n // or a migration strands the role. Rebase's OWN tables are the exception:\n // refresh tokens, MFA secrets, API keys and the rest carry no RLS and no\n // row an end user should ever address. Taking the privilege back here\n // covers every table that already exists; each creator revokes on the\n // table it just made, for the boot that creates them for the first time.\n await revokeInternalTableAccess(async (text) => { await run(text); }, schema, {\n onError: (table, error) => logger.warn(\n `🔐 [rls] Could not revoke \"${REBASE_USER_ROLE}\" access to \"${schema}\".\"${table}\" — ` +\n \"it stays reachable by authenticated requests: \" +\n (error instanceof Error ? error.message : String(error))\n )\n });\n }\n\n logger.debug(`🔐 [rls] User role \"${REBASE_USER_ROLE}\" provisioned (schemas: ${uniqueSchemas.join(\", \")})`);\n}\n\n/**\n * Apply the authenticated context to a transaction: the `app.*` GUCs that RLS\n * policies read via `rebase.uid()` / `rebase.roles()` / `rebase.jwt()`, and — when\n * `userRole` is set — `SET LOCAL ROLE` so RLS binds every statement in this\n * transaction (reads *and* writes).\n *\n * GUCs are set with `is_local = true` and the role switch is `LOCAL`: both\n * reset at commit/rollback, so pooled connections are never polluted.\n *\n * Fails closed by construction: if the role switch errors, the transaction\n * aborts instead of proceeding privileged.\n *\n * SECURITY: this function is only ever called on the **user** path (the server\n * context uses the base/owner driver and never calls it). The default policies\n * treat `rebase.uid() IS NULL` as the trusted server context, and `rebase.uid()`\n * is `NULLIF(current_setting('app.uid'), '')` — so an EMPTY user id would\n * be read as NULL and silently escalate a user request to server privileges.\n * Coerce empty/blank ids to `ANONYMOUS_USER_ID` here, at the single chokepoint,\n * rather than trusting every caller (e.g. realtime subscription auth) to do it.\n * That sentinel is exported from `@rebasepro/types` because it leaks into rule\n * semantics: it is why `rebase.uid() IS NOT NULL` is true for anonymous requests.\n */\nexport async function applyAuthContext(tx: SqlTx, auth: AuthContext, userRole?: string): Promise<void> {\n const uid = typeof auth.uid === \"string\" && auth.uid.trim() !== \"\" ? auth.uid : ANONYMOUS_USER_ID;\n const normalizedRoles = auth.roles.map((r: unknown) =>\n typeof r === \"string\" ? r : (r as Record<string, unknown>)?.id ?? String(r)\n );\n // `app.user_id` is the pre-rename spelling, still written because policies\n // are data: a database provisioned before the rename holds rules compiled\n // to `current_setting('app.user_id')`, and those predicates would evaluate\n // to NULL — failing open or locking out — if we stopped setting it. Drop\n // the alias only once no live database carries a legacy policy.\n // A guest is a signed-in caller with no account behind them, and until this\n // the database could not tell the difference — see `rebase.is_anonymous()`.\n // Written as a string because that is what `set_config` takes.\n const isAnonymous = auth.isAnonymous === true ? \"true\" : \"false\";\n\n await tx.execute(drizzleSql`\n SELECT\n set_config('app.uid', ${uid}, true),\n set_config('app.user_id', ${uid}, true),\n set_config('app.user_roles', ${normalizedRoles.join(\",\")}, true),\n set_config('app.is_anonymous', ${isAnonymous}, true),\n set_config('app.jwt', ${JSON.stringify({\n // The caller's custom claims, FIRST, so the identity keys below\n // overwrite them rather than the other way round. Same ordering, and\n // the same reason, as `generateAccessToken`: a claims hook is a place\n // to add facts about a session, not the place its subject is decided.\n // `verifyAccessToken` already strips the identity claims out of this\n // object; the ordering is the second lock on the same door.\n ...(auth.claims ?? {}),\n sub: uid,\n roles: auth.roles,\n // In the claims too, so a policy reading `rebase.jwt()` — which is\n // how a rule reaches anything that is not uid or roles — sees the\n // same fact as `rebase.is_anonymous()`. Two sources that could\n // disagree would be worse than one.\n is_anonymous: auth.isAnonymous === true\n })}, true)\n `);\n if (userRole) {\n await tx.execute(drizzleSql.raw(`SET LOCAL ROLE ${quoteIdent(userRole)}`));\n }\n}\n\n/** Role names from other BaaS platforms that people reach for out of habit. */\nconst FOREIGN_CONVENTION_ROLES: Record<string, string> = {\n authenticated: \"Supabase\",\n anon: \"Supabase\",\n service_role: \"Supabase\"\n};\n\n/**\n * Warn about rules that read as \"signed-in users only\" but admit anonymous\n * callers — `rebase.uid() IS NOT NULL`, or a comparison against another\n * platform's magic user id such as `'anon'`.\n *\n * The sibling of {@link validatePolicyPgRoles}, for the more dangerous spelling\n * of the same habit. A foreign `pgRoles` value makes a policy unreachable and\n * the table reads empty — loud, and that guard throws. These do the opposite:\n * the rule compiles to a grant, and nothing looks wrong until the data is\n * already public.\n *\n * Warns rather than throws. Unlike an unreachable `pgRoles`, these rules are\n * serving traffic today: refusing to boot would take an app offline to report a\n * problem it already has, and on the read path it would take it offline\n * *because* its data was exposed. Rewriting the author's SQL is not an option\n * either — this is the escape hatch whose whole promise is that it means what it\n * says. So: say so, loudly, and leave the rule alone.\n */\nexport function warnOnAnonymousGrants(\n collections: { slug?: string; securityRules?: readonly SecurityRule[] }[]\n): void {\n // Grouped by the mistake, not by the rule: one habit typically repeats\n // across every collection an author wrote, and a per-rule list would repeat\n // the same paragraph dozens of times and get skimmed.\n const byRisk = new Map<string, { risk: AnonymousGrantRisk; sites: string[] }>();\n\n for (const collection of collections) {\n for (const rule of collection.securityRules ?? []) {\n const { usingExpr, withCheckExpr } = securityRuleToConditions(rule);\n const risks = [usingExpr, withCheckExpr]\n .filter((e): e is PolicyExpression => e !== null)\n .flatMap(findAnonymousGrants);\n\n for (const risk of risks) {\n const key = `${risk.pattern}:${risk.detail}`;\n const site = `${collection.slug ?? \"(unnamed)\"} → \"${rule.name ?? \"(unnamed rule)\"}\"`;\n const entry = byRisk.get(key) ?? { risk, sites: [] };\n if (!entry.sites.includes(site)) entry.sites.push(site);\n byRisk.set(key, entry);\n }\n }\n }\n\n if (byRisk.size === 0) return;\n\n const problems = [...byRisk.values()].map(({ risk, sites }) =>\n ` • ${risk.explanation}\\n ${sites.length} rule(s): ${sites.join(\", \")}`\n );\n\n logger.warn(\n `Security rules that read as a lockdown but grant access to anonymous requests. Every caller from a ` +\n `client carries a user id ('${ANONYMOUS_USER_ID}' when nobody is signed in), so these clauses are ` +\n `true for everyone:\\n\\n` +\n problems.join(\"\\n\\n\") + \"\\n\"\n );\n}\n\n/**\n * Name the collections whose raw policy SQL still calls the pre-1.0 helpers.\n *\n * The compiler rewrites `auth.uid()` to `rebase.uid()` on the way into the\n * database, so nothing is broken and no policy is wrong — which is exactly why\n * this has to be said out loud. A silent rewrite that works forever is not a\n * migration, it is a second supported spelling nobody wrote down, and the next\n * person to read those rules will copy the old one.\n *\n * Only `raw` expressions can carry it. Structured rules (`policy.authUid()`,\n * `policy.rolesOverlap(...)`) compile from the model and were never affected.\n */\nexport function warnOnLegacyRlsFunctions(\n collections: { slug?: string; securityRules?: readonly SecurityRule[] }[]\n): void {\n const sites: string[] = [];\n\n for (const collection of collections) {\n for (const rule of collection.securityRules ?? []) {\n const { usingExpr, withCheckExpr } = securityRuleToConditions(rule);\n const carriesLegacy = [usingExpr, withCheckExpr]\n .filter((e): e is PolicyExpression => e !== null)\n .some(containsLegacyRlsCall);\n if (!carriesLegacy) continue;\n\n const site = `${collection.slug ?? \"(unnamed)\"} → \"${rule.name ?? \"(unnamed rule)\"}\"`;\n if (!sites.includes(site)) sites.push(site);\n }\n }\n\n if (sites.length === 0) return;\n\n logger.warn(\n `These security rules call the pre-1.0 RLS helpers (\\`auth.uid()\\`, \\`auth.roles()\\`, \\`auth.jwt()\\`). ` +\n `They still work — the compiler rewrites them — but the functions now live in the \\`rebase\\` schema, ` +\n `and the \\`auth\\` one is Supabase's. Update the raw SQL in these rules to \\`${REBASE_SCHEMA}.uid()\\` ` +\n `and friends, or switch them to the structured helpers (\\`policy.authUid()\\`, \\`policy.rolesOverlap()\\`), ` +\n `which never had to be spelled by hand:\\n\\n` +\n sites.map(s => ` • ${s}`).join(\"\\n\") + \"\\n\"\n );\n}\n\n/** Whether any `raw` expression in the tree calls a pre-1.0 helper. */\nfunction containsLegacyRlsCall(expr: PolicyExpression): boolean {\n switch (expr.kind) {\n case \"raw\":\n return usesLegacyRlsFunctions(expr.sql);\n case \"and\":\n case \"or\":\n return expr.operands.some(containsLegacyRlsCall);\n case \"not\":\n return containsLegacyRlsCall(expr.operand);\n case \"existsIn\":\n return containsLegacyRlsCall(expr.where);\n default:\n return false;\n }\n}\n\n/**\n * Reject `pgRoles` that this server can never satisfy.\n *\n * `pgRoles` sets the `TO` clause of a generated policy, so a policy naming a\n * role the request never runs as simply never applies — and RLS then filters\n * every row. The table reads as empty, which is indistinguishable from having\n * no data, so the mistake survives review and ships.\n *\n * Requests run as `rebase_user`, so a policy is only reachable if it targets\n * `public` or a role `rebase_user` holds. Anything else is a configuration\n * error worth failing the boot for.\n */\nexport async function validatePolicyPgRoles(\n run: RawSqlRunner,\n collections: { slug?: string; securityRules?: readonly { name?: string; pgRoles?: readonly string[] }[] }[],\n /** The role requests actually run as: `rebase_user` when the connection is\n * privileged enough to switch, otherwise the connection role itself. */\n requestRole: string = REBASE_USER_ROLE\n): Promise<void> {\n const wanted = new Map<string, string[]>();\n for (const collection of collections) {\n for (const rule of collection.securityRules ?? []) {\n for (const role of rule.pgRoles ?? []) {\n if (role === \"public\") continue;\n wanted.set(role, [...(wanted.get(role) ?? []), collection.slug ?? \"(unnamed)\"]);\n }\n }\n }\n if (wanted.size === 0) return;\n\n const names = [...wanted.keys()].map((r) => `'${r.replace(/'/g, \"''\")}'`).join(\",\");\n const escapedRequestRole = requestRole.replace(/'/g, \"''\");\n const rows = await run(`\n SELECT r.rolname AS role,\n COALESCE(pg_has_role(to_regrole('${escapedRequestRole}'), r.oid, 'MEMBER'), false) AS reachable\n FROM pg_roles r\n WHERE r.rolname IN (${names})\n `);\n\n const reachable = new Map(rows.map((row) => [String(row.role), row.reachable === true]));\n const problems: string[] = [];\n\n for (const [role, slugs] of wanted) {\n if (reachable.get(role) === true) continue;\n\n const why = reachable.has(role)\n ? `\"${requestRole}\" is not a member of it`\n : \"no such role exists in this database\";\n const platform = FOREIGN_CONVENTION_ROLES[role];\n const hint = platform\n ? `\"${role}\" is a ${platform} convention, not a PostgreSQL role. Application roles belong in \\`roles: [\"${role === \"service_role\" ? \"admin\" : role}\"]\\`, which is checked inside the policy via rebase.roles().`\n : `Either grant it (GRANT ${role} TO ${requestRole}) or drop \\`pgRoles\\` so the policy targets \\`public\\`.`;\n\n problems.push(\n ` • pgRoles: [\"${role}\"] on ${slugs.join(\", \")} — ${why}.\\n ${hint}`\n );\n }\n\n if (problems.length > 0) {\n throw new Error(\n `Security rules target PostgreSQL roles this server cannot use. Requests run as ` +\n `\"${requestRole}\", so these policies would never apply and every row would be ` +\n `filtered out — the collections would look empty rather than error.\\n\\n` +\n problems.join(\"\\n\\n\") + \"\\n\"\n );\n }\n}\n"],"mappings":"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"}
@@ -0,0 +1,97 @@
1
+ /**
2
+ * The runtime's Drizzle schema, read back from the database it is about to
3
+ * serve.
4
+ *
5
+ * This is the whole point of the module: **the live catalogue is the schema**.
6
+ * Boot-ensure has already created and altered whatever the collections describe
7
+ * by the time this runs, so `information_schema` is both current and complete,
8
+ * and the committed `schema.generated.ts` — which is a build artefact for Atlas,
9
+ * `db push`, `eject` and user code — never decides what the server serves.
10
+ *
11
+ * It used to. The default data source was handed the bundle's generated module
12
+ * and nothing reconciled it, so a file one commit behind the database changed
13
+ * behaviour in ways that looked nothing like "your schema file is old":
14
+ *
15
+ * - a foreign-key column renamed by boot-ensure (`categorie_id` → `category_id`)
16
+ * left every request 500ing, because the module still declared the old name;
17
+ * - a property added to a collection answered `400 VALIDATION_UNKNOWN_FIELDS`
18
+ * on first save, because the write check reads the table and the table came
19
+ * from the file, while the column itself existed;
20
+ * - relations missing from the module were silently missing from list reads.
21
+ *
22
+ * The catalogue cannot drift from itself. What the generated module is still
23
+ * good for is telling the developer their file is stale — see
24
+ * `generated-schema-diff.ts`, which is now its only reader at boot.
25
+ */
26
+ import type { PgTable } from "drizzle-orm/pg-core";
27
+ import type { Relations } from "drizzle-orm";
28
+ import { CollectionConfig } from "@rebasepro/types";
29
+ import { type ColumnKeyResolver } from "./dynamic-tables.js";
30
+ import { type Queryable } from "./introspect-runtime.js";
31
+ import type { TableMeta } from "./introspect-db-logic.js";
32
+ /** The Postgres schema a collection's table lives in. */
33
+ export declare function schemaOfCollection(collection: CollectionConfig, fallback: string): string;
34
+ /**
35
+ * How a column of each table is keyed in the Drizzle object.
36
+ *
37
+ * The wire name, which is the property key — `authorId`, never `author_id` —
38
+ * because that is what every read and write path in this driver indexes tables
39
+ * and rows by, and what the generated module has always emitted. Getting this
40
+ * wrong does not throw: drizzle leaves an unknown key out of the statement and
41
+ * the write reports success having stored nothing.
42
+ *
43
+ * Two exceptions, both columns no property declares:
44
+ *
45
+ * - the generated search columns keep their raw names, because `search_vector`
46
+ * is how `searchColumnNames` excludes them from responses and how the
47
+ * condition builder finds the one to match against;
48
+ * - a table no collection claims (a junction) is keyed by its **key** columns'
49
+ * names, which is also what the generated file does for them — but a
50
+ * junction that declares `through.properties` keys those payload columns by
51
+ * their property key, for the same reason a collection does. `joinedAt` with
52
+ * `columnName: "joined_at"` has to be one name on both sides or a `_pivot`
53
+ * write is silently dropped by drizzle and the read serves a key the caller
54
+ * never declared.
55
+ */
56
+ export declare function columnKeysFromCollections(collections: CollectionConfig[]): ColumnKeyResolver;
57
+ export interface CatalogueSchema {
58
+ /** Drizzle tables, keyed by bare table name. */
59
+ tables: Record<string, PgTable>;
60
+ /** Drizzle relations, keyed `<table>Relations`. */
61
+ relations: Record<string, Relations>;
62
+ /** Introspected metadata per table — what the boot diff compares against. */
63
+ meta: Map<string, TableMeta>;
64
+ /** Tables a collection declares that the database does not have. */
65
+ missing: string[];
66
+ }
67
+ /**
68
+ * Read the tables this data source serves out of `information_schema`.
69
+ *
70
+ * Restricted to the collections handed in, because the driver is given every
71
+ * collection the project declares while each database holds only its own: an
72
+ * unfiltered read had a second source (`database("analytics")`) reporting the
73
+ * default's tables as its own. Junction tables come too — they back no
74
+ * collection and a many-to-many is unreadable without them.
75
+ *
76
+ * Every schema the collections name is read, not just `public`. The users
77
+ * collection lives in `rebase`, and a run restricted to `public` would leave the
78
+ * registry with no table for it at all.
79
+ */
80
+ export declare function readCatalogueSchema(options: {
81
+ client: Queryable;
82
+ collections: CollectionConfig[];
83
+ defaultSchema: string;
84
+ }): Promise<CatalogueSchema>;
85
+ /**
86
+ * The BaaS variant: no collections are declared, so every table in one schema is
87
+ * served and both the collections and the relations are derived from the
88
+ * foreign keys.
89
+ *
90
+ * Kept apart from {@link readCatalogueSchema} because the two answer different
91
+ * questions. Here the database is the entire specification; there it supplies
92
+ * the columns for a specification the project already wrote down.
93
+ */
94
+ export declare function buildBaasSchema(tablesMap: Map<string, TableMeta>, pgSchemaName: string, collections: CollectionConfig[]): {
95
+ tables: Record<string, PgTable>;
96
+ relations: Record<string, Relations>;
97
+ };
@@ -180,3 +180,12 @@ export declare const buildCollectionIndexSpecs: (collection: CollectionConfig, r
180
180
  * depend on the order files happened to load in.
181
181
  */
182
182
  export declare const buildCollectionIndexPlan: (collections: readonly CollectionConfig[], resolveColumnName: ResolveColumnName) => CollectionIndexSpec[];
183
+ /**
184
+ * The stable order every consumer of a set of index specs uses.
185
+ *
186
+ * Exported because the boot-time diff reads the specs off the schema plan
187
+ * rather than rebuilding them, and it has to reach the same order the CLI's
188
+ * exclude list and `schema.sql` do — a second sort written out beside this one
189
+ * is a second order the moment either is edited.
190
+ */
191
+ export declare const sortIndexSpecs: (specs: readonly CollectionIndexSpec[]) => CollectionIndexSpec[];
@@ -0,0 +1,125 @@
1
+ /**
2
+ * The questions about a property that the schema planner asks first.
3
+ *
4
+ * What is the primary key, what is this column called, what does an id default
5
+ * to, what labels does this enum have. `schema/plan/plan-schema.ts` is the only
6
+ * caller — it is the only reader of a `Property` — and these live beside it
7
+ * rather than inside it because they are also where the *refusals* are, and a
8
+ * refusal wants to be reachable and testable on its own.
9
+ *
10
+ * They were extracted while there were still three emitters, each carrying its
11
+ * own verbatim copy of all four rules until one of them was edited: the audit
12
+ * that produced this module counted twelve open disagreements and found that a
13
+ * third of the commits touching those files were re-synchronisations. The
14
+ * emitters are renderers now and there is nothing left to keep in step, but the
15
+ * questions are still questions about a property rather than about a plan, so
16
+ * this is where they belong.
17
+ *
18
+ * Everything here is pure and synchronous, and it throws rather than guesses:
19
+ * an enum with no values, a `cuid` id on Postgres and a second `isId` are all
20
+ * configurations that produce a broken database in silence, so they are refused
21
+ * where every path passes.
22
+ */
23
+ import type { CollectionConfig, Property } from "@rebasepro/types";
24
+ /**
25
+ * Resolve the SQL column name for a property.
26
+ *
27
+ * Uses the explicit `columnName` when set (e.g. from introspection), falling
28
+ * back to `toSnakeCase(propName)` for manually-authored collections.
29
+ */
30
+ export declare const resolveColumnName: (propName: string, prop?: Property | null) => string;
31
+ /**
32
+ * Refuse a collection that declares two primary keys.
33
+ *
34
+ * Postgres allows a composite primary key; Rebase does not model one, and the
35
+ * three emitters each invented a different wrong answer for it. The Drizzle
36
+ * generator appended `.primaryKey()` to both columns (two primary keys, which
37
+ * drizzle-kit turns into an error at push time), the DDL generator wrote two
38
+ * inline `PRIMARY KEY` clauses in one `CREATE TABLE` (Postgres: "multiple
39
+ * primary keys for table are not allowed"), and boot-ensure created the table
40
+ * with the *first* id column and silently never added the second — so a managed
41
+ * tenant got a table missing a column its own collection reads.
42
+ *
43
+ * None of those is recoverable at runtime, and two of them fail long after the
44
+ * config was written. Refused here instead, where the collection has a name.
45
+ */
46
+ export declare const assertSinglePrimaryKey: (collection: CollectionConfig) => void;
47
+ export declare const getPrimaryKeyProp: (collection: CollectionConfig) => {
48
+ name: string;
49
+ type: "string" | "number";
50
+ isUuid: boolean;
51
+ };
52
+ export declare const getPrimaryKeyName: (collection: CollectionConfig) => string;
53
+ export declare const isIdProperty: (propName: string, prop: Property, collection: CollectionConfig) => boolean;
54
+ /**
55
+ * The labels an `enum` declares, in order, whichever form it was written in.
56
+ *
57
+ * `EnumValues` is `EnumValueConfig[] | Record<id, label>` and the docs recommend
58
+ * the record. Boot-ensure read only the array form — `(p.enum as unknown[]).map`
59
+ * — so a record-form enum threw `p.enum.map is not a function` on a managed
60
+ * tenant, at boot, with no developer in the loop. The other two emitters each
61
+ * carried their own copy of the both-forms walk.
62
+ *
63
+ * An empty list throws rather than returning `[]`. `CREATE TYPE … AS ENUM ()`
64
+ * is not valid SQL, so every emitter "handled" it by skipping the type and then
65
+ * typing the column with it anyway: Drizzle referenced an enum variable it never
66
+ * declared (the generated file does not compile), the DDL named a type nothing
67
+ * creates, and boot-ensure's `ADD COLUMN` failed — and add-column is not a
68
+ * survivable action, so the boot died. There is no useful column at the end of
69
+ * any of those paths.
70
+ */
71
+ export declare const enumLabelsOf: (propName: string, prop: Property, collection: CollectionConfig) => string[];
72
+ /**
73
+ * Does this property compile to a Postgres enum *type*?
74
+ *
75
+ * Strings only. A `number` property with an `enum` used to create a type on all
76
+ * three paths and then declare the column `NUMERIC`/`INTEGER`, so the type was
77
+ * orphaned the moment it was created and the values it listed were enforced by
78
+ * nothing. The dropdown in the panel is driven by the config, not the column, so
79
+ * nothing is lost by not creating it — and a type no column uses is one more
80
+ * object `db push` has to plan a DROP for.
81
+ */
82
+ export declare const declaresEnumType: (prop: Property) => boolean;
83
+ /**
84
+ * The `DEFAULT` an id column carries, as the thing itself rather than a
85
+ * rendered string — each emitter spells it differently.
86
+ *
87
+ * The three of them disagreed on all four strategies. The DDL generator wrote
88
+ * `DEFAULT ${prop.isId}` verbatim, so the documented `` isId: "sql`gen_id()`" ``
89
+ * reached Postgres with the template wrapper still on it — a syntax error in
90
+ * `schema.sql`. Boot-ensure gave a default to `uuid` and to nothing else, so on
91
+ * a managed tenant every other strategy produced a column with no default at
92
+ * all, and the first insert that trusted the config failed on a NULL primary
93
+ * key. Only the Drizzle generator stripped the wrapper.
94
+ */
95
+ export type IdColumnDefault =
96
+ /** `gen_random_uuid()` / `.defaultRandom()`. */
97
+ {
98
+ kind: "uuid";
99
+ }
100
+ /** `GENERATED BY DEFAULT AS IDENTITY` / `.generatedByDefaultAsIdentity()` — part of the type, not a DEFAULT. */
101
+ | {
102
+ kind: "identity";
103
+ }
104
+ /** A raw SQL expression, wrapper already stripped. */
105
+ | {
106
+ kind: "sql";
107
+ expression: string;
108
+ };
109
+ /**
110
+ * Refuses `isId: "cuid"`.
111
+ *
112
+ * `DEFAULT cuid()` has been emitted since the option existed and no `cuid()`
113
+ * function has ever been created — not by the generators, not by boot, not by
114
+ * any migration. So the column has never had a working default on Postgres: the
115
+ * first insert that relied on it failed with `function cuid() does not exist`.
116
+ * There is nothing to keep compatible.
117
+ *
118
+ * It is refused rather than implemented. Shipping a real `rebase.cuid()` means a
119
+ * new SQL function in the same carved-out class as the search helpers — excluded
120
+ * from Atlas by pattern, applied by boot, re-applied by push, checked by the
121
+ * doctor, and versioned across the fleet forever — to reproduce a monotonic id
122
+ * that `uuid` already provides and that a project wanting cuid's exact shape can
123
+ * write itself in one line of SQL.
124
+ */
125
+ export declare const idColumnDefault: (propName: string, prop: Property, collection: CollectionConfig) => IdColumnDefault | undefined;
@@ -0,0 +1,32 @@
1
+ /**
2
+ * Build drizzle `relations()` for declared collections, from the collections.
3
+ *
4
+ * The heuristic builder in `dynamic-tables.ts` names a relation after the
5
+ * foreign-key column (`author_id` → `author`), which is the only evidence a
6
+ * database offers and is right for BaaS mode. It is wrong for a project that
7
+ * declares its collections: there the relation's name is whatever the author
8
+ * called it, `include=writer` addresses that name, and a relation whose key was
9
+ * guessed from the column simply does not answer to it — the read returns the
10
+ * bare foreign key and no target, silently, which is how a demo lost every
11
+ * relation in its list view while the detail view (a different code path) kept
12
+ * working.
13
+ *
14
+ * So when there are collections, they are the authority for *which* relations
15
+ * exist and what they are called; the live catalogue is the authority for the
16
+ * columns underneath. This is the same pairing `generate-drizzle-schema-logic`
17
+ * emits into `schema.generated.ts` — kind for kind, key for key, and paired by
18
+ * the same {@link sharedRelationName} — only built against tables read back from
19
+ * `information_schema` rather than against a file.
20
+ */
21
+ import { type Relations } from "drizzle-orm";
22
+ import type { PgTable } from "drizzle-orm/pg-core";
23
+ import { CollectionConfig } from "@rebasepro/types";
24
+ /** A collection's table, with any schema prefix stripped — as the DDL creates it. */
25
+ export declare function bareTableName(name: string): string;
26
+ /**
27
+ * Drizzle relations for every declared collection, keyed `<table>Relations`.
28
+ *
29
+ * @param collections every collection backed by a table in `tables`
30
+ * @param tables catalogue-built tables, keyed by bare table name
31
+ */
32
+ export declare function buildDrizzleRelationsFromCollections(collections: CollectionConfig[], tables: Record<string, PgTable>): Record<string, Relations>;
@@ -1,11 +1,11 @@
1
1
  #!/usr/bin/env node
2
2
  import { createRequire as __createRequire } from "module";
3
3
  __createRequire(import.meta.url);
4
- import { i as diagnoseDbError } from "../cli-errors-DxN4yzdH.js";
5
- import { o as validatePolicyPgRoles, s as warnOnAnonymousGrants } from "../rls-enforcement-DeYEjYLt.js";
4
+ import { i as diagnoseDbError } from "../cli-errors-C3g_kHBw.js";
5
+ import { o as validatePolicyPgRoles, s as warnOnAnonymousGrants } from "../rls-enforcement-BV12vBwQ.js";
6
6
  import { t as chalk } from "../source-Br7L7GOI.js";
7
- import { n as loadCollections, r as runDoctor } from "../doctor-D7HWmGzs.js";
8
- import { n as formatPolicyDrift, r as hasDrift, t as checkPolicyDrift } from "../policy-drift-kNJ6X2Uj.js";
7
+ import { n as loadCollections, r as runDoctor } from "../doctor-D5SrGJ5P.js";
8
+ import { n as formatPolicyDrift, r as hasDrift, t as checkPolicyDrift } from "../policy-drift-B0GDRh7_.js";
9
9
  import path from "path";
10
10
  //#region src/schema/doctor-policy-checks.ts
11
11
  /**
@@ -65,6 +65,15 @@ export interface DoctorReport {
65
65
  errors: number;
66
66
  };
67
67
  }
68
+ /**
69
+ * The `data_type` the database should report for a property's column, or `null`
70
+ * when there is nothing to compare.
71
+ *
72
+ * `null` covers the two honest cases: a `relation`, whose column is derived from
73
+ * the relation rather than from the property, and a configuration the planner
74
+ * refuses outright — which is a problem the doctor's other phases report with a
75
+ * message, not a type mismatch to invent one for.
76
+ */
68
77
  export declare function getExpectedColumnType(prop: Property): string | null;
69
78
  /**
70
79
  * Re-exported so callers keep importing it from here, but the implementation is
@@ -1,22 +1,43 @@
1
1
  /**
2
2
  * Build drizzle tables at runtime from an introspected schema.
3
3
  *
4
- * A project with declared collections gets its drizzle tables from a generated `schema.generated.ts` that
5
- * the developer commits. BaaS mode has no such file it points at a database
6
- * and serves it so the equivalent table objects are constructed here from
7
- * `information_schema` metadata.
4
+ * **Every** data source's tables are built here, from `information_schema`,
5
+ * after boot-ensure has run a project with declared collections included. The
6
+ * committed `schema.generated.ts` is a build artefact for Atlas, `db push`,
7
+ * `eject` and user code; the runtime does not read it for its tables, because a
8
+ * file that is one commit behind the database used to change what the server
9
+ * served (a renamed foreign-key column 500'd every request, a lost relation
10
+ * silently emptied a list). See `catalogue-schema.ts` for the caller.
8
11
  *
9
12
  * These are handed to drizzle as its schema, which keeps the relational query
10
13
  * path (`db.query.*`) working; without them FetchService would fall back to
11
14
  * plain selects and lose relation loading.
15
+ *
16
+ * Two things a caller has to supply for the tables to match what the generated
17
+ * module used to produce, both about *names*: the Drizzle object is keyed by the
18
+ * wire name of a column and not by the column (`authorId`, not `author_id`), and
19
+ * a relation is keyed by the collection's own relation key rather than by a
20
+ * guess made from the foreign key. Both come from the collections — see
21
+ * `columnKeysFromCollections` and `buildDrizzleRelationsFromCollections`.
12
22
  */
13
23
  import { type PgTable } from "drizzle-orm/pg-core";
14
24
  import { type Relations } from "drizzle-orm";
15
25
  import type { TableMeta } from "./introspect-db-logic.js";
26
+ /**
27
+ * The Drizzle object key a column is served under, given its table.
28
+ *
29
+ * Defaults to the column name — right for a junction table and for a database
30
+ * nothing declares — and is supplied from the collections wherever there is
31
+ * one, because there the wire name is the property key. Every read and write
32
+ * path in this driver indexes tables and rows by that key.
33
+ */
34
+ export type ColumnKeyResolver = (tableName: string, columnName: string) => string;
16
35
  /**
17
36
  * Build one drizzle table per introspected table, keyed by table name.
37
+ *
38
+ * @param columnKey how a column is named on the wire — see {@link ColumnKeyResolver}.
18
39
  */
19
- export declare function buildDrizzleTablesFromSchema(tablesMap: Map<string, TableMeta>, pgSchemaName?: string): Record<string, PgTable>;
40
+ export declare function buildDrizzleTablesFromSchema(tablesMap: Map<string, TableMeta>, pgSchemaName?: string, columnKey?: ColumnKeyResolver): Record<string, PgTable>;
20
41
  /**
21
42
  * Build drizzle `relations()` for the foreign keys, so the relational query
22
43
  * path can actually load them.
@@ -28,4 +49,4 @@ export declare function buildDrizzleTablesFromSchema(tablesMap: Map<string, Tabl
28
49
  * The keys here must match `buildRelations` in introspect-runtime.ts, which is
29
50
  * what names the collection's relation properties.
30
51
  */
31
- export declare function buildDrizzleRelationsFromSchema(tablesMap: Map<string, TableMeta>, tables: Record<string, PgTable>): Record<string, Relations>;
52
+ export declare function buildDrizzleRelationsFromSchema(tablesMap: Map<string, TableMeta>, tables: Record<string, PgTable>, columnKey?: ColumnKeyResolver): Record<string, Relations>;