--- convoy: reconcile-historical-add-scripts brief_number: 4 depends_on: [] files: - migrations/1781000000004_reconcile-favorites-system.js --- # Brief 4: Reconcile favorites system into migration history ## Goal (1 sentence) Capture the `user_favorites` table and its 4 indexes from `scripts/add-favorites-system.js` into a single new `node-pg-migrate` migration so a brand-new Neon branch ends up with the favorites surface after `npm run setup-db`. ## Scope (files in scope — do not edit anything else) - `migrations/1781000000004_reconcile-favorites-system.js` — **new** ## Source script (read-only audit reference; DO NOT EDIT — no-go-zone) `scripts/add-favorites-system.js` lines 11-27 (entire DDL — script has no DML beyond the table create): ```js await 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, -- 'card', 'collection', 'deck' item_id INTEGER NOT NULL, created_at TIMESTAMP DEFAULT CURRENT_TIMESTAMP, UNIQUE(user_id, item_type, item_id) ) `; // 4 indexes await sql`CREATE INDEX IF NOT EXISTS idx_user_favorites_user_id ON user_favorites(user_id)`; await sql`CREATE INDEX IF NOT EXISTS idx_user_favorites_item_type ON user_favorites(item_type)`; await sql`CREATE INDEX IF NOT EXISTS idx_user_favorites_item_id ON user_favorites(item_id)`; await sql`CREATE INDEX IF NOT EXISTS idx_user_favorites_user_type ON user_favorites(user_id, item_type)`; ``` **Note on SCHEMA_MAP drift.** `docs/SCHEMA_MAP.md` § `user_favorites` (lines 104-111) shows the table with only `(user_id, card_id)` columns — that's a **doc bug**. The actual source script (and prod schema) uses the polymorphic `(item_type, item_id)` shape that supports cards, collections, AND decks (per the script's inline comment + the runtime usage in `pages/api/favorites.js`). B7 corrects the SCHEMA_MAP entry. This brief faithfully reproduces the **script's** shape — `(item_type, item_id)` — not the doc's. ## Target migration file Path: `migrations/1781000000004_reconcile-favorites-system.js` Contents: ```js /** * Reconcile scripts/add-favorites-system.js into the migration history. * * Creates user_favorites with the polymorphic (item_type, item_id) * shape that supports favoriting cards, collections, and decks via a * single table. Adds 4 indexes for the common query shapes: * * - idx_user_favorites_user_id — "all favorites for user X" * - idx_user_favorites_item_type — "all card favorites" / "all deck favorites" * - idx_user_favorites_item_id — back-link from an item to its favoriters * - idx_user_favorites_user_type — composite for "user X's card favorites" * * Note: docs/SCHEMA_MAP.md § user_favorites currently shows only * (user_id, card_id) — that's a doc bug. The actual prod shape (and * the source script, and the runtime in pages/api/favorites.js) uses * the polymorphic shape. B7 of this convoy corrects the SCHEMA_MAP * entry; this migration reproduces the script's shape faithfully. * * Idempotent re-apply: CREATE TABLE IF NOT EXISTS + CREATE INDEX IF NOT EXISTS. * * @type {import('node-pg-migrate').ColumnDefinitions | undefined} */ export const shorthands = undefined; /** * @param {import('node-pg-migrate').MigrationBuilder} pgm */ 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) ); 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); `); }; /** * Down-migration intentionally throws. Dropping user_favorites on a * long-lived env would erase every user's saved favorites list and * break runtime reads in pages/api/favorites.js. * * @returns {void} */ export const down = () => { throw new Error( '[migration:1781000000004_reconcile-favorites-system] Down not supported. ' + 'Dropping user_favorites would erase every saved favorite and break ' + 'pages/api/favorites.js. Write a new dated migration for any future ' + 'schema correction.' ); }; ``` ## Conventions to follow - `.cursor/rules/db-and-schema.mdc` § "Schema source of truth" — `migrations/` is canonical. - `.cursor/rules/no-go-zones.mdc` — `scripts/add-favorites-system.js` is append-only history. - Style: raw `pgm.sql(...)` template literals. Per D2. - ESM exports; `"type": "module"`. - `IF NOT EXISTS` on every CREATE. - Match the source script's column types, FK ON DELETE rule (`CASCADE`), and UNIQUE shape verbatim. ## Acceptance criteria - [ ] `migrations/1781000000004_reconcile-favorites-system.js` exists with the exact filename above. - [ ] The file's `up()` creates `user_favorites` with all 5 columns + UNIQUE constraint, then 4 indexes, all via `IF NOT EXISTS`. - [ ] The column shape is `(item_type VARCHAR(50), item_id INTEGER)` — the polymorphic shape — NOT `(card_id)`. See "Note on SCHEMA_MAP drift" above. - [ ] The file's `down()` throws with a clear message. - [ ] The file's docstring cites the source script + the convoy file + the SCHEMA_MAP doc-bug note. - [ ] `node --check migrations/1781000000004_reconcile-favorites-system.js` passes. - [ ] `node -e "import('./migrations/1781000000004_reconcile-favorites-system.js').then(m => console.log(typeof m.up, typeof m.down, typeof m.shorthands))"` prints `function function undefined`. - [ ] `npm run lint` matches baseline. - [ ] `npm run test:run` reports 21/21 passing. ## Verification ```bash node --check migrations/1781000000004_reconcile-favorites-system.js node -e "import('./migrations/1781000000004_reconcile-favorites-system.js').then(m => console.log(typeof m.up, typeof m.down, typeof m.shorthands))" npm run lint npm run test:run ``` Expected: `node --check` silent, `node -e` prints `function function undefined`, lint baseline, 21/21 tests pass. **Do NOT** run `npm run migrate up` against any environment. ## Commit message ``` feat(migrations): reconcile favorites system into migration history (brief 4/7) Captures scripts/add-favorites-system.js into one new node-pg-migrate migration: - CREATE TABLE user_favorites (polymorphic (item_type, item_id) shape; UNIQUE(user_id, item_type, item_id)) - 4 indexes (user_id, item_type, item_id, (user_id, item_type)) Note: docs/SCHEMA_MAP.md § user_favorites currently shows the table with only (user_id, card_id) — that's a doc bug; B7 of this convoy corrects the entry. This migration faithfully reproduces the source script's polymorphic shape (also what runtime in pages/api/favorites.js uses). Per .convoys/reconcile-historical-add-scripts.md § Brief outline → B4. Idempotent re-apply (IF NOT EXISTS guards). ``` ## PR shape **Title:** `feat(migrations): reconcile favorites system into migration history (brief 4/7)` **Body template:** ```markdown Brief 4 of the `reconcile-historical-add-scripts` convoy. See [`.convoys/reconcile-historical-add-scripts.md`](../.convoys/reconcile-historical-add-scripts.md) for the full plan and rationale. ## What this PR does Adds `migrations/1781000000004_reconcile-favorites-system.js` — captures `scripts/add-favorites-system.js` into a single new `node-pg-migrate` migration. - `CREATE TABLE IF NOT EXISTS user_favorites` with polymorphic `(item_type VARCHAR(50), item_id INTEGER)` shape supporting cards / collections / decks favorites in one table. - `UNIQUE(user_id, item_type, item_id)` prevents duplicate favorites. - 4 supporting indexes via `CREATE INDEX IF NOT EXISTS`. ## Note on a SCHEMA_MAP doc bug `docs/SCHEMA_MAP.md` § `user_favorites` (current `main`) shows the table with only `(user_id, card_id)` columns. That's a doc bug — the actual prod shape (and the source script, and `pages/api/favorites.js` runtime usage) uses the polymorphic `(item_type, item_id)` shape. This migration faithfully reproduces the script's shape; Brief 7 of this convoy fixes the SCHEMA_MAP entry. ## What this PR does NOT do - Does **NOT** edit `scripts/add-favorites-system.js` (no-go-zone). - Does **NOT** edit `docs/SCHEMA_MAP.md` (Brief 7's job). - Does **NOT** edit `scripts/setup-neon-db.js`, `package.json`, README, or `AGENTS.md`. - Does **NOT** run `npm run migrate up` against any environment. ## Verification checklist - [ ] `node --check migrations/1781000000004_reconcile-favorites-system.js` exits 0 - [ ] `node -e "import('./migrations/1781000000004_reconcile-favorites-system.js').then(m => console.log(typeof m.up, typeof m.down, typeof m.shorthands))"` prints `function function undefined` - [ ] `npm run lint` matches baseline - [ ] `npm run test:run` reports 21/21 passing - [ ] Did not run `npm run migrate up` against any environment in this PR ## Cross-references - Convoy file: `.convoys/reconcile-historical-add-scripts.md` - Source script (no-go-zone, audit reference only): `scripts/add-favorites-system.js` ``` ## DO NOT - DO NOT edit `scripts/add-favorites-system.js` or any file under `scripts/`. - DO NOT edit any other file under `migrations/`. - DO NOT edit `scripts/setup-neon-db.js`, `package.json`, README, `AGENTS.md`, or `docs/SCHEMA_MAP.md` (B7 owns SCHEMA_MAP). - DO NOT run `npm run migrate up` against any environment. - DO NOT use the SCHEMA_MAP `(user_id, card_id)` shape — that doc entry is wrong; reproduce the SOURCE SCRIPT'S polymorphic shape. - DO NOT call `npm run migrate create`. ## Rationale (≤3 sentences) The favorites system is a single self-contained table — natural fit for one small migration that maps 1:1 with the historical script. The polymorphic shape (`item_type`, `item_id`) is what runtime code actually uses, so reproducing it from the script's verbatim DDL — rather than the stale `(user_id, card_id)` doc entry — is the safe choice. Brief 7's SCHEMA_MAP rewrite resolves the doc-bug separately so this brief stays scoped to one new file.