/** * Database Migration Script * Executes SQL migration files in order */ import { readFileSync, readdirSync } from 'fs'; import { join } from 'path'; import pg from 'pg'; import { getPoolConfig } from './poolConfig.js'; const { Pool } = pg; const MIGRATIONS_DIR = join(process.cwd(), 'src', 'db', 'migrations'); /** * Get list of migration files sorted by name */ function getMigrationFiles() { const files = readdirSync(MIGRATIONS_DIR) .filter((file) => file.endsWith('.sql')) .sort(); return files; } /** * Create migrations tracking table if not exists */ async function ensureMigrationsTable(pool) { await pool.query(` CREATE TABLE IF NOT EXISTS migrations ( id SERIAL PRIMARY KEY, name VARCHAR(255) NOT NULL UNIQUE, executed_at TIMESTAMP DEFAULT NOW() ) `); } /** * Get list of already executed migrations */ async function getExecutedMigrations(pool) { const result = await pool.query('SELECT name FROM migrations ORDER BY name'); return result.rows.map((row) => row.name); } /** * Execute a single migration file */ async function executeMigration(pool, filename) { const filePath = join(MIGRATIONS_DIR, filename); const sql = readFileSync(filePath, 'utf-8'); console.log(`Executing migration: ${filename}`); await pool.query('BEGIN'); try { await pool.query(sql); await pool.query( 'INSERT INTO migrations (name) VALUES ($1)', [filename] ); await pool.query('COMMIT'); console.log(`✓ Migration ${filename} completed successfully`); } catch (error) { await pool.query('ROLLBACK'); console.error(`✗ Migration ${filename} failed:`, error.message); throw error; } } /** * Main migration function */ function logMigrationError(error) { const msg = error?.message || String(error); console.error('\n✗ Migration failed:', msg || '(no message)'); if (error?.code) { console.error(' PG code:', error.code); } if (error?.address && error?.port) { console.error(' connect:', `${error.address}:${error.port}`); } if (error?.name === 'AggregateError' && Array.isArray(error.errors)) { for (const e of error.errors) { console.error(' —', e?.message || e); } } if (error?.code === 'ECONNREFUSED') { console.error( ' hint: проверьте, что Postgres запущен и DATABASE_URL / DB_* в backend/.env совпадают с портом (часто 5432 для Postgres_TG_Bots или 5433 для локального compose).' ); } if (process.env.DEBUG_MIGRATE === '1' || !msg) { console.error(error); } } async function migrate() { const pool = new Pool(getPoolConfig()); try { console.log('Connecting to database...'); const client = await pool.connect(); try { await client.query('SELECT 1'); } finally { client.release(); } console.log('Connected to database\n'); // Ensure migrations table exists await ensureMigrationsTable(pool); // Get migration files and already executed migrations const migrationFiles = getMigrationFiles(); const executedMigrations = await getExecutedMigrations(pool); console.log(`Found ${migrationFiles.length} migration file(s)`); console.log(`Already executed: ${executedMigrations.length} migration(s)\n`); // Execute pending migrations const pendingMigrations = migrationFiles.filter( (file) => !executedMigrations.includes(file) ); if (pendingMigrations.length === 0) { console.log('All migrations already executed.'); } else { console.log(`Pending migrations: ${pendingMigrations.length}\n`); for (const filename of pendingMigrations) { await executeMigration(pool, filename); } console.log(`\n✓ Successfully executed ${pendingMigrations.length} migration(s)`); } } catch (error) { logMigrationError(error); process.exit(1); } finally { await pool.end(); } } // Run migrations if this script is executed directly migrate();