UNPKG

cube-ms

Version:

Production-ready microservice framework with health monitoring, validation, error handling, and Docker Swarm support

273 lines (240 loc) 7.89 kB
import connectionManager from "./db_connection_manager.js"; import { CreateLogger } from "./db_logger.js"; class MultiDatabaseManager { constructor() { this.databases = new Map(); this.configurations = new Map(); this.logger = CreateLogger('MultiDatabaseManager', 'database', 'manager'); } /** * Register a database connection configuration * @param {string} name - Database connection name (e.g., 'primary', 'analytics', 'logs') * @param {string} url - MongoDB connection URL * @param {object} options - MongoDB connection options * @param {object} config - Additional configuration */ registerDatabase(name, url, options = {}, config = {}) { this.configurations.set(name, { url, options, config: { description: config.description || `${name} database`, readPreference: config.readPreference || 'primary', retryWrites: config.retryWrites !== false, ...config } }); this.logger.info(`Database configuration registered`, { name, url: this.maskUrl(url), description: config.description }); } /** * Get database connection by name * @param {string} name - Database connection name * @returns {Promise<object>} MongoDB client and database instance */ async getDatabase(name) { if (!this.configurations.has(name)) { throw new Error(`Database configuration '${name}' not found. Available: ${Array.from(this.configurations.keys()).join(', ')}`); } if (this.databases.has(name)) { return this.databases.get(name); } const config = this.configurations.get(name); try { this.logger.info(`Connecting to database`, { name, description: config.config.description }); const client = await connectionManager.getClient(config.url, config.options); const dbName = this.extractDbNameFromUrl(config.url); const db = client.db(dbName); const databaseConnection = { client, db, name, url: config.url, config: config.config, isConnected: () => client.topology?.isConnected() || false, ping: async () => { try { await db.admin().ping(); return true; } catch (error) { this.logger.error(`Database ping failed`, { name, error: error.message }); return false; } } }; this.databases.set(name, databaseConnection); this.logger.info(`Database connection established`, { name, database: dbName, description: config.config.description }); return databaseConnection; } catch (error) { this.logger.error(`Failed to connect to database`, { name, url: this.maskUrl(config.url), error: error.message }); throw error; } } /** * Get multiple databases at once * @param {string[]} names - Array of database names * @returns {Promise<object>} Object with database connections keyed by name */ async getDatabases(names) { const connections = {}; const promises = names.map(async (name) => { connections[name] = await this.getDatabase(name); }); await Promise.all(promises); return connections; } /** * Get all registered database configurations * @returns {Array} Array of database configuration info */ getRegisteredDatabases() { return Array.from(this.configurations.entries()).map(([name, config]) => ({ name, url: this.maskUrl(config.url), description: config.config.description, readPreference: config.config.readPreference, isConnected: this.databases.has(name) ? this.databases.get(name).isConnected() : false })); } /** * Health check for all registered databases * @returns {Promise<object>} Health status for all databases */ async healthCheck() { const status = { overall: 'healthy', databases: {}, totalConnections: this.databases.size, registeredDatabases: this.configurations.size }; const promises = Array.from(this.databases.entries()).map(async ([name, connection]) => { try { const isHealthy = await connection.ping(); status.databases[name] = { status: isHealthy ? 'healthy' : 'unhealthy', connected: connection.isConnected(), description: connection.config.description }; if (!isHealthy) { status.overall = 'degraded'; } } catch (error) { status.databases[name] = { status: 'error', connected: false, error: error.message, description: connection.config.description }; status.overall = 'degraded'; } }); await Promise.all(promises); return status; } /** * Close specific database connection * @param {string} name - Database name to close */ async closeDatabaseConnection(name) { const connection = this.databases.get(name); if (connection) { await connectionManager.closeConnection(connection.url); this.databases.delete(name); this.logger.info(`Database connection closed`, { name }); } } /** * Close all database connections */ async closeAllConnections() { const promises = Array.from(this.databases.keys()).map(name => this.closeDatabaseConnection(name).catch(error => this.logger.error(`Error closing database connection`, { name, error: error.message }) ) ); await Promise.all(promises); this.databases.clear(); this.logger.info('All database connections closed'); } /** * Create a database transaction across multiple databases (if supported) * @param {string[]} databaseNames - Names of databases to include in transaction * @param {function} callback - Transaction callback function */ async withTransaction(databaseNames, callback) { const connections = await this.getDatabases(databaseNames); const sessions = {}; try { // Start sessions for each database for (const [name, connection] of Object.entries(connections)) { sessions[name] = connection.client.startSession(); } // Start transactions for (const session of Object.values(sessions)) { session.startTransaction(); } // Execute callback with sessions const result = await callback(connections, sessions); // Commit all transactions for (const session of Object.values(sessions)) { await session.commitTransaction(); } return result; } catch (error) { // Abort all transactions on error for (const session of Object.values(sessions)) { try { await session.abortTransaction(); } catch (abortError) { this.logger.error('Failed to abort transaction', { error: abortError.message }); } } throw error; } finally { // End all sessions for (const session of Object.values(sessions)) { await session.endSession(); } } } /** * Utility method to mask sensitive parts of URL * @private */ maskUrl(url) { return url.replace(/:\/\/[^:]+:[^@]+@/, '://***:***@'); } /** * Extract database name from MongoDB URL * @private */ extractDbNameFromUrl(url) { const match = url.match(/\/([^?]+)(\?|$)/); return match ? match[1] : 'default'; } /** * Get connection statistics */ getStats() { return { registeredDatabases: this.configurations.size, activeDatabases: this.databases.size, totalConnections: connectionManager.getConnectionCount(), databases: this.getRegisteredDatabases() }; } } // Singleton instance const multiDatabaseManager = new MultiDatabaseManager(); export default multiDatabaseManager;