/** * Migration runner - discovers, orders, and executes database migrations. * Mirrors the pattern from @serve.zone/nupst. */ import type { TQueryFunction } from '../types.ts'; import { logger } from '../../logging.ts'; import { getErrorMessage } from '../../utils/error.ts'; import { Migration001Initial } from './migration-001-initial.ts'; import { Migration002TimestampsToReal } from './migration-002-timestamps-to-real.ts'; import { Migration003DomainManagement } from './migration-003-domain-management.ts'; import { Migration004RegistryColumns } from './migration-004-registry-columns.ts'; import { Migration005RegistryTokens } from './migration-005-registry-tokens.ts'; import { Migration006DropRegistryToken } from './migration-006-drop-registry-token.ts'; import { Migration007PlatformServices } from './migration-007-platform-services.ts'; import { Migration008CertPemContent } from './migration-008-cert-pem-content.ts'; import { Migration009BackupSystem } from './migration-009-backup-system.ts'; import { Migration010BackupSchedules } from './migration-010-backup-schedules.ts'; import { Migration011ScopeColumns } from './migration-011-scope-columns.ts'; import { Migration012GfsRetention } from './migration-012-gfs-retention.ts'; import type { BaseMigration } from './base-migration.ts'; export class MigrationRunner { private query: TQueryFunction; private migrations: BaseMigration[]; constructor(query: TQueryFunction) { this.query = query; // Register all migrations in order this.migrations = [ new Migration001Initial(), new Migration002TimestampsToReal(), new Migration003DomainManagement(), new Migration004RegistryColumns(), new Migration005RegistryTokens(), new Migration006DropRegistryToken(), new Migration007PlatformServices(), new Migration008CertPemContent(), new Migration009BackupSystem(), new Migration010BackupSchedules(), new Migration011ScopeColumns(), new Migration012GfsRetention(), ].sort((a, b) => a.version - b.version); } /** Run all pending migrations */ run(): void { try { const currentVersion = this.getMigrationVersion(); logger.info(`Current database migration version: ${currentVersion}`); let applied = 0; for (const migration of this.migrations) { if (migration.version <= currentVersion) continue; logger.info(`Running ${migration.getName()}...`); migration.up(this.query); this.setMigrationVersion(migration.version); logger.success(`${migration.getName()} completed`); applied++; } if (applied > 0) { logger.success(`Applied ${applied} migration(s)`); } } catch (error) { logger.error(`Migration failed: ${getErrorMessage(error)}`); if (error instanceof Error && error.stack) { logger.error(`Stack: ${error.stack}`); } throw error; } } /** Get current migration version from the migrations table */ private getMigrationVersion(): number { try { const result = this.query<{ version?: number | null; [key: number]: unknown }>( 'SELECT MAX(version) as version FROM migrations', ); if (result.length === 0) return 0; const versionValue = result[0].version ?? (result[0] as Record)[0]; return versionValue !== null && versionValue !== undefined ? Number(versionValue) : 0; } catch { // Table might not exist yet on fresh databases return 0; } } /** Record a migration version as applied */ private setMigrationVersion(version: number): void { this.query('INSERT INTO migrations (version, applied_at) VALUES (?, ?)', [ version, Date.now(), ]); } }