/** * Manage database migrations as people upgrade their app over time */ import { logger } from "../utils/logger"; /** * Migration interface for database schema migrations */ interface Migration { name: string; sql: string; } /** * Migration registry to store and manage database migrations */ class MigrationRegistry { private migrations: Migration[] = []; /** * Register a migration with the registry * * @param migration - The migration to register */ registerMigration(migration: Migration): void { this.migrations.push(migration); } /** * Get all registered migrations * * @returns Array of registered migrations */ getMigrations(): Migration[] { return this.migrations; } /** * Clear all registered migrations */ clearMigrations(): void { this.migrations = []; } } // Create a singleton instance of the migration registry const migrationRegistry = new MigrationRegistry(); /** * Register a migration with the migration service * * This function is used by the migration system to register database * schema migrations that need to be applied to the database. * * @param migration - The migration to register */ export function registerMigration(migration: Migration): void { migrationRegistry.registerMigration(migration); } /** * Run all registered migrations against the database * * This function executes all registered migrations in order, checking * which ones have already been applied to avoid duplicate execution. * It creates a migrations table if it doesn't exist to track applied * migrations. * * @param sqlExec - Function to execute SQL statements * @param sqlQuery - Function to query SQL data * @param extractMigrationNames - Function to extract migration names from query results * @returns Promise that resolves when all migrations are complete */ export async function runMigrations( sqlExec: (sql: string, params?: unknown[]) => Promise, sqlQuery: (sql: string, params?: unknown[]) => Promise, extractMigrationNames: (result: T) => Set, ): Promise { try { // Create migrations table if it doesn't exist await sqlExec(` CREATE TABLE IF NOT EXISTS migrations ( name TEXT PRIMARY KEY, applied_at TEXT DEFAULT CURRENT_TIMESTAMP ); `); // Get list of already applied migrations const appliedMigrationsResult = await sqlQuery( "SELECT name FROM migrations", ); const appliedMigrations = extractMigrationNames(appliedMigrationsResult); // Get all registered migrations const migrations = migrationRegistry.getMigrations(); if (migrations.length === 0) { logger.warn("[MigrationService] No migrations registered"); return; } // Run each migration that hasn't been applied yet for (const migration of migrations) { if (appliedMigrations.has(migration.name)) { continue; } try { // Execute the migration SQL await sqlExec(migration.sql); // Record that the migration was applied await sqlExec("INSERT INTO migrations (name) VALUES (?)", [ migration.name, ]); logger.info( `[MigrationService] Successfully applied migration: ${migration.name}`, ); } catch (error) { logger.error( `[MigrationService] Failed to apply migration ${migration.name}:`, error, ); throw new Error(`Migration ${migration.name} failed: ${error}`); } } } catch (error) { logger.error("[MigrationService] Migration process failed:", error); throw error; } }