/** * Migrate Rollback Command * * Rollback migrations from a database. * * @module cli/commands/migrate-rollback */ import { resolve } from 'node:path' import { existsSync } from 'node:fs' import type { MigrateCLIOptions } from '../types.js' import { formatSuccess, formatError, formatInfo, formatWarning, formatRollbackResult, formatDuration } from '../formatting.js' import { loadMigrationsFromDirectory, createQueryExecutor } from '../utils.js' /** * Rollback migrations command */ export async function runMigrateRollback(options: MigrateCLIOptions): Promise { const url = options.url || process.env['DATABASE_URL'] if (!url) { console.error(formatError('Database URL is required')) console.error(formatInfo('Provide --url or set DATABASE_URL environment variable')) process.exit(1) } // Load migrations from directory const migrationsDir = resolve(process.cwd(), options.migrationsDir || './migrations') if (!existsSync(migrationsDir)) { console.error(formatError(`Migrations directory not found: ${migrationsDir}`)) console.error(formatInfo('Create a migrations directory or specify one with --migrations-dir')) process.exit(1) } try { const migrations = loadMigrationsFromDirectory(migrationsDir) // Import migration system dynamically const { createMigrationRegistry, createMigrationRunner } = await import('@dotdo/postgres/migrations') // Create registry - cast MigrationDefinition to Migration (structurally compatible) const registry = createMigrationRegistry({ migrations: migrations as import('@dotdo/postgres/migrations').Migration[] }) // Connect to database console.log(formatInfo('Connecting to database...')) const executor = await createQueryExecutor(url) // Create runner - use conditional spreading for optional properties const runner = createMigrationRunner(executor, registry, { ...(options.verbose ? { debug: options.verbose } : {}), }) // Get current version const schemaVersion = await runner.getSchemaVersion() if (schemaVersion.currentVersion === 0) { console.log(formatInfo('No migrations to rollback')) return } // Determine rollback target let targetVersion: number if (options.steps !== undefined) { // Rollback N steps const appliedVersions = schemaVersion.migrations .filter((m) => m.status === 'applied') .map((m) => m.version) .sort((a, b) => b - a) const stepsToRollback = Math.min(options.steps, appliedVersions.length) targetVersion = stepsToRollback >= appliedVersions.length ? 0 : appliedVersions[stepsToRollback] || 0 } else if (options.toVersion !== undefined) { // Rollback to specific version targetVersion = options.toVersion } else { // Default: rollback last migration const appliedVersions = schemaVersion.migrations .filter((m) => m.status === 'applied') .map((m) => m.version) .sort((a, b) => b - a) if (appliedVersions.length < 2) { targetVersion = 0 } else { targetVersion = appliedVersions[1] || 0 } } console.log(formatInfo(`Current version: ${schemaVersion.currentVersion}`)) console.log(formatInfo(`Target version: ${targetVersion}`)) // Plan rollback first - use conditional spreading for optional properties const plan = await runner.planRollback({ toVersion: targetVersion, ...(options.force ? { force: options.force } : {}) }) if (plan.migrations.length === 0) { if (plan.blockers.length > 0) { console.log(formatWarning('Cannot rollback - blocked by:')) for (const blocker of plan.blockers) { console.log(` - [${blocker.version}] ${blocker.id}: ${blocker.reason}`) } if (!options.force) { console.log(formatInfo('Use --force to attempt rollback anyway')) } process.exit(1) } console.log(formatInfo('Nothing to rollback')) return } // Show what will be rolled back console.log('') console.log(formatWarning(`Will rollback ${plan.migrations.length} migration(s):`)) for (const migration of plan.migrations) { const reversible = migration.down ? 'Yes' : 'No' console.log(` - [${migration.version}] ${migration.id} (reversible: ${reversible})`) } console.log('') // Dry run mode if (options.dryRun) { console.log(formatInfo('Dry run mode - no changes made')) const result = await runner.rollback({ toVersion: targetVersion, dryRun: true, ...(options.force ? { force: options.force } : {}), }) if (options.json) { console.log(JSON.stringify(result, null, 2)) } else { console.log(formatInfo(`Would rollback from version ${result.fromVersion} to ${result.toVersion}`)) console.log(formatInfo(`Would rollback ${result.rolledBackCount} migration(s)`)) } return } // Execute rollback - use conditional spreading for optional properties console.log(formatInfo('Rolling back...')) const startTime = Date.now() const result = await runner.rollback({ toVersion: targetVersion, ...(options.force ? { force: options.force } : {}), onProgress: (event) => { if (options.verbose || event.phase === 'completed' || event.phase === 'failed') { // Adapt RollbackProgressEvent to RollbackProgressDisplay const displayEvent = { migration: event.migration, index: event.index, total: event.total, phase: event.phase, ...(event.durationMs !== undefined && { durationMs: event.durationMs }), ...(event.error !== undefined && { error: event.error }), ...(event.isDryRun !== undefined && { isDryRun: event.isDryRun }), } console.log(formatRollbackResult(displayEvent)) } }, }) const duration = Date.now() - startTime // Output result if (options.json) { console.log(JSON.stringify(result, null, 2)) } else { console.log('') if (result.success) { console.log(formatSuccess(`Successfully rolled back ${result.rolledBackCount} migration(s) in ${formatDuration(duration)}`)) console.log(formatInfo(`Schema version: ${result.fromVersion} -> ${result.toVersion}`)) } else { console.error(formatError('Rollback failed')) const failedResult = result.results.find((r) => !r.success && !r.skipped) if (failedResult) { console.error(formatError(`Failed on: ${failedResult.id}`)) console.error(formatError(`Error: ${failedResult.error}`)) } if (result.blockedBy && result.blockedBy.length > 0) { console.log(formatWarning('Blocked by:')) for (const blocker of result.blockedBy) { console.log(` - [${blocker.version}] ${blocker.id}: ${blocker.reason}`) } } process.exit(1) } } // Cleanup if ('end' in executor && typeof executor.end === 'function') { await executor.end() } } catch (error) { console.error(formatError('Rollback error:'), error instanceof Error ? error.message : error) if (options.verbose && error instanceof Error && error.stack) { console.error(error.stack) } process.exit(1) } }