@rebasepro/server-postgres 0.23.0 → 0.24.0
This diff represents the content of publicly available package versions that have been released to one of the supported registries. The information contained in this diff is provided for informational purposes only and reflects changes between package versions as they appear in their respective public registries.
- package/dist/{BranchService-DGPL6G_C.js → BranchService-BRt78gfa.js} +35 -18
- package/dist/BranchService-BRt78gfa.js.map +1 -0
- package/dist/PostgresBackendDriver.d.ts +84 -12
- package/dist/auth/services.d.ts +72 -14
- package/dist/{auth-users-columns-D2LBFrMH.js → auth-users-columns-C72EMoDJ.js} +17 -1
- package/dist/{auth-users-columns-D2LBFrMH.js.map → auth-users-columns-C72EMoDJ.js.map} +1 -1
- package/dist/{backup-cli-24v4OlSp.js → backup-cli-DW5p9_zv.js} +2 -2
- package/dist/{backup-cli-24v4OlSp.js.map → backup-cli-DW5p9_zv.js.map} +1 -1
- package/dist/{backup-service-HQ9GC4tN.js → backup-service-EwcDVG-8.js} +7 -9
- package/dist/{backup-service-HQ9GC4tN.js.map → backup-service-EwcDVG-8.js.map} +1 -1
- package/dist/{cli-errors-B8qHg02P.js → cli-errors-DsA-K9uP.js} +96 -1
- package/dist/{cli-errors-B8qHg02P.js.map → cli-errors-DsA-K9uP.js.map} +1 -1
- package/dist/cli-errors.d.ts +42 -0
- package/dist/cli-helpers.d.ts +12 -12
- package/dist/cli.js +91 -37
- package/dist/cli.js.map +1 -1
- package/dist/{column-plan-helpers-DF-8dTVa.js → column-plan-helpers-1-LQD0yI.js} +34 -24
- package/dist/column-plan-helpers-1-LQD0yI.js.map +1 -0
- package/dist/data-transformer.d.ts +0 -8
- package/dist/{doctor-Cb2thZ8s.js → doctor-C-sYWbmt.js} +224 -43
- package/dist/doctor-C-sYWbmt.js.map +1 -0
- package/dist/{ensure-collection-policies-RHUcEp4v.js → ensure-collection-policies-B1ureSIV.js} +73 -18
- package/dist/ensure-collection-policies-B1ureSIV.js.map +1 -0
- package/dist/{ensure-collection-tables-BEEjn5cn.js → ensure-collection-tables-kkkHk8oo.js} +76 -20
- package/dist/ensure-collection-tables-kkkHk8oo.js.map +1 -0
- package/dist/{ensure-tables-Dhn9KM3B.js → ensure-tables-BmI_tRxc.js} +10 -3
- package/dist/ensure-tables-BmI_tRxc.js.map +1 -0
- package/dist/{generate-drizzle-schema-yzY_BLhr.js → generate-drizzle-schema-93M0lUxK.js} +2 -2
- package/dist/{generate-drizzle-schema-yzY_BLhr.js.map → generate-drizzle-schema-93M0lUxK.js.map} +1 -1
- package/dist/{generate-drizzle-schema-logic-BfzK7UQd.js → generate-drizzle-schema-logic-sSFDp6LR.js} +26 -8
- package/dist/generate-drizzle-schema-logic-sSFDp6LR.js.map +1 -0
- package/dist/generate-postgres-ddl-logic-BJsLaVNX.js +152 -0
- package/dist/generate-postgres-ddl-logic-BJsLaVNX.js.map +1 -0
- package/dist/generated-sql.d.ts +28 -0
- package/dist/index.es.js +3415 -917
- package/dist/index.es.js.map +1 -1
- package/dist/{introspect-db-logic-WMuAfxvw.js → introspect-db-logic-kCETE8TY.js} +523 -349
- package/dist/introspect-db-logic-kCETE8TY.js.map +1 -0
- package/dist/introspect-db-queries-C_Q5VgQw.js +317 -0
- package/dist/introspect-db-queries-C_Q5VgQw.js.map +1 -0
- package/dist/{plan-schema-C0fxM8dY.js → plan-schema-DU9exq6C.js} +139 -504
- package/dist/plan-schema-DU9exq6C.js.map +1 -0
- package/dist/{policy-drift-DljYdrpW.js → policy-drift-B-J2hhm0.js} +3 -3
- package/dist/policy-drift-B-J2hhm0.js.map +1 -0
- package/dist/{generate-postgres-ddl-logic-Bt2d2mRH.js → render-ddl-Ds2t_d9V.js} +11 -149
- package/dist/render-ddl-Ds2t_d9V.js.map +1 -0
- package/dist/{rls-bootstrap-sql-B8EclDyM.js → rls-bootstrap-sql-_KNnjanK.js} +263 -39
- package/dist/rls-bootstrap-sql-_KNnjanK.js.map +1 -0
- package/dist/{rls-enforcement-C6Xk0lA6.js → rls-enforcement-CfXOJJaW.js} +10 -2
- package/dist/rls-enforcement-CfXOJJaW.js.map +1 -0
- package/dist/schema/auth-schema.d.ts +170 -0
- package/dist/schema/classify-change.d.ts +29 -1
- package/dist/schema/column-plan-helpers.d.ts +33 -21
- package/dist/schema/destructive-sql.d.ts +20 -1
- package/dist/schema/doctor-cli.js +4 -4
- package/dist/schema/doctor.d.ts +34 -1
- package/dist/schema/ensure-collection-policies.d.ts +22 -0
- package/dist/schema/generate-drizzle-schema.js +1 -1
- package/dist/schema/generate-postgres-ddl-logic.d.ts +5 -5
- package/dist/schema/generate-postgres-ddl.js +1 -1
- package/dist/schema/generate-schema-commit.d.ts +12 -0
- package/dist/schema/introspect-db-logic.d.ts +31 -0
- package/dist/schema/introspect-db-queries.d.ts +1 -1
- package/dist/schema/introspect-db-search.d.ts +22 -0
- package/dist/schema/introspect-db-storage.d.ts +83 -0
- package/dist/schema/introspect-db.js +15 -3
- package/dist/schema/introspect-db.js.map +1 -1
- package/dist/schema/plan/diff-plan.d.ts +1 -1
- package/dist/schema/plan/plan-schema.d.ts +16 -8
- package/dist/schema/plan/render-ddl.d.ts +6 -0
- package/dist/schema/plan/types.d.ts +37 -11
- package/dist/search-column-BM-GV6vH.js +442 -0
- package/dist/search-column-BM-GV6vH.js.map +1 -0
- package/dist/security/policy-drift.d.ts +1 -1
- package/dist/security/rls-enforcement.d.ts +7 -0
- package/dist/services/BranchService.d.ts +22 -1
- package/dist/services/FetchService.d.ts +5 -4
- package/dist/services/RelationService.d.ts +18 -0
- package/dist/services/cdc/CdcListener.d.ts +9 -4
- package/dist/services/cdc/identity-columns.d.ts +19 -0
- package/dist/services/cdc/trigger-cdc.d.ts +45 -7
- package/dist/services/channel-bus/PostgresChannelBus.d.ts +12 -7
- package/dist/services/channel-history.d.ts +17 -1
- package/dist/services/collection-helpers.d.ts +11 -1
- package/dist/services/pg-notify-listener.d.ts +88 -6
- package/dist/services/realtimeService.d.ts +159 -45
- package/dist/services/socket-liveness.d.ts +45 -0
- package/dist/services/soft-delete.d.ts +12 -0
- package/dist/services/sql-script.d.ts +71 -0
- package/dist/services/write-depth.d.ts +16 -0
- package/dist/services/write-transaction-scope.d.ts +6 -3
- package/dist/utils/drizzle-conditions.d.ts +42 -2
- package/dist/utils/sql-redaction.d.ts +21 -0
- package/dist/websocket.d.ts +50 -18
- package/package.json +7 -7
- package/dist/BranchService-DGPL6G_C.js.map +0 -1
- package/dist/column-plan-helpers-DF-8dTVa.js.map +0 -1
- package/dist/doctor-Cb2thZ8s.js.map +0 -1
- package/dist/ensure-collection-policies-RHUcEp4v.js.map +0 -1
- package/dist/ensure-collection-tables-BEEjn5cn.js.map +0 -1
- package/dist/ensure-tables-Dhn9KM3B.js.map +0 -1
- package/dist/generate-drizzle-schema-logic-BfzK7UQd.js.map +0 -1
- package/dist/generate-postgres-ddl-logic-Bt2d2mRH.js.map +0 -1
- package/dist/introspect-db-logic-WMuAfxvw.js.map +0 -1
- package/dist/plan-schema-C0fxM8dY.js.map +0 -1
- package/dist/policy-drift-DljYdrpW.js.map +0 -1
- package/dist/rls-bootstrap-sql-B8EclDyM.js.map +0 -1
- package/dist/rls-enforcement-C6Xk0lA6.js.map +0 -1
|
@@ -0,0 +1 @@
|
|
|
1
|
+
{"version":3,"file":"ensure-collection-tables-kkkHk8oo.js","names":[],"sources":["../src/schema/column-type-drift.ts","../src/schema/plan/diff-plan.ts","../src/schema/ensure-collection-tables.ts"],"sourcesContent":["/**\n * A column whose type in the database is not the type the collection declares.\n *\n * The boot ensure is additive: it creates tables, columns, enum values and\n * indexes, and it never changes an existing column's type. That is the right\n * default — an automatic `ALTER COLUMN … TYPE` against customer data, decided\n * by a process nobody is watching, is not a thing to do quietly.\n *\n * What was wrong is that it happened *silently*. A property's `columnType`\n * changed after the first deploy, the deploy reported success, the column stayed\n * as it was, and the divergence surfaced later as writes failing:\n *\n * Invalid data format in \"listing_observations\":\n * malformed array literal: \"[0,0,0.0755,…]\"\n *\n * ...after a 10,690-row import, from a deployment that had said \"success\". The\n * two ends disagreed about the schema, so no value satisfied both: the declared\n * shape was rejected by Postgres and the Postgres shape was rejected by the\n * API's own validator, first. There is no encoding out of that, only a report.\n *\n * ## Why the comparison is deliberately blunt\n *\n * A drift report that cries wolf is worse than none, because the boot that says\n * it every time is the boot nobody reads. So types are compared by *family*, and\n * a family is only split from another when the difference changes what the\n * column can hold:\n *\n * - every numeric type is one family. `int4`, `numeric` and `float8` all arrive\n * as JavaScript numbers and accept each other's values; a database adopted\n * from an existing schema legitimately has `int4` where `type: \"number\"`\n * generates `NUMERIC`, and that is not news.\n * - every character type is one family, for the same reason: `text`,\n * `varchar(255)` and `char(3)` differ in what they *reject*, not in shape.\n * - `jsonb` and an array are different families, and that is the case this\n * exists for.\n *\n * Anything this module does not recognise — an enum type, a domain, a PostGIS\n * geometry, an extension type it has never met — returns `null` and is compared\n * with nothing. Silence on an unknown type is the deliberate choice: the cost of\n * a missed report is a question somebody asks once, and the cost of a false one\n * is a warning everybody learns to skip.\n */\n\n/** A column whose live type and declared type are in different families. */\nexport interface ColumnTypeDrift {\n /** `schema.table`. */\n table: string;\n /** The column name as it exists in the database. */\n column: string;\n /** What the collection says the column should be, e.g. `JSONB`. */\n declared: string;\n /** What the database actually has, as `udt_name` reports it, e.g. `_numeric`. */\n actual: string;\n}\n\n/**\n * Types that mean the same thing to everything above the driver.\n *\n * Keyed by every spelling either side can produce: `information_schema` reports\n * `udt_name` (`int4`, `_numeric`, `bpchar`), while the generators emit SQL\n * (`INTEGER`, `NUMERIC[]`, `CHAR(3)`). Both are normalised through here.\n */\nconst FAMILIES: Record<string, string> = {\n // Numbers. One family on purpose — see the note above.\n int2: \"number\", smallint: \"number\",\n int4: \"number\", int: \"number\", integer: \"number\",\n int8: \"number\", bigint: \"number\",\n numeric: \"number\", decimal: \"number\",\n float4: \"number\", real: \"number\",\n float8: \"number\", \"double precision\": \"number\",\n money: \"number\",\n\n // Characters. Also one family: the differences are limits, not shapes.\n text: \"text\", varchar: \"text\", \"character varying\": \"text\",\n bpchar: \"text\", char: \"text\", character: \"text\", citext: \"text\",\n\n bool: \"boolean\", boolean: \"boolean\",\n\n json: \"json\",\n jsonb: \"jsonb\",\n\n uuid: \"uuid\",\n bytea: \"bytea\",\n\n date: \"date\",\n time: \"time\", \"time without time zone\": \"time\",\n timetz: \"time\", \"time with time zone\": \"time\",\n timestamp: \"timestamp\", \"timestamp without time zone\": \"timestamp\",\n timestamptz: \"timestamptz\", \"timestamp with time zone\": \"timestamptz\",\n interval: \"interval\",\n\n tsvector: \"tsvector\",\n vector: \"vector\",\n halfvec: \"halfvec\",\n sparsevec: \"sparsevec\"\n};\n\n/**\n * One SQL or `udt_name` spelling reduced to its family, or `null` when this\n * module has no opinion about it.\n *\n * `null` is not a failure. It is the answer for an enum type, a domain, a\n * PostGIS column, a quoted qualified name — everything whose identity is a\n * *name* rather than a shape, where two spellings being different says nothing\n * about whether the values are compatible.\n */\nexport function typeFamily(raw: string | undefined | null): string | null {\n if (!raw) return null;\n let type = raw.trim().toLowerCase();\n if (!type) return null;\n\n // `\"public\".\"posts_status\"` — an enum, identified by name. Not comparable.\n if (type.startsWith(\"\\\"\")) return null;\n\n // `INTEGER GENERATED BY DEFAULT AS IDENTITY` is an `int4` with a default\n // attached; the identity clause is not part of the type.\n const generated = type.indexOf(\" generated \");\n if (generated > 0) type = type.slice(0, generated).trim();\n\n // Arrays, in both spellings: `NUMERIC[]` from the generators, `_numeric`\n // from `udt_name`. Compared element-wise, so `text[]` and `numeric[]` are\n // different and `int4[]` and `numeric[]` are not.\n if (type.endsWith(\"[]\")) {\n const element = typeFamily(type.slice(0, -2));\n return element && `array:${element}`;\n }\n if (type.startsWith(\"_\")) {\n const element = typeFamily(type.slice(1));\n return element && `array:${element}`;\n }\n\n // `VARCHAR(255)`, `VECTOR(384)`, `NUMERIC(10, 2)` — the width is a limit on\n // a family, not a family. A vector whose *dimensions* changed is a separate\n // concern with its own handling in the ensure.\n const paren = type.indexOf(\"(\");\n if (paren > 0) type = type.slice(0, paren).trim();\n\n return FAMILIES[type] ?? null;\n}\n\n/**\n * Do these two spellings describe columns that hold the same kind of value?\n *\n * `true` whenever the answer is not confidently \"no\" — an unrecognised type on\n * either side agrees with everything, because a report needs evidence and an\n * unknown type is the absence of it.\n */\nexport function typesAgree(declared: string, actual: string): boolean {\n const a = typeFamily(declared);\n const b = typeFamily(actual);\n if (a === null || b === null) return true;\n if (a !== b) return false;\n // Same family, and for `numeric` that is not the end of it: the modifier is\n // what makes `NUMERIC(10, 2)` a price and a bare `NUMERIC` a number that\n // keeps whatever the caller sent. A property that declares\n // `precision`/`scale` is asking the database to round, and a column without\n // them silently does not — so the two disagree, and the drift report says\n // so. A declaration with no modifier accepts whatever is there, which is\n // what it always did.\n //\n // Tested on the spelling, not the family: the family is `number` for every\n // numeric type, and an `int4` under a `NUMERIC(10, 2)` stays the adopted\n // schema it was.\n if (!isNumeric(declared) || !isNumeric(actual)) return true;\n const declaredModifier = numericModifier(declared);\n if (declaredModifier === null) return true;\n return declaredModifier === numericModifier(actual);\n}\n\n/** `NUMERIC`, `numeric(10,2)`, `DECIMAL(10, 2)` — the arbitrary-precision type itself. */\nfunction isNumeric(raw: string): boolean {\n return /^\\s*(?:numeric|decimal)\\s*(?:\\(|$)/i.test(raw);\n}\n\n/**\n * `numeric(10,2)` / `NUMERIC(10, 2)` → `10,2`; a bare `numeric` → `null`. A\n * precision alone means a scale of 0, which is how the catalogue reports it:\n * `NUMERIC(10)` → `10,0`.\n */\nfunction numericModifier(raw: string): string | null {\n const match = raw.match(/\\(([^)]*)\\)/);\n if (!match) return null;\n const [precision, scale = \"0\"] = match[1].split(\",\").map(part => part.trim());\n return `${precision},${scale}`;\n}\n\n/**\n * The sentence a boot log says about one drifted column.\n *\n * Written to be actionable in the place it is read, which is a runtime log with\n * no way to run a migration from it: it names both types, says which surface\n * will break first, and gives the statement that resolves it.\n */\nexport function columnTypeDriftMessage(drift: ColumnTypeDrift): string {\n const [schema, table] = drift.table.split(\".\");\n return (\n `Column \"${drift.table}\".\"${drift.column}\" is ${drift.actual} in the database, but this ` +\n `collection declares ${drift.declared}. Boot does not change an existing column's type, so ` +\n \"the database keeps what it has and writes that match the declaration will be rejected by \" +\n \"Postgres. Change it deliberately, when you can watch it:\\n\" +\n ` ALTER TABLE \"${schema}\".\"${table}\" ALTER COLUMN \"${drift.column}\" ` +\n `TYPE ${drift.declared} USING \"${drift.column}\"::${drift.declared};`\n );\n}\n","/**\n * The additive statements that bring a database up to a {@link SchemaPlan}.\n *\n * ## Why additive-only, forever\n *\n * This runs unattended, at boot, against a database with customers' data in it,\n * with no human reading a diff. So it may only ever do things that cannot lose\n * data: create a missing table, add a missing column, create a missing enum\n * type, add an enum value, create an index.\n *\n * It will **never** drop a table or a column, narrow a type, or alter a\n * constraint at boot. A removed field leaves its column behind; a renamed field\n * looks like an addition and the old column stays. That is the correct trade\n * for an automated path — the alternative is an unattended process that can\n * silently destroy a column. Destructive changes stay a deliberate,\n * human-reviewed migration. Because of that, this is safe to run on every boot,\n * and re-running it is a no-op.\n *\n * ## What changed when the planner arrived\n *\n * This used to read `Property` itself — its own reading of the type, of\n * `validation`, of relations, of enums — and disagreed with `db push` about all\n * of them. The audit found a required `author_id` that was NOT NULL after a\n * push and nullable after a boot, on the one path with no developer in the\n * loop. Now it reads the same {@link ColumnPlan} the DDL renderer does, so a\n * column's type, default, uniqueness and foreign key are decided once. What is\n * left here is the half that is genuinely this path's own: *what the database\n * already has*, and which of the planned constraints are safe to apply to it.\n */\nimport type { ColumnPlan, SchemaPlan, TablePlan } from \"./types\";\nimport { renderColumnDefinition, renderEnableRls, renderPgType, renderPrimaryKeyConstraint } from \"./render-ddl\";\nimport { quoteSqlLiteral } from \"./plan-schema\";\nimport { SET_UPDATED_AT_FN, triggerStatements } from \"./updated-at-trigger\";\nimport {\n SEARCH_STAMP_PREFIX,\n SEARCH_TEXT_FN,\n SEARCH_UNACCENT_FN,\n searchColumnStamps,\n searchIndexStatements\n} from \"../search-column\";\nimport { collectionIndexStatement, sortIndexSpecs } from \"../collection-index\";\nimport { vectorIndexStatement, type SkippedVectorIndex } from \"../vector-index\";\nimport { typesAgree, type ColumnTypeDrift } from \"../column-type-drift\";\n\n/** Postgres identifiers this module is willing to interpolate. */\nconst SAFE_IDENTIFIER = /^[A-Za-z_][A-Za-z0-9_$]*$/;\n\n/**\n * Validation used to run only on the names read back OUT of the catalogue,\n * which is the direction that cannot hurt anyone: those came from Postgres. The\n * names going IN — the schema and table a collection declares — were quoted and\n * concatenated on trust, and quoting is not escaping. A table named\n * `x\" (id int); DROP TABLE users; --` closes the quote and the statement, and\n * the whole thing runs on the owner connection, which is the one connection in\n * the system that is exempt from every RLS policy.\n *\n * That is only a config file on a self-hosted project, where the person writing\n * it could run the SQL directly anyway. It is not only a config file where a\n * collection can be defined over the wire — the live-schema and source editors\n * both do that — and there the caller is an admin on the app, not an operator\n * of the database.\n */\nexport function assertSafeIdentifier(value: string, what: string): string {\n if (!SAFE_IDENTIFIER.test(value)) {\n throw new Error(`Refusing to build SQL with an unsafe ${what}: ${JSON.stringify(value)}`);\n }\n return value;\n}\n\n/** What the database currently has, as the planner needs it. */\nexport interface ExistingSchema {\n /** `schema.table` → set of column names. */\n tables: Map<string, Set<string>>;\n /** `schema.typename` of every enum type that already exists. */\n enums: Set<string>;\n /**\n * `schema.table.constraint` of every constraint that already exists.\n *\n * Optional so a caller that only cares about tables can still build one by\n * hand; absent is read as \"none known\", which at worst re-attempts a\n * constraint that then fails harmlessly as a duplicate.\n */\n constraints?: Set<string>;\n /**\n * `schema.table.column` → that column's comment, for the columns that have\n * one. This is where a generated search column's fingerprint lives.\n */\n columnComments?: Map<string, string>;\n /** `schema.typename` → its labels, in `enumsortorder`. */\n enumValues?: Map<string, string[]>;\n /** `schema.table.column` of every NOT NULL column. */\n notNullColumns?: Set<string>;\n /** `schema.table.column` of every column that has a DEFAULT. */\n columnDefaults?: Set<string>;\n /** `schema.table` of every table that holds at least one row. */\n populatedTables?: Set<string>;\n /**\n * `schema.table.column` → the type Postgres reports, as `udt_name` (plus a\n * modifier where one distinguishes two columns of the same family, e.g.\n * `numeric(10,2)`).\n */\n columnTypes?: Map<string, string>;\n /** `schema.table.trigger` of every trigger that already exists. */\n triggers?: Set<string>;\n}\n\n/**\n * How far the planner may go in making the database's constraints match the\n * configuration.\n *\n * - `additive` — the boot default. Columns, tables, indexes and enum values are\n * created; no existing column's constraints are touched. Unattended boots run\n * against customer data with nobody reading a diff, and a database adopted by\n * introspection legitimately carries NOT NULL on columns the generated\n * collection leaves optional (`introspect-db-logic` withholds `required` from\n * a column with a default or a trigger behind it). Converging there would\n * strip real constraints on first boot.\n * - `converge` — the live schema editor. Every statement is planned, shown to\n * the person making the change, and applied only once they confirm it.\n */\nexport type ConstraintPolicy = \"additive\" | \"converge\";\n\nexport interface DiffOptions {\n /** Defaults to `additive`. See {@link ConstraintPolicy}. */\n constraints?: ConstraintPolicy;\n}\n\nexport interface EnsureAction {\n kind: \"create-enum\" | \"create-table\" | \"enable-rls\" | \"add-column\" | \"add-constraint\" | \"rename-column\"\n | \"create-extension\" | \"create-function\" | \"create-index\" | \"comment-column\"\n | \"add-enum-value\" | \"set-not-null\" | \"drop-not-null\" | \"set-default\" | \"create-trigger\";\n /** Qualified target, for logging: `public.posts` or `public.posts.title`. */\n target: string;\n sql: string;\n}\n\n/** A NOT NULL column with no default that no property declares. */\nexport interface OrphanedRequiredColumn {\n /** `schema.table`. */\n table: string;\n /** The column left behind. */\n column: string;\n /** The collection that no longer declares it. */\n slug: string;\n}\n\n/** A constraint the configuration asks for that the planner is not applying. */\nexport interface WithheldConstraint {\n /** `schema.table.column`. */\n target: string;\n kind: \"not-null\" | \"unique\";\n /**\n * Why, in a sentence that names the obstacle rather than the rule. The\n * reader is looking at a column that is nullable when they asked for\n * required, or accepts duplicates when they asked for unique, and needs to\n * know what to do about it.\n */\n reason: string;\n /** What would make it applicable. */\n remedy: string;\n}\n\n/**\n * A generated search column built from a `search` block that has since changed.\n *\n * Reported instead of applied because the two ways to apply it are both worse\n * than stopping. `ALTER COLUMN … SET EXPRESSION` exists only on PG17+ and\n * rewrites the table either way; `DROP COLUMN` + `ADD COLUMN` rewrites it under\n * an ACCESS EXCLUSIVE lock and rebuilds the GIN index. This module runs\n * unattended against live customer data with nobody reading a diff, so a\n * multi-minute outage is not a decision it may take on its own.\n *\n * Not applying it silently is not an option either: a collection that added a\n * field, flipped `unaccent` or raised a weight kept indexing the *old* set\n * forever, and the only symptom was searches returning nothing for content\n * plainly in the row.\n */\nexport interface SearchColumnDrift {\n /** `schema.table`. */\n table: string;\n column: string;\n /** The fingerprint recorded on the column. */\n found: string;\n /** The fingerprint the current `search` block computes. */\n expected: string;\n /** The statements that would rebuild the column, for the operator to run. */\n rebuild: string[];\n}\n\n/** A relation column whose old and new spellings both plausibly apply. */\nexport interface LegacyForeignKey {\n /** `schema.table`. */\n table: string;\n /** The name the current rule derives, and what this plan would create. */\n expected: string;\n /** The name the old rule derived, which the table already has. */\n legacy: string;\n}\n\nexport interface EnsurePlan {\n actions: EnsureAction[];\n /** Every statement, in dependency order. Empty when the schema is current. */\n statements: string[];\n /**\n * Relation columns this plan is about to create where the table already\n * carries the same column under its pre-singularization name.\n *\n * Reported rather than silently handled: the ensure is additive, so it\n * would add `category_id` beside a populated `categorie_id` and the\n * relation would then read the new, empty one. No statement fails, no table\n * is missing, and the only symptom is relations resolving to nothing.\n */\n legacyForeignKeys: LegacyForeignKey[];\n /** @see SearchColumnDrift */\n searchDrift: SearchColumnDrift[];\n /**\n * Generated search columns that exist but carry no fingerprint — created\n * before this check existed, or by `search.sql` on an older CLI. The plan\n * stamps them so the *next* change is detectable.\n */\n searchAdopted: { table: string; column: string }[];\n /**\n * Vector columns this plan is deliberately leaving unindexed, because\n * pgvector cannot build an ANN index that wide. Reported rather than\n * thrown: the column is valid, storable and searchable.\n */\n vectorIndexSkipped: SkippedVectorIndex[];\n /**\n * Constraints the configuration asks for that this plan is not applying,\n * and why. Every one of these was previously withheld in silence.\n */\n withheldConstraints: WithheldConstraint[];\n /**\n * Columns whose type in the database is in a different family from the one\n * the collection declares. Reported and never acted on — this is the one\n * divergence that presents as a *working* deploy.\n */\n columnTypeDrift: ColumnTypeDrift[];\n /**\n * Columns that are NOT NULL, have no default, and that no property declares\n * any more — so every insert through the API is rejected for a field the\n * author cannot name, because they already deleted it. This is what a\n * **rename** looks like to an additive provisioner. Reported, never acted\n * on: dropping the column is the right fix roughly always and destructive\n * exactly once, which is not a decision to take unattended.\n */\n orphanedRequiredColumns: OrphanedRequiredColumn[];\n}\n\n/**\n * Whether a column is part of the table's primary key.\n *\n * Those are created with the table, in step 2, and never added afterwards: a\n * key column missing from a table that already exists cannot be made part of\n * its key by an additive change, so there is nothing safe to plan for one.\n *\n * A relation's entry for a column a declared property owns is not the column —\n * it carries only the constraint — so it is not a key column either, or the\n * key would be created twice.\n */\nconst isKeyColumn = (table: TablePlan, column: ColumnPlan): boolean =>\n !column.columnOwnedByProperty && table.primaryKey.includes(column.column);\n\n/** Which pass a column belongs to. The order below is the order they run in. */\nconst isScalarProperty = (table: TablePlan, column: ColumnPlan): boolean =>\n (column.source.kind === \"property\" || column.source.kind === \"implicit-id\") && !isKeyColumn(table, column);\n\n/**\n * `ENABLE ROW LEVEL SECURITY` on a table this plan has just created — the\n * statement immediately after its `CREATE TABLE`, never later.\n *\n * The user role every authenticated request runs as holds DML on a new table\n * the moment it exists: the schema's default privileges grant it, and boot's\n * `ensureAppRole` grants it on every table before the policies run. The\n * policies — which also switch RLS on — come after the driver and auth have\n * initialised, so any failure in between left a table with full DML and no row\n * filtering, served by every process in the deployment that does not\n * provision. Born locked, a table denies until its policies arrive, which is\n * the state a failure is allowed to leave behind. Idempotent, so a table a\n * concurrent boot created gets the same statement and nothing changes.\n */\nconst lockAction = (table: TablePlan): EnsureAction => ({\n kind: \"enable-rls\",\n target: table.qualified,\n sql: renderEnableRls(table)\n});\n\nexport function diffPlanAgainstCatalogue(\n plan: SchemaPlan,\n existing: ExistingSchema,\n options: DiffOptions = {}\n): EnsurePlan {\n const constraintPolicy: ConstraintPolicy = options.constraints ?? \"additive\";\n const actions: EnsureAction[] = [];\n const withheldConstraints: WithheldConstraint[] = [];\n const columnTypeDrift: ColumnTypeDrift[] = [];\n const legacyForeignKeys: LegacyForeignKey[] = [];\n const searchDrift: SearchColumnDrift[] = [];\n const searchAdopted: { table: string; column: string }[] = [];\n\n const collectionTables = plan.tables.filter(t => t.kind === \"collection\");\n const junctionTables = plan.tables.filter(t => t.kind === \"junction\");\n\n // 1. Enum types. `CREATE TYPE` has no IF NOT EXISTS, so an existing type is\n // skipped by name rather than guarded in SQL.\n for (const enumPlan of plan.enums) {\n assertSafeIdentifier(enumPlan.schema, \"schema name\");\n if (existing.enums.has(enumPlan.qualified)) {\n // The type is there, but that says nothing about its *values*.\n // Skipping the whole type by name is what made an added enum value\n // vanish: nothing was planned, the boot reported success, and the\n // first row using the value was rejected by a type that had never\n // heard of it. `ADD VALUE` is the one alteration Postgres offers\n // here, it is purely additive, and it is idempotent.\n //\n // `enumValues` absent means the caller built the schema by hand and\n // does not know the values; skip by name as before.\n const current = existing.enumValues?.get(enumPlan.qualified);\n if (!current) continue;\n for (const value of enumPlan.labels) {\n if (current.includes(value)) continue;\n actions.push({\n kind: \"add-enum-value\",\n target: `${enumPlan.qualified}.${value}`,\n // Not inside a transaction with any use of the value:\n // Postgres refuses to read a value added by the transaction\n // still adding it. The applier runs these one statement at a\n // time, which is what makes it legal.\n sql: `ALTER TYPE \"${enumPlan.schema}\".\"${enumPlan.name}\" ADD VALUE IF NOT EXISTS ${quoteSqlLiteral(value)};`\n });\n }\n continue;\n }\n actions.push({\n kind: \"create-enum\",\n target: enumPlan.qualified,\n sql: `CREATE TYPE \"${enumPlan.schema}\".\"${enumPlan.name}\" AS ENUM (${enumPlan.labels.map(quoteSqlLiteral).join(\", \")});`\n });\n }\n\n // 1b. Extensions and helper functions, before the tables and columns that\n // reference them: a generated column's expression is resolved when the\n // column is created, so a table whose search column calls\n // `rebase_search_text` cannot be added before that function exists.\n // Both forms are idempotent, so a boot against a database that already\n // has them plans nothing.\n for (const statement of plan.extensions) {\n actions.push({\n kind: \"create-extension\",\n target: statement.replace(/^CREATE EXTENSION IF NOT EXISTS |;$/g, \"\"),\n sql: statement\n });\n }\n for (const statement of plan.functions) {\n actions.push({ kind: \"create-function\", target: functionTarget(statement), sql: statement });\n }\n\n // 2. Missing tables. Only the key columns are created here; every other\n // column is added by step 3, so a new table and an existing table that\n // gained a field travel the exact same code path. One way to build a\n // column means one way for it to be wrong.\n //\n // A composite key's columns all arrive here, with the one `PRIMARY KEY`\n // constraint naming them: a primary key can only be declared when the\n // table is created, and a table created with the first key column alone\n // would never gain the rest as part of its key.\n const created = new Set<string>();\n for (const table of collectionTables) {\n if (existing.tables.has(table.qualified) || created.has(table.qualified)) continue;\n created.add(table.qualified);\n const schema = assertSafeIdentifier(table.schema, \"schema name\");\n const name = assertSafeIdentifier(table.table, \"table name\");\n const keyColumns = table.columns.filter(c => isKeyColumn(table, c));\n const definitions = keyColumns.length > 0\n ? keyColumns.map(c => `\"${assertSafeIdentifier(c.column, \"column name\")}\" ${renderColumnDefinition(c)}`)\n : ['\"id\" TEXT PRIMARY KEY'];\n if (keyColumns.length > 1) definitions.push(renderPrimaryKeyConstraint(table));\n actions.push({\n kind: \"create-table\",\n target: table.qualified,\n sql: `CREATE TABLE IF NOT EXISTS \"${schema}\".\"${name}\" (${definitions.join(\", \")});`\n });\n actions.push(lockAction(table));\n }\n\n // 2b. Junction tables behind many-to-many relations. No collection declares\n // them, so until they existed an m2m write had nowhere to land and the\n // junction's derived RLS had nothing to attach to. Both columns are\n // created here — unlike a collection table, whose CREATE declares only\n // the identity column — because there is nothing else to them.\n for (const table of junctionTables) {\n if (existing.tables.has(table.qualified) || created.has(table.qualified)) continue;\n created.add(table.qualified);\n const columns = table.columns\n .map(c => `\"${c.column}\" ${renderColumnDefinition(c)}`)\n .join(\", \");\n actions.push({\n kind: \"create-table\",\n target: table.qualified,\n sql: `CREATE TABLE IF NOT EXISTS \"${table.schema}\".\"${table.table}\" (${columns}` +\n `, ${renderPrimaryKeyConstraint(table)});`\n });\n actions.push(lockAction(table));\n }\n\n const addColumn = (table: TablePlan, column: string, definition: string): void => {\n if (existing.tables.get(table.qualified)?.has(column)) return;\n // Same reason as `assertSafeIdentifier` above: a column name reaching\n // here came from a property declaration, which on the editor paths came\n // over the wire. `definition` is built by the renderer from a closed set\n // of type mappings and is not caller text.\n assertSafeIdentifier(column, \"column name\");\n actions.push({\n kind: \"add-column\",\n target: `${table.qualified}.${column}`,\n sql: `ALTER TABLE \"${table.schema}\".\"${table.table}\" ADD COLUMN IF NOT EXISTS \"${column}\" ${definition};`\n });\n };\n\n /**\n * Move a column that is only missing because it was renamed.\n *\n * Returns true when it handled the column, so the caller skips the ordinary\n * ADD. Adding would be the wrong move and a quiet one: the data is in the\n * old column, `ADD COLUMN` creates the new one empty beside it, every\n * statement succeeds, and the relation reads the empty one. A rename is\n * metadata-only in Postgres, keeps the values, and carries the column's\n * indexes and constraints with it.\n */\n const renameLegacyColumn = (table: TablePlan, column: ColumnPlan): boolean => {\n const present = existing.tables.get(table.qualified);\n if (!column.legacyColumn || !present) return false;\n if (present.has(column.column) || !present.has(column.legacyColumn)) return false;\n legacyForeignKeys.push({ table: table.qualified, expected: column.column, legacy: column.legacyColumn });\n actions.push({\n kind: \"rename-column\",\n target: `${table.qualified}.${column.column}`,\n sql: `ALTER TABLE \"${table.schema}\".\"${table.table}\" RENAME COLUMN \"${column.legacyColumn}\" TO \"${column.column}\";`\n });\n return true;\n };\n\n /**\n * A `NOT NULL` is safe exactly when it cannot fail against rows that are\n * already there, and there are three ways to know that:\n *\n * - the table is being created by this plan (no rows yet);\n * - the table exists and is empty;\n * - the column arrives with a DEFAULT, which Postgres backfills into every\n * existing row as part of ADD COLUMN.\n *\n * `populatedTables` absent means the caller does not know, and not knowing\n * has to read as \"assume rows\" — the other direction emits a NOT NULL that\n * is checked against live data and aborts the boot. Written as an explicit\n * `!== undefined` because the optional-chain form quietly says *empty* when\n * the fact is missing, which is the wrong way to be wrong.\n */\n const tableIsEmpty = (table: TablePlan): boolean =>\n existing.populatedTables !== undefined\n && existing.tables.has(table.qualified)\n && !existing.populatedTables.has(table.qualified);\n\n /**\n * The column is there and holds a different kind of value than the\n * collection says it does. Reported, never altered: an unattended `ALTER\n * COLUMN … TYPE` over customer data is not a thing to do quietly, and the\n * *quiet* is what this fixes. A deploy that changed a `columnType` used to\n * report success while the column stayed as it was, and the divergence\n * surfaced later as every write failing. A column whose definition is owned\n * elsewhere (auth, or a generated search column) is not compared: the\n * declaration there is a rendered string rather than a type, and auth's own\n * reconcile owns it.\n */\n const reportTypeDrift = (table: TablePlan, column: ColumnPlan): void => {\n const key = `${table.qualified}.${column.column}`;\n if (existing.tables.get(table.qualified)?.has(column.column) !== true || column.sqlDefinition) return;\n const declaredType = renderPgType(column.type);\n const actualType = existing.columnTypes?.get(key);\n if (actualType && !typesAgree(declaredType, actualType)) {\n columnTypeDrift.push({ table: table.qualified, column: column.column, declared: declaredType, actual: actualType });\n }\n };\n\n /**\n * `SET DEFAULT` on a column that is already there and has none.\n *\n * A DEFAULT binds future writes only, so `SET DEFAULT` cannot fail on data\n * and cannot touch a row that already exists. That makes it the one\n * convergence this path may do unattended — and it has to, because\n * `ADD COLUMN IF NOT EXISTS` is a no-op against an existing column, so a\n * `defaultValue` added to a live collection would otherwise never reach the\n * database at all.\n */\n const planMissingDefault = (table: TablePlan, column: ColumnPlan): void => {\n if (!column.default || column.default.kind === \"identity\" || column.generated) return;\n const key = `${table.qualified}.${column.column}`;\n if (existing.columnDefaults?.has(key)) return;\n const expression = column.default.kind === \"sql\" ? column.default.expression : column.default.sql;\n actions.push({\n kind: \"set-default\",\n target: key,\n sql: `ALTER TABLE \"${table.schema}\".\"${table.table}\" ALTER COLUMN \"${column.column}\" SET DEFAULT ${expression};`\n });\n };\n\n /**\n * Plan one column, with the constraints this database can safely take.\n *\n * The definition comes from the same renderer `schema.sql` uses, minus the\n * constraints that are checked against live rows. That subtraction is the\n * whole of what this path is allowed to decide for itself.\n */\n const planColumn = (table: TablePlan, column: ColumnPlan): void => {\n const key = `${table.qualified}.${column.column}`;\n const columnExists = existing.tables.get(table.qualified)?.has(column.column) === true;\n const fresh = created.has(table.qualified);\n const empty = tableIsEmpty(table);\n const required = !column.nullable;\n const hasDefault = column.default !== undefined;\n\n reportTypeDrift(table, column);\n\n // A table this run is creating has no rows yet, so the constraints\n // `db push` writes are free to apply. On a table that already exists\n // they are checked against live rows, so each is applied only where it\n // cannot fail.\n //\n // NOT NULL needs every existing row to get a value: an empty table, or\n // a default that backfills them.\n //\n // UNIQUE needs every existing row to get a *different* value, and a\n // column being added gets either its default in every row — the same\n // value, so a second row fails — or NULL in every row, and NULLs are\n // distinct. So it holds on an empty table, and on any table when the\n // column arrives with no default. A column that is already there is\n // not touched: whether it carries a unique constraint or index under\n // some other name is not something the catalogue read here says.\n const notNullIsSafe = fresh || empty || hasDefault;\n const uniqueIsSafe = fresh || empty || !hasDefault;\n const applicable: ColumnPlan = {\n ...column,\n unique: column.unique && !columnExists && uniqueIsSafe,\n nullable: column.nullable || !(notNullIsSafe || columnExists)\n };\n if (column.unique && !columnExists && !uniqueIsSafe) {\n withheldConstraints.push({\n target: key,\n kind: \"unique\",\n reason:\n `\"${column.column}\" is unique, but \"${table.qualified}\" ` +\n (existing.populatedTables === undefined ? \"may already hold rows\" : \"already holds rows\") +\n \" and the column's default would give every one of them the same value, so UNIQUE \" +\n \"would fail as it was added.\",\n remedy:\n \"Give each existing row its own value, then add the constraint — \" +\n `\\`ALTER TABLE \"${table.schema}\".\"${table.table}\" ADD UNIQUE (\"${column.column}\")\\` — ` +\n \"or drop the property's default.\"\n });\n }\n if (required && !columnExists && !notNullIsSafe) {\n withheldConstraints.push({\n target: key,\n kind: \"not-null\",\n reason: column.source.kind === \"relation\" || column.source.kind === \"reference\"\n ? `\"${column.column}\" is a required link, but \"${table.qualified}\" already holds rows and ` +\n \"the column has no default to backfill them with, so NOT NULL would be checked \" +\n \"against data that does not have a value yet.\"\n : `\"${column.column}\" is required, but \"${table.qualified}\" already holds rows and the column ` +\n \"has no default to backfill them with, so NOT NULL would be checked \" +\n \"against data that does not have a value yet.\",\n remedy: column.source.kind === \"relation\" || column.source.kind === \"reference\"\n ? \"Backfill the column, then add the constraint — or make the relation optional.\"\n : \"Backfill the column, then add the constraint — or give the property a \" +\n \"default so every existing row gets one.\"\n });\n }\n addColumn(table, column.column, renderColumnDefinition(applicable));\n\n if (!columnExists) return;\n\n planMissingDefault(table, column);\n\n // Two directions on NOT NULL, and they are not equally safe — see\n // `ConstraintPolicy` for why neither runs at an unattended boot.\n if (constraintPolicy !== \"converge\") return;\n const isNotNull = existing.notNullColumns?.has(key) === true;\n if (required && !isNotNull) {\n if (empty) {\n actions.push({\n kind: \"set-not-null\",\n target: key,\n sql: `ALTER TABLE \"${table.schema}\".\"${table.table}\" ALTER COLUMN \"${column.column}\" SET NOT NULL;`\n });\n } else {\n withheldConstraints.push({\n target: key,\n kind: \"not-null\",\n reason:\n `\"${column.column}\" became required, but \"${table.qualified}\" holds rows and any of them ` +\n \"with no value would make SET NOT NULL fail.\",\n remedy:\n \"Backfill the column first — `UPDATE … SET \\\"\" + column.column +\n \"\\\" = … WHERE \\\"\" + column.column + \"\\\" IS NULL` — then apply this again.\"\n });\n }\n }\n if (!required && isNotNull) {\n // Loosening never fails and never loses data. It is here rather\n // than at boot because a database adopted by introspection carries\n // NOT NULL on columns the generated collection deliberately leaves\n // optional, and converging those unasked would drop constraints\n // nobody edited.\n actions.push({\n kind: \"drop-not-null\",\n target: key,\n sql: `ALTER TABLE \"${table.schema}\".\"${table.table}\" ALTER COLUMN \"${column.column}\" DROP NOT NULL;`\n });\n }\n };\n\n // 3. Scalar columns, then the auth columns the collection never mentions.\n // The scaffold's users collection describes 12 of the 14 columns auth\n // reads and writes, so planning only from properties left `is_anonymous`\n // and `tokens_valid_after` to `ensureAuthTablesExist` — which does create\n // them, but only because that function happens to run later in the same\n // boot. Planning them here makes this path self-contained.\n for (const table of collectionTables) {\n assertSafeIdentifier(table.schema, \"schema name\");\n assertSafeIdentifier(table.table, \"table name\");\n for (const column of table.columns) {\n if (!isScalarProperty(table, column)) continue;\n planColumn(table, column);\n }\n // The implicit key of a collection that declares none. It is created\n // with the table, so the only thing to plan is the default a table\n // made before the key had one is missing — without it, nothing can\n // insert a row there: no door sends a key that is not a property.\n for (const column of table.columns) {\n if (column.source.kind !== \"implicit-id\" || !column.primaryKey) continue;\n if (existing.tables.get(table.qualified)?.has(column.column) !== true) continue;\n planMissingDefault(table, column);\n }\n for (const column of table.columns) {\n if (column.source.kind !== \"auth\") continue;\n addColumn(table, column.column, renderColumnDefinition(column));\n }\n }\n\n // 3a. The generated search columns.\n //\n // Adding a STORED generated column rewrites the table, which on a large\n // one is not free — but it is the same additive shape as every other\n // column here, and the alternative (leaving it out until someone runs a\n // migration) is a declared `search` block that silently does nothing.\n //\n // Changing one is not additive, and `ADD COLUMN IF NOT EXISTS` is a\n // no-op against a column that is already there — which is why a `search`\n // block that gained a field, flipped `unaccent` or moved a weight used\n // to be inert forever, on every path, with nothing logged. Each column\n // carries a fingerprint of the expression it was built from, and a\n // mismatch is reported rather than applied.\n for (const table of collectionTables) {\n if (!table.search) continue;\n const definitions = new Map(\n table.columns\n .filter(c => c.source.kind === \"search\")\n .map(c => [c.column, renderColumnDefinition(c)] as const)\n );\n for (const stamp of searchColumnStamps(table.search)) {\n const definition = definitions.get(stamp.column)!;\n const exists = existing.tables.get(table.qualified)?.has(stamp.column) === true;\n const recorded = existing.columnComments?.get(`${table.qualified}.${stamp.column}`);\n\n if (exists && recorded?.startsWith(SEARCH_STAMP_PREFIX) && recorded !== stamp.fingerprint) {\n searchDrift.push({\n table: table.qualified,\n column: stamp.column,\n found: recorded,\n expected: stamp.fingerprint,\n rebuild: [\n `ALTER TABLE \"${table.schema}\".\"${table.table}\" DROP COLUMN \"${stamp.column}\";`,\n `ALTER TABLE \"${table.schema}\".\"${table.table}\" ADD COLUMN \"${stamp.column}\" ${definition};`,\n stamp.sql\n ]\n });\n // The old stamp is the only evidence of what the column holds;\n // overwriting it here would erase the drift instead of fixing it.\n continue;\n }\n\n addColumn(table, stamp.column, definition);\n if (exists && recorded === undefined) {\n searchAdopted.push({ table: table.qualified, column: stamp.column });\n }\n if (recorded !== stamp.fingerprint) {\n actions.push({ kind: \"comment-column\", target: `${table.qualified}.${stamp.column}`, sql: stamp.sql });\n }\n }\n }\n\n // 3b. A junction that already existed, but is short a column. One created\n // above already carries both, so re-listing them would log two no-op\n // statements and inflate the count of changes applied.\n for (const table of junctionTables) {\n if (created.has(table.qualified)) continue;\n for (const column of table.columns) {\n if (renameLegacyColumn(table, column)) continue;\n // A payload column (`through.properties`) goes through `planColumn`,\n // the same pass a collection's scalar columns take. The bare\n // `ADD COLUMN <type>` below is right for the two key columns — they\n // are NOT NULL by the composite primary key and have no default —\n // and wrong for everything else: it would drop a `defaultValue`, and\n // it would either omit a NOT NULL the collection declared or apply\n // one to a junction that already holds rows without a value to\n // backfill them. `planColumn` is the one place that decides which of\n // those is safe, and it reports the constraint it withheld.\n if (column.source.kind === \"property\") {\n planColumn(table, column);\n continue;\n }\n // A key column whose type is not its endpoint's key type is a\n // foreign key that can never be added (42804) — said, like any\n // other column's drift, rather than failed on every boot.\n reportTypeDrift(table, column);\n addColumn(table, column.column, renderPgType(column.type));\n }\n }\n\n // 3c. The columns relation and reference properties own.\n for (const table of collectionTables) {\n for (const column of table.columns) {\n if (column.source.kind !== \"relation\" && column.source.kind !== \"reference\") continue;\n // A declared property already emits this column (an explicit\n // `postId` beside the `belongsTo` that uses it); the relation\n // contributes only the constraint, below.\n if (column.columnOwnedByProperty) continue;\n // A reference that is part of a composite key was created with\n // the table, in step 2.\n if (isKeyColumn(table, column)) continue;\n if (renameLegacyColumn(table, column)) continue;\n planColumn(table, column);\n }\n }\n\n // 4. Foreign keys, last: the tables and columns on both ends have to exist\n // first, and a constraint is the one thing here that can fail on data\n // rather than on schema, so nothing else depends on it.\n const knownConstraints = existing.constraints ?? new Set<string>();\n const plannedConstraints = new Set<string>();\n for (const table of [...collectionTables, ...junctionTables]) {\n for (const column of table.columns) {\n const fk = column.foreignKey;\n if (!fk) continue;\n const name = `${fk.schema}.${fk.table}.${fk.constraintName}`;\n if (knownConstraints.has(name) || plannedConstraints.has(name)) continue;\n plannedConstraints.add(name);\n actions.push({ kind: \"add-constraint\", target: name, sql: `${fk.sql};` });\n }\n }\n\n // 5. Indexes, after everything — the column has to exist, and this is the\n // one step that runs against a populated table for real work.\n //\n // CONCURRENTLY: a plain CREATE INDEX takes a lock that blocks writes for\n // the duration of the build, which on a live table is an outage. Each\n // statement here is issued on its own, outside any transaction, which is\n // the condition CONCURRENTLY requires.\n //\n // ...and only on a live table. It is withheld for a table this same plan\n // is creating: there are no rows to build from and no writer to block, so\n // the lock CONCURRENTLY avoids is a lock nobody would have taken. Worth\n // withholding rather than merely harmless, because CONCURRENTLY is the\n // one index form Postgres refuses inside a transaction block.\n const concurrent = (schema: string, table: string): boolean => !created.has(`${schema}.${table}`);\n for (const table of collectionTables) {\n if (!table.search) continue;\n for (const statement of searchIndexStatements(table.search)) {\n actions.push({\n kind: \"create-index\",\n target: table.qualified,\n sql: concurrent(table.schema, table.table)\n ? statement.replace(\"CREATE INDEX IF NOT EXISTS\", \"CREATE INDEX CONCURRENTLY IF NOT EXISTS\")\n : statement\n });\n }\n }\n\n // ANN indexes for vector columns, on the same terms. A column too wide\n // for pgvector to index is reported, not planned — silence there would\n // read as \"indexed\" to anyone watching the boot.\n const vectorIndexSkipped: SkippedVectorIndex[] = [];\n for (const table of collectionTables) {\n if (!table.vector) continue;\n for (const spec of table.vector.specs) {\n actions.push({\n kind: \"create-index\",\n target: `${spec.schema}.${spec.table}`,\n sql: concurrent(spec.schema, spec.table)\n ? vectorIndexStatement(spec).replace(\"CREATE INDEX IF NOT EXISTS\", \"CREATE INDEX CONCURRENTLY IF NOT EXISTS\")\n : vectorIndexStatement(spec)\n });\n }\n vectorIndexSkipped.push(...table.vector.skipped);\n }\n\n // Declared indexes, on exactly the same terms. Boot has to emit these,\n // not just `db push`: the managed runtime provisions at boot and never\n // runs a push, so a push-only index would simply not exist there — and\n // nothing would say so. `contracts/derived-names.txt` states the rule\n // (\"Both, or it is a bug\") and the gate enforces it.\n //\n // `concurrently` is a parameter rather than a string replacement on the\n // rendered SQL: the `.replace(…)` above silently does nothing for a\n // UNIQUE index, whose text is `CREATE UNIQUE INDEX …`.\n for (const spec of sortIndexSpecs(collectionTables.flatMap(t => t.indexes))) {\n actions.push({\n kind: \"create-index\",\n target: `${spec.schema}.${spec.table}`,\n sql: collectionIndexStatement(spec, {\n concurrently: concurrent(spec.schema, spec.table),\n ifNotExists: true\n })\n });\n }\n\n // 6. `autoValue: \"on_update\"` triggers. After the column, and idempotent by\n // DROP-then-CREATE rather than `CREATE OR REPLACE TRIGGER`, which exists\n // only on Postgres 14+.\n for (const table of collectionTables) {\n for (const trigger of table.triggers) {\n // Named after its table and column, so a trigger already in the\n // catalogue is this one — re-issuing it every boot would work and\n // would report a change on a database that has none.\n if (existing.triggers?.has(`${table.qualified}.${trigger.name}`)) continue;\n for (const statement of triggerStatements(trigger)) {\n actions.push({\n kind: \"create-trigger\",\n target: `${table.qualified}.${trigger.name}`,\n sql: statement\n });\n }\n }\n }\n\n // A column the database still requires that no property declares any more.\n //\n // Computed last, over the same live snapshot the rest of the plan read, and\n // only for tables this run did not create — a table created here has exactly\n // the columns the collection asked for, so there is nothing to orphan.\n //\n // Every element of the test matters. NOT NULL, because a nullable leftover\n // accepts a write and is merely untidy. No DEFAULT, because a leftover with\n // one is filled in for you. Not declared, because that is what makes it\n // unreachable — the author has no field to send.\n const orphanedRequiredColumns: OrphanedRequiredColumn[] = [];\n if (existing.notNullColumns && existing.columnDefaults) {\n for (const table of collectionTables) {\n const live = existing.tables.get(table.qualified);\n if (!live || created.has(table.qualified)) continue;\n const declared = new Set<string>();\n for (const column of table.columns) {\n declared.add(column.column);\n if (column.legacyColumn) declared.add(column.legacyColumn);\n }\n for (const column of live) {\n const key = `${table.qualified}.${column}`;\n if (declared.has(column)) continue;\n if (!existing.notNullColumns.has(key)) continue;\n if (existing.columnDefaults.has(key)) continue;\n orphanedRequiredColumns.push({ table: table.qualified, column, slug: table.slug! });\n }\n }\n }\n\n return {\n actions,\n statements: actions.map(a => a.sql),\n legacyForeignKeys,\n searchDrift,\n searchAdopted,\n vectorIndexSkipped,\n withheldConstraints,\n columnTypeDrift,\n orphanedRequiredColumns\n };\n}\n\n/** What a `CREATE FUNCTION` statement is called, for the action log. */\nconst functionTarget = (statement: string): string => {\n if (statement.includes(\"set_updated_at\")) return SET_UPDATED_AT_FN;\n return statement.includes(\"unaccent\") ? SEARCH_UNACCENT_FN : SEARCH_TEXT_FN;\n};\n","/**\n * Applying a schema plan to a database that is already running.\n *\n * ## What this module is\n *\n * A managed runtime boots someone else's compiled project against a database it\n * has never seen. Auth tables are ensured at boot already, but collection tables\n * were not created by anything: the platform ran the app and every `/api/data/*`\n * request answered 500 on a missing relation. `rebase db push` cannot help — it\n * is an Atlas-driven CLI command, and the runtime image ships no CLI.\n *\n * Two halves, and only one of them is here. *What the collections ask for* is\n * `schema/plan/plan-schema.ts`, shared with `db push` and the generated Drizzle\n * file; *what the database already has, and which of those statements are safe\n * to run against it* is `schema/plan/diff-plan.ts`. This module reads the\n * catalogue, runs the statements, and reports what happened.\n *\n * ## Why additive-only, forever\n *\n * This runs unattended, against a database with customers' data in it, with no\n * human reading a diff. So it may only ever do things that cannot lose data.\n * See `diff-plan.ts` for the whole of that argument.\n */\nimport {\n declaredDatabaseExtensions,\n REBASE_SCHEMA,\n type CollectionConfig\n} from \"@rebasepro/types\";\nimport { relationalCollections } from \"@rebasepro/common\";\nimport { logger, isConcurrentDdlRace, isDuplicateObjectRace } from \"@rebasepro/server\";\nimport { SEARCH_STAMP_PREFIX } from \"./search-column\";\nimport { vectorExtensionHint } from \"./vector-index\";\nimport { planJunctionTables, quoteSqlLiteral } from \"./generate-postgres-ddl-logic\";\nimport { quoteRelation } from \"./introspect-db-queries\";\nimport { planSchema } from \"./plan/plan-schema\";\nimport {\n assertSafeIdentifier,\n diffPlanAgainstCatalogue,\n type ConstraintPolicy,\n type DiffOptions,\n type EnsureAction,\n type EnsurePlan,\n type ExistingSchema,\n type LegacyForeignKey,\n type OrphanedRequiredColumn,\n type SearchColumnDrift,\n type WithheldConstraint\n} from \"./plan/diff-plan\";\nimport { extractCauseMessage, extractPgError } from \"../utils/pg-error-utils\";\nimport { columnTypeDriftMessage, type ColumnTypeDrift } from \"./column-type-drift\";\n\nexport type {\n ConstraintPolicy,\n EnsureAction,\n EnsurePlan,\n ExistingSchema,\n LegacyForeignKey,\n OrphanedRequiredColumn,\n SearchColumnDrift,\n WithheldConstraint\n};\n\n/**\n * The subset of a database handle this needs: run a statement, get rows back.\n *\n * Deliberately parameterless. Everything here is DDL or catalogue reads keyed by\n * schema name, and schema names are identifiers — they cannot be bound as\n * parameters anyway. They are validated by `assertSafeIdentifier` before they\n * reach a statement, so a config that somehow carried a quote is refused rather\n * than concatenated.\n */\nexport interface Queryable {\n query<T = unknown>(sql: string): Promise<{ rows: T[] }>;\n}\n\nexport interface EnsureOptions extends DiffOptions {\n /**\n * Server extensions the project's databases gave Rebase leave to install —\n * `declaredDatabaseExtensions()`. Absent means none, which is a refusal and\n * is the right default for a planner given no configuration at all.\n *\n * Explicit rather than read from the resource registry in here, because the\n * planner is pure and several callers plan against fixtures: reaching for a\n * process-wide registry would make the plan depend on whatever some other\n * module happened to import. `ensureCollectionTables` is the boundary that\n * reads the world.\n */\n databaseExtensions?: readonly string[];\n}\n\nexport interface EnsureOutcome extends EnsurePlan {\n /**\n * Actions that could not be applied and are non-fatal by nature.\n *\n * Two kinds qualify. A foreign key can only fail on data that already\n * violates it, and the column it would police exists either way, so the\n * collection still serves; refusing to boot over one would turn a\n * pre-existing data problem into an outage. A column comment is the search\n * fingerprint, which needs table ownership — losing it costs drift\n * detection on the next boot, not the deployment. Both are reported loudly.\n */\n failures: { kind: EnsureAction[\"kind\"]; target: string; error: string }[];\n}\n\n/** The schema a collection lives in, validated before it reaches any DDL. */\nfunction schemaOf(collection: CollectionConfig): string {\n const schema = (collection as { schema?: string }).schema || \"public\";\n return assertSafeIdentifier(schema, \"schema name\");\n}\n\n/**\n * Decide what to add. Pure — the caller supplies what exists and runs the\n * result.\n *\n * A plan and a diff: the collections are read once, by `planSchema`, into the\n * same {@link SchemaPlan} `db push` renders `schema.sql` from, and the diff\n * subtracts what the database already has. Before that split this function had\n * its own reading of every `Property`, and the audit found twelve places where\n * it disagreed with the two generators — a required `author_id` that was\n * NOT NULL after a push and nullable after a boot among them, on the one path\n * with no developer in the loop.\n */\nexport function planCollectionSchemaEnsure(\n allCollections: CollectionConfig[],\n existing: ExistingSchema,\n options: EnsureOptions = {}\n): EnsurePlan {\n const plan = planSchema(allCollections, { databaseExtensions: options.databaseExtensions });\n return diffPlanAgainstCatalogue(plan, existing, { constraints: options.constraints });\n}\n\n/** Read what the database has, for the schemas the collections live in. */\nexport async function readExistingSchema(\n client: Queryable,\n schemas: string[]\n): Promise<ExistingSchema> {\n const tables = new Map<string, Set<string>>();\n const enums = new Set<string>();\n if (schemas.length === 0) return { tables, enums };\n\n const inList = schemas\n .map(schema => `'${assertSafeIdentifier(schema, \"schema name\")}'`)\n .join(\", \");\n\n const notNullColumns = new Set<string>();\n // `udt_name` rather than `data_type`: `data_type` collapses every array to\n // the literal string `ARRAY` and every extension type to `USER-DEFINED`,\n // which cannot be compared with anything. `udt_name` names the actual type\n // — `int4`, `jsonb`, `_numeric`, `vector` — which is what the drift check\n // needs.\n const columnTypes = new Map<string, string>();\n // A NOT NULL column that has a DEFAULT is not a problem for a write that\n // omits it, so orphan detection needs the default as well as the nullability\n // — otherwise every `created_at DEFAULT now()` on a table whose collection\n // does not declare it would be reported as a blocker.\n const columnDefaults = new Set<string>();\n const { rows: columns } = await client.query<{\n table_schema: string;\n table_name: string;\n column_name: string;\n is_nullable: string;\n udt_name: string | null;\n column_default: string | null;\n numeric_precision: number | null;\n numeric_scale: number | null;\n }>(\n // `numeric_precision`/`numeric_scale` because `udt_name` is `numeric`\n // for both `NUMERIC` and `NUMERIC(10, 2)`, and a property that declares\n // a precision means it: money stored in an unbounded column keeps the\n // third decimal the rounding was supposed to remove. Only carried for\n // `numeric`, where the modifier changes what a value *is*; a `varchar`\n // width is a limit on the same family and `typesAgree` ignores it.\n `SELECT table_schema, table_name, column_name, is_nullable, udt_name, column_default,\n numeric_precision, numeric_scale\n FROM information_schema.columns\n WHERE table_schema IN (${inList})`\n );\n for (const row of columns) {\n const key = `${row.table_schema}.${row.table_name}`;\n if (!tables.has(key)) tables.set(key, new Set());\n tables.get(key)!.add(row.column_name);\n if (row.is_nullable === \"NO\") notNullColumns.add(`${key}.${row.column_name}`);\n if (row.udt_name) {\n const modifier = row.udt_name === \"numeric\" && row.numeric_precision !== null\n ? `(${row.numeric_precision},${row.numeric_scale ?? 0})`\n : \"\";\n columnTypes.set(`${key}.${row.column_name}`, `${row.udt_name}${modifier}`);\n }\n if (row.column_default !== null) columnDefaults.add(`${key}.${row.column_name}`);\n }\n\n // Trigger names, so `autoValue: \"on_update\"` is installed once rather than\n // re-issued on every boot. `tgisinternal` excludes the ones Postgres itself\n // creates to enforce foreign keys.\n const triggers = new Set<string>();\n const { rows: triggerRows } = await client.query<{ schema: string; table: string; name: string }>(\n `SELECT n.nspname AS schema, c.relname AS table, t.tgname AS name\n FROM pg_trigger t\n JOIN pg_class c ON t.tgrelid = c.oid\n JOIN pg_namespace n ON c.relnamespace = n.oid\n WHERE NOT t.tgisinternal AND n.nspname IN (${inList})`\n );\n for (const row of triggerRows) triggers.add(`${row.schema}.${row.table}.${row.name}`);\n\n // Which tables hold rows. This is the only fact that decides whether a\n // NOT NULL can be added without reading the data, so it is worth a query.\n //\n // `reltuples` would be cheaper and is wrong for this: it is a planner\n // estimate, it is -1 on a table that has never been analyzed, and a table\n // that was full an hour ago still reads as full after a DELETE. A wrong\n // \"empty\" here means a boot that aborts on a constraint violation, so the\n // estimate is not good enough. `EXISTS … LIMIT 1` stops at the first row,\n // which makes the true cost one page read per table.\n //\n // Restricted to ordinary and partitioned tables: `information_schema.columns`\n // also lists views and materialized views, and probing those runs whatever\n // query defines them.\n const populatedTables = new Set<string>();\n const { rows: realTables } = await client.query<{ schema: string; name: string }>(\n `SELECT n.nspname AS schema, c.relname AS name\n FROM pg_class c\n JOIN pg_namespace n ON c.relnamespace = n.oid\n WHERE c.relkind IN ('r', 'p') AND n.nspname IN (${inList})`\n );\n // The names come from `pg_class`, not from the configuration, so on an\n // adopted database they include tables nobody declared — `2024_archive`,\n // `order-items`, `Sales Data`. Those are quoted, not refused: refusing a\n // name here refuses the boot over a table Rebase will never touch.\n if (realTables.length > 0) {\n const probes = realTables.map(row =>\n `SELECT ${quoteSqlLiteral(`${row.schema}.${row.name}`)} AS key, ` +\n `EXISTS(SELECT 1 FROM ${quoteRelation(row.schema, row.name)} LIMIT 1) AS populated`\n );\n const { rows: populationRows } = await client.query<{ key: string; populated: boolean }>(\n probes.join(\" UNION ALL \")\n );\n for (const row of populationRows) {\n if (row.populated) populatedTables.add(row.key);\n }\n }\n\n const enumValues = new Map<string, string[]>();\n const { rows: enumValueRows } = await client.query<{\n schema: string;\n name: string;\n value: string;\n }>(\n // Ordered by `enumsortorder`, not by label: an enum's order is part of\n // its meaning (it is what `<` compares), and reading it back sorted\n // alphabetically would make a correct type look drifted.\n `SELECT n.nspname AS schema, t.typname AS name, e.enumlabel AS value\n FROM pg_enum e\n JOIN pg_type t ON e.enumtypid = t.oid\n JOIN pg_namespace n ON t.typnamespace = n.oid\n WHERE n.nspname IN (${inList})\n ORDER BY t.typname, e.enumsortorder`\n );\n for (const row of enumValueRows) {\n const key = `${row.schema}.${row.name}`;\n if (!enumValues.has(key)) enumValues.set(key, []);\n enumValues.get(key)!.push(row.value);\n }\n\n const { rows: enumRows } = await client.query<{ schema: string; name: string }>(\n `SELECT n.nspname AS schema, t.typname AS name\n FROM pg_type t\n JOIN pg_namespace n ON t.typnamespace = n.oid\n WHERE t.typtype = 'e' AND n.nspname IN (${inList})`\n );\n for (const row of enumRows) enums.add(`${row.schema}.${row.name}`);\n\n // `ADD CONSTRAINT` has no IF NOT EXISTS, so an existing foreign key is\n // skipped by name rather than guarded in SQL.\n const constraints = new Set<string>();\n const { rows: constraintRows } = await client.query<{\n schema: string;\n table: string;\n name: string;\n }>(\n `SELECT n.nspname AS schema, c.relname AS table, con.conname AS name\n FROM pg_constraint con\n JOIN pg_class c ON con.conrelid = c.oid\n JOIN pg_namespace n ON c.relnamespace = n.oid\n WHERE n.nspname IN (${inList})`\n );\n for (const row of constraintRows) constraints.add(`${row.schema}.${row.table}.${row.name}`);\n\n // Column comments, which is where a generated search column records the\n // expression it was built from. `objsubid > 0` is what makes a row a\n // *column* comment rather than the table's own.\n const columnComments = new Map<string, string>();\n const { rows: commentRows } = await client.query<{\n schema: string;\n table: string;\n column: string;\n comment: string | null;\n }>(\n `SELECT n.nspname AS schema, c.relname AS table, a.attname AS column, d.description AS comment\n FROM pg_description d\n JOIN pg_class c ON d.objoid = c.oid\n JOIN pg_namespace n ON c.relnamespace = n.oid\n JOIN pg_attribute a ON a.attrelid = c.oid AND a.attnum = d.objsubid\n WHERE d.objsubid > 0 AND n.nspname IN (${inList})`\n );\n for (const row of commentRows) {\n if (row.comment == null) continue;\n columnComments.set(`${row.schema}.${row.table}.${row.column}`, row.comment);\n }\n\n return {\n tables, enums, constraints, columnComments, enumValues, notNullColumns, populatedTables, columnTypes,\n columnDefaults, triggers\n };\n}\n\n/**\n * What to tell an operator whose `search` block no longer matches its column.\n *\n * Every line here is doing work: naming the collection is not enough, because\n * the symptom (a search that finds nothing) points at the data, not the schema;\n * and the remediation has to be exact, because it is a table rewrite the\n * operator is being asked to schedule rather than discover.\n */\nfunction searchDriftMessage(drift: SearchColumnDrift[]): string {\n const blocks = drift.map(d =>\n ` \"${d.table}\".\"${d.column}\" was generated from a different \\`search\\` block ` +\n `(recorded ${d.found}, current ${d.expected}).\\n` +\n d.rebuild.map(s => ` ${s}`).join(\"\\n\")\n );\n return (\n \"The `search` block changed after its generated column was created, and Postgres cannot alter a \" +\n \"generated expression in place.\\n\" +\n \"Rebase will not rebuild it for you: dropping and re-adding a STORED generated column rewrites the whole \" +\n \"table under an ACCESS EXCLUSIVE lock and rebuilds its GIN index, which is an outage this unattended path \" +\n \"may not schedule on your behalf.\\n\" +\n \"Until it is rebuilt the column keeps indexing the previous fields, weights and language — searches for \" +\n \"anything added since return nothing, which reads from outside as \\\"no such row\\\".\\n\" +\n \"Run these (or revert the block to what the column was built from), then boot again:\\n\" +\n blocks.join(\"\\n\") +\n \"\\n The GIN index is dropped with the column and recreated concurrently on the next boot.\"\n );\n}\n\n\n/**\n * Read what the database looks like, for the schemas a set of collections\n * lives in.\n *\n * The same read `ensureCollectionTables` does at boot, exposed on its own for\n * the callers that want to *plan* against a real database without changing it —\n * the live schema editor, which has to tell somebody what a change would do\n * before they agree to it.\n */\nexport async function readSchemaFactsFor(\n client: Queryable,\n collections: CollectionConfig[]\n): Promise<ExistingSchema> {\n const relational = relationalCollections(collections);\n const schemas = Array.from(new Set([\n ...relational.map(schemaOf),\n ...planJunctionTables(relational).map(junction => junction.schema)\n ]));\n return readExistingSchema(client, schemas);\n}\n\n/**\n * Bring the database up to date. Returns what it did.\n *\n * Each statement runs on its own rather than in one transaction: they are all\n * independently safe and idempotent, and a single failure (an enum label that\n * cannot be added, say) should not roll back the tables that were created fine.\n * The error is surfaced with the statement that caused it.\n */\nexport async function ensureCollectionTables(\n client: Queryable,\n collections: CollectionConfig[],\n log?: (message: string) => void,\n options: EnsureOptions = {}\n): Promise<EnsureOutcome> {\n // This is the boundary, so this is where the world is read: the planner\n // itself takes the permission as an argument and never reaches for the\n // registry. By the time boot gets here `loadBundleResourceGraph` has\n // evaluated the project's `resources.ts`, so the declarations are there —\n // and a caller that already knows them can still say so.\n const schema = planSchema(collections, {\n databaseExtensions: options.databaseExtensions ?? declaredDatabaseExtensions()\n });\n\n // Junctions live alongside the collections that declare them, so their\n // schema has to be read too — otherwise an existing junction reads as\n // missing and its constraints as unplanned. Taken off the plan, which lists\n // both kinds of table.\n const schemas = Array.from(new Set(schema.tables.map(table => table.schema)));\n for (const name of schemas) {\n assertSafeIdentifier(name, \"schema name\");\n if (name !== \"public\") {\n await client.query(`CREATE SCHEMA IF NOT EXISTS \"${name}\";`);\n }\n }\n // `rebase` is where the `updated_at` trigger's function lives. Every real\n // boot has the schema by the time this runs (auth ensures it first) and\n // `db push` creates it in `schema.sql`, but this path is also driven by the\n // live schema editor and by tests, where \"the function's schema happens to\n // exist already\" is not a thing to rely on. Only when something needs it,\n // so a project with no trigger issues no statement — and not added to the\n // read set: what is *in* `rebase` is auth's business, not this planner's.\n if (schema.tables.some(table => table.triggers.length > 0)) {\n await client.query(`CREATE SCHEMA IF NOT EXISTS \"${REBASE_SCHEMA}\";`);\n }\n\n const existing = await readExistingSchema(client, schemas);\n const plan = diffPlanAgainstCatalogue(schema, existing, { constraints: options.constraints });\n const failures: EnsureOutcome[\"failures\"] = [];\n\n // Reported, not warned: this is a rename the ensure is about to perform, and\n // the operator should be able to see in the log why a column changed name.\n // The interesting case is the one that no longer happens — before this,\n // ensure added the new column empty beside the populated old one, every\n // statement succeeded, and the relation read the empty one.\n for (const legacy of plan.legacyForeignKeys) {\n const message =\n `Renaming \"${legacy.table}\".\"${legacy.legacy}\" to \"${legacy.expected}\". The old name is ` +\n \"the one Rebase derived for this relation before it singularized properly; the column \" +\n \"keeps its data, indexes and constraints. To keep the old name instead, set \" +\n `\\`localKey: \"${legacy.legacy}\"\\` on the relation and this will stop.`;\n logger.info(`[schema] ${message}`);\n log?.(message);\n }\n\n // Before anything is applied: a `search` block that changed after its column\n // was generated cannot be honoured by an additive plan, and serving the old\n // index while the config describes a new one is the silent failure this\n // check exists to end. Refusing is the loud half — boot is fatal on purpose\n // (see `ensureCollectionSchema` in the server's boot) and the message\n // carries the exact statements that resolve it.\n if (plan.searchDrift.length > 0) {\n throw new Error(searchDriftMessage(plan.searchDrift));\n }\n\n // Said once per column, at the moment the stamp is applied: from here on a\n // change is detected, but whether *this* column matches the block it is\n // being stamped with is not knowable — it predates the stamp.\n for (const adopted of plan.searchAdopted) {\n const message =\n `Adopting the existing generated column \"${adopted.table}\".\"${adopted.column}\" and recording what the ` +\n \"current `search` block would generate. Any later change to that block will be detected and refused; a \" +\n \"change made *before* this version was deployed cannot be, so if search has been missing content, \" +\n `rebuild the column once: ALTER TABLE \"${adopted.table.split(\".\").join('\".\"')}\" DROP COLUMN \"${adopted.column}\"; and boot again.`;\n logger.info(`[schema] ${message}`);\n log?.(message);\n }\n\n // Said once per column, every boot: an unindexed vector column and an\n // indexed one behave identically apart from latency, so the only way anyone\n // learns which one they have is if the boot says so.\n for (const skip of plan.vectorIndexSkipped) {\n const message = `No ANN index on \"${skip.table}\".\"${skip.column}\": ${skip.reason}`;\n logger.warn(`[schema] ${message}`);\n log?.(message);\n }\n\n // Said once per column, every boot, because the alternative is what this\n // whole feature exists to end: a column the configuration calls required,\n // sitting there nullable, with every surface reporting success. The boot\n // does not fail over it — the column is usable and the data is intact — but\n // it stops being invisible.\n for (const withheld of plan.withheldConstraints) {\n const message =\n `No ${withheld.kind === \"unique\" ? \"UNIQUE\" : \"NOT NULL\"} on \"${withheld.target}\": ` +\n `${withheld.reason} ${withheld.remedy}`;\n logger.warn(`[schema] ${message}`);\n log?.(message);\n }\n\n // Said once per drifted column, every boot, and this is the one report here\n // that describes a system which currently *looks* healthy. Everything else\n // in this function explains a statement that did not run; this explains a\n // statement that was never planned, on a deploy that reported success, and\n // whose first symptom is a write rejected long afterwards.\n for (const drift of plan.columnTypeDrift) {\n const message = columnTypeDriftMessage(drift);\n logger.warn(`[schema] ${message}`);\n log?.(message);\n }\n\n // Said once per column, every boot, and it matters more than the rest here\n // because the table is not merely imperfect — it is unwritable, and the\n // error the API returns names a field the author has already deleted.\n // This is what a renamed property looks like to an additive provisioner.\n for (const orphan of plan.orphanedRequiredColumns) {\n const [schema, table] = orphan.table.split(\".\");\n const message =\n `\"${orphan.table}\".\"${orphan.column}\" is NOT NULL with no default, and the \"${orphan.slug}\" ` +\n \"collection no longer declares it. Every insert will be rejected for a field you cannot set \" +\n `(PG_23502, naming \"${orphan.column}\"). This is what a renamed property looks like: the new ` +\n \"column was added, and the old one cannot be dropped unattended. Fix it with ONE of:\\n\" +\n ` ALTER TABLE \"${schema}\".\"${table}\" DROP COLUMN \"${orphan.column}\"; -- the rename is complete\\n` +\n ` ALTER TABLE \"${schema}\".\"${table}\" ALTER COLUMN \"${orphan.column}\" DROP NOT NULL; -- keep the data, stop requiring it\\n` +\n \" ...or declare the property again, if the column is still wanted.\";\n logger.warn(`[schema] ${message}`);\n log?.(message);\n }\n\n if (plan.actions.length === 0) {\n log?.(\"Schema is up to date; nothing to create.\");\n return { ...plan, failures };\n }\n\n for (const action of plan.actions) {\n try {\n if (await applyAction(client, action)) {\n log?.(`${action.kind}: ${action.target}`);\n } else {\n log?.(`${action.kind}: ${action.target} (already created by a peer)`);\n }\n } catch (err) {\n const message = describeDriverError(err);\n // A foreign key is the only action that can fail on the customer's\n // data rather than on the schema. The column it polices is already\n // there, so the collection serves either way — record it and carry\n // on rather than crash-looping the deployment.\n //\n // A comment is metadata about a column that was just created\n // successfully, and it can only fail on ownership (COMMENT requires\n // owning the table, which an adopted table may not grant). Losing\n // the stamp costs drift detection on the next boot; it must not cost\n // the deployment.\n //\n // An index that is not UNIQUE is an optimization, and losing an\n // optimization must not cost the deployment. This was the difference\n // between a slow collection and a tenant that would not boot: on a\n // managed runtime the throw killed the process, the pod never went\n // ready, and the deploy reported a 300-second readiness timeout — for\n // an ANN index whose absence changes latency and nothing else.\n //\n // UNIQUE is excluded because it is not an optimization. It is the\n // collection declaring that two rows cannot share a value, and\n // serving without it is serving a promise the database is not\n // keeping.\n const survivable =\n action.kind === \"add-constraint\" ||\n action.kind === \"comment-column\" ||\n (action.kind === \"create-index\" && !/CREATE\\s+UNIQUE\\s+INDEX/i.test(action.sql));\n if (survivable) {\n failures.push({ kind: action.kind, target: action.target, error: message });\n continue;\n }\n throw new Error(\n `Failed to ${action.kind} ${action.target}: ${message}${vectorExtensionHint(message)}\\n ${action.sql}`\n );\n }\n }\n return { ...plan, failures };\n}\n\n/**\n * What actually went wrong, rather than what the ORM said about it.\n *\n * Drizzle wraps every driver error, and its `message` is literally\n * ``Failed query: ${sql}\\nparams: ${params}`` — the statement we already know,\n * and not one word of Postgres's answer. Reporting `err.message` therefore\n * produced a failure that named the statement twice and the *reason* zero\n * times, so four unrelated boot failures — a missing extension, a wrong\n * privilege, a bad index method, a constraint the rows violate — all read\n * identically to whoever had to fix them.\n *\n * Two consequences, both observed in the field before this existed:\n *\n * 1. On a managed deployment the reason never reaches anybody. Boot dies, the\n * pod never goes ready, and the deploy reports a readiness timeout — so the\n * one line that says why is the one line that was thrown away here.\n * 2. {@link vectorExtensionHint} matches on the error text. Against the\n * wrapper it matched nothing, so the hint written specifically to explain a\n * missing pgvector never fired on the path that raises it.\n *\n * `detail` and `hint` come along because they are the fields Postgres uses to\n * say which object it meant and what to do about it — `CREATE EXTENSION` names\n * the missing library there, and a rejected constraint names the row. This\n * writes into the tenant's own boot log, about the tenant's own database.\n */\nexport function describeDriverError(err: unknown): string {\n const pg = extractPgError(err);\n if (pg) {\n const parts = [pg.code ? `${pg.message} [${pg.code}]` : pg.message];\n if (pg.detail) parts.push(` detail: ${pg.detail}`);\n if (pg.hint) parts.push(` hint: ${pg.hint}`);\n return parts.join(\"\\n\");\n }\n // No SQLSTATE anywhere in the chain: not a database answer at all — a\n // socket that closed, a bug in this file. The deepest cause still beats the\n // wrapper, and the wrapper still beats nothing.\n const cause = extractCauseMessage(err);\n if (cause) return cause;\n return err instanceof Error ? err.message : String(err);\n}\n\n/** ` CONCURRENTLY ` as it appears in a rendered `CREATE [UNIQUE] INDEX`. */\nconst CONCURRENTLY_RE = /\\sCONCURRENTLY\\s/i;\n\n/**\n * `25001 active_sql_transaction` — \"cannot run inside a transaction block\".\n *\n * Only ever raised here by `CREATE INDEX CONCURRENTLY`, and only when the\n * connection we were handed is inside a transaction. Deliberately narrow: it\n * decides to retry a *different* statement, so it must not fire on anything it\n * has not been reasoned about.\n */\nfunction isTransactionBlockRefusal(err: unknown): boolean {\n return extractPgError(err)?.code === \"25001\";\n}\n\n/** Attempts per action, including the first. Matches the server's bootstraps. */\nconst DDL_ATTEMPTS = 4;\n\n/**\n * Run one planned statement, surviving a simultaneous boot.\n *\n * Every statement in a plan is written to be idempotent, and that is not the\n * same as being safe to run concurrently: `CREATE … IF NOT EXISTS` reads the\n * catalog and then writes to it as two steps, so peers starting together both\n * see \"absent\" and the loser gets a duplicate key on a *catalog* index. Measured\n * against Postgres 18: five instances, 8 of 10 calls lost. `CREATE TYPE` is\n * worse, because Postgres has no `IF NOT EXISTS` for it at all.\n *\n * What made that fatal here rather than merely noisy is the loop this sits in.\n * A losing statement threw, and the throw abandoned **every remaining action in\n * the plan** — so a replica that lost one race came up missing tables it never\n * attempted, and the boot log blamed the one statement that failed.\n *\n * @returns `true` if this process applied the statement, `false` if a peer had\n * already created the object. The distinction is only for the log; both mean\n * the object is now there.\n * @throws the original error for anything that is not a race — a syntax error, a\n * permission failure, a unique constraint the customer's own rows violate.\n */\nasync function applyAction(\n client: Queryable,\n action: EnsureAction\n): Promise<boolean> {\n let sqlText = action.sql;\n for (let attempt = 1; ; attempt++) {\n try {\n await client.query(sqlText);\n return true;\n } catch (err) {\n // `CREATE INDEX CONCURRENTLY` is the one statement here Postgres\n // refuses inside a transaction block (25001), and whether we are in\n // one is not ours to know: the client is whatever handle the caller\n // bootstrapped with, and an application that builds its own adapter\n // can hand us a connection already inside a transaction. The plain\n // form is always correct and never refused — it takes a write lock\n // for the build, which is the cost of getting the index at all.\n if (isTransactionBlockRefusal(err) && CONCURRENTLY_RE.test(sqlText)) {\n sqlText = sqlText.replace(CONCURRENTLY_RE, \" \");\n logger.warn(\n `[schema] ${action.kind} ${action.target}: this connection is inside a transaction, ` +\n \"so the index is being built with a write lock rather than concurrently. Writes to \" +\n \"this table block until it finishes.\"\n );\n continue;\n }\n // Already there. Not \"retry\" — the end state this statement wanted\n // is the end state the database is in, so carry on to the next\n // action rather than spending three more attempts proving it.\n if (isDuplicateObjectRace(err)) {\n logger.debug(\n `[schema] ${action.kind} ${action.target}: already created by another instance`\n );\n return false;\n }\n // Retryable but not yet satisfied — a deadlock between two boots\n // taking catalog locks in step. The statement did nothing; run it\n // again after a jittered pause so peers that collided once do not\n // collide again in lockstep.\n if (isConcurrentDdlRace(err) && attempt < DDL_ATTEMPTS) {\n logger.debug(\n `[schema] ${action.kind} ${action.target}: lost a race with another instance ` +\n `(attempt ${attempt}/${DDL_ATTEMPTS}) — retrying`\n );\n await new Promise(resolve => setTimeout(resolve, 40 * attempt * (1 + Math.random())));\n continue;\n }\n throw err;\n }\n }\n}\n"],"mappings":"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"}
|
|
@@ -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 { r as authUsersColumnSql, t as AUTH_USERS_COLUMNS } from "./auth-users-columns-
|
|
5
|
-
import { n as RLS_BOOTSTRAP_STATEMENTS } from "./rls-bootstrap-sql-
|
|
4
|
+
import { r as authUsersColumnSql, t as AUTH_USERS_COLUMNS } from "./auth-users-columns-C72EMoDJ.js";
|
|
5
|
+
import { n as RLS_BOOTSTRAP_STATEMENTS } from "./rls-bootstrap-sql-_KNnjanK.js";
|
|
6
6
|
import { logger } from "@rebasepro/server";
|
|
7
7
|
import { sql } from "drizzle-orm";
|
|
8
8
|
import { revokeInternalTableAccess } from "@rebasepro/common";
|
|
@@ -219,6 +219,7 @@ async function ensureAuthTablesExist(db, collection) {
|
|
|
219
219
|
const emailLengthConstraint = `"${authIdentifier("email_length_check")}"`;
|
|
220
220
|
const emailLowerUniqueIndex = authIdentifier("email_lower_key");
|
|
221
221
|
const verificationTokenIndex = authIdentifier("email_verification_token_idx");
|
|
222
|
+
const emailChangeTokenIndex = authIdentifier("email_change_token_idx");
|
|
222
223
|
const usersColumnDdl = AUTH_USERS_COLUMNS.map((spec) => spec.column === "email" ? `${spec.column} ${authUsersColumnSql(spec)} CONSTRAINT ${emailLengthConstraint} CHECK (length(email) <= 320)` : `${spec.column} ${authUsersColumnSql(spec)}`).join(",\n ");
|
|
223
224
|
await db.execute(sql`
|
|
224
225
|
CREATE TABLE IF NOT EXISTS ${sql.raw(usersTableName)} (
|
|
@@ -482,6 +483,11 @@ async function ensureAuthTablesExist(db, collection) {
|
|
|
482
483
|
CREATE INDEX IF NOT EXISTS ${sql.raw(`"${verificationTokenIndex}"`)}
|
|
483
484
|
ON ${sql.raw(usersTableName)} (email_verification_token)
|
|
484
485
|
WHERE email_verification_token IS NOT NULL
|
|
486
|
+
`);
|
|
487
|
+
if (usersColumnTypes.has("email_change_token")) await db.execute(sql`
|
|
488
|
+
CREATE INDEX IF NOT EXISTS ${sql.raw(`"${emailChangeTokenIndex}"`)}
|
|
489
|
+
ON ${sql.raw(usersTableName)} (email_change_token)
|
|
490
|
+
WHERE email_change_token IS NOT NULL
|
|
485
491
|
`);
|
|
486
492
|
try {
|
|
487
493
|
const found = (await db.execute(sql`
|
|
@@ -498,6 +504,7 @@ async function ensureAuthTablesExist(db, collection) {
|
|
|
498
504
|
await db.execute(sql`ALTER TABLE ${sql.raw(qualified)} ADD COLUMN IF NOT EXISTS rotated_at TIMESTAMP WITH TIME ZONE`);
|
|
499
505
|
await db.execute(sql`ALTER TABLE ${sql.raw(qualified)} ADD COLUMN IF NOT EXISTS session_started_at TIMESTAMP WITH TIME ZONE`);
|
|
500
506
|
await db.execute(sql`ALTER TABLE ${sql.raw(qualified)} ADD COLUMN IF NOT EXISTS aal TEXT`);
|
|
507
|
+
await db.execute(sql`ALTER TABLE ${sql.raw(qualified)} ADD COLUMN IF NOT EXISTS method TEXT`);
|
|
501
508
|
await db.execute(sql`
|
|
502
509
|
UPDATE ${sql.raw(qualified)}
|
|
503
510
|
SET session_id = gen_random_uuid()::text
|
|
@@ -647,4 +654,4 @@ async function ensureAuthTablesExist(db, collection) {
|
|
|
647
654
|
//#endregion
|
|
648
655
|
export { resolveAuthSchema as i, ensure_tables_exports as n, probeAuthSchema as r, ensureAuthTablesExist as t };
|
|
649
656
|
|
|
650
|
-
//# sourceMappingURL=ensure-tables-
|
|
657
|
+
//# sourceMappingURL=ensure-tables-BmI_tRxc.js.map
|
|
@@ -0,0 +1 @@
|
|
|
1
|
+
{"version":3,"file":"ensure-tables-BmI_tRxc.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 } from \"../schema/rls-bootstrap-sql\";\nimport {\n AuthSchemaVersionError,\n assertAuthSchemaCompatible,\n resolveAuthSchema,\n stampAuthSchemaVersion\n} from \"./schema-version\";\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 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 db.execute(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 token_hash for faster lookups\n await db.execute(sql`\n CREATE INDEX IF NOT EXISTS idx_refresh_tokens_hash \n ON ${sql.raw(refreshTokensTableName)}(token_hash)\n `);\n\n // Create index on uid for cleanup operations\n await db.execute(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 token_hash for password reset lookups\n await db.execute(sql`\n CREATE INDEX IF NOT EXISTS idx_password_reset_tokens_hash \n ON ${sql.raw(passwordResetTokensTableName)}(token_hash)\n `);\n\n // Create index on uid for password reset cleanup\n await db.execute(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 token_hash for magic link lookups\n await db.execute(sql`\n CREATE INDEX IF NOT EXISTS idx_magic_link_tokens_hash \n ON ${sql.raw(magicLinkTokensTableName)}(token_hash)\n `);\n\n // Create index on uid for magic link cleanup\n await db.execute(sql`\n CREATE INDEX IF NOT EXISTS idx_magic_link_tokens_user \n ON ${sql.raw(magicLinkTokensTableName)}(uid)\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 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 // 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 for (const spec of AUTH_USERS_COLUMNS) {\n if (spec.column === \"email\") 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 }\n\n // Which of the columns below the table actually has. An adopted table —\n // one 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 usersColumns = 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 `);\n type UsersColumnRow = {\n column_name: string;\n data_type: string;\n is_nullable: \"YES\" | \"NO\";\n column_default: string | null;\n };\n const usersColumnRows = usersColumns.rows as UsersColumnRow[];\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 const indexPresent = await db.execute(sql`\n SELECT 1 FROM pg_class c\n JOIN pg_namespace n ON n.oid = c.relnamespace\n WHERE n.nspname = ${usersSchema} AND c.relname = ${emailLowerUniqueIndex} AND c.relkind = 'i'\n `);\n if (indexPresent.rows.length === 0) {\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 db.execute(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 db.execute(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 for (const { table_schema } of found) {\n const qualified = `\"${table_schema}\".\"refresh_tokens\"`;\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 db.execute(sql`ALTER TABLE ${sql.raw(qualified)} ADD COLUMN IF NOT EXISTS session_id TEXT`);\n await db.execute(sql`ALTER TABLE ${sql.raw(qualified)} ADD COLUMN IF NOT EXISTS revoked BOOLEAN DEFAULT FALSE NOT NULL`);\n await db.execute(sql`ALTER TABLE ${sql.raw(qualified)} ADD COLUMN IF NOT EXISTS rotated_at TIMESTAMP WITH TIME ZONE`);\n await db.execute(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 db.execute(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 db.execute(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.\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 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 await db.execute(sql`ALTER TABLE ${sql.raw(qualified)} ALTER COLUMN session_id SET DEFAULT gen_random_uuid()::text`);\n await db.execute(sql`ALTER TABLE ${sql.raw(qualified)} ALTER COLUMN session_started_at SET DEFAULT NOW()`);\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 await db.execute(sql`ALTER TABLE ${sql.raw(qualified)} ALTER COLUMN session_id SET NOT NULL`);\n await db.execute(sql`ALTER TABLE ${sql.raw(qualified)} ALTER COLUMN session_started_at SET NOT NULL`);\n\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 // 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 await db.execute(sql`ALTER TABLE ${sql.raw(qualified)} DROP CONSTRAINT IF EXISTS unique_device_session`);\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 db.execute(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 db.execute(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 try {\n await db.execute(sql`ALTER TABLE ${sql.raw(mfaFactorsTableName)} ADD COLUMN IF NOT EXISTS last_used_counter BIGINT`);\n await db.execute(sql`ALTER TABLE ${sql.raw(mfaChallengesTableName)} ADD COLUMN IF NOT EXISTS attempts INTEGER NOT NULL DEFAULT 0`);\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 db.execute(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 for (const [schemaName, tableName] of authTablePairs) {\n const forced = await db.execute(sql`\n SELECT 1\n FROM pg_class c\n JOIN pg_namespace n ON n.oid = c.relnamespace\n WHERE n.nspname = ${schemaName}\n AND c.relname = ${tableName}\n AND c.relforcerowsecurity\n `);\n if (forced.rows.length > 0) {\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 await revokeInternalTableAccess(\n async (text) => { await db.execute(sql.raw(text)); },\n authSchema,\n {\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 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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"}
|
|
@@ -1,6 +1,6 @@
|
|
|
1
1
|
import { createRequire as __createRequire } from "module";
|
|
2
2
|
__createRequire(import.meta.url);
|
|
3
|
-
import { t as generateSchema } from "./generate-drizzle-schema-logic-
|
|
3
|
+
import { t as generateSchema } from "./generate-drizzle-schema-logic-sSFDp6LR.js";
|
|
4
4
|
import { n as outError, t as out } from "./cli-output-CNdMql-L.js";
|
|
5
5
|
import { loadCollectionsFromDirectory } from "@rebasepro/server";
|
|
6
6
|
import { promises } from "fs";
|
|
@@ -120,4 +120,4 @@ if (import.meta.url.endsWith(process.argv[1])) main().catch(() => {
|
|
|
120
120
|
//#endregion
|
|
121
121
|
export { main as t };
|
|
122
122
|
|
|
123
|
-
//# sourceMappingURL=generate-drizzle-schema-
|
|
123
|
+
//# sourceMappingURL=generate-drizzle-schema-93M0lUxK.js.map
|
package/dist/{generate-drizzle-schema-yzY_BLhr.js.map → generate-drizzle-schema-93M0lUxK.js.map}
RENAMED
|
@@ -1 +1 @@
|
|
|
1
|
-
{"version":3,"file":"generate-drizzle-schema-yzY_BLhr.js","names":[],"sources":["../src/schema/generate-next-step.ts","../src/schema/generate-drizzle-schema.ts"],"sourcesContent":["/**\n * The line `rebase schema generate` ends on.\n *\n * Its own module because `generate-drizzle-schema.ts` uses `import.meta` to\n * decide whether it was executed directly, which the driver's jest suite cannot\n * load at all — so the one sentence in that file with a decision in it had no\n * test and could not have one where it stood.\n */\n\nexport const formatTerminalText = (text: string, options: {\n bold?: boolean;\n backgroundColor?: \"blue\" | \"green\" | \"red\" | \"yellow\" | \"cyan\" | \"magenta\";\n textColor?: \"white\" | \"black\" | \"red\" | \"green\" | \"yellow\" | \"blue\" | \"magenta\" | \"cyan\";\n} = {}): string => {\n let codes = \"\";\n if (options.bold) codes += \"\\x1b[1m\";\n if (options.backgroundColor) {\n const bgColors = {\n blue: \"\\x1b[44m\",\n green: \"\\x1b[42m\",\n red: \"\\x1b[41m\",\n yellow: \"\\x1b[43m\",\n cyan: \"\\x1b[46m\",\n magenta: \"\\x1b[45m\"\n } as const;\n codes += bgColors[options.backgroundColor];\n }\n if (options.textColor) {\n const textColors = {\n white: \"\\x1b[37m\",\n black: \"\\x1b[30m\",\n red: \"\\x1b[31m\",\n green: \"\\x1b[32m\",\n yellow: \"\\x1b[33m\",\n blue: \"\\x1b[34m\",\n magenta: \"\\x1b[35m\",\n cyan: \"\\x1b[36m\"\n } as const;\n codes += textColors[options.textColor];\n }\n return `${codes}${text}\\x1b[0m`;\n};\n\n/**\n * What to do with the file that was just generated.\n *\n * Not the same answer on the managed development database: `rebase db generate`\n * plans with Atlas, Atlas needs a second empty database to diff against, and\n * PGlite serves exactly one — so the CLI refuses that command outright there.\n * This line recommended it unconditionally, which meant every stock scaffold\n * was told, by the tool itself, to run something the same tool would refuse.\n *\n * The kind arrives as `REBASE_DEV_DATABASE_KIND`, set by the CLI from the\n * ordered resolution in `dev-db/resolve.ts`. It is a pure decision — nothing is\n * started to make it — and the driver cannot make it for itself: on the managed\n * path `DATABASE_URL` is a perfectly ordinary connection string to a Postgres\n * on loopback. Absent (the driver run directly, outside the CLI) keeps the\n * original wording.\n */\nexport const nextStepAfterGenerate = (): string => {\n if (process.env.REBASE_DEV_DATABASE_KIND === \"managed\") {\n return `Start or restart ${formatTerminalText(\"rebase dev\", {\n bold: true,\n backgroundColor: \"blue\",\n textColor: \"black\"\n })} — boot applies your collections to the managed development database.`;\n }\n\n return `You can now run ${formatTerminalText(\"rebase db generate\", {\n bold: true,\n backgroundColor: \"blue\",\n textColor: \"black\"\n })} to generate the SQL migration files.`;\n};\n","import { promises as fsPromises } from \"fs\";\nimport * as fs from \"fs\";\nimport path from \"path\";\nimport { pathToFileURL } from \"url\";\nimport { generateSchema } from \"./generate-drizzle-schema-logic\";\nimport { CollectionConfig } from \"@rebasepro/types\";\nimport { loadCollectionsFromDirectory } from \"@rebasepro/server\";\nimport { out, outError } from \"../cli-output\";\nimport { nextStepAfterGenerate } from \"./generate-next-step\";\n\n\n// --- Execution and Watch Logic ---\n\nconst runGeneration = async (collectionsFilePath?: string, outputPath?: string) => {\n try {\n if (!collectionsFilePath) {\n throw new Error(\"No collections file path provided.\");\n }\n\n const resolvedPath = path.resolve(collectionsFilePath);\n\n // Shared with the runtime and the doctor: what gets generated here must\n // be exactly what the server serves, including directory-level defaults.\n let collections: CollectionConfig[] = await loadCollectionsFromDirectory(resolvedPath);\n\n\n // If collections directory is empty but exists, or failed to find any, we still want to inject defaults\n if (!collections || !Array.isArray(collections)) {\n collections = [];\n }\n\n\n // Sort collections by slug alphabetically to ensure deterministic schema generation\n collections.sort((a, b) => a.slug.localeCompare(b.slug));\n\n const schemaContent = generateSchema(collections);\n\n if (outputPath) {\n const outputDir = path.dirname(outputPath);\n await fsPromises.mkdir(outputDir, { recursive: true });\n await fsPromises.writeFile(outputPath, schemaContent);\n out(`✅ Drizzle schema generated successfully at ${outputPath}`);\n } else {\n out(\"✅ Drizzle schema generated successfully.\");\n out(String(schemaContent));\n }\n\n out(nextStepAfterGenerate());\n\n } catch (error) {\n outError(`Error generating schema: ${error instanceof Error ? (error.stack ?? error.message) : String(error)}`);\n // The same contract its sibling `generate-postgres-ddl.ts` keeps, and\n // for the same reason: `rebase db push` runs both as step 1 and reads\n // nothing but their exit codes. Exiting 0 after printing an error made\n // the push apply whatever `src/schema.generated.ts` was already there.\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\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).catch(() => undefined);\n });\n } else {\n await runGeneration(resolvedPath, resolvedOutputPath);\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":"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"}
|
|
1
|
+
{"version":3,"file":"generate-drizzle-schema-93M0lUxK.js","names":[],"sources":["../src/schema/generate-next-step.ts","../src/schema/generate-drizzle-schema.ts"],"sourcesContent":["/**\n * The line `rebase schema generate` ends on.\n *\n * Its own module because `generate-drizzle-schema.ts` uses `import.meta` to\n * decide whether it was executed directly, which the driver's jest suite cannot\n * load at all — so the one sentence in that file with a decision in it had no\n * test and could not have one where it stood.\n */\n\nexport const formatTerminalText = (text: string, options: {\n bold?: boolean;\n backgroundColor?: \"blue\" | \"green\" | \"red\" | \"yellow\" | \"cyan\" | \"magenta\";\n textColor?: \"white\" | \"black\" | \"red\" | \"green\" | \"yellow\" | \"blue\" | \"magenta\" | \"cyan\";\n} = {}): string => {\n let codes = \"\";\n if (options.bold) codes += \"\\x1b[1m\";\n if (options.backgroundColor) {\n const bgColors = {\n blue: \"\\x1b[44m\",\n green: \"\\x1b[42m\",\n red: \"\\x1b[41m\",\n yellow: \"\\x1b[43m\",\n cyan: \"\\x1b[46m\",\n magenta: \"\\x1b[45m\"\n } as const;\n codes += bgColors[options.backgroundColor];\n }\n if (options.textColor) {\n const textColors = {\n white: \"\\x1b[37m\",\n black: \"\\x1b[30m\",\n red: \"\\x1b[31m\",\n green: \"\\x1b[32m\",\n yellow: \"\\x1b[33m\",\n blue: \"\\x1b[34m\",\n magenta: \"\\x1b[35m\",\n cyan: \"\\x1b[36m\"\n } as const;\n codes += textColors[options.textColor];\n }\n return `${codes}${text}\\x1b[0m`;\n};\n\n/**\n * What to do with the file that was just generated.\n *\n * Not the same answer on the managed development database: `rebase db generate`\n * plans with Atlas, Atlas needs a second empty database to diff against, and\n * PGlite serves exactly one — so the CLI refuses that command outright there.\n * This line recommended it unconditionally, which meant every stock scaffold\n * was told, by the tool itself, to run something the same tool would refuse.\n *\n * The kind arrives as `REBASE_DEV_DATABASE_KIND`, set by the CLI from the\n * ordered resolution in `dev-db/resolve.ts`. It is a pure decision — nothing is\n * started to make it — and the driver cannot make it for itself: on the managed\n * path `DATABASE_URL` is a perfectly ordinary connection string to a Postgres\n * on loopback. Absent (the driver run directly, outside the CLI) keeps the\n * original wording.\n */\nexport const nextStepAfterGenerate = (): string => {\n if (process.env.REBASE_DEV_DATABASE_KIND === \"managed\") {\n return `Start or restart ${formatTerminalText(\"rebase dev\", {\n bold: true,\n backgroundColor: \"blue\",\n textColor: \"black\"\n })} — boot applies your collections to the managed development database.`;\n }\n\n return `You can now run ${formatTerminalText(\"rebase db generate\", {\n bold: true,\n backgroundColor: \"blue\",\n textColor: \"black\"\n })} to generate the SQL migration files.`;\n};\n","import { promises as fsPromises } from \"fs\";\nimport * as fs from \"fs\";\nimport path from \"path\";\nimport { pathToFileURL } from \"url\";\nimport { generateSchema } from \"./generate-drizzle-schema-logic\";\nimport { CollectionConfig } from \"@rebasepro/types\";\nimport { loadCollectionsFromDirectory } from \"@rebasepro/server\";\nimport { out, outError } from \"../cli-output\";\nimport { nextStepAfterGenerate } from \"./generate-next-step\";\n\n\n// --- Execution and Watch Logic ---\n\nconst runGeneration = async (collectionsFilePath?: string, outputPath?: string) => {\n try {\n if (!collectionsFilePath) {\n throw new Error(\"No collections file path provided.\");\n }\n\n const resolvedPath = path.resolve(collectionsFilePath);\n\n // Shared with the runtime and the doctor: what gets generated here must\n // be exactly what the server serves, including directory-level defaults.\n let collections: CollectionConfig[] = await loadCollectionsFromDirectory(resolvedPath);\n\n\n // If collections directory is empty but exists, or failed to find any, we still want to inject defaults\n if (!collections || !Array.isArray(collections)) {\n collections = [];\n }\n\n\n // Sort collections by slug alphabetically to ensure deterministic schema generation\n collections.sort((a, b) => a.slug.localeCompare(b.slug));\n\n const schemaContent = generateSchema(collections);\n\n if (outputPath) {\n const outputDir = path.dirname(outputPath);\n await fsPromises.mkdir(outputDir, { recursive: true });\n await fsPromises.writeFile(outputPath, schemaContent);\n out(`✅ Drizzle schema generated successfully at ${outputPath}`);\n } else {\n out(\"✅ Drizzle schema generated successfully.\");\n out(String(schemaContent));\n }\n\n out(nextStepAfterGenerate());\n\n } catch (error) {\n outError(`Error generating schema: ${error instanceof Error ? (error.stack ?? error.message) : String(error)}`);\n // The same contract its sibling `generate-postgres-ddl.ts` keeps, and\n // for the same reason: `rebase db push` runs both as step 1 and reads\n // nothing but their exit codes. Exiting 0 after printing an error made\n // the push apply whatever `src/schema.generated.ts` was already there.\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\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).catch(() => undefined);\n });\n } else {\n await runGeneration(resolvedPath, resolvedOutputPath);\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":"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"}
|