241 lines
10 KiB
Markdown
241 lines
10 KiB
Markdown
---
|
|
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.
|