import { Pool, PoolConfig, types } from 'pg'; import * as fs from 'fs'; import * as path from 'path'; import environment from './environment'; import logger from '../utils/logger'; // Override pg's default DATE parser: return plain 'YYYY-MM-DD' strings // instead of JavaScript Date objects (which introduce timezone shifts). types.setTypeParser(1082, (val: string) => val); const poolConfig: PoolConfig = { host: environment.database.host, port: environment.database.port, database: environment.database.name, user: environment.database.user, password: environment.database.password, max: 30, // Maximum number of clients in the pool idleTimeoutMillis: 30000, // Close idle clients after 30 seconds connectionTimeoutMillis: 5000, // Return an error if connection takes longer than 5 seconds }; const pool = new Pool(poolConfig); // Handle pool errors pool.on('error', (err) => { logger.error('Unexpected error on idle database client', err); }); // Log pool exhaustion warnings every 60s (only when requests are waiting) setInterval(() => { if (pool.waitingCount > 0) { logger.warn('DB pool pressure detected', { total: pool.totalCount, idle: pool.idleCount, waiting: pool.waitingCount, }); } }, 60_000).unref(); // Test database connection export const testConnection = async (): Promise => { try { const client = await pool.connect(); const result = await client.query('SELECT NOW()'); logger.info('Database connection successful', { timestamp: result.rows[0].now }); client.release(); return true; } catch (error) { logger.error('Failed to connect to database', { error }); return false; } }; // Graceful shutdown export const closePool = async (): Promise => { try { await pool.end(); logger.info('Database pool closed'); } catch (error) { logger.error('Error closing database pool', { error }); } }; /** * Check if a table exists in the database */ export const tableExists = async (tableName: string): Promise => { try { const result = await pool.query( `SELECT EXISTS ( SELECT FROM information_schema.tables WHERE table_schema = 'public' AND table_name = $1 )`, [tableName] ); return result.rows[0].exists; } catch (error) { logger.error(`Failed to check if table ${tableName} exists`, { error }); return false; } }; /** * Run database migrations from SQL files */ export const runMigrations = async (): Promise => { const migrationsDir = path.join(__dirname, '../database/migrations'); try { // Check if migrations directory exists if (!fs.existsSync(migrationsDir)) { logger.warn('Migrations directory not found', { path: migrationsDir }); return; } // Read all migration files const files = fs.readdirSync(migrationsDir) .filter(file => file.endsWith('.sql')) .sort(); // Sort to ensure migrations run in order if (files.length === 0) { logger.info('No migration files found'); return; } logger.info(`Found ${files.length} migration file(s)`); // Create migrations tracking table if it doesn't exist await pool.query(` CREATE TABLE IF NOT EXISTS migrations ( id SERIAL PRIMARY KEY, filename VARCHAR(255) UNIQUE NOT NULL, executed_at TIMESTAMP WITH TIME ZONE DEFAULT NOW() ) `); // Run each migration for (const file of files) { // Check if migration has already been run const result = await pool.query( 'SELECT filename FROM migrations WHERE filename = $1', [file] ); if (result.rows.length > 0) { logger.info(`Migration ${file} already executed, skipping`); continue; } // Read and execute migration const filePath = path.join(migrationsDir, file); const sql = fs.readFileSync(filePath, 'utf8'); logger.info(`Running migration: ${file}`); await pool.query('BEGIN'); try { await pool.query(sql); await pool.query( 'INSERT INTO migrations (filename) VALUES ($1)', [file] ); await pool.query('COMMIT'); logger.info(`Migration ${file} completed successfully`); } catch (error) { await pool.query('ROLLBACK'); logger.error(`Migration ${file} failed`, { error }); throw error; } } logger.info('All migrations completed successfully'); } catch (error) { logger.error('Failed to run migrations', { error }); throw error; } }; export default pool;