feat(migrations): reconcile collections columns missed by initial-schema backfill (B2) #150

Merged
varutasu merged 1 commit from convoy/reconcile-b2-collections-columns into main 2026-06-14 09:18:05 -04:00

View file

@ -0,0 +1,114 @@
/**
* Reconcile-historical-add-scripts convoy Brief 2.
*
* Backfills the `collections`-table columns that historical scripts added to
* prod but which `migrations/1779853647564_initial-schema.js` never captured:
*
* - `visibility VARCHAR(20) DEFAULT 'private'` (from `scripts/add-collaboration-features.js`, lines 15-21 collections half only)
* - `tcg VARCHAR(50) DEFAULT 'MTG'` (from `scripts/add-collaboration-features.js`, lines 15-21 collections half only)
* - `tags TEXT` (from `scripts/add-collaboration-features.js`, lines 15-21 collections half only)
* - `slug VARCHAR(100) UNIQUE` (from `scripts/add-collection-slugs.js`, lines 17-20)
* - `idx_collections_slug` UNIQUE INDEX (from `scripts/add-collection-slugs.js`, line 79)
* - `check_slug_format` CHECK constraint (from `scripts/add-collection-slugs.js`, line 91)
* - `image TEXT` (from `scripts/add-image-column.js`, lines 14-17)
*
* Excluded from B2 (out of scope per architect inventory):
* - `collection_permissions`, `collection_activity`, `users.is_pending`,
* `idx_collections_visibility`, `idx_collection_permissions_*`,
* `idx_collection_activity_collection_id` these belong to B3
* (`reconcile-collaboration-tables`) per the architect plan.
* - `is_system_collection` already captured by
* `migrations/1780378340194_system-collection-description.js`.
*
* Excluded DML: per-row UPDATE that backfills slugs from `name` via
* `lib/slug-utils.js::generateUniqueSlug` (script lines 41-74). Generating
* slugs on a fresh env is moot there are no pre-existing collections to
* backfill and replicating the runtime helper inside a migration would
* couple the schema timeline to application code. Operators of long-lived
* envs already ran the backfill historically; this migration does NOT
* re-run it.
*
* Idempotency strategy (D2): every statement is `IF NOT EXISTS`-guarded.
*
* - `ADD COLUMN IF NOT EXISTS` Postgres native; the `UNIQUE` inline
* clause on `slug` is ignored when the column already exists, so prod
* (which already has the autogen `collections_slug_key` constraint)
* and fresh envs (where the autogen constraint will be added) both
* converge correctly.
* - `CREATE UNIQUE INDEX IF NOT EXISTS` Postgres native.
* - `ALTER TABLE ... ADD CONSTRAINT` for CHECK Postgres has no
* `IF NOT EXISTS` clause for named constraints, so we wrap in a
* `DO $$ BEGIN ... END $$` block that checks `pg_constraint` first.
*
* Down-migration is a hard stub. See `down()` below matches
* `1779853647564_initial-schema.js` style.
*
* @type {import('node-pg-migrate').ColumnDefinitions | undefined}
*/
export const shorthands = undefined;
/**
* @param {import('node-pg-migrate').MigrationBuilder} pgm
* @returns {void}
*/
export const up = (pgm) => {
pgm.sql(`
ALTER TABLE collections
ADD COLUMN IF NOT EXISTS visibility VARCHAR(20) DEFAULT 'private',
ADD COLUMN IF NOT EXISTS tcg VARCHAR(50) DEFAULT 'MTG',
ADD COLUMN IF NOT EXISTS tags TEXT
`);
pgm.sql(`
ALTER TABLE collections
ADD COLUMN IF NOT EXISTS image TEXT
`);
pgm.sql(`
ALTER TABLE collections
ADD COLUMN IF NOT EXISTS slug VARCHAR(100) UNIQUE
`);
pgm.sql(`
CREATE UNIQUE INDEX IF NOT EXISTS idx_collections_slug
ON collections(slug)
`);
pgm.sql(`
DO $$
BEGIN
IF NOT EXISTS (
SELECT 1
FROM pg_constraint
WHERE conname = 'check_slug_format'
AND conrelid = 'collections'::regclass
) THEN
ALTER TABLE collections
ADD CONSTRAINT check_slug_format
CHECK (slug ~ '^[a-z0-9]([a-z0-9-]*[a-z0-9])?$' AND length(slug) <= 50);
END IF;
END
$$;
`);
};
/**
* Down-migration is a hard stub. This is a reconciliation migration: the
* prod schema state is the source of truth, and dropping these columns
* would discard slugs / visibility flags / tcg labels / tags / images that
* application code reads on every collection page render. If you need a
* clean schema for testing, branch the Neon database and run
* `npm run migrate up` against the branch instead of rolling this back.
*
* @returns {void}
*/
export const down = () => {
throw new Error(
'[migration:1781000000002_reconcile-collections-columns] Refusing to drop reconciled columns. ' +
'Rolling back this migration would erase visibility/tcg/tags/slug/image data on every ' +
'collections row in the database. If you need a clean schema for testing, branch the ' +
'Neon database and run `npm run migrate up` against the branch instead. See ' +
'migrations/1781000000002_reconcile-collections-columns.js up() docstring for the ' +
'reconciliation rationale.'
);
};