@rebasepro/server-postgres 0.19.1 → 0.19.2-canary.g08eed46
This diff represents the content of publicly available package versions that have been released to one of the supported registries. The information contained in this diff is provided for informational purposes only and reflects changes between package versions as they appear in their respective public registries.
- package/dist/{BranchService-ByLfM_Tt.js → BranchService-W3DMfcZZ.js} +2 -2
- package/dist/{BranchService-ByLfM_Tt.js.map → BranchService-W3DMfcZZ.js.map} +1 -1
- package/dist/PostgresBackendDriver.d.ts +81 -7
- package/dist/PostgresBootstrapper.d.ts +10 -0
- package/dist/{backup-cli-oM9WQVlV.js → backup-cli-DSpQyqcG.js} +2 -2
- package/dist/{backup-cli-oM9WQVlV.js.map → backup-cli-DSpQyqcG.js.map} +1 -1
- package/dist/{backup-service-Br3tqwsy.js → backup-service-DA7a6SUV.js} +2 -2
- package/dist/{backup-service-Br3tqwsy.js.map → backup-service-DA7a6SUV.js.map} +1 -1
- package/dist/{cli-errors-DxN4yzdH.js → cli-errors-C3g_kHBw.js} +137 -18
- package/dist/cli-errors-C3g_kHBw.js.map +1 -0
- package/dist/cli-errors.d.ts +19 -0
- package/dist/cli-helpers.d.ts +47 -0
- package/dist/cli.js +233 -19
- package/dist/cli.js.map +1 -1
- package/dist/{collection-index-BHgT_TKd.js → collection-index-DdnsxiJ_.js} +14 -4
- package/dist/{collection-index-BHgT_TKd.js.map → collection-index-DdnsxiJ_.js.map} +1 -1
- package/dist/collections/validate-relations.d.ts +7 -0
- package/dist/column-plan-helpers-CpILzHJS.js +145 -0
- package/dist/column-plan-helpers-CpILzHJS.js.map +1 -0
- package/dist/{doctor-D7HWmGzs.js → doctor-D5SrGJ5P.js} +175 -61
- package/dist/doctor-D5SrGJ5P.js.map +1 -0
- package/dist/{ensure-collection-policies-2c2NV8zX.js → ensure-collection-policies-CHO0moXQ.js} +5 -5
- package/dist/{ensure-collection-policies-2c2NV8zX.js.map → ensure-collection-policies-CHO0moXQ.js.map} +1 -1
- package/dist/{ensure-collection-tables-D4wJzM4y.js → ensure-collection-tables-Bkvehxdm.js} +318 -312
- package/dist/ensure-collection-tables-Bkvehxdm.js.map +1 -0
- package/dist/{ensure-tables-CIojK-Rx.js → ensure-tables-CYMtuuOd.js} +2 -2
- package/dist/{ensure-tables-CIojK-Rx.js.map → ensure-tables-CYMtuuOd.js.map} +1 -1
- package/dist/generate-drizzle-schema-logic-Dfyf_MRu.js +228 -0
- package/dist/generate-drizzle-schema-logic-Dfyf_MRu.js.map +1 -0
- package/dist/{generate-drizzle-schema-CwEgVyjO.js → generate-drizzle-schema-vSK-VdYT.js} +3 -3
- package/dist/generate-drizzle-schema-vSK-VdYT.js.map +1 -0
- package/dist/generate-postgres-ddl-logic-CR2xcS7e.js +475 -0
- package/dist/generate-postgres-ddl-logic-CR2xcS7e.js.map +1 -0
- package/dist/{generated-schema-staleness-DQdfJl2A.js → generated-schema-staleness-DRQe2BpC.js} +81 -3
- package/dist/{generated-schema-staleness-DQdfJl2A.js.map → generated-schema-staleness-DRQe2BpC.js.map} +1 -1
- package/dist/history/HistoryService.d.ts +23 -4
- package/dist/index.es.js +1991 -580
- package/dist/index.es.js.map +1 -1
- package/dist/introspect-db-logic-C6LQdTxj.js.map +1 -1
- package/dist/plan-schema-Hgl62S-w.js +1531 -0
- package/dist/plan-schema-Hgl62S-w.js.map +1 -0
- package/dist/{policy-drift-kNJ6X2Uj.js → policy-drift-B0GDRh7_.js} +2 -2
- package/dist/{policy-drift-kNJ6X2Uj.js.map → policy-drift-B0GDRh7_.js.map} +1 -1
- package/dist/{rls-bootstrap-sql-DLDnUjtx.js → rls-bootstrap-sql-BlzsOUtz.js} +111 -8
- package/dist/{rls-bootstrap-sql-DLDnUjtx.js.map → rls-bootstrap-sql-BlzsOUtz.js.map} +1 -1
- package/dist/{rls-enforcement-DeYEjYLt.js → rls-enforcement-BV12vBwQ.js} +2 -1
- package/dist/rls-enforcement-BV12vBwQ.js.map +1 -0
- package/dist/schema/catalogue-schema.d.ts +97 -0
- package/dist/schema/collection-index.d.ts +9 -0
- package/dist/schema/column-plan-helpers.d.ts +125 -0
- package/dist/schema/config-relations.d.ts +32 -0
- package/dist/schema/doctor-cli.js +4 -4
- package/dist/schema/doctor.d.ts +9 -0
- package/dist/schema/dynamic-tables.d.ts +27 -6
- package/dist/schema/ensure-collection-tables.d.ts +31 -268
- package/dist/schema/generate-drizzle-schema-logic.d.ts +26 -8
- package/dist/schema/generate-drizzle-schema.js +1 -1
- package/dist/schema/generate-postgres-ddl-logic.d.ts +84 -168
- package/dist/schema/generate-postgres-ddl.js +8 -2
- package/dist/schema/generate-postgres-ddl.js.map +1 -1
- package/dist/schema/generated-column-conflicts.d.ts +84 -0
- package/dist/schema/generated-schema-diff.d.ts +56 -0
- package/dist/schema/generated-schema-staleness.d.ts +33 -0
- package/dist/schema/introspect-db-logic.d.ts +2 -0
- package/dist/schema/plan/diff-plan.d.ts +220 -0
- package/dist/schema/plan/plan-schema.d.ts +84 -0
- package/dist/schema/plan/render-ddl.d.ts +62 -0
- package/dist/schema/plan/render-drizzle.d.ts +31 -0
- package/dist/schema/plan/types.d.ts +410 -0
- package/dist/schema/plan/updated-at-trigger.d.ts +22 -0
- package/dist/schema/relation-names.d.ts +28 -0
- package/dist/schema/search-column.d.ts +9 -0
- package/dist/security/rls-enforcement.d.ts +17 -0
- package/dist/services/FetchService.d.ts +176 -36
- package/dist/services/PersistService.d.ts +17 -4
- package/dist/services/RelationService.d.ts +11 -3
- package/dist/services/collection-helpers.d.ts +16 -2
- package/dist/services/dataService.d.ts +29 -2
- package/dist/services/field-op-sql.d.ts +16 -0
- package/dist/services/field-viewer.d.ts +19 -0
- package/dist/services/realtimeService.d.ts +35 -1
- package/dist/services/row-pipeline.d.ts +29 -5
- package/dist/services/soft-delete.d.ts +73 -0
- package/dist/utils/pg-error-utils.d.ts +41 -1
- package/dist/{vector-index-DkWxm2de.js → vector-index-CMJKb3C9.js} +3 -14
- package/dist/{vector-index-DkWxm2de.js.map → vector-index-CMJKb3C9.js.map} +1 -1
- package/package.json +18 -10
- package/dist/cli-errors-DxN4yzdH.js.map +0 -1
- package/dist/doctor-D7HWmGzs.js.map +0 -1
- package/dist/ensure-collection-tables-D4wJzM4y.js.map +0 -1
- package/dist/generate-drizzle-schema-CwEgVyjO.js.map +0 -1
- package/dist/generate-drizzle-schema-logic-so2Z_lyC.js +0 -528
- package/dist/generate-drizzle-schema-logic-so2Z_lyC.js.map +0 -1
- package/dist/generate-postgres-ddl-logic-CtnpIBVv.js +0 -1312
- package/dist/generate-postgres-ddl-logic-CtnpIBVv.js.map +0 -1
- package/dist/rls-enforcement-DeYEjYLt.js.map +0 -1
|
@@ -0,0 +1 @@
|
|
|
1
|
+
{"version":3,"file":"generate-postgres-ddl-logic-CR2xcS7e.js","names":[],"sources":["../src/schema/plan/render-ddl.ts","../src/schema/generate-postgres-ddl-logic.ts"],"sourcesContent":["/**\n * The SQL files, rendered from a {@link SchemaPlan}.\n *\n * Four files, because Atlas owns exactly one of them:\n *\n * - `schema.sql` — the desired state Atlas diffs the database against.\n * - `policies.sql` — the RLS policies. Atlas has no model for a policy.\n * - `search.sql` — the generated `tsvector` column, its GIN index and the\n * IMMUTABLE helpers its expression calls. Atlas's free tier refuses to\n * *parse* a file containing a function, and it wipes the dev database it\n * diffs against, so a helper seeded there is gone by the time the plan runs.\n * - `vector.sql` — the `vector` columns and their ANN indexes. Atlas cannot\n * *execute* these: it materialises `schema.sql` in a dev database that is\n * created empty and emptied again on every run, so `VECTOR(384)` resolves\n * against a database that structurally cannot have pgvector.\n * - `triggers.sql` — `autoValue: \"on_update\"`, which is a function plus a\n * binding, so it is out for the same reason search is.\n *\n * Everything not in `schema.sql` is excluded from Atlas's view by pattern\n * (`…ExcludePatterns` below) and applied by Rebase — at push time by the CLI,\n * at boot by the schema ensure. Without the excludes a desired state that omits\n * an object reads to Atlas as an instruction to drop it.\n */\nimport {\n fuzzyColumnDefinition,\n searchColumnDefinition,\n searchColumnStamps,\n searchExtensionStatements,\n searchHelperFunctions,\n searchIndexNames,\n searchIndexStatements,\n searchStampGuards\n} from \"../search-column\";\nimport {\n VECTOR_EXTENSION_OPT_IN,\n vectorColumnDefinition,\n vectorDimensionGuard,\n vectorExtensionDeclared,\n vectorExtensionStatement,\n vectorIndexNames,\n vectorIndexStatements\n} from \"../vector-index\";\nimport { collectionIndexStatements } from \"../collection-index\";\nimport { quoteSqlLiteral } from \"./plan-schema\";\nimport { triggerStatements } from \"./updated-at-trigger\";\nimport type { ColumnPlan, PgType, PolicyPlan, SchemaPlan, TablePlan } from \"./types\";\n\n/** What `schema.sql` may leave out, because another file owns it. */\nexport interface DdlRenderOptions {\n /** RLS policies. `false` puts them in `policies.sql` alone. */\n includePolicies?: boolean;\n /** The search column, its helpers and its index. See `search.sql`. */\n includeSearch?: boolean;\n /** The vector columns and their ANN indexes. See `vector.sql`. */\n includeVector?: boolean;\n}\n\n/**\n * A {@link PgType} as Postgres spells it.\n *\n * The one place. `getSqlColumnType` and `getDrizzleColumn` each carried their\n * own ladder and disagreed about `VARCHAR` widths, about `geopoint`, and about\n * whether an `increment` id honours `columnType`.\n */\nexport const renderPgType = (type: PgType): string => {\n switch (type.kind) {\n case \"text\": return \"TEXT\";\n case \"varchar\": return `VARCHAR(${type.length})`;\n case \"char\": return `CHAR(${type.length})`;\n case \"uuid\": return \"UUID\";\n case \"enum\": return `\"${type.schema}\".\"${type.name}\"`;\n case \"smallint\": return \"SMALLINT\";\n case \"integer\": return \"INTEGER\";\n case \"bigint\": return \"BIGINT\";\n case \"smallserial\": return \"SMALLSERIAL\";\n case \"serial\": return \"SERIAL\";\n case \"bigserial\": return \"BIGSERIAL\";\n case \"real\": return \"REAL\";\n case \"doublePrecision\": return \"DOUBLE PRECISION\";\n case \"numeric\":\n if (type.precision === undefined) return \"NUMERIC\";\n return type.scale === undefined\n ? `NUMERIC(${type.precision})`\n : `NUMERIC(${type.precision}, ${type.scale})`;\n case \"boolean\": return \"BOOLEAN\";\n case \"timestamptz\": return \"TIMESTAMP WITH TIME ZONE\";\n case \"date\": return \"DATE\";\n case \"time\": return \"TIME\";\n case \"json\": return \"JSON\";\n case \"jsonb\": return \"JSONB\";\n case \"vector\": return `VECTOR(${type.dimensions})`;\n case \"bytea\": return \"BYTEA\";\n case \"tsvector\": return \"tsvector\";\n case \"array\": return `${renderPgType(type.of)}[]`;\n }\n};\n\n/**\n * Everything after the column name, in one order for every SQL emitter.\n *\n * `CREATE TABLE` and `ADD COLUMN` used to assemble this separately, which is\n * how a required relation column came out `NOT NULL` after `db push` and\n * nullable after a boot-ensure. `identity` is part of the type rather than a\n * DEFAULT, which is why it is rendered here and not in the DEFAULT slot.\n */\nexport const renderColumnDefinition = (column: ColumnPlan): string => {\n // `auth-users-columns` is the single description of what an auth user\n // table's columns must be, and it hands over a rendered definition.\n if (column.sqlDefinition) return column.sqlDefinition;\n\n if (column.generated) {\n return `${renderPgType(column.type)} GENERATED ALWAYS AS (${column.generated.expression}) STORED`;\n }\n\n let definition = renderPgType(column.type);\n if (column.default?.kind === \"identity\") definition += \" GENERATED BY DEFAULT AS IDENTITY\";\n if (column.primaryKey) definition += \" PRIMARY KEY\";\n if (column.unique) definition += \" UNIQUE\";\n if (column.default && column.default.kind !== \"identity\") {\n definition += ` DEFAULT ${column.default.kind === \"sql\" ? column.default.expression : column.default.sql}`;\n }\n if (!column.nullable && !column.primaryKey) definition += \" NOT NULL\";\n return definition;\n};\n\n/** `\"col\" TYPE …`, as it appears inside a `CREATE TABLE`. */\nconst renderColumn = (column: ColumnPlan): string =>\n `\"${column.column}\" ${renderColumnDefinition(column)}`;\n\n// ── Policies ─────────────────────────────────────────────────────────────────\n\n/**\n * One policy as its `DROP` / `CREATE` pair — each a complete statement.\n *\n * This is the primitive the boot-time RLS applier runs one statement at a time\n * (the runtime's DB handle speaks the extended query protocol, which forbids\n * multiple commands in one execute), while `db push` writes the joined string.\n */\nexport const renderPolicyStatements = (\n table: Pick<TablePlan, \"schema\" | \"table\">,\n policy: PolicyPlan\n): string[] => {\n const target = `\"${table.schema}\".\"${table.table}\"`;\n let create = `CREATE POLICY \"${policy.name}\" ON ${target} AS ${policy.mode.toUpperCase()} ` +\n `FOR ${policy.operation.toUpperCase()} TO ${policy.roles.map(r => `\"${r}\"`).join(\", \")}`;\n if (policy.using) create += ` USING (${policy.using})`;\n if (policy.withCheck) create += ` WITH CHECK (${policy.withCheck})`;\n return [`DROP POLICY IF EXISTS \"${policy.name}\" ON ${target};`, `${create};`];\n};\n\n/** `ALTER TABLE … ENABLE ROW LEVEL SECURITY;` — every generated table is locked. */\nexport const renderEnableRls = (table: Pick<TablePlan, \"schema\" | \"table\">): string =>\n `ALTER TABLE \"${table.schema}\".\"${table.table}\" ENABLE ROW LEVEL SECURITY;`;\n\nconst statementsToDdl = (statements: string[]): string => statements.map(s => `${s}\\n`).join(\"\");\n\n// ── schema.sql ───────────────────────────────────────────────────────────────\n\nexport function renderPostgresDdl(plan: SchemaPlan, options: DdlRenderOptions = {}): string {\n const includePolicies = options.includePolicies !== false;\n const includeSearch = options.includeSearch !== false;\n const includeVector = options.includeVector !== false;\n\n let ddl = \"-- This file is auto-generated by the Rebase DDL generator. Do not edit manually.\\n\\n\";\n\n // 1. Custom schemas.\n //\n // `rebase` is unconditional and load-bearing. The RLS helper functions live\n // in it, so the migration preamble creates it — which puts it in Atlas's\n // replayed state, and anything in that state but absent from this desired\n // schema gets a `DROP SCHEMA … CASCADE` planned against it. That would take\n // the auth tables.\n plan.schemas.forEach(schema => { ddl += `CREATE SCHEMA IF NOT EXISTS \"${schema}\";\\n`; });\n if (plan.schemas.length > 0) ddl += \"\\n\";\n\n // 1b. Search support, for collections that opted in.\n //\n // Extensions and helper functions come before every CREATE TABLE because a\n // generated column's expression is resolved at creation time: a table whose\n // search column calls `rebase_search_text` cannot be created before that\n // function exists. Both are `IF NOT EXISTS` / `OR REPLACE`, so a file\n // replayed against a live database is a no-op here.\n const searchTables = plan.tables.filter(t => t.search);\n if (searchTables.length > 0) {\n if (!includeSearch) {\n ddl += \"-- Full-text search support lives in `search.sql`, applied separately.\\n\\n\";\n } else {\n const extensions = Array.from(new Set(searchTables.flatMap(t => searchExtensionStatements(t.search!))));\n const helpers = Array.from(new Set(searchTables.flatMap(t => searchHelperFunctions(t.search!))));\n ddl += \"-- Full-text search support (collections declaring a `search` block)\\n\";\n extensions.forEach(s => { ddl += `${s}\\n`; });\n if (extensions.length > 0) ddl += \"\\n\";\n helpers.forEach(s => { ddl += `${s}\\n\\n`; });\n }\n }\n\n // 1c. Vector support. Only ever a note, never a statement — unlike 1b.\n // `CREATE EXTENSION vector` is the one thing this file must never carry\n // whichever way the flag goes: Atlas rejects an extension in a desired\n // state as a paid feature.\n if (!includeVector && plan.tables.some(t => t.vectorColumns.length > 0)) {\n ddl += \"-- Vector columns and their ANN indexes live in `vector.sql`, applied separately.\\n\\n\";\n }\n\n // 1d. `autoValue: \"on_update\"`. A trigger is a function plus a binding, so\n // it is out of Atlas's sight for the reason search is — and out\n // unconditionally, because `schema.sql` has no flag that would make Atlas\n // able to read one.\n if (plan.tables.some(t => t.triggers.length > 0)) {\n ddl += \"-- `autoValue: \\\"on_update\\\"` triggers live in `triggers.sql`, applied separately.\\n\\n\";\n }\n\n // 2. Enum types.\n plan.enums.forEach(enumPlan => {\n ddl += `CREATE TYPE \"${enumPlan.schema}\".\"${enumPlan.name}\" AS ENUM (${enumPlan.labels.map(quoteSqlLiteral).join(\", \")});\\n`;\n });\n if (ddl.endsWith(\";\\n\")) ddl += \"\\n\";\n\n // 3. Tables, in the plan's order — each collection, then any junction its\n // relations imply.\n const fkStatements: string[] = [];\n // Indexes follow the tables for the same reason the FK constraints do: the\n // table has to exist first.\n const indexStatements: string[] = [];\n // Policies are emitted after every CREATE TABLE, like the FK constraints: a\n // policy may reference other tables (a junction's derived policies always\n // reference both endpoints; `policy.existsIn` references a join table), and\n // CREATE POLICY validates those relations at creation time.\n const policyStatements: string[] = [];\n\n for (const table of plan.tables) {\n const emitted = table.columns.filter(column => {\n // A vector column is not Atlas's — `vector.sql` adds it. Filtered\n // here rather than upstream so the column keeps its place in the\n // generator's order when it *is* included, which is what the\n // derived-names contract renders.\n if (!includeVector && column.type.kind === \"vector\") return false;\n if (!includeSearch && column.source.kind === \"search\") return false;\n // A declared property already emits this column (an explicit\n // `postId` beside the `belongsTo` that uses it); the relation\n // contributes only the constraint.\n if (column.columnOwnedByProperty) return false;\n return true;\n });\n\n ddl += `CREATE TABLE \"${table.schema}\".\"${table.table}\" (\\n`;\n const lines = emitted.map(column => ` ${renderColumn(column)}`);\n if (table.kind === \"junction\") {\n lines.push(` PRIMARY KEY (${table.primaryKey.map(c => `\"${c}\"`).join(\", \")})`);\n }\n ddl += lines.join(\",\\n\");\n ddl += \"\\n);\\n\\n\";\n\n for (const column of table.columns) {\n if (column.foreignKey) fkStatements.push(`${column.foreignKey.sql};`);\n }\n\n if (includeSearch && table.search) {\n indexStatements.push(...searchIndexStatements(table.search));\n }\n // ANN indexes are emitted with the other indexes rather than inline,\n // because `CREATE INDEX` is a statement and a column definition is not\n // — and because a column too wide for pgvector to index still needs its\n // column. Absent alongside their column when vector is carved out.\n if (includeVector && table.vector) {\n indexStatements.push(...vectorIndexStatements(table.vector));\n for (const skip of table.vector.skipped) {\n indexStatements.push(`-- No ANN index on \"${skip.schema}\".\"${skip.table}\".\"${skip.column}\": ${skip.reason}`);\n }\n }\n // The collection's own `indexes:` block. Last of the three index\n // producers, and the only one the developer wrote deliberately rather\n // than getting as a side effect of another feature.\n indexStatements.push(...collectionIndexStatements(table.indexes));\n\n if (includePolicies) {\n // No FORCE: authenticated requests run as the non-owner\n // `rebase_user` role, which plain ENABLE already binds. The owner\n // (server context) must bypass — it is the trusted plane.\n ddl += `${renderEnableRls(table)}\\n`;\n ddl += \"\\n\";\n for (const policy of table.policies) {\n policyStatements.push(statementsToDdl(renderPolicyStatements(table, policy)));\n }\n }\n }\n\n // Indexes before foreign keys. Today every FK targets a primary key created\n // inline, so the order is safe by accident; the moment a declared\n // `unique: true` index can back an FK target, a constraint emitted first\n // would reference an index that does not exist yet.\n if (indexStatements.length > 0) {\n ddl += \"-- Indexes\\n\";\n ddl += indexStatements.join(\"\\n\") + \"\\n\\n\";\n }\n\n if (fkStatements.length > 0) {\n ddl += \"-- Foreign Key Constraints\\n\";\n ddl += fkStatements.join(\"\\n\") + \"\\n\\n\";\n }\n\n if (policyStatements.length > 0) {\n ddl += \"-- Row Level Security Policies\\n\";\n ddl += policyStatements.join(\"\");\n ddl += \"\\n\";\n }\n\n return ddl;\n}\n\n// ── policies.sql ─────────────────────────────────────────────────────────────\n\nexport function renderPoliciesDdl(plan: SchemaPlan): string {\n let ddl = \"-- This file contains RLS policies generated by Rebase. Applied separately from migrations.\\n\\n\";\n\n // Collections first, then the junctions no collection declares — the order\n // the boot-time applier plans them in.\n for (const table of plan.tables.filter(t => t.kind === \"collection\")) {\n ddl += `${renderEnableRls(table)}\\n`;\n ddl += \"\\n\";\n if (table.policies.length === 0) continue;\n let announced: string | undefined;\n for (const policy of table.policies) {\n // Say which policies the author did not write. They are permissive,\n // so they OR with the declared rules and widen the final ACL beyond\n // what `securityRules` reads like — and re-appear after any manual\n // DROP, because a push asserts the declared state. Once per *rule*:\n // one rule with four operations is one thing the author did not\n // write, not four.\n if (policy.injected && policy.ruleKey !== announced) {\n ddl += \"-- Injected by Rebase (not from this collection's securityRules).\\n\";\n ddl += `-- Set \\`disableDefaultPolicies: true\\` on \"${table.slug}\" to drop these and own its RLS outright.\\n`;\n }\n announced = policy.ruleKey;\n ddl += statementsToDdl(renderPolicyStatements(table, policy));\n }\n ddl += \"\\n\";\n }\n\n for (const table of plan.tables.filter(t => t.kind === \"junction\")) {\n ddl += `${renderEnableRls(table)}\\n`;\n ddl += \"\\n\";\n if (table.policies.length === 0) continue;\n const declaringSlugs = (table.declaringSlugs ?? []).join('\", \"');\n ddl += `-- Derived by Rebase for the junction \"${table.table}\" (no collection declares it).\\n`;\n ddl += \"-- Reads require both endpoint rows to be visible; writes follow the update\\n\";\n ddl += `-- rules of \"${declaringSlugs}\". Set \\`disableDefaultPolicies: true\\` on the\\n`;\n ddl += \"-- declaring collection(s) to drop these and police the junction yourself.\\n\";\n for (const policy of table.policies) {\n ddl += statementsToDdl(renderPolicyStatements(table, policy));\n }\n ddl += \"\\n\";\n }\n\n return ddl;\n}\n\n// ── search.sql ───────────────────────────────────────────────────────────────\n\nexport function renderSearchDdl(plan: SchemaPlan): string {\n const tables = plan.tables.filter(t => t.search);\n if (tables.length === 0) return \"\";\n\n const extensions = Array.from(new Set(tables.flatMap(t => searchExtensionStatements(t.search!))));\n const helpers = Array.from(new Set(tables.flatMap(t => searchHelperFunctions(t.search!))));\n\n let ddl = \"-- This file is auto-generated by the Rebase DDL generator. Do not edit manually.\\n\";\n ddl += \"--\\n\";\n ddl += \"-- Full-text search for the collections declaring a `search` block.\\n\";\n ddl += \"-- Applied by Rebase, not by Atlas — see generatePostgresSearchDdl.\\n\\n\";\n\n extensions.forEach(s => { ddl += `${s}\\n`; });\n if (extensions.length > 0) ddl += \"\\n\";\n helpers.forEach(s => { ddl += `${s}\\n\\n`; });\n\n for (const table of tables) {\n const spec = table.search!;\n const target = `\"${table.schema}\".\"${table.table}\"`;\n // ADD COLUMN IF NOT EXISTS rather than the inline definition the\n // CREATE TABLE would use: by the time this runs the table exists,\n // whether Atlas just created it or it has been live for a year. The\n // guard first, because `ADD COLUMN IF NOT EXISTS` is a no-op against an\n // existing column — without it the file would report success, leave the\n // old expression in place, and then stamp it as current.\n searchStampGuards(spec).forEach(s => { ddl += `${s}\\n`; });\n ddl += `ALTER TABLE ${target} ADD COLUMN IF NOT EXISTS ${searchColumnDefinition(spec)};\\n`;\n const fuzzy = fuzzyColumnDefinition(spec);\n if (fuzzy) ddl += `ALTER TABLE ${target} ADD COLUMN IF NOT EXISTS ${fuzzy};\\n`;\n searchColumnStamps(spec).forEach(s => { ddl += `${s.sql}\\n`; });\n searchIndexStatements(spec).forEach(s => { ddl += `${s}\\n`; });\n ddl += \"\\n\";\n }\n\n return ddl;\n}\n\n/**\n * Glob patterns telling Atlas to leave an object alone.\n *\n * Fully qualified, `schema.table.object`, matching the include list. The\n * two-part form is what Atlas wants when the connection URL scopes it to one\n * schema and is *silently ignored* otherwise: it reads `posts.search_vector` as\n * a table named `search_vector` in a schema named `posts`, matches nothing, and\n * reports no error for the pattern that never fired.\n */\nexport const searchExcludePatternsOf = (plan: SchemaPlan): string[] => {\n const patterns: string[] = [];\n for (const table of plan.tables) {\n if (!table.search) continue;\n for (const column of table.columns) {\n if (column.source.kind !== \"search\") continue;\n patterns.push(`${table.schema}.${table.table}.${column.column}`);\n }\n for (const index of searchIndexNames(table.search)) {\n patterns.push(`${table.schema}.${table.table}.${index}`);\n }\n }\n return patterns;\n};\n\n// ── vector.sql ───────────────────────────────────────────────────────────────\n\nexport function renderVectorDdl(plan: SchemaPlan): string {\n const tables = plan.tables.filter(t => t.vectorColumns.length > 0);\n if (tables.length === 0) return \"\";\n\n let ddl = \"-- This file is auto-generated by the Rebase DDL generator. Do not edit manually.\\n\";\n ddl += \"--\\n\";\n ddl += \"-- Vector columns and their ANN indexes, for the collections declaring a\\n\";\n ddl += \"-- `vector` property. Applied by Rebase, not by Atlas — see\\n\";\n ddl += \"-- generatePostgresVectorDdl.\\n\\n\";\n // The note is not decoration. Someone reading this file after a failed push\n // is looking for the reason the type does not exist, and \"no CREATE\n // EXTENSION here\" is invisible unless it is written down.\n ddl += vectorExtensionDeclared(plan.options.databaseExtensions)\n ? `${vectorExtensionStatement()}\\n\\n`\n : \"-- pgvector is not installed from here: no database declared it. To let Rebase\\n\" +\n `-- install it, add ${VECTOR_EXTENSION_OPT_IN} in config/resources.ts.\\n` +\n \"-- Otherwise install it once by hand; the column below needs the type either way.\\n\\n\";\n\n for (const table of tables) {\n const target = `\"${table.schema}\".\"${table.table}\"`;\n for (const spec of table.vectorColumns) {\n // Guard before the ADD, so a changed `dimensions` is reported\n // rather than skipped over — see `vectorDimensionGuard`.\n ddl += `${vectorDimensionGuard(spec)}\\n`;\n ddl += `ALTER TABLE ${target} ADD COLUMN IF NOT EXISTS ${vectorColumnDefinition(spec)};\\n`;\n }\n if (table.vector) {\n vectorIndexStatements(table.vector).forEach(s => { ddl += `${s}\\n`; });\n for (const skip of table.vector.skipped) {\n ddl += `-- No ANN index on \"${skip.schema}\".\"${skip.table}\".\"${skip.column}\": ${skip.reason}\\n`;\n }\n }\n ddl += \"\\n\";\n }\n\n return ddl;\n}\n\n/** @see searchExcludePatternsOf */\nexport const vectorExcludePatternsOf = (plan: SchemaPlan): string[] => {\n const patterns: string[] = [];\n for (const table of plan.tables) {\n const names = [\n ...table.vectorColumns.map(spec => spec.column),\n ...(table.vector ? vectorIndexNames(table.vector) : [])\n ];\n for (const name of names) patterns.push(`${table.schema}.${table.table}.${name}`);\n }\n return patterns;\n};\n\n// ── triggers.sql ─────────────────────────────────────────────────────────────\n\n/**\n * `autoValue: \"on_update\"`, as the file Rebase applies itself.\n *\n * Empty when nothing declares one — the caller writes no file then, and removes\n * a stale one.\n */\nexport function renderTriggersDdl(plan: SchemaPlan): string {\n const tables = plan.tables.filter(t => t.triggers.length > 0);\n if (tables.length === 0) return \"\";\n\n let ddl = \"-- This file is auto-generated by the Rebase DDL generator. Do not edit manually.\\n\";\n ddl += \"--\\n\";\n ddl += \"-- `updated_at` triggers, for the properties declaring `autoValue: \\\"on_update\\\"`.\\n\";\n ddl += \"-- Applied by Rebase, not by Atlas — a trigger is a function plus a binding,\\n\";\n ddl += \"-- and Atlas's free tier refuses to parse a desired state containing one.\\n\\n\";\n\n for (const statement of plan.functions.filter(isTriggerFunction)) {\n ddl += `${statement}\\n\\n`;\n }\n for (const table of tables) {\n for (const trigger of table.triggers) {\n triggerStatements(trigger).forEach(s => { ddl += `${s}\\n`; });\n }\n ddl += \"\\n\";\n }\n return ddl;\n}\n\nconst isTriggerFunction = (statement: string): boolean => statement.includes(\"RETURNS trigger\");\n\n/** @see searchExcludePatternsOf */\nexport const triggerExcludePatternsOf = (plan: SchemaPlan): string[] =>\n plan.tables.flatMap(table => table.triggers.map(t => `${table.schema}.${table.table}.${t.name}`));\n","/**\n * The SQL half of `rebase db generate`, as a plan and a renderer.\n *\n * There is no interpretation left in this file. `schema/plan/plan-schema.ts`\n * reads the collections into a {@link SchemaPlan} and `schema/plan/render-ddl.ts`\n * writes the files; what remains here is the public entry points those two are\n * reached through, plus the smaller plans (`planRelationalColumns`,\n * `planJunctionTables`, `planCollectionPolicies`) that other modules ask for as\n * slices of the same plan.\n *\n * It used to hold `getSqlColumnType` and a `CREATE TABLE` walk with its own\n * reading of relations, junctions, enums and search — one of three such\n * readings. See `schema/plan/types.ts` for what that cost.\n */\nimport { CollectionConfig, SecurityRule, isPostgresCollectionConfig } from \"@rebasepro/types\";\nimport { getTableName } from \"@rebasepro/common\";\nimport { planSchema, compileSecurityRule, quoteSqlLiteral, defaultBelongsToOnDelete } from \"./plan/plan-schema\";\nimport {\n renderPgType,\n renderPoliciesDdl,\n renderPolicyStatements,\n renderPostgresDdl,\n renderSearchDdl,\n renderTriggersDdl,\n renderVectorDdl,\n searchExcludePatternsOf,\n triggerExcludePatternsOf,\n vectorExcludePatternsOf,\n type DdlRenderOptions\n} from \"./plan/render-ddl\";\nimport type { ForeignKeyPlan, SchemaPlan } from \"./plan/types\";\nimport type { GeneratedColumnDependency } from \"./generated-column-conflicts\";\n\nexport { quoteSqlLiteral, defaultBelongsToOnDelete };\nexport type { ForeignKeyPlan };\n\ntype ResolveCollection = (slug: string) => CollectionConfig | undefined;\n\n/**\n * The individual SQL statements a single security rule compiles to: a\n * `DROP POLICY IF EXISTS` / `CREATE POLICY` pair per operation, each a complete\n * statement (terminated by `;`, no trailing newline).\n *\n * This is the primitive the boot-time RLS applier runs one statement at a time\n * (the runtime's DB handle speaks the extended query protocol, which forbids\n * multiple commands in one execute), while `db push` writes the joined string.\n */\nexport const generatePolicyStatements = (\n collection: CollectionConfig,\n rule: SecurityRule,\n resolveCollection: ResolveCollection\n): string[] => {\n const table = {\n schema: isPostgresCollectionConfig(collection) && collection.schema ? collection.schema : \"public\",\n table: getTableName(collection)\n };\n return compileSecurityRule(collection, rule, resolveCollection, false)\n .flatMap(policy => renderPolicyStatements(table, policy));\n};\n\n/**\n * `drizzle/schema.sql` — the desired state Atlas diffs the database against.\n *\n * Synchronous. It was `async` with nothing to await, which made every caller\n * (the writer script, the doctor, the live editor, two gates) carry a promise\n * for no reason and made the one place that forgot to await it silently compare\n * `[object Promise]`.\n */\nexport const generatePostgresDdl = (\n allCollections: CollectionConfig[],\n options: DdlRenderOptions = {}\n): string => renderPostgresDdl(planSchema(allCollections), options);\n\n/** `drizzle/policies.sql` — the RLS policies. Atlas has no model for a policy. */\nexport const generatePostgresPoliciesDdl = (allCollections: CollectionConfig[]): string =>\n renderPoliciesDdl(planSchema(allCollections));\n\n/**\n * `drizzle/search.sql` — everything a `search` block needs, as a file Rebase\n * applies itself.\n *\n * Search is one of the parts of the schema Atlas does not own, for two\n * independent reasons and either alone would be enough:\n *\n * 1. Its free tier refuses to *parse* a desired-state file that so much as\n * contains a function — \"functions and procedures are available to\n * logged-in users only\". A generated `tsvector` column cannot avoid one:\n * `unaccent` is STABLE and jsonb flattening needs a set-returning function,\n * so both have to be wrapped in an IMMUTABLE helper to be legal in a\n * generated column at all.\n * 2. Even with the file accepted, Atlas *wipes* the dev database it diffs\n * against, so a helper seeded there beforehand is gone by the time the plan\n * is analysed. There is no hook to reinstate it.\n *\n * Empty when nothing opted in — the caller writes no file then.\n */\nexport const generatePostgresSearchDdl = (allCollections: CollectionConfig[]): string =>\n renderSearchDdl(planSchema(allCollections));\n\n/**\n * Glob patterns telling Atlas to leave the search column and its index alone.\n *\n * Without these, a desired state that omits search reads to Atlas as an\n * instruction to drop the column — taking the index and the whole search\n * feature with it on the next push.\n */\nexport const searchExcludePatterns = (allCollections: CollectionConfig[]): string[] =>\n searchExcludePatternsOf(planSchema(allCollections));\n\n/**\n * `drizzle/vector.sql` — the `vector` columns, their ANN indexes and, when a\n * database declared it, the extension.\n *\n * Vector is out of Atlas's hands for a narrower reason than search: not that\n * Atlas refuses to parse the file, but that it cannot *execute* it. To compute\n * a desired state Atlas materialises `schema.sql` in a dev database, that\n * database is created empty, and Atlas empties it again at the start of every\n * run — so `VECTOR(384)` resolves against a database that structurally cannot\n * have pgvector, and every push dies with `type \"vector\" does not exist`.\n */\nexport const generatePostgresVectorDdl = (\n allCollections: CollectionConfig[],\n options: { extensions?: readonly string[] } = {}\n): string => renderVectorDdl(planSchema(allCollections, { databaseExtensions: options.extensions }));\n\n/** @see searchExcludePatterns */\nexport const vectorExcludePatterns = (allCollections: CollectionConfig[]): string[] =>\n vectorExcludePatternsOf(planSchema(allCollections));\n\n/**\n * `drizzle/triggers.sql` — the `BEFORE UPDATE` triggers behind\n * `autoValue: \"on_update\"`, and the one function they share.\n *\n * A trigger is a function plus a binding, so it is carved out of Atlas's view\n * for the first of the two reasons search is. Empty when no property declares\n * `on_update` — the caller writes no file then, and removes a stale one.\n */\nexport const generatePostgresTriggersDdl = (allCollections: CollectionConfig[]): string =>\n renderTriggersDdl(planSchema(allCollections));\n\n/** @see searchExcludePatterns */\nexport const triggerExcludePatterns = (allCollections: CollectionConfig[]): string[] =>\n triggerExcludePatternsOf(planSchema(allCollections));\n\n// ── Slices of the plan other modules ask for ─────────────────────────────────\n\n/** A column a `relation` or `reference` property owns on its own table. */\nexport interface RelationalColumnPlan {\n schema: string;\n /** Bare table name, no schema prefix. */\n table: string;\n column: string;\n /** Postgres type, exactly as the DDL renderer declares it. */\n type: string;\n /** `validation.required` on the property that owns the link. */\n required: boolean;\n /**\n * True when a declared property emits this column and the relation\n * contributes only the constraint — `postId` with `columnName: \"post_id\"`\n * beside a `belongsTo` on `post_id`.\n */\n columnOwnedByProperty?: boolean;\n /** Absent when the target collection is not part of this bundle. */\n foreignKey?: ForeignKeyPlan;\n /**\n * What this column would have been called before `generateForeignKeyName`\n * learned to singularize. Carried so the boot-time ensure can notice a\n * database provisioned under the old rule; never used to name anything.\n */\n legacyColumn?: string;\n}\n\n/** The table behind a many-to-many `through` relation. */\nexport interface JunctionTablePlan {\n schema: string;\n /** Bare table name, no schema prefix. */\n table: string;\n columns: { name: string; type: string; legacyName?: string }[];\n /** Both endpoint columns plus the composite primary key. */\n createTable: string;\n foreignKeys: ForeignKeyPlan[];\n}\n\nconst planOf = (collections: CollectionConfig[]): SchemaPlan => planSchema(collections);\n\n/**\n * The FK columns the declared collections own — one entry per `relation`\n * (`belongsTo` side) or `reference` property.\n *\n * A view over the schema plan. It was a second walk of the collections beside\n * the `CREATE TABLE` one, which is how boot-ensure came to add every relation\n * column bare while `db push` made the same column `NOT NULL`.\n */\nexport const planRelationalColumns = (allCollections: CollectionConfig[]): RelationalColumnPlan[] => {\n const plan = planOf(allCollections);\n const out: RelationalColumnPlan[] = [];\n for (const table of plan.tables) {\n if (table.kind !== \"collection\") continue;\n for (const column of table.columns) {\n if (column.source.kind !== \"relation\" && column.source.kind !== \"reference\") continue;\n out.push({\n schema: table.schema,\n table: table.table,\n column: column.column,\n type: renderPgType(column.type),\n required: !column.nullable,\n columnOwnedByProperty: column.columnOwnedByProperty,\n legacyColumn: column.legacyColumn,\n foreignKey: column.foreignKey\n });\n }\n }\n return out;\n};\n\n/**\n * The junction tables a bundle's many-to-many relations imply.\n *\n * Derived from the same plan the derived RLS comes from, so a table created\n * here always has policies planned for it — a junction with row-level security\n * left off is readable and writable by every signed-in user.\n */\nexport const planJunctionTables = (allCollections: CollectionConfig[]): JunctionTablePlan[] =>\n planOf(allCollections).tables\n .filter(table => table.kind === \"junction\")\n .map(table => ({\n schema: table.schema,\n table: table.table,\n columns: table.columns.map(c => ({\n name: c.column,\n type: renderPgType(c.type),\n legacyName: c.legacyColumn\n })),\n createTable:\n `CREATE TABLE IF NOT EXISTS \"${table.schema}\".\"${table.table}\" (` +\n table.columns.map(c => `\"${c.column}\" ${renderPgType(c.type)} NOT NULL`).join(\", \") +\n `, PRIMARY KEY (${table.primaryKey.map(c => `\"${c}\"`).join(\", \")}));`,\n foreignKeys: table.columns.map(c => c.foreignKey).filter((fk): fk is ForeignKeyPlan => fk !== undefined)\n }));\n\nexport interface CollectionPolicyPlan {\n /** The table's schema (e.g. `public`, `rebase`). */\n schema: string;\n /** The bare table name, no schema prefix. */\n table: string;\n /** `schema.table` — matches the keys `readExistingSchema` returns. */\n qualified: string;\n /** `ALTER TABLE … ENABLE ROW LEVEL SECURITY;` — locked by default. */\n enableRls: string;\n /** `DROP POLICY IF EXISTS` / `CREATE POLICY` statements, in order. */\n policyStatements: string[];\n}\n\n/**\n * The per-table RLS plan for the declared collections and their junctions, as\n * executable statements — what the managed runtime applies at boot so a freshly\n * provisioned tenant database serves data instead of 401ing every read.\n *\n * The same policies `policies.sql` carries, from the same plan, so boot and\n * `db push` produce identical RLS from identical collections.\n */\nexport const planCollectionPolicies = (allCollections: CollectionConfig[]): CollectionPolicyPlan[] => {\n const plan = planOf(allCollections);\n // Collections first, then the junctions no collection declares.\n const ordered = [\n ...plan.tables.filter(t => t.kind === \"collection\"),\n ...plan.tables.filter(t => t.kind === \"junction\")\n ];\n return ordered.map(table => ({\n schema: table.schema,\n table: table.table,\n qualified: table.qualified,\n enableRls: `ALTER TABLE \"${table.schema}\".\"${table.table}\" ENABLE ROW LEVEL SECURITY;`,\n policyStatements: table.policies.flatMap(policy => renderPolicyStatements(table, policy))\n }));\n};\n\n/**\n * The `generated column → column it reads` edges a project's `search` blocks\n * imply, derived from the collections alone.\n *\n * The same edges `queryGeneratedColumnDependencies` reads out of `pg_depend`,\n * but available where no database is: `rebase db generate` writes a migration\n * that will run somewhere else, later, against a database it cannot inspect.\n * A spec's `fields[].column` is the physical column each search field starts\n * at, which is exactly what the generated expression depends on.\n */\nexport const searchColumnDependencies = (\n allCollections: CollectionConfig[]\n): GeneratedColumnDependency[] => {\n const dependencies: GeneratedColumnDependency[] = [];\n for (const table of planOf(allCollections).tables) {\n const spec = table.search;\n if (!spec) continue;\n const generated = [spec.column, ...(spec.fuzzy ? [spec.fuzzy.column] : [])];\n const sources = new Set(spec.fields.map(field => field.column));\n for (const column of generated) {\n for (const dependsOn of sources) {\n dependencies.push({ schema: table.schema, table: table.table, column, dependsOn });\n }\n }\n }\n return dependencies;\n};\n"],"mappings":"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"}
|
package/dist/{generated-schema-staleness-DQdfJl2A.js → generated-schema-staleness-DRQe2BpC.js}
RENAMED
|
@@ -1,8 +1,16 @@
|
|
|
1
1
|
import { createRequire as __createRequire } from "module";
|
|
2
2
|
__createRequire(import.meta.url);
|
|
3
|
+
import { r as __exportAll } from "./__vite-browser-external-BnuHet1e.js";
|
|
3
4
|
import { getTableName, relationalCollections, resolveCollectionRelations } from "@rebasepro/common";
|
|
4
5
|
import { generateForeignKeyName, legacyForeignKeyName } from "@rebasepro/utils";
|
|
5
6
|
//#region src/schema/generated-schema-staleness.ts
|
|
7
|
+
var generated_schema_staleness_exports = /* @__PURE__ */ __exportAll({
|
|
8
|
+
compareGeneratedDeclarations: () => compareGeneratedDeclarations,
|
|
9
|
+
describeDeclarationDifferences: () => describeDeclarationDifferences,
|
|
10
|
+
describeLegacyForeignKeyNames: () => describeLegacyForeignKeyNames,
|
|
11
|
+
findLegacyForeignKeyNames: () => findLegacyForeignKeyNames,
|
|
12
|
+
staleVerdict: () => staleVerdict
|
|
13
|
+
});
|
|
6
14
|
/**
|
|
7
15
|
* The slice of the generated source declaring one table.
|
|
8
16
|
*
|
|
@@ -144,14 +152,84 @@ function staleVerdict(input) {
|
|
|
144
152
|
return {
|
|
145
153
|
lines: [
|
|
146
154
|
...found,
|
|
147
|
-
" The database column has already been renamed at boot, so
|
|
155
|
+
" The database column has already been renamed at boot. The server reads the database, so it will still start — but `db push`, Atlas, `eject` and any code importing this file are all reading a description that no longer matches.",
|
|
148
156
|
" Run `rebase schema generate` to regenerate it."
|
|
149
157
|
],
|
|
150
158
|
exitCode: 1,
|
|
151
159
|
regenerate: false
|
|
152
160
|
};
|
|
153
161
|
}
|
|
162
|
+
/**
|
|
163
|
+
* `export const NAME = …` → the declaration, with formatting and the
|
|
164
|
+
* generator's own comments removed.
|
|
165
|
+
*
|
|
166
|
+
* Every generated declaration starts at column 0 and runs to the next one, so
|
|
167
|
+
* splitting on them needs no brace counting and cannot get out of step with a
|
|
168
|
+
* policy clause that happens to contain a brace.
|
|
169
|
+
*/
|
|
170
|
+
function declarations(source) {
|
|
171
|
+
const out = /* @__PURE__ */ new Map();
|
|
172
|
+
const starts = [...source.matchAll(/^export const (\w+) =/gm)];
|
|
173
|
+
starts.forEach((match, index) => {
|
|
174
|
+
const from = match.index + match[0].length;
|
|
175
|
+
const to = index + 1 < starts.length ? starts[index + 1].index : source.length;
|
|
176
|
+
out.set(match[1], source.slice(from, to).replace(/\/\/[^\n]*/g, "").replace(/\s+/g, " ").trim());
|
|
177
|
+
});
|
|
178
|
+
return out;
|
|
179
|
+
}
|
|
180
|
+
/**
|
|
181
|
+
* What differs between the schema the current collections generate and the one
|
|
182
|
+
* on disk — by declaration, not by byte.
|
|
183
|
+
*
|
|
184
|
+
* `rebase doctor` used to whitespace-normalise both files and compare the
|
|
185
|
+
* strings, which answers "are these the same text" when the question is "does
|
|
186
|
+
* this file still describe the same schema". The difference is not academic:
|
|
187
|
+
* the answer was a single yes/no with nothing to act on, so a developer told
|
|
188
|
+
* their schema was stale had to regenerate and read the diff to find out what
|
|
189
|
+
* had changed.
|
|
190
|
+
*
|
|
191
|
+
* Comparing declarations answers the question the reader actually has. The
|
|
192
|
+
* generated file is a flat list of `export const`s — one per table, enum and
|
|
193
|
+
* relations block, each the rendering of a piece of the schema plan — and two
|
|
194
|
+
* files declaring the same symbols with the same bodies describe the same
|
|
195
|
+
* database whatever their formatting.
|
|
196
|
+
*
|
|
197
|
+
* Not a plan-to-plan comparison, and deliberately not: reading a plan back out
|
|
198
|
+
* of `schema.generated.ts` means interpreting drizzle-orm builder calls, which
|
|
199
|
+
* is a second interpreter — the exact thing this area was just rid of. The
|
|
200
|
+
* rendered declarations are the closest honest proxy, and the renderer is
|
|
201
|
+
* deterministic, so the only things normalised away are what it is free to vary
|
|
202
|
+
* independently of the plan: whitespace and its own comments.
|
|
203
|
+
*/
|
|
204
|
+
function compareGeneratedDeclarations(expected, actual) {
|
|
205
|
+
const want = declarations(expected);
|
|
206
|
+
const have = declarations(actual);
|
|
207
|
+
const differences = [];
|
|
208
|
+
for (const [name, body] of want) if (!have.has(name)) differences.push({
|
|
209
|
+
name,
|
|
210
|
+
kind: "missing"
|
|
211
|
+
});
|
|
212
|
+
else if (have.get(name) !== body) differences.push({
|
|
213
|
+
name,
|
|
214
|
+
kind: "changed"
|
|
215
|
+
});
|
|
216
|
+
for (const name of have.keys()) if (!want.has(name)) differences.push({
|
|
217
|
+
name,
|
|
218
|
+
kind: "unexpected"
|
|
219
|
+
});
|
|
220
|
+
return differences;
|
|
221
|
+
}
|
|
222
|
+
/** The differences, as the line a developer reads in `rebase doctor`. */
|
|
223
|
+
function describeDeclarationDifferences(differences) {
|
|
224
|
+
const shown = differences.slice(0, 5).map((difference) => {
|
|
225
|
+
if (difference.kind === "missing") return `${difference.name} (not in the file)`;
|
|
226
|
+
if (difference.kind === "unexpected") return `${difference.name} (in the file, no longer generated)`;
|
|
227
|
+
return `${difference.name} (differs)`;
|
|
228
|
+
});
|
|
229
|
+
const rest = differences.length - shown.length;
|
|
230
|
+
return shown.join(", ") + (rest > 0 ? `, and ${rest} more` : "");
|
|
231
|
+
}
|
|
154
232
|
//#endregion
|
|
155
|
-
export {
|
|
233
|
+
export { describeDeclarationDifferences as n, generated_schema_staleness_exports as r, compareGeneratedDeclarations as t };
|
|
156
234
|
|
|
157
|
-
//# sourceMappingURL=generated-schema-staleness-
|
|
235
|
+
//# sourceMappingURL=generated-schema-staleness-DRQe2BpC.js.map
|
|
@@ -1 +1 @@
|
|
|
1
|
-
{"version":3,"file":"generated-schema-staleness-DQdfJl2A.js","names":[],"sources":["../src/schema/generated-schema-staleness.ts"],"sourcesContent":["import { CollectionConfig } from \"@rebasepro/types\";\nimport { getTableName, resolveCollectionRelations, relationalCollections } from \"@rebasepro/common\";\nimport { generateForeignKeyName, legacyForeignKeyName } from \"@rebasepro/utils\";\n\n/**\n * Notice a generated Drizzle schema that a *library upgrade* invalidated.\n *\n * `rebase dev` already watches `config/collections` and warns when a collection\n * file changes. That covers drift the developer caused. It cannot cover this one,\n * because nothing the developer owns changed: 0.13 derives `category_id` where\n * 0.12 derived `categorie_id`, from the same unedited collection. The watcher\n * never fires, and `backend/src/schema.generated.ts` quietly stops describing the\n * schema the runtime expects.\n *\n * The consequence is not cosmetic. Boot-ensure renames the column in the\n * database, then relation validation reads the stale module and refuses to\n * start — on that boot and every boot after it, because the rename is already\n * applied and will not be attempted again.\n *\n * Deliberately narrow: this answers \"does the generated schema name a foreign key\n * the way the previous rule did\", not \"is this file what we would generate now\".\n * The wide question would report every whitespace change in the generator as a\n * fatal staleness, and a check that cries wolf gets switched off.\n */\n\n/** One column the generated schema names under the pre-0.13 rule. */\nexport interface LegacyForeignKeyName {\n /** Table whose column declaration is stale. */\n table: string;\n /** The name the generated schema declares. */\n legacy: string;\n /** The name this release derives, and which the database now carries. */\n current: string;\n /** `<collection>.<relation>` that derives it, for the message. */\n relation: string;\n}\n\n/**\n * The slice of the generated source declaring one table.\n *\n * Scoping matters: two junctions in one file can carry columns of the same name,\n * and a whole-file match would attribute a stale column to whichever table the\n * reader looks at first. Returns \"\" when the table is not in the file at all,\n * which is not staleness — it is a table the generator has not been asked about.\n */\nfunction tableBlock(source: string, table: string): string {\n // Both forms the generator emits. A collection with `schema: \"rebase\"` — the\n // auth users collection, in every scaffold — is written as\n // `rebaseSchema.table(\"users\", …)`, so matching only `pgTable(` made this\n // blind to every table outside `public`: the block came back empty, and an\n // empty block reads as \"a table the generator has not been asked about\".\n // The rename check therefore never inspected them.\n const literal = table.replace(/[.*+?^${}()|[\\]\\\\]/g, \"\\\\$&\");\n const start = source.search(new RegExp(`(?:pgTable|\\\\.table)\\\\(\\\\s*[\"']${literal}[\"']`));\n if (start === -1) return \"\";\n // Generated files put every table in its own `export const`, so the next one\n // is the end of this block. No brace counting, nothing to get wrong.\n const next = source.indexOf(\"\\nexport \", start);\n return next === -1 ? source.slice(start) : source.slice(start, next);\n}\n\n/**\n * Whether a block *declares* the column, rather than merely mentioning it.\n *\n * A comment explaining the rename, or a policy expression naming the old column,\n * must not read as a declaration — otherwise regenerating the file would not\n * clear the finding and the check would be permanently red.\n */\nfunction declaresColumn(block: string, column: string): boolean {\n // `categorieId: integer(\"categorie_id\")` — the Drizzle column shape. Only\n // the string argument is the column; the key beside it is the *wire* name\n // and no longer agrees with it (`authorId: integer(\"author_id\")`). Matching\n // both, as this did, made every camelCased key look like a column the\n // generated file did not declare.\n const literal = column.replace(/[.*+?^${}()|[\\]\\\\]/g, \"\\\\$&\");\n return new RegExp(`(^|[\\\\s,{])[\\\\w$\"']+\\\\s*:\\\\s*\\\\w+\\\\(\\\\s*[\"']${literal}[\"']`, \"m\").test(block);\n}\n\n/**\n * @param generatedSource contents of `backend/src/schema.generated.ts`\n * @param collections the project's collections, as this release reads them\n */\nexport function findLegacyForeignKeyNames(\n generatedSource: string,\n collections: CollectionConfig[]\n): LegacyForeignKeyName[] {\n const found: LegacyForeignKeyName[] = [];\n const seen = new Set<string>();\n\n /**\n * Report `wanted` as stale when the generated schema declares what the\n * previous rule would have derived from `source` instead.\n *\n * The `wanted !== current` guard is what honours an explicitly named column.\n * An author who pinned `categorie_id` has said so on the relation, so the\n * generated file agreeing with them is correct, not stale — and the rename\n * note documents exactly that opt-out.\n */\n const consider = (\n table: string,\n wanted: string,\n sourceName: string | undefined,\n relation: string\n ): void => {\n if (!table || !wanted || !sourceName) return;\n\n const current = generateForeignKeyName(sourceName);\n const legacy = legacyForeignKeyName(sourceName);\n if (legacy === current) return; // this name never moved\n if (wanted !== current) return; // pinned by the author, or unrelated\n\n const block = tableBlock(generatedSource, table);\n if (!block) return;\n if (!declaresColumn(block, legacy)) return;\n if (declaresColumn(block, current)) return; // already regenerated\n\n const key = `${table}.${legacy}`;\n if (seen.has(key)) return;\n seen.add(key);\n found.push({ table, legacy, current, relation });\n };\n\n // Same rule as the generator whose output this reads: a collection that\n // gets no table cannot have named a column in it. Inert in practice today —\n // the finding also requires the generated file to declare the table — but\n // the guard is what keeps that true when a non-SQL collection's slug happens\n // to match a SQL one's table.\n for (const collection of relationalCollections(collections)) {\n const sourceTable = getTableName(collection);\n\n for (const [name, relation] of Object.entries(resolveCollectionRelations(collection))) {\n const at = `${collection.slug}.${name}`;\n\n let target: CollectionConfig | undefined;\n try {\n target = relation.target?.();\n } catch {\n // A throwing target thunk is its own defect, reported at boot by\n // `validate-relations`. Nothing to say about its column names.\n continue;\n }\n\n switch (relation.kind) {\n case \"belongsTo\":\n consider(sourceTable, relation.localKey, relation.relationName, at);\n break;\n\n case \"hasOne\":\n case \"hasMany\":\n if (target) {\n consider(getTableName(target), relation.foreignKeyOnTarget, collection.slug, at);\n }\n break;\n\n case \"manyToMany\":\n consider(relation.through.table, relation.through.sourceColumn, collection.slug, at);\n if (target) {\n consider(relation.through.table, relation.through.targetColumn, target.slug, at);\n }\n break;\n\n default:\n // `via` joins are written by hand — there is no derived name\n // for the rule change to have moved.\n break;\n }\n }\n }\n\n return found;\n}\n\n/** One-line summary for a log or a CLI notice. */\nexport function describeLegacyForeignKeyNames(found: LegacyForeignKeyName[]): string {\n return found\n .map(f => ` • ${f.table}.${f.legacy} → ${f.current} (${f.relation})`)\n .join(\"\\n\");\n}\n\n// ── The other way a generated schema goes stale ──────────────────────────────\n//\n// The rename above is the subtle one: nothing the developer owns changed. The\n// ordinary one is the opposite — a collection or a relation was added and the\n// generator was not re-run, so the checked-in module simply does not describe\n// part of the schema. It fails later and elsewhere: relation validation, or a\n// query against a table the module has never heard of.\n//\n// Still not \"is this byte-for-byte what the generator would emit now\". This asks\n// only about *names*, which is what the rest of the system reads out of the\n// file, so reformatting the generator cannot make it red.\n\n/** Something the collections derive that the generated schema does not declare. */\nexport interface MissingGeneratedName {\n /** The table it belongs to, or is. */\n table: string;\n /** The column, when a column is what is missing. */\n column?: string;\n /** `<collection>` or `<collection>.<relation>`, for the message. */\n source: string;\n}\n\n/**\n * Tables and derived foreign-key columns the collections name and the generated\n * schema does not.\n *\n * @param generatedSource contents of `backend/src/schema.generated.ts`\n * @param collections the project's collections, as this release reads them\n */\nexport function findMissingGeneratedNames(\n generatedSource: string,\n collections: CollectionConfig[]\n): MissingGeneratedName[] {\n const missing: MissingGeneratedName[] = [];\n const seen = new Set<string>();\n\n const report = (table: string, source: string, column?: string): void => {\n const key = `${table}.${column ?? \"\"}`;\n if (seen.has(key)) return;\n seen.add(key);\n missing.push({ table, source, ...(column ? { column } : {}) });\n };\n\n const requireColumn = (table: string, column: string | undefined, source: string): void => {\n if (!table || !column) return;\n const block = tableBlock(generatedSource, table);\n // A table that is absent is reported once, as a table. Listing each of\n // its columns as separately missing would bury the one fact that\n // explains all of them.\n if (!block) return report(table, source);\n if (!declaresColumn(block, column)) report(table, source, column);\n };\n\n for (const collection of relationalCollections(collections)) {\n const sourceTable = getTableName(collection);\n if (!tableBlock(generatedSource, sourceTable)) {\n report(sourceTable, collection.slug);\n continue;\n }\n\n for (const [name, relation] of Object.entries(resolveCollectionRelations(collection))) {\n const at = `${collection.slug}.${name}`;\n\n let target: CollectionConfig | undefined;\n try {\n target = relation.target?.();\n } catch {\n // Its own defect, reported at boot by `validate-relations`.\n continue;\n }\n\n switch (relation.kind) {\n case \"belongsTo\":\n requireColumn(sourceTable, relation.localKey, at);\n break;\n\n case \"hasOne\":\n case \"hasMany\":\n if (target) requireColumn(getTableName(target), relation.foreignKeyOnTarget, at);\n break;\n\n case \"manyToMany\":\n requireColumn(relation.through.table, relation.through.sourceColumn, at);\n requireColumn(relation.through.table, relation.through.targetColumn, at);\n break;\n\n default:\n // `via` joins are written by hand; there is no derived name\n // to be missing.\n break;\n }\n }\n }\n\n return missing;\n}\n\n/** One-line summary for a log or a CLI notice. */\nexport function describeMissingGeneratedNames(missing: MissingGeneratedName[]): string {\n return missing\n .map(m => (m.column\n ? ` • ${m.table}.${m.column} is not declared (${m.source})`\n : ` • table ${m.table} is not declared (${m.source})`))\n .join(\"\\n\");\n}\n\n/** What `rebase schema stale` found, and what that costs. */\nexport interface StaleVerdict {\n /** The lines to print, in order. Empty only under `--fix` with nothing to fix. */\n lines: string[];\n /** Non-zero only when the reader must act before the server will boot. */\n exitCode: 0 | 1;\n /** True when `--fix` should regenerate. */\n regenerate: boolean;\n}\n\n/**\n * The whole of `schema stale`'s decision, separated from doing it.\n *\n * Separated because of what the command used to do on the clean path: **exit 0\n * having written zero bytes to stdout and zero to stderr.** For a command the\n * top-level help describes as \"Report generated schema files the collections\n * have moved past\", that is indistinguishable from a no-op, a crash, and a\n * subcommand the driver does not implement. Three of its four paths were\n * silent, and none of them could be tested where they stood: `cli.ts` uses\n * `import.meta`, which the driver's jest suite cannot load at all.\n *\n * `--fix` is the exception, and stays silent when there is nothing to fix.\n * `rebase dev` runs `schema stale --fix` before every boot with inherited\n * stdio, and \"nothing was wrong\" is not news a hundred lines into a start-up\n * transcript. The flag is the automated caller's; its silence is the point.\n */\nexport function staleVerdict(input: {\n /** `--output`, as the reader typed it. */\n outputPath: string;\n /** Whether the generated schema exists at all. */\n generatedExists: boolean;\n /** The legacy foreign-key names found in it, if it was read. */\n stale: LegacyForeignKeyName[];\n /** Why the comparison could not be made, when it could not. */\n unreadable?: string;\n /** Whether `--fix` was given. */\n fix: boolean;\n}): StaleVerdict {\n const quiet = (lines: string[]): string[] => (input.fix ? [] : lines);\n\n if (!input.generatedExists) {\n // Not staleness: a fresh project has not run the generator, and saying\n // \"stale\" about a file that does not exist sends the reader looking for\n // something to fix.\n return {\n lines: quiet([` No generated schema yet at ${input.outputPath} — nothing to compare.`,\n \" `rebase schema generate` creates it.\"]),\n exitCode: 0,\n regenerate: false\n };\n }\n\n if (input.unreadable !== undefined) {\n // Best-effort by design: a collections directory that will not load is a\n // real error, but boot reports it far better than this does. Saying so\n // is still better than exiting 0 in silence about a check that did not\n // run — this file's whole subject.\n return {\n lines: quiet([` ⏭ Not checked: ${input.unreadable}`]),\n exitCode: 0,\n regenerate: false\n };\n }\n\n if (input.stale.length === 0) {\n return {\n lines: quiet([` ✓ Nothing stale — 1 generated file matches (${input.outputPath}).`]),\n exitCode: 0,\n regenerate: false\n };\n }\n\n const found = [\n \"\",\n ` ⚠️ ${input.outputPath} names ${input.stale.length} foreign key(s) the way an earlier release did:`,\n describeLegacyForeignKeyNames(input.stale),\n \"\"\n ];\n\n if (input.fix) {\n return { lines: [...found, \" Regenerating the Drizzle schema so it matches...\"], exitCode: 0, regenerate: true };\n }\n\n return {\n lines: [\n ...found,\n \" The database column has already been renamed at boot, so the generated schema no \"\n + \"longer matches it and the server will refuse to start.\",\n \" Run `rebase schema generate` to regenerate it.\"\n ],\n exitCode: 1,\n regenerate: false\n };\n}\n"],"mappings":"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"}
|
|
1
|
+
{"version":3,"file":"generated-schema-staleness-DRQe2BpC.js","names":[],"sources":["../src/schema/generated-schema-staleness.ts"],"sourcesContent":["import { CollectionConfig } from \"@rebasepro/types\";\nimport { getTableName, resolveCollectionRelations, relationalCollections } from \"@rebasepro/common\";\nimport { generateForeignKeyName, legacyForeignKeyName } from \"@rebasepro/utils\";\n\n/**\n * Notice a generated Drizzle schema that a *library upgrade* invalidated.\n *\n * `rebase dev` already watches `config/collections` and warns when a collection\n * file changes. That covers drift the developer caused. It cannot cover this one,\n * because nothing the developer owns changed: 0.13 derives `category_id` where\n * 0.12 derived `categorie_id`, from the same unedited collection. The watcher\n * never fires, and `backend/src/schema.generated.ts` quietly stops describing the\n * schema the runtime expects.\n *\n * The consequence is not cosmetic. Boot-ensure renames the column in the\n * database, then relation validation reads the stale module and refuses to\n * start — on that boot and every boot after it, because the rename is already\n * applied and will not be attempted again.\n *\n * Deliberately narrow: this answers \"does the generated schema name a foreign key\n * the way the previous rule did\", not \"is this file what we would generate now\".\n * The wide question would report every whitespace change in the generator as a\n * fatal staleness, and a check that cries wolf gets switched off.\n */\n\n/** One column the generated schema names under the pre-0.13 rule. */\nexport interface LegacyForeignKeyName {\n /** Table whose column declaration is stale. */\n table: string;\n /** The name the generated schema declares. */\n legacy: string;\n /** The name this release derives, and which the database now carries. */\n current: string;\n /** `<collection>.<relation>` that derives it, for the message. */\n relation: string;\n}\n\n/**\n * The slice of the generated source declaring one table.\n *\n * Scoping matters: two junctions in one file can carry columns of the same name,\n * and a whole-file match would attribute a stale column to whichever table the\n * reader looks at first. Returns \"\" when the table is not in the file at all,\n * which is not staleness — it is a table the generator has not been asked about.\n */\nfunction tableBlock(source: string, table: string): string {\n // Both forms the generator emits. A collection with `schema: \"rebase\"` — the\n // auth users collection, in every scaffold — is written as\n // `rebaseSchema.table(\"users\", …)`, so matching only `pgTable(` made this\n // blind to every table outside `public`: the block came back empty, and an\n // empty block reads as \"a table the generator has not been asked about\".\n // The rename check therefore never inspected them.\n const literal = table.replace(/[.*+?^${}()|[\\]\\\\]/g, \"\\\\$&\");\n const start = source.search(new RegExp(`(?:pgTable|\\\\.table)\\\\(\\\\s*[\"']${literal}[\"']`));\n if (start === -1) return \"\";\n // Generated files put every table in its own `export const`, so the next one\n // is the end of this block. No brace counting, nothing to get wrong.\n const next = source.indexOf(\"\\nexport \", start);\n return next === -1 ? source.slice(start) : source.slice(start, next);\n}\n\n/**\n * Whether a block *declares* the column, rather than merely mentioning it.\n *\n * A comment explaining the rename, or a policy expression naming the old column,\n * must not read as a declaration — otherwise regenerating the file would not\n * clear the finding and the check would be permanently red.\n */\nfunction declaresColumn(block: string, column: string): boolean {\n // `categorieId: integer(\"categorie_id\")` — the Drizzle column shape. Only\n // the string argument is the column; the key beside it is the *wire* name\n // and no longer agrees with it (`authorId: integer(\"author_id\")`). Matching\n // both, as this did, made every camelCased key look like a column the\n // generated file did not declare.\n const literal = column.replace(/[.*+?^${}()|[\\]\\\\]/g, \"\\\\$&\");\n return new RegExp(`(^|[\\\\s,{])[\\\\w$\"']+\\\\s*:\\\\s*\\\\w+\\\\(\\\\s*[\"']${literal}[\"']`, \"m\").test(block);\n}\n\n/**\n * @param generatedSource contents of `backend/src/schema.generated.ts`\n * @param collections the project's collections, as this release reads them\n */\nexport function findLegacyForeignKeyNames(\n generatedSource: string,\n collections: CollectionConfig[]\n): LegacyForeignKeyName[] {\n const found: LegacyForeignKeyName[] = [];\n const seen = new Set<string>();\n\n /**\n * Report `wanted` as stale when the generated schema declares what the\n * previous rule would have derived from `source` instead.\n *\n * The `wanted !== current` guard is what honours an explicitly named column.\n * An author who pinned `categorie_id` has said so on the relation, so the\n * generated file agreeing with them is correct, not stale — and the rename\n * note documents exactly that opt-out.\n */\n const consider = (\n table: string,\n wanted: string,\n sourceName: string | undefined,\n relation: string\n ): void => {\n if (!table || !wanted || !sourceName) return;\n\n const current = generateForeignKeyName(sourceName);\n const legacy = legacyForeignKeyName(sourceName);\n if (legacy === current) return; // this name never moved\n if (wanted !== current) return; // pinned by the author, or unrelated\n\n const block = tableBlock(generatedSource, table);\n if (!block) return;\n if (!declaresColumn(block, legacy)) return;\n if (declaresColumn(block, current)) return; // already regenerated\n\n const key = `${table}.${legacy}`;\n if (seen.has(key)) return;\n seen.add(key);\n found.push({ table, legacy, current, relation });\n };\n\n // Same rule as the generator whose output this reads: a collection that\n // gets no table cannot have named a column in it. Inert in practice today —\n // the finding also requires the generated file to declare the table — but\n // the guard is what keeps that true when a non-SQL collection's slug happens\n // to match a SQL one's table.\n for (const collection of relationalCollections(collections)) {\n const sourceTable = getTableName(collection);\n\n for (const [name, relation] of Object.entries(resolveCollectionRelations(collection))) {\n const at = `${collection.slug}.${name}`;\n\n let target: CollectionConfig | undefined;\n try {\n target = relation.target?.();\n } catch {\n // A throwing target thunk is its own defect, reported at boot by\n // `validate-relations`. Nothing to say about its column names.\n continue;\n }\n\n switch (relation.kind) {\n case \"belongsTo\":\n consider(sourceTable, relation.localKey, relation.relationName, at);\n break;\n\n case \"hasOne\":\n case \"hasMany\":\n if (target) {\n consider(getTableName(target), relation.foreignKeyOnTarget, collection.slug, at);\n }\n break;\n\n case \"manyToMany\":\n consider(relation.through.table, relation.through.sourceColumn, collection.slug, at);\n if (target) {\n consider(relation.through.table, relation.through.targetColumn, target.slug, at);\n }\n break;\n\n default:\n // `via` joins are written by hand — there is no derived name\n // for the rule change to have moved.\n break;\n }\n }\n }\n\n return found;\n}\n\n/** One-line summary for a log or a CLI notice. */\nexport function describeLegacyForeignKeyNames(found: LegacyForeignKeyName[]): string {\n return found\n .map(f => ` • ${f.table}.${f.legacy} → ${f.current} (${f.relation})`)\n .join(\"\\n\");\n}\n\n// ── The other way a generated schema goes stale ──────────────────────────────\n//\n// The rename above is the subtle one: nothing the developer owns changed. The\n// ordinary one is the opposite — a collection or a relation was added and the\n// generator was not re-run, so the checked-in module simply does not describe\n// part of the schema. It fails later and elsewhere: relation validation, or a\n// query against a table the module has never heard of.\n//\n// Still not \"is this byte-for-byte what the generator would emit now\". This asks\n// only about *names*, which is what the rest of the system reads out of the\n// file, so reformatting the generator cannot make it red.\n\n/** Something the collections derive that the generated schema does not declare. */\nexport interface MissingGeneratedName {\n /** The table it belongs to, or is. */\n table: string;\n /** The column, when a column is what is missing. */\n column?: string;\n /** `<collection>` or `<collection>.<relation>`, for the message. */\n source: string;\n}\n\n/**\n * Tables and derived foreign-key columns the collections name and the generated\n * schema does not.\n *\n * @param generatedSource contents of `backend/src/schema.generated.ts`\n * @param collections the project's collections, as this release reads them\n */\nexport function findMissingGeneratedNames(\n generatedSource: string,\n collections: CollectionConfig[]\n): MissingGeneratedName[] {\n const missing: MissingGeneratedName[] = [];\n const seen = new Set<string>();\n\n const report = (table: string, source: string, column?: string): void => {\n const key = `${table}.${column ?? \"\"}`;\n if (seen.has(key)) return;\n seen.add(key);\n missing.push({ table, source, ...(column ? { column } : {}) });\n };\n\n const requireColumn = (table: string, column: string | undefined, source: string): void => {\n if (!table || !column) return;\n const block = tableBlock(generatedSource, table);\n // A table that is absent is reported once, as a table. Listing each of\n // its columns as separately missing would bury the one fact that\n // explains all of them.\n if (!block) return report(table, source);\n if (!declaresColumn(block, column)) report(table, source, column);\n };\n\n for (const collection of relationalCollections(collections)) {\n const sourceTable = getTableName(collection);\n if (!tableBlock(generatedSource, sourceTable)) {\n report(sourceTable, collection.slug);\n continue;\n }\n\n for (const [name, relation] of Object.entries(resolveCollectionRelations(collection))) {\n const at = `${collection.slug}.${name}`;\n\n let target: CollectionConfig | undefined;\n try {\n target = relation.target?.();\n } catch {\n // Its own defect, reported at boot by `validate-relations`.\n continue;\n }\n\n switch (relation.kind) {\n case \"belongsTo\":\n requireColumn(sourceTable, relation.localKey, at);\n break;\n\n case \"hasOne\":\n case \"hasMany\":\n if (target) requireColumn(getTableName(target), relation.foreignKeyOnTarget, at);\n break;\n\n case \"manyToMany\":\n requireColumn(relation.through.table, relation.through.sourceColumn, at);\n requireColumn(relation.through.table, relation.through.targetColumn, at);\n break;\n\n default:\n // `via` joins are written by hand; there is no derived name\n // to be missing.\n break;\n }\n }\n }\n\n return missing;\n}\n\n/** One-line summary for a log or a CLI notice. */\nexport function describeMissingGeneratedNames(missing: MissingGeneratedName[]): string {\n return missing\n .map(m => (m.column\n ? ` • ${m.table}.${m.column} is not declared (${m.source})`\n : ` • table ${m.table} is not declared (${m.source})`))\n .join(\"\\n\");\n}\n\n/** What `rebase schema stale` found, and what that costs. */\nexport interface StaleVerdict {\n /** The lines to print, in order. Empty only under `--fix` with nothing to fix. */\n lines: string[];\n /** Non-zero only when the reader must act before the server will boot. */\n exitCode: 0 | 1;\n /** True when `--fix` should regenerate. */\n regenerate: boolean;\n}\n\n/**\n * The whole of `schema stale`'s decision, separated from doing it.\n *\n * Separated because of what the command used to do on the clean path: **exit 0\n * having written zero bytes to stdout and zero to stderr.** For a command the\n * top-level help describes as \"Report generated schema files the collections\n * have moved past\", that is indistinguishable from a no-op, a crash, and a\n * subcommand the driver does not implement. Three of its four paths were\n * silent, and none of them could be tested where they stood: `cli.ts` uses\n * `import.meta`, which the driver's jest suite cannot load at all.\n *\n * `--fix` is the exception, and stays silent when there is nothing to fix.\n * `rebase dev` runs `schema stale --fix` before every boot with inherited\n * stdio, and \"nothing was wrong\" is not news a hundred lines into a start-up\n * transcript. The flag is the automated caller's; its silence is the point.\n */\nexport function staleVerdict(input: {\n /** `--output`, as the reader typed it. */\n outputPath: string;\n /** Whether the generated schema exists at all. */\n generatedExists: boolean;\n /** The legacy foreign-key names found in it, if it was read. */\n stale: LegacyForeignKeyName[];\n /** Why the comparison could not be made, when it could not. */\n unreadable?: string;\n /** Whether `--fix` was given. */\n fix: boolean;\n}): StaleVerdict {\n const quiet = (lines: string[]): string[] => (input.fix ? [] : lines);\n\n if (!input.generatedExists) {\n // Not staleness: a fresh project has not run the generator, and saying\n // \"stale\" about a file that does not exist sends the reader looking for\n // something to fix.\n return {\n lines: quiet([` No generated schema yet at ${input.outputPath} — nothing to compare.`,\n \" `rebase schema generate` creates it.\"]),\n exitCode: 0,\n regenerate: false\n };\n }\n\n if (input.unreadable !== undefined) {\n // Best-effort by design: a collections directory that will not load is a\n // real error, but boot reports it far better than this does. Saying so\n // is still better than exiting 0 in silence about a check that did not\n // run — this file's whole subject.\n return {\n lines: quiet([` ⏭ Not checked: ${input.unreadable}`]),\n exitCode: 0,\n regenerate: false\n };\n }\n\n if (input.stale.length === 0) {\n return {\n lines: quiet([` ✓ Nothing stale — 1 generated file matches (${input.outputPath}).`]),\n exitCode: 0,\n regenerate: false\n };\n }\n\n const found = [\n \"\",\n ` ⚠️ ${input.outputPath} names ${input.stale.length} foreign key(s) the way an earlier release did:`,\n describeLegacyForeignKeyNames(input.stale),\n \"\"\n ];\n\n if (input.fix) {\n return { lines: [...found, \" Regenerating the Drizzle schema so it matches...\"], exitCode: 0, regenerate: true };\n }\n\n return {\n lines: [\n ...found,\n // Not \"the server will refuse to start\" any more. The runtime builds\n // its tables from `information_schema`, so a stale file changes\n // nothing it serves — it warns once at boot and carries on. What the\n // file still decides is everything around the server: Atlas plans\n // migrations from it, `db push` diffs it, `eject` writes it out, and\n // user code imports it.\n \" The database column has already been renamed at boot. The server reads the database, \"\n + \"so it will still start — but `db push`, Atlas, `eject` and any code importing this \"\n + \"file are all reading a description that no longer matches.\",\n \" Run `rebase schema generate` to regenerate it.\"\n ],\n exitCode: 1,\n regenerate: false\n };\n}\n\n\n// ── Is this file what the current collections generate? ──────────────────────\n\n/** One exported declaration that differs between two generated schemas. */\nexport interface DeclarationDifference {\n /** The exported symbol: a table, an enum, a relations block. */\n name: string;\n kind: \"missing\" | \"unexpected\" | \"changed\";\n}\n\n/**\n * `export const NAME = …` → the declaration, with formatting and the\n * generator's own comments removed.\n *\n * Every generated declaration starts at column 0 and runs to the next one, so\n * splitting on them needs no brace counting and cannot get out of step with a\n * policy clause that happens to contain a brace.\n */\nfunction declarations(source: string): Map<string, string> {\n const out = new Map<string, string>();\n const starts = [...source.matchAll(/^export const (\\w+) =/gm)];\n starts.forEach((match, index) => {\n const from = match.index! + match[0].length;\n const to = index + 1 < starts.length ? starts[index + 1].index! : source.length;\n out.set(\n match[1],\n source.slice(from, to)\n // The `// INCLUDE (...)` note on a covering index, and any other\n // remark the renderer leaves: they describe the declaration,\n // they are not part of it.\n .replace(/\\/\\/[^\\n]*/g, \"\")\n .replace(/\\s+/g, \" \")\n .trim()\n );\n });\n return out;\n}\n\n/**\n * What differs between the schema the current collections generate and the one\n * on disk — by declaration, not by byte.\n *\n * `rebase doctor` used to whitespace-normalise both files and compare the\n * strings, which answers \"are these the same text\" when the question is \"does\n * this file still describe the same schema\". The difference is not academic:\n * the answer was a single yes/no with nothing to act on, so a developer told\n * their schema was stale had to regenerate and read the diff to find out what\n * had changed.\n *\n * Comparing declarations answers the question the reader actually has. The\n * generated file is a flat list of `export const`s — one per table, enum and\n * relations block, each the rendering of a piece of the schema plan — and two\n * files declaring the same symbols with the same bodies describe the same\n * database whatever their formatting.\n *\n * Not a plan-to-plan comparison, and deliberately not: reading a plan back out\n * of `schema.generated.ts` means interpreting drizzle-orm builder calls, which\n * is a second interpreter — the exact thing this area was just rid of. The\n * rendered declarations are the closest honest proxy, and the renderer is\n * deterministic, so the only things normalised away are what it is free to vary\n * independently of the plan: whitespace and its own comments.\n */\nexport function compareGeneratedDeclarations(expected: string, actual: string): DeclarationDifference[] {\n const want = declarations(expected);\n const have = declarations(actual);\n const differences: DeclarationDifference[] = [];\n for (const [name, body] of want) {\n if (!have.has(name)) differences.push({ name, kind: \"missing\" });\n else if (have.get(name) !== body) differences.push({ name, kind: \"changed\" });\n }\n for (const name of have.keys()) {\n if (!want.has(name)) differences.push({ name, kind: \"unexpected\" });\n }\n return differences;\n}\n\n/** The differences, as the line a developer reads in `rebase doctor`. */\nexport function describeDeclarationDifferences(differences: DeclarationDifference[]): string {\n const shown = differences.slice(0, 5).map(difference => {\n if (difference.kind === \"missing\") return `${difference.name} (not in the file)`;\n if (difference.kind === \"unexpected\") return `${difference.name} (in the file, no longer generated)`;\n return `${difference.name} (differs)`;\n });\n const rest = differences.length - shown.length;\n return shown.join(\", \") + (rest > 0 ? `, and ${rest} more` : \"\");\n}\n"],"mappings":"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"}
|
|
@@ -17,11 +17,30 @@ export declare class HistoryService {
|
|
|
17
17
|
retention: HistoryRetentionConfig;
|
|
18
18
|
constructor(db: NodePgDatabase, retention?: Partial<HistoryRetentionConfig>);
|
|
19
19
|
/**
|
|
20
|
-
* Record a history entry for
|
|
21
|
-
* This is intentionally fire-and-forget safe — errors are logged but never
|
|
22
|
-
* bubble up to block the main save/delete operation.
|
|
20
|
+
* Record a history entry for a row change.
|
|
23
21
|
*
|
|
24
|
-
*
|
|
22
|
+
* ## Why this is allowed to fail the write
|
|
23
|
+
*
|
|
24
|
+
* It used to swallow every error into a log line, and the driver called it
|
|
25
|
+
* without `await`, so the promise was dropped on the floor. Both halves of
|
|
26
|
+
* that made the same claim: that the audit trail is a nice-to-have. It is
|
|
27
|
+
* not — `history: true` is opted into by collections whose changes somebody
|
|
28
|
+
* has to be able to reconstruct, and a trail with gaps is worse than no
|
|
29
|
+
* trail, because nothing distinguishes "no change was made" from "the entry
|
|
30
|
+
* did not get written". The gaps were silent and unbounded: a transient
|
|
31
|
+
* error on the history insert lost the entry for a row that committed
|
|
32
|
+
* anyway, and nobody found out.
|
|
33
|
+
*
|
|
34
|
+
* So it throws, and the driver awaits it inside the write's transaction.
|
|
35
|
+
* The row and its history entry commit together or neither does. That is a
|
|
36
|
+
* real trade — a broken `rebase.entity_history` now fails writes to the
|
|
37
|
+
* collections that declare history, where before it failed quietly — and it
|
|
38
|
+
* is the right side of it for a table that exists to answer "who changed
|
|
39
|
+
* this". The table is created at boot (`ensureHistoryTableExists`), so the
|
|
40
|
+
* failure mode is a database that is broken in other ways too.
|
|
41
|
+
*
|
|
42
|
+
* The pruning pass stays non-blocking: a prune that does not run leaves
|
|
43
|
+
* *extra* history, which is not a loss and is fixed by the next write.
|
|
25
44
|
*/
|
|
26
45
|
recordHistory(params: RecordHistoryParams): Promise<void>;
|
|
27
46
|
/**
|