/** * Database connection utility class * Provides centralized connection handling for both main application and worker processes */ const debug = require('debug')('agm:db'); class DBConnection { constructor(name = 'Database') { this.name = name; this.connection = null; this.isConnected = false; } /** * Connect to MongoDB with comprehensive configuration support * @param {Object} options - Connection options * @param {boolean} options.debugMode - Enable debug mode for mongoose * @param {Function} options.onReady - Callback to execute when DB is ready * @param {boolean} options.exitOnError - Whether to exit process on connection error (default: true) * @param {boolean} options.setupEventListeners - Whether to set up disconnect/error listeners (default: true) * @param {boolean} options.setupExitHandlers - Whether to set up process exit handlers (default: true) * @returns {Object} Mongoose connection */ async connect(options = {}) { const mongoose = require('mongoose'); const fs = require('fs-extra'); const env = require('../env'); const { debugMode = false, onReady = null, exitOnError = true, setupEventListeners = true, setupExitHandlers = true } = options; // Our custom middleware mongoose.Promise = global.Promise; const ops = { connectTimeoutMS: 40000, socketTimeoutMS: 30 * 60 * 1000, keepAlive: true, keepAliveInitialDelay: 30000, autoIndex: !(env.PRODUCTION), maxPoolSize: env.DB_MAX_POOLSIZE, family: 4, } // Validate required environment variables if (!env.DB_HOSTS) { throw new Error('DB_HOSTS environment variable is required'); } if (!env.DB_NAME) { throw new Error('DB_NAME environment variable is required'); } if (!env.DB_USR) { throw new Error('DB_USR environment variable is required'); } if (!env.DB_PWD) { throw new Error('DB_PWD environment variable is required'); } let _uri = `@${env.DB_HOSTS}/${env.DB_NAME}`; if (env.DB_HOSTS && !env.DB_HOSTS.includes(',')) { ops.directConnection = true; } if (env.DB_USE_SCALEGRID) { // ScaleGrid connection with modern MongoDB driver options if (!env.DB_TLS_CA_FILE) { throw new Error('DB_TLS_CA_FILE is required when using ScaleGrid'); } try { // Don't read the cert file if we're disabling SSL validation if (!env.DB_DISABLE_SSL_VALIDATE) { const certFileBuf = await fs.readFile(env.DB_TLS_CA_FILE); } // Modern MongoDB driver configuration for ScaleGrid if (env.DB_USE_TLS) { // Use TLS but with proper workaround for Node.js v16 bug ops.tls = true; // Complete workaround for Node.js v16 TLS assertion bug if (env.DB_DISABLE_SSL_VALIDATE) { // Disable all SSL validation to avoid Node.js TLS assertion error // tlsInsecure encompasses tlsAllowInvalidCertificates and tlsAllowInvalidHostnames ops.tlsInsecure = true; } else { // Use CA file for proper validation (only if not bypassing) ops.tlsCAFile = env.DB_TLS_CA_FILE; } // Additional hostname bypass if needed (only if not using tlsInsecure) if (env.DB_DISABLE_HOSTNAME_VERIFY && !env.DB_DISABLE_SSL_VALIDATE) { ops.tlsAllowInvalidHostnames = true; ops.checkServerIdentity = false; } } // Set replica set in connection options (not deprecated replset) if (env.DB_REPLSET) { ops.replicaSet = env.DB_REPLSET; } // Set authentication options for ScaleGrid ops.authSource = env.DB_AUTH_SOURCE || 'admin'; // Use env variable, fallback to admin ops.authMechanism = 'SCRAM-SHA-256'; } catch (certError) { console.error(`Error loading ScaleGrid CA certificate: ${certError.message}`); throw certError; } // Build ScaleGrid connection string - connect to target database, authenticate via authSource _uri = `mongodb://${encodeURIComponent(env.DB_USR)}:${encodeURIComponent(env.DB_PWD)}@${env.DB_HOSTS}/${env.DB_NAME}`; } else { if (env.DB_REPLSET) ops.replicaSet = env.DB_REPLSET if (env.DB_USE_TLS) { ops.tls = true; ops.sslValidate = true; ops.tlsCAFile = env.DB_TLS_CA_FILE; ops.tlsCertificateKeyFile = env.DB_TLS_CERT_FILE; } if (env.DB_USE_X509) { ops.authMechanism = 'MONGODB-X509'; _uri = `mongodb://${encodeURIComponent(env.DB_USR)}` + _uri; } else { _uri = `mongodb://${env.DB_USR}:${env.DB_PWD}` + _uri; ops.authSource = env.DB_AUTH_SOURCE || env.DB_NAME ops.authMechanism = 'SCRAM-SHA-1' } } mongoose.set('strictQuery', false); mongoose.set('strictPopulate', false); mongoose.set('bufferCommands', false); // Disable buffering globally // Debug logging before connection debug(`Initializing database connection for ${this.name}...`); debug(`Connection URI: ${_uri.replace(/\/\/[^:]+:[^@]+@/, '//***:***@')}`); debug(`Connection options:`, JSON.stringify(ops, (key, value) => { if (key === 'tlsCAFile' || key === 'sslCA') return '[Certificate Path/Data]'; return value; }, 2)); try { // IMPORTANT: Await the connection await mongoose.connect(_uri, ops); this.connection = mongoose.connection; this.isConnected = true; debug(`✅-> MongoDB connected - ${this.name} ready`); // Set up event listeners for connection monitoring if (setupEventListeners) { this.setupEventListeners(exitOnError); } // Set up process exit handlers if (setupExitHandlers) { this.setupExitHandlers(); } if (debugMode) mongoose.set('debug', true); // Execute ready callback if provided if (onReady && typeof onReady === 'function') { await onReady(); } } catch (error) { debug(`-> MongoDB connection failed for ${this.name}: ${error.message}`); if (exitOnError) { process.exit(1); } else { throw error; } } return this.connection; } /** * Set up connection event listeners * @param {boolean} exitOnError - Whether to exit process on connection issues */ setupEventListeners(exitOnError = true) { if (!this.connection) { debug('Warning: No connection available for event listeners'); return; } this.connection.on('disconnected', () => { debug(`-> MongoDB lost connection for ${this.name}`); this.isConnected = false; if (exitOnError) { debug('-> Going to exit...'); process.exit(1); } }); this.connection.on('error', (err) => { console.error(`MongoDB connection error for ${this.name}:`, err); this.isConnected = false; if (exitOnError) { debug('-> Going to exit...'); process.exit(1); } }); this.connection.on('reconnected', () => { debug(`-> MongoDB reconnected for ${this.name}`); this.isConnected = true; }); this.connection.on('reconnect', () => { debug(`-> MongoDB reconnect event for ${this.name}`); }); this.connection.on('reconnectFailed', () => { debug(`-> MongoDB gave up reconnecting for ${this.name}`); }); } /** * Set up process exit handlers */ setupExitHandlers() { const postExit = () => { if (this.connection) { this.connection.close(() => { debug(`Mongoose connection for ${this.name} disconnected through app termination`); process.exit(0); }); } else { process.exit(0); } }; process.on('SIGINT', postExit); process.on('SIGTERM', postExit); } /** * Check if database connection is ready for operations * @returns {boolean} True if connection is ready */ isReady() { return this.connection && this.connection.readyState === 1 && this.isConnected; } /** * Get the mongoose connection object * @returns {Object} Mongoose connection */ getConnection() { return this.connection; } /** * Close the database connection gracefully */ async close() { if (this.connection) { await this.connection.close(); this.isConnected = false; debug(`-> ${this.name} database connection closed`); } } /** * Simple initialization with sensible defaults * @param {Object} options - Configuration options * @returns {Object} Mongoose connection */ async initialize(options = {}) { const defaults = { setupEventListeners: true, setupExitHandlers: true, // Can be overridden for workers/special cases exitOnError: true }; return await this.connect({ ...defaults, ...options }); } } /** * Legacy connect function for backward compatibility * @param {boolean} debugMode - Enable debug mode for mongoose * @returns {Object} Mongoose connection */ async function connect(debugMode = false) { const dbConnection = new DBConnection('Main Application'); return await dbConnection.initialize({ debugMode }); } // Export the class and utility functions module.exports = connect; // Default export for backward compatibility module.exports.connect = connect; module.exports.DBConnection = DBConnection;