316 lines
9.7 KiB
JavaScript
316 lines
9.7 KiB
JavaScript
/**
|
|
* 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;
|
|
if (env.DB_TLS_CERT_FILE) {
|
|
const certExists = await fs.pathExists(env.DB_TLS_CERT_FILE);
|
|
if (certExists) {
|
|
ops.tlsCertificateKeyFile = env.DB_TLS_CERT_FILE;
|
|
} else if (env.DB_USE_X509) {
|
|
throw new Error(`DB_TLS_CERT_FILE not found: ${env.DB_TLS_CERT_FILE}`);
|
|
} else {
|
|
console.warn(`DB TLS client cert file not found, continuing without 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;
|