Last active
September 8, 2024 16:42
-
-
Save kevboutin/cc4e49b08a1ff763e5094dfb2a6dea6a to your computer and use it in GitHub Desktop.
The DatabaseService class
This file contains hidden or bidirectional Unicode text that may be interpreted or compiled differently than what appears below. To review, open the file in an editor that reveals hidden Unicode characters.
Learn more about bidirectional Unicode characters
const mongoose = require('mongoose').set('debug', process.env.NODE_ENV !== 'production'); | |
/** @constant {boolean} */ | |
const DB_BUFFER_COMMANDS = process.env.DB_BUFFER_COMMANDS === 'true'; | |
/** | |
* Database options. | |
* @typedef {Object.<string, boolean|number|string>} DatabaseOptions | |
*/ | |
/** | |
* Default database options. | |
* @type {Object.<string, boolean|number|string>} | |
*/ | |
const defaultMongoOpts = { | |
/* | |
bufferCommands should be true and bufferMaxEntries should be -1 or non zero to | |
make autoReconnect behave correctly. Currently there is a bug when setting bufferCommands true | |
thus commenting it for now https://github.com/Automattic/mongoose/issues/9218 | |
*/ | |
bufferCommands: DB_BUFFER_COMMANDS, | |
readPreference: process.env.DB_READ_PREF || 'secondaryPreferred', | |
minPoolSize: parseInt(process.env.DB_MIN_POOL_SIZE, 10) || 2, | |
maxPoolSize: parseInt(process.env.DB_MAX_POOL_SIZE, 10) || 6, | |
// The connectTimeoutMS sets the number of milliseconds a socket will stay inactive before closing | |
// during the connection phase of the driver | |
connectTimeoutMS: parseInt(process.env.DB_CONNECT_TIMEOUT, 10) || 2000, | |
// The socketTimeoutMS sets the number of milliseconds a socket will stay inactive after the driver | |
// has successfully connected before closing. | |
socketTimeoutMS: parseInt(process.env.DB_SOCKET_TIMEOUT, 10) || 120000, | |
// The maximum number of milliseconds that a connection can remain idle in the pool before being removed and closed. | |
maxIdleTimeMS: parseInt(process.env.DB_MAX_IDLE_TIME, 10) || 3550000, | |
}; | |
/** | |
* @class DatabaseService | |
*/ | |
class DatabaseService { | |
/** | |
* Creates a DatabaseService. | |
* | |
* @constructor | |
* @param {string} dbUri The database URI. | |
* @param {DatabaseOptions} databaseOpts The DatabaseOptions. | |
* @param {string} dbName The database name. | |
*/ | |
constructor({ dbUri, databaseOpts, dbName }) { | |
this.dbUri = dbUri; | |
this.dbName = dbName; | |
this.databaseOpts = { ...defaultMongoOpts, ...databaseOpts }; | |
this.connection = null; | |
this.initing = false; | |
} | |
/** | |
* Get the database name. | |
* | |
* @return {string} The database name. | |
*/ | |
getDatabaseName() { | |
return this.dbName; | |
} | |
/** | |
* Set the database name. This will force a connection object change. | |
* | |
* @param {string} dbName The database name. | |
*/ | |
setDatabaseName(dbName) { | |
this.dbName = dbName; | |
// The useDb() function will update the connection object. | |
if (this.connection) this.connection = this.connection.useDb(dbName, { useCache: true }); | |
} | |
/** | |
* Returns a promise that returns a new connection. If a connection was already created, it will return a | |
* cached connection. Skip cached connection by setting requireNewConnection to true. | |
* | |
* @param {Object} context The context. | |
* @param {boolean} requireNewConnection True to return a new connection. This is usually needed when | |
* multiple concurrent transactions are needed. The connection needs to be managed by the | |
* caller (by closing it appropriately) since the DatabaseService loses all reference to it. | |
* @throws {Object} Throws an error if connection fails to open. | |
* @return {Promise<object>} The connection. | |
*/ | |
async createConnection(context, requireNewConnection = false) { | |
const secureUri = this.dbUri.replace(/\/\/.*@/, '//***:***@'); | |
if (requireNewConnection) { | |
console.log(`createConnection: Creating a new database connection to ${secureUri}.`); | |
const connection = await mongoose | |
.createConnection(`${this.dbUri}${this.dbName}`, this.databaseOpts) | |
.asPromise(); | |
connection.on('disconnected', async () => { | |
console.log( | |
`Lost connection to ${secureUri} so closing database connection as part of cleanup.`, | |
); | |
await this.closeConnection(connection); | |
}); | |
connection.on('error', (err) => { | |
console.log(`${TAG}::Error occurred with established connection.`, err); | |
}); | |
return connection; | |
} | |
if (this.connection === null && !this.initing) { | |
this.initing = true; | |
console.log(`createConnection: Creating new connection and caching it to ${secureUri}.`); | |
this.connection = await mongoose | |
.createConnection(`${this.dbUri}${this.dbName}`, this.databaseOpts) | |
.asPromise(); | |
const connectedDbName = this.connection.db.databaseName || null; | |
console.log( | |
`createConnection: connection was created. Total of ${mongoose.connections.length} connections to host ${this.connection.host} with db ${connectedDbName}.`, | |
); | |
this.initing = false; | |
this.connection.on('disconnected', async () => { | |
console.log( | |
`Lost connection to ${secureUri} so closing database connection as part of cleanup.`, | |
); | |
await this.closeConnection(this.connection); | |
}); | |
this.connection.on('error', (err) => { | |
console.log(`${TAG}::Error occurred with established connection.`, err); | |
}); | |
return this.connection; | |
} | |
if (await !this.isConnectionAlive(this.connection)) { | |
console.log( | |
`createConnection: Connection is not alive, creating new connection and caching it to ${secureUri}.`, | |
); | |
this.connection = await mongoose | |
.createConnection(`${this.dbUri}${this.dbName}`, this.databaseOpts) | |
.asPromise(); | |
const connectedDbName = this.connection.db.databaseName || null; | |
console.log( | |
`createConnection: connection was created. Total of ${mongoose.connections.length} connections to host ${this.connection.host} with db ${connectedDbName}.`, | |
); | |
this.connection.on('disconnected', async () => { | |
console.log( | |
`${TAG}::Lost connection to ${secureUri} so closing database connection as part of cleanup.`, | |
); | |
await this.closeConnection(this.connection); | |
}); | |
this.connection.on('error', (err) => { | |
console.log(`${TAG}::Error occurred with established connection.`, err); | |
}); | |
return this.connection; | |
} | |
const connectedDbName = this.connection.db.databaseName || null; | |
console.log( | |
`Using cached connection. Total of ${mongoose.connections.length} connections to host ${this.connection.host} with db ${connectedDbName}.`, | |
); | |
return this.connection; | |
} | |
/** | |
* Determines if the database connection is alive/active. | |
* | |
* @param {Object} conn The database connection. | |
* @return {boolean} True if the connection is still considered active. | |
*/ | |
async isConnectionAlive(conn) { | |
if (!conn) return false; | |
console.log(`Database connection readyState:`, conn.readyState); | |
// Return false if not connected or connecting | |
if (conn.readyState !== 1 && conn.readyState !== 2) return false; | |
try { | |
const adminUtil = conn.db.admin(); | |
const result = await adminUtil.ping(); | |
console.log("Ping result: ", result); // { ok: 1 } | |
return !!result?.ok === 1; | |
} catch (error) { | |
console.log("Error with ping: ", error); | |
return false; | |
} | |
} | |
/** | |
* Returns a DB connection by calling createConnection. If a function is passed, it will pass the connection | |
* to the function and run the function. | |
* | |
* @param {Object} context The context. | |
* @param {function} fn Function worker that accepts a connection. This is optional. | |
* @param {boolean} requireNewConnection True if we force a connection to be created. | |
* @return {Promise<object>} The result of the fn or the connection itself. | |
*/ | |
async getConnection(context, fn, requireNewConnection = false) { | |
// If function is not passed, return the promise | |
if (!fn) { | |
return await this.createConnection(context, requireNewConnection); | |
} | |
// If function is passed, call the function by passing the connection to it | |
return await this.createConnection(context, requireNewConnection).then((conn) => fn(conn)); | |
} | |
/** | |
* If connection is passed, close connection; else close the cached connection. | |
* | |
* @param {Object} connection An optional connection to close | |
* @return {Promise<Object>} | |
*/ | |
async closeConnection(connection) { | |
if (connection) { | |
return await connection.close(); | |
} | |
if (this.connection !== null) { | |
return await this.connection.close(); | |
} | |
return null; | |
} | |
} | |
module.exports = DatabaseService; |
Sign up for free
to join this conversation on GitHub.
Already have an account?
Sign in to comment