/** * Auto-Migration System * * Runs SQL migration files from the migrations/ folder automatically on server startup. * Uses a schema_migrations table to track which migrations have been applied. * * Safe to run multiple times - only applies new migrations. */ import { Pool } from 'pg'; import fs from 'fs'; import path from 'path'; const MIGRATIONS_DIR = path.join(__dirname, '../../migrations'); /** * Ensure schema_migrations table exists */ async function ensureMigrationsTable(pool: Pool): Promise { await pool.query(` CREATE TABLE IF NOT EXISTS schema_migrations ( id SERIAL PRIMARY KEY, name VARCHAR(255) UNIQUE NOT NULL, applied_at TIMESTAMP WITH TIME ZONE DEFAULT NOW() ) `); } /** * Get list of already-applied migrations */ async function getAppliedMigrations(pool: Pool): Promise> { const result = await pool.query('SELECT name FROM schema_migrations'); return new Set(result.rows.map(row => row.name)); } /** * Get list of migration files from disk */ function getMigrationFiles(): string[] { if (!fs.existsSync(MIGRATIONS_DIR)) { console.log('[AutoMigrate] No migrations directory found'); return []; } return fs.readdirSync(MIGRATIONS_DIR) .filter(f => f.endsWith('.sql')) .sort(); // Sort alphabetically (001_, 002_, etc.) } /** * Run a single migration file */ async function runMigration(pool: Pool, filename: string): Promise { const filepath = path.join(MIGRATIONS_DIR, filename); const sql = fs.readFileSync(filepath, 'utf8'); const client = await pool.connect(); try { await client.query('BEGIN'); // Run the migration SQL await client.query(sql); // Record that this migration was applied await client.query( 'INSERT INTO schema_migrations (name) VALUES ($1) ON CONFLICT (name) DO NOTHING', [filename] ); await client.query('COMMIT'); console.log(`[AutoMigrate] ✓ Applied: ${filename}`); } catch (error: any) { await client.query('ROLLBACK'); console.error(`[AutoMigrate] ✗ Failed: ${filename}`); throw error; } finally { client.release(); } } /** * Run all pending migrations * * @param pool - Database connection pool * @returns Number of migrations applied */ export async function runAutoMigrations(pool: Pool): Promise { console.log('[AutoMigrate] Checking for pending migrations...'); try { // Ensure migrations table exists await ensureMigrationsTable(pool); // Get applied and available migrations const applied = await getAppliedMigrations(pool); const available = getMigrationFiles(); // Find pending migrations const pending = available.filter(f => !applied.has(f)); if (pending.length === 0) { console.log('[AutoMigrate] No pending migrations'); return 0; } console.log(`[AutoMigrate] Found ${pending.length} pending migrations`); // Run each pending migration in order for (const filename of pending) { await runMigration(pool, filename); } console.log(`[AutoMigrate] Successfully applied ${pending.length} migrations`); return pending.length; } catch (error: any) { console.error('[AutoMigrate] Migration failed:', error.message); // Don't crash the server - log and continue // The specific failing migration will have been rolled back return -1; } } /** * Check migration status without running anything */ export async function checkMigrationStatus(pool: Pool): Promise<{ applied: string[]; pending: string[]; }> { await ensureMigrationsTable(pool); const applied = await getAppliedMigrations(pool); const available = getMigrationFiles(); return { applied: available.filter(f => applied.has(f)), pending: available.filter(f => !applied.has(f)), }; }