UNPKG

pgsql-test

Version:

pgsql-test offers isolated, role-aware, and rollback-friendly PostgreSQL environments for integration tests — giving developers realistic test coverage without external state pollution

138 lines (137 loc) 4.77 kB
"use strict"; Object.defineProperty(exports, "__esModule", { value: true }); exports.PgTestConnector = void 0; const logger_1 = require("@launchql/logger"); const pg_1 = require("pg"); const pg_env_1 = require("pg-env"); const admin_1 = require("./admin"); const test_client_1 = require("./test-client"); const log = new logger_1.Logger('test-connector'); const SYS_EVENTS = ['SIGTERM']; const end = (pool) => { try { if (pool.ended || pool.ending) { log.warn('⚠️ pg pool already ended or ending'); return; } pool.end(); } catch (err) { log.error('❌ pg pool termination error:', err); } }; class PgTestConnector { static instance; clients = new Set(); pgPools = new Map(); seenDbConfigs = new Map(); pendingConnects = new Set(); verbose = false; shuttingDown = false; constructor(verbose = false) { this.verbose = verbose; SYS_EVENTS.forEach((event) => { process.on(event, () => { log.info(`⏹ Received ${event}, closing all connections...`); this.closeAll(); }); }); } static getInstance(verbose = false) { if (!PgTestConnector.instance) { PgTestConnector.instance = new PgTestConnector(verbose); } return PgTestConnector.instance; } poolKey(config) { return `${config.user}@${config.host}:${config.port}/${config.database}`; } dbKey(config) { return `${config.host}:${config.port}/${config.database}`; } beginTeardown() { this.shuttingDown = true; } registerConnect(p) { this.pendingConnects.add(p); p.finally(() => this.pendingConnects.delete(p)); } async awaitPendingConnects() { const arr = Array.from(this.pendingConnects); if (arr.length) { await Promise.allSettled(arr); } } getPool(config) { const key = this.poolKey(config); if (!this.pgPools.has(key)) { const pool = new pg_1.Pool(config); this.pgPools.set(key, pool); log.info(`📘 Created new pg pool: ${key}`); } return this.pgPools.get(key); } getClient(config) { if (this.shuttingDown) { throw new Error('PgTestConnector is shutting down; no new clients allowed'); } const client = new test_client_1.PgTestClient(config, { trackConnect: (p) => this.registerConnect(p) }); this.clients.add(client); const key = this.dbKey(config); this.seenDbConfigs.set(key, config); log.info(`🔌 New PgTestClient connected to ${config.database}`); return client; } async closeAll() { this.beginTeardown(); await this.awaitPendingConnects(); log.info('🧹 Closing all PgTestClients...'); await Promise.all(Array.from(this.clients).map(async (client) => { try { await client.close(); log.success(`✅ Closed client for ${client.config.database}`); } catch (err) { log.error(`❌ Error closing PgTestClient for ${client.config.database}:`, err); } })); this.clients.clear(); log.info('🧯 Disposing pg pools...'); for (const [key, pool] of this.pgPools.entries()) { log.debug(`🧯 Disposing pg pool [${key}]`); end(pool); } this.pgPools.clear(); log.info('🗑️ Dropping seen databases...'); await Promise.all(Array.from(this.seenDbConfigs.values()).map(async (config) => { try { const rootPg = (0, pg_env_1.getPgEnvOptions)(); const admin = new admin_1.DbAdmin({ ...config, user: rootPg.user, password: rootPg.password }, this.verbose); admin.drop(); log.warn(`🧨 Dropped database: ${config.database}`); } catch (err) { log.error(`❌ Failed to drop database ${config.database}:`, err); } })); this.seenDbConfigs.clear(); log.success('✅ All PgTestClients closed, pools disposed, databases dropped.'); this.pendingConnects.clear(); this.shuttingDown = false; } close() { this.closeAll(); } drop(config) { const key = this.dbKey(config); const admin = new admin_1.DbAdmin(config, this.verbose); admin.drop(); log.warn(`🧨 Dropped database: ${config.database}`); this.seenDbConfigs.delete(key); } async kill(client) { await client.close(); this.drop(client.config); } } exports.PgTestConnector = PgTestConnector;