multibridge
Version:
A multi-database connection framework with centralized configuration
349 lines (348 loc) • 13.1 kB
JavaScript
;
var __importDefault = (this && this.__importDefault) || function (mod) {
return (mod && mod.__esModule) ? mod : { "default": mod };
};
Object.defineProperty(exports, "__esModule", { value: true });
exports.getConnection = getConnection;
exports.closeConnection = closeConnection;
exports.closeAllConnections = closeAllConnections;
exports.getConnectionStats = getConnectionStats;
const dbConfig_1 = require("../config/dbConfig");
const tenantContext_1 = require("../context/tenantContext");
const postgres_1 = require("./postgres");
const mysql_1 = require("./mysql");
const mongodb_1 = require("./mongodb");
const cassandra_1 = require("./cassandra");
const loggers_1 = __importDefault(require("../utils/loggers"));
const lruCache_1 = require("../utils/lruCache");
const rateLimiter_1 = require("../utils/rateLimiter");
const envConfig_1 = require("../config/envConfig");
const errors_1 = require("../utils/errors");
// LRU Cache for connections with configurable size and TTL
const connectionCache = new lruCache_1.LRUCache(envConfig_1.envConfig.CONNECTION_CACHE_MAX_SIZE, envConfig_1.envConfig.CONNECTION_CACHE_TTL_MS || undefined);
// Track in-flight connection creations to prevent race conditions
const pendingConnections = new Map();
// Rate limiter to prevent excessive connection creation
const rateLimiter = new rateLimiter_1.RateLimiter(envConfig_1.envConfig.RATE_LIMIT_MAX_REQUESTS, envConfig_1.envConfig.RATE_LIMIT_WINDOW_MS);
function generateCacheKey(appid, orgid, appdbname) {
return `${appid}-${orgid}-${appdbname}`;
}
// Type guard to identify SQL pools
function isSQLPool(connection) {
return "query" in connection;
}
async function isSQLConnectionValid(connection, cacheKey) {
try {
if (isSQLPool(connection)) {
// Use explicit typing to help TypeScript with overload resolution
if ("execute" in connection) {
// MySQL pool
await connection.query("SELECT 1");
}
else {
// PostgreSQL pool
await connection.query("SELECT 1");
}
return true;
}
return false;
}
catch (error) {
loggers_1.default.warn(`SQL connection validation failed for ${cacheKey}: ${error.message}`, {
cacheKey,
error: error.stack,
});
return false;
}
}
async function isMongoConnectionValid(connection, cacheKey) {
try {
await connection.client.db().admin().ping();
return true;
}
catch (error) {
loggers_1.default.warn(`MongoDB connection validation failed for ${cacheKey}: ${error.message}`, {
cacheKey,
error: error.stack,
});
return false;
}
}
async function isCassandraConnectionValid(connection, cacheKey) {
try {
await connection.execute("SELECT release_version FROM system.local");
return true;
}
catch (error) {
loggers_1.default.warn(`Cassandra connection validation failed for ${cacheKey}: ${error.message}`, {
cacheKey,
error: error.stack,
});
return false;
}
}
async function isConnectionValid(cached, cacheKey) {
switch (cached.dbType) {
case "postgres":
case "mysql":
return isSQLConnectionValid(cached.connection, cacheKey);
case "mongodb":
return isMongoConnectionValid(cached.connection, cacheKey);
case "cassandra":
return isCassandraConnectionValid(cached.connection, cacheKey);
default:
return true;
}
}
/**
* Check if connection needs validation based on TTL
*/
function needsValidation(cached) {
if (!cached.lastValidated) {
return true;
}
const validationTTL = envConfig_1.envConfig.CONNECTION_VALIDATION_TTL_MS;
if (validationTTL <= 0) {
return false; // Validation disabled
}
return Date.now() - cached.lastValidated > validationTTL;
}
async function _closeAndLog(connection, dbType, cacheKey) {
try {
switch (dbType) {
case "postgres":
case "mysql":
await connection.end();
loggers_1.default.info(`[MultiBridge] SQL connection closed for ${cacheKey}`);
break;
case "mongodb":
await connection.client.close();
loggers_1.default.info(`[MultiBridge] MongoDB connection closed for ${cacheKey}`);
break;
case "cassandra":
await connection.shutdown();
loggers_1.default.info(`[MultiBridge] Cassandra connection closed for ${cacheKey}`);
break;
default:
loggers_1.default.warn(`[MultiBridge] Unknown DB type or close method missing for ${cacheKey}`);
}
}
catch (error) {
loggers_1.default.error(`Error closing connection for ${cacheKey}: ${error.message}`, {
cacheKey,
dbType,
error: error.stack,
});
}
}
/**
* Retry connection creation with exponential backoff
*/
async function _createConnectionWithRetry(dbConfig, schema, attempt = 1) {
const maxAttempts = envConfig_1.envConfig.CONNECTION_RETRY_ATTEMPTS;
const baseDelay = envConfig_1.envConfig.CONNECTION_RETRY_DELAY_MS;
try {
return await _createConnection(dbConfig, schema);
}
catch (error) {
// Check if error is retryable
const isRetryable = isRetryableError(error);
if (!isRetryable || attempt >= maxAttempts) {
throw error;
}
// Calculate exponential backoff delay
const delay = baseDelay * Math.pow(2, attempt - 1);
loggers_1.default.warn(`Connection creation failed (attempt ${attempt}/${maxAttempts}), retrying in ${delay}ms: ${error.message}`, {
attempt,
maxAttempts,
delay,
dbType: dbConfig.db_type,
error: error.stack,
});
await new Promise((resolve) => setTimeout(resolve, delay));
return _createConnectionWithRetry(dbConfig, schema, attempt + 1);
}
}
/**
* Check if an error is retryable
*/
function isRetryableError(error) {
if (!error)
return false;
const errorMessage = error.message?.toLowerCase() || "";
const retryablePatterns = [
"econnrefused",
"etimedout",
"timeout",
"econnreset",
"enotfound",
"temporary",
"retry",
"connection",
];
return retryablePatterns.some((pattern) => errorMessage.includes(pattern));
}
async function _createConnection(dbConfig, schema) {
const { db_type: dbType } = dbConfig;
switch (dbType) {
case "postgres":
case "mysql":
if (!schema) {
throw new errors_1.ConnectionError(`Database schema is required for connection type '${dbType}' but was not found.`, {
dbType,
});
}
const sqlConfig = {
host: dbConfig.host,
port: dbConfig.port,
username: dbConfig.username,
password: dbConfig.password,
database: dbConfig.database,
schema: schema,
};
return dbType === "postgres" ? (0, postgres_1.createPostgresConnection)(sqlConfig) : (0, mysql_1.createMySQLConnection)(sqlConfig);
case "mongodb":
return (0, mongodb_1.createMongoDBConnection)({
host: dbConfig.host,
port: dbConfig.port,
username: dbConfig.username,
password: dbConfig.password,
database: dbConfig.database,
});
case "cassandra":
if (!dbConfig.data_center) {
throw new errors_1.ConnectionError(`'data_center' is required for Cassandra connections.`, {
dbType,
});
}
return (0, cassandra_1.createCassandraConnection)({
host: dbConfig.host,
port: dbConfig.port,
username: dbConfig.username,
password: dbConfig.password,
database: dbConfig.database,
dataCenter: dbConfig.data_center,
});
default:
throw new errors_1.ConnectionError(`Unsupported database type: ${dbType}`, {
dbType,
});
}
}
async function getConnection(tenant) {
const currentTenant = tenant || (0, tenantContext_1.getTenant)();
if (!currentTenant) {
throw new errors_1.TenantContextError("No tenant context available");
}
const { appid, orgid, appdbname } = currentTenant;
const cacheKey = generateCacheKey(appid, orgid, appdbname);
// Check rate limit
if (!rateLimiter.check(cacheKey)) {
throw new errors_1.ConnectionError(`Rate limit exceeded for connection creation. Max ${envConfig_1.envConfig.RATE_LIMIT_MAX_REQUESTS} requests per ${envConfig_1.envConfig.RATE_LIMIT_WINDOW_MS}ms`, { cacheKey, appid, orgid, appdbname });
}
// Check if connection is being created (race condition prevention)
const pending = pendingConnections.get(cacheKey);
if (pending) {
loggers_1.default.debug(`[MultiBridge] Waiting for pending connection creation for ${cacheKey}`);
return pending;
}
// Check cache
const cached = connectionCache.get(cacheKey);
if (cached) {
// Lazy validation: only validate if needed
if (!needsValidation(cached)) {
loggers_1.default.debug(`[MultiBridge] Reusing cached connection for ${cacheKey} (validation skipped)`);
return cached;
}
// Validate connection
if (await isConnectionValid(cached, cacheKey)) {
// Update validation timestamp
cached.lastValidated = Date.now();
connectionCache.set(cacheKey, cached);
loggers_1.default.debug(`[MultiBridge] Reusing valid connection for ${cacheKey}`);
return cached;
}
else {
loggers_1.default.warn(`[MultiBridge] Cached connection for ${cacheKey} is stale. Closing and recreating connection.`);
await _closeAndLog(cached.connection, cached.dbType, cacheKey);
connectionCache.delete(cacheKey);
}
}
// Create new connection with race condition prevention
const connectionPromise = (async () => {
try {
const dbConfig = await (0, dbConfig_1.fetchDBConfig)(appid, orgid);
if (!dbConfig) {
throw new errors_1.ConnectionError(`No configuration found for appid: ${appid}, orgid: ${orgid}`, {
appid,
orgid,
});
}
const schema = appdbname || dbConfig.schema;
const connection = await _createConnectionWithRetry(dbConfig, schema);
const ret = {
connection,
dbType: dbConfig.db_type,
config: { schema },
lastValidated: Date.now(),
};
connectionCache.set(cacheKey, ret);
loggers_1.default.info(`[MultiBridge] Created new connection for ${cacheKey}`, {
cacheKey,
dbType: dbConfig.db_type,
appid,
orgid,
});
return ret;
}
finally {
// Remove from pending connections
pendingConnections.delete(cacheKey);
}
})();
// Store pending connection to prevent duplicates
pendingConnections.set(cacheKey, connectionPromise);
return connectionPromise;
}
async function closeConnection(tenant) {
const currentTenant = tenant || (0, tenantContext_1.getTenant)();
if (!currentTenant)
return;
const cacheKey = generateCacheKey(currentTenant.appid, currentTenant.orgid, currentTenant.appdbname);
const cached = connectionCache.get(cacheKey);
if (cached) {
await _closeAndLog(cached.connection, cached.dbType, cacheKey);
connectionCache.delete(cacheKey);
rateLimiter.reset(cacheKey);
}
}
async function closeAllConnections() {
const entries = [];
const keys = [];
for (const key of connectionCache.keys()) {
keys.push(key);
}
for (const cacheKey of keys) {
const data = connectionCache.get(cacheKey);
if (data) {
entries.push([cacheKey, data]);
}
}
for (const [cacheKey, { connection, dbType }] of entries) {
await _closeAndLog(connection, dbType, cacheKey);
}
connectionCache.clear();
pendingConnections.clear();
rateLimiter.clear();
loggers_1.default.info("[MultiBridge] All connections closed.");
}
/**
* Get connection pool statistics for monitoring
*/
function getConnectionStats() {
return {
cachedConnections: connectionCache.size(),
pendingConnections: pendingConnections.size,
cacheMaxSize: envConfig_1.envConfig.CONNECTION_CACHE_MAX_SIZE,
};
}