@bitblit/ratchet-rdbms
Version:
Ratchet tooling for working with relational databases
202 lines • 9.28 kB
JavaScript
import { RequireRatchet } from '@bitblit/ratchet-common/lang/require-ratchet';
import { Logger } from '@bitblit/ratchet-common/logger/logger';
import { ErrorRatchet } from '@bitblit/ratchet-common/lang/error-ratchet';
import { StringRatchet } from '@bitblit/ratchet-common/lang/string-ratchet';
import getPort from 'get-port';
import { Client } from 'pg';
import { PostgresStyleDatabaseAccess } from "./postgres-style-database-access.js";
export class PostgresStyleConnectionProvider {
configPromiseProvider;
ssh;
connectionCache = new Map();
cacheConfigPromise;
constructor(configPromiseProvider, ssh) {
this.configPromiseProvider = configPromiseProvider;
this.ssh = ssh;
this.cacheConfigPromise = this.createConnectionConfig();
Logger.info('Added shutdown handler to the process (Only once per instantiation)');
this.addShutdownHandlerToProcess();
}
get usingSshTunnel() {
return !!this.ssh;
}
addShutdownHandlerToProcess() {
process.on('exit', () => {
Logger.info('Process is shutting down, closing connections');
this.clearDatabaseAccessCache().catch((err) => {
Logger.error('Shutdown connection failed : %s', err);
});
});
}
async clearDatabaseAccessCache() {
const rval = false;
Logger.info('Clearing connection cache for PostgresStyleConnectionProvider');
const oldConnections = Array.from(this.connectionCache.values());
this.cacheConfigPromise = null;
this.connectionCache = new Map();
if (oldConnections.length > 0) {
for (let i = 0; i < oldConnections.length; i++) {
Logger.info('Shutting down old connection %d of %d', i, oldConnections.length);
try {
const conn = await oldConnections[i];
Logger.info('Conn %d is %s', i, conn?.config?.label);
if (conn.db) {
Logger.info('Stopping connection to database');
try {
await conn.db.end();
Logger.info('Database connection closed');
}
catch (err) {
if (ErrorRatchet.asErr(err).message.includes('closed state')) {
}
else {
Logger.error('Something went wrong closing the database connection : %s', err);
}
}
}
if (conn.ssh) {
try {
Logger.info('Stopping ssh tunnel');
await this.ssh.shutdown(conn.ssh);
Logger.info('Ssh tunnel stopped');
}
catch (err) {
Logger.warn('Failed to stop ssh tunnel : %s', err, err);
}
}
}
catch (err) {
Logger.warn('Shutdown failed : %s ', err, err);
}
}
}
Logger.info('Old db and tunnels removed');
return rval;
}
async getConnectionAndTunnel(name) {
Logger.silly('getConnectionAndTunnel : %s', name);
if (!this.connectionCache.has(name)) {
Logger.info('No connectionCache found for %s - creating new one', name);
const dbConfig = await this.getDbConfig(name);
const connection = this.createConnectionAndTunnel(dbConfig, true);
this.connectionCache.set(name, connection);
Logger.info('Added connectionCache for %s', name);
}
return this.connectionCache.get(name);
}
async getDatabaseAccess(name) {
Logger.silly('getConnection : %s', name);
const conn = await this.getConnectionAndTunnel(name);
const dbConfig = await this.getDbConfig(name);
const rval = conn?.db ? new PostgresStyleDatabaseAccess(conn.db, dbConfig) : null;
return rval;
}
async createNonPooledConnectionAndTunnel(queryDefaults) {
Logger.info('Creating non-pooled connection for %s', queryDefaults.databaseName);
const dbConfig = await this.getDbConfig(queryDefaults.databaseName);
const rval = await this.createConnectionAndTunnel(dbConfig, false);
return rval;
}
async createNonPooledDatabaseConnection(queryDefaults) {
const conTunnel = await this.createNonPooledConnectionAndTunnel(queryDefaults);
return conTunnel?.db;
}
async getDbConfig(name) {
Logger.info('PostgresStyleConnectionProvider:getDbConfig:Initiating promise for %s', name);
const cfgs = await this.configPromise();
const finder = StringRatchet.trimToEmpty(name).toLowerCase();
const dbConfig = cfgs.dbList.find((s) => StringRatchet.trimToEmpty(s.label).toLowerCase() === finder);
if (!dbConfig) {
throw ErrorRatchet.fErr('Cannot find any connection config named %s (Available are %j)', name, cfgs.dbList.map((d) => d.label));
}
return dbConfig;
}
async createConnectionAndTunnel(dbCfg, clearCacheOnConnectionFailure) {
Logger.info('In PostgresStyleConnectionProvider:createConnectionAndTunnel : %s', dbCfg.label);
RequireRatchet.notNullOrUndefined(dbCfg, 'dbCfg');
let tunnel = null;
if (dbCfg.sshTunnelConfig) {
const localPort = dbCfg.sshTunnelConfig.forceLocalPort || (await getPort());
Logger.debug('SSH tunnel config found, opening tunnel to %s / %s to using local port %s', dbCfg.sshTunnelConfig.host, dbCfg.sshTunnelConfig.port, localPort);
tunnel = await this.ssh.createSSHTunnel(dbCfg.sshTunnelConfig, dbCfg.dbConfig.host, dbCfg.dbConfig.port, localPort);
Logger.debug('SSH Tunnel open');
}
else {
Logger.debug('No ssh configuration - skipping tunnel');
}
Logger.debug('Opening connection for PostgresStyleConnectionProvider');
let connection;
try {
const cfgCopy = structuredClone(dbCfg);
delete cfgCopy.label;
delete cfgCopy.sshTunnelConfig;
if (tunnel) {
cfgCopy.dbConfig.host = 'localhost';
cfgCopy.dbConfig.port = tunnel.localPort;
}
connection = new Client(cfgCopy.dbConfig);
await connection.connect();
}
catch (err) {
Logger.info('Failed trying to create connection : %s : clearing for retry', err);
if (clearCacheOnConnectionFailure) {
this.connectionCache = new Map();
}
return undefined;
}
const rval = {
config: dbCfg,
db: connection,
ssh: tunnel,
};
return rval;
}
configPromise() {
if (!this.cacheConfigPromise) {
this.cacheConfigPromise = this.createConnectionConfig();
}
return this.cacheConfigPromise;
}
async createConnectionConfig() {
RequireRatchet.notNullOrUndefined(this.configPromiseProvider, 'input');
const inputPromise = this.configPromiseProvider();
Logger.info('Creating connection config');
const cfg = await inputPromise;
RequireRatchet.true(cfg.dbList.length > 0, 'input.dbList');
cfg.dbList.forEach((db) => {
const errors = PostgresStyleConnectionProvider.validDbConfig(db);
if (errors?.length) {
throw ErrorRatchet.fErr('Errors found in db config : %j', errors);
}
});
return cfg;
}
static validDbConfig(cfg) {
let rval = [];
if (!cfg) {
rval.push('The config is null');
}
else if (!cfg.dbConfig) {
rval.push('The options field is null');
}
else {
if (StringRatchet.trimToNull(cfg?.dbConfig?.connectionString)) {
}
else {
rval.push(StringRatchet.trimToNull(cfg.dbConfig.host) ? null : 'host is required and non-empty');
rval.push(StringRatchet.trimToNull(cfg.label) ? null : 'label is required and non-empty');
rval.push(StringRatchet.trimToNull(cfg.dbConfig.database) ? null : 'database is required and non-empty');
rval.push(StringRatchet.trimToNull(cfg.dbConfig.user) ? null : 'user is required and non-empty');
rval.push(cfg.dbConfig.password ? null : 'password is required and non-empty');
rval.push(cfg.dbConfig.port ? null : 'port is required and non-empty');
}
}
if (cfg.sshTunnelConfig) {
rval.push(StringRatchet.trimToNull(cfg.sshTunnelConfig.host) ? null : 'If sshTunnelConfig is non-null, host is required and non-empty');
rval.push(cfg.sshTunnelConfig.port ? null : 'If sshTunnelConfig is non-null, port is required and non-empty');
}
rval = rval.filter((s) => !!s);
return rval;
}
}
//# sourceMappingURL=postgres-style-connection-provider.js.map