/** * One-time split of legacy Gallery → gallery_prod + gallery_dev on TrueNAS PostgreSQL. * * Usage: * PGPASSWORD=... PGHOST=192.168.10.122 PGUSER=postgres npm run infra:db:split-dev-prod * * Options: * --copy pg_dump legacy → gallery_prod instead of rename * --skip-dev Do not create gallery_dev */ const { execFileSync } = require('child_process'); const pg = require('pg'); const { DEV_DB_NAME, LEGACY_DB_NAMES, PROD_DB_NAME, } = require('./db-env'); const { Client } = pg; const args = new Set(process.argv.slice(2)); const useCopy = args.has('--copy'); const skipDev = args.has('--skip-dev'); const host = process.env.PGHOST ?? process.env.DB_HOST ?? '192.168.10.122'; const port = Number(process.env.PGPORT ?? process.env.DB_PORT ?? 5432); const user = process.env.PGUSER ?? process.env.DB_USER ?? 'postgres'; const password = process.env.PGPASSWORD ?? process.env.DB_PASSWORD; if (!password) { console.error('PGPASSWORD (or DB_PASSWORD) is required'); process.exit(1); } function quoteIdent(ident) { return `"${ident.replace(/"/g, '""')}"`; } async function databaseExists(client, name) { const res = await client.query( 'SELECT 1 FROM pg_database WHERE datname = $1', [name], ); return res.rowCount > 0; } async function findLegacyName(client) { for (const name of LEGACY_DB_NAMES) { if (await databaseExists(client, name)) return name; } return null; } async function createDatabaseIfMissing(admin, name) { if (await databaseExists(admin, name)) { console.log(`Database "${name}" already exists`); return false; } await admin.query(`CREATE DATABASE ${quoteIdent(name)}`); console.log(`Created database "${name}"`); return true; } async function terminateConnections(admin, dbName) { await admin.query( `SELECT pg_terminate_backend(pid) FROM pg_stat_activity WHERE datname = $1 AND pid <> pg_backend_pid()`, [dbName], ); } async function cloneDevFromProd(admin) { if (await databaseExists(admin, DEV_DB_NAME)) { console.log(`"${DEV_DB_NAME}" already exists — skipping dev clone`); return; } await terminateConnections(admin, PROD_DB_NAME); console.log(`Creating "${DEV_DB_NAME}" from TEMPLATE "${PROD_DB_NAME}"...`); await admin.query( `CREATE DATABASE ${quoteIdent(DEV_DB_NAME)} WITH TEMPLATE ${quoteIdent(PROD_DB_NAME)} OWNER ${quoteIdent(user)}`, ); console.log(`Created "${DEV_DB_NAME}" as a full copy of "${PROD_DB_NAME}"`); } async function main() { const admin = new Client({ host, port, user, password, database: 'postgres' }); await admin.connect(); const legacyName = await findLegacyName(admin); const hasProd = await databaseExists(admin, PROD_DB_NAME); if (hasProd) { console.log(`"${PROD_DB_NAME}" already exists — skipping prod migration`); } else if (legacyName) { if (useCopy) { await createDatabaseIfMissing(admin, PROD_DB_NAME); console.log(`Copying ${legacyName} → ${PROD_DB_NAME} via pg_dump...`); const dump = execFileSync( 'pg_dump', ['-h', host, '-p', String(port), '-U', user, '-d', legacyName, '--no-owner', '--no-acl'], { env: { ...process.env, PGPASSWORD: password }, encoding: 'buffer' }, ); execFileSync( 'psql', ['-h', host, '-p', String(port), '-U', user, '-d', PROD_DB_NAME, '-v', 'ON_ERROR_STOP=1'], { env: { ...process.env, PGPASSWORD: password }, input: dump, stdio: ['pipe', 'inherit', 'inherit'] }, ); console.log(`Copied ${legacyName} → ${PROD_DB_NAME}`); } else { await terminateConnections(admin, legacyName); await admin.query(`ALTER DATABASE ${quoteIdent(legacyName)} RENAME TO ${quoteIdent(PROD_DB_NAME)}`); console.log(`Renamed ${legacyName} → ${PROD_DB_NAME}`); } } else { console.warn(`No legacy database (${LEGACY_DB_NAMES.join(' / ')}) or "${PROD_DB_NAME}" found`); await createDatabaseIfMissing(admin, PROD_DB_NAME); console.log('Run npm run dev:migrate against gallery_prod to apply schema'); } if (!skipDev) { await cloneDevFromProd(admin); console.log(`Next on dev PC: set .env DB_NAME=${DEV_DB_NAME}, then npm run dev:migrate`); } await admin.end(); console.log('Done. TrueNAS compose.truenas.yaml should use DB_NAME=gallery_prod.'); } main().catch((error) => { if (error.code === '42501') { console.error( 'Permission denied — the gallery user cannot rename databases.\n' + 'Use pgAdmin on the dev PC (recommended):\n' + ' Open db/split-dev-prod-pgadmin.sql → connect as postgres → run each STEP (F5)\n' + 'Or: PGUSER=postgres PGPASSWORD=... npm run infra:db:split-dev-prod\n' + 'Or: psql -h 192.168.10.122 -U postgres -d postgres -f db/split-dev-prod.sql', ); } console.error(error.message || error); process.exit(1); });