UNPKG

@bitblit/ratchet-rdbms

Version:

Ratchet tooling for working with relational databases

202 lines 9.28 kB
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