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
JavaScript
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;
;