deckhearth/.convoys/reconcile-historical-add-scripts/brief-4-reconcile-favorites-system.md
Randall Stillwell 1db6d4dd60 convoy(architect): draft B1-B5 + B7 implementer briefs (Finding 1 → A)
Co-authored-by: Cursor <cursoragent@cursor.com>
2026-06-14 08:52:04 -05:00

10 KiB

convoy brief_number depends_on files
reconcile-historical-add-scripts 4
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.jsnew

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):

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:

/**
 * 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.mdcscripts/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

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:

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.