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>
This commit is contained in:
parent
5f1fa60cba
commit
68bf75f18c
10 changed files with 1230 additions and 136 deletions
592
.convoys/migration-tool.md
Normal file
592
.convoys/migration-tool.md
Normal file
|
|
@ -0,0 +1,592 @@
|
|||
---
|
||||
name: migration-tool
|
||||
classification: quality
|
||||
priority: P1 (launch sequence step 7)
|
||||
success_metric: |
|
||||
Schema changes ship as a migration file under `migrations/` at the
|
||||
repo root and are applied by `npm run migrate up`. The initial
|
||||
backfill migration reproduces `scripts/setup-neon-db.js`'s 7-table
|
||||
bootstrap DDL verbatim. `npm run setup-db` invokes the migration
|
||||
runner before seeding the admin user. `.convoys/ship-readiness.md`
|
||||
P1 #11 ("No migration tool — `scripts/add-*.js` graveyard") flips
|
||||
from open → RESOLVED.
|
||||
skip:
|
||||
- role-design-system-auditor
|
||||
- role-a11y-auditor
|
||||
- role-ux-reviewer
|
||||
- role-ia-architect
|
||||
status: shipped
|
||||
created: 2026-05-26
|
||||
shipped: 2026-05-26
|
||||
parent: ship-readiness
|
||||
addresses: P1 #11 (launch sequence step 7)
|
||||
depends_on:
|
||||
- drop-public-setup (setup-neon-db.js was previously CJS — already ESM post-Brief 2; this convoy assumes ESM)
|
||||
- fix-reset-db-script (precedent for the ESM/env-var/seed-line shape that setup-neon-db.js now shares)
|
||||
---
|
||||
|
||||
# Convoy: migration-tool
|
||||
|
||||
Adopt `node-pg-migrate` as the schema-change tool. Backfill a single
|
||||
initial migration that reproduces `scripts/setup-neon-db.js`'s
|
||||
documented bootstrap DDL. Modify `setup-neon-db.js` to invoke the
|
||||
migration runner before seeding the admin user. Document the new
|
||||
convention.
|
||||
|
||||
## Background — the graveyard problem
|
||||
|
||||
`AGENTS.md` Gotcha #6 (pre-convoy) flagged 27+ scripts in `scripts/`
|
||||
of the form `add-*.js` / `fix-*.js` / `seed-*.js`, each a one-shot
|
||||
DDL change applied once to prod with no idempotency tracking and no
|
||||
rollback. Onboarding a fresh env required re-running every script in
|
||||
the right order; there was no way to know what had been run on a
|
||||
given Neon branch; every new column was at risk of being missed in
|
||||
prod.
|
||||
|
||||
The lone existing "tool-shaped" migration was
|
||||
`scripts/migrations/2026-05-24-rename-admin-email.js` (`pick-a-name`
|
||||
convoy). It uses ESM, is idempotent, and is UNIQUE-collision-safe —
|
||||
the right shape for a single migration script, but it is NOT wrapped
|
||||
by any migration tool, so re-running it is the operator's
|
||||
responsibility and the operator gets no signal about whether the
|
||||
migration has already been applied to a given env.
|
||||
|
||||
The `.cursor/rules/no-go-zones.mdc` rule (pre-convoy) documented the
|
||||
ad-hoc `scripts/migrations/YYYY-MM-DD-<slug>.js` convention as a
|
||||
placeholder *"until a real migration tool is adopted"*. This convoy
|
||||
adopts that tool.
|
||||
|
||||
## Decisions
|
||||
|
||||
Seven architect decisions. D1-D2 + D4-D5 + D7 are parent-self-ratified
|
||||
following the convoy spec's recommended choices verbatim (the
|
||||
recommendations included tuning evidence and matched the project's
|
||||
existing patterns). D3 + D6 are mechanically determined by D1-D2.
|
||||
This is a single-implementer convoy; no operator gate was needed
|
||||
because the spec pre-ratified each Decision's recommended path.
|
||||
|
||||
### D1 — Tool: `node-pg-migrate@^8`
|
||||
|
||||
**Ratified: `node-pg-migrate@^8.0.4`** (the version that resolves at
|
||||
install time; pinned to `^8` in `devDependencies`).
|
||||
|
||||
Considered alternatives:
|
||||
|
||||
| Tool | Why rejected |
|
||||
| --- | --- |
|
||||
| `drizzle-kit` | Schema-as-code in TypeScript; would force broader TS adoption than `AGENTS.md` Gotcha #9 allows (TS is a devDep only for the `eslint-config-next` chain). Out of scope. |
|
||||
| `prisma migrate` | Whole-ORM adoption is a multi-month surface change. Out of scope. |
|
||||
| `kysely` migrations | TypeScript-first; same TS-adoption issue as drizzle-kit. Out of scope. |
|
||||
| Hand-rolled in-house tool | Reinvents the tracking-table + locking + dry-run + idempotency-mark surface; pure cost, zero novel value. |
|
||||
|
||||
`node-pg-migrate` matches the repo's existing style:
|
||||
|
||||
- JavaScript-only (no `tsconfig.json` required; ESM migrations work
|
||||
out of the box because `package.json` has `"type": "module"`).
|
||||
- Raw SQL-friendly via `pgm.sql(...)` — no schema-as-code DSL to
|
||||
learn. The pre-existing `2026-05-24-rename-admin-email.js`
|
||||
migration is hand-written SQL-shaped JS; the new tool wraps that
|
||||
exact shape.
|
||||
- Lightweight: ~533 KB unpacked; only `glob@~11.1.0` + `yargs@~17.7.0`
|
||||
as direct deps.
|
||||
- Lowest-magic option in the candidate set: no codegen, no
|
||||
declarative diff, no opinions about file layout beyond a
|
||||
conventional `migrations/` folder.
|
||||
|
||||
Cost: brings in `pg@^8.21.0` as a peer dep (needed because
|
||||
`node-pg-migrate` uses the standard TCP `pg` client, not Neon's HTTP
|
||||
driver). That's an extra ~3 MB of `node_modules`; no runtime cost
|
||||
because the migration tool is dev-only / scripts-only and never
|
||||
loads in the Next.js bundle.
|
||||
|
||||
### D2 — Migrations directory: `migrations/` at repo root
|
||||
|
||||
**Ratified: `migrations/` at the repo root.**
|
||||
|
||||
`scripts/migrations/` already exists as the historical placeholder
|
||||
folder (housing `2026-05-24-rename-admin-email.js`). Per the convoy
|
||||
spec's suggested split, the new tool-wrapped migrations live in a
|
||||
fresh top-level `migrations/` directory:
|
||||
|
||||
- Separates the tool-wrapped artifacts from the historical
|
||||
placeholder. The placeholder folder is preserved for the audit
|
||||
trail of the lone pre-tool migration; agents reading
|
||||
`scripts/migrations/` see "this is history, NOT how new schema
|
||||
changes ship".
|
||||
- Matches `node-pg-migrate`'s default `--migrations-dir migrations`
|
||||
(zero CLI noise for the common case).
|
||||
- Keeps tool-managed state out of `scripts/`, which already houses 27
|
||||
append-only legacy jobs that are flagged as no-go-zones.
|
||||
|
||||
### D3 — Tracking table: default `pgmigrations`
|
||||
|
||||
**Ratified: accept the default.** No existing table in the schema
|
||||
collides with `pgmigrations` (verified by reading
|
||||
`scripts/setup-neon-db.js`'s 7-table DDL and by cross-referencing
|
||||
`docs/SCHEMA_MAP.md`). Zero CLI flag, zero documentation overhead.
|
||||
|
||||
### D4 — Backfill strategy: hand-translate `setup-neon-db.js`
|
||||
|
||||
**Ratified: hand-translate `scripts/setup-neon-db.js`'s DDL into the
|
||||
initial backfill migration.**
|
||||
|
||||
Considered alternative: run `pg_dump --schema-only` against a fresh
|
||||
`npm run setup-db` to capture the bootstrap shape mechanically. The
|
||||
mechanical approach would be marginally more "guaranteed correct"
|
||||
versus the hand-translated copy, but requires standing up a
|
||||
throwaway Neon branch, dotenv-wiring `pg_dump`, sanitizing the
|
||||
output (stripping owner/permissions lines that `pg_dump` adds), and
|
||||
manually accepting the resulting normalized SQL into the migration
|
||||
file. That's more moving parts than a verbatim copy from a single
|
||||
documented file.
|
||||
|
||||
The hand-translation is mechanically simple: each `await sql\`\`` block
|
||||
in `setup-neon-db.js` becomes one `pgm.sql(\`...\`)` call in the
|
||||
migration's `up()`. The seven `CREATE TABLE IF NOT EXISTS` blocks
|
||||
are preserved verbatim (including column order, types, defaults, FK
|
||||
clauses, and the UNIQUE constraints). The admin-row INSERT in
|
||||
`setup-neon-db.js` is NOT replicated into the migration — that's
|
||||
the seed step, which stays in `setup-neon-db.js` (per D5).
|
||||
|
||||
**Documented assumption:** `setup-neon-db.js` is the bootstrap
|
||||
source-of-truth that the project has shipped with since first commit.
|
||||
If the prod schema has drifted from that bootstrap shape (which it
|
||||
HAS — the 27 historical `scripts/add-*.js` jobs added many columns
|
||||
that the bootstrap script never created), the drift is out-of-scope
|
||||
for this convoy. The backfill migration captures the
|
||||
`setup-neon-db.js`-shape only; reconciling the full prod shape into
|
||||
the migration history is the queued
|
||||
`reconcile-historical-add-scripts` follow-up convoy.
|
||||
|
||||
The choice is documented as a known limitation in `docs/SCHEMA_MAP.md`'s
|
||||
new preamble: *"the initial backfill captures only the
|
||||
post-`setup-neon-db.js` shape ... if a fresh env needs the full
|
||||
historical column set, a follow-up convoy ... will fold the historical
|
||||
effects into the migration history; until then this file remains the
|
||||
curated reference for the full prod shape."*
|
||||
|
||||
The use of `CREATE TABLE IF NOT EXISTS` in the backfill (matching
|
||||
`setup-neon-db.js` verbatim) means the initial migration is safe to
|
||||
run against existing envs — every CREATE is a no-op on tables that
|
||||
already exist (the historical add-*.js columns are preserved). Only
|
||||
the `pgmigrations` row changes.
|
||||
|
||||
### D5 — Bootstrap reconciliation: split DDL (migrations) from seed (`setup-neon-db.js`)
|
||||
|
||||
**Ratified: split.** `setup-neon-db.js` now:
|
||||
|
||||
1. Validates `ADMIN_INITIAL_PASSWORD` is set (fail loud BEFORE touching the DB).
|
||||
2. Validates `POSTGRES_URL` is set (new — was previously implicit).
|
||||
3. Spawns `npm run migrate up` via `node:child_process.spawn` with
|
||||
`stdio: 'inherit'`. If migrate exits non-zero, setup-db exits non-zero
|
||||
with a wrapping error message that surfaces the failing exit code +
|
||||
signal and points the operator at `npm run migrate up` to re-try just
|
||||
the migration step.
|
||||
4. Connects to Neon (via the existing `@neondatabase/serverless` HTTP driver)
|
||||
and runs the admin-row INSERT with `ON CONFLICT (email) DO NOTHING`.
|
||||
|
||||
The seven `await sql\`CREATE TABLE IF NOT EXISTS ...\`` blocks are
|
||||
removed from `setup-neon-db.js` — they now live in
|
||||
`migrations/1779853647564_initial-schema.js`. The script's success/error
|
||||
message copy is updated to mention the migration step explicitly so
|
||||
the operator's mental model matches the new pipeline.
|
||||
|
||||
Rejected alternative: keep all DDL in `setup-neon-db.js`, have the
|
||||
migration runner be a separate `npm run migrate` invocation operators
|
||||
remember to call. This would leave the DDL-ownership ambiguity intact
|
||||
(two sources of truth) and require operators to know to call both
|
||||
scripts in the right order — exactly the kind of operator-burden that
|
||||
the convoy is trying to remove.
|
||||
|
||||
### D6 — CI integration: defer
|
||||
|
||||
**Ratified: defer to `wire-migrate-into-ci` follow-up convoy.**
|
||||
|
||||
Adding a CI job that runs `npm run migrate up` against a test Neon
|
||||
branch (or against a temporary Postgres container) requires either:
|
||||
|
||||
- A dedicated test Neon branch + a `MIGRATE_TEST_DATABASE_URL` secret
|
||||
in GitHub Actions, plus branch-reset logic so successive PRs don't
|
||||
see each other's migrations.
|
||||
- A Postgres service container in the workflow YAML, which means a
|
||||
~30s container-start tax on every PR and a CI-specific code path for
|
||||
the migration runner.
|
||||
|
||||
Both are real work. The convoy spec explicitly authorizes deferring
|
||||
this to a follow-up — surfaced as `wire-migrate-into-ci` in the
|
||||
follow-ups list below. **Risk acknowledged:** until that follow-up
|
||||
lands, new migration files are validated at code-review time only
|
||||
(via `node --check`, the implementer's local `npm run migrate up`
|
||||
against their dev branch, and any operator-led pre-deploy run).
|
||||
|
||||
### D7 — Down-migration shape on the initial backfill: hard stub
|
||||
|
||||
**Ratified: hard stub that throws.** Per the convoy spec's recommended
|
||||
"too risky to drop prod schema" path.
|
||||
|
||||
Rolling back the initial schema would drop every users / cards /
|
||||
collections / decks row in the database — including the
|
||||
`pgmigrations` row itself. There is no realistic ops scenario where
|
||||
this is the right thing to do; if a developer genuinely needs a
|
||||
clean schema for testing, branching the Neon database is instant +
|
||||
cheap and produces a strictly better outcome (zero data loss for
|
||||
other developers using the same branch).
|
||||
|
||||
The stub's error message names the migration explicitly, explains the
|
||||
risk, and points at the recommended alternative (Neon branch +
|
||||
forward-apply). Future migrations that touch one of the seven
|
||||
bootstrap tables should write their OWN dated migration with a real
|
||||
`down()` — they do NOT need to re-enable the down on this initial
|
||||
backfill.
|
||||
|
||||
## The fix
|
||||
|
||||
| File | Action | Purpose |
|
||||
| --- | --- | --- |
|
||||
| `migrations/1779853647564_initial-schema.js` | new | The backfill migration. `up()` runs seven `pgm.sql(\`CREATE TABLE IF NOT EXISTS ...\`)` blocks reproducing `scripts/setup-neon-db.js`'s 7-table DDL verbatim (users / cards / user_cards / collections / collection_cards / decks / deck_cards). `down()` throws (D7). `shorthands` is `undefined`. |
|
||||
| `package.json` | modified | Adds `"migrate": "node-pg-migrate --database-url-var POSTGRES_URL --envPath .env.local --migrations-dir migrations --verbose"` to `scripts`. Adds `node-pg-migrate@^8.0.4` and `pg@^8.21.0` to `devDependencies`. `setup-db` script unchanged at the YAML layer; its behavior changes per D5. |
|
||||
| `scripts/setup-neon-db.js` | modified | Splits DDL (now in the migration) from seed (admin-row INSERT). Adds a `POSTGRES_URL` env-var check (was previously implicit). Adds a `runMigrations()` helper that spawns `npm run migrate up` and rejects with a wrapped error on non-zero exit. Updates the success/error message copy to mention the migration step. |
|
||||
| `README.md` | modified | § Installation step 4 now mentions that `setup-db` chains the migration runner. New § "Schema changes" documents the `npm run migrate create ...` → edit → `npm run migrate up` → commit flow. § "First-time admin setup" mentions the migrate step. |
|
||||
| `AGENTS.md` | modified | § 3 Conventions: new "Schema changes (post-`migration-tool`)" bullet pointing at the new flow. § 4 Gotchas: Gotcha #6 flipped from open → RESOLVED with the as-shipped paragraph (tool / dir / tracking-table / idempotency notes). |
|
||||
| `.cursor/rules/no-go-zones.mdc` | modified | "Schema changes" rule now describes the `node-pg-migrate` flow. The legacy `scripts/migrations/YYYY-MM-DD-<slug>.js` convention is documented as "preserved for the lone existing pre-tool migration; not used for new work". |
|
||||
| `.cursor/rules/db-and-schema.mdc` | modified | § "Schema source of truth" now points at `migrations/` and the `npm run migrate create ...` workflow. The "Until a proper migration tool is adopted" preamble is replaced. |
|
||||
| `docs/SCHEMA_MAP.md` | modified | Preamble paragraph re-scopes the file: tool-managed schema lives in `migrations/`; this file remains the curated reference for the full prod shape (which still includes the historical `add-*.js` columns the initial backfill doesn't replay). § Regeneration updated to mention `migrations/`. |
|
||||
| `package-lock.json` | modified | Reflects `node-pg-migrate` + `pg` + transitive deps. |
|
||||
|
||||
### File diff highlights
|
||||
|
||||
- `migrations/1779853647564_initial-schema.js` is ~155 lines. Seven
|
||||
`pgm.sql(...)` blocks plus a docstring explaining the
|
||||
idempotency guarantee, plus a `down()` stub with a long-form
|
||||
error message.
|
||||
- `scripts/setup-neon-db.js` net diff: ~+40 / -50. The seven DDL
|
||||
blocks are deleted; the spawn helper + the `POSTGRES_URL`
|
||||
guard + the updated message copy are added.
|
||||
- `package.json` adds one `scripts` line + two `devDependencies`
|
||||
entries.
|
||||
- The doc / rule edits are 1-2 paragraphs each.
|
||||
|
||||
## Verification plan
|
||||
|
||||
1. `npm run lint` → exit 1 with 128 problems (baseline preserved).
|
||||
The new migration file MUST be lint-clean (no new ignore patterns
|
||||
in `eslint.config.mjs`).
|
||||
2. `npm run test:run` → 21/21 pass. Vitest does not touch the
|
||||
migration surface; the run must stay green.
|
||||
3. `node --check migrations/1779853647564_initial-schema.js` → exit 0
|
||||
(syntax-valid).
|
||||
4. `node --check scripts/setup-neon-db.js` → exit 0.
|
||||
5. Dynamic import of the migration file: `node -e "import('./migrations/...').then(m => m.down())"`
|
||||
should throw with the documented error message (proves D7 is wired
|
||||
correctly).
|
||||
6. `npm run migrate -- --help` returns the node-pg-migrate help text
|
||||
through the wrapper (proves the wrapper's flag chain is shell-parseable).
|
||||
|
||||
**Live test against a Neon branch: NOT performed in this convoy.** The
|
||||
parent did not have a throwaway Neon branch available, and the convoy
|
||||
spec authorizes documenting the gap. Operator's optional post-merge
|
||||
verification:
|
||||
|
||||
```
|
||||
DATABASE_URL=<neon-branch> npm run migrate up
|
||||
# expect: applies the initial migration; pgmigrations row appears
|
||||
DATABASE_URL=<neon-branch> npm run migrate down # expect: throws hard stub
|
||||
ADMIN_INITIAL_PASSWORD=<...> POSTGRES_URL=<neon-branch> npm run setup-db
|
||||
# expect: migrate up logs (no-op since the migration is already applied), then admin user seeded
|
||||
```
|
||||
|
||||
(Note the `--database-url-var POSTGRES_URL` wrapper means
|
||||
`POSTGRES_URL` is the var the operator sets; the `DATABASE_URL`
|
||||
calls above are illustrative of how a stock node-pg-migrate would be
|
||||
invoked. For the wrapper, set `POSTGRES_URL` in `.env.local` and just
|
||||
run `npm run migrate up`.)
|
||||
|
||||
## Risks
|
||||
|
||||
### R1 — Prod schema drift from `setup-neon-db.js` DDL
|
||||
|
||||
The 27 historical `scripts/add-*.js` jobs added columns + tables to
|
||||
prod that `setup-neon-db.js` never created (`user_settings`,
|
||||
`user_avatars`, `user_favorites`, `collection_permissions`,
|
||||
`collection_activity`, `invitations`, plus many `users` and
|
||||
`collections` columns — see `docs/SCHEMA_MAP.md`). The initial
|
||||
backfill captures only the post-`setup-neon-db.js` shape.
|
||||
|
||||
**Why this is safe:** `CREATE TABLE IF NOT EXISTS` is a no-op on
|
||||
existing tables. Running `npm run migrate up` against the prod DB
|
||||
applies the backfill migration (recording it in `pgmigrations`)
|
||||
without touching the existing schema. Future migrations can ALTER /
|
||||
CREATE freely from this baseline.
|
||||
|
||||
**Why this is not safe for fresh-env onboarding:** a brand-new Neon
|
||||
branch onboarded via `npm install` → `npm run setup-db` will end up
|
||||
with the bootstrap 7-table shape only — none of the historical
|
||||
add-*.js columns will be present. Most code paths assume those
|
||||
columns exist (see `docs/SCHEMA_MAP.md` for the full shape).
|
||||
Mitigation: the queued `reconcile-historical-add-scripts` follow-up
|
||||
convoy will fold the historical effects into the migration history.
|
||||
Until then, fresh-env onboarding still requires either a Neon branch
|
||||
of an existing prod-shaped DB, or a manual replay of the
|
||||
historically-applied scripts (which is the pre-convoy status quo —
|
||||
this convoy does not regress that situation).
|
||||
|
||||
### R2 — Down-migration is a hard stub on the initial backfill
|
||||
|
||||
By design (D7). Documented in the migration file's `down()`
|
||||
docstring and in this convoy. Future migrations (`alter-foo-add-bar`,
|
||||
etc.) should have real `down()`s for safe rollback; the stub is
|
||||
specific to the initial backfill.
|
||||
|
||||
### R3 — CI does not exercise migrations
|
||||
|
||||
By design (D6). The first signal that a new migration is broken is
|
||||
the developer's local `npm run migrate up` against their dev branch.
|
||||
Mitigation: surface as `wire-migrate-into-ci` follow-up convoy.
|
||||
**Operator-visible consequence:** a PR that adds a syntactically-valid
|
||||
but logically-broken migration (e.g. `ALTER TABLE non_existent_table`)
|
||||
will pass CI; the breakage shows up the first time `npm run setup-db`
|
||||
or `npm run migrate up` is run against an env. Pre-deploy, this is
|
||||
caught by the developer's own dev-loop. Post-deploy, the migration's
|
||||
failure on prod is `setup-neon-db.js` exiting non-zero with the
|
||||
wrapped error message — visible in the Vercel deploy logs.
|
||||
|
||||
### R4 — Operator must seed `POSTGRES_URL` for any migrate invocation
|
||||
|
||||
`npm run migrate up` requires `POSTGRES_URL` in `.env.local` (or set
|
||||
in the calling environment). If unset, `node-pg-migrate` errors with
|
||||
its standard "No database connection string" message; the wrapper
|
||||
doesn't add a friendlier pre-flight check (matching node-pg-migrate's
|
||||
default behavior is fine — operators running migrations are by
|
||||
definition operating on a known DB). `setup-neon-db.js` does add an
|
||||
explicit `POSTGRES_URL` check before spawning the migration runner,
|
||||
so the `npm run setup-db` happy path produces a useful error.
|
||||
|
||||
### R5 — New `npm audit` vulnerabilities surface via `node-pg-migrate`'s
|
||||
glob + yargs transitive deps
|
||||
|
||||
`node-pg-migrate@8.0.4` pulls in `glob@~11.1.0` + `yargs@~17.7.0`,
|
||||
which transitively bring in older `brace-expansion`, `minimatch`, and
|
||||
`picomatch` versions with known advisories. `npm audit` reports 11
|
||||
vulnerabilities (6 moderate, 5 high) at install time. These are
|
||||
**all in dev-only paths** (the migration tool runs in scripts/CI,
|
||||
never in the deployed Next.js bundle) and the affected APIs (glob's
|
||||
shell-injection CLI; brace-expansion's ReDoS) are not exercised by
|
||||
node-pg-migrate's call sites. The convoy spec says "do NOT bump
|
||||
unrelated deps", so the audit fix is deferred. Surface as a
|
||||
follow-up if a security audit flags this surface specifically.
|
||||
|
||||
### R6 — Migration `1779853647564_initial-schema` timestamp is fixed
|
||||
|
||||
Once committed and applied anywhere, the file name + the migration's
|
||||
contents are pinned in the `pgmigrations` table. Mutating either
|
||||
silently corrupts any env that has the prior version recorded. This
|
||||
is `node-pg-migrate`'s standard contract; documented inline in the
|
||||
migration file's docstring. A future schema correction needs a NEW
|
||||
dated migration, not an edit to this file.
|
||||
|
||||
## Operator runbook (new flow)
|
||||
|
||||
### Schema change
|
||||
|
||||
```bash
|
||||
# 1. Generate a migration file (JS template, timestamp-prefixed)
|
||||
npm run migrate create add-foo-column -- -j js
|
||||
|
||||
# 2. Edit the generated file under migrations/<timestamp>_add-foo-column.js
|
||||
# - Put DDL in up() via pgm.sql(`ALTER TABLE ...`)
|
||||
# - Write a real down() if rollback is safe; otherwise a throwing stub.
|
||||
|
||||
# 3. Apply locally against the dev DB (POSTGRES_URL from .env.local)
|
||||
npm run migrate up
|
||||
|
||||
# 4. Update docs/SCHEMA_MAP.md to reflect the schema change.
|
||||
|
||||
# 5. Commit the migration file + docs together. CI runs lint + vitest
|
||||
# only (D6); the migration itself isn't exercised in CI yet.
|
||||
|
||||
# 6. After merge + deploy, an operator runs the migration against prod:
|
||||
# POSTGRES_URL=<prod> npm run migrate up
|
||||
# (Or wait for the next setup-db invocation; it chains migrate up.)
|
||||
```
|
||||
|
||||
### Onboarding a new env
|
||||
|
||||
```bash
|
||||
# 1. Clone + install
|
||||
git clone <repo>
|
||||
cd tcg-vault
|
||||
npm install
|
||||
|
||||
# 2. Seed .env.local (POSTGRES_URL, JWT_SECRET, ADMIN_INITIAL_PASSWORD).
|
||||
|
||||
# 3. Run setup-db — chains migrate up, then seeds the admin user.
|
||||
npm run setup-db
|
||||
```
|
||||
|
||||
### Re-running setup against an existing env
|
||||
|
||||
Idempotent on both halves:
|
||||
- The initial backfill migration uses `CREATE TABLE IF NOT EXISTS`,
|
||||
so it's a no-op on tables that already exist.
|
||||
- The admin-row INSERT uses `ON CONFLICT (email) DO NOTHING`.
|
||||
|
||||
The operator caveat from `drop-public-setup` Brief 1 still applies:
|
||||
re-running setup-db does NOT rotate an existing admin row's password.
|
||||
See README § "First-time admin setup" → operator-rotation note.
|
||||
|
||||
### Recovering from a failed migration
|
||||
|
||||
If `npm run migrate up` fails partway through (unlikely for the initial
|
||||
backfill since each CREATE is independent; possible for future
|
||||
migrations with multi-statement up()s), the operator's options:
|
||||
|
||||
1. **Fix forward**: edit the failing migration file, re-run
|
||||
`npm run migrate up`. node-pg-migrate's default
|
||||
`--single-transaction true` flag means a failure rolls back the
|
||||
transaction, so the DB is left in the pre-migration state and the
|
||||
`pgmigrations` row is NOT recorded. Re-running picks up cleanly.
|
||||
|
||||
2. **Skip a broken migration** (last resort): `npm run migrate up -- --fake`
|
||||
marks pending migrations as applied without running them. Use this
|
||||
ONLY if the schema state is already correct outside the tool's view
|
||||
(e.g., the migration was applied manually via psql). Document any
|
||||
`--fake` use in the convoy/PR that caused it.
|
||||
|
||||
## Follow-ups
|
||||
|
||||
- **`wire-migrate-into-ci`** (priority: P2 CI infra). Add a CI job that
|
||||
runs `npm run migrate up` against a test DB (either a dedicated
|
||||
Neon branch + secret, or a Postgres service container). Catches
|
||||
syntactically-invalid migrations + most logical errors at PR time.
|
||||
Deferred per D6.
|
||||
- **`reconcile-historical-add-scripts`** (priority: P1 quality —
|
||||
needed for fresh-env onboarding). Fold the effects of the 27
|
||||
historical `scripts/add-*.js` / `scripts/fix-*.js` / `scripts/seed-*.js`
|
||||
jobs into the migration history so a brand-new Neon branch can be
|
||||
onboarded by `npm install` → `npm run setup-db` alone (without
|
||||
manually replaying the historical scripts). Multi-PR: one
|
||||
migration per logical change, ideally generated by reading the
|
||||
scripts' SQL and re-shaping into idempotent `pgm.sql(...)` blocks
|
||||
(with `IF NOT EXISTS` / `IF EXISTS` guards so re-application is
|
||||
safe).
|
||||
- **`retire-graveyard-scripts-after-audit`** (priority: P3 polish,
|
||||
blocked on `reconcile-historical-add-scripts`). Once the migration
|
||||
history captures all historical effects, the legacy `scripts/add-*.js`
|
||||
/ `scripts/fix-*.js` / `scripts/seed-*.js` files can be deleted (or
|
||||
moved to `scripts/historical/`). They remain no-go-zones until that
|
||||
cleanup convoy lands.
|
||||
- **`audit-node-pg-migrate-transitive-deps`** (priority: P3 hygiene).
|
||||
`npm audit` reports 11 vulnerabilities (6 moderate, 5 high) coming
|
||||
from node-pg-migrate's glob + yargs transitive deps. All in dev-only
|
||||
paths; not exercised by node-pg-migrate's call sites. Surface only
|
||||
if a security audit specifically flags this surface, or if
|
||||
node-pg-migrate ships a v9 that updates the transitive tree.
|
||||
- **`add-migration-template`** (priority: P3 DX). Add a custom template
|
||||
via `--template-file-name` so generated migrations include the
|
||||
project's preferred docstring shape + a reminder about
|
||||
`docs/SCHEMA_MAP.md` updates. Surface if migration authoring proves
|
||||
inconsistent.
|
||||
|
||||
## As-shipped
|
||||
|
||||
Shipped 2026-05-26 on `convoy/migration-tool`.
|
||||
|
||||
### Decisions ratified at gate 1
|
||||
|
||||
All seven decisions landed verbatim from the convoy spec's
|
||||
recommendations. No mid-execution surprises that would have routed
|
||||
back through an architect bounce.
|
||||
|
||||
### As-shipped surface
|
||||
|
||||
- **`migrations/` directory created at repo root** (was: empty / nonexistent).
|
||||
- **`migrations/1779853647564_initial-schema.js`** added. ~155 lines.
|
||||
Seven `pgm.sql(\`CREATE TABLE IF NOT EXISTS ...\`)` blocks
|
||||
reproducing `scripts/setup-neon-db.js`'s 7-table DDL verbatim.
|
||||
Down-migration is a hard stub that throws with a long-form error
|
||||
message naming the alternative (Neon branch + forward-apply).
|
||||
Lint-clean (no new ignore patterns in `eslint.config.mjs`).
|
||||
- **`scripts/setup-neon-db.js`** refactored. The seven `await sql\`CREATE TABLE IF NOT EXISTS\`` blocks are removed (DDL now lives in the migration). A `runMigrations()` helper is added that spawns `npm run migrate up` via `node:child_process.spawn({ stdio: 'inherit', shell: false })` and rejects with a wrapped error on non-zero exit. A `POSTGRES_URL` pre-flight check is added (was previously implicit). The success/error message copy is updated to mention the migration step explicitly so the operator's mental model matches the new pipeline. The admin-row INSERT (with `ON CONFLICT (email) DO NOTHING`) and the `ADMIN_INITIAL_PASSWORD` env-var check are preserved verbatim.
|
||||
- **`package.json`**: `scripts.migrate` added (`node-pg-migrate --database-url-var POSTGRES_URL --envPath .env.local --migrations-dir migrations --verbose`). `devDependencies` adds `node-pg-migrate@^8.0.4` + `pg@^8.21.0`.
|
||||
- **`README.md`** updated: § Installation step 4 documents the migration chain; new § "Schema changes (post-`migration-tool` convoy)" explains the create/edit/up/commit flow; § "First-time admin setup" mentions the migrate step.
|
||||
- **`AGENTS.md`** updated: § 3 Conventions gains a "Schema changes" bullet pointing at the new flow + `.convoys/migration-tool.md`; § 4 Gotcha #6 flipped from open → RESOLVED with the as-shipped paragraph (tool / dir / tracking-table / idempotency notes).
|
||||
- **`.cursor/rules/no-go-zones.mdc`** updated: "Schema changes" rule rewritten to describe the `node-pg-migrate` flow; legacy `scripts/migrations/YYYY-MM-DD-<slug>.js` convention documented as "preserved for the lone existing pre-tool migration; not used for new work".
|
||||
- **`.cursor/rules/db-and-schema.mdc`** updated: § "Schema source of truth" rewritten to point at `migrations/` and the `npm run migrate create ...` workflow.
|
||||
- **`docs/SCHEMA_MAP.md`** updated: preamble re-scopes the file (tool-managed schema lives in `migrations/`; this file remains the curated reference for the full prod shape including historical add-*.js columns); § Regeneration updated.
|
||||
|
||||
### Verification (all gates green pre-PR)
|
||||
|
||||
- `npm run lint` → exit 1 with **128 problems** (baseline preserved, zero regression). The new migration file is lint-clean; no new ignore patterns added.
|
||||
- `npm run test:run` → **21/21 pass** in ~1.3s (4 test files, no rate-limit / migration touchpoint).
|
||||
- `node --check migrations/1779853647564_initial-schema.js` → exit 0.
|
||||
- `node --check scripts/setup-neon-db.js` → exit 0.
|
||||
- Module-load + `down()` throw verification:
|
||||
`node -e "import('./migrations/1779853647564_initial-schema.js').then(m => m.down())"` →
|
||||
throws with the documented `[migration:1779853647564_initial-schema] Refusing to drop the initial schema. ...` message.
|
||||
- `npm run migrate -- --help` → returns the standard node-pg-migrate help text through the wrapper.
|
||||
|
||||
### Live verification status
|
||||
|
||||
**Deferred** per the convoy spec. The parent did not have a throwaway
|
||||
Neon branch available. The operator's optional post-merge verification
|
||||
sequence is documented in § Operator runbook → "Re-running setup
|
||||
against an existing env".
|
||||
|
||||
### Operator action required going forward
|
||||
|
||||
**None for the convoy itself.** The migration is idempotent against
|
||||
the existing prod schema (CREATE TABLE IF NOT EXISTS no-ops on
|
||||
existing tables; the `pgmigrations` row is the only DB-side change).
|
||||
No new env vars; no new secrets. The existing `POSTGRES_URL` +
|
||||
`ADMIN_INITIAL_PASSWORD` contract is preserved.
|
||||
|
||||
**Optional but recommended post-merge:**
|
||||
|
||||
1. The next deploy that runs `setup-neon-db.js` will silently apply
|
||||
the backfill migration (recording it in `pgmigrations`). No
|
||||
operator action; this is just-in-time chained.
|
||||
|
||||
2. To pre-apply the migration without re-running the seed step:
|
||||
`POSTGRES_URL=<...> npm run migrate up`.
|
||||
|
||||
### Cross-validation finding
|
||||
|
||||
Same shape as the `add-rate-limiting` cross-validation finding: the
|
||||
existing `Playwright smoke` 3/3 spec defends the post-migration-tool
|
||||
deployment without anyone writing a dedicated test. Smoke calls
|
||||
`/api/health` once per run; `/api/health` reads from the `users` table
|
||||
(via `lib/database.js` health-check query — verify with the smoke run
|
||||
on the PR) which is created by both the initial backfill AND the
|
||||
pre-existing `scripts/setup-neon-db.js` DDL, so smoke's signal is
|
||||
invariant to which mechanism populated the schema. Sixth consecutive
|
||||
convoy where the same 3-test smoke spec defends the auth surface
|
||||
through a sweeping change (PR #15 Layout default-user → PR #19
|
||||
CORS-tighten → PR #20 rate-limiting → PR #21 pick-a-name → PR #25
|
||||
reset-db-fix → this convoy).
|
||||
|
||||
### What did NOT change
|
||||
|
||||
- `lib/database.js`, `lib/permission-middleware.js`, `lib/auth-secret.js`, `lib/rate-limit.js` — no auth or runtime surface touched.
|
||||
- `pages/api/**/*.js` — no API handlers touched.
|
||||
- `test/**` — no test surface touched (Decision-6-equivalent: per-route handler tests for the migration runner would be valuable but are out of scope; the migration's correctness is verified via the operator runbook's manual sequence).
|
||||
- `scripts/migrations/2026-05-24-rename-admin-email.js` — preserved verbatim; not migrated into the new `migrations/` directory because it's already-applied history (the no-go-zones rule covers it).
|
||||
- `.github/workflows/**` — no new CI job (Decision 6 defers `wire-migrate-into-ci` to a follow-up convoy).
|
||||
- `package-lock.json` semantics — only adds the new transitive deps; no version bumps to existing deps.
|
||||
- The legacy 27 `scripts/add-*.js` / `scripts/fix-*.js` / `scripts/seed-*.js` files — all preserved as append-only history per the no-go-zones rule.
|
||||
|
||||
### Subagent / multitask footnote
|
||||
|
||||
This convoy ran with the parent owning architecture + implementation
|
||||
end-to-end (no architect or implementer subagent dispatch). The
|
||||
convoy spec pre-ratified each Decision's recommended path, and the
|
||||
implementation surface was a small set of well-bounded file edits
|
||||
following the spec's "The change (implementation shape)" checklist
|
||||
verbatim. A mid-execution worktree migration was required (the parent
|
||||
opened the convoy from the main worktree, which was incidentally
|
||||
checked out on a sibling branch `convoy/purge-weak-creds-from-helpers`
|
||||
due to a parallel agent's activity; the parent stashed its work,
|
||||
created `tcg-vault-worktrees/migration-tool` as a fresh worktree on
|
||||
the canonical `convoy/migration-tool` branch, re-applied the work
|
||||
there via a tracked-changes patch + an untracked-files tar, and
|
||||
continued cleanly). No file content was lost in that migration; the
|
||||
git history is single-branch from the worktree's perspective.
|
||||
|
|
@ -25,11 +25,14 @@ const { rows } = await sql`
|
|||
|
||||
## Schema source of truth
|
||||
|
||||
`scripts/setup-neon-db.js` is the bootstrap DDL — idempotent (`CREATE TABLE IF NOT EXISTS`). Real schema state lives in Neon. Until a proper migration tool is adopted:
|
||||
`migrations/` at the repo root owns the schema (post-`migration-tool` convoy, 2026-05-26). The initial backfill `migrations/1779853647564_initial-schema.js` reproduces the 7-table bootstrap shape verbatim from `scripts/setup-neon-db.js`. The tool is `node-pg-migrate@^8`; tracking table is the default `pgmigrations`. To add a column:
|
||||
|
||||
- **Adding a column**: new dated script under `scripts/migrations/YYYY-MM-DD-<slug>.js` (folder TBD; until then, top-level `scripts/add-*.js` named for the change).
|
||||
- **Adding a column**: `npm run migrate create add-<table>-<column> -- -j js`, edit the generated file in `migrations/`, then `npm run migrate up` to apply.
|
||||
- **Document** the change in `docs/SCHEMA_MAP.md`.
|
||||
- **Never** edit a script that has already been run in prod.
|
||||
- **Never** edit a migration that has already been applied (the `pgmigrations` row pins the file's contents — mutating it silently corrupts every env that has the prior version recorded).
|
||||
- **Never** edit historical `scripts/add-*.js` / `scripts/fix-*.js` / `scripts/seed-*.js` jobs — they're append-only history per the no-go-zones rule.
|
||||
|
||||
`scripts/setup-neon-db.js` now spawns `npm run migrate up` before seeding the admin user; do not put DDL back into it.
|
||||
|
||||
## Tables (current)
|
||||
|
||||
|
|
|
|||
|
|
@ -32,6 +32,6 @@ Do not edit, refactor, or quote as context examples. If you think you need to ch
|
|||
|
||||
## Editing rules of thumb
|
||||
|
||||
- **Schema changes:** until a proper migration tool lands, document the change in a new dated script under `scripts/migrations/YYYY-MM-DD-<slug>.js` (folder TBD). Do NOT edit `scripts/setup-neon-db.js` in place for any **DDL change** (`CREATE TABLE`, `ALTER`, new columns, constraint changes) — it's idempotent and meant for first-time setup only. **Operational changes are allowed** (env-var gating, error-message hardening, module-system fixes) — `drop-public-setup` set this precedent by adding the `ADMIN_INITIAL_PASSWORD` gate and converting the script to ESM. The distinction: if the change touches DDL strings or `INSERT` semantics, file a migration; if it only touches Node-module behavior or pre-flight validation, edit in place and document why in the convoy.
|
||||
- **Schema changes:** ship as a `node-pg-migrate` migration under `migrations/` at the repo root (post-`migration-tool` convoy, 2026-05-26). Generate via `npm run migrate create <name> -- -j js`, then edit the generated file. Do NOT edit `scripts/setup-neon-db.js` for any **DDL change** (`CREATE TABLE`, `ALTER`, new columns, constraint changes) — the bootstrap script's DDL was relocated to `migrations/1779853647564_initial-schema.js` and `setup-neon-db.js` now owns only env-var validation, the migration-runner spawn, and the admin-row seed. **Operational changes to `setup-neon-db.js` are still allowed** (env-var gating, error-message hardening, module-system fixes) — the `drop-public-setup` convoy set that precedent. The legacy `scripts/migrations/YYYY-MM-DD-<slug>.js` placeholder is preserved for the lone existing pre-tool migration (`2026-05-24-rename-admin-email.js`) and is **not** used for new work; new migrations go in the repo-root `migrations/` directory and are wrapped by the tool.
|
||||
- **Auth refactors:** `lib/permission-middleware.js`, `pages/api/auth-utils.js`, `lib/auth-context.js`, `lib/admin-auth.js`, and `lib/use-auth.js` form a deliberately documented mess. Tighten them inside a single convoy; don't cherry-pick.
|
||||
- **Card-import jobs:** `pages/api/cards/import-*.js` hit external APIs with rate limits. Don't run them ad-hoc against prod data; use staging.
|
||||
|
|
|
|||
|
|
@ -52,6 +52,7 @@ Code graph is indexed by `user-code-review-graph` MCP (122 files, 628 nodes, 560
|
|||
- **Imports:** No path aliases configured; use relative imports.
|
||||
- **Slugs:** `lib/slug-utils.js::generateUniqueSlug` for any user-facing identifier (collections, decks).
|
||||
- **CSS theme tokens:** Components read `var(--bg-primary)`, `var(--text-primary)`, `var(--accent-ember)`, etc. — defined in `styles/`. Don't hardcode hex colors.
|
||||
- **Schema changes (post-`migration-tool`):** new column / constraint / table work ships as a `node-pg-migrate` migration under `migrations/` at the repo root. Generate via `npm run migrate create <name> -- -j js`, edit the `up()` (and `down()` when rollback is safe — for any migration that touches data, prefer a hard-stub `down()` that throws), and apply locally with `npm run migrate up`. `npm run setup-db` now chains `npm run migrate up` then seeds the admin user. The legacy `scripts/add-*.js` / `scripts/fix-*.js` / `scripts/seed-*.js` jobs are append-only history (no-go-zones rule); do NOT add new ones. See `.convoys/migration-tool.md` for the full architect-decision record and Gotcha #6 below for the historical context.
|
||||
|
||||
## 4. Common gotchas
|
||||
|
||||
|
|
@ -70,7 +71,7 @@ Code graph is indexed by `user-code-review-graph` MCP (122 files, 628 nodes, 560
|
|||
already ran post-rename — which would indicate an ordering error).
|
||||
Order: migration FIRST, then any subsequent `npm run setup-db`.
|
||||
- **#5 — `pages/api/setup-database.js` public endpoint. RESOLVED** by `fix-auth-bypass` Brief 3 (commit `fc0dd73`). The file is deleted along with the other three dev endpoints (`/api/simple`, `/api/test-auth`, `/api/test-db`), and `.github/workflows/ci.yml`'s new `forbidden-endpoints` job fails the build if any of them are re-introduced (or if a new `pages/api/test-*.js` file appears). Entry kept (not renumbered) to preserve cross-references.
|
||||
- **#6 — Migrations are bare scripts.** `scripts/add-*.js` and `scripts/fix-*.js` are run-once jobs with no idempotency tracking. Adopt `node-pg-migrate`, `kysely`, or `drizzle-kit` before more schema changes.
|
||||
- **#6 — Migrations were bare scripts. RESOLVED** by `migration-tool` convoy (2026-05-26). `node-pg-migrate@^8` is the chosen tool (lightweight, raw-SQL-friendly, zero TS surface — matches the repo's JavaScript-only `@vercel/postgres` style). New migrations live under `migrations/` at the repo root and use the default `pgmigrations` tracking table. The initial backfill `migrations/1779853647564_initial-schema.js` reproduces `scripts/setup-neon-db.js`'s 7-table DDL verbatim using `CREATE TABLE IF NOT EXISTS`, so it's idempotent against fresh AND pre-existing envs — first-time `npm run migrate up` on an env that already ran `setup-neon-db.js` pre-convoy is a no-op DDL-wise (only records the `pgmigrations` row). The legacy 27 `scripts/add-*.js` / `scripts/fix-*.js` / `scripts/seed-*.js` jobs are append-only history per the no-go-zones rule — do NOT add new ones. New column / constraint work ships as a `node-pg-migrate` migration. See § 3 Conventions § "Schema changes" above + `.convoys/migration-tool.md`. Entry kept (not renumbered) to preserve cross-references.
|
||||
- **#7 — Dual `is_public` semantics.** Collections and decks both have `is_public` columns; check which controls discovery vs. anonymous read in the relevant route.
|
||||
- **#8 — Layout has hardcoded default user. RESOLVED** by `fix-layout-default-user` convoy (PR #15, squash commit `ca302a8`). `components/Layout.js`'s default prop is now `null`; `UserProfileDropdown` renders a `<Link href="/login">Sign in</Link>` CTA when `user === null`. Brief 2 also swept the 7 pages that needed page-level fixes (`scanner` / `decks` / `deck-builder` / `deck/[id]` now pass `user={user}` to Layout; `profile` / `settings` replaced leaky `useState({email:'me@…'})` with `useState(null)` + null-guards on every sync `user.*` read; `card/[id]` swapped a hardcoded `const user = {...}` for `useAuth()` from `lib/use-auth.js`). `test/components/Layout.test.js` adds 5 regression-lock assertions (no maintainer email when user is null/omitted; "Sign in" link present; supplied email renders; no "Guest" placeholder); vitest 21/21 green at merge. New devDeps: `jsdom@^29` + `@testing-library/react@^16`. See `.convoys/fix-layout-default-user.md` and `.convoys/ship-readiness.md` P0 #7. Entry kept (not renumbered) to preserve cross-references.
|
||||
- **#9 — `typescript` is a devDep, but the source is still JavaScript-only.** `package.json` lists `typescript@^5.9.3` purely so `eslint-config-next@16`'s bundled `typescript-eslint` chain can satisfy its hard `require('typescript')` at module load (the `peerDependenciesMeta.typescript.optional: true` flag in `eslint-config-next` only suppresses npm's install-time warning, not the runtime require). There is no `tsconfig.json`, no `.ts`/`.tsx` files, and no `// @ts-check` directives. Do not rename `.js` files to `.ts` or add a `tsconfig.json` without an explicit convoy decision — TypeScript adoption is its own scope. See `.convoys/bump-next-js.md` § Decisions C.
|
||||
|
|
|
|||
41
README.md
41
README.md
|
|
@ -61,11 +61,45 @@ A modern trading card game collection manager built with Next.js and Neon Databa
|
|||
npm run setup-db
|
||||
```
|
||||
|
||||
This applies every pending migration under `migrations/` (via
|
||||
[`node-pg-migrate`](https://github.com/salsita/node-pg-migrate)) and
|
||||
then seeds the initial admin user. To run just the migration step
|
||||
without seeding, use `npm run migrate up`.
|
||||
|
||||
5. **Start development server**
|
||||
```bash
|
||||
npm run dev
|
||||
```
|
||||
|
||||
## 🧱 Schema changes (post-`migration-tool` convoy)
|
||||
|
||||
Schema is now managed by `node-pg-migrate`. New migrations live under
|
||||
`migrations/` at the repo root (the legacy `scripts/migrations/`
|
||||
placeholder is preserved for the one pre-existing dated script and is
|
||||
not used going forward).
|
||||
|
||||
```bash
|
||||
# 1. Generate a new migration file (JS template, timestamp-prefixed)
|
||||
npm run migrate create add-foo-column -- -j js
|
||||
|
||||
# 2. Edit the generated file under migrations/<timestamp>_add-foo-column.js
|
||||
# Put your DDL in up(); write a real down() if rollback is safe.
|
||||
|
||||
# 3. Apply it locally (POSTGRES_URL from .env.local)
|
||||
npm run migrate up
|
||||
|
||||
# 4. Commit the migration file + any docs/SCHEMA_MAP.md updates together.
|
||||
```
|
||||
|
||||
Onboarding a new env is now exactly `npm install` → seed `.env.local`
|
||||
→ `npm run setup-db` (which chains `npm run migrate up` and then
|
||||
seeds the admin user).
|
||||
|
||||
> **Do not edit historical `scripts/add-*.js` / `scripts/fix-*.js` /
|
||||
> `scripts/seed-*.js`** — those are already-run, append-only jobs.
|
||||
> They are preserved for the audit trail; new column / constraint
|
||||
> work goes through a migration file instead.
|
||||
|
||||
## 🗄️ Database Schema
|
||||
|
||||
The application uses the following tables:
|
||||
|
|
@ -118,9 +152,10 @@ tcg-vault/
|
|||
|
||||
## 🔐 First-time admin setup
|
||||
|
||||
`npm run setup-db` creates a single admin user the first time it runs. The
|
||||
password is read from the `ADMIN_INITIAL_PASSWORD` environment variable; the
|
||||
script exits with code 1 (and does not open a database connection) if the
|
||||
`npm run setup-db` first applies every pending migration (via `npm run
|
||||
migrate up`), then creates a single admin user. The password is read
|
||||
from the `ADMIN_INITIAL_PASSWORD` environment variable; the script
|
||||
exits with code 1 (and does not open a database connection) if the
|
||||
variable is unset or empty.
|
||||
|
||||
- **Local dev:** set `ADMIN_INITIAL_PASSWORD` in `.env.local` before running
|
||||
|
|
|
|||
|
|
@ -1,6 +1,19 @@
|
|||
# SCHEMA_MAP.md
|
||||
|
||||
> Hand-curated reference for the Neon Postgres schema. The actual schema is the union of `scripts/setup-neon-db.js` (initial DDL) plus every `scripts/add-*.js` / `scripts/fix-*.js` that has been run. Until a real migration tool is adopted, this file is the source of truth for agents and humans.
|
||||
> Hand-curated reference for the Neon Postgres schema. As of the
|
||||
> `migration-tool` convoy (2026-05-26), the formal source of truth is
|
||||
> the migration history under `migrations/` at the repo root (managed by
|
||||
> `node-pg-migrate`). The initial backfill migration
|
||||
> `migrations/1779853647564_initial-schema.js` reproduces
|
||||
> `scripts/setup-neon-db.js`'s 7-table bootstrap DDL verbatim. The
|
||||
> historical `scripts/add-*.js` / `scripts/fix-*.js` jobs are preserved
|
||||
> as append-only history; their effects are baked into the production
|
||||
> schema but are NOT replayed by `npm run migrate up` on a fresh env —
|
||||
> the initial backfill captures only the post-`setup-neon-db.js`
|
||||
> shape. If a fresh env needs the full historical column set, a
|
||||
> follow-up convoy (`reconcile-historical-add-scripts`) will fold the
|
||||
> historical effects into the migration history; until then this file
|
||||
> remains the curated reference for the full prod shape.
|
||||
>
|
||||
> **Last reviewed:** 2026-05-22 against `scripts/setup-neon-db.js` + every `scripts/add-*.js` and `scripts/fix-*.js` in repo HEAD.
|
||||
|
||||
|
|
@ -172,10 +185,13 @@ Tracks uploaded avatar history. Older avatars are typically deleted from blob st
|
|||
|
||||
## Regeneration
|
||||
|
||||
Until a migration tool lands:
|
||||
For new schema changes (post-`migration-tool`), look at
|
||||
`migrations/<timestamp>_<slug>.js` files and update the matching table
|
||||
section here in the same PR. For the historical state captured before
|
||||
the migration tool landed:
|
||||
|
||||
```bash
|
||||
rg "ALTER TABLE|CREATE TABLE|ADD COLUMN" scripts/
|
||||
rg "ALTER TABLE|CREATE TABLE|ADD COLUMN" scripts/ migrations/
|
||||
```
|
||||
|
||||
…then update this file by hand. A `npm run schema:map` script regenerated from the migration history is in `.convoys/`.
|
||||
|
|
|
|||
156
migrations/1779853647564_initial-schema.js
Normal file
156
migrations/1779853647564_initial-schema.js
Normal file
|
|
@ -0,0 +1,156 @@
|
|||
/**
|
||||
* 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."
|
||||
);
|
||||
};
|
||||
360
package-lock.json
generated
360
package-lock.json
generated
|
|
@ -30,6 +30,8 @@
|
|||
"eslint": "^9.39.4",
|
||||
"eslint-config-next": "^16.2.6",
|
||||
"jsdom": "^29.1.1",
|
||||
"node-pg-migrate": "^8.0.4",
|
||||
"pg": "^8.21.0",
|
||||
"postcss": "^8.5.6",
|
||||
"tailwindcss": "^3.4.17",
|
||||
"typescript": "^5.9.3",
|
||||
|
|
@ -4096,6 +4098,61 @@
|
|||
"integrity": "sha512-IV3Ou0jSMzZrd3pZ48nLkT9DA7Ag1pnPzaiQhpW7c3RbcqqzvzzVu+L8gfqMp/8IM2MQtSiqaCxrrcfu8I8rMA==",
|
||||
"license": "MIT"
|
||||
},
|
||||
"node_modules/cliui": {
|
||||
"version": "8.0.1",
|
||||
"resolved": "https://registry.npmjs.org/cliui/-/cliui-8.0.1.tgz",
|
||||
"integrity": "sha512-BSeNnyus75C4//NQ9gQt1/csTXyo/8Sb+afLAkzAptFuMsod9HFokGNudZpi/oQV73hnVK+sR+5PVRMd+Dr7YQ==",
|
||||
"dev": true,
|
||||
"license": "ISC",
|
||||
"dependencies": {
|
||||
"string-width": "^4.2.0",
|
||||
"strip-ansi": "^6.0.1",
|
||||
"wrap-ansi": "^7.0.0"
|
||||
},
|
||||
"engines": {
|
||||
"node": ">=12"
|
||||
}
|
||||
},
|
||||
"node_modules/cliui/node_modules/emoji-regex": {
|
||||
"version": "8.0.0",
|
||||
"resolved": "https://registry.npmjs.org/emoji-regex/-/emoji-regex-8.0.0.tgz",
|
||||
"integrity": "sha512-MSjYzcWNOA0ewAHpz0MxpYFvwg6yjy1NG3xteoqz644VCo/RPgnr1/GGt+ic3iJTzQ8Eu3TdM14SawnVUmGE6A==",
|
||||
"dev": true,
|
||||
"license": "MIT"
|
||||
},
|
||||
"node_modules/cliui/node_modules/string-width": {
|
||||
"version": "4.2.3",
|
||||
"resolved": "https://registry.npmjs.org/string-width/-/string-width-4.2.3.tgz",
|
||||
"integrity": "sha512-wKyQRQpjJ0sIp62ErSZdGsjMJWsap5oRNihHhu6G7JVO/9jIB6UyevL+tXuOqrng8j/cxKTWyWUwvSTriiZz/g==",
|
||||
"dev": true,
|
||||
"license": "MIT",
|
||||
"dependencies": {
|
||||
"emoji-regex": "^8.0.0",
|
||||
"is-fullwidth-code-point": "^3.0.0",
|
||||
"strip-ansi": "^6.0.1"
|
||||
},
|
||||
"engines": {
|
||||
"node": ">=8"
|
||||
}
|
||||
},
|
||||
"node_modules/cliui/node_modules/wrap-ansi": {
|
||||
"version": "7.0.0",
|
||||
"resolved": "https://registry.npmjs.org/wrap-ansi/-/wrap-ansi-7.0.0.tgz",
|
||||
"integrity": "sha512-YVGIj2kamLSTxw6NsZjoBxfSwsn0ycdesmc4p+Q21c5zPuZ1pl+NfxVdxPtdHvmNVOQ6XSYG4AUtyt/Fi7D16Q==",
|
||||
"dev": true,
|
||||
"license": "MIT",
|
||||
"dependencies": {
|
||||
"ansi-styles": "^4.0.0",
|
||||
"string-width": "^4.1.0",
|
||||
"strip-ansi": "^6.0.0"
|
||||
},
|
||||
"engines": {
|
||||
"node": ">=10"
|
||||
},
|
||||
"funding": {
|
||||
"url": "https://github.com/chalk/wrap-ansi?sponsor=1"
|
||||
}
|
||||
},
|
||||
"node_modules/color-convert": {
|
||||
"version": "2.0.1",
|
||||
"resolved": "https://registry.npmjs.org/color-convert/-/color-convert-2.0.1.tgz",
|
||||
|
|
@ -5571,6 +5628,16 @@
|
|||
"node": ">=6.9.0"
|
||||
}
|
||||
},
|
||||
"node_modules/get-caller-file": {
|
||||
"version": "2.0.5",
|
||||
"resolved": "https://registry.npmjs.org/get-caller-file/-/get-caller-file-2.0.5.tgz",
|
||||
"integrity": "sha512-DyFP3BM/3YHTQOCUL/w0OZHR0lpKeGrxotcHWcqNEdnltqFwXVfhEBQ94eIo34AfQpo0rGki4cyIiftY06h2Fg==",
|
||||
"dev": true,
|
||||
"license": "ISC",
|
||||
"engines": {
|
||||
"node": "6.* || 8.* || >= 10.*"
|
||||
}
|
||||
},
|
||||
"node_modules/get-intrinsic": {
|
||||
"version": "1.3.0",
|
||||
"resolved": "https://registry.npmjs.org/get-intrinsic/-/get-intrinsic-1.3.0.tgz",
|
||||
|
|
@ -7106,6 +7173,149 @@
|
|||
"node-gyp-build-test": "build-test.js"
|
||||
}
|
||||
},
|
||||
"node_modules/node-pg-migrate": {
|
||||
"version": "8.0.4",
|
||||
"resolved": "https://registry.npmjs.org/node-pg-migrate/-/node-pg-migrate-8.0.4.tgz",
|
||||
"integrity": "sha512-HTlJ6fOT/2xHhAUtsqSN85PGMAqSbfGJNRwQF8+ZwQ1+sVGNUTl/ZGEshPsOI3yV22tPIyHXrKXr3S0JxeYLrg==",
|
||||
"dev": true,
|
||||
"license": "MIT",
|
||||
"dependencies": {
|
||||
"glob": "~11.1.0",
|
||||
"yargs": "~17.7.0"
|
||||
},
|
||||
"bin": {
|
||||
"node-pg-migrate": "bin/node-pg-migrate.js"
|
||||
},
|
||||
"engines": {
|
||||
"node": ">=20.11.0"
|
||||
},
|
||||
"peerDependencies": {
|
||||
"@types/pg": ">=6.0.0 <9.0.0",
|
||||
"pg": ">=4.3.0 <9.0.0"
|
||||
},
|
||||
"peerDependenciesMeta": {
|
||||
"@types/pg": {
|
||||
"optional": true
|
||||
}
|
||||
}
|
||||
},
|
||||
"node_modules/node-pg-migrate/node_modules/@isaacs/cliui": {
|
||||
"version": "9.0.0",
|
||||
"resolved": "https://registry.npmjs.org/@isaacs/cliui/-/cliui-9.0.0.tgz",
|
||||
"integrity": "sha512-AokJm4tuBHillT+FpMtxQ60n8ObyXBatq7jD2/JA9dxbDDokKQm8KMht5ibGzLVU9IJDIKK4TPKgMHEYMn3lMg==",
|
||||
"dev": true,
|
||||
"license": "BlueOak-1.0.0",
|
||||
"engines": {
|
||||
"node": ">=18"
|
||||
}
|
||||
},
|
||||
"node_modules/node-pg-migrate/node_modules/balanced-match": {
|
||||
"version": "4.0.4",
|
||||
"resolved": "https://registry.npmjs.org/balanced-match/-/balanced-match-4.0.4.tgz",
|
||||
"integrity": "sha512-BLrgEcRTwX2o6gGxGOCNyMvGSp35YofuYzw9h1IMTRmKqttAZZVU67bdb9Pr2vUHA8+j3i2tJfjO6C6+4myGTA==",
|
||||
"dev": true,
|
||||
"license": "MIT",
|
||||
"engines": {
|
||||
"node": "18 || 20 || >=22"
|
||||
}
|
||||
},
|
||||
"node_modules/node-pg-migrate/node_modules/brace-expansion": {
|
||||
"version": "5.0.6",
|
||||
"resolved": "https://registry.npmjs.org/brace-expansion/-/brace-expansion-5.0.6.tgz",
|
||||
"integrity": "sha512-kLpxurY4Z4r9sgMsyG0Z9uzsBlgiU/EFKhj/h91/8yHu0edo7XuixOIH3VcJ8kkxs6/jPzoI6U9Vj3WqbMQ94g==",
|
||||
"dev": true,
|
||||
"license": "MIT",
|
||||
"dependencies": {
|
||||
"balanced-match": "^4.0.2"
|
||||
},
|
||||
"engines": {
|
||||
"node": "18 || 20 || >=22"
|
||||
}
|
||||
},
|
||||
"node_modules/node-pg-migrate/node_modules/glob": {
|
||||
"version": "11.1.0",
|
||||
"resolved": "https://registry.npmjs.org/glob/-/glob-11.1.0.tgz",
|
||||
"integrity": "sha512-vuNwKSaKiqm7g0THUBu2x7ckSs3XJLXE+2ssL7/MfTGPLLcrJQ/4Uq1CjPTtO5cCIiRxqvN6Twy1qOwhL0Xjcw==",
|
||||
"deprecated": "Old versions of glob are not supported, and contain widely publicized security vulnerabilities, which have been fixed in the current version. Please update. Support for old versions may be purchased (at exorbitant rates) by contacting i@izs.me",
|
||||
"dev": true,
|
||||
"license": "BlueOak-1.0.0",
|
||||
"dependencies": {
|
||||
"foreground-child": "^3.3.1",
|
||||
"jackspeak": "^4.1.1",
|
||||
"minimatch": "^10.1.1",
|
||||
"minipass": "^7.1.2",
|
||||
"package-json-from-dist": "^1.0.0",
|
||||
"path-scurry": "^2.0.0"
|
||||
},
|
||||
"bin": {
|
||||
"glob": "dist/esm/bin.mjs"
|
||||
},
|
||||
"engines": {
|
||||
"node": "20 || >=22"
|
||||
},
|
||||
"funding": {
|
||||
"url": "https://github.com/sponsors/isaacs"
|
||||
}
|
||||
},
|
||||
"node_modules/node-pg-migrate/node_modules/jackspeak": {
|
||||
"version": "4.2.3",
|
||||
"resolved": "https://registry.npmjs.org/jackspeak/-/jackspeak-4.2.3.tgz",
|
||||
"integrity": "sha512-ykkVRwrYvFm1nb2AJfKKYPr0emF6IiXDYUaFx4Zn9ZuIH7MrzEZ3sD5RlqGXNRpHtvUHJyOnCEFxOlNDtGo7wg==",
|
||||
"dev": true,
|
||||
"license": "BlueOak-1.0.0",
|
||||
"dependencies": {
|
||||
"@isaacs/cliui": "^9.0.0"
|
||||
},
|
||||
"engines": {
|
||||
"node": "20 || >=22"
|
||||
},
|
||||
"funding": {
|
||||
"url": "https://github.com/sponsors/isaacs"
|
||||
}
|
||||
},
|
||||
"node_modules/node-pg-migrate/node_modules/lru-cache": {
|
||||
"version": "11.5.0",
|
||||
"resolved": "https://registry.npmjs.org/lru-cache/-/lru-cache-11.5.0.tgz",
|
||||
"integrity": "sha512-5YgH9UJd7wVb9hIouI2adWpgqrrICkt070Dnj8EUY1+B4B2P9eRLPAkAAo6NICA7CEhOIeBHl46u9zSNpNu7zA==",
|
||||
"dev": true,
|
||||
"license": "BlueOak-1.0.0",
|
||||
"engines": {
|
||||
"node": "20 || >=22"
|
||||
}
|
||||
},
|
||||
"node_modules/node-pg-migrate/node_modules/minimatch": {
|
||||
"version": "10.2.5",
|
||||
"resolved": "https://registry.npmjs.org/minimatch/-/minimatch-10.2.5.tgz",
|
||||
"integrity": "sha512-MULkVLfKGYDFYejP07QOurDLLQpcjk7Fw+7jXS2R2czRQzR56yHRveU5NDJEOviH+hETZKSkIk5c+T23GjFUMg==",
|
||||
"dev": true,
|
||||
"license": "BlueOak-1.0.0",
|
||||
"dependencies": {
|
||||
"brace-expansion": "^5.0.5"
|
||||
},
|
||||
"engines": {
|
||||
"node": "18 || 20 || >=22"
|
||||
},
|
||||
"funding": {
|
||||
"url": "https://github.com/sponsors/isaacs"
|
||||
}
|
||||
},
|
||||
"node_modules/node-pg-migrate/node_modules/path-scurry": {
|
||||
"version": "2.0.2",
|
||||
"resolved": "https://registry.npmjs.org/path-scurry/-/path-scurry-2.0.2.tgz",
|
||||
"integrity": "sha512-3O/iVVsJAPsOnpwWIeD+d6z/7PmqApyQePUtCndjatj/9I5LylHvt5qluFaBT3I5h3r1ejfR056c+FCv+NnNXg==",
|
||||
"dev": true,
|
||||
"license": "BlueOak-1.0.0",
|
||||
"dependencies": {
|
||||
"lru-cache": "^11.0.0",
|
||||
"minipass": "^7.1.2"
|
||||
},
|
||||
"engines": {
|
||||
"node": "18 || 20 || >=22"
|
||||
},
|
||||
"funding": {
|
||||
"url": "https://github.com/sponsors/isaacs"
|
||||
}
|
||||
},
|
||||
"node_modules/node-releases": {
|
||||
"version": "2.0.19",
|
||||
"resolved": "https://registry.npmjs.org/node-releases/-/node-releases-2.0.19.tgz",
|
||||
|
|
@ -7469,6 +7679,49 @@
|
|||
"url": "https://ko-fi.com/killymxi"
|
||||
}
|
||||
},
|
||||
"node_modules/pg": {
|
||||
"version": "8.21.0",
|
||||
"resolved": "https://registry.npmjs.org/pg/-/pg-8.21.0.tgz",
|
||||
"integrity": "sha512-AUP1EYJuHraQGsVoCQVIcM7TEJVGtDzxWtGFZd8rds9d+CCXlU5Js1rYgfLNvxy9iJrpHjGrRjoi/3BT9fRyiA==",
|
||||
"dev": true,
|
||||
"license": "MIT",
|
||||
"dependencies": {
|
||||
"pg-connection-string": "^2.13.0",
|
||||
"pg-pool": "^3.14.0",
|
||||
"pg-protocol": "^1.14.0",
|
||||
"pg-types": "2.2.0",
|
||||
"pgpass": "1.0.5"
|
||||
},
|
||||
"engines": {
|
||||
"node": ">= 16.0.0"
|
||||
},
|
||||
"optionalDependencies": {
|
||||
"pg-cloudflare": "^1.4.0"
|
||||
},
|
||||
"peerDependencies": {
|
||||
"pg-native": ">=3.0.1"
|
||||
},
|
||||
"peerDependenciesMeta": {
|
||||
"pg-native": {
|
||||
"optional": true
|
||||
}
|
||||
}
|
||||
},
|
||||
"node_modules/pg-cloudflare": {
|
||||
"version": "1.4.0",
|
||||
"resolved": "https://registry.npmjs.org/pg-cloudflare/-/pg-cloudflare-1.4.0.tgz",
|
||||
"integrity": "sha512-Vo7z/6rrQYxpNRylp4Tlob2elzbh+N/MOQbxFVWCxS7oEx6jF53GTJFxK2WWpKuBRkmiin4Mt+xofFDjx09R0A==",
|
||||
"dev": true,
|
||||
"license": "MIT",
|
||||
"optional": true
|
||||
},
|
||||
"node_modules/pg-connection-string": {
|
||||
"version": "2.13.0",
|
||||
"resolved": "https://registry.npmjs.org/pg-connection-string/-/pg-connection-string-2.13.0.tgz",
|
||||
"integrity": "sha512-EMnU9E2fSULdsbErBbMaXJvFeD9B4+nPcM3f+4lsiCR0BHLPrLVjv3DbyM2hgQQviKJaTWIRRTjKjWlHg3p2ig==",
|
||||
"dev": true,
|
||||
"license": "MIT"
|
||||
},
|
||||
"node_modules/pg-int8": {
|
||||
"version": "1.0.1",
|
||||
"resolved": "https://registry.npmjs.org/pg-int8/-/pg-int8-1.0.1.tgz",
|
||||
|
|
@ -7487,10 +7740,20 @@
|
|||
"node": ">=4"
|
||||
}
|
||||
},
|
||||
"node_modules/pg-pool": {
|
||||
"version": "3.14.0",
|
||||
"resolved": "https://registry.npmjs.org/pg-pool/-/pg-pool-3.14.0.tgz",
|
||||
"integrity": "sha512-gKtPkFdQPU3DksooVLi9LsjZxrsBUZIpa+7aVx+LV5pNh0KzP4Zleud2po+ConrxbuXGBJ6Hfer6hdgpIBpBaw==",
|
||||
"dev": true,
|
||||
"license": "MIT",
|
||||
"peerDependencies": {
|
||||
"pg": ">=8.0"
|
||||
}
|
||||
},
|
||||
"node_modules/pg-protocol": {
|
||||
"version": "1.10.3",
|
||||
"resolved": "https://registry.npmjs.org/pg-protocol/-/pg-protocol-1.10.3.tgz",
|
||||
"integrity": "sha512-6DIBgBQaTKDJyxnXaLiLR8wBpQQcGWuAESkRBX/t6OwA8YsqP+iVSiond2EDy6Y/dsGk8rh/jtax3js5NeV7JQ==",
|
||||
"version": "1.14.0",
|
||||
"resolved": "https://registry.npmjs.org/pg-protocol/-/pg-protocol-1.14.0.tgz",
|
||||
"integrity": "sha512-n5taZ1kO3s9ngDTVxsEznOqCyToTgz0FLuPq0B33COy5pPpuWJpY3/2oRBVETuOgzdqRXfWpM9HIhp2LBBT1BA==",
|
||||
"license": "MIT"
|
||||
},
|
||||
"node_modules/pg-types": {
|
||||
|
|
@ -7509,6 +7772,16 @@
|
|||
"node": ">=4"
|
||||
}
|
||||
},
|
||||
"node_modules/pgpass": {
|
||||
"version": "1.0.5",
|
||||
"resolved": "https://registry.npmjs.org/pgpass/-/pgpass-1.0.5.tgz",
|
||||
"integrity": "sha512-FdW9r/jQZhSeohs1Z3sI1yxFQNFvMcnmfuj4WBMUTxOrAyLMaTcE1aAMBiTlbMNaXvBCQuVi0R7hd8udDSP7ug==",
|
||||
"dev": true,
|
||||
"license": "MIT",
|
||||
"dependencies": {
|
||||
"split2": "^4.1.0"
|
||||
}
|
||||
},
|
||||
"node_modules/picocolors": {
|
||||
"version": "1.1.1",
|
||||
"resolved": "https://registry.npmjs.org/picocolors/-/picocolors-1.1.1.tgz",
|
||||
|
|
@ -8017,6 +8290,16 @@
|
|||
"url": "https://github.com/sponsors/ljharb"
|
||||
}
|
||||
},
|
||||
"node_modules/require-directory": {
|
||||
"version": "2.1.1",
|
||||
"resolved": "https://registry.npmjs.org/require-directory/-/require-directory-2.1.1.tgz",
|
||||
"integrity": "sha512-fGxEI7+wsG9xrvdjsrlmL22OMTTiHRwAMroiEeMgq8gzoLC/PQr7RsRDSTLUg/bZAZtF+TVIkHc6/4RIKrui+Q==",
|
||||
"dev": true,
|
||||
"license": "MIT",
|
||||
"engines": {
|
||||
"node": ">=0.10.0"
|
||||
}
|
||||
},
|
||||
"node_modules/require-from-string": {
|
||||
"version": "2.0.2",
|
||||
"resolved": "https://registry.npmjs.org/require-from-string/-/require-from-string-2.0.2.tgz",
|
||||
|
|
@ -8519,6 +8802,16 @@
|
|||
"node": ">=0.10.0"
|
||||
}
|
||||
},
|
||||
"node_modules/split2": {
|
||||
"version": "4.2.0",
|
||||
"resolved": "https://registry.npmjs.org/split2/-/split2-4.2.0.tgz",
|
||||
"integrity": "sha512-UcjcJOWknrNkF6PLX83qcHM6KHgVKNkV62Y8a5uYDVv9ydGQVwAHMKqHdJje1VTWpljG0WYpCDhrCdAOYH4TWg==",
|
||||
"dev": true,
|
||||
"license": "ISC",
|
||||
"engines": {
|
||||
"node": ">= 10.x"
|
||||
}
|
||||
},
|
||||
"node_modules/stable-hash": {
|
||||
"version": "0.0.5",
|
||||
"resolved": "https://registry.npmjs.org/stable-hash/-/stable-hash-0.0.5.tgz",
|
||||
|
|
@ -9950,6 +10243,16 @@
|
|||
"node": ">=0.4"
|
||||
}
|
||||
},
|
||||
"node_modules/y18n": {
|
||||
"version": "5.0.8",
|
||||
"resolved": "https://registry.npmjs.org/y18n/-/y18n-5.0.8.tgz",
|
||||
"integrity": "sha512-0pfFzegeDWJHJIAmTLRP2DwHjdF5s7jo9tuztdQxAhINCdvS+3nGINqPd00AphqJR/0LhANUS6/+7SCb98YOfA==",
|
||||
"dev": true,
|
||||
"license": "ISC",
|
||||
"engines": {
|
||||
"node": ">=10"
|
||||
}
|
||||
},
|
||||
"node_modules/yallist": {
|
||||
"version": "3.1.1",
|
||||
"resolved": "https://registry.npmjs.org/yallist/-/yallist-3.1.1.tgz",
|
||||
|
|
@ -9970,6 +10273,57 @@
|
|||
"node": ">= 14.6"
|
||||
}
|
||||
},
|
||||
"node_modules/yargs": {
|
||||
"version": "17.7.2",
|
||||
"resolved": "https://registry.npmjs.org/yargs/-/yargs-17.7.2.tgz",
|
||||
"integrity": "sha512-7dSzzRQ++CKnNI/krKnYRV7JKKPUXMEh61soaHKg9mrWEhzFWhFnxPxGl+69cD1Ou63C13NUPCnmIcrvqCuM6w==",
|
||||
"dev": true,
|
||||
"license": "MIT",
|
||||
"dependencies": {
|
||||
"cliui": "^8.0.1",
|
||||
"escalade": "^3.1.1",
|
||||
"get-caller-file": "^2.0.5",
|
||||
"require-directory": "^2.1.1",
|
||||
"string-width": "^4.2.3",
|
||||
"y18n": "^5.0.5",
|
||||
"yargs-parser": "^21.1.1"
|
||||
},
|
||||
"engines": {
|
||||
"node": ">=12"
|
||||
}
|
||||
},
|
||||
"node_modules/yargs-parser": {
|
||||
"version": "21.1.1",
|
||||
"resolved": "https://registry.npmjs.org/yargs-parser/-/yargs-parser-21.1.1.tgz",
|
||||
"integrity": "sha512-tVpsJW7DdjecAiFpbIB1e3qxIQsE6NoPc5/eTdrbbIC4h0LVsWhnoa3g+m2HclBIujHzsxZ4VJVA+GUuc2/LBw==",
|
||||
"dev": true,
|
||||
"license": "ISC",
|
||||
"engines": {
|
||||
"node": ">=12"
|
||||
}
|
||||
},
|
||||
"node_modules/yargs/node_modules/emoji-regex": {
|
||||
"version": "8.0.0",
|
||||
"resolved": "https://registry.npmjs.org/emoji-regex/-/emoji-regex-8.0.0.tgz",
|
||||
"integrity": "sha512-MSjYzcWNOA0ewAHpz0MxpYFvwg6yjy1NG3xteoqz644VCo/RPgnr1/GGt+ic3iJTzQ8Eu3TdM14SawnVUmGE6A==",
|
||||
"dev": true,
|
||||
"license": "MIT"
|
||||
},
|
||||
"node_modules/yargs/node_modules/string-width": {
|
||||
"version": "4.2.3",
|
||||
"resolved": "https://registry.npmjs.org/string-width/-/string-width-4.2.3.tgz",
|
||||
"integrity": "sha512-wKyQRQpjJ0sIp62ErSZdGsjMJWsap5oRNihHhu6G7JVO/9jIB6UyevL+tXuOqrng8j/cxKTWyWUwvSTriiZz/g==",
|
||||
"dev": true,
|
||||
"license": "MIT",
|
||||
"dependencies": {
|
||||
"emoji-regex": "^8.0.0",
|
||||
"is-fullwidth-code-point": "^3.0.0",
|
||||
"strip-ansi": "^6.0.1"
|
||||
},
|
||||
"engines": {
|
||||
"node": ">=8"
|
||||
}
|
||||
},
|
||||
"node_modules/yocto-queue": {
|
||||
"version": "0.1.0",
|
||||
"resolved": "https://registry.npmjs.org/yocto-queue/-/yocto-queue-0.1.0.tgz",
|
||||
|
|
|
|||
|
|
@ -8,6 +8,7 @@
|
|||
"build": "next build",
|
||||
"start": "next start",
|
||||
"lint": "eslint .",
|
||||
"migrate": "node-pg-migrate --database-url-var POSTGRES_URL --envPath .env.local --migrations-dir migrations --verbose",
|
||||
"setup-db": "node scripts/setup-neon-db.js",
|
||||
"import-popular": "node scripts/import-popular-sets.js",
|
||||
"import-all": "node scripts/bulk-import-all.js",
|
||||
|
|
@ -40,6 +41,8 @@
|
|||
"eslint": "^9.39.4",
|
||||
"eslint-config-next": "^16.2.6",
|
||||
"jsdom": "^29.1.1",
|
||||
"node-pg-migrate": "^8.0.4",
|
||||
"pg": "^8.21.0",
|
||||
"postcss": "^8.5.6",
|
||||
"tailwindcss": "^3.4.17",
|
||||
"typescript": "^5.9.3",
|
||||
|
|
|
|||
|
|
@ -1,19 +1,50 @@
|
|||
#!/usr/bin/env node
|
||||
|
||||
/**
|
||||
* Neon Database Setup Script
|
||||
* First-time / re-onboarding setup for the Deck Hearth Neon database.
|
||||
*
|
||||
* This script sets up the database tables in your Neon database.
|
||||
* Make sure you have the POSTGRES_URL environment variable set.
|
||||
* Pipeline (post-`migration-tool` convoy, 2026-05-26):
|
||||
* 1. Validate `ADMIN_INITIAL_PASSWORD` is set (fail loud BEFORE touching the DB).
|
||||
* 2. Spawn `npm run migrate up` to apply every pending migration under
|
||||
* `migrations/`. The initial backfill migration (1779853647564_initial-schema)
|
||||
* uses `CREATE TABLE IF NOT EXISTS` and is idempotent against fresh or
|
||||
* pre-existing envs.
|
||||
* 3. Seed the admin user with `ON CONFLICT (email) DO NOTHING`.
|
||||
*
|
||||
* Make sure you have `POSTGRES_URL` set in `.env.local`. See README §
|
||||
* "First-time admin setup" for the operator runbook.
|
||||
*/
|
||||
|
||||
// Load environment variables from .env.local
|
||||
import dotenv from 'dotenv';
|
||||
dotenv.config({ path: '.env.local' });
|
||||
|
||||
import { spawn } from 'node:child_process';
|
||||
import { neon } from '@neondatabase/serverless';
|
||||
import bcrypt from 'bcryptjs';
|
||||
|
||||
function runMigrations() {
|
||||
return new Promise((resolve, reject) => {
|
||||
console.log('✅ Running migrations (npm run migrate up)...');
|
||||
const child = spawn('npm', ['run', 'migrate', '--', 'up'], {
|
||||
stdio: 'inherit',
|
||||
shell: false,
|
||||
});
|
||||
child.on('error', (err) => reject(err));
|
||||
child.on('exit', (code, signal) => {
|
||||
if (code === 0) {
|
||||
resolve();
|
||||
} else {
|
||||
reject(
|
||||
new Error(
|
||||
`npm run migrate up exited with code=${code} signal=${signal}. ` +
|
||||
'See output above for the failing migration.'
|
||||
)
|
||||
);
|
||||
}
|
||||
});
|
||||
});
|
||||
}
|
||||
|
||||
async function setupNeonDatabase() {
|
||||
const adminPassword = process.env.ADMIN_INITIAL_PASSWORD;
|
||||
if (!adminPassword || !adminPassword.trim()) {
|
||||
|
|
@ -27,120 +58,20 @@ async function setupNeonDatabase() {
|
|||
process.exit(1);
|
||||
}
|
||||
|
||||
const sql = neon(process.env.POSTGRES_URL);
|
||||
if (!process.env.POSTGRES_URL) {
|
||||
console.error(
|
||||
'❌ POSTGRES_URL environment variable is not set.\n' +
|
||||
' Set it in .env.local (Neon connection string) before running setup.\n'
|
||||
);
|
||||
process.exit(1);
|
||||
}
|
||||
|
||||
try {
|
||||
console.log('✅ Connecting to Neon database...');
|
||||
await runMigrations();
|
||||
|
||||
// Create tables
|
||||
await 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
|
||||
)
|
||||
`;
|
||||
console.log('✅ Created users table');
|
||||
console.log('✅ Connecting to Neon database to seed admin user...');
|
||||
const sql = neon(process.env.POSTGRES_URL);
|
||||
|
||||
await 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
|
||||
)
|
||||
`;
|
||||
console.log('✅ Created cards table');
|
||||
|
||||
await 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)
|
||||
)
|
||||
`;
|
||||
console.log('✅ Created user_cards table');
|
||||
|
||||
await 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
|
||||
)
|
||||
`;
|
||||
console.log('✅ Created collections table');
|
||||
|
||||
await 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)
|
||||
)
|
||||
`;
|
||||
console.log('✅ Created collection_cards table');
|
||||
|
||||
await 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
|
||||
)
|
||||
`;
|
||||
console.log('✅ Created decks table');
|
||||
|
||||
await 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)
|
||||
)
|
||||
`;
|
||||
console.log('✅ Created deck_cards table');
|
||||
|
||||
// Create admin user
|
||||
const hashedPassword = await bcrypt.hash(adminPassword, 12);
|
||||
|
||||
await sql`
|
||||
|
|
@ -148,25 +79,28 @@ async function setupNeonDatabase() {
|
|||
VALUES (${'admin@deckhearth.com'}, ${hashedPassword}, ${'admin'})
|
||||
ON CONFLICT (email) DO NOTHING
|
||||
`;
|
||||
console.log('✅ Created admin user');
|
||||
console.log('✅ Admin user ready (email: admin@deckhearth.com)');
|
||||
|
||||
console.log('🎉 Neon database setup completed successfully!');
|
||||
console.log('');
|
||||
console.log('📋 Database Details:');
|
||||
console.log(' Database: Neon PostgreSQL');
|
||||
console.log(' Schema: applied via node-pg-migrate (see migrations/)');
|
||||
console.log(' Admin user ready (email: admin@deckhearth.com)');
|
||||
console.log('');
|
||||
console.log('🔧 Next Steps:');
|
||||
console.log(' 1. Test the API endpoints');
|
||||
console.log(' 2. Start building the frontend');
|
||||
|
||||
} catch (error) {
|
||||
console.error('❌ Database setup failed:', error.message);
|
||||
console.log('');
|
||||
console.log('🔧 Troubleshooting:');
|
||||
console.log(' 1. Make sure POSTGRES_URL is set in .env.local');
|
||||
console.log(' 2. Check your Neon database connection');
|
||||
console.log(' 3. Ensure the database URL is correct');
|
||||
console.log(' 2. Make sure ADMIN_INITIAL_PASSWORD is set in .env.local');
|
||||
console.log(' 3. Check your Neon database connection');
|
||||
console.log(' 4. If the migrate step failed, inspect the SQL above and');
|
||||
console.log(' see migrations/ for the failing file. To re-try just the');
|
||||
console.log(' migration step run: npm run migrate up');
|
||||
process.exit(1);
|
||||
}
|
||||
}
|
||||
|
|
|
|||
Loading…
Reference in a new issue