10 KiB
| convoy | brief_number | depends_on | files | |
|---|---|---|---|---|
| reconcile-historical-add-scripts | 4 |
|
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):
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.mdc—scripts/add-favorites-system.jsis append-only history.- Style: raw
pgm.sql(...)template literals. Per D2. - ESM exports;
"type": "module". IF NOT EXISTSon 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.jsexists with the exact filename above.- The file's
up()createsuser_favoriteswith all 5 columns + UNIQUE constraint, then 4 indexes, all viaIF 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.jspasses.node -e "import('./migrations/1781000000004_reconcile-favorites-system.js').then(m => console.log(typeof m.up, typeof m.down, typeof m.shorthands))"printsfunction function undefined.npm run lintmatches baseline.npm run test:runreports 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.jsor any file underscripts/. - DO NOT edit any other file under
migrations/. - DO NOT edit
scripts/setup-neon-db.js,package.json, README,AGENTS.md, ordocs/SCHEMA_MAP.md(B7 owns SCHEMA_MAP). - DO NOT run
npm run migrate upagainst 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.