import 'dotenv/config' import { Pool } from 'pg' import { readdir } from 'fs/promises' import { join, dirname } from 'path' import { fileURLToPath } from 'url' import { getDb } from './index.js' import { logger } from '../lib/logger.js' const __filename = fileURLToPath(import.meta.url) const __dirname = dirname(__filename) export interface Migration { up: (db: Pool) => Promise down: (db: Pool) => Promise } const MIGRATIONS_TABLE = 'schema_migrations' async function ensureMigrationsTable(db: Pool): Promise { await db.query(` CREATE TABLE IF NOT EXISTS ${MIGRATIONS_TABLE} ( version VARCHAR(255) PRIMARY KEY, name VARCHAR(255) NOT NULL, executed_at TIMESTAMP WITH TIME ZONE DEFAULT NOW() ) `) } async function getExecutedMigrations(db: Pool): Promise { await ensureMigrationsTable(db) const result = await db.query(`SELECT version FROM ${MIGRATIONS_TABLE} ORDER BY version`) return result.rows.map((row) => row.version) } async function recordMigration(db: Pool, version: string, name: string): Promise { await db.query( `INSERT INTO ${MIGRATIONS_TABLE} (version, name) VALUES ($1, $2) ON CONFLICT (version) DO NOTHING`, [version, name] ) } async function removeMigration(db: Pool, version: string): Promise { await db.query(`DELETE FROM ${MIGRATIONS_TABLE} WHERE version = $1`, [version]) } async function loadMigration(version: string): Promise { const migrationPath = join(__dirname, 'migrations', `${version}.ts`) try { const migration = await import(migrationPath) return { up: migration.up, down: migration.down, } } catch (error) { throw new Error(`Failed to load migration ${version}: ${error}`) } } async function getAllMigrations(): Promise { const migrationsDir = join(__dirname, 'migrations') const files = await readdir(migrationsDir) return files .filter((file) => file.endsWith('.ts') && file !== 'index.ts') .map((file) => file.replace('.ts', '')) .sort() } async function migrateUp(db: Pool): Promise { await ensureMigrationsTable(db) const executed = await getExecutedMigrations(db) const allMigrations = await getAllMigrations() const pending = allMigrations.filter((m) => !executed.includes(m)) logger.info(`Found ${pending.length} pending migrations`) for (const version of pending) { logger.info(`Running migration ${version}...`) const migration = await loadMigration(version) await migration.up(db) const name = version.replace(/^\d+_/, '').replace(/_/g, ' ') await recordMigration(db, version, name) logger.info(`✓ Migration ${version} completed`) } if (pending.length === 0) { logger.info('No pending migrations') } } async function migrateDown(db: Pool, targetVersion?: string): Promise { await ensureMigrationsTable(db) const executed = await getExecutedMigrations(db) if (executed.length === 0) { logger.info('No migrations to roll back') return } const toRollback = targetVersion ? executed.slice(executed.indexOf(targetVersion) + 1).reverse() : [executed[executed.length - 1]] for (const version of toRollback) { logger.info(`Rolling back migration ${version}...`) const migration = await loadMigration(version) await migration.down(db) await removeMigration(db, version) logger.info(`✓ Migration ${version} rolled back`) } } async function showStatus(db: Pool): Promise { await ensureMigrationsTable(db) const executed = await getExecutedMigrations(db) const allMigrations = await getAllMigrations() logger.info('\nMigration Status:') logger.info('================\n') for (const migration of allMigrations) { const status = executed.includes(migration) ? '✓' : '✗' const name = migration.replace(/^\d+_/, '').replace(/_/g, ' ') logger.info(`${status} ${migration} - ${name}`) } logger.info(`\nTotal: ${executed.length}/${allMigrations.length} executed\n`) } async function main(): Promise { const command = process.argv[2] const db = getDb() try { switch (command) { case 'up': await migrateUp(db) break case 'down': const targetVersion = process.argv[3] await migrateDown(db, targetVersion) break case 'status': await showStatus(db) break default: logger.info('Usage: npm run db:migrate [up|down|status]') logger.info(' up - Run all pending migrations') logger.info(' down - Roll back the last migration') logger.info(' status - Show migration status') process.exit(1) } } catch (error) { logger.error('Migration error', { error }) process.exit(1) } finally { await db.end() } } if (import.meta.url === `file://${process.argv[1]}`) { main() }