Files
TestingWebApp/backend/src/db/migrate.js
T
2026-04-24 20:31:43 +05:00

148 lines
4.0 KiB
JavaScript

/**
* 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();