/** * Migrate Validate Command * * Validate migrations before deployment. * Performs syntax checking, destructive operation detection, * and best practice enforcement. * * @module cli/commands/migrate-validate */ import { resolve } from 'node:path' import { existsSync } from 'node:fs' import type { MigrateCLIOptions } from '../types.js' import { formatSuccess, formatError, formatInfo, formatWarning, formatDim, } from '../formatting.js' import { loadMigrationsFromDirectory, createQueryExecutor } from '../utils.js' /** * Severity color mapping */ function formatSeverity(severity: 'error' | 'warning' | 'info'): string { switch (severity) { case 'error': return formatError('ERROR') case 'warning': return formatWarning('WARN') case 'info': return formatDim('INFO') default: return severity } } /** * Migrate validate command */ export async function runMigrateValidate(options: MigrateCLIOptions): Promise { // 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) } console.log(formatInfo(`Validating migrations from: ${migrationsDir}`)) try { const migrations = loadMigrationsFromDirectory(migrationsDir) if (migrations.length === 0) { console.log(formatWarning('No migrations found')) return } console.log(formatInfo(`Found ${migrations.length} migration(s) to validate`)) console.log('') // Import validation module dynamically const { createMigrationRegistry } = await import('@dotdo/postgres/migrations') const { createMigrationValidator } = await import('@dotdo/postgres/migrations/validator') // Create registry - cast MigrationDefinition to Migration (structurally compatible) const registry = createMigrationRegistry({ migrations: migrations as import('@dotdo/postgres/migrations').Migration[], validateOnCreate: false, allowVersionGaps: true, }) // If URL is provided, enable syntax checking const url = options.url || process.env['DATABASE_URL'] let executor: import('@dotdo/postgres/migrations').QueryExecutor | undefined if (url && !options.dryRun) { console.log(formatInfo('Connecting to database for syntax validation...')) executor = await createQueryExecutor(url) } // Create validator with options - use conditional spreading for optional properties const validator = createMigrationValidator({ warnDestructive: true, checkDownMigrations: true, checkVersionGaps: true, checkChecksums: true, checkBestPractices: true, ...(url && !options.dryRun ? { checkSyntax: true } : {}), ...(executor ? { executor } : {}), }) // Run validation console.log(formatInfo('Running validation checks...')) const result = await validator.validateAll(registry) // Output results if (options.json) { console.log(JSON.stringify(result, null, 2)) } else { // Summary console.log('') console.log('Validation Results') console.log('==================') console.log('') console.log(`Migrations checked: ${migrations.length}`) console.log(`Errors: ${result.errorCount}`) console.log(`Warnings: ${result.warningCount}`) console.log('') // Show issues by migration if (result.issues.length > 0) { console.log('Issues Found:') console.log('') // Group by migration const issuesByMigration = new Map() for (const issue of result.issues) { const existing = issuesByMigration.get(issue.migrationId) || [] existing.push(issue) issuesByMigration.set(issue.migrationId, existing) } for (const [migrationId, issues] of issuesByMigration) { console.log(` ${formatInfo(migrationId)}`) for (const issue of issues) { const severity = formatSeverity(issue.severity) console.log(` ${severity} [${issue.category}] ${issue.message}`) if (issue.suggestion && options.verbose) { console.log(` ${formatDim(`Suggestion: ${issue.suggestion}`)}`) } if (issue.sql && options.verbose) { const truncatedSql = issue.sql.length > 60 ? issue.sql.substring(0, 60) + '...' : issue.sql console.log(` ${formatDim(`SQL: ${truncatedSql}`)}`) } } console.log('') } } // Show missing down migrations if (result.missingDownMigrations.length > 0 && options.verbose) { console.log(formatWarning('Migrations without rollback support:')) for (const id of result.missingDownMigrations) { console.log(` - ${id}`) } console.log('') } // Show version gaps if (result.versionGaps.length > 0) { console.log(formatWarning('Version gaps detected:')) console.log(` Missing versions: ${result.versionGaps.join(', ')}`) console.log('') } // Show checksum conflicts if (result.checksumConflicts.length > 0) { console.log(formatWarning('Duplicate migration content detected:')) for (const conflict of result.checksumConflicts) { console.log(` - ${conflict.migrationIds.join(', ')}`) } console.log('') } // Per-migration details in verbose mode if (options.verbose) { console.log('') console.log('Migration Details:') console.log('') for (const migResult of result.results) { const status = migResult.valid ? formatSuccess('VALID') : formatError('INVALID') console.log(` ${status} ${migResult.migrationId}`) if (migResult.affectedTables.length > 0) { console.log(` Tables: ${migResult.affectedTables.join(', ')}`) } if (migResult.operations.length > 0) { const ops = migResult.operations.map((o: { type: string }) => o.type).join(', ') console.log(` Operations: ${ops}`) } const destructiveOps = migResult.operations.filter((o: { isDestructive: boolean }) => o.isDestructive) if (destructiveOps.length > 0) { console.log(` ${formatWarning(`Destructive operations: ${destructiveOps.length}`)}`) } } console.log('') } // Final summary if (result.valid) { console.log(formatSuccess('All migrations passed validation')) } else { console.log(formatError(`Validation failed with ${result.errorCount} error(s)`)) process.exit(1) } } // Cleanup executor if we created one if (executor && 'end' in executor) { await (executor as { end: () => Promise }).end() } } catch (error) { console.error(formatError('Validation error:'), error instanceof Error ? error.message : error) if (options.verbose && error instanceof Error && error.stack) { console.error(error.stack) } process.exit(1) } }