@rebasepro/server-postgres 0.24.0 → 0.24.1-canary.gcb918fc
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/README.md +16 -0
- package/dist/{BranchService-BRt78gfa.js → BranchService-Bsn2wfaj.js} +3 -2
- package/dist/{BranchService-BRt78gfa.js.map → BranchService-Bsn2wfaj.js.map} +1 -1
- package/dist/PostgresBackendDriver.d.ts +91 -2
- package/dist/PostgresBootstrapper.d.ts +101 -2
- package/dist/auth/ensure-tables.d.ts +12 -0
- package/dist/auth/services.d.ts +6 -0
- package/dist/backup/backup-cli.d.ts +14 -0
- package/dist/backup/backup-service.d.ts +2 -2
- package/dist/backup/pg-binary.d.ts +5 -0
- package/dist/{backup-cli-DW5p9_zv.js → backup-cli-DnpJi6I7.js} +37 -29
- package/dist/backup-cli-DnpJi6I7.js.map +1 -0
- package/dist/{backup-service-EwcDVG-8.js → backup-service-BTBB7RXn.js} +55 -21
- package/dist/backup-service-BTBB7RXn.js.map +1 -0
- package/dist/boot-ddl-DVpsFN6r.js +105 -0
- package/dist/boot-ddl-DVpsFN6r.js.map +1 -0
- package/dist/cli-console-Ca6ZlAVU.js +154 -0
- package/dist/cli-console-Ca6ZlAVU.js.map +1 -0
- package/dist/cli-console.d.ts +39 -0
- package/dist/cli-errors-CaI4ntyD.js +1919 -0
- package/dist/cli-errors-CaI4ntyD.js.map +1 -0
- package/dist/cli-errors.d.ts +51 -5
- package/dist/{cli-flags-BglvpjHv.js → cli-flags-JNI-pwSf.js} +6 -3
- package/dist/cli-flags-JNI-pwSf.js.map +1 -0
- package/dist/cli-helpers.d.ts +40 -0
- package/dist/cli-output.d.ts +16 -28
- package/dist/cli-push.d.ts +87 -0
- package/dist/cli.js +566 -182
- package/dist/cli.js.map +1 -1
- package/dist/{column-plan-helpers-1-LQD0yI.js → column-plan-helpers-drZIsSNf.js} +10 -4
- package/dist/{column-plan-helpers-1-LQD0yI.js.map → column-plan-helpers-drZIsSNf.js.map} +1 -1
- package/dist/{databasePoolManager-Bj5FbeAs.js → connection-C9mT8bgf.js} +24 -112
- package/dist/connection-C9mT8bgf.js.map +1 -0
- package/dist/connection.d.ts +13 -2
- package/dist/databasePoolManager-BIuewidb.js +119 -0
- package/dist/databasePoolManager-BIuewidb.js.map +1 -0
- package/dist/{ensure-collection-tables-kkkHk8oo.js → deferred-index-build-DP1_SNFo.js} +484 -346
- package/dist/deferred-index-build-DP1_SNFo.js.map +1 -0
- package/dist/{doctor-C-sYWbmt.js → doctor-C4WiftHm.js} +63 -52
- package/dist/doctor-C4WiftHm.js.map +1 -0
- package/dist/ensure-collection-policies-CXpbeNag.js +541 -0
- package/dist/ensure-collection-policies-CXpbeNag.js.map +1 -0
- package/dist/ensure-collection-tables-BhsU6qUE.js +532 -0
- package/dist/ensure-collection-tables-BhsU6qUE.js.map +1 -0
- package/dist/{ensure-tables-BmI_tRxc.js → ensure-tables-CVm_yvzb.js} +173 -77
- package/dist/ensure-tables-CVm_yvzb.js.map +1 -0
- package/dist/enum-labels-kept-D8fI1MoX.js +56 -0
- package/dist/enum-labels-kept-D8fI1MoX.js.map +1 -0
- package/dist/generate-drizzle-schema-Bz52jH2h.js +57 -0
- package/dist/generate-drizzle-schema-Bz52jH2h.js.map +1 -0
- package/dist/{generate-drizzle-schema-logic-sSFDp6LR.js → generate-drizzle-schema-logic-B74WN37S.js} +5 -5
- package/dist/generate-drizzle-schema-logic-B74WN37S.js.map +1 -0
- package/dist/generate-files-CdEckX6c.js +199 -0
- package/dist/generate-files-CdEckX6c.js.map +1 -0
- package/dist/{generate-postgres-ddl-logic-BJsLaVNX.js → generate-postgres-ddl-logic-B64O2oZF.js} +3 -3
- package/dist/{generate-postgres-ddl-logic-BJsLaVNX.js.map → generate-postgres-ddl-logic-B64O2oZF.js.map} +1 -1
- package/dist/generated-sql.d.ts +8 -2
- package/dist/history/HistoryService.d.ts +64 -5
- package/dist/history/ensure-history-table.d.ts +63 -4
- package/dist/index.d.ts +1 -0
- package/dist/index.es.js +139 -19236
- package/dist/index.es.js.map +1 -1
- package/dist/{introspect-db-logic-kCETE8TY.js → introspect-db-logic-C-Di8dA9.js} +44 -6
- package/dist/introspect-db-logic-C-Di8dA9.js.map +1 -0
- package/dist/{introspect-db-queries-C_Q5VgQw.js → introspect-db-queries-D6kAqReI.js} +23 -1
- package/dist/introspect-db-queries-D6kAqReI.js.map +1 -0
- package/dist/open-rebase-CbOur0TU.js +20858 -0
- package/dist/open-rebase-CbOur0TU.js.map +1 -0
- package/dist/open-rebase.d.ts +86 -0
- package/dist/pg-error-utils-vJezlXdI.js +388 -0
- package/dist/pg-error-utils-vJezlXdI.js.map +1 -0
- package/dist/{plan-schema-DU9exq6C.js → plan-schema-B1YF_tYq.js} +151 -41
- package/dist/plan-schema-B1YF_tYq.js.map +1 -0
- package/dist/{policy-drift-B-J2hhm0.js → policy-drift-Dm8i-s1J.js} +23 -6
- package/dist/policy-drift-Dm8i-s1J.js.map +1 -0
- package/dist/push-index-prebuild-Db4AQF4i.js +45 -0
- package/dist/push-index-prebuild-Db4AQF4i.js.map +1 -0
- package/dist/push-legacy-renames-BK6ilQq2.js +34 -0
- package/dist/push-legacy-renames-BK6ilQq2.js.map +1 -0
- package/dist/{render-ddl-Ds2t_d9V.js → render-ddl-CtC3y9Ol.js} +8 -4
- package/dist/render-ddl-CtC3y9Ol.js.map +1 -0
- package/dist/{rls-bootstrap-sql-_KNnjanK.js → rls-bootstrap-sql-RQUljnJ1.js} +125 -1270
- package/dist/rls-bootstrap-sql-RQUljnJ1.js.map +1 -0
- package/dist/{rls-enforcement-CfXOJJaW.js → rls-enforcement-DvzqlaL1.js} +129 -17
- package/dist/rls-enforcement-DvzqlaL1.js.map +1 -0
- package/dist/schema/auth-schema.d.ts +2 -2
- package/dist/schema/boot-ddl.d.ts +47 -0
- package/dist/schema/collection-index.d.ts +45 -0
- package/dist/schema/column-plan-helpers.d.ts +5 -0
- package/dist/schema/deferred-index-build.d.ts +131 -0
- package/dist/schema/destructive-sql.d.ts +7 -0
- package/dist/schema/doctor-cli.js +110 -6
- package/dist/schema/doctor-cli.js.map +1 -1
- package/dist/schema/doctor-search-scale.d.ts +45 -0
- package/dist/schema/doctor.d.ts +20 -1
- package/dist/schema/ensure-collection-policies.d.ts +73 -35
- package/dist/schema/ensure-collection-tables.d.ts +56 -3
- package/dist/schema/enum-labels-kept.d.ts +26 -0
- package/dist/schema/generate-drizzle-schema.js +1 -1
- package/dist/schema/generate-files.d.ts +44 -0
- package/dist/schema/generate-next-step.d.ts +0 -13
- package/dist/schema/generate-postgres-ddl.js +10 -81
- package/dist/schema/generate-postgres-ddl.js.map +1 -1
- package/dist/schema/introspect-db-logic.d.ts +12 -0
- package/dist/schema/introspect-db-queries.d.ts +11 -1
- package/dist/schema/introspect-db-structure.d.ts +10 -0
- package/dist/schema/introspect-db.js +4 -5
- package/dist/schema/introspect-db.js.map +1 -1
- package/dist/schema/migration-post-process.d.ts +34 -0
- package/dist/schema/plan/diff-plan.d.ts +126 -0
- package/dist/schema/plan/types.d.ts +8 -0
- package/dist/schema/push-index-prebuild.d.ts +25 -0
- package/dist/schema/push-legacy-renames.d.ts +20 -0
- package/dist/schema/rls-bootstrap-sql.d.ts +15 -0
- package/dist/security/rls-enforcement.d.ts +32 -4
- package/dist/services/FetchService.d.ts +58 -8
- package/dist/services/PersistService.d.ts +15 -0
- package/dist/services/RelationService.d.ts +88 -3
- package/dist/services/cdc/CdcListener.d.ts +17 -6
- package/dist/services/cdc/trigger-cdc.d.ts +151 -27
- package/dist/services/channel-bus/index.d.ts +1 -1
- package/dist/services/channel-history.d.ts +15 -4
- package/dist/services/collection-helpers.d.ts +15 -0
- package/dist/services/field-viewer.d.ts +15 -1
- package/dist/services/pg-notify-listener.d.ts +7 -0
- package/dist/services/realtimeService.d.ts +314 -14
- package/dist/services/row-pipeline.d.ts +16 -9
- package/dist/services/socket-liveness.d.ts +42 -2
- package/dist/services/sql-script.d.ts +207 -3
- package/dist/services/vector-iterative-scan.d.ts +40 -0
- package/dist/services/write-depth.d.ts +6 -0
- package/dist/services/write-transaction-scope.d.ts +9 -0
- package/dist/socket-opt-ins.d.ts +64 -0
- package/dist/source-DBDbBTL4.js +811 -0
- package/dist/source-DBDbBTL4.js.map +1 -0
- package/dist/tsx-loader.d.ts +14 -0
- package/dist/utils/drizzle-conditions.d.ts +21 -1
- package/dist/utils/error-cause.d.ts +7 -0
- package/dist/utils/pg-error-utils.d.ts +2 -4
- package/dist/utils/result-rows.d.ts +10 -0
- package/dist/websocket-BLGc5yhs.js +1604 -0
- package/dist/websocket-BLGc5yhs.js.map +1 -0
- package/dist/websocket.d.ts +13 -0
- package/package.json +7 -7
- package/dist/backup-cli-DW5p9_zv.js.map +0 -1
- package/dist/backup-service-EwcDVG-8.js.map +0 -1
- package/dist/cli-errors-DsA-K9uP.js +0 -893
- package/dist/cli-errors-DsA-K9uP.js.map +0 -1
- package/dist/cli-flags-BglvpjHv.js.map +0 -1
- package/dist/cli-output-CNdMql-L.js +0 -47
- package/dist/cli-output-CNdMql-L.js.map +0 -1
- package/dist/collection-index-DdnsxiJ_.js +0 -286
- package/dist/collection-index-DdnsxiJ_.js.map +0 -1
- package/dist/databasePoolManager-Bj5FbeAs.js.map +0 -1
- package/dist/doctor-C-sYWbmt.js.map +0 -1
- package/dist/ensure-collection-policies-B1ureSIV.js +0 -160
- package/dist/ensure-collection-policies-B1ureSIV.js.map +0 -1
- package/dist/ensure-collection-tables-kkkHk8oo.js.map +0 -1
- package/dist/ensure-tables-BmI_tRxc.js.map +0 -1
- package/dist/generate-drizzle-schema-93M0lUxK.js +0 -123
- package/dist/generate-drizzle-schema-93M0lUxK.js.map +0 -1
- package/dist/generate-drizzle-schema-logic-sSFDp6LR.js.map +0 -1
- package/dist/introspect-db-logic-kCETE8TY.js.map +0 -1
- package/dist/introspect-db-queries-C_Q5VgQw.js.map +0 -1
- package/dist/plan-schema-DU9exq6C.js.map +0 -1
- package/dist/policy-drift-B-J2hhm0.js.map +0 -1
- package/dist/render-ddl-Ds2t_d9V.js.map +0 -1
- package/dist/rls-bootstrap-sql-_KNnjanK.js.map +0 -1
- package/dist/rls-enforcement-CfXOJJaW.js.map +0 -1
- package/dist/source-Br7L7GOI.js +0 -410
- package/dist/source-Br7L7GOI.js.map +0 -1
- package/dist/utils/sql-redaction.d.ts +0 -21
|
@@ -0,0 +1 @@
|
|
|
1
|
+
{"version":3,"file":"ensure-tables-CVm_yvzb.js","names":[],"sources":["../src/auth/schema-version.ts","../src/auth/ensure-tables.ts"],"sourcesContent":["import { sql } from \"drizzle-orm\";\nimport { NodePgDatabase } from \"drizzle-orm/node-postgres\";\nimport type { CollectionConfig } from \"@rebasepro/types\";\n\n/**\n * The auth schema version this runtime expects to find in the database.\n *\n * Bump this whenever a migration in `ensureAuthTablesExist` makes the schema\n * unreadable by the runtime that came before it — that is, whenever a *previous*\n * version's auth queries would break against the migrated shape. Additive\n * changes (a new nullable column nobody older references) do not need a bump.\n *\n * History. Note that 1 is a label for an era, not a value any database holds:\n * stamping did not exist then, so an era-1 database reads as unstamped\n * (`null`), and 2 is the first version ever actually written. The numbering\n * starts at 2 only because two schema eras already existed when it was\n * introduced; it could just as well have started at 1. It is not worth\n * renumbering now — deployed databases already carry 2, and lowering the\n * constant would make them look newer than the runtime and refuse the boot.\n *\n * 1 — Device-session refresh tokens. A row *was* a session, identified by\n * `unique_device_session UNIQUE (uid, user_agent, ip_address)`, and\n * `createToken` upserted with `ON CONFLICT (uid, user_agent, ip_address)`.\n * 2 — Session-scoped, rotation-safe refresh tokens: `session_id`, `revoked`,\n * `rotated_at`, `session_started_at`, and `unique_device_session`\n * dropped because two live tokens of one session share all three columns.\n *\n * The 1 → 2 migration is why this file exists. Dropping the constraint is\n * one-way: a version-1 runtime deployed afterwards boots perfectly, logs\n * `✅ Auth tables ready` (its `CREATE TABLE IF NOT EXISTS` never revisits the\n * existing table, so it cannot re-add the constraint), answers `/health` with\n * 200 — and then fails every single login and refresh with SQLSTATE 42P10,\n * because its `ON CONFLICT` names a constraint that no longer exists. A silent\n * total auth outage behind a green health check. The stamp below turns that\n * into a boot refusal.\n */\nexport const AUTH_SCHEMA_VERSION = 2;\n\n/** Key under which the version is stored in the auth schema's meta table. */\nconst VERSION_KEY = \"auth_schema_version\";\n\n/**\n * Columns `refresh_tokens` must have for the current runtime's auth write path\n * to work. Checked by the health probe so a database that drifted *below* this\n * runtime is reported as unhealthy rather than discovered one failed login at a\n * time. Kept in step with the migration in `ensureAuthTablesExist`.\n */\nconst REQUIRED_REFRESH_TOKEN_COLUMNS = [\"session_id\", \"revoked\", \"rotated_at\", \"session_started_at\"];\n\n/**\n * A constraint whose *presence* means the database is still at version 1, in a\n * shape this runtime's rotation logic cannot write to: it makes two live tokens\n * of one rotating session collide.\n */\nconst RETIRED_REFRESH_TOKEN_CONSTRAINT = \"unique_device_session\";\n\n/**\n * Thrown when the database was migrated by a runtime newer than this one.\n *\n * Distinct class rather than a bare `Error` because `ensureAuthTablesExist`\n * wraps its migrations in a catch that deliberately swallows failures and\n * continues — every other problem there is better survived than crashed on.\n * This one is not, so the catch rethrows on this type specifically.\n */\nexport class AuthSchemaVersionError extends Error {\n readonly databaseVersion: number;\n readonly runtimeVersion: number;\n\n constructor(databaseVersion: number, runtimeVersion: number) {\n super(\n `Auth schema version mismatch: the database is at version ${databaseVersion}, ` +\n `but this runtime understands version ${runtimeVersion}.\\n\\n` +\n \"A newer version of the framework has already migrated this database. Running this \" +\n \"older runtime against it would boot cleanly and then fail every login and token \" +\n \"refresh, because the auth schema it expects no longer exists.\\n\\n\" +\n \"Refusing to start. Deploy a framework version at or above the one that migrated \" +\n \"this database, or restore the database from a backup taken before the upgrade.\"\n );\n this.name = \"AuthSchemaVersionError\";\n this.databaseVersion = databaseVersion;\n this.runtimeVersion = runtimeVersion;\n }\n}\n\n/**\n * The schema the auth tables live in, derived exactly as `ensureAuthTablesExist`\n * derives it. Shared so the two cannot drift: a stamp written to one schema and\n * read from another would read as \"never stamped\" forever.\n */\nexport function resolveAuthSchema(collection?: CollectionConfig): string {\n if (!collection) return \"rebase\";\n const usersSchema = (\"schema\" in collection && typeof collection.schema === \"string\")\n ? collection.schema\n : \"public\";\n return usersSchema === \"public\" ? \"rebase\" : usersSchema;\n}\n\n/**\n * Read the stamped version, or `null` when the database has never been stamped.\n *\n * `null` is not an error and must not be treated as one: every database\n * provisioned before this file existed is unstamped, and so is every fresh one.\n * Uses `to_regclass` rather than selecting straight from the table so a missing\n * schema or table is a `null` rather than a thrown 42P01.\n */\nexport async function readAuthSchemaVersion(\n db: NodePgDatabase,\n authSchema: string\n): Promise<number | null> {\n const qualified = `\"${authSchema}\".\"schema_meta\"`;\n const exists = await db.execute(sql`SELECT to_regclass(${qualified}) IS NOT NULL AS present`);\n if (!(exists.rows[0] as { present: boolean } | undefined)?.present) return null;\n\n const result = await db.execute(sql`\n SELECT value FROM ${sql.raw(qualified)} WHERE key = ${VERSION_KEY}\n `);\n const raw = (result.rows[0] as { value: string } | undefined)?.value;\n if (raw === undefined) return null;\n\n const parsed = Number.parseInt(raw, 10);\n // A meta row we cannot parse is treated as unstamped rather than as version\n // 0: refusing to boot over a garbled string would be a worse failure than\n // the drift it is meant to catch.\n return Number.isFinite(parsed) ? parsed : null;\n}\n\n/**\n * Refuse to run against a database a newer runtime has already migrated.\n *\n * Deliberately one-directional. A database *older* than this runtime is the\n * normal upgrade path — the migrations in `ensureAuthTablesExist` are about to\n * bring it forward, so it is not an error. Only the reverse is unrecoverable.\n */\nexport async function assertAuthSchemaCompatible(\n db: NodePgDatabase,\n authSchema: string\n): Promise<void> {\n const databaseVersion = await readAuthSchemaVersion(db, authSchema);\n if (databaseVersion !== null && databaseVersion > AUTH_SCHEMA_VERSION) {\n throw new AuthSchemaVersionError(databaseVersion, AUTH_SCHEMA_VERSION);\n }\n}\n\n/**\n * Record that this runtime's migrations have been applied.\n *\n * Called at the end of `ensureAuthTablesExist`, so a boot that failed partway\n * through leaves the older stamp in place and the next boot migrates again.\n */\nexport async function stampAuthSchemaVersion(\n db: NodePgDatabase,\n authSchema: string\n): Promise<void> {\n const qualified = `\"${authSchema}\".\"schema_meta\"`;\n await db.execute(sql`\n CREATE TABLE IF NOT EXISTS ${sql.raw(qualified)} (\n key TEXT PRIMARY KEY,\n value TEXT NOT NULL,\n updated_at TIMESTAMP WITH TIME ZONE DEFAULT NOW() NOT NULL\n )\n `);\n await db.execute(sql`\n INSERT INTO ${sql.raw(qualified)} (key, value)\n VALUES (${VERSION_KEY}, ${String(AUTH_SCHEMA_VERSION)})\n ON CONFLICT (key) DO UPDATE SET value = EXCLUDED.value, updated_at = NOW()\n `);\n}\n\n/** What {@link probeAuthSchema} found. */\nexport interface AuthSchemaProbeResult {\n /** False when this runtime cannot be trusted to serve auth against this database. */\n healthy: boolean;\n /** The stamped version, or `null` on a database that predates stamping. */\n databaseVersion: number | null;\n /** {@link AUTH_SCHEMA_VERSION}. */\n runtimeVersion: number;\n /** Human-readable descriptions of each mismatch found. Empty when healthy. */\n problems: string[];\n}\n\n/**\n * Check that the auth schema is one this runtime can actually write to.\n *\n * Two independent checks, because either alone has a blind spot:\n *\n * - The **stamp** catches a runtime older than the database. It is the precise\n * signal, but it is blind on every database provisioned before stamping\n * existed — which today is all of them.\n * - The **structure** catches a database older than the runtime, and works on\n * unstamped databases. It is what makes this useful immediately rather than\n * one upgrade cycle from now.\n *\n * Never throws: a probe that fails to run reports unhealthy with the reason, so\n * a broken check surfaces as a degraded health response rather than a 500 from\n * the health endpoint itself.\n */\nexport async function probeAuthSchema(\n db: NodePgDatabase,\n authSchema: string\n): Promise<AuthSchemaProbeResult> {\n const problems: string[] = [];\n let databaseVersion: number | null = null;\n\n try {\n databaseVersion = await readAuthSchemaVersion(db, authSchema);\n if (databaseVersion !== null && databaseVersion > AUTH_SCHEMA_VERSION) {\n problems.push(\n `database is at auth schema version ${databaseVersion}, this runtime understands ` +\n `${AUTH_SCHEMA_VERSION} — it was migrated by a newer framework version`\n );\n }\n\n const refreshTokens = `\"${authSchema}\".\"refresh_tokens\"`;\n const present = await db.execute(sql`SELECT to_regclass(${refreshTokens}) IS NOT NULL AS present`);\n if (!(present.rows[0] as { present: boolean } | undefined)?.present) {\n // Not a problem in itself: auth may simply not be configured on this\n // deployment, and the table is created on demand at boot when it is.\n return { healthy: problems.length === 0, databaseVersion, runtimeVersion: AUTH_SCHEMA_VERSION, problems };\n }\n\n const columns = await db.execute(sql`\n SELECT column_name FROM information_schema.columns\n WHERE table_schema = ${authSchema} AND table_name = 'refresh_tokens'\n `);\n const found = new Set((columns.rows as { column_name: string }[]).map(row => row.column_name));\n const missing = REQUIRED_REFRESH_TOKEN_COLUMNS.filter(column => !found.has(column));\n if (missing.length > 0) {\n problems.push(\n `refresh_tokens is missing ${missing.join(\", \")} — the auth migrations have not been ` +\n \"applied to this database, so token rotation will fail\"\n );\n }\n\n const retired = await db.execute(sql`\n SELECT 1 FROM pg_constraint c\n JOIN pg_class t ON t.oid = c.conrelid\n JOIN pg_namespace n ON n.oid = t.relnamespace\n WHERE n.nspname = ${authSchema}\n AND t.relname = 'refresh_tokens'\n AND c.conname = ${RETIRED_REFRESH_TOKEN_CONSTRAINT}\n `);\n if (retired.rows.length > 0) {\n problems.push(\n `refresh_tokens still carries ${RETIRED_REFRESH_TOKEN_CONSTRAINT} — concurrent token ` +\n \"rotation for one session will fail on it\"\n );\n }\n } catch (error: unknown) {\n problems.push(\n `auth schema probe failed: ${error instanceof Error ? error.message : String(error)}`\n );\n }\n\n return {\n healthy: problems.length === 0,\n databaseVersion,\n runtimeVersion: AUTH_SCHEMA_VERSION,\n problems\n };\n}\n","import { sql } from \"drizzle-orm\";\nimport { NodePgDatabase } from \"drizzle-orm/node-postgres\";\nimport { logger } from \"@rebasepro/server\";\nimport { revokeInternalTableAccess } from \"@rebasepro/common\";\nimport type { CollectionConfig } from \"@rebasepro/types\";\nimport { AUTH_USERS_COLUMNS, authUsersColumnSql } from \"../schema/auth-users-columns\";\nimport { RLS_BOOTSTRAP_STATEMENTS, rlsRuntimeIsCurrent } from \"../schema/rls-bootstrap-sql\";\nimport { internalTablesGrantedToUserRole } from \"../security/rls-enforcement\";\nimport { BOOT_LOCK_TIMEOUT_MS } from \"../schema/boot-ddl\";\nimport {\n AuthSchemaVersionError,\n assertAuthSchemaCompatible,\n resolveAuthSchema,\n stampAuthSchemaVersion\n} from \"./schema-version\";\n\n\n/** A string literal for a catalogue lookup. The names come from config, never from a request. */\nconst literal = (value: string): string => `'${value.replace(/'/g, \"''\")}'`;\n\n/**\n * Every index name in these schemas, as `schema.name`.\n *\n * `CREATE INDEX IF NOT EXISTS` is not free when the index exists: Postgres\n * takes the table's SHARE lock — which blocks writers, and queues behind any\n * open write transaction — before it looks the name up. So the names are read\n * once and only a missing index is created. Read before anything is created:\n * this function never drops an index, so present then means present now.\n */\nasync function readIndexNames(db: NodePgDatabase, schemas: string[]): Promise<Set<string>> {\n const names = new Set<string>();\n const list = [...new Set(schemas)];\n if (list.length === 0) return names;\n const result = await db.execute(sql.raw(\n \"SELECT n.nspname AS schema, c.relname AS name FROM pg_class c \" +\n \"JOIN pg_namespace n ON n.oid = c.relnamespace \" +\n `WHERE c.relkind IN ('i', 'I') AND n.nspname IN (${list.map(literal).join(\", \")})`\n ));\n for (const row of result.rows as { schema: string; name: string }[]) names.add(`${row.schema}.${row.name}`);\n return names;\n}\n\n/**\n * A column default as `information_schema` reports it, reduced to what decides\n * equality: `(gen_random_uuid())::text` and `gen_random_uuid()::text` are the\n * same default, written two ways.\n */\nconst sameDefault = (actual: string | null, expected: string): boolean =>\n actual !== null && actual.toLowerCase().replace(/[\\s()]/g, \"\") === expected.toLowerCase().replace(/[\\s()]/g, \"\");\n\n/**\n * The plain `token_hash` indexes every era created next to the UNIQUE\n * constraint's own index on the same column. No longer created; dropped where\n * found — see {@link dropDuplicateTokenHashIndexesSql}.\n */\nconst DUPLICATE_TOKEN_HASH_INDEXES = [\n \"idx_refresh_tokens_hash\",\n \"idx_password_reset_tokens_hash\",\n \"idx_magic_link_tokens_hash\"\n] as const;\n\n/**\n * One statement that drops each named index — but only one that covers a\n * single column that a valid, unique, non-partial btree on the same table also\n * leads with. That is the UNIQUE (token_hash) constraint's index, which serves\n * every `token_hash = $1` lookup; a table that somehow lacks it keeps its\n * plain index, so no lookup is ever left without one.\n *\n * `DROP INDEX` takes the table's ACCESS EXCLUSIVE lock, on the tables every\n * sign-in and token refresh touches, so it runs under the boot lock budget\n * (`lock_timeout`, transaction-local to this statement — see `boot-ddl`).\n */\nexport function dropDuplicateTokenHashIndexesSql(schema: string, names: readonly string[]): string {\n return `\nDO $rebase_drop_hash_index$\nDECLARE duplicate record;\nBEGIN\n PERFORM set_config('lock_timeout', '${BOOT_LOCK_TIMEOUT_MS}ms', true);\n FOR duplicate IN\n SELECT ic.relname AS name\n FROM pg_index d\n JOIN pg_class ic ON ic.oid = d.indexrelid\n JOIN pg_namespace n ON n.oid = ic.relnamespace\n WHERE n.nspname = ${literal(schema)}\n AND ic.relname IN (${names.map(literal).join(\", \")})\n AND d.indnatts = 1\n AND EXISTS (\n SELECT 1 FROM pg_index o\n JOIN pg_class oc ON oc.oid = o.indexrelid\n JOIN pg_am am ON am.oid = oc.relam\n WHERE o.indrelid = d.indrelid AND o.indexrelid <> d.indexrelid\n AND o.indisvalid AND o.indisunique AND o.indpred IS NULL AND o.indexprs IS NULL\n AND am.amname = 'btree' AND o.indkey[0] = d.indkey[0])\n LOOP\n EXECUTE format('DROP INDEX IF EXISTS %I.%I', ${literal(schema)}, duplicate.name);\n END LOOP;\nEND\n$rebase_drop_hash_index$`.trim();\n}\n\n/**\n * Auto-create auth tables if they don't exist.\n *\n * @param db — Drizzle database instance\n * @param collection — The collection that represents auth users.\n * When omitted, a default `rebase.users` table is created.\n */\nexport async function ensureAuthTablesExist(db: NodePgDatabase, collection?: CollectionConfig): Promise<void> {\n logger.debug(\"🔍 Checking auth tables...\");\n\n // Before anything else, and deliberately outside the catch below: refuse to\n // run against a database that a newer framework version has already\n // migrated. Everything past this point is best-effort by design, which is\n // exactly the wrong posture for an incompatibility that would otherwise\n // surface as a fully booted server failing every login.\n await assertAuthSchemaCompatible(db, resolveAuthSchema(collection));\n\n try {\n // Resolve dynamic user table name and ID type from the collection\n let usersTableName = '\"rebase\".\"users\"';\n let userIdType = \"TEXT\";\n let usersSchema = \"rebase\";\n let resolvedTable = \"users\";\n if (collection) {\n resolvedTable = (\"table\" in collection && typeof collection.table === \"string\")\n ? collection.table\n : collection.slug;\n usersSchema = (\"schema\" in collection && typeof collection.schema === \"string\")\n ? collection.schema\n : \"public\";\n usersTableName = usersSchema === \"public\"\n ? `\"${resolvedTable}\"`\n : `\"${usersSchema}\".\"${resolvedTable}\"`;\n\n // Derive ID column type from collection properties.\n //\n // `\"increment\"`, not `\"autoincrement\"`. The latter was tested for\n // here and exists nowhere in the type system — the union is\n // `boolean | \"manual\" | \"increment\" | string` — so the INTEGER branch\n // was unreachable and an integer-keyed auth collection fell through\n // to TEXT. Introspection below hid it whenever the table already\n // existed; on a database where it did not, this created\n // `id TEXT DEFAULT gen_random_uuid()::text` for a collection that\n // declares a number, and every `uid` foreign key was typed to match\n // the wrong thing.\n const idProp = collection.properties?.id;\n if (idProp) {\n const isId = (\"isId\" in idProp) ? (idProp as Record<string, unknown>).isId : undefined;\n if (isId === \"uuid\") {\n userIdType = \"UUID\";\n } else if (isId === \"increment\") {\n userIdType = \"INTEGER\";\n }\n // Otherwise keep TEXT as default\n }\n }\n\n // Introspect the database to find the actual type of usersTableName's ID column if the table exists\n try {\n const result = await db.execute(sql`\n SELECT data_type \n FROM information_schema.columns \n WHERE table_schema = ${usersSchema} \n AND table_name = ${resolvedTable} \n AND column_name = 'id'\n `);\n if (result && result.rows && result.rows.length > 0) {\n const dbType = String((result.rows[0] as { data_type: string }).data_type).toUpperCase();\n if (dbType === \"UUID\") {\n userIdType = \"UUID\";\n } else if (dbType === \"INTEGER\" || dbType === \"SMALLINT\" || dbType === \"BIGINT\") {\n userIdType = \"INTEGER\";\n } else {\n userIdType = \"TEXT\";\n }\n logger.debug(`✨ Detected ${usersTableName}.id type from database: ${dbType}. Using user_id type: ${userIdType}`);\n }\n } catch (err) {\n // Ignore introspection errors, fallback to derived/default type\n logger.warn(`⚠️ Failed to introspect ${usersTableName}.id type from database, falling back to config type: ${userIdType}`, { error: err });\n }\n\n\n // ── Create schemas (idempotent) ──────────────────────────────────\n if (usersSchema !== \"public\") {\n await db.execute(sql`CREATE SCHEMA IF NOT EXISTS ${sql.raw(usersSchema)}`);\n }\n await db.execute(sql`CREATE SCHEMA IF NOT EXISTS rebase`);\n\n const authSchema = usersSchema === \"public\" ? \"rebase\" : usersSchema;\n const indexNames = await readIndexNames(db, [usersSchema, authSchema]);\n const runRaw = async (text: string): Promise<Record<string, unknown>[]> =>\n (await db.execute(sql.raw(text))).rows as Record<string, unknown>[];\n /** Create an index only when it is missing — see {@link readIndexNames}. */\n const ensureIndex = async (schema: string, name: string, statement: ReturnType<typeof sql>): Promise<void> => {\n if (indexNames.has(`${schema}.${name}`)) return;\n await db.execute(statement);\n };\n const userIdentitiesTable = `\"${authSchema}\".\"user_identities\"`;\n const refreshTokensTableName = `\"${authSchema}\".\"refresh_tokens\"`;\n const passwordResetTokensTableName = `\"${authSchema}\".\"password_reset_tokens\"`;\n const appConfigTableName = `\"${authSchema}\".\"app_config\"`;\n\n // ── Create users table (idempotent) ─────────────────────────────\n // The users table MUST be created before any dependent auth tables\n // (user_identities, refresh_tokens, etc.) because they all hold\n // foreign keys referencing users(id). When a developer runs\n // `pnpm dev` for the first time without `db:migrate`, this ensures\n // the server can self-bootstrap.\n const idDefault = userIdType === \"UUID\"\n ? \"DEFAULT gen_random_uuid()\"\n : userIdType === \"INTEGER\"\n ? \"GENERATED ALWAYS AS IDENTITY\"\n : \"DEFAULT gen_random_uuid()::text\";\n\n // Identifiers for the constraint and indexes reconciled further down.\n // Derived from the resolved table name so two auth tables in different\n // schemas cannot collide, and truncated to Postgres's 63-byte identifier\n // limit here rather than letting the server truncate silently — the\n // `IF NOT EXISTS` guards below have to compare against the same name\n // Postgres actually stored, or they re-run forever.\n const authIdentifier = (suffix: string) => `${resolvedTable}_${suffix}`.slice(0, 63);\n const emailLengthConstraint = `\"${authIdentifier(\"email_length_check\")}\"`;\n const emailLowerUniqueIndex = authIdentifier(\"email_lower_key\");\n const verificationTokenIndex = authIdentifier(\"email_verification_token_idx\");\n const emailChangeTokenIndex = authIdentifier(\"email_change_token_idx\");\n\n // Every string column here is TEXT, deliberately. In Postgres VARCHAR(n)\n // and TEXT are the same type with the same storage and the same\n // performance; the only difference is a length check, and none of these\n // columns wants one. The widths this table used to carry were inherited\n // MySQL habit (255) and they were all wrong in the same direction —\n // `password_hash VARCHAR(255)` against a 193-char scrypt string left 62\n // characters of headroom in front of a KEY_LENGTH constant living in\n // another package, and `photo_url VARCHAR(500)` rejected the `data:` URIs\n // and long signed URLs that OAuth providers hand back. A limit worth\n // having is a CHECK — alterable without a table rewrite, unlike a type\n // modifier — which is why `email` has one and nothing else does.\n //\n // The column list comes from AUTH_USERS_COLUMNS rather than being spelled\n // out here, because this is not the only place that creates this table:\n // `db push` and the boot-time collection ensure do too, and when the\n // three lists were maintained separately they disagreed and boot order\n // silently decided which shape the database got. The `email` CHECK is\n // appended rather than listed there — it is a named constraint the\n // migration below has to be able to add separately, `NOT VALID`, to a\n // table that already holds rows.\n const usersColumnDdl = AUTH_USERS_COLUMNS\n .map((spec) => spec.column === \"email\"\n ? `${spec.column} ${authUsersColumnSql(spec)} CONSTRAINT ${emailLengthConstraint} CHECK (length(email) <= 320)`\n : `${spec.column} ${authUsersColumnSql(spec)}`)\n .join(\",\\n \");\n await db.execute(sql`\n CREATE TABLE IF NOT EXISTS ${sql.raw(usersTableName)} (\n id ${sql.raw(userIdType)} PRIMARY KEY ${sql.raw(idDefault)},\n ${sql.raw(usersColumnDdl)}\n )\n `);\n\n // ── Migration: auth FK column user_id → uid, phase 1 (expand) ───────\n // Must run BEFORE the dependent tables below: CREATE TABLE IF NOT\n // EXISTS never revisits an existing table, so a database provisioned\n // before this migration still lacks `uid` — and the\n // CREATE INDEX ... (uid) statements that follow would fail on it.\n //\n // Deliberately NOT a plain RENAME. Both Cloud Run and Kubernetes roll\n // deploys, so old and new pods serve the same database at the same time,\n // and a rollback puts old code back in front of a migrated database. A\n // rename breaks every auth query on whichever side is out of step.\n // Instead: add `uid`, backfill it, drop the NOT NULL on `user_id`, and\n // keep the two in sync with a trigger, so a backend of either era can\n // read and write. `scripts/drop-legacy-auth-user-id.sql` removes the\n // column once no old backend remains (phase 2, contract).\n //\n // Idempotent throughout: every step is guarded on catalogue state.\n const legacyFkTables = [\n \"user_identities\",\n \"refresh_tokens\",\n \"password_reset_tokens\",\n \"magic_link_tokens\",\n \"mfa_factors\",\n \"recovery_codes\"\n ];\n\n // Only on a database that actually carries the legacy column. This whole\n // block is a 0.x compatibility shim, and it used to run unconditionally —\n // so every brand-new database was provisioned with a trigger function\n // written to reconcile a column it can never have, permanently, as part\n // of its first boot. A fresh install should not ship someone else's\n // migration history.\n // The table list is inlined rather than bound: drizzle expands a JS\n // array into a parameter TUPLE — `ANY(($2, $3, …))` — which Postgres\n // rejects, and the thrown error is swallowed by the catch around this\n // whole function, so auth would silently stop provisioning. These are\n // module-level constants, not input.\n const legacyFkTableList = legacyFkTables.map(t => `'${t}'`).join(\", \");\n const legacyUserIdPresent = await db.execute(sql`\n SELECT 1\n FROM information_schema.columns\n WHERE table_schema = ${authSchema}\n AND table_name IN (${sql.raw(legacyFkTableList)})\n AND column_name = 'user_id'\n LIMIT 1\n `);\n\n if (legacyUserIdPresent.rows.length > 0) {\n await db.execute(sql`\n CREATE OR REPLACE FUNCTION ${sql.raw(`\"${authSchema}\"`)}.sync_uid_user_id() RETURNS trigger AS $$\n BEGIN\n IF NEW.uid IS NULL AND NEW.user_id IS NOT NULL THEN\n NEW.uid := NEW.user_id;\n ELSIF NEW.user_id IS NULL AND NEW.uid IS NOT NULL THEN\n NEW.user_id := NEW.uid;\n END IF;\n RETURN NEW;\n END $$ LANGUAGE plpgsql\n `);\n }\n\n for (const authTable of legacyUserIdPresent.rows.length > 0 ? legacyFkTables : []) {\n const qualified = `\"${authSchema}\".\"${authTable}\"`;\n await db.execute(sql`\n DO $$\n DECLARE\n has_legacy boolean;\n has_uid boolean;\n BEGIN\n SELECT\n bool_or(column_name = 'user_id'),\n bool_or(column_name = 'uid')\n INTO has_legacy, has_uid\n FROM information_schema.columns\n WHERE table_schema = ${sql.raw(`'${authSchema}'`)}\n AND table_name = ${sql.raw(`'${authTable}'`)};\n\n -- Table absent, or already uid-only (a fresh install, or\n -- phase 2 already run): nothing to do.\n IF has_legacy IS NOT TRUE THEN\n RETURN;\n END IF;\n\n IF has_uid IS NOT TRUE THEN\n EXECUTE ${sql.raw(`'ALTER TABLE ${qualified} ADD COLUMN uid ${userIdType} REFERENCES ${usersTableName}(id) ON DELETE CASCADE'`)};\n EXECUTE ${sql.raw(`'UPDATE ${qualified} SET uid = user_id WHERE uid IS NULL'`)};\n EXECUTE ${sql.raw(`'CREATE INDEX IF NOT EXISTS idx_${authTable}_uid ON ${qualified}(uid)'`)};\n END IF;\n\n -- New code inserts uid and never user_id, so the legacy\n -- column can no longer be NOT NULL. The trigger below\n -- backfills it, but the constraint is checked first.\n EXECUTE ${sql.raw(`'ALTER TABLE ${qualified} ALTER COLUMN user_id DROP NOT NULL'`)};\n\n EXECUTE ${sql.raw(`'DROP TRIGGER IF EXISTS sync_uid_user_id ON ${qualified}'`)};\n EXECUTE ${sql.raw(`'CREATE TRIGGER sync_uid_user_id BEFORE INSERT OR UPDATE ON ${qualified} FOR EACH ROW EXECUTE FUNCTION \"${authSchema}\".sync_uid_user_id()'`)};\n END $$\n `);\n }\n\n // ── Create dependent auth tables (idempotent) ───────────────────\n\n // Create user_identities table\n await db.execute(sql`\n CREATE TABLE IF NOT EXISTS ${sql.raw(userIdentitiesTable)} (\n id TEXT PRIMARY KEY DEFAULT gen_random_uuid()::text,\n uid ${sql.raw(userIdType)} NOT NULL REFERENCES ${sql.raw(usersTableName)}(id) ON DELETE CASCADE,\n provider TEXT NOT NULL,\n provider_id TEXT NOT NULL,\n profile_data JSONB,\n created_at TIMESTAMP WITH TIME ZONE DEFAULT NOW(),\n updated_at TIMESTAMP WITH TIME ZONE DEFAULT NOW(),\n UNIQUE(provider, provider_id)\n )\n `);\n\n // Create indexes on user_identities\n await ensureIndex(authSchema, \"idx_user_identities_user\", sql`\n CREATE INDEX IF NOT EXISTS idx_user_identities_user\n ON ${sql.raw(userIdentitiesTable)}(uid)\n `);\n\n\n // Create refresh tokens table. One row per TOKEN, grouped into a\n // sign-in by session_id — deliberately without a uniqueness rule on\n // (uid, user_agent, ip_address); see the schema module for why that\n // constraint had to go.\n await db.execute(sql`\n CREATE TABLE IF NOT EXISTS ${sql.raw(refreshTokensTableName)} (\n id TEXT PRIMARY KEY DEFAULT gen_random_uuid()::text,\n uid ${sql.raw(userIdType)} NOT NULL REFERENCES ${sql.raw(usersTableName)}(id) ON DELETE CASCADE,\n session_id TEXT NOT NULL DEFAULT gen_random_uuid()::text,\n token_hash TEXT NOT NULL UNIQUE,\n expires_at TIMESTAMP WITH TIME ZONE NOT NULL,\n revoked BOOLEAN DEFAULT FALSE NOT NULL,\n rotated_at TIMESTAMP WITH TIME ZONE,\n session_started_at TIMESTAMP WITH TIME ZONE DEFAULT NOW() NOT NULL,\n user_agent TEXT,\n ip_address TEXT,\n created_at TIMESTAMP WITH TIME ZONE DEFAULT NOW()\n )\n `);\n\n // Create index on uid for cleanup operations\n await ensureIndex(authSchema, \"idx_refresh_tokens_user\", sql`\n CREATE INDEX IF NOT EXISTS idx_refresh_tokens_user\n ON ${sql.raw(refreshTokensTableName)}(uid)\n `);\n\n // Create password reset tokens table\n await db.execute(sql`\n CREATE TABLE IF NOT EXISTS ${sql.raw(passwordResetTokensTableName)} (\n id TEXT PRIMARY KEY DEFAULT gen_random_uuid()::text,\n uid ${sql.raw(userIdType)} NOT NULL REFERENCES ${sql.raw(usersTableName)}(id) ON DELETE CASCADE,\n token_hash TEXT NOT NULL UNIQUE,\n expires_at TIMESTAMP WITH TIME ZONE NOT NULL,\n used_at TIMESTAMP WITH TIME ZONE,\n created_at TIMESTAMP WITH TIME ZONE DEFAULT NOW()\n )\n `);\n\n // Create index on uid for password reset cleanup\n await ensureIndex(authSchema, \"idx_password_reset_tokens_user\", sql`\n CREATE INDEX IF NOT EXISTS idx_password_reset_tokens_user\n ON ${sql.raw(passwordResetTokensTableName)}(uid)\n `);\n\n // Create magic link tokens table\n const magicLinkTokensTableName = `\"${authSchema}\".\"magic_link_tokens\"`;\n await db.execute(sql`\n CREATE TABLE IF NOT EXISTS ${sql.raw(magicLinkTokensTableName)} (\n id TEXT PRIMARY KEY DEFAULT gen_random_uuid()::text,\n uid ${sql.raw(userIdType)} NOT NULL REFERENCES ${sql.raw(usersTableName)}(id) ON DELETE CASCADE,\n token_hash TEXT NOT NULL UNIQUE,\n expires_at TIMESTAMP WITH TIME ZONE NOT NULL,\n used_at TIMESTAMP WITH TIME ZONE,\n created_at TIMESTAMP WITH TIME ZONE DEFAULT NOW()\n )\n `);\n\n // Create index on uid for magic link cleanup\n await ensureIndex(authSchema, \"idx_magic_link_tokens_user\", sql`\n CREATE INDEX IF NOT EXISTS idx_magic_link_tokens_user\n ON ${sql.raw(magicLinkTokensTableName)}(uid)\n `);\n\n // Every lookup on the three token tables is `token_hash = $1`, which\n // the index behind each table's UNIQUE (token_hash) answers. Every era\n // also created `idx_<table>_hash`, a second btree on the same column,\n // which each sign-in, refresh, reset and magic link paid to maintain\n // and never used. Dropped where present — read from the catalogue\n // above, so a database without them issues nothing.\n const duplicateHashIndexes = DUPLICATE_TOKEN_HASH_INDEXES.filter(name => indexNames.has(`${authSchema}.${name}`));\n if (duplicateHashIndexes.length > 0) {\n try {\n await db.execute(sql.raw(dropDuplicateTokenHashIndexesSql(authSchema, duplicateHashIndexes)));\n } catch (err) {\n // Redundant, not wrong: a table whose lock another session\n // held for the whole budget keeps its extra index until a\n // later boot drops it.\n logger.warn(\n `⚠️ Left the duplicate token-hash index(es) ${duplicateHashIndexes.join(\", \")} in place for now: ` +\n (err instanceof Error ? err.message : String(err))\n );\n }\n }\n\n // Create app config table\n await db.execute(sql`\n CREATE TABLE IF NOT EXISTS ${sql.raw(appConfigTableName)} (\n key TEXT PRIMARY KEY,\n value JSONB NOT NULL,\n updated_at TIMESTAMP WITH TIME ZONE DEFAULT NOW()\n )\n `);\n\n // The RLS helper functions every generated policy calls. They live in\n // `rebase`, alongside the tables above — Rebase creates exactly one\n // schema in a user's database. Advisory-locked so concurrent HMR\n // reloads cannot race on `CREATE OR REPLACE`.\n //\n // The same statements the migration preamble carries, from the same\n // constant — these definitions being identical across the boot path and\n // the migration stream is the whole point of having them in one place.\n // One call per statement: this handle speaks the extended query\n // protocol, which rejects multi-command strings.\n //\n // Only when the catalogue says they differ: replacing an unchanged\n // function still rewrites it, and invalidates every cached plan a\n // policy calling it is part of. See `rlsRuntimeIsCurrent`.\n if (!await rlsRuntimeIsCurrent(runRaw).catch(() => false)) {\n await db.transaction(async (tx) => {\n await tx.execute(sql`SELECT pg_advisory_xact_lock(hashtext('rebase_auth_functions_init'))`);\n for (const statement of RLS_BOOTSTRAP_STATEMENTS) {\n await tx.execute(sql.raw(statement));\n }\n });\n }\n\n // Seed default roles if none exist\n // (no-op: roles are now stored inline on the users table)\n\n // ── Migration: reconcile the full users column set (safe for existing tables) ──\n // CREATE TABLE IF NOT EXISTS never revisits an existing table, so a\n // database provisioned by an older framework era is missing every\n // column added since. Each column the auth services read or write must\n // be back-filled here, or upgraded deployments break on the first\n // statement that references it.\n //\n // `email` is skipped: it has existed since the first era, so it is never\n // the missing one, and `ADD COLUMN … NOT NULL` with no default fails on\n // a table with rows.\n //\n // Only the columns the table is missing are added. `ADD COLUMN IF NOT\n // EXISTS` on a column that exists changes nothing and still queues for\n // the table's ACCESS EXCLUSIVE lock — on the table every sign-in reads —\n // and it ran for every column on every boot.\n type UsersColumnRow = {\n column_name: string;\n data_type: string;\n is_nullable: \"YES\" | \"NO\";\n column_default: string | null;\n };\n // Which of the columns the table actually has. An adopted table — one\n // this framework did not create, which the column-name resolution in\n // `services.ts` exists to support — may be missing any of them, and every\n // statement past this point has to tolerate that rather than abort the\n // whole migration block.\n const readUsersColumns = async (): Promise<UsersColumnRow[]> => (await db.execute(sql`\n SELECT column_name, data_type, is_nullable, column_default\n FROM information_schema.columns\n WHERE table_schema = ${usersSchema} AND table_name = ${resolvedTable}\n `)).rows as UsersColumnRow[];\n let usersColumnRows = await readUsersColumns();\n const usersColumnsBefore = new Set(usersColumnRows.map(row => row.column_name));\n let usersColumnsAdded = false;\n for (const spec of AUTH_USERS_COLUMNS) {\n if (spec.column === \"email\") continue;\n if (usersColumnsBefore.has(spec.column)) continue;\n await db.execute(sql`\n ALTER TABLE ${sql.raw(usersTableName)}\n ADD COLUMN IF NOT EXISTS ${sql.raw(`${spec.column} ${authUsersColumnSql(spec)}`)}\n `);\n usersColumnsAdded = true;\n }\n if (usersColumnsAdded) usersColumnRows = await readUsersColumns();\n const usersColumnTypes = new Map(usersColumnRows.map(row => [row.column_name, row.data_type]));\n const usersColumnState = new Map(usersColumnRows.map(row => [row.column_name, row]));\n\n // ── Migration: restore defaults and NOT NULL that another creator dropped ──\n // `ADD COLUMN IF NOT EXISTS` above only creates what is MISSING. A column\n // that exists with the wrong shape stays wrong forever — and until\n // AUTH_USERS_COLUMNS became the single source, that was the normal\n // outcome rather than an edge case: whichever of `db push`, boot-ensure\n // and this function reached the table first decided its constraints, so\n // a managed deploy ended up with a nullable `email`, a `roles` with no\n // `'{}'` default, and an `email_verified` that could be NULL.\n //\n // Ordered DEFAULT → back-fill → SET NOT NULL, because SET NOT NULL is\n // checked against existing rows: without the back-fill it throws on the\n // very databases that need it. `email` can carry no default, so a NULL\n // there is not repairable automatically — say so and leave the column\n // alone rather than inventing an address.\n for (const spec of AUTH_USERS_COLUMNS) {\n const state = usersColumnState.get(spec.column);\n if (!state) continue;\n\n if (spec.default !== undefined && state.column_default === null) {\n await db.execute(sql`\n ALTER TABLE ${sql.raw(usersTableName)}\n ALTER COLUMN ${sql.raw(`\"${spec.column}\"`)} SET DEFAULT ${sql.raw(spec.default)}\n `);\n logger.info(`🔧 Restored the default on ${usersTableName}.${spec.column}`);\n }\n\n if (!spec.notNull || state.is_nullable !== \"YES\") continue;\n\n if (spec.default !== undefined) {\n await db.execute(sql`\n UPDATE ${sql.raw(usersTableName)}\n SET ${sql.raw(`\"${spec.column}\"`)} = ${sql.raw(spec.default)}\n WHERE ${sql.raw(`\"${spec.column}\"`)} IS NULL\n `);\n }\n try {\n await db.execute(sql`\n ALTER TABLE ${sql.raw(usersTableName)}\n ALTER COLUMN ${sql.raw(`\"${spec.column}\"`)} SET NOT NULL\n `);\n logger.info(`🔧 Restored NOT NULL on ${usersTableName}.${spec.column}`);\n } catch (err) {\n logger.warn(\n `⚠️ ${usersTableName}.${spec.column} should be NOT NULL but still holds NULLs, so the ` +\n \"constraint was not applied. Fill or remove those rows and restart: \" +\n (err instanceof Error ? err.message : String(err))\n );\n }\n }\n\n // ── Migration: VARCHAR(n) → TEXT on the users string columns ────────\n // Tables created before the widths came off still carry them. Postgres\n // treats varchar(n) → text as binary-coercible with no stricter\n // constraint, so this is a catalogue-only change: no table rewrite, no\n // index rebuild, just a brief ACCESS EXCLUSIVE lock. Guarded on the\n // current type so it runs once and is a pure catalogue read thereafter.\n for (const column of [\"email\", \"display_name\", \"photo_url\", \"password_hash\", \"email_verification_token\"]) {\n if (usersColumnTypes.get(column) !== \"character varying\") continue;\n await db.execute(sql`\n ALTER TABLE ${sql.raw(usersTableName)}\n ALTER COLUMN ${sql.raw(`\"${column}\"`)} TYPE TEXT\n `);\n logger.info(`🔧 Widened ${usersTableName}.${column} from VARCHAR(n) to TEXT`);\n }\n\n // ── Migration: case-insensitive email identity ──────────────────────\n // `getUserByEmail` has always searched `email.toLowerCase()` while the\n // write path stored whatever it was handed, leaving normalisation to a\n // convention every caller had to remember. A row that reached the table\n // with mixed case is then invisible to every lookup — the account exists,\n // login reports no such user, and the plain UNIQUE on `email` does not\n // stop a second row differing only in case, because it compares bytes.\n //\n // Fixed on both sides: `mapPayload` now folds on write, and this index\n // makes the database agree. A unique index on lower(email) is strictly\n // stronger than the byte-exact UNIQUE that older tables carry, so the\n // old constraint is left alone — it can no longer fire on anything the\n // new one would allow.\n //\n // Deliberately no AUTH_SCHEMA_VERSION bump: a runtime that predates this\n // migration keeps working against the migrated table (all of its own\n // write paths already lower-cased), which is exactly the additive case\n // the version stamp is documented not to cover.\n if (usersColumnTypes.has(\"email\")) {\n if (!indexNames.has(`${usersSchema}.${emailLowerUniqueIndex}`)) {\n // Case-collisions already in the table would make the unique\n // index impossible to build. Report them and leave the table\n // alone: the next boot retries, so fixing the rows is all the\n // operator has to do. Failing loudly beats folding the emails\n // and letting CREATE INDEX pick which account survives.\n const collisions = await db.execute(sql`\n SELECT lower(email) AS normalized, count(*)::int AS occurrences\n FROM ${sql.raw(usersTableName)}\n WHERE email IS NOT NULL\n GROUP BY lower(email)\n HAVING count(*) > 1\n LIMIT 10\n `);\n if (collisions.rows.length > 0) {\n const sample = (collisions.rows as { normalized: string; occurrences: number }[])\n .map(row => `${row.normalized} (×${row.occurrences})`)\n .join(\", \");\n logger.error(\n `❌ Cannot enforce case-insensitive email uniqueness on ${usersTableName}: ` +\n `these addresses already exist more than once, differing only in case — ${sample}. ` +\n \"Merge or delete the duplicates and restart; until then two accounts can share \" +\n \"one address and only the lower-cased one is reachable by login.\"\n );\n } else {\n const folded = await db.execute(sql`\n UPDATE ${sql.raw(usersTableName)}\n SET email = lower(email)\n WHERE email IS NOT NULL AND email <> lower(email)\n `);\n if (folded.rowCount) {\n logger.info(`🔧 Lower-cased ${folded.rowCount} email address(es) in ${usersTableName}`);\n }\n await db.execute(sql`\n CREATE UNIQUE INDEX IF NOT EXISTS ${sql.raw(`\"${emailLowerUniqueIndex}\"`)}\n ON ${sql.raw(usersTableName)} (lower(email))\n `);\n logger.info(`✅ Email uniqueness on ${usersTableName} is now case-insensitive`);\n }\n }\n }\n\n // ── Migration: bound the email column's length ──────────────────────\n // The only length limit on this table worth keeping. 320 is the RFC 5321\n // maximum (64-char local part + @ + 255-char domain), and it matters here\n // beyond tidiness: `email` carries a btree index, and a sufficiently long\n // value fails index insertion with an error that says nothing about\n // email. NOT VALID so an adopted table with a long row still migrates —\n // it binds all new writes, which is the part that matters.\n if (usersColumnTypes.has(\"email\")) {\n const checkPresent = await db.execute(sql`\n SELECT 1 FROM pg_constraint c\n JOIN pg_class t ON t.oid = c.conrelid\n JOIN pg_namespace n ON n.oid = t.relnamespace\n WHERE n.nspname = ${usersSchema}\n AND t.relname = ${resolvedTable}\n AND c.conname = ${authIdentifier(\"email_length_check\")}\n `);\n if (checkPresent.rows.length === 0) {\n await db.execute(sql`\n ALTER TABLE ${sql.raw(usersTableName)}\n ADD CONSTRAINT ${sql.raw(emailLengthConstraint)} CHECK (length(email) <= 320) NOT VALID\n `);\n }\n }\n\n // ── Index: email verification token lookups ─────────────────────────\n // `getUserByVerificationToken` filters on this column, which had no\n // index — every click of a verification link was a sequential scan of\n // the whole users table. Partial, because the column is NULL for every\n // user who is not mid-verification, which is nearly all of them.\n if (usersColumnTypes.has(\"email_verification_token\")) {\n await ensureIndex(usersSchema, verificationTokenIndex, sql`\n CREATE INDEX IF NOT EXISTS ${sql.raw(`\"${verificationTokenIndex}\"`)}\n ON ${sql.raw(usersTableName)} (email_verification_token)\n WHERE email_verification_token IS NOT NULL\n `);\n }\n // The same for the address-change link, which is looked up the same way.\n if (usersColumnTypes.has(\"email_change_token\")) {\n await ensureIndex(usersSchema, emailChangeTokenIndex, sql`\n CREATE INDEX IF NOT EXISTS ${sql.raw(`\"${emailChangeTokenIndex}\"`)}\n ON ${sql.raw(usersTableName)} (email_change_token)\n WHERE email_change_token IS NOT NULL\n `);\n }\n\n // ── Migration: refresh_tokens become session-scoped, rotation-safe ──\n // Two shapes are reconciled here, on EVERY table named refresh_tokens\n // in whatever schema it lives (a database provisioned by an older era\n // can carry the table in a different schema than the one this run\n // derives, and auth would then read a table nobody migrated):\n //\n // 1. The new columns. A token is now a member of a session\n // (`session_id`) and is retained after rotation (`revoked`,\n // `rotated_at`) so a replayed token can be recognised instead of\n // looking like a forgery. `session_started_at` is carried across\n // rotations so `users.tokens_valid_after` cannot be outrun.\n // 2. The removal of `unique_device_session`. It made (uid,\n // user_agent, ip_address) the identity of a session, which evicted\n // a second browser profile behind one NAT and churned rows as\n // phones changed networks. UA and IP are metadata now.\n //\n // Every existing row is adopted rather than dropped: it keeps its\n // token_hash, gets a session of its own, and stays unrevoked — so the\n // sessions live in browsers right now survive the upgrade rather than\n // everyone being signed out by the fix for being signed out.\n try {\n const rtTables = await db.execute(sql`\n SELECT table_schema, table_name\n FROM information_schema.tables\n WHERE table_name = 'refresh_tokens'\n `);\n const found = (rtTables.rows as { table_schema: string; table_name: string }[]);\n logger.debug(`🔍 refresh_tokens reconcile: found ${found.length} table(s): ${found.map(r => `\"${r.table_schema}\".\"${r.table_name}\"`).join(\", \") || \"(none)\"}`);\n // What each of those tables already has, read once, so a table that\n // is already session-scoped gets no statement at all. Each step\n // below used to run on every boot: six `ADD COLUMN IF NOT EXISTS`,\n // two `SET DEFAULT`, two `SET NOT NULL` and a `DROP CONSTRAINT IF\n // EXISTS` — eleven waits for the ACCESS EXCLUSIVE lock on the table\n // every token refresh writes — and two `UPDATE … WHERE … IS NULL`\n // back-fills that scan the whole table, since nothing indexes them.\n const rtSchemas = found.map(r => r.table_schema);\n const rtColumnRows = rtSchemas.length === 0 ? [] : (await db.execute(sql.raw(\n \"SELECT table_schema, column_name, is_nullable, column_default FROM information_schema.columns \" +\n `WHERE table_name = 'refresh_tokens' AND table_schema IN (${rtSchemas.map(literal).join(\", \")})`\n ))).rows as { table_schema: string; column_name: string; is_nullable: \"YES\" | \"NO\"; column_default: string | null }[];\n const rtDeviceConstraint = new Set(rtSchemas.length === 0 ? [] : ((await db.execute(sql.raw(\n \"SELECT n.nspname AS schema FROM pg_constraint c JOIN pg_class t ON t.oid = c.conrelid \" +\n \"JOIN pg_namespace n ON n.oid = t.relnamespace \" +\n \"WHERE t.relname = 'refresh_tokens' AND c.conname = 'unique_device_session' \" +\n `AND n.nspname IN (${rtSchemas.map(literal).join(\", \")})`\n ))).rows as { schema: string }[]).map(row => row.schema));\n const rtIndexNames = await readIndexNames(db, rtSchemas);\n for (const { table_schema } of found) {\n const qualified = `\"${table_schema}\".\"refresh_tokens\"`;\n const columns = new Map(rtColumnRows.filter(row => row.table_schema === table_schema).map(row => [row.column_name, row]));\n /** Missing, or present and nullable: a column the back-fill and SET NOT NULL still have to reach. */\n const nullable = (column: string) => columns.get(column)?.is_nullable !== \"NO\";\n const addColumn = async (column: string, statement: ReturnType<typeof sql>) => {\n if (!columns.has(column)) await db.execute(statement);\n };\n try {\n // Added nullable, then back-filled, then constrained: adding\n // `session_id NOT NULL DEFAULT gen_random_uuid()` in one step\n // would stamp every existing row with the SAME uuid on some\n // Postgres versions, silently merging every live session into\n // one that a single logout would then wipe.\n await addColumn(\"session_id\", sql`ALTER TABLE ${sql.raw(qualified)} ADD COLUMN IF NOT EXISTS session_id TEXT`);\n await addColumn(\"revoked\", sql`ALTER TABLE ${sql.raw(qualified)} ADD COLUMN IF NOT EXISTS revoked BOOLEAN DEFAULT FALSE NOT NULL`);\n await addColumn(\"rotated_at\", sql`ALTER TABLE ${sql.raw(qualified)} ADD COLUMN IF NOT EXISTS rotated_at TIMESTAMP WITH TIME ZONE`);\n await addColumn(\"session_started_at\", sql`ALTER TABLE ${sql.raw(qualified)} ADD COLUMN IF NOT EXISTS session_started_at TIMESTAMP WITH TIME ZONE`);\n // Nullable with no default and no back-fill: a row written\n // before this column existed says nothing about whether a\n // second factor was presented, and the reader treats \"says\n // nothing\" as `aal1` — the restrictive answer. Stamping\n // every existing row would be inventing evidence.\n await addColumn(\"aal\", sql`ALTER TABLE ${sql.raw(qualified)} ADD COLUMN IF NOT EXISTS aal TEXT`);\n // How the sign-in happened, carried across rotations like\n // `aal`. Nullable and not back-filled for the same reason:\n // an older row reads as a password sign-in, which is what\n // every refresh answered before the column existed.\n await addColumn(\"method\", sql`ALTER TABLE ${sql.raw(qualified)} ADD COLUMN IF NOT EXISTS method TEXT`);\n\n // One session per pre-existing row: under the old model a row\n // WAS a device session, and there is no record of which rows\n // descended from the same sign-in. A NOT NULL column holds no\n // NULL to back-fill.\n if (nullable(\"session_id\")) {\n await db.execute(sql`\n UPDATE ${sql.raw(qualified)}\n SET session_id = gen_random_uuid()::text\n WHERE session_id IS NULL\n `);\n }\n if (nullable(\"session_started_at\")) {\n await db.execute(sql`\n UPDATE ${sql.raw(qualified)}\n SET session_started_at = COALESCE(created_at, NOW())\n WHERE session_started_at IS NULL\n `);\n }\n if (!sameDefault(columns.get(\"session_id\")?.column_default ?? null, \"gen_random_uuid()::text\")) {\n await db.execute(sql`ALTER TABLE ${sql.raw(qualified)} ALTER COLUMN session_id SET DEFAULT gen_random_uuid()::text`);\n }\n if (!sameDefault(columns.get(\"session_started_at\")?.column_default ?? null, \"now()\")) {\n await db.execute(sql`ALTER TABLE ${sql.raw(qualified)} ALTER COLUMN session_started_at SET DEFAULT NOW()`);\n }\n // SET NOT NULL only once the back-fill above has definitely\n // run; on a table that somehow still holds a NULL this throws\n // and is caught below rather than failing the boot.\n if (nullable(\"session_id\")) {\n await db.execute(sql`ALTER TABLE ${sql.raw(qualified)} ALTER COLUMN session_id SET NOT NULL`);\n }\n if (nullable(\"session_started_at\")) {\n await db.execute(sql`ALTER TABLE ${sql.raw(qualified)} ALTER COLUMN session_started_at SET NOT NULL`);\n }\n\n if (!rtIndexNames.has(`${table_schema}.idx_refresh_tokens_session`)) {\n await db.execute(sql`\n CREATE INDEX IF NOT EXISTS idx_refresh_tokens_session\n ON ${sql.raw(qualified)}(session_id)\n `);\n }\n\n // The device-session constraint is now actively harmful: two\n // live tokens of one session (a rotation in flight) share a\n // uid, and usually a user agent and IP too.\n if (rtDeviceConstraint.has(table_schema)) {\n await db.execute(sql`ALTER TABLE ${sql.raw(qualified)} DROP CONSTRAINT IF EXISTS unique_device_session`);\n }\n logger.debug(`✅ refresh_tokens reconciled for session-scoped rotation: ${qualified}`);\n } catch (perTableError: unknown) {\n logger.warn(`⚠️ refresh_tokens reconcile failed for ${qualified}: ${perTableError instanceof Error ? perTableError.message : String(perTableError)}`);\n }\n }\n } catch (migrationError: unknown) {\n logger.warn(`⚠️ refresh_tokens session migration skipped: ${migrationError instanceof Error ? migrationError.message : String(migrationError)}`);\n }\n\n // ── Migration: Copy roles from legacy junction table to inline column ──\n // If the old rebase.user_roles and rebase.roles tables exist, migrate\n // the data into the new TEXT[] column then drop the legacy tables.\n try {\n const legacyCheck = await db.execute(sql`\n SELECT EXISTS (\n SELECT 1 FROM information_schema.tables\n WHERE table_schema = 'rebase' AND table_name = 'user_roles'\n ) AS has_user_roles\n `);\n const hasLegacyTables = (legacyCheck.rows[0] as { has_user_roles: boolean }).has_user_roles;\n\n if (hasLegacyTables) {\n logger.info(\"🔄 Migrating roles from legacy user_roles table...\");\n // Update users' roles column from the junction table\n await db.execute(sql`\n UPDATE ${sql.raw(usersTableName)} u\n SET roles = COALESCE((\n SELECT array_agg(ur.role_id)\n FROM \"rebase\".\"user_roles\" ur\n WHERE ur.user_id = u.id\n ), '{}')\n WHERE u.roles = '{}' OR u.roles IS NULL\n `);\n\n // Drop legacy tables (junction first due to FK)\n await db.execute(sql`DROP TABLE IF EXISTS \"rebase\".\"user_roles\" CASCADE`);\n await db.execute(sql`DROP TABLE IF EXISTS \"rebase\".\"roles\" CASCADE`);\n logger.info(\"✅ Legacy roles tables migrated and dropped\");\n }\n } catch (migrationError: unknown) {\n // Non-fatal: log and continue — the column exists and will work\n logger.warn(`⚠️ Legacy roles migration skipped: ${migrationError instanceof Error ? migrationError.message : String(migrationError)}`);\n }\n\n // ── MFA tables ──────────────────────────────────────────────────────\n const mfaFactorsTableName = `\"${authSchema}\".\"mfa_factors\"`;\n const mfaChallengesTableName = `\"${authSchema}\".\"mfa_challenges\"`;\n const recoveryCodesTableName = `\"${authSchema}\".\"recovery_codes\"`;\n\n // Create mfa_factors table\n await db.execute(sql`\n CREATE TABLE IF NOT EXISTS ${sql.raw(mfaFactorsTableName)} (\n id TEXT PRIMARY KEY DEFAULT gen_random_uuid()::text,\n uid ${sql.raw(userIdType)} NOT NULL REFERENCES ${sql.raw(usersTableName)}(id) ON DELETE CASCADE,\n factor_type TEXT NOT NULL DEFAULT 'totp',\n secret_encrypted TEXT NOT NULL,\n friendly_name TEXT,\n verified BOOLEAN DEFAULT FALSE,\n last_used_counter BIGINT,\n created_at TIMESTAMP WITH TIME ZONE DEFAULT NOW(),\n updated_at TIMESTAMP WITH TIME ZONE DEFAULT NOW()\n )\n `);\n\n // Create indexes on mfa_factors\n await ensureIndex(authSchema, \"idx_mfa_factors_user\", sql`\n CREATE INDEX IF NOT EXISTS idx_mfa_factors_user\n ON ${sql.raw(mfaFactorsTableName)}(uid)\n `);\n\n // Create mfa_challenges table\n await db.execute(sql`\n CREATE TABLE IF NOT EXISTS ${sql.raw(mfaChallengesTableName)} (\n id TEXT PRIMARY KEY DEFAULT gen_random_uuid()::text,\n factor_id TEXT NOT NULL REFERENCES ${sql.raw(mfaFactorsTableName)}(id) ON DELETE CASCADE,\n created_at TIMESTAMP WITH TIME ZONE DEFAULT NOW(),\n verified_at TIMESTAMP WITH TIME ZONE,\n ip_address TEXT,\n attempts INTEGER NOT NULL DEFAULT 0,\n expires_at TIMESTAMP WITH TIME ZONE NOT NULL\n )\n `);\n\n // Create indexes on mfa_challenges\n await ensureIndex(authSchema, \"idx_mfa_challenges_factor\", sql`\n CREATE INDEX IF NOT EXISTS idx_mfa_challenges_factor\n ON ${sql.raw(mfaChallengesTableName)}(factor_id)\n `);\n\n // ── Migration: replay and brute-force state on the MFA tables ───────\n // Both are additive and nullable-or-defaulted, so a runtime that\n // predates them reads the tables unchanged. `last_used_counter` records\n // the TOTP step a factor has already spent (RFC 6238 §5.2); `attempts`\n // bounds how many guesses one challenge will take before it is dead.\n // Without them the code paths degrade to \"no replay protection, rate\n // limiters only\" rather than failing, which is why this is a warn.\n // Read first, like the columns above: each was an ACCESS EXCLUSIVE wait\n // on every boot.\n try {\n const mfaColumns = new Set(((await db.execute(sql`\n SELECT table_name || '.' || column_name AS key\n FROM information_schema.columns\n WHERE table_schema = ${authSchema}\n AND table_name IN ('mfa_factors', 'mfa_challenges')\n AND column_name IN ('last_used_counter', 'attempts')\n `)).rows as { key: string }[]).map(row => row.key));\n if (!mfaColumns.has(\"mfa_factors.last_used_counter\")) {\n await db.execute(sql`ALTER TABLE ${sql.raw(mfaFactorsTableName)} ADD COLUMN IF NOT EXISTS last_used_counter BIGINT`);\n }\n if (!mfaColumns.has(\"mfa_challenges.attempts\")) {\n await db.execute(sql`ALTER TABLE ${sql.raw(mfaChallengesTableName)} ADD COLUMN IF NOT EXISTS attempts INTEGER NOT NULL DEFAULT 0`);\n }\n } catch (mfaMigrationError: unknown) {\n logger.warn(`⚠️ MFA hardening columns skipped: ${mfaMigrationError instanceof Error ? mfaMigrationError.message : String(mfaMigrationError)}`);\n }\n\n // Create recovery_codes table\n await db.execute(sql`\n CREATE TABLE IF NOT EXISTS ${sql.raw(recoveryCodesTableName)} (\n id TEXT PRIMARY KEY DEFAULT gen_random_uuid()::text,\n uid ${sql.raw(userIdType)} NOT NULL REFERENCES ${sql.raw(usersTableName)}(id) ON DELETE CASCADE,\n code_hash TEXT NOT NULL,\n used_at TIMESTAMP WITH TIME ZONE,\n created_at TIMESTAMP WITH TIME ZONE DEFAULT NOW()\n )\n `);\n\n // Create indexes on recovery_codes\n await ensureIndex(authSchema, \"idx_recovery_codes_user\", sql`\n CREATE INDEX IF NOT EXISTS idx_recovery_codes_user\n ON ${sql.raw(recoveryCodesTableName)}(uid)\n `);\n\n // ── Migration: clear stale FORCE ROW LEVEL SECURITY (older RLS model) ──\n // The current model never emits FORCE: privileged auth writes run as\n // the table owner and rely on the owner bypassing plain ENABLE RLS\n // (see generate-postgres-ddl-logic). A table still carrying FORCE from\n // an older framework era binds the owner too, so the first user\n // registration after an upgrade fails with SQLSTATE 42501. Reconcile\n // on boot; only tables actually flagged get the ALTER (and its lock).\n try {\n // Every table this function creates, not a subset. `magic_link_tokens`\n // and `schema_meta` were missing here while their six siblings were\n // listed — so on a database carrying FORCE from the older RLS model,\n // magic-link sign-in kept failing 42501 after the upgrade that was\n // supposed to fix exactly that, and only for the one auth method.\n const authTablePairs: [string, string][] = [\n [usersSchema, resolvedTable],\n [authSchema, \"user_identities\"],\n [authSchema, \"refresh_tokens\"],\n [authSchema, \"password_reset_tokens\"],\n [authSchema, \"magic_link_tokens\"],\n [authSchema, \"app_config\"],\n [authSchema, \"mfa_factors\"],\n [authSchema, \"mfa_challenges\"],\n [authSchema, \"recovery_codes\"],\n [authSchema, \"schema_meta\"]\n ];\n // One catalogue read for all of them, not one per table.\n const forced = new Set(((await db.execute(sql.raw(\n \"SELECT n.nspname AS schema, c.relname AS name FROM pg_class c \" +\n \"JOIN pg_namespace n ON n.oid = c.relnamespace \" +\n \"WHERE c.relforcerowsecurity AND (n.nspname, c.relname) IN (\" +\n authTablePairs.map(([schemaName, tableName]) => `(${literal(schemaName)}, ${literal(tableName)})`).join(\", \") +\n \")\"\n ))).rows as { schema: string; name: string }[]).map(row => `${row.schema}.${row.name}`));\n for (const [schemaName, tableName] of authTablePairs) {\n if (forced.has(`${schemaName}.${tableName}`)) {\n await db.execute(sql`\n ALTER TABLE ${sql.raw(`\"${schemaName}\".\"${tableName}\"`)}\n NO FORCE ROW LEVEL SECURITY\n `);\n logger.warn(\n `🔧 Cleared stale FORCE ROW LEVEL SECURITY on \"${schemaName}\".\"${tableName}\" ` +\n \"(legacy RLS model — it binds the owner connection and breaks privileged auth writes)\"\n );\n }\n }\n } catch (rlsReconcileError: unknown) {\n // Non-fatal: the connection may lack ownership on a pre-provisioned\n // table; registration will still fail loudly (42501) if FORCE remains.\n logger.warn(\n `⚠️ Could not reconcile FORCE ROW LEVEL SECURITY on auth tables: ` +\n `${rlsReconcileError instanceof Error ? rlsReconcileError.message : String(rlsReconcileError)}`\n );\n }\n\n // Stamped last of the MIGRATIONS, so a boot that died partway through\n // the ones above leaves the older stamp in place and the next boot runs\n // them again.\n await stampAuthSchemaVersion(db, authSchema);\n\n // ── Keep the end-user role out of auth's tables ─────────────────────\n // These carry session token hashes, TOTP secrets and recovery codes, and\n // none of them has RLS — they are not collections, so nothing ever\n // compiled a policy for them. Meanwhile the role provisioning grants\n // `rebase_user` DML on every table in this schema, and its\n // ALTER DEFAULT PRIVILEGES reaches the ones created right here, after it\n // ran. So the grant has to come back off; see `revokeInternalTableSql`\n // for why a revoke rather than an empty RLS policy set.\n //\n // After the stamp deliberately: `schema_meta` is created BY the stamp,\n // so revoking first would leave the one table holding this database's\n // schema version writable by every signed-in user until the next boot.\n // Nothing below re-runs the migrations, so the stamp's guarantee holds.\n //\n // Only the tables the user role still holds a privilege on — one read\n // instead of a `REVOKE` per name, each rewriting the table's catalogue\n // row on every boot. A read that fails sweeps every name, as before.\n const stillGranted = await internalTablesGrantedToUserRole(runRaw, authSchema).catch(() => undefined);\n if (stillGranted === undefined || stillGranted.length > 0) {\n await revokeInternalTableAccess(\n async (text) => { await db.execute(sql.raw(text)); },\n authSchema,\n {\n tables: stillGranted,\n onError: (table, err) => logger.warn(\n `🔐 Could not revoke authenticated-role access to \"${authSchema}\".\"${table}\": ` +\n (err instanceof Error ? err.message : String(err))\n )\n }\n );\n }\n\n logger.debug(\"✅ Auth tables ready\");\n } catch (error) {\n // The one failure that must not be survived. Continuing here is what\n // produced a server that answered /health with 200 while every login\n // returned 500 — the incompatibility is total, so crashing is the\n // kinder outcome: an orchestrator will not route traffic to a pod that\n // never came up.\n if (error instanceof AuthSchemaVersionError) throw error;\n logger.error(\"❌ Failed to create auth tables\", { error });\n logger.warn(\"⚠️ Continuing without creating auth tables.\");\n }\n}\n\n"],"mappings":"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"}
|
|
@@ -0,0 +1,56 @@
|
|
|
1
|
+
import { createRequire as __createRequire } from "module";
|
|
2
|
+
__createRequire(import.meta.url);
|
|
3
|
+
import { n as planSchema } from "./plan-schema-B1YF_tYq.js";
|
|
4
|
+
import { i as readExistingSchema } from "./ensure-collection-tables-BhsU6qUE.js";
|
|
5
|
+
import { declaredDatabaseExtensions } from "@rebasepro/types";
|
|
6
|
+
//#region src/schema/enum-labels-kept.ts
|
|
7
|
+
/**
|
|
8
|
+
* Enum labels the database holds and the collections no longer declare.
|
|
9
|
+
*
|
|
10
|
+
* PostgreSQL has no `ALTER TYPE … DROP VALUE`, and Atlas plans nothing for a
|
|
11
|
+
* removed label — `db push` reported success while the database kept the old
|
|
12
|
+
* label, so writes of it (by SQL, or an older client) went on succeeding, and
|
|
13
|
+
* `rebase doctor` sent the developer back to the same push. Boot behaves the
|
|
14
|
+
* same way by design: it adds labels and never removes one. So this is not a
|
|
15
|
+
* refusal — the push stays what boot would do — but the push says it, with the
|
|
16
|
+
* real way to retire a label.
|
|
17
|
+
*/
|
|
18
|
+
/** Pure: the declared enums against the catalogue's labels, keyed `schema.type`. */
|
|
19
|
+
function findKeptEnumLabels(declared, existing) {
|
|
20
|
+
const kept = [];
|
|
21
|
+
for (const plan of declared) {
|
|
22
|
+
const inDatabase = existing.get(plan.qualified);
|
|
23
|
+
if (!inDatabase) continue;
|
|
24
|
+
const labels = inDatabase.filter((label) => !plan.labels.includes(label));
|
|
25
|
+
if (labels.length > 0) kept.push({
|
|
26
|
+
type: plan.qualified,
|
|
27
|
+
labels
|
|
28
|
+
});
|
|
29
|
+
}
|
|
30
|
+
return kept;
|
|
31
|
+
}
|
|
32
|
+
/** Plans the collections and reads the catalogue's enum labels. */
|
|
33
|
+
async function keptEnumLabels(client, collections) {
|
|
34
|
+
const plan = planSchema(collections, { databaseExtensions: declaredDatabaseExtensions() });
|
|
35
|
+
if (plan.enums.length === 0) return [];
|
|
36
|
+
const existing = await readExistingSchema(client, Array.from(new Set(plan.enums.map((e) => e.schema))), { probeTables: () => [] });
|
|
37
|
+
return findKeptEnumLabels(plan.enums, existing.enumValues ?? /* @__PURE__ */ new Map());
|
|
38
|
+
}
|
|
39
|
+
/** What the push prints: which labels stay, and how one is actually retired. */
|
|
40
|
+
function formatKeptEnumLabelsNotice(kept) {
|
|
41
|
+
const lines = kept.map((k) => ` ${k.type}: ${k.labels.map((l) => `"${l}"`).join(", ")}`);
|
|
42
|
+
return [
|
|
43
|
+
` ⚠️ ${kept.length === 1 ? "An enum keeps a label" : `${kept.length} enums keep labels`} your collections no longer declare:`,
|
|
44
|
+
...lines,
|
|
45
|
+
" PostgreSQL has no ALTER TYPE … DROP VALUE, so a push (like boot) adds labels",
|
|
46
|
+
" and never removes one: the database still accepts these values. To retire one,",
|
|
47
|
+
" move its rows to another value with an UPDATE, then rewrite the type in a",
|
|
48
|
+
" migration (rebase db generate <name>: CREATE TYPE …, ALTER TABLE … TYPE … USING,",
|
|
49
|
+
" DROP TYPE) and apply it with rebase db migrate.",
|
|
50
|
+
""
|
|
51
|
+
].join("\n");
|
|
52
|
+
}
|
|
53
|
+
//#endregion
|
|
54
|
+
export { formatKeptEnumLabelsNotice, keptEnumLabels };
|
|
55
|
+
|
|
56
|
+
//# sourceMappingURL=enum-labels-kept-D8fI1MoX.js.map
|
|
@@ -0,0 +1 @@
|
|
|
1
|
+
{"version":3,"file":"enum-labels-kept-D8fI1MoX.js","names":[],"sources":["../src/schema/enum-labels-kept.ts"],"sourcesContent":["/**\n * Enum labels the database holds and the collections no longer declare.\n *\n * PostgreSQL has no `ALTER TYPE … DROP VALUE`, and Atlas plans nothing for a\n * removed label — `db push` reported success while the database kept the old\n * label, so writes of it (by SQL, or an older client) went on succeeding, and\n * `rebase doctor` sent the developer back to the same push. Boot behaves the\n * same way by design: it adds labels and never removes one. So this is not a\n * refusal — the push stays what boot would do — but the push says it, with the\n * real way to retire a label.\n */\nimport { declaredDatabaseExtensions, type CollectionConfig } from \"@rebasepro/types\";\nimport { planSchema } from \"./plan/plan-schema\";\nimport type { EnumPlan } from \"./plan/types\";\nimport { readExistingSchema, type Queryable } from \"./ensure-collection-tables\";\n\nexport interface KeptEnumLabels {\n /** `schema.type`, as the catalogue names it. */\n type: string;\n /** Labels in the database that no collection declares, in the type's order. */\n labels: string[];\n}\n\n/** Pure: the declared enums against the catalogue's labels, keyed `schema.type`. */\nexport function findKeptEnumLabels(\n declared: Pick<EnumPlan, \"qualified\" | \"labels\">[],\n existing: ReadonlyMap<string, string[]>\n): KeptEnumLabels[] {\n const kept: KeptEnumLabels[] = [];\n for (const plan of declared) {\n const inDatabase = existing.get(plan.qualified);\n if (!inDatabase) continue;\n const labels = inDatabase.filter(label => !plan.labels.includes(label));\n if (labels.length > 0) kept.push({ type: plan.qualified, labels });\n }\n return kept;\n}\n\n/** Plans the collections and reads the catalogue's enum labels. */\nexport async function keptEnumLabels(client: Queryable, collections: CollectionConfig[]): Promise<KeptEnumLabels[]> {\n const plan = planSchema(collections, { databaseExtensions: declaredDatabaseExtensions() });\n if (plan.enums.length === 0) return [];\n const schemas = Array.from(new Set(plan.enums.map(e => e.schema)));\n const existing = await readExistingSchema(client, schemas, { probeTables: () => [] });\n return findKeptEnumLabels(plan.enums, existing.enumValues ?? new Map());\n}\n\n/** What the push prints: which labels stay, and how one is actually retired. */\nexport function formatKeptEnumLabelsNotice(kept: KeptEnumLabels[]): string {\n const lines = kept.map(k => ` ${k.type}: ${k.labels.map(l => `\"${l}\"`).join(\", \")}`);\n return [\n ` ⚠️ ${kept.length === 1 ? \"An enum keeps a label\" : `${kept.length} enums keep labels`} your collections no longer declare:`,\n ...lines,\n \" PostgreSQL has no ALTER TYPE … DROP VALUE, so a push (like boot) adds labels\",\n \" and never removes one: the database still accepts these values. To retire one,\",\n \" move its rows to another value with an UPDATE, then rewrite the type in a\",\n \" migration (rebase db generate <name>: CREATE TYPE …, ALTER TABLE … TYPE … USING,\",\n \" DROP TYPE) and apply it with rebase db migrate.\",\n \"\"\n ].join(\"\\n\");\n}\n"],"mappings":";;;;;;;;;;;;;;;;;;AAwBA,SAAgB,mBACZ,UACA,UACgB;CAChB,MAAM,OAAyB,CAAC;CAChC,KAAK,MAAM,QAAQ,UAAU;EACzB,MAAM,aAAa,SAAS,IAAI,KAAK,SAAS;EAC9C,IAAI,CAAC,YAAY;EACjB,MAAM,SAAS,WAAW,QAAO,UAAS,CAAC,KAAK,OAAO,SAAS,KAAK,CAAC;EACtE,IAAI,OAAO,SAAS,GAAG,KAAK,KAAK;GAAE,MAAM,KAAK;GAAW;EAAO,CAAC;CACrE;CACA,OAAO;AACX;;AAGA,eAAsB,eAAe,QAAmB,aAA4D;CAChH,MAAM,OAAO,WAAW,aAAa,EAAE,oBAAoB,2BAA2B,EAAE,CAAC;CACzF,IAAI,KAAK,MAAM,WAAW,GAAG,OAAO,CAAC;CAErC,MAAM,WAAW,MAAM,mBAAmB,QAD1B,MAAM,KAAK,IAAI,IAAI,KAAK,MAAM,KAAI,MAAK,EAAE,MAAM,CAAC,CACd,GAAS,EAAE,mBAAmB,CAAC,EAAE,CAAC;CACpF,OAAO,mBAAmB,KAAK,OAAO,SAAS,8BAAc,IAAI,IAAI,CAAC;AAC1E;;AAGA,SAAgB,2BAA2B,MAAgC;CACvE,MAAM,QAAQ,KAAK,KAAI,MAAK,UAAU,EAAE,KAAK,IAAI,EAAE,OAAO,KAAI,MAAK,IAAI,EAAE,EAAE,CAAC,CAAC,KAAK,IAAI,GAAG;CACzF,OAAO;EACH,SAAS,KAAK,WAAW,IAAI,0BAA0B,GAAG,KAAK,OAAO,oBAAoB;EAC1F,GAAG;EACH;EACA;EACA;EACA;EACA;EACA;CACJ,CAAC,CAAC,KAAK,IAAI;AACf"}
|
|
@@ -0,0 +1,57 @@
|
|
|
1
|
+
import { createRequire as __createRequire } from "module";
|
|
2
|
+
__createRequire(import.meta.url);
|
|
3
|
+
import { r as out } from "./source-DBDbBTL4.js";
|
|
4
|
+
import { n as writeDrizzleSchema } from "./generate-files-CdEckX6c.js";
|
|
5
|
+
import path from "path";
|
|
6
|
+
//#region src/schema/generate-drizzle-schema.ts
|
|
7
|
+
/**
|
|
8
|
+
* The generator as its own process — what `--watch` runs, because a watch has
|
|
9
|
+
* to re-import the collections on every save and Node's module cache would
|
|
10
|
+
* serve it the first version forever. One-shot generation runs in the driver's
|
|
11
|
+
* own process instead; see `generate-files.ts`.
|
|
12
|
+
*/
|
|
13
|
+
var runGeneration = async (collectionsFilePath, outputPath, nextStep = true) => {
|
|
14
|
+
if (!collectionsFilePath) throw new Error("No collections file path provided.");
|
|
15
|
+
try {
|
|
16
|
+
await writeDrizzleSchema({
|
|
17
|
+
collectionsPath: path.resolve(collectionsFilePath),
|
|
18
|
+
outputPath,
|
|
19
|
+
nextStep
|
|
20
|
+
});
|
|
21
|
+
} catch (error) {
|
|
22
|
+
process.exitCode = 1;
|
|
23
|
+
throw error;
|
|
24
|
+
}
|
|
25
|
+
};
|
|
26
|
+
var main = async () => {
|
|
27
|
+
const collectionsFilePathArg = process.argv.find((arg) => arg.startsWith("--collections="));
|
|
28
|
+
const collectionsFilePath = collectionsFilePathArg ? collectionsFilePathArg.split("=")[1] : process.argv[2];
|
|
29
|
+
const outputPathArg = process.argv.find((arg) => arg.startsWith("--output="));
|
|
30
|
+
const outputPath = outputPathArg ? outputPathArg.split("=")[1] : void 0;
|
|
31
|
+
const watch = process.argv.includes("--watch");
|
|
32
|
+
const nextStep = !process.argv.includes("--no-next-step");
|
|
33
|
+
if (!collectionsFilePath) {
|
|
34
|
+
out("Usage: ts-node generate-drizzle-schema.ts <path-to-collections-file> [--output <path-to-output-file>] [--watch]");
|
|
35
|
+
return;
|
|
36
|
+
}
|
|
37
|
+
const resolvedPath = path.resolve(process.cwd(), collectionsFilePath);
|
|
38
|
+
const resolvedOutputPath = outputPath ? path.resolve(process.cwd(), outputPath) : void 0;
|
|
39
|
+
if (watch) {
|
|
40
|
+
out(`Watching for changes in ${resolvedPath}...`);
|
|
41
|
+
const { default: chokidar } = await import("chokidar");
|
|
42
|
+
chokidar.watch(resolvedPath, {
|
|
43
|
+
persistent: true,
|
|
44
|
+
ignoreInitial: false
|
|
45
|
+
}).on("all", (event, filePath) => {
|
|
46
|
+
out(`[${event}] ${filePath}. Regenerating schema...`);
|
|
47
|
+
runGeneration(resolvedPath, resolvedOutputPath, nextStep).catch(() => void 0);
|
|
48
|
+
});
|
|
49
|
+
} else await runGeneration(resolvedPath, resolvedOutputPath, nextStep);
|
|
50
|
+
};
|
|
51
|
+
if (import.meta.url.endsWith(process.argv[1])) main().catch(() => {
|
|
52
|
+
process.exitCode = 1;
|
|
53
|
+
});
|
|
54
|
+
//#endregion
|
|
55
|
+
export { main as t };
|
|
56
|
+
|
|
57
|
+
//# sourceMappingURL=generate-drizzle-schema-Bz52jH2h.js.map
|
|
@@ -0,0 +1 @@
|
|
|
1
|
+
{"version":3,"file":"generate-drizzle-schema-Bz52jH2h.js","names":[],"sources":["../src/schema/generate-drizzle-schema.ts"],"sourcesContent":["import path from \"path\";\nimport { out } from \"../cli-output\";\nimport { writeDrizzleSchema } from \"./generate-files\";\n\n\n// --- Execution and Watch Logic ---\n\n/**\n * The generator as its own process — what `--watch` runs, because a watch has\n * to re-import the collections on every save and Node's module cache would\n * serve it the first version forever. One-shot generation runs in the driver's\n * own process instead; see `generate-files.ts`.\n */\nconst runGeneration = async (collectionsFilePath?: string, outputPath?: string, nextStep = true) => {\n if (!collectionsFilePath) {\n throw new Error(\"No collections file path provided.\");\n }\n try {\n await writeDrizzleSchema({ collectionsPath: path.resolve(collectionsFilePath), outputPath, nextStep });\n } catch (error) {\n // The same contract its sibling `generate-postgres-ddl.ts` keeps, and\n // for the same reason: a caller that reads nothing but this process's\n // exit code must not see 0 after an error was printed.\n process.exitCode = 1;\n throw error;\n }\n};\n\nexport const main = async () => {\n const collectionsFilePathArg = process.argv.find(arg => arg.startsWith(\"--collections=\"));\n const collectionsFilePath = collectionsFilePathArg ? collectionsFilePathArg.split(\"=\")[1] : process.argv[2];\n\n const outputPathArg = process.argv.find(arg => arg.startsWith(\"--output=\"));\n const outputPath = outputPathArg ? outputPathArg.split(\"=\")[1] : undefined;\n\n const watch = process.argv.includes(\"--watch\");\n // Set when `db push` / `db generate` run this as their first step.\n const nextStep = !process.argv.includes(\"--no-next-step\");\n\n if (!collectionsFilePath) {\n out(\"Usage: ts-node generate-drizzle-schema.ts <path-to-collections-file> [--output <path-to-output-file>] [--watch]\");\n return;\n }\n\n const resolvedPath = path.resolve(process.cwd(), collectionsFilePath);\n const resolvedOutputPath = outputPath ? path.resolve(process.cwd(), outputPath) : undefined;\n\n if (watch) {\n out(`Watching for changes in ${resolvedPath}...`);\n // Imported here rather than at module scope, and this is not a style\n // choice: chokidar is needed only by `--watch`, which is a\n // schema-authoring path that never runs inside the runtime image. A\n // top-level import puts it on the boot path of the published driver\n // bundle, and the image installs a hand-listed set of runtime\n // dependencies that does not include it — so the whole driver failed to\n // load with \"Cannot find package 'chokidar'\", and every self-hosted\n // container answered 500 with a stack trace about a file watcher.\n //\n // Same reasoning the image already applies to @ariga/atlas: an\n // authoring-only dependency does not belong on a boot path.\n const { default: chokidar } = await import(\"chokidar\");\n const watcher = chokidar.watch(resolvedPath, {\n persistent: true,\n ignoreInitial: false\n });\n\n watcher.on(\"all\", (event, filePath) => {\n out(`[${event}] ${filePath}. Regenerating schema...`);\n // A watch session outlives a bad edit; the next save is the retry.\n void runGeneration(resolvedPath, resolvedOutputPath, nextStep).catch(() => undefined);\n });\n } else {\n await runGeneration(resolvedPath, resolvedOutputPath, nextStep);\n }\n};\n\n// This check ensures the script only runs when executed directly\nif (import.meta.url.endsWith(process.argv[1])) {\n // The cause is already on stderr with the exit code set; this keeps Node\n // from printing the same stack again as an unhandled rejection.\n main().catch(() => {\n process.exitCode = 1;\n });\n}\n"],"mappings":";;;;;;;;;;;;AAaA,IAAM,gBAAgB,OAAO,qBAA8B,YAAqB,WAAW,SAAS;CAChG,IAAI,CAAC,qBACD,MAAM,IAAI,MAAM,oCAAoC;CAExD,IAAI;EACA,MAAM,mBAAmB;GAAE,iBAAiB,KAAK,QAAQ,mBAAmB;GAAG;GAAY;EAAS,CAAC;CACzG,SAAS,OAAO;EAIZ,QAAQ,WAAW;EACnB,MAAM;CACV;AACJ;AAEA,IAAa,OAAO,YAAY;CAC5B,MAAM,yBAAyB,QAAQ,KAAK,MAAK,QAAO,IAAI,WAAW,gBAAgB,CAAC;CACxF,MAAM,sBAAsB,yBAAyB,uBAAuB,MAAM,GAAG,CAAC,CAAC,KAAK,QAAQ,KAAK;CAEzG,MAAM,gBAAgB,QAAQ,KAAK,MAAK,QAAO,IAAI,WAAW,WAAW,CAAC;CAC1E,MAAM,aAAa,gBAAgB,cAAc,MAAM,GAAG,CAAC,CAAC,KAAK,KAAA;CAEjE,MAAM,QAAQ,QAAQ,KAAK,SAAS,SAAS;CAE7C,MAAM,WAAW,CAAC,QAAQ,KAAK,SAAS,gBAAgB;CAExD,IAAI,CAAC,qBAAqB;EACtB,IAAI,iHAAiH;EACrH;CACJ;CAEA,MAAM,eAAe,KAAK,QAAQ,QAAQ,IAAI,GAAG,mBAAmB;CACpE,MAAM,qBAAqB,aAAa,KAAK,QAAQ,QAAQ,IAAI,GAAG,UAAU,IAAI,KAAA;CAElF,IAAI,OAAO;EACP,IAAI,2BAA2B,aAAa,IAAI;EAYhD,MAAM,EAAE,SAAS,aAAa,MAAM,OAAO;EAM3C,SALyB,MAAM,cAAc;GACzC,YAAY;GACZ,eAAe;EACnB,CAEA,CAAA,CAAQ,GAAG,QAAQ,OAAO,aAAa;GACnC,IAAI,IAAI,MAAM,IAAI,SAAS,yBAAyB;GAEpD,cAAmB,cAAc,oBAAoB,QAAQ,CAAC,CAAC,YAAY,KAAA,CAAS;EACxF,CAAC;CACL,OACI,MAAM,cAAc,cAAc,oBAAoB,QAAQ;AAEtE;AAGA,IAAI,OAAO,KAAK,IAAI,SAAS,QAAQ,KAAK,EAAE,GAGxC,KAAK,CAAC,CAAC,YAAY;CACf,QAAQ,WAAW;AACvB,CAAC"}
|
package/dist/{generate-drizzle-schema-logic-sSFDp6LR.js → generate-drizzle-schema-logic-B74WN37S.js}
RENAMED
|
@@ -1,8 +1,8 @@
|
|
|
1
1
|
import { createRequire as __createRequire } from "module";
|
|
2
2
|
__createRequire(import.meta.url);
|
|
3
3
|
import { r as __exportAll } from "./__vite-browser-external-BnuHet1e.js";
|
|
4
|
-
import {
|
|
5
|
-
import { n as planSchema } from "./plan-schema-
|
|
4
|
+
import { m as renderPredicate } from "./source-DBDbBTL4.js";
|
|
5
|
+
import { n as planSchema } from "./plan-schema-B1YF_tYq.js";
|
|
6
6
|
import { relationalCollections, sortCollectionsBySlug } from "@rebasepro/common";
|
|
7
7
|
//#region src/schema/plan/render-drizzle.ts
|
|
8
8
|
var JS_IDENTIFIER = /^[A-Za-z_$][A-Za-z0-9_$]*$/;
|
|
@@ -118,7 +118,7 @@ var renderColumn = (column, table, plan, uses) => {
|
|
|
118
118
|
if (!column.nullable && !column.primaryKey && !junctionKey) out += ".notNull()";
|
|
119
119
|
return out;
|
|
120
120
|
};
|
|
121
|
-
/** The
|
|
121
|
+
/** The table's indexes — declared, tenant and foreign-key — as Drizzle table extras. */
|
|
122
122
|
var indexExtras = (table, uses) => table.indexes.map((spec) => {
|
|
123
123
|
const builder = spec.unique ? needs(uses, "uniqueIndex") : needs(uses, "index");
|
|
124
124
|
const column = (name) => member("table", table.columns.find((c) => c.column === name)?.key ?? name);
|
|
@@ -175,7 +175,7 @@ function renderDrizzleSchema(plan, options = {}) {
|
|
|
175
175
|
const keyMember = (name) => member("table", table.columns.find((c) => c.column === name && !c.columnOwnedByProperty)?.key ?? name);
|
|
176
176
|
extras.push(` ${needs(uses, "primaryKey")}({ columns: [${table.primaryKey.map(keyMember).join(", ")}] }),`);
|
|
177
177
|
}
|
|
178
|
-
|
|
178
|
+
extras.push(...indexExtras(table, uses));
|
|
179
179
|
if (withPolicies) extras.push(...table.policies.map((policy) => policyExtra(policy, uses)));
|
|
180
180
|
if (extras.length > 0) {
|
|
181
181
|
body += "}, (table) => ([\n";
|
|
@@ -245,4 +245,4 @@ var generateSchema = (allCollections, options = {}) => {
|
|
|
245
245
|
//#endregion
|
|
246
246
|
export { generate_drizzle_schema_logic_exports as n, generateSchema as t };
|
|
247
247
|
|
|
248
|
-
//# sourceMappingURL=generate-drizzle-schema-logic-
|
|
248
|
+
//# sourceMappingURL=generate-drizzle-schema-logic-B74WN37S.js.map
|
|
@@ -0,0 +1 @@
|
|
|
1
|
+
{"version":3,"file":"generate-drizzle-schema-logic-B74WN37S.js","names":[],"sources":["../src/schema/plan/render-drizzle.ts","../src/schema/generate-drizzle-schema-logic.ts"],"sourcesContent":["/**\n * `schema.generated.ts`, rendered from a {@link SchemaPlan}.\n *\n * This file is what a developer running drizzle-kit themselves diffs against,\n * and what the runtime's query builder is typed by. It has to *compile*, which\n * nothing in this repo checked until `generated-schema-compiles.test.ts`: six\n * ordinary inputs used to produce a file TypeScript rejects, and the class the\n * bigint incident already taught is that a file which will not build gets\n * hand-patched and then sits stale until a security fix misses production.\n *\n * Two structural rules keep that from coming back:\n *\n * - **The import list is derived from what was emitted.** Every builder is\n * recorded as it is used ({@link BuilderUses}), and the header is written\n * last, from that set. It used to be a fixed roster plus three\n * property-scanning heuristics, so `columnType: \"uuid\"` on a plain string\n * emitted `uuid(…)` and imported nothing.\n * - **Nothing here reads a `Property`.** Types, defaults, keys and constraints\n * all arrive decided. The Drizzle generator disagreeing with the DDL one is\n * what left `geopoint` with a database column and no Drizzle key.\n */\nimport type { ColumnPlan, ForeignKeyPlan, OneRelationPlan, PgType, PolicyPlan, RelationPlan, SchemaPlan, TablePlan } from \"./types\";\nimport { renderPredicate } from \"../collection-index\";\n\n/** What the generated file may leave out. */\nexport interface DrizzleRenderOptions {\n /**\n * `false` strips the `pgPolicy(...)` calls. RLS is still enabled on every\n * table regardless — a bare table must default-deny, not fail open.\n */\n policies?: boolean;\n}\n\nconst JS_IDENTIFIER = /^[A-Za-z_$][A-Za-z0-9_$]*$/;\n\n/**\n * A string literal for the generated file.\n *\n * Column names, table names and enum values are all written in as literals and\n * none of them is constrained to be quote-free: a Postgres identifier only has\n * to be quoted, and `O'Brien` is an ordinary enum value. Interpolating them raw\n * ended the literal early.\n */\nconst quote = (value: string): string => JSON.stringify(value);\n\n/** An object key: verbatim when it is an identifier, quoted otherwise. */\nconst propKey = (name: string): string => (JS_IDENTIFIER.test(name) ? name : quote(name));\n\n/**\n * A property access on a generated table variable.\n *\n * `users.full name` is not an expression; `users[\"full name\"]` is, and Drizzle\n * treats the two identically.\n */\nconst member = (object: string, key: string): string =>\n (JS_IDENTIFIER.test(key) ? `${object}.${key}` : `${object}[${quote(key)}]`);\n\n/**\n * The drizzle-orm builders this file emitted, collected as they are written.\n * The only way to use a builder is to record it here.\n */\ntype BuilderUses = Set<string>;\n\n/** Record a builder and hand back its name, so a call site cannot use one silently. */\nconst needs = (uses: BuilderUses, builder: string): string => {\n uses.add(builder);\n return builder;\n};\n\n/**\n * Wraps a compiled SQL clause in a Drizzle `sql\\`...\\`` template literal.\n *\n * The clause is SQL being written into a TypeScript file, so it has to survive\n * being read back as a template literal. Three characters do not:\n *\n * - `` ` `` closes the template early, and the rest of the clause becomes code.\n * - `${` opens an interpolation — the file stops compiling, or worse, compiles\n * against whatever identifier happens to be in scope.\n * - `\\` is an escape, and Drizzle's `sql` tag reads the *cooked* strings, not\n * `.raw`. So a policy written as `email ~ '^admin\\.user@corp\\.com$'` reached\n * the database as `^admin.user@corp.com$`, where every `\\.` now matches any\n * character. A `USING` clause is a security boundary and that one silently\n * widened it.\n */\nconst wrapSql = (clause: string): string =>\n `sql\\`${clause.replace(/\\\\/g, \"\\\\\\\\\").replace(/`/g, \"\\\\`\").replace(/\\$\\{/g, \"\\\\${\")}\\``;\n\n/** The pg-core builder call for a tagged type, minus the column name. */\nconst builderFor = (type: PgType, column: string, uses: BuilderUses): string => {\n const name = quote(column);\n switch (type.kind) {\n case \"text\": return `${needs(uses, \"text\")}(${name})`;\n case \"varchar\": return `${needs(uses, \"varchar\")}(${name}, { length: ${type.length} })`;\n case \"char\": return `${needs(uses, \"char\")}(${name}, { length: ${type.length} })`;\n case \"uuid\": return `${needs(uses, \"uuid\")}(${name})`;\n // The enum variable is declared at the top of the same file; it is not\n // a pg-core import.\n case \"enum\": return `${type.varName}(${name})`;\n case \"smallint\": return `${needs(uses, \"smallint\")}(${name})`;\n case \"integer\": return `${needs(uses, \"integer\")}(${name})`;\n // `bigint` and `bigserial` are the only pg-core builders that *require*\n // a config argument: without `mode`, drizzle cannot know whether to hand\n // back a `number` or a `bigint`, and the emitted call does not\n // typecheck. This is why `schema.generated.ts` drifted — regenerating it\n // produced a file that would not compile, so the bigint lines were\n // hand-patched, and every regeneration after that looked like a large,\n // alarming diff nobody wanted to ship.\n //\n // `number` rather than `bigint`: these are counters and byte totals that\n // every caller already treats as numbers.\n case \"bigint\": return `${needs(uses, \"bigint\")}(${name}, { mode: \"number\" })`;\n case \"bigserial\": return `${needs(uses, \"bigserial\")}(${name}, { mode: \"number\" })`;\n case \"smallserial\": return `${needs(uses, \"smallserial\")}(${name})`;\n case \"serial\": return `${needs(uses, \"serial\")}(${name})`;\n case \"real\": return `${needs(uses, \"real\")}(${name})`;\n case \"doublePrecision\": return `${needs(uses, \"doublePrecision\")}(${name})`;\n case \"numeric\":\n if (type.precision === undefined) return `${needs(uses, \"numeric\")}(${name})`;\n return type.scale === undefined\n ? `${needs(uses, \"numeric\")}(${name}, { precision: ${type.precision} })`\n : `${needs(uses, \"numeric\")}(${name}, { precision: ${type.precision}, scale: ${type.scale} })`;\n case \"boolean\": return `${needs(uses, \"boolean\")}(${name})`;\n case \"timestamptz\": return `${needs(uses, \"timestamp\")}(${name}, { withTimezone: true, mode: 'string' })`;\n case \"date\": return `${needs(uses, \"date\")}(${name}, { mode: 'string' })`;\n case \"time\": return `${needs(uses, \"time\")}(${name})`;\n case \"json\": return `${needs(uses, \"json\")}(${name})`;\n case \"jsonb\": return `${needs(uses, \"jsonb\")}(${name})`;\n case \"vector\": return `${needs(uses, \"vector\")}(${name}, { dimensions: ${type.dimensions} })`;\n case \"bytea\": return `${needs(uses, \"customType\")}({ dataType() { return 'bytea'; } })(${name})`;\n case \"tsvector\": return `${needs(uses, \"customType\")}({ dataType() { return 'tsvector'; } })(${name})`;\n // pg-core has no array *type*; `.array()` is a modifier on the element.\n case \"array\": return `${builderFor(type.of, column, uses)}.array()`;\n }\n};\n\n/**\n * The `.references(...)` clause a foreign key column carries.\n *\n * The `(): AnyPgColumn =>` annotation is not optional decoration and it is\n * emitted on every reference rather than only the self-referential ones. A\n * `belongsTo` pointing at its own table — a comment thread, a category tree —\n * produces `.references(() => posts.id)` inside the initializer of `posts`, and\n * TypeScript cannot infer a type for a `const` that appears in its own\n * initializer (TS7022). Drizzle documents the annotation as the fix; applying\n * it everywhere means the generated line does not depend on whether the author\n * happened to point the relation at another table.\n */\nconst referencesClause = (\n fk: ForeignKeyPlan,\n targetVar: string,\n targetKey: string,\n uses: BuilderUses\n): string => {\n needs(uses, \"type AnyPgColumn\");\n const parts = [\n fk.onUpdate ? `onUpdate: \"${fk.onUpdate.toLowerCase()}\"` : \"\",\n `onDelete: \"${fk.onDelete.toLowerCase()}\"`\n ].filter(Boolean);\n return `.references((): AnyPgColumn => ${member(targetVar, targetKey)}, { ${parts.join(\", \")} })`;\n};\n\n/** The declaration a column compiles to, without its object key. */\nconst renderColumn = (column: ColumnPlan, table: TablePlan, plan: SchemaPlan, uses: BuilderUses): string => {\n let out = builderFor(column.type, column.column, uses);\n\n if (column.generated) {\n return `${out}.generatedAlwaysAs(${wrapSql(column.generated.expression)})`;\n }\n\n // A junction endpoint spells `.notNull()` before its reference; every other\n // column spells it last. Both are the same constraint — this keeps the\n // generated file byte-stable across the move to one planner.\n const junctionKey = column.source.kind === \"junction-key\";\n if (junctionKey) out += \".notNull()\";\n\n if (column.default?.kind === \"identity\") out += \".generatedByDefaultAsIdentity()\";\n if (column.primaryKey) out += \".primaryKey()\";\n if (column.default && column.default.kind !== \"identity\") {\n const expression = column.default.kind === \"sql\" ? column.default.expression : column.default.sql;\n // `.defaultRandom()` is drizzle's own spelling of `gen_random_uuid()`,\n // and it exists **only on the uuid builder** — a `text` id whose\n // strategy happens to be spelled ``isId: \"sql`gen_random_uuid()`\"`` has\n // to take the generic form, which is what the file has always emitted\n // for it.\n out += column.type.kind === \"uuid\" && expression === \"gen_random_uuid()\"\n ? \".defaultRandom()\"\n : `.default(${wrapSql(expression)})`;\n }\n // A plain property that carries a relation's foreign key column gets the\n // constraint the relation would have emitted. `postId: { columnName:\n // \"post_id\" }` beside a `belongsTo` on `post_id` used to emit an integer\n // with no `.references()` at all, so that project got no foreign key on the\n // Drizzle side while `db push` created one.\n const foreignKey = column.foreignKey\n ?? table.columns.find(c => c.columnOwnedByProperty && c.column === column.column)?.foreignKey;\n if (foreignKey) {\n const target = plan.tables.find(t =>\n t.schema === foreignKey.targetSchema && t.table === foreignKey.targetTable);\n if (target) {\n const targetColumn = target.columns.find(c => c.column === foreignKey.targetColumn);\n out += referencesClause(foreignKey, target.varName, targetColumn?.key ?? foreignKey.targetColumn, uses);\n }\n }\n if (column.unique) out += \".unique()\";\n if (!column.nullable && !column.primaryKey && !junctionKey) out += \".notNull()\";\n return out;\n};\n\n/** The table's indexes — declared, tenant and foreign-key — as Drizzle table extras. */\nconst indexExtras = (table: TablePlan, uses: BuilderUses): string[] =>\n table.indexes.map(spec => {\n const builder = spec.unique ? needs(uses, \"uniqueIndex\") : needs(uses, \"index\");\n // The spec holds COLUMN names; the generated table is keyed by field\n // name (`authorId` for `author_id`).\n const column = (name: string): string =>\n member(\"table\", table.columns.find(c => c.column === name)?.key ?? name);\n const keys = spec.keys.map(key => {\n if (spec.method !== \"btree\") return column(key.column);\n const direction = key.direction === \"desc\" ? \".desc()\" : \".asc()\";\n const nulls = key.nulls === \"first\" ? \".nullsFirst()\" : \".nullsLast()\";\n return `${column(key.column)}${direction}${nulls}`;\n });\n const on = spec.method === \"btree\"\n ? `.on(${keys.join(\", \")})`\n : `.using(${quote(spec.method)}, ${keys.join(\", \")})`;\n const where = spec.predicate ? `.where(sql\\`${renderPredicate(spec.predicate)}\\`)` : \"\";\n // drizzle-orm has no INCLUDE in its index builder (0.45), so a covering\n // index is emitted here as its key columns alone. The database still\n // gets the INCLUDE — `schema.sql` and boot-ensure both emit it.\n const covering = spec.include.length > 0\n ? ` // INCLUDE (${spec.include.join(\", \")}) — drizzle cannot express it; schema.sql does`\n : \"\";\n return ` ${builder}(${quote(spec.indexName)})${on}${where},${covering}`;\n });\n\nconst policyExtra = (policy: PolicyPlan, uses: BuilderUses): string => {\n needs(uses, \"pgPolicy\");\n const parts = [\n `as: \"${policy.mode}\"`,\n `for: \"${policy.operation}\"`,\n `to: [${policy.roles.map(r => `\"${r}\"`).join(\", \")}]`\n ];\n if (policy.using) parts.push(`using: ${wrapSql(policy.using)}`);\n if (policy.withCheck) parts.push(`withCheck: ${wrapSql(policy.withCheck)}`);\n return ` pgPolicy(${quote(policy.name)}, { ${parts.join(\", \")} }),`;\n};\n\nexport function renderDrizzleSchema(plan: SchemaPlan, options: DrizzleRenderOptions = {}): string {\n const withPolicies = options.policies !== false;\n const uses: BuilderUses = new Set();\n // The body is assembled first and the header composed around it, because\n // the header cannot be written until the body has said what it needs.\n let body = \"\";\n\n let schemaDeclarations = \"\";\n if (plan.declaredSchemas.length > 0) {\n needs(uses, \"pgSchema\");\n plan.declaredSchemas.forEach(schema => {\n schemaDeclarations += `export const ${schema}Schema = pgSchema(\"${schema}\");\\n`;\n });\n schemaDeclarations += \"\\n\";\n }\n\n const enumVars: string[] = [];\n for (const enumPlan of plan.enums) {\n // A collection with `schema: \"app\"` gets `CREATE TYPE \"app\".\"…\"` from\n // the DDL renderer, so the type has to be declared in the same schema\n // here — `pgEnum` is `public` and nothing else. Unqualified, the runtime\n // and drizzle-kit looked for a type that does not exist in `public`.\n const labels = enumPlan.labels.map(quote).join(\", \");\n const declaration = enumPlan.declaredSchema\n ? `${enumPlan.declaredSchema}Schema.enum(${quote(enumPlan.name)}, [${labels}])`\n : `${needs(uses, \"pgEnum\")}(${quote(enumPlan.name)}, [${labels}])`;\n body += `export const ${enumPlan.varName} = ${declaration};\\n`;\n if (!enumVars.includes(enumPlan.varName)) enumVars.push(enumPlan.varName);\n }\n body += \"\\n\";\n\n const tableVars: string[] = [];\n for (const table of plan.tables) {\n const creator = table.declaredSchema\n ? `${table.declaredSchema}Schema.table`\n : needs(uses, \"pgTable\");\n body += `export const ${table.varName} = ${creator}(\"${table.table}\", {\\n`;\n\n // The implicit `id` goes last here and first in the SQL file: the DDL\n // generator unshifts it so the key leads the `CREATE TABLE`, and this\n // one appends it after the generated search columns. Both describe the\n // same table; only the reading order differs.\n const ordered = [\n ...table.columns.filter(c => c.source.kind !== \"implicit-id\"),\n ...table.columns.filter(c => c.source.kind === \"implicit-id\")\n ].filter(c =>\n !c.columnOwnedByProperty\n // The auth columns a users collection does not itself declare —\n // `is_anonymous`, `tokens_valid_after`. `db push` emits them because\n // Atlas diffs the database against exactly what it is shown and\n // would otherwise plan a DROP; drizzle-kit creates no auth table, so\n // this file describes the collection as written and leaves auth's\n // own contract to the SQL side that owns it.\n && c.source.kind !== \"auth\");\n\n const lines = ordered.map(column => ` ${propKey(column.key)}: ${renderColumn(column, table, plan, uses)}`);\n // A junction's column list carries a trailing comma; a collection's does\n // not. Kept as the two files have always spelled them.\n body += table.kind === \"junction\"\n ? `${lines.join(\",\\n\")},\\n`\n : `${lines.join(\",\\n\")}\\n`;\n\n const extras: string[] = [];\n // A key over several columns is one `primaryKey({ columns })` extra —\n // `.primaryKey()` on each column would declare several primary keys.\n // The plan lists COLUMN names and the table is keyed by field name\n // (`authorId` for `author_id`), so each is looked up.\n if (table.primaryKey.length > 1) {\n const keyMember = (name: string): string =>\n member(\"table\", table.columns.find(c => c.column === name && !c.columnOwnedByProperty)?.key ?? name);\n extras.push(` ${needs(uses, \"primaryKey\")}({ columns: [${table.primaryKey.map(keyMember).join(\", \")}] }),`);\n }\n // A junction has indexes too since the plan derives one for its\n // target column, and the file drizzle-kit plans from has to carry it\n // or a `drizzle-kit push` would drop what `db push` created.\n extras.push(...indexExtras(table, uses));\n if (withPolicies) {\n extras.push(...table.policies.map(policy => policyExtra(policy, uses)));\n }\n\n if (extras.length > 0) {\n body += \"}, (table) => ([\\n\";\n body += `${extras.join(\"\\n\")}\\n`;\n body += \"])).enableRLS();\\n\\n\";\n } else {\n // No explicit policies and no declared indexes — RLS enabled with a\n // deny-all default (Postgres denies everything when RLS is on and no\n // permissive policies exist).\n body += \"}).enableRLS();\\n\\n\";\n }\n if (!tableVars.includes(table.varName)) tableVars.push(table.varName);\n }\n\n const relationVars: string[] = [];\n for (const table of plan.tables) {\n const entries = plan.relations.filter(r => r.tableVar === table.varName);\n if (entries.length === 0) continue;\n const rendered = entries.map(relation => renderRelation(table, relation, uses));\n const varName = `${table.varName}Relations`;\n body += `export const ${varName} = drizzleRelations(${table.varName}, ({ one, many }) => ({\\n${rendered.join(\",\\n\")}\\n}));\\n\\n`;\n if (!relationVars.includes(varName)) relationVars.push(varName);\n }\n\n // ── The header, written last because the body decides what is in it ──────\n const imports = Array.from(uses).sort((a, b) =>\n a.replace(/^type /, \"\").localeCompare(b.replace(/^type /, \"\")));\n\n let out = \"// This file is auto-generated by the Rebase Drizzle generator. Do not edit manually.\\n\\n\";\n out += `import { ${imports.join(\", \")} } from 'drizzle-orm/pg-core';\\n`;\n out += \"import { relations as drizzleRelations, sql } from 'drizzle-orm';\\n\\n\";\n out += schemaDeclarations;\n out += body;\n out += `export const tables = { ${tableVars.join(\", \")} };\\n`;\n out += `export const enums = { ${enumVars.join(\", \")} };\\n`;\n out += `export const relations = { ${relationVars.join(\", \")} };\\n\\n`;\n return out;\n}\n\nconst renderRelation = (table: TablePlan, relation: RelationPlan, uses: BuilderUses): string => {\n if (relation.kind === \"many\") {\n return ` ${quote(relation.key)}: many(${relation.targetVar}, { relationName: ${quote(relation.relationName)} })`;\n }\n return ` ${quote(relation.key)}: one${oneTypeArguments(table, relation, uses)}(${relation.targetVar}, {\\n` +\n ` fields: [${relation.fields.map(f => member(table.varName, f)).join(\", \")}],\\n` +\n ` references: [${relation.references.map(r => member(relation.targetVar, r)).join(\", \")}],\\n` +\n ` relationName: ${quote(relation.relationName)}\\n` +\n \" })\";\n};\n\n/**\n * Explicit type arguments for a `one()` whose target row may not exist.\n *\n * Drizzle types a `one()` as never null when every column in `fields` is NOT\n * NULL — right for a foreign key, wrong for a `hasOne`, whose `fields` are this\n * table's own key. Giving `TColumns` as a column of this table with no declared\n * nullability makes the related row `T | null` in a relational query's result,\n * which is what the database returns. Empty for every other `one()`, whose\n * inferred type is already right.\n */\nconst oneTypeArguments = (table: TablePlan, relation: OneRelationPlan, uses: BuilderUses): string => {\n if (!relation.nullable) return \"\";\n needs(uses, \"type AnyPgColumn\");\n const column = `AnyPgColumn<{ tableName: ${quote(table.table)} }>`;\n return `<typeof ${relation.targetVar}, [${relation.fields.map(() => column).join(\", \")}]>`;\n};\n","/**\n * `schema.generated.ts`, the Drizzle half of `rebase db generate`.\n *\n * There is no interpretation left in this file. `schema/plan/plan-schema.ts`\n * reads the collections into a {@link SchemaPlan} and\n * `schema/plan/render-drizzle.ts` writes the TypeScript; this is the entry\n * point they are reached through.\n *\n * It used to hold `getDrizzleColumn` — one of three `switch (prop.type)`\n * statements, each with its own reading of relations, enums, search and\n * `validation.unique`, and each disagreeing with the other two about something.\n * See `schema/plan/types.ts`.\n */\nimport type { CollectionConfig } from \"@rebasepro/types\";\nimport { relationalCollections, sortCollectionsBySlug } from \"@rebasepro/common\";\nimport { planSchema } from \"./plan/plan-schema\";\nimport { renderDrizzleSchema, type DrizzleRenderOptions } from \"./plan/render-drizzle\";\n\n/**\n * The generated Drizzle schema for a set of collections.\n *\n * Synchronous, and takes an options object. It was `async` with nothing to\n * await and a positional `stripPolicies` boolean — `generateSchema(c, true)`,\n * which reads as neither \"with policies\" nor \"without\".\n *\n * Sorted by slug here rather than in the writer script: the output is\n * order-dependent and `rebase doctor` regenerates it in memory to compare\n * against the file on disk, so with the sort in the writer only, a project\n * whose file order differed from its slug order was reported stale forever.\n */\nexport const generateSchema = (\n allCollections: CollectionConfig[],\n options: DrizzleRenderOptions = {}\n): string => {\n // A Firestore or MongoDB collection has no table to generate, and\n // generating one for it is not merely wasted output: `db push` would create\n // it, and `rebase doctor` would then report the store the collection\n // actually reads from as drift.\n const collections = sortCollectionsBySlug(relationalCollections(allCollections));\n return renderDrizzleSchema(planSchema(collections), options);\n};\n"],"mappings":";;;;;;;AAiCA,IAAM,gBAAgB;;;;;;;;;AAUtB,IAAM,SAAS,UAA0B,KAAK,UAAU,KAAK;;AAG7D,IAAM,WAAW,SAA0B,cAAc,KAAK,IAAI,IAAI,OAAO,MAAM,IAAI;;;;;;;AAQvF,IAAM,UAAU,QAAgB,QAC3B,cAAc,KAAK,GAAG,IAAI,GAAG,OAAO,GAAG,QAAQ,GAAG,OAAO,GAAG,MAAM,GAAG,EAAE;;AAS5E,IAAM,SAAS,MAAmB,YAA4B;CAC1D,KAAK,IAAI,OAAO;CAChB,OAAO;AACX;;;;;;;;;;;;;;;;AAiBA,IAAM,WAAW,WACb,QAAQ,OAAO,QAAQ,OAAO,MAAM,CAAC,CAAC,QAAQ,MAAM,KAAK,CAAC,CAAC,QAAQ,SAAS,MAAM,EAAE;;AAGxF,IAAM,cAAc,MAAc,QAAgB,SAA8B;CAC5E,MAAM,OAAO,MAAM,MAAM;CACzB,QAAQ,KAAK,MAAb;EACI,KAAK,QAAQ,OAAO,GAAG,MAAM,MAAM,MAAM,EAAE,GAAG,KAAK;EACnD,KAAK,WAAW,OAAO,GAAG,MAAM,MAAM,SAAS,EAAE,GAAG,KAAK,cAAc,KAAK,OAAO;EACnF,KAAK,QAAQ,OAAO,GAAG,MAAM,MAAM,MAAM,EAAE,GAAG,KAAK,cAAc,KAAK,OAAO;EAC7E,KAAK,QAAQ,OAAO,GAAG,MAAM,MAAM,MAAM,EAAE,GAAG,KAAK;EAGnD,KAAK,QAAQ,OAAO,GAAG,KAAK,QAAQ,GAAG,KAAK;EAC5C,KAAK,YAAY,OAAO,GAAG,MAAM,MAAM,UAAU,EAAE,GAAG,KAAK;EAC3D,KAAK,WAAW,OAAO,GAAG,MAAM,MAAM,SAAS,EAAE,GAAG,KAAK;EAWzD,KAAK,UAAU,OAAO,GAAG,MAAM,MAAM,QAAQ,EAAE,GAAG,KAAK;EACvD,KAAK,aAAa,OAAO,GAAG,MAAM,MAAM,WAAW,EAAE,GAAG,KAAK;EAC7D,KAAK,eAAe,OAAO,GAAG,MAAM,MAAM,aAAa,EAAE,GAAG,KAAK;EACjE,KAAK,UAAU,OAAO,GAAG,MAAM,MAAM,QAAQ,EAAE,GAAG,KAAK;EACvD,KAAK,QAAQ,OAAO,GAAG,MAAM,MAAM,MAAM,EAAE,GAAG,KAAK;EACnD,KAAK,mBAAmB,OAAO,GAAG,MAAM,MAAM,iBAAiB,EAAE,GAAG,KAAK;EACzE,KAAK;GACD,IAAI,KAAK,cAAc,KAAA,GAAW,OAAO,GAAG,MAAM,MAAM,SAAS,EAAE,GAAG,KAAK;GAC3E,OAAO,KAAK,UAAU,KAAA,IAChB,GAAG,MAAM,MAAM,SAAS,EAAE,GAAG,KAAK,iBAAiB,KAAK,UAAU,OAClE,GAAG,MAAM,MAAM,SAAS,EAAE,GAAG,KAAK,iBAAiB,KAAK,UAAU,WAAW,KAAK,MAAM;EAClG,KAAK,WAAW,OAAO,GAAG,MAAM,MAAM,SAAS,EAAE,GAAG,KAAK;EACzD,KAAK,eAAe,OAAO,GAAG,MAAM,MAAM,WAAW,EAAE,GAAG,KAAK;EAC/D,KAAK,QAAQ,OAAO,GAAG,MAAM,MAAM,MAAM,EAAE,GAAG,KAAK;EACnD,KAAK,QAAQ,OAAO,GAAG,MAAM,MAAM,MAAM,EAAE,GAAG,KAAK;EACnD,KAAK,QAAQ,OAAO,GAAG,MAAM,MAAM,MAAM,EAAE,GAAG,KAAK;EACnD,KAAK,SAAS,OAAO,GAAG,MAAM,MAAM,OAAO,EAAE,GAAG,KAAK;EACrD,KAAK,UAAU,OAAO,GAAG,MAAM,MAAM,QAAQ,EAAE,GAAG,KAAK,kBAAkB,KAAK,WAAW;EACzF,KAAK,SAAS,OAAO,GAAG,MAAM,MAAM,YAAY,EAAE,uCAAuC,KAAK;EAC9F,KAAK,YAAY,OAAO,GAAG,MAAM,MAAM,YAAY,EAAE,0CAA0C,KAAK;EAEpG,KAAK,SAAS,OAAO,GAAG,WAAW,KAAK,IAAI,QAAQ,IAAI,EAAE;CAC9D;AACJ;;;;;;;;;;;;;AAcA,IAAM,oBACF,IACA,WACA,WACA,SACS;CACT,MAAM,MAAM,kBAAkB;CAC9B,MAAM,QAAQ,CACV,GAAG,WAAW,cAAc,GAAG,SAAS,YAAY,EAAE,KAAK,IAC3D,cAAc,GAAG,SAAS,YAAY,EAAE,EAC5C,CAAC,CAAC,OAAO,OAAO;CAChB,OAAO,kCAAkC,OAAO,WAAW,SAAS,EAAE,MAAM,MAAM,KAAK,IAAI,EAAE;AACjG;;AAGA,IAAM,gBAAgB,QAAoB,OAAkB,MAAkB,SAA8B;CACxG,IAAI,MAAM,WAAW,OAAO,MAAM,OAAO,QAAQ,IAAI;CAErD,IAAI,OAAO,WACP,OAAO,GAAG,IAAI,qBAAqB,QAAQ,OAAO,UAAU,UAAU,EAAE;CAM5E,MAAM,cAAc,OAAO,OAAO,SAAS;CAC3C,IAAI,aAAa,OAAO;CAExB,IAAI,OAAO,SAAS,SAAS,YAAY,OAAO;CAChD,IAAI,OAAO,YAAY,OAAO;CAC9B,IAAI,OAAO,WAAW,OAAO,QAAQ,SAAS,YAAY;EACtD,MAAM,aAAa,OAAO,QAAQ,SAAS,QAAQ,OAAO,QAAQ,aAAa,OAAO,QAAQ;EAM9F,OAAO,OAAO,KAAK,SAAS,UAAU,eAAe,sBAC/C,qBACA,YAAY,QAAQ,UAAU,EAAE;CAC1C;CAMA,MAAM,aAAa,OAAO,cACnB,MAAM,QAAQ,MAAK,MAAK,EAAE,yBAAyB,EAAE,WAAW,OAAO,MAAM,CAAC,EAAE;CACvF,IAAI,YAAY;EACZ,MAAM,SAAS,KAAK,OAAO,MAAK,MAC5B,EAAE,WAAW,WAAW,gBAAgB,EAAE,UAAU,WAAW,WAAW;EAC9E,IAAI,QAAQ;GACR,MAAM,eAAe,OAAO,QAAQ,MAAK,MAAK,EAAE,WAAW,WAAW,YAAY;GAClF,OAAO,iBAAiB,YAAY,OAAO,SAAS,cAAc,OAAO,WAAW,cAAc,IAAI;EAC1G;CACJ;CACA,IAAI,OAAO,QAAQ,OAAO;CAC1B,IAAI,CAAC,OAAO,YAAY,CAAC,OAAO,cAAc,CAAC,aAAa,OAAO;CACnE,OAAO;AACX;;AAGA,IAAM,eAAe,OAAkB,SACnC,MAAM,QAAQ,KAAI,SAAQ;CACtB,MAAM,UAAU,KAAK,SAAS,MAAM,MAAM,aAAa,IAAI,MAAM,MAAM,OAAO;CAG9E,MAAM,UAAU,SACZ,OAAO,SAAS,MAAM,QAAQ,MAAK,MAAK,EAAE,WAAW,IAAI,CAAC,EAAE,OAAO,IAAI;CAC3E,MAAM,OAAO,KAAK,KAAK,KAAI,QAAO;EAC9B,IAAI,KAAK,WAAW,SAAS,OAAO,OAAO,IAAI,MAAM;EACrD,MAAM,YAAY,IAAI,cAAc,SAAS,YAAY;EACzD,MAAM,QAAQ,IAAI,UAAU,UAAU,kBAAkB;EACxD,OAAO,GAAG,OAAO,IAAI,MAAM,IAAI,YAAY;CAC/C,CAAC;CACD,MAAM,KAAK,KAAK,WAAW,UACrB,OAAO,KAAK,KAAK,IAAI,EAAE,KACvB,UAAU,MAAM,KAAK,MAAM,EAAE,IAAI,KAAK,KAAK,IAAI,EAAE;CACvD,MAAM,QAAQ,KAAK,YAAY,eAAe,gBAAgB,KAAK,SAAS,EAAE,OAAO;CAIrF,MAAM,WAAW,KAAK,QAAQ,SAAS,IACjC,gBAAgB,KAAK,QAAQ,KAAK,IAAI,EAAE,kDACxC;CACN,OAAO,OAAO,QAAQ,GAAG,MAAM,KAAK,SAAS,EAAE,GAAG,KAAK,MAAM,GAAG;AACpE,CAAC;AAEL,IAAM,eAAe,QAAoB,SAA8B;CACnE,MAAM,MAAM,UAAU;CACtB,MAAM,QAAQ;EACV,QAAQ,OAAO,KAAK;EACpB,SAAS,OAAO,UAAU;EAC1B,QAAQ,OAAO,MAAM,KAAI,MAAK,IAAI,EAAE,EAAE,CAAC,CAAC,KAAK,IAAI,EAAE;CACvD;CACA,IAAI,OAAO,OAAO,MAAM,KAAK,UAAU,QAAQ,OAAO,KAAK,GAAG;CAC9D,IAAI,OAAO,WAAW,MAAM,KAAK,cAAc,QAAQ,OAAO,SAAS,GAAG;CAC1E,OAAO,gBAAgB,MAAM,OAAO,IAAI,EAAE,MAAM,MAAM,KAAK,IAAI,EAAE;AACrE;AAEA,SAAgB,oBAAoB,MAAkB,UAAgC,CAAC,GAAW;CAC9F,MAAM,eAAe,QAAQ,aAAa;CAC1C,MAAM,uBAAoB,IAAI,IAAI;CAGlC,IAAI,OAAO;CAEX,IAAI,qBAAqB;CACzB,IAAI,KAAK,gBAAgB,SAAS,GAAG;EACjC,MAAM,MAAM,UAAU;EACtB,KAAK,gBAAgB,SAAQ,WAAU;GACnC,sBAAsB,gBAAgB,OAAO,qBAAqB,OAAO;EAC7E,CAAC;EACD,sBAAsB;CAC1B;CAEA,MAAM,WAAqB,CAAC;CAC5B,KAAK,MAAM,YAAY,KAAK,OAAO;EAK/B,MAAM,SAAS,SAAS,OAAO,IAAI,KAAK,CAAC,CAAC,KAAK,IAAI;EACnD,MAAM,cAAc,SAAS,iBACvB,GAAG,SAAS,eAAe,cAAc,MAAM,SAAS,IAAI,EAAE,KAAK,OAAO,MAC1E,GAAG,MAAM,MAAM,QAAQ,EAAE,GAAG,MAAM,SAAS,IAAI,EAAE,KAAK,OAAO;EACnE,QAAQ,gBAAgB,SAAS,QAAQ,KAAK,YAAY;EAC1D,IAAI,CAAC,SAAS,SAAS,SAAS,OAAO,GAAG,SAAS,KAAK,SAAS,OAAO;CAC5E;CACA,QAAQ;CAER,MAAM,YAAsB,CAAC;CAC7B,KAAK,MAAM,SAAS,KAAK,QAAQ;EAC7B,MAAM,UAAU,MAAM,iBAChB,GAAG,MAAM,eAAe,gBACxB,MAAM,MAAM,SAAS;EAC3B,QAAQ,gBAAgB,MAAM,QAAQ,KAAK,QAAQ,IAAI,MAAM,MAAM;EAmBnE,MAAM,QAbU,CACZ,GAAG,MAAM,QAAQ,QAAO,MAAK,EAAE,OAAO,SAAS,aAAa,GAC5D,GAAG,MAAM,QAAQ,QAAO,MAAK,EAAE,OAAO,SAAS,aAAa,CAChE,CAAC,CAAC,QAAO,MACL,CAAC,EAAE,yBAOA,EAAE,OAAO,SAAS,MAEX,CAAA,CAAQ,KAAI,WAAU,OAAO,QAAQ,OAAO,GAAG,EAAE,IAAI,aAAa,QAAQ,OAAO,MAAM,IAAI,GAAG;EAG5G,QAAQ,MAAM,SAAS,aACjB,GAAG,MAAM,KAAK,KAAK,EAAE,OACrB,GAAG,MAAM,KAAK,KAAK,EAAE;EAE3B,MAAM,SAAmB,CAAC;EAK1B,IAAI,MAAM,WAAW,SAAS,GAAG;GAC7B,MAAM,aAAa,SACf,OAAO,SAAS,MAAM,QAAQ,MAAK,MAAK,EAAE,WAAW,QAAQ,CAAC,EAAE,qBAAqB,CAAC,EAAE,OAAO,IAAI;GACvG,OAAO,KAAK,OAAO,MAAM,MAAM,YAAY,EAAE,eAAe,MAAM,WAAW,IAAI,SAAS,CAAC,CAAC,KAAK,IAAI,EAAE,MAAM;EACjH;EAIA,OAAO,KAAK,GAAG,YAAY,OAAO,IAAI,CAAC;EACvC,IAAI,cACA,OAAO,KAAK,GAAG,MAAM,SAAS,KAAI,WAAU,YAAY,QAAQ,IAAI,CAAC,CAAC;EAG1E,IAAI,OAAO,SAAS,GAAG;GACnB,QAAQ;GACR,QAAQ,GAAG,OAAO,KAAK,IAAI,EAAE;GAC7B,QAAQ;EACZ,OAII,QAAQ;EAEZ,IAAI,CAAC,UAAU,SAAS,MAAM,OAAO,GAAG,UAAU,KAAK,MAAM,OAAO;CACxE;CAEA,MAAM,eAAyB,CAAC;CAChC,KAAK,MAAM,SAAS,KAAK,QAAQ;EAC7B,MAAM,UAAU,KAAK,UAAU,QAAO,MAAK,EAAE,aAAa,MAAM,OAAO;EACvE,IAAI,QAAQ,WAAW,GAAG;EAC1B,MAAM,WAAW,QAAQ,KAAI,aAAY,eAAe,OAAO,UAAU,IAAI,CAAC;EAC9E,MAAM,UAAU,GAAG,MAAM,QAAQ;EACjC,QAAQ,gBAAgB,QAAQ,sBAAsB,MAAM,QAAQ,2BAA2B,SAAS,KAAK,KAAK,EAAE;EACpH,IAAI,CAAC,aAAa,SAAS,OAAO,GAAG,aAAa,KAAK,OAAO;CAClE;CAGA,MAAM,UAAU,MAAM,KAAK,IAAI,CAAC,CAAC,MAAM,GAAG,MACtC,EAAE,QAAQ,UAAU,EAAE,CAAC,CAAC,cAAc,EAAE,QAAQ,UAAU,EAAE,CAAC,CAAC;CAElE,IAAI,MAAM;CACV,OAAO,YAAY,QAAQ,KAAK,IAAI,EAAE;CACtC,OAAO;CACP,OAAO;CACP,OAAO;CACP,OAAO,2BAA2B,UAAU,KAAK,IAAI,EAAE;CACvD,OAAO,0BAA0B,SAAS,KAAK,IAAI,EAAE;CACrD,OAAO,8BAA8B,aAAa,KAAK,IAAI,EAAE;CAC7D,OAAO;AACX;AAEA,IAAM,kBAAkB,OAAkB,UAAwB,SAA8B;CAC5F,IAAI,SAAS,SAAS,QAClB,OAAO,OAAO,MAAM,SAAS,GAAG,EAAE,SAAS,SAAS,UAAU,oBAAoB,MAAM,SAAS,YAAY,EAAE;CAEnH,OAAO,OAAO,MAAM,SAAS,GAAG,EAAE,OAAO,iBAAiB,OAAO,UAAU,IAAI,EAAE,GAAG,SAAS,UAAU,wBAC/E,SAAS,OAAO,KAAI,MAAK,OAAO,MAAM,SAAS,CAAC,CAAC,CAAC,CAAC,KAAK,IAAI,EAAE,2BAC1D,SAAS,WAAW,KAAI,MAAK,OAAO,SAAS,WAAW,CAAC,CAAC,CAAC,CAAC,KAAK,IAAI,EAAE,4BACtE,MAAM,SAAS,YAAY,EAAE;AAE9D;;;;;;;;;;;AAYA,IAAM,oBAAoB,OAAkB,UAA2B,SAA8B;CACjG,IAAI,CAAC,SAAS,UAAU,OAAO;CAC/B,MAAM,MAAM,kBAAkB;CAC9B,MAAM,SAAS,4BAA4B,MAAM,MAAM,KAAK,EAAE;CAC9D,OAAO,WAAW,SAAS,UAAU,KAAK,SAAS,OAAO,UAAU,MAAM,CAAC,CAAC,KAAK,IAAI,EAAE;AAC3F;;;;;;;;;;;;;;;;ACzWA,IAAa,kBACT,gBACA,UAAgC,CAAC,MACxB;CAMT,OAAO,oBAAoB,WADP,sBAAsB,sBAAsB,cAAc,CACxC,CAAW,GAAG,OAAO;AAC/D"}
|