101 lines
3.7 KiB
TypeScript
101 lines
3.7 KiB
TypeScript
/**
|
|
* 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<number, unknown>)[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(),
|
|
]);
|
|
}
|
|
}
|