197 lines
4.6 KiB
TypeScript
197 lines
4.6 KiB
TypeScript
/**
|
|
* 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<void> {
|
|
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<PoolClient> {
|
|
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<QueryResult> {
|
|
const client = await this.getClient();
|
|
return client.query(text, params);
|
|
}
|
|
|
|
/**
|
|
* Begin transaction
|
|
*/
|
|
async begin(): Promise<void> {
|
|
const client = await this.getClient();
|
|
await client.query('BEGIN');
|
|
}
|
|
|
|
/**
|
|
* Commit transaction
|
|
*/
|
|
async commit(): Promise<void> {
|
|
if (this.client) {
|
|
await this.client.query('COMMIT');
|
|
}
|
|
}
|
|
|
|
/**
|
|
* Rollback transaction
|
|
*/
|
|
async rollback(): Promise<void> {
|
|
if (this.client) {
|
|
await this.client.query('ROLLBACK');
|
|
}
|
|
}
|
|
|
|
/**
|
|
* Truncate all tables (for cleanup between tests)
|
|
*/
|
|
async truncateAllTables(): Promise<void> {
|
|
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<void> {
|
|
// 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<void> {
|
|
// 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<string[]> {
|
|
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<unknown[]> {
|
|
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<void> {
|
|
if (this.client) {
|
|
this.client.release();
|
|
this.client = null;
|
|
}
|
|
await this.pool.end();
|
|
}
|
|
} |