agmission/Development/server/helpers/db/connect.js

307 lines
9.3 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;
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;