/** * Reconciliation migration — favorites system (B4 of reconcile-historical-add-scripts). * * Captures the DDL effects of the historical `scripts/add-favorites-system.js` * job, which has been applied to every long-lived environment but is NOT * reproduced by the post-migration-tool history on a fresh Neon branch. After * this migration lands, `npm install` → `npm run setup-db` against a clean * branch produces a `user_favorites` table that is structurally equivalent to * prod (same columns, same UNIQUE tuple, same four supporting indexes). * * Source script (READ ONLY, per `.cursor/rules/no-go-zones.mdc`): * scripts/add-favorites-system.js * * Runtime evidence the table exists in prod and that this is the exact shape * the API expects: * pages/api/favorites.js — SELECT / INSERT / DELETE against * `user_favorites` keyed by (user_id, item_type, item_id), with * item_type ∈ {'card', 'collection', 'deck'} and a polymorphic item_id. * * Idempotency (convoy D2): * - `CREATE TABLE IF NOT EXISTS` — re-applying against any env where the * historical script already ran is a documented no-op. * - `CREATE INDEX IF NOT EXISTS` — same idempotency contract for all four * indexes. * - `UNIQUE(user_id, item_type, item_id)` is declared inline on the table * so it is created at the same moment as the table and never needs a * separate guarded `ADD CONSTRAINT` path. * * Shape parity with the historical script: * - Same column set (id SERIAL PK, user_id INTEGER FK CASCADE, * item_type VARCHAR(50) NOT NULL, item_id INTEGER NOT NULL, * created_at TIMESTAMP DEFAULT CURRENT_TIMESTAMP). * - Same UNIQUE tuple. * - Same four index definitions (single-column on user_id, item_type, * item_id; composite (user_id, item_type)). * - Same index naming (idx_user_favorites_*). * * FK style follows `migrations/1779853647564_initial-schema.js` — * `INTEGER REFERENCES users(id) ON DELETE CASCADE`, no separate FK * constraint name. * * Down-migration is a hard stub: this is a reconciliation migration whose * purpose is to bring fresh envs to parity with prod; rolling back would * drop `user_favorites` on prod, which is a destructive operation that * deserves its own scoped convoy. * * Note on schema-map terminology drift: * `.cursor/rules/schema-map.mdc` § "Common reference" mentions * `favorites (user_id, card_id)` — that line is a minimal shorthand and * does NOT match the actual prod table (`user_favorites`, polymorphic on * item_type + item_id, per `pages/api/favorites.js`). The rule's wording * is updated by B7 of this convoy; this migration follows the runtime * evidence. * * @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(` CREATE TABLE IF NOT EXISTS user_favorites ( id SERIAL PRIMARY KEY, user_id INTEGER REFERENCES users(id) ON DELETE CASCADE, item_type VARCHAR(50) NOT NULL, item_id INTEGER NOT NULL, created_at TIMESTAMP DEFAULT CURRENT_TIMESTAMP, UNIQUE(user_id, item_type, item_id) ) `); pgm.sql(` CREATE INDEX IF NOT EXISTS idx_user_favorites_user_id ON user_favorites(user_id); CREATE INDEX IF NOT EXISTS idx_user_favorites_item_type ON user_favorites(item_type); CREATE INDEX IF NOT EXISTS idx_user_favorites_item_id ON user_favorites(item_id); CREATE INDEX IF NOT EXISTS idx_user_favorites_user_type ON user_favorites(user_id, item_type); `); }; /** * @returns {void} */ export const down = () => { throw new Error( '[migration:1781000000004_reconcile-favorites-system] Refusing to drop user_favorites. ' + 'This is a reconciliation migration that brings fresh envs to parity with prod; rolling it ' + 'back would drop the user_favorites table (and every row in it) on whatever env it runs against. ' + 'If you need to remove the favorites system, ship a dedicated convoy with a real down() that ' + 'audits readers (pages/api/favorites.js + any UI surfaces) first.' ); };