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>
156 lines
5.5 KiB
JavaScript
156 lines
5.5 KiB
JavaScript
/**
|
|
* 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."
|
|
);
|
|
};
|