UNPKG

@itentialopensource/adapter-db_mongo

Version:

Itential adapter to connect to mongo

1,394 lines (1,252 loc) 129 kB
/* @copyright Itential, LLC 2019 (pre-modifications) */ // Set globals /* global log */ /* eslint no-underscore-dangle: warn */ /* eslint no-loop-func: warn */ /* eslint no-cond-assign: warn */ /* eslint no-unused-vars: warn */ /* eslint consistent-return: warn */ /* eslint no-param-reassign: warn */ /* Required libraries. */ const fs = require('fs-extra'); const path = require('path'); const uuid = require('uuid'); /* The schema validator */ const AjvCl = require('ajv'); /* Fetch in the other needed components for the this Adaptor */ const EventEmitterCl = require('events').EventEmitter; const { MongoClient } = require('mongodb'); const { ObjectId } = require('mongodb'); let myid = null; let errors = []; /** * @summary Build a standard error object from the data provided * * @function formatErrorObject * @param {String} origin - the originator of the error (optional). * @param {String} type - the internal error type (optional). * @param {String} variables - the variables to put into the error message (optional). * @param {Integer} sysCode - the error code from the other system (optional). * @param {Object} sysRes - the raw response from the other system (optional). * @param {Exception} stack - any available stack trace from the issue (optional). * * @return {Object} - the error object, null if missing pertinent information */ function formatErrorObject(origin, type, variables, sysCode, sysRes, stack) { log.trace(`${myid}-adapter-formatErrorObject`); // add the required fields const errorObject = { icode: 'AD.999', IAPerror: { origin: `${myid}-unidentified`, displayString: 'error not provided', recommendation: 'report this issue to the adapter team!' } }; if (origin) { errorObject.IAPerror.origin = origin; } if (type) { errorObject.IAPerror.displayString = type; } // add the messages from the error.json for (let e = 0; e < errors.length; e += 1) { if (errors[e].key === type) { errorObject.icode = errors[e].icode; errorObject.IAPerror.displayString = errors[e].displayString; errorObject.IAPerror.recommendation = errors[e].recommendation; } else if (errors[e].icode === type) { errorObject.icode = errors[e].icode; errorObject.IAPerror.displayString = errors[e].displayString; errorObject.IAPerror.recommendation = errors[e].recommendation; } } // replace the variables let varCnt = 0; while (errorObject.IAPerror.displayString.indexOf('$VARIABLE$') >= 0) { let curVar = ''; // get the current variable if (variables && Array.isArray(variables) && variables.length >= varCnt + 1) { curVar = variables[varCnt]; } varCnt += 1; errorObject.IAPerror.displayString = errorObject.IAPerror.displayString.replace('$VARIABLE$', curVar); } // add all of the optional fields if (sysCode) { errorObject.IAPerror.code = sysCode; } if (sysRes) { errorObject.IAPerror.raw_response = sysRes; } if (stack) { errorObject.IAPerror.stack = stack; } // return the object return errorObject; } /** * @summary Parses the inputted data for the trigger "<op>" and replaces it with "$" * * @function convertOperatorTriggers * @param {AnyData} data - the object to check for triggers within (required) * * @return {Object} - the object with all triggers replaced with "$" */ function convertOperatorTriggers(data) { try { if (typeof data !== 'string') { if (Object.keys(data).length === 0) { // data is an empty object return {}; } data = JSON.stringify(data); } const regex = /{op}/gi; return JSON.parse(data.replace(regex, '$$')); } catch (err) { log.error(err); return data; } } /** * @summary Converts ObjectIds * * @function convertObjectId * @param {AnyData} data - the data to check for ObjectId (required) * * @return {Object} - the resulting ObjectId */ function convertObjectId(data) { try { if (typeof data === 'string' && data.indexOf('ObjectId') === 0) { const oid = data.substring(10, data.length - 2); log.debug(`OID ${oid}`); if (ObjectId.isValid(oid)) { return new ObjectId(oid); } } return data; } catch (err) { log.debug(err); return data; } } /** * @summary Converts ObjectId to String representation for return Data * * @function convertToObjectIdString * @param {AnyData} data - value to check for ObjectId (required) * * @return {String} - the resulting String */ function convertToObjectIdString(data) { if (data === undefined || data === null) { return data; } if (data instanceof ObjectId) { const idString = data.toString(); return `ObjectId("${idString}")`; } if (typeof data !== 'object' || data instanceof Date) { return data; } if (typeof data === 'object' && !Array.isArray(data)) { // object - need to recurse const keys = Object.keys(data); const converted = {}; for (let k = 0; k < keys.length; k += 1) { converted[[keys[k]]] = convertToObjectIdString(data[keys[k]]); } return converted; } // Array const converted = []; for (let a = 0; a < data.length; a += 1) { converted.push(convertToObjectIdString(data[a])); } return converted; } /** * @summary Checks for dates or ObjectId that have been stringified and converts them back to their respective object * * @function convertToDateOrObjectId * @param {AnyData} data - the object to check for dates within (required). * * @return {Object} - the object containing dates */ function convertToDateOrObjectId(data) { log.trace(`${myid}-adapter-convertToDateOrObjectId`); // if no data - return it if (data === undefined || data === null) { return data; } // if it is a number or boolean - just return it if (typeof data === 'number' || typeof data === 'boolean') { return data; } // if it is a string need to check if a date string or objectId string if (typeof data === 'string') { // if string check to see if in data format const dateRegex = /^[0-9]{4}-(0[1-9]|1[0-2])-(0[1-9]|[1-2][0-9]|3[0-1])T(0[0-9]|1[0-9]|2[0-3]):([0-5][0-9]):([0-5][0-9]).([0-9][0-9][0-9])Z$/; if (dateRegex.test(data)) { // return the date instead of the string return new Date(data); } return convertObjectId(data); // Handle if ObjectId string } // if the data is a regular expression - just return it if (typeof data === 'object' && data.constructor === RegExp) { return data; } // if data is an object but not an array if (typeof data === 'object' && !Array.isArray(data)) { // get all of the keys from the object const keys = Object.keys(data); const converted = {}; // go through the keys to handle the individual fields for (let k = 0; k < keys.length; k += 1) { if (data[keys[k]] instanceof ObjectId) { // value has already been converted to ObjectId converted[[keys[k]]] = data[keys[k]]; } else { converted[[keys[k]]] = convertToDateOrObjectId(data[keys[k]]); } } return converted; } // must be an array const converted = []; for (let a = 0; a < data.length; a += 1) { converted.push(convertToDateOrObjectId(data[a])); } return converted; } /** * This is the adapter/interface into Mongo */ class DBMongo extends EventEmitterCl { /** * DBMongo Adapter * @constructor */ constructor(prongid, properties) { super(); this.alive = false; this.id = prongid; myid = prongid; // set up the properties I care about this.refreshProperties(properties); // get the path for the specific error file const errorFile = path.join(__dirname, '/error.json'); // if the file does not exist - error if (!fs.existsSync(errorFile)) { const origin = `${this.id}-adapter-constructor`; log.warn(`${origin}: Could not locate ${errorFile} - errors will be missing details`); } // Read the action from the file system const errorData = JSON.parse(fs.readFileSync(errorFile, 'utf-8')); ({ errors } = errorData); } /** * refreshProperties is used to set up all of the properties for the adapter. * It allows properties to be changed later by simply calling refreshProperties rather * than having to restart the adapter. * * @function refreshProperties * @param {Object} properties - an object containing all of the properties */ refreshProperties(properties) { const meth = 'adapter-refreshProperties'; const origin = `${this.id}-${meth}`; log.trace(origin); try { // Read the properties schema from the file system const propertiesSchema = JSON.parse(fs.readFileSync(path.join(__dirname, 'propertiesSchema.json'), 'utf-8')); // validate the entity against the schema const ajvInst = new AjvCl(); const validate = ajvInst.compile(propertiesSchema); const result = validate(properties); // if invalid properties and did not already have properties, stop if (!result && !this.props) { log.error(`Attempt to configure adapter with invalid properties - ${JSON.stringify(validate.errors)}`); this.alive = false; this.emit('OFFLINE', { id: this.id }); return; } // if invalid properties but had valid ones, keep the valid ones if (!result) { log.warn('Attempt to configure adapter with invalid properties!'); return; } // for now just set this.props - may do individual properties later this.props = properties; } catch (e) { log.error(`${origin}: Properties may not have been set properly. ${e}`); } } /** * Call to connect and authenticate to the database * * @function connect */ connect() { const meth = 'adapter-connect'; const origin = `${this.id}-${meth}`; log.trace(origin); // if there is no url and no host then we need to error if (!this.props.host && !this.props.url) { log.error('ERROR: Missing required host or url - can not connect to database'); this.alive = false; this.emit('OFFLINE', { id: this.id }); return; } // set up the default variables that will be used let url = ''; let { db } = this.props; // if a url has been provided, pull it apart. URL overrides the individual properties! if (this.props.url && this.props.url.length > 0) { ({ url } = this.props); // checking if credentials are present in the URL const creds = url.split(/(?<=\/\/)(.*?)(?=@)/); // if credentials are not present in the url, find them and place them into URL if (creds.length !== 3) { if (this.props.credentials) { // check if dbAuth is enabled if (this.props.credentials.dbAuth) { if (this.props.credentials.user && this.props.credentials.passwd) { const dbAuthString = `${encodeURIComponent(this.props.credentials.user)}:${encodeURIComponent(this.props.credentials.passwd)}`; const decompStr = url.split('//'); decompStr.splice(1, 0, `//${dbAuthString}@`); url = decompStr.join(''); } } } } this.database = 'inurl'; } else { const dbProtocol = 'mongodb'; let dbAuthEnabled = false; let dbUsername = null; let dbPassword = null; let dbAuthString = null; let hostString = `${this.props.host}:27017`; let replSetEnabled = false; let replSet = null; // if no db throw a warning but then use test as the database if (!db) { log.warn('WARNING: No database found in property - connecting to test.'); db = 'test'; } this.database = db; // if there is a port defined change the hostString to include it if (this.props.port) { hostString = `${this.props.host}:${this.props.port}`; } // get the authentication properties if provided if (this.props.credentials) { if (this.props.credentials.dbAuth === true) { dbAuthEnabled = true; } if (this.props.credentials.user) { dbUsername = this.props.credentials.user; } if (this.props.credentials.passwd) { dbPassword = this.props.credentials.passwd; } } // get the replica set properties if provided if (this.props.replSet) { if (this.props.replSet.enabled === true) { replSetEnabled = true; } if (this.props.replSet.replicaSet) { if (!this.props.replSet.replicaSet.includes('replicaSet=')) { replSet = `replicaSet=${this.props.replSet.replicaSet}`; } else { replSet = this.props.replSet.replicaSet; } } } // format the url based on authentication or no authentication if (!dbAuthEnabled) { // format the url url = `${dbProtocol}://${hostString}`; } else { if (!dbAuthString) { // if authenticating but no credentials error out if (!dbUsername || !dbPassword) { log.error('ERROR: Database authentication is configured but username or password is not provided'); this.alive = false; this.emit('OFFLINE', { id: this.id }); return; } // format the dbAuthString - urlencoding the username and password dbAuthString = `${encodeURIComponent(dbUsername)}:${encodeURIComponent(dbPassword)}`; } // format the url url = `${dbProtocol}://${dbAuthString}@${hostString}/${db}`; } // are we using a replication set need to add it to the url if (replSetEnabled && replSet) { url += `?${replSet}`; } } // define some local variables to help in validating the properties.json file let sslEnabled = false; let sslValidate = false; let sslCheckServerIdentity = false; let sslCA = null; /* * this first section is configuration mapping * it can be replaced with the config object when available */ if (this.props.ssl) { // enable ssl encryption? if (this.props.ssl.enabled === true) { log.info('Connecting to MongoDB with SSL.'); sslEnabled = true; // validate the server's certificate // against a known certificate authority? if (this.props.ssl.acceptInvalidCerts === false) { sslValidate = true; log.info('Certificate based SSL MongoDB connections will be used.'); // if validation is enabled, we need to read the CA file if (this.props.ssl.sslCA) { try { sslCA = this.props.ssl.sslCA; } catch (err) { log.error(`Error: Unable to load Mongo CA file path: ${err}`); this.alive = false; this.emit('OFFLINE', { id: this.id }); return; } } else { log.error('Error: Certificate validation' + 'is enabled but a CA is not specified.'); this.alive = false; this.emit('OFFLINE', { id: this.id }); return; } } else { log.info('SSL MongoDB connection without CA certificate validation.'); } // validate the server certificate against the configured url? if (this.props.ssl.checkServerIdentity === true) { sslCheckServerIdentity = true; } else { log.info('INFO: Skipping server identity validation as it is not supported by mongodb driver >=v4'); } } else { log.warn('WARNING: Connecting to MongoDB without SSL.'); } } else { log.warn('WARNING: Connecting to MongoDB without SSL.'); } // This second section is to construct the mongo options object const options = { ssl: sslEnabled, tls: sslEnabled, tlsAllowInvalidCertificates: sslValidate, tlsAllowInvalidHostnames: sslCheckServerIdentity }; if (sslValidate === true) { options.tlsCAFile = sslCA; } if (this.props.databaseConnection === 'connect on request') { this.db = db; this.url = url; this.options = options; return this.emit('ONLINE', { id: this.id }); } log.debug('Connecting to MongoDB with provided url and options'); // Now we will start the process of connecting to mongo db MongoClient.connect(url, options, (err, mongoClient) => { if (!mongoClient) { log.error(`Error! Exiting... Must start MongoDB first ${err}`); this.alive = false; this.emit('OFFLINE', { id: this.id }); } else { log.info('mongo running'); this.clientDB = mongoClient.db(db); mongoClient.on('close', () => { this.alive = false; this.emit('OFFLINE', { id: this.id }); log.error('MONGO CONNECTION LOST...'); }); mongoClient.on('reconnect', () => { // we still need to check if we are properly authenticated // so we just list collections to test it. this.clientDB.collections((error) => { if (error) { log.error(error); this.alive = false; this.emit('OFFLINE', { id: this.id }); } else { log.info('MONGO CONNECTION BACK...'); this.alive = true; this.emit('ONLINE', { id: this.id }); } }); }); // we still need to check if we are properly authenticated // so we just list collections to test it. this.clientDB.collections((error) => { if (error) { log.error(error); this.alive = false; this.emit('OFFLINE', { id: this.id }); } else { /* * once we are connected to mongo, we need to perform a health check * to ensure we can read from the database */ log.info('MONGO CONNECTION UP...'); this.alive = true; this.emit('ONLINE', { id: this.id }); log.info('Successfully authenticated with the Mongo DB server'); let healthtype = 'startup'; if (this.props.healthcheck && this.props.healthcheck.type) { healthtype = this.props.healthcheck.type; } if (healthtype === 'startup') { this.healthCheck((hc) => { if (hc.status === 'fail') { log.error('Error connecting to Mongo DB:' + 'health check has failed.'); this.alive = false; this.emit('OFFLINE', { id: this.id }); } else { // successful health check; now we // can declare the database as online log.info('MongoDB connection has been established'); this.alive = true; this.emit('ONLINE', { id: this.id }); } }); } } }); } }); } /** * Call to run a healthcheck on the database * * @function healthCheck * @param {healthCallback} callback - a callback function to return a result * healthcheck success or failure */ healthCheck(callback) { const meth = 'adapter-healthCheck'; const origin = `${this.id}-${meth}`; log.trace(origin); try { // verify that we are connected to Mongo if (!this.alive || !this.clientDB) { log.error('Error during healthcheck: Not connected to Database'); return callback({ id: this.id, status: 'fail' }); } return this.clientDB.stats((err) => { if (err) { log.error(`Error during healthcheck: ${err}`); return callback({ id: this.id, status: 'fail' }); } return callback({ id: this.id, status: 'success' }); }); } catch (ex) { log.error(`Exception during healthcheck: ${ex}`); return callback({ id: this.id, status: 'fail' }); } } /** * getAllFunctions is used to get all of the exposed function in the adapter * * @function getAllFunctions */ getAllFunctions() { let myfunctions = []; let obj = this; // find the functions in this class do { const l = Object.getOwnPropertyNames(obj) .concat(Object.getOwnPropertySymbols(obj).map((s) => s.toString())) .sort() .filter((p, i, arr) => typeof obj[p] === 'function' && p !== 'constructor' && (i === 0 || p !== arr[i - 1]) && myfunctions.indexOf(p) === -1); myfunctions = myfunctions.concat(l); } while ( (obj = Object.getPrototypeOf(obj)) && Object.getPrototypeOf(obj) ); return myfunctions; } /** * getWorkflowFunctions is used to get all of the workflow function in the adapter * * @function getWorkflowFunctions */ getWorkflowFunctions() { const myfunctions = this.getAllFunctions(); const wffunctions = []; // remove the functions that should not be in a Workflow for (let m = 0; m < myfunctions.length; m += 1) { if (myfunctions[m] === 'addListener') { // got to the second tier (adapterBase) break; } if (myfunctions[m] !== 'connect' && myfunctions[m] !== 'healthCheck' && myfunctions[m] !== 'getAllFunctions' && myfunctions[m] !== 'getWorkflowFunctions' && myfunctions[m] !== 'refreshProperties') { wffunctions.push(myfunctions[m]); } } return wffunctions; } /** * Call to create an item in the database * * @function create * @param {String} collectionName - the collection to save the item in. (required) * @param {String} data - the data to add. (required) * @param {createCallback} callback - a callback function to return a result * (created item) or the error */ async create(collectionName, data, callback) { const meth = 'adapter-create'; const origin = `${this.id}-${meth}`; log.trace(origin); let mongoClient; try { if (this.props.databaseConnection === 'connect on request') { mongoClient = await MongoClient.connect(this.url, this.options); if (!mongoClient) { const errorObj = formatErrorObject(origin, 'Mongo connection failed', null, null, null, null); return callback(null, errorObj); } this.clientDB = mongoClient.db(this.db); mongoClient.on('close', () => { log.debug(`${origin} db close`); }); mongoClient.on('error', () => { log.error(`${origin} db error`); }); mongoClient.on('fullsetup', () => { log.debug(`${origin} db fullsetup`); }); mongoClient.on('parseError', () => { log.error(`${origin} db parseError`); }); mongoClient.on('timeout', () => { log.error(`${origin} db timeout`); }); this.alive = true; } // verify the required data has been provided if (collectionName === undefined || collectionName === null || collectionName === '') { const errorObj = formatErrorObject(origin, 'Missing Data', ['collectionName'], null, null, null); log.error(`${origin}: ${errorObj.IAPerror.displayString}`); return callback(null, errorObj); } if (data === undefined || data === null || data === '') { const errorObj = formatErrorObject(origin, 'Missing Data', ['data'], null, null, null); log.error(`${origin}: ${errorObj.IAPerror.displayString}`); return callback(null, errorObj); } // verify that we are connected to Mongo if (!this.alive || !this.clientDB) { const errorObj = formatErrorObject(origin, 'Database Error', [`${this.database} not connected`], null, null, null); log.error(`${origin}: ${errorObj.IAPerror.displayString}`); return callback(null, errorObj); } // get the collection so we can run the remove on the collection const collection = this.clientDB.collection(collectionName); if (!collection) { const errorObj = formatErrorObject(origin, 'Database Error', [`Collection ${collectionName} not found`], null, null, null); log.error(`${origin}: ${errorObj.IAPerror.displayString}`); return callback(null, errorObj); } const dataInfo = data; let createId = true; if (this.props.createUuid !== undefined && this.props.createUuid !== null && this.props.createUuid === false) { createId = false; } if (!{}.hasOwnProperty.call(dataInfo, '_id') && createId) { dataInfo._id = uuid.v4(); } if (dataInfo._id) { dataInfo._id = convertObjectId(dataInfo._id); } // insert the item into the collection return collection.insertOne(convertToDateOrObjectId(dataInfo), {}, async (err, result) => { if (mongoClient) { this.alive = false; mongoClient.close().catch((closeErr) => { log.error(`${origin}: Error closing MongoDB client - ${closeErr.message}`); }); } if (err) { const errorObj = formatErrorObject(origin, 'Database Error', [err], null, null, null); log.error(`${origin}: ${errorObj.IAPerror.displayString}`); return callback(null, errorObj); } try { // Wait for the document to be available const insertedDocument = await collection.findOne({ _id: result.insertedId }); if (insertedDocument) { return callback({ status: 'success', code: 200, response: insertedDocument }); } } catch (findErr) { log.error(`${origin}: Error retrieving inserted document - ${findErr.message}`); const errorObj = formatErrorObject(origin, 'Document Retrieval Error', [findErr], null, null, null); return callback(null, errorObj); } }); } catch (ex) { const errorObj = formatErrorObject(origin, 'Caught Exception', null, null, null, ex); log.error(`${origin}: ${errorObj.IAPerror.displayString}`); return callback(null, errorObj); } } /** * Call to create many items in the database * * @function createMany * @param {String} collectionName - the collection to save the items in. (required) * @param {Array} data - the modification to make. (required) * @param {Boolean} ordered - whether the data needs to be ordered. (optional) * @param {String} writeConcern - whether the data should be overwritten. (optional) * @param {createCallback} callback - a callback function to return a result * (created items) or the error */ async createMany(collectionName, data, ordered, writeConcern, callback) { const meth = 'adapter-createMany'; const origin = `${this.id}-${meth}`; log.trace(origin); let mongoClient; try { if (this.props.databaseConnection === 'connect on request') { mongoClient = await MongoClient.connect(this.url, this.options); if (!mongoClient) { const errorObj = formatErrorObject(origin, 'Mongo connection failed', null, null, null, null); return callback(null, errorObj); } this.clientDB = mongoClient.db(this.db); mongoClient.on('close', () => { log.debug(`${origin} db close`); }); mongoClient.on('error', () => { log.error(`${origin} db error`); }); mongoClient.on('fullsetup', () => { log.debug(`${origin} db fullsetup`); }); mongoClient.on('parseError', () => { log.error(`${origin} db parseError`); }); mongoClient.on('timeout', () => { log.error(`${origin} db timeout`); }); this.alive = true; } // verify the required data has been provided if (collectionName === undefined || collectionName === null || collectionName === '') { const errorObj = formatErrorObject(origin, 'Missing Data', ['collectionName'], null, null, null); log.error(`${origin}: ${errorObj.IAPerror.displayString}`); return callback(null, errorObj); } if (data === undefined || data === null || data === '') { const errorObj = formatErrorObject(origin, 'Missing Data', ['data'], null, null, null); log.error(`${origin}: ${errorObj.IAPerror.displayString}`); return callback(null, errorObj); } if (!Array.isArray(data)) { const errorObj = formatErrorObject(origin, 'Invalid data format - data must be an array', null, null, null, null); log.error(`${origin}: ${errorObj.IAPerror.displayString}`); return callback(null, errorObj); } // IAP is passing empty strings instead of null so would prefer null if no ordered. if (ordered === '') { ordered = null; } if (writeConcern === '') { writeConcern = null; } // verify that we are connected to Mongo if (!this.alive || !this.clientDB) { const errorObj = formatErrorObject(origin, 'Database Error', [`${this.database} not connected`], null, null, null); log.error(`${origin}: ${errorObj.IAPerror.displayString}`); return callback(null, errorObj); } // get the collection so we can run the remove on the collection const collection = this.clientDB.collection(collectionName); if (!collection) { const errorObj = formatErrorObject(origin, 'Database Error', [`Collection ${collectionName} not found`], null, null, null); log.error(`${origin}: ${errorObj.IAPerror.displayString}`); return callback(null, errorObj); } const dataInfo = data; let createId = true; if (this.props.createUuid !== undefined && this.props.createUuid !== null && this.props.createUuid === false) { createId = false; } const options = { ordered, writeConcern }; for (let i = 0; i < dataInfo.length; i += 1) { if (!{}.hasOwnProperty.call(dataInfo, '_id') && createId) { dataInfo._id = uuid.v4(); } } // insert the items into the collection return collection.insertMany(convertToDateOrObjectId(dataInfo), options, (err, result) => { if (mongoClient) { this.alive = false; mongoClient.close().catch((closeErr) => { log.error(`${origin}: Error closing MongoDB client - ${closeErr.message}`); }); } if (err) { const errorObj = formatErrorObject(origin, 'Database Error', [err], null, null, null); log.error(`${origin}: ${errorObj.IAPerror.displayString}`); return callback(null, errorObj); } // only doing this to hopefully make this a non-breaking change although I think the library version changes the response format if (result && result.ops && Array.isArray(result.ops) && result.ops.length > 0) { return callback({ status: 'success', code: 200, response: result.ops[0] }); } return callback({ status: 'success', code: 200, response: result }); }); } catch (ex) { const errorObj = formatErrorObject(origin, 'Caught Exception', null, null, null, ex); log.error(`${origin}: ${errorObj.IAPerror.displayString}`); return callback(null, errorObj); } } /** * Call to get the size of a collection in the database * * @function collectionSize * @param {String} collectionName - the collection to get. (required) * @param {getCallback} callback - a callback function to return a result */ async collectionSize(collectionName, callback) { const meth = 'adapter-collectionSize'; const origin = `${this.id}-${meth}`; log.trace(origin); let mongoClient; try { if (this.props.databaseConnection === 'connect on request') { mongoClient = await MongoClient.connect(this.url, this.options); if (!mongoClient) { const errorObj = formatErrorObject(origin, 'Mongo connection failed', null, null, null, null); return callback(null, errorObj); } this.clientDB = mongoClient.db(this.db); mongoClient.on('close', () => { log.debug(`${origin} db close`); }); mongoClient.on('error', () => { log.error(`${origin} db error`); }); mongoClient.on('fullsetup', () => { log.debug(`${origin} db fullsetup`); }); mongoClient.on('parseError', () => { log.error(`${origin} db parseError`); }); mongoClient.on('timeout', () => { log.error(`${origin} db timeout`); }); this.alive = true; } // verify the required data has been provided if (!collectionName) { const errorObj = formatErrorObject(origin, 'Missing Data', ['collectionName'], null, null, null); log.error(`${origin}: ${errorObj.IAPerror.displayString}`); return callback(null, errorObj); } // verify that we are connected to Mongo if (!this.alive || !this.clientDB) { const errorObj = formatErrorObject(origin, 'Database Error', [`${this.database} not connected`], null, null, null); log.error(`${origin}: ${errorObj.IAPerror.displayString}`); return callback(null, errorObj); } // get the collection so we can run the remove on the collection const collection = this.clientDB.collection(collectionName); if (!collection) { const errorObj = formatErrorObject(origin, 'Database Error', [`Collection ${collectionName} not found`], null, null, null); log.error(`${origin}: ${errorObj.IAPerror.displayString}`); return callback(null, errorObj); } // get the size of the collection const stats = await collection.stats(); const { size } = stats; return callback({ status: 'success', code: 200, response: size }); } catch (ex) { const errorObj = formatErrorObject(origin, 'Caught Exception', null, null, null, ex); log.error(`${origin}: ${errorObj.IAPerror.displayString}`); return callback(null, errorObj); } finally { if (mongoClient) { this.alive = false; await mongoClient.close(); } } } /** * Call to query items in the database * * @function query * @param {Object} queryDoc - the query to use to find data. (required) * queryDoc = { * collection : <collection_name>, * filter : <filter Obj>, * projection : <projection Obj>, * sort : <sort Obj>, * start : <start position>, * limit : <limit of results> * } * @param {getCallback} callback - a callback function to return a result * (items) or the error */ async query(queryDoc, callback) { const meth = 'adapter-query'; const origin = `${this.id}-${meth}`; log.trace(origin); await this.queryWithOptions(queryDoc, null, callback); } /** * Call to query items in the database * * @function query * @param {Object} queryDoc - the query to use to find data. (required) * queryDoc = { * collection : <collection_name>, * filter : <filter Obj>, * projection : <projection Obj>, * sort : <sort Obj>, * start : <start position>, * limit : <limit of results> * } * @param {Object} findOptions - Find options (optional) * queryDoc = { * allowDiskUse : <boolean>, * ... * } * @param {getCallback} callback - a callback function to return a result * (items) or the error */ async queryWithOptions(queryDoc, findOptions, callback) { const meth = 'adapter-queryWithOptions'; const origin = `${this.id}-${meth}`; log.trace(origin); let mongoClient; try { if (this.props.databaseConnection === 'connect on request') { mongoClient = await MongoClient.connect(this.url, this.options); if (!mongoClient) { const errorObj = formatErrorObject(origin, 'Mongo connection failed', null, null, null, null); return callback(null, errorObj); } this.clientDB = mongoClient.db(this.db); mongoClient.on('close', () => { log.debug(`${origin} db close`); }); mongoClient.on('error', () => { log.error(`${origin} db error`); }); mongoClient.on('fullsetup', () => { log.debug(`${origin} db fullsetup`); }); mongoClient.on('parseError', () => { log.error(`${origin} db parseError`); }); mongoClient.on('timeout', () => { log.error(`${origin} db timeout`); }); this.alive = true; } // verify the required data has been provided if (queryDoc === undefined || queryDoc === null || queryDoc === '') { const errorObj = formatErrorObject(origin, 'Missing Data', ['queryDoc'], null, null, null); log.error(`${origin}: ${errorObj.IAPerror.displayString}`); return callback(null, errorObj); } if (typeof queryDoc === 'string') { queryDoc = JSON.parse(queryDoc); } if (!queryDoc.collection) { const errorObj = formatErrorObject(origin, 'Missing Data', ['queryDoc.collection'], null, null, null); log.error(`${origin}: ${errorObj.IAPerror.displayString}`); return callback(null, errorObj); } let filter = queryDoc.filter || {}; filter = convertOperatorTriggers(filter); let projection = queryDoc.projection || {}; projection = convertOperatorTriggers(projection); const sort = queryDoc.sort || {}; const start = queryDoc.start || 0; const limit = queryDoc.limit || 0; const options = findOptions || {}; // verify that we are connected to Mongo if (!this.alive || !this.clientDB) { const errorObj = formatErrorObject(origin, 'Database Error', [`${this.database} not connected`], null, null, null); log.error(`${origin}: ${errorObj.IAPerror.displayString}`); return callback(null, errorObj); } // get the collection so we can run the remove on the collection const collection = this.clientDB.collection(queryDoc.collection); if (!collection) { const errorObj = formatErrorObject(origin, 'Database Error', [`Collection ${queryDoc.collection} not found`], null, null, null); log.error(`${origin}: ${errorObj.IAPerror.displayString}`); return callback(null, errorObj); } // find the items in the collection return collection.find((convertToDateOrObjectId(filter) || {}), options).sort(sort).skip(start).limit(limit) .project((projection || {})) .toArray((err, docs) => { if (err) { const errorObj = formatErrorObject(origin, 'Database Error', [err], null, null, null); log.error(`${origin}: ${errorObj.IAPerror.displayString}`); return callback(null, errorObj); } let convertedDocs = docs; if (this.props.convertObjectId) { convertedDocs = convertToObjectIdString(docs); } return callback({ status: 'success', code: 200, response: convertedDocs }); }); } catch (ex) { const errorObj = formatErrorObject(origin, 'Caught Exception', null, null, null, ex); log.error(`${origin}: ${errorObj.IAPerror.displayString}`); return callback(null, errorObj); } } /** * Call to find items in the database * * @function find * @param {Object} options - the options to use to find data. (required) * options = { * entity : <collection_name>, * filter : <filter Obj>, * sort : <sort Obj>, * start : <start position>, * limit : <limit of results>, * } * @param {deleteCallback} callback - a callback function to return a result * (status of the request) or the error */ async find(options, callback) { const meth = 'adapter-find'; const origin = `${this.id}-${meth}`; log.trace(origin); let mongoClient; try { if (this.props.databaseConnection === 'connect on request') { mongoClient = await MongoClient.connect(this.url, this.options); if (!mongoClient) { const errorObj = formatErrorObject(origin, 'Mongo connection failed', null, null, null, null); return callback(null, errorObj); } this.clientDB = mongoClient.db(this.db); mongoClient.on('close', () => { log.debug(`${origin} db close`); }); mongoClient.on('error', () => { log.error(`${origin} db error`); }); mongoClient.on('fullsetup', () => { log.debug(`${origin} db fullsetup`); }); mongoClient.on('parseError', () => { log.error(`${origin} db parseError`); }); mongoClient.on('timeout', () => { log.error(`${origin} db timeout`); }); this.alive = true; } // verify the required data has been provided if (options === undefined || options === null || options === '') { const errorObj = formatErrorObject(origin, 'Missing Data', ['options'], null, null, null); log.error(`${origin}: ${errorObj.IAPerror.displayString}`); return callback(null, errorObj); } if (typeof options === 'string') { try { options = JSON.parse(options); } catch (pex) { const errorObj = formatErrorObject(origin, 'Missing Data', ['options could not be parsed'], null, null, null); log.error(`${origin}: ${errorObj.IAPerror.displayString}`); return callback(null, errorObj); } } if (!options.entity) { const errorObj = formatErrorObject(origin, 'Missing Data', ['options.entity'], null, null, null); log.error(`${origin}: ${errorObj.IAPerror.displayString}`); return callback(null, errorObj); } let filter = options.filter || {}; filter = convertOperatorTriggers(filter); // verify that we are connected to Mongo if (!this.alive || !this.clientDB) { const errorObj = formatErrorObject(origin, 'Database Error', [`${this.database} not connected`], null, null, null); log.error(`${origin}: ${errorObj.IAPerror.displayString}`); return callback(null, errorObj); } const { entity } = options; const collections = await this.clientDB.listCollections({ name: entity }).toArray(); if (!collections.length) { // The collection does not exist const errorObj = formatErrorObject(origin, 'Database Error', [`Collection ${entity} not found`], null, null, null); log.error(`${origin}: ${errorObj.IAPerror.displayString}`); return callback(null, errorObj); } // get the collection so we can run the remove on the collection log.debug(`Using Mongo Collection ${entity}`); const collection = this.clientDB.collection(entity); if (!collection) { const errorObj = formatErrorObject(origin, 'Database Error', [`Collection ${entity} not found`], null, null, null); log.error(`${origin}: ${errorObj.IAPerror.displayString}`); return callback(null, errorObj); } const sort = options.sort || {}; const start = options.start || 0; let limit = 10; // If limit is not specified, default to 10. // Note: limit may be 0, which is equivalent to setting no limit. if (Object.hasOwnProperty.call(options, 'limit')) { ({ limit } = options); } // Replace filter with regex to allow for substring lookup // TODO: Need to create a new filter object instead of mutating the exsisting one const filterKeys = Object.keys(filter).filter((key) => (key[0] !== '$' && typeof filter[key] === 'string')); filterKeys.map((key) => { try { if (key !== '_id') { log.debug(`Updating Value for ${key}`); const escapedFilter = filter[key].replace(/([.?*+^$[\]\\(){}|-])/g, '\\$1'); const regexedFilter = new RegExp(`.*${escapedFilter}.*`, 'i'); filter[key] = { $regex: regexedFilter }; log.debug(`Updating Value to ${JSON.stringify(filter[key])}`); } else { filter[key] = convertObjectId(filter[key]); } } catch (e) { delete filter[key]; } return key; }); // find the items in the collection filter = convertToDateOrObjectId(filter); log.debug(`Using Filter: ${JSON.stringify(filter)}`); log.debug(`Using Start: ${start}, Limit: ${limit}, and Sort: ${JSON.stringify(sort)}`); return collection.find(filter).sort(sort).skip(start).limit(limit) .toArray((err, docs) => { if (err) { if (mongoClient) { this.alive = false; mongoClient.close().catch((closeErr) => { log.error(`${origin}: Error closing MongoDB client - ${closeErr.message}`); }); } const errorObj = formatErrorObject(origin, 'Database Error', [err], null, null, null); log.error(`${origin}: ${errorObj.IAPerror.displayString}`); return callback(null, errorObj); } return collection.count(filter, (error, docCount) => { if (mongoClient) { this.alive = false; mongoClient.close().catch((closeErr) => { log.error(`${origin}: Error closing MongoDB client - ${closeErr.message}`); }); } if (error) { log.warn('returning results without total count'); } let convertedDocs = docs; if (this.props.convertObjectId) { convertedDocs = convertToObjectIdString(docs); } return callback({ status: 'success', code: 200, response: convertedDocs, total: docCount }); }); }); } catch (ex) { const errorObj = formatErrorObject(origin, 'Caught Exception', null, null, null, ex); log.error(`${origin}: ${errorObj.IAPerror.displayString}`); return callback(null, errorObj); } } /** * Call to search for count of items in the database * * @function count * @param {String} collectionName - the collection to find things from. (required) * @param {Object} filter - the filter for items to search for. (optional) * @param {deleteCallback} callback - a callback function to return a result * (status of the request) or the error */ async count(collectionName, filter, callback) { const meth = 'adapter-count'; const origin = `${this.id}-${meth}`; log.trace(origin); let mongoClient; try { if (this.props.databaseConnection === 'connect on request') { mongoClient = await MongoClient.connect(this.url, this.options); if (!mongoClient) { const errorObj = formatErrorObject(origin, 'Mongo connection failed', null, null, null, null); return callback(null, errorObj); } this.clientDB = mongoClient.db(this.db); mongoClient.on('close', () => { log.debug(`${origin} db close`); }); mongoClient.on('error', () => { log.error(`${origin} db error`); }); mongoClient.on('fullsetup', () => { log.debug(`${origin} db fullsetup`); }); mongoClient.on('parseError', () => { log.error(`${origin} db parseError`); }); mongoClient.on('timeout', () => { log.error(`${origin} db timeout`); }); this.alive = true; } // verify the required data has been provided if (collectionName === undefined || collectionName === null || collectionName === '') { const errorObj = formatErrorObject(origin, 'Missing Data', ['collectionName'], null, null, null); log.error(`${origin}: ${errorObj.IAPerror.displayString}`); return callback(null, errorObj); } // IAP is passing empty strings instead of null so would prefer null if no filter. if (filter === '') { filter = {}; } filter = convertOperatorTriggers(filter); // verify that we are connected to Mongo if (!this.alive || !this.clientDB) { const errorObj = formatErrorObject(origin, 'Database Error', [`${this.database} not connected`], null, null, null); log.error(`${origin}: ${errorObj.IAPerror.displayString}`); return callback(null, errorObj); } const collections = await this.clientDB.listCollections({ name: collectionName }).toArray(); if (!collections.length) { // The collection does not exist const errorObj = formatErrorObject(origin, 'Database Error', [`Collection ${collectionName} not found`], null, null, null); log.error(`${origin}: ${errorObj.IAPerror.displayString}`); return callback(null, errorObj); } // get the collection const collection = this.clientDB.collection(collectionName); if (!collection) { const errorObj = formatErrorObject(origin, 'Database Error', [`Collection ${collectionName} not found`], null, null, null); log.error(`${origin}: ${errorObj.IAPerror.displayString}`); return callback(null, errorObj); } return collection.count(filter, (error, docCount) => { if (mongoClient