/** * Database migration runner for SentryAgent.ai AgentIdP. * Reads all .sql files from src/db/migrations/ in alphabetical order, * tracks applied migrations in a schema_migrations table, and executes * only unapplied migrations. */ import * as fs from 'fs'; import * as path from 'path'; import { Pool } from 'pg'; import * as dotenv from 'dotenv'; dotenv.config(); const MIGRATIONS_DIR = path.join(__dirname, '../src/db/migrations'); interface MigrationRow { name: string; applied_at: Date; } /** * Ensures the schema_migrations tracking table exists. * * @param pool - The PostgreSQL connection pool. */ async function ensureMigrationsTable(pool: Pool): Promise { await pool.query(` CREATE TABLE IF NOT EXISTS schema_migrations ( name VARCHAR(255) PRIMARY KEY, applied_at TIMESTAMPTZ NOT NULL DEFAULT NOW() ) `); } /** * Returns the list of already-applied migration names. * * @param pool - The PostgreSQL connection pool. * @returns Array of applied migration names. */ async function getAppliedMigrations(pool: Pool): Promise { const result = await pool.query('SELECT name FROM schema_migrations ORDER BY name'); return result.rows.map((row) => row.name); } /** * Applies a single migration file within a transaction. * * @param pool - The PostgreSQL connection pool. * @param name - The migration file name (without path). * @param sql - The SQL to execute. */ async function applyMigration(pool: Pool, name: string, sql: string): Promise { const client = await pool.connect(); try { await client.query('BEGIN'); await client.query(sql); await client.query('INSERT INTO schema_migrations (name) VALUES ($1)', [name]); await client.query('COMMIT'); // eslint-disable-next-line no-console console.log(` ✓ Applied: ${name}`); } catch (err) { await client.query('ROLLBACK'); throw err; } finally { client.release(); } } /** * Main migration runner. * Reads all .sql files in alphabetical order and applies unapplied ones. */ async function migrate(): Promise { const connectionString = process.env['DATABASE_URL']; if (!connectionString) { throw new Error('DATABASE_URL environment variable is required'); } const pool = new Pool({ connectionString }); try { // eslint-disable-next-line no-console console.log('Running database migrations...'); await ensureMigrationsTable(pool); const applied = await getAppliedMigrations(pool); const files = fs .readdirSync(MIGRATIONS_DIR) .filter((f) => f.endsWith('.sql')) .sort(); let count = 0; for (const file of files) { if (applied.includes(file)) { // eslint-disable-next-line no-console console.log(` - Skipped (already applied): ${file}`); continue; } const filePath = path.join(MIGRATIONS_DIR, file); const sql = fs.readFileSync(filePath, 'utf-8'); await applyMigration(pool, file, sql); count++; } // eslint-disable-next-line no-console console.log(`\nMigrations complete. ${count} migration(s) applied.`); } finally { await pool.end(); } } migrate().catch((err: unknown) => { // eslint-disable-next-line no-console console.error('Migration failed:', err); process.exit(1); });