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>
108 lines
3.8 KiB
JavaScript
108 lines
3.8 KiB
JavaScript
#!/usr/bin/env node
|
|
|
|
/**
|
|
* First-time / re-onboarding setup for the Deck Hearth Neon database.
|
|
*
|
|
* 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.
|
|
*/
|
|
|
|
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()) {
|
|
console.error(
|
|
'❌ ADMIN_INITIAL_PASSWORD environment variable is not set.\n' +
|
|
'\n' +
|
|
' Set it in .env.local for local dev, or as a CI secret if you run setup from CI.\n' +
|
|
' Generate a strong password with: openssl rand -base64 24\n' +
|
|
' See README.md → "First-time admin setup" for the full flow.\n'
|
|
);
|
|
process.exit(1);
|
|
}
|
|
|
|
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 {
|
|
await runMigrations();
|
|
|
|
console.log('✅ Connecting to Neon database to seed admin user...');
|
|
const sql = neon(process.env.POSTGRES_URL);
|
|
|
|
const hashedPassword = await bcrypt.hash(adminPassword, 12);
|
|
|
|
await sql`
|
|
INSERT INTO users (email, password, role)
|
|
VALUES (${'admin@deckhearth.com'}, ${hashedPassword}, ${'admin'})
|
|
ON CONFLICT (email) DO NOTHING
|
|
`;
|
|
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. 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);
|
|
}
|
|
}
|
|
|
|
setupNeonDatabase();
|