---
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.
