feat(migrations): reconcile favorites system missed by initial-schema backfill (B4) (#152)
Captures the DDL effects of scripts/add-favorites-system.js (a historical
no-go-zone script) so a fresh Neon branch onboarded via npm run setup-db
has the same user_favorites table + 4 indexes that prod has via the
historical script. Brings fresh envs to parity with prod for the
favorites surface used by pages/api/favorites.js.
Shape matches the historical script and the runtime API verbatim:
- user_favorites(id, user_id FK CASCADE, item_type VARCHAR(50),
item_id INTEGER, created_at, UNIQUE(user_id, item_type, item_id))
- idx_user_favorites_user_id / _item_type / _item_id / _user_type
CREATE TABLE / CREATE INDEX guarded with IF NOT EXISTS per convoy
decision D2 — re-running against any env where the historical script
already ran is a documented no-op (only the pgmigrations row is new).
down() is a hard stub: rolling back would drop user_favorites and every
row in it; removal deserves its own scoped convoy.
Part of .convoys/reconcile-historical-add-scripts (commit 22ebef2),
Brief 4 of 7. Base PR is main, not the parent convoy branch, per the
parallel-implementer dispatch pattern.
Co-authored-by: Cursor <cursoragent@cursor.com>
This commit is contained in:
parent
a35ce01ba0
commit
ef2cfb8547
1 changed files with 98 additions and 0 deletions
98
migrations/1781000000004_reconcile-favorites-system.js
Normal file
98
migrations/1781000000004_reconcile-favorites-system.js
Normal file
|
|
@ -0,0 +1,98 @@
|
|||
/**
|
||||
* Reconciliation migration — favorites system (B4 of reconcile-historical-add-scripts).
|
||||
*
|
||||
* Captures the DDL effects of the historical `scripts/add-favorites-system.js`
|
||||
* job, which has been applied to every long-lived environment but is NOT
|
||||
* reproduced by the post-migration-tool history on a fresh Neon branch. After
|
||||
* this migration lands, `npm install` → `npm run setup-db` against a clean
|
||||
* branch produces a `user_favorites` table that is structurally equivalent to
|
||||
* prod (same columns, same UNIQUE tuple, same four supporting indexes).
|
||||
*
|
||||
* Source script (READ ONLY, per `.cursor/rules/no-go-zones.mdc`):
|
||||
* scripts/add-favorites-system.js
|
||||
*
|
||||
* Runtime evidence the table exists in prod and that this is the exact shape
|
||||
* the API expects:
|
||||
* pages/api/favorites.js — SELECT / INSERT / DELETE against
|
||||
* `user_favorites` keyed by (user_id, item_type, item_id), with
|
||||
* item_type ∈ {'card', 'collection', 'deck'} and a polymorphic item_id.
|
||||
*
|
||||
* Idempotency (convoy D2):
|
||||
* - `CREATE TABLE IF NOT EXISTS` — re-applying against any env where the
|
||||
* historical script already ran is a documented no-op.
|
||||
* - `CREATE INDEX IF NOT EXISTS` — same idempotency contract for all four
|
||||
* indexes.
|
||||
* - `UNIQUE(user_id, item_type, item_id)` is declared inline on the table
|
||||
* so it is created at the same moment as the table and never needs a
|
||||
* separate guarded `ADD CONSTRAINT` path.
|
||||
*
|
||||
* Shape parity with the historical script:
|
||||
* - Same column set (id SERIAL PK, user_id INTEGER FK CASCADE,
|
||||
* item_type VARCHAR(50) NOT NULL, item_id INTEGER NOT NULL,
|
||||
* created_at TIMESTAMP DEFAULT CURRENT_TIMESTAMP).
|
||||
* - Same UNIQUE tuple.
|
||||
* - Same four index definitions (single-column on user_id, item_type,
|
||||
* item_id; composite (user_id, item_type)).
|
||||
* - Same index naming (idx_user_favorites_*).
|
||||
*
|
||||
* FK style follows `migrations/1779853647564_initial-schema.js` —
|
||||
* `INTEGER REFERENCES users(id) ON DELETE CASCADE`, no separate FK
|
||||
* constraint name.
|
||||
*
|
||||
* Down-migration is a hard stub: this is a reconciliation migration whose
|
||||
* purpose is to bring fresh envs to parity with prod; rolling back would
|
||||
* drop `user_favorites` on prod, which is a destructive operation that
|
||||
* deserves its own scoped convoy.
|
||||
*
|
||||
* Note on schema-map terminology drift:
|
||||
* `.cursor/rules/schema-map.mdc` § "Common reference" mentions
|
||||
* `favorites (user_id, card_id)` — that line is a minimal shorthand and
|
||||
* does NOT match the actual prod table (`user_favorites`, polymorphic on
|
||||
* item_type + item_id, per `pages/api/favorites.js`). The rule's wording
|
||||
* is updated by B7 of this convoy; this migration follows the runtime
|
||||
* evidence.
|
||||
*
|
||||
* @type {import('node-pg-migrate').ColumnDefinitions | undefined}
|
||||
*/
|
||||
export const shorthands = undefined;
|
||||
|
||||
/**
|
||||
* @param {import('node-pg-migrate').MigrationBuilder} pgm
|
||||
* @returns {void}
|
||||
*/
|
||||
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)
|
||||
)
|
||||
`);
|
||||
|
||||
pgm.sql(`
|
||||
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);
|
||||
`);
|
||||
};
|
||||
|
||||
/**
|
||||
* @returns {void}
|
||||
*/
|
||||
export const down = () => {
|
||||
throw new Error(
|
||||
'[migration:1781000000004_reconcile-favorites-system] Refusing to drop user_favorites. ' +
|
||||
'This is a reconciliation migration that brings fresh envs to parity with prod; rolling it ' +
|
||||
'back would drop the user_favorites table (and every row in it) on whatever env it runs against. ' +
|
||||
'If you need to remove the favorites system, ship a dedicated convoy with a real down() that ' +
|
||||
'audits readers (pages/api/favorites.js + any UI surfaces) first.'
|
||||
);
|
||||
};
|
||||
Loading…
Reference in a new issue