/** * Database Manager for Integration Tests * Handles database connections, migrations, seeding, and cleanup */ import { Pool, PoolClient, QueryResult } from 'pg'; import { setTimeout } from 'timers/promises'; export interface DatabaseConfig { host: string; port: number; database: string; user: string; password: string; } export class DatabaseManager { private pool: Pool; private client: PoolClient | null = null; constructor(config: DatabaseConfig) { this.pool = new Pool({ host: config.host, port: config.port, database: config.database, user: config.user, password: config.password, max: 1, idleTimeoutMillis: 30000, connectionTimeoutMillis: 10000, }); } /** * Wait for database to be ready */ async waitForReady(timeout: number = 30000): Promise { const startTime = Date.now(); while (Date.now() - startTime < timeout) { try { const client = await this.pool.connect(); await client.query('SELECT 1'); client.release(); console.log('[DatabaseManager] ✓ Database is ready'); return; } catch (error) { await setTimeout(1000); } } throw new Error('Database failed to become ready'); } /** * Get a client for transactions */ async getClient(): Promise { if (!this.client) { this.client = await this.pool.connect(); } return this.client; } /** * Execute query with automatic client management */ async query(text: string, params?: unknown[]): Promise { const client = await this.getClient(); return client.query(text, params); } /** * Begin transaction */ async begin(): Promise { const client = await this.getClient(); await client.query('BEGIN'); } /** * Commit transaction */ async commit(): Promise { if (this.client) { await this.client.query('COMMIT'); } } /** * Rollback transaction */ async rollback(): Promise { if (this.client) { await this.client.query('ROLLBACK'); } } /** * Truncate all tables (for cleanup between tests) */ async truncateAllTables(): Promise { const client = await this.getClient(); // Get all table names const result = await client.query(` SELECT tablename FROM pg_tables WHERE schemaname = 'public' AND tablename NOT LIKE 'pg_%' AND tablename NOT LIKE 'sql_%' `); if (result.rows.length === 0) return; // Disable triggers temporarily to allow truncation await client.query('SET session_replication_role = replica'); const tableNames = result.rows.map(r => r.tablename).join(', '); try { await client.query(`TRUNCATE TABLE ${tableNames} CASCADE`); console.log(`[DatabaseManager] ✓ Truncated tables: ${tableNames}`); } finally { await client.query('SET session_replication_role = DEFAULT'); } } /** * Run database migrations */ async runMigrations(): Promise { // This would typically run TypeORM migrations // For now, we'll assume the API handles this on startup console.log('[DatabaseManager] Migrations handled by API startup'); } /** * Seed minimal test data */ async seedMinimalData(): Promise { // Insert minimal required data for tests // This will be extended based on test requirements console.log('[DatabaseManager] ✓ Minimal test data seeded'); } /** * Check for constraint violations in recent operations */ async getRecentConstraintErrors(since: Date): Promise { const client = await this.getClient(); const result = await client.query(` SELECT sqlstate, message, detail, constraint_name FROM pg_last_error_log() WHERE sqlstate IN ('23505', '23503', '23514') AND log_time > $1 ORDER BY log_time DESC `, [since]); return (result.rows as { message: string }[]).map(r => r.message); } /** * Get table constraints */ async getTableConstraints(tableName: string): Promise { const client = await this.getClient(); const result = await client.query(` SELECT conname as constraint_name, contype as constraint_type, pg_get_constraintdef(oid) as definition FROM pg_constraint WHERE conrelid = $1::regclass ORDER BY contype `, [tableName]); return result.rows; } /** * Close connection pool */ async close(): Promise { if (this.client) { this.client.release(); this.client = null; } await this.pool.end(); } }