#!/usr/bin/env node /* eslint-disable no-console */ const fs = require('fs') const path = require('path') const { spawnSync } = require('child_process') require('dotenv').config({ path: path.join(__dirname, '..', '.env') }) function run(command, args, options = {}) { const result = spawnSync(command, args, { stdio: 'inherit', shell: process.platform === 'win32', ...options, }) if (result.status !== 0) { process.exit(result.status || 1) } } function nowStamp() { const d = new Date() const pad = (n) => String(n).padStart(2, '0') return `${d.getFullYear()}${pad(d.getMonth() + 1)}${pad(d.getDate())}_${pad(d.getHours())}${pad(d.getMinutes())}${pad(d.getSeconds())}` } const databaseUrl = process.env.DATABASE_URL if (!databaseUrl) { console.error('[safe-migrate] DATABASE_URL is missing in environment/.env') process.exit(1) } const backupsDir = path.join(__dirname, '..', 'backups', 'migrations') fs.mkdirSync(backupsDir, { recursive: true }) const isPostgres = databaseUrl.startsWith('postgres://') || databaseUrl.startsWith('postgresql://') const isSqlite = databaseUrl.startsWith('file:') console.log('[safe-migrate] Starting safe migration flow') if (isPostgres) { const backupFile = path.join(backupsDir, `pre_migrate_${nowStamp()}.sql`) console.log(`[safe-migrate] Creating PostgreSQL backup: ${backupFile}`) let dump = spawnSync( 'pg_dump', ['--no-owner', '--no-privileges', '--format=plain', '--file', backupFile, databaseUrl], { stdio: 'inherit', shell: process.platform === 'win32' } ) if (dump.status !== 0) { console.warn('[safe-migrate] Local pg_dump unavailable, trying Docker fallback...') const pgUser = process.env.POSTGRES_USER || 'memento' const pgDb = process.env.POSTGRES_DB || 'memento' const dockerCmd = `docker exec memento-postgres pg_dump -U ${pgUser} -d ${pgDb} --no-owner --no-privileges --format=plain > "${backupFile}"` dump = spawnSync(dockerCmd, { stdio: 'inherit', shell: true }) } if (dump.status !== 0) { console.error('[safe-migrate] Backup failed (local + docker). Migration aborted to protect data.') process.exit(dump.status || 1) } } else if (isSqlite) { const dbPath = databaseUrl.replace(/^file:/, '') const absoluteDbPath = path.isAbsolute(dbPath) ? dbPath : path.join(__dirname, '..', dbPath) if (fs.existsSync(absoluteDbPath)) { const backupFile = path.join(backupsDir, `pre_migrate_${nowStamp()}.sqlite`) console.log(`[safe-migrate] Creating SQLite backup: ${backupFile}`) fs.copyFileSync(absoluteDbPath, backupFile) } else { console.warn(`[safe-migrate] SQLite file not found at ${absoluteDbPath}, skipping backup`) } } else { console.warn('[safe-migrate] Unknown DATABASE_URL protocol, skipping backup step') } console.log('[safe-migrate] Applying migrations with prisma migrate deploy') run('npx', ['prisma', 'migrate', 'deploy']) console.log('[safe-migrate] Migration completed successfully')