#!/usr/bin/env node import { createReadStream, existsSync } from 'node:fs'; import { resolve } from 'node:path'; import { pathToFileURL } from 'node:url'; import { spawn } from 'node:child_process'; import { commandToString, composePrefix, flag, option, parseArgs, quotePgLiteral, readEnvFile, runCommand, } from './ops-utils.mjs'; function usage() { return `Usage: node scripts/restore-postgres.mjs --input --confirm-overwrite [options] This script restores into a freshly recreated PostgreSQL database. It refuses to drop/recreate a database unless --confirm-overwrite is provided. Options: --input pg_dump custom-format dump file --confirm-overwrite Required safety flag for destructive restore --env-file Compose env file (default: .env.production) --compose-file Compose file (default: docker-compose.prod.yml) --service Postgres service name (default: postgres) --user Database user (default: POSTGRES_USER or postgres) --target-db Target database (default: POSTGRES_DB or ftb_pm) --maintenance-db Maintenance database (default: postgres) --dry-run Print restore commands without changing data --help Show this help `; } function buildOptions(argv) { const args = parseArgs(argv); if (flag(args, 'help')) return { help: true }; const envFile = option(args, 'env-file', '.env.production'); const env = readEnvFile(envFile, { optional: flag(args, 'dry-run') }); const input = option(args, 'input'); if (!input) { throw new Error('Missing required --input '); } const inputPath = resolve(input); if (!existsSync(inputPath)) { throw new Error(`Restore input not found: ${inputPath}`); } if (!flag(args, 'confirm-overwrite')) { throw new Error('Refusing destructive restore. Re-run with --confirm-overwrite.'); } return { help: false, dryRun: flag(args, 'dry-run'), input: inputPath, envFile, composeFile: option(args, 'compose-file', 'docker-compose.prod.yml'), service: option(args, 'service', 'postgres'), user: option(args, 'user', env.POSTGRES_USER || 'postgres'), targetDb: option(args, 'target-db', env.POSTGRES_DB || 'ftb_pm'), maintenanceDb: option(args, 'maintenance-db', 'postgres'), }; } export function buildRestoreCommands(options) { const prefix = composePrefix(options); const terminateSql = [ 'SELECT pg_terminate_backend(pid)', 'FROM pg_stat_activity', `WHERE datname = ${quotePgLiteral(options.targetDb)} AND pid <> pg_backend_pid();`, ].join(' '); return [ [ ...prefix, 'exec', '-T', options.service, 'psql', '-U', options.user, '-d', options.maintenanceDb, '-v', 'ON_ERROR_STOP=1', '-c', terminateSql, ], [ ...prefix, 'exec', '-T', options.service, 'dropdb', '--if-exists', '-U', options.user, options.targetDb, ], [ ...prefix, 'exec', '-T', options.service, 'createdb', '-U', options.user, options.targetDb, ], [ ...prefix, 'exec', '-T', options.service, 'pg_restore', '-U', options.user, '-d', options.targetDb, '--no-owner', '--no-acl', ], ]; } function runRestore(command, input) { return new Promise((resolveRun, rejectRun) => { const child = spawn(command[0], command.slice(1), { cwd: process.cwd(), stdio: ['pipe', 'inherit', 'inherit'], }); createReadStream(input).pipe(child.stdin); child.on('error', rejectRun); child.on('close', (code) => { if (code === 0) { resolveRun(); } else { rejectRun(new Error(`pg_restore failed with exit code ${code}`)); } }); }); } export async function main(argv = process.argv.slice(2)) { const options = buildOptions(argv); if (options.help) { process.stdout.write(usage()); return; } const commands = buildRestoreCommands(options); if (options.dryRun) { process.stdout.write( `[dry-run] PostgreSQL restore would recreate database: ${options.targetDb}\n`, ); for (const command of commands.slice(0, -1)) { process.stdout.write(`${commandToString(command)}\n`); } process.stdout.write(`${commandToString(commands.at(-1), { stdin: options.input })}\n`); return; } for (const command of commands.slice(0, -1)) { await runCommand(command); } await runRestore(commands.at(-1), options.input); process.stdout.write(`PostgreSQL restore completed into fresh database: ${options.targetDb}\n`); } if (import.meta.url === pathToFileURL(process.argv[1]).href) { main().catch((error) => { process.stderr.write(`${error.message}\n`); process.exit(1); }); }