cube-ms
Version:
Production-ready microservice framework with health monitoring, validation, error handling, and Docker Swarm support
273 lines (240 loc) • 7.89 kB
JavaScript
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;