You can not select more than 25 topics
Topics must start with a letter or number, can include dashes ('-') and can be up to 35 characters long.
133 lines
3.6 KiB
133 lines
3.6 KiB
/**
|
|
* 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<T>(
|
|
sqlExec: (sql: string, params?: unknown[]) => Promise<unknown>,
|
|
sqlQuery: (sql: string, params?: unknown[]) => Promise<T>,
|
|
extractMigrationNames: (result: T) => Set<string>,
|
|
): Promise<void> {
|
|
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;
|
|
}
|
|
}
|
|
|