deckhearth/migrations/1779853647564_initial-schema.js

157 lines
5.5 KiB
JavaScript
Raw Normal View History

feat(infra): adopt node-pg-migrate + backfill initial schema migration Closes P1 #11 of .convoys/ship-readiness.md (launch sequence step 7) — "No migration tool — scripts/add-*.js graveyard". Schema changes post-this-convoy ship as node-pg-migrate migrations under migrations/ at the repo root; the legacy 27 scripts/add-*.js / scripts/fix-*.js / scripts/seed-*.js jobs remain append-only history per the no-go-zones rule. Decisions (full record in .convoys/migration-tool.md § Decisions): D1 — Tool: node-pg-migrate@^8. Rejected drizzle-kit / prisma migrate / kysely because each forces broader TypeScript surface than AGENTS.md Gotcha #9 allows (TS is a devDep only). node-pg-migrate is JavaScript-native, raw-SQL-friendly via pgm.sql(), and ESM-clean for the post-bump-next-js "type": "module" repo. Brings pg@^8.21.0 as a peer dep (dev-only; never loaded in the Next.js bundle). D2 — Migrations directory: migrations/ at the repo root. Separates the tool-wrapped artifacts from the historical scripts/migrations/ placeholder folder (which housed the lone pre-tool 2026-05-24-rename-admin-email.js migration and remains preserved for the audit trail). Matches node-pg-migrate's default flag. D3 — Tracking table: default pgmigrations (no name collision with the existing 7-table bootstrap; zero CLI noise). D4 — Backfill strategy: hand-translate scripts/setup-neon-db.js's DDL into the initial migration verbatim. Each await sql`...` block becomes one pgm.sql(`...`) call. Each CREATE uses IF NOT EXISTS, so the migration is idempotent against fresh AND pre-existing envs — re-running setup-db on an env that already has the schema is a no-op DDL-wise (only records the pgmigrations row). Documented assumption: prod has drifted via the 27 historical add-*.js scripts; reconciling those into the migration history is the queued reconcile-historical-add-scripts follow-up convoy. D5 — Bootstrap reconciliation: split. setup-neon-db.js now (1) validates ADMIN_INITIAL_PASSWORD + POSTGRES_URL, (2) spawns `npm run migrate up` via child_process with stdio inherited, (3) seeds the admin row with ON CONFLICT (email) DO NOTHING. The seven DDL blocks are deleted from setup-neon-db.js; success/error message copy is updated to mention the migration step explicitly. D6 — CI integration: defer. Wiring a CI job that runs migrate up against a test DB needs either a dedicated Neon branch + secret OR a Postgres service container; both are real work. Surface as wire-migrate-into-ci follow-up. Risk acknowledged in .convoys/migration-tool.md § R3. D7 — Down-migration on the initial backfill: hard stub. Rolling back the initial schema would drop every user / card / collection / deck row in the DB. The stub throws with a long-form error pointing at the recommended alternative (branch the Neon database + forward-apply). Future migrations that touch one of the seven bootstrap tables write their own dated migration with a real down(). Verification (pre-PR): - npm run lint → 128 problems (baseline preserved, zero regression; migration file is lint-clean, no new ignore patterns) - npm run test:run → 21/21 pass - node --check on migrations/1779853647564_initial-schema.js + on scripts/setup-neon-db.js → exit 0 - Module load + down() throw verified via dynamic import - npm run migrate -- --help reaches the node-pg-migrate CLI through the wrapper Live verification against a Neon branch is deferred (no throwaway branch available); the operator's optional post-merge sequence is documented in .convoys/migration-tool.md § Operator runbook. See .convoys/migration-tool.md § Follow-ups for the queued wire-migrate-into-ci / reconcile-historical-add-scripts / retire-graveyard-scripts-after-audit / audit-node-pg-migrate-transitive-deps / add-migration-template follow-up convoys. Co-authored-by: Cursor <cursoragent@cursor.com>
2026-05-26 23:55:09 -04:00
/**
* Initial schema backfill migration.
*
* Reproduces the 7-table DDL that `scripts/setup-neon-db.js` has been the
* documented source-of-truth bootstrap for since the project's first commit.
* Every statement uses `CREATE TABLE IF NOT EXISTS` so the migration is
* idempotent against:
*
* 1. A brand-new Neon branch (creates all tables fresh).
* 2. An existing env where `npm run setup-db` has already run pre-convoy
* (every CREATE is a no-op; the `pgmigrations` row is the only change).
* 3. An existing env where the legacy `scripts/add-*.js` / `scripts/fix-*.js`
* jobs added columns beyond the bootstrap shape those columns are
* preserved (CREATE TABLE IF NOT EXISTS does not touch existing tables).
*
* The shape is byte-equivalent to `scripts/setup-neon-db.js` HEAD as of
* `convoy/migration-tool` branch. If those scripts diverge again in the
* future, ship a new dated migration alongside the script edit do NOT
* edit this file in place (this file is now itself an append-only artifact;
* mutating it would silently corrupt any env that has it recorded as run).
*
* Down-migration is intentionally a hard stub. See `down()` below.
*
* @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 users (
id SERIAL PRIMARY KEY,
email VARCHAR(255) UNIQUE NOT NULL,
password VARCHAR(255) NOT NULL,
role VARCHAR(50) DEFAULT 'user',
created_at TIMESTAMP DEFAULT CURRENT_TIMESTAMP,
updated_at TIMESTAMP DEFAULT CURRENT_TIMESTAMP
)
`);
pgm.sql(`
CREATE TABLE IF NOT EXISTS cards (
id SERIAL PRIMARY KEY,
name VARCHAR(255) NOT NULL,
set_name VARCHAR(255),
set_code VARCHAR(50),
card_number VARCHAR(50),
rarity VARCHAR(50),
game VARCHAR(50) NOT NULL,
mana_cost VARCHAR(50),
cmc INTEGER,
card_type VARCHAR(255),
colors JSONB,
oracle_text TEXT,
power VARCHAR(10),
toughness VARCHAR(10),
image_url TEXT,
stock_image_url TEXT,
current_price DECIMAL(10,2),
market_price DECIMAL(10,2),
scryfall_id VARCHAR(255) UNIQUE,
verified BOOLEAN DEFAULT false,
created_at TIMESTAMP DEFAULT CURRENT_TIMESTAMP,
updated_at TIMESTAMP DEFAULT CURRENT_TIMESTAMP
)
`);
pgm.sql(`
CREATE TABLE IF NOT EXISTS user_cards (
id SERIAL PRIMARY KEY,
user_id INTEGER REFERENCES users(id) ON DELETE CASCADE,
card_id INTEGER REFERENCES cards(id) ON DELETE CASCADE,
quantity INTEGER DEFAULT 1,
condition VARCHAR(50) DEFAULT 'NM',
is_foil BOOLEAN DEFAULT false,
notes TEXT,
created_at TIMESTAMP DEFAULT CURRENT_TIMESTAMP,
updated_at TIMESTAMP DEFAULT CURRENT_TIMESTAMP,
UNIQUE(user_id, card_id, is_foil)
)
`);
pgm.sql(`
CREATE TABLE IF NOT EXISTS collections (
id SERIAL PRIMARY KEY,
user_id INTEGER REFERENCES users(id) ON DELETE CASCADE,
name VARCHAR(255) NOT NULL,
description TEXT,
is_public BOOLEAN DEFAULT false,
created_at TIMESTAMP DEFAULT CURRENT_TIMESTAMP,
updated_at TIMESTAMP DEFAULT CURRENT_TIMESTAMP
)
`);
pgm.sql(`
CREATE TABLE IF NOT EXISTS collection_cards (
id SERIAL PRIMARY KEY,
collection_id INTEGER REFERENCES collections(id) ON DELETE CASCADE,
card_id INTEGER REFERENCES cards(id) ON DELETE CASCADE,
quantity INTEGER DEFAULT 1,
created_at TIMESTAMP DEFAULT CURRENT_TIMESTAMP,
UNIQUE(collection_id, card_id)
)
`);
pgm.sql(`
CREATE TABLE IF NOT EXISTS decks (
id SERIAL PRIMARY KEY,
user_id INTEGER REFERENCES users(id) ON DELETE CASCADE,
name VARCHAR(255) NOT NULL,
description TEXT,
game VARCHAR(50),
is_public BOOLEAN DEFAULT false,
created_at TIMESTAMP DEFAULT CURRENT_TIMESTAMP,
updated_at TIMESTAMP DEFAULT CURRENT_TIMESTAMP
)
`);
pgm.sql(`
CREATE TABLE IF NOT EXISTS deck_cards (
id SERIAL PRIMARY KEY,
deck_id INTEGER REFERENCES decks(id) ON DELETE CASCADE,
card_id INTEGER REFERENCES cards(id) ON DELETE CASCADE,
quantity INTEGER DEFAULT 1,
created_at TIMESTAMP DEFAULT CURRENT_TIMESTAMP,
UNIQUE(deck_id, card_id)
)
`);
};
/**
* Down-migration is a hard stub. Rolling back the initial schema would drop
* every user, card, collection, and deck row in the database and the
* `pgmigrations` row itself leaving nothing to migrate forward from. If
* you genuinely need a clean schema for testing, branch the Neon database
* (instant + cheap) and run `npm run migrate up` against the branch instead
* of rolling this migration back.
*
* If a future schema correction needs to mutate one of the seven bootstrap
* tables, write a NEW dated migration (`npm run migrate create <name>`)
* with a real `down` do NOT remove this stub.
*
* @returns {void}
*/
export const down = () => {
throw new Error(
'[migration:1779853647564_initial-schema] Refusing to drop the initial schema. ' +
'Rolling back this migration would erase every users / cards / collections / decks row ' +
'in the database. If you need a clean schema for testing, branch the Neon database and ' +
'run `npm run migrate up` against the branch instead. See migrations/1779853647564_initial-schema.js ' +
"down()'s docstring for the long form."
);
};