import mongoose from 'mongoose'; import cfenv from 'cfenv'; import _ from 'lodash'; import ChangelogModel from './models/changelog'; import cron from 'node-cron'; // Database urls, prod db url is retrieved automatically const TESTING_URL = 'mongodb://localhost/dfopdb_test'; const DEV_URL = 'mongodb://localhost/dfopdb'; const debugging = true; const changelogKeepDays = 30; // Days to keep the changelog if (process.env.NODE_ENV !== 'production' && debugging) { mongoose.set('debug', true); // Enable mongoose debug } export default class db { private static state = { // Db object and current mode (test, dev, prod) db: null, mode: null, }; // Set mode to test for unit/integration tests, otherwise skip parameters. done is also only needed for testing static connect (mode = '', done: Function = () => {}) { if (this.state.db) return done(); // Db is already connected // Find right connection url let connectionString: string = ""; if (mode === 'test') { // Testing connectionString = TESTING_URL; this.state.mode = 'test'; } else if(process.env.NODE_ENV === 'production') { let services = cfenv.getAppEnv().getServices(); for (let service in services) { if(services[service].tags.indexOf("mongodb") >= 0) { connectionString = services[service]["credentials"].uri; } } this.state.mode = 'prod'; } else { connectionString = DEV_URL; this.state.mode = 'dev'; } // Connect to db mongoose.connect(connectionString, { useNewUrlParser: true, useUnifiedTopology: true, useCreateIndex: true, connectTimeoutMS: 10000 }, err => { if (err) done(err); }); mongoose.connection.on('error', console.error.bind(console, 'connection error:')); mongoose.connection.on('connected', () => { // Evaluation connection behaviour on prod if (process.env.NODE_ENV !== 'test') { // Do not interfere with testing console.info('Database connected'); } }); mongoose.connection.on('disconnected', () => { // Reset state on disconnect if (process.env.NODE_ENV !== 'test') { // Do not interfere with testing console.info('Database disconnected'); // This.state.db = 0; // prod database connects and disconnects automatically } }); process.on('SIGINT', () => { // Close connection when app is terminated if (!this.state.db) { // Database still connected mongoose.connection.close(() => { console.info('Mongoose default connection disconnected through app termination'); process.exit(0); }); } }); mongoose.connection.once('open', () => { mongoose.set('useFindAndModify', false); console.info(process.env.NODE_ENV === 'test' ? '' : `Connected to ${connectionString}`); this.state.db = mongoose.connection; done(); }); if (mode !== 'test') { // Clear old changelog regularly cron.schedule('0 0 * * *', () => { ChangelogModel.deleteMany({_id: {$lt: // Id from time Math.floor(new Date().getTime() / 1000 - changelogKeepDays * 24 * 60 * 60).toString(16) + '0000000000000000' }}).lean().exec(err => { if (err) console.error(err); }); }); } } static disconnect (done) { mongoose.connection.close(() => { console.info(process.env.NODE_ENV === 'test' ? '' : `Disconnected from database`); this.state.db = 0; done(); }); } static getState () { return this.state; } // Drop all collections of connected db (only dev and test for safety reasons) static drop (done: Function = () => {}) { if (!this.state.db || this.state.mode === 'prod') return done(); // No db connection or prod db this.state.db.db.listCollections().toArray((err, collections) => { // Get list of all collections if (collections.length === 0) { // There are no collections to drop return done(); } else { let dropCounter = 0; // Count number of dropped collections to know when to return done() collections.forEach(collection => { // Drop each collection this.state.db.dropCollection(collection.name, () => { if (++ dropCounter >= collections.length) { // All collections dropped done(); } }); }); } }); } static loadJson (json, done: Function = () => {}) { // Insert given JSON data into db, uses core mongodb methods // No db connection or nothing to load if (!this.state.db || !json.hasOwnProperty('collections') || json.collections.length === 0) { return done(); } let loadCounter = 0; // Count number of loaded collections to know when to return done() Object.keys(json.collections).forEach(collectionName => { // Create each collection json.collections[collectionName] = this.oidResolve(json.collections[collectionName]); this.state.db.createCollection(collectionName, (err, collection) => { if (err) { console.error(err); } collection.insertMany(json.collections[collectionName], () => { // Insert JSON data if (++ loadCounter >= Object.keys(json.collections).length) { // All collections loaded done(); } }); }); }); } // Changelog entry, expects (req, this (from query helper)) or (req, collection, conditions, data) static log(req, thisOrCollection, conditions = null, data = null) { if (! (conditions || data)) { // (req, this) data = thisOrCollection._update ? _.cloneDeep(thisOrCollection._update) : {}; // Replace undefined with {} // Replace keys with a leading $ Object.keys(data).forEach(key => { if (key[0] === '$') { data[key.substr(1)] = data[key]; delete data[key]; } }); new ChangelogModel(this.logEscape(_.cloneDeep({ action: req.method + ' ' + req.url, collection_name: thisOrCollection._collection.collectionName, conditions: thisOrCollection._conditions, data: data, user_id: req.authDetails.id ? req.authDetails.id : null }))).save({validateBeforeSave: false}, err => { if (err) console.error(err); }); } else { // (req, collection, conditions, data) new ChangelogModel(this.logEscape(_.cloneDeep({ action: req.method + ' ' + req.url, collection_name: thisOrCollection, conditions: conditions, data: data, user_id: req.authDetails.id ? req.authDetails.id : null }))).save(err => { if (err) console.error(err); }); } } private static oidResolve (object: any) { // Resolve $oid fields to actual ObjectIds recursively Object.keys(object).forEach(key => { if (object[key] !== null && object[key].hasOwnProperty('$oid')) { // Found oid, replace object[key] = mongoose.Types.ObjectId(object[key].$oid); } else if (typeof object[key] === 'object' && object[key] !== null) { // Deeper into recursion object[key] = this.oidResolve(object[key]); } }); return object; } private static logEscape(obj) { // Replace MongoDB control characters in keys if (Object(obj) === obj && Object.keys(obj).length > 0) { Object.keys(obj).forEach(key => { const safeKey = key.replace(/[$.]/g, ''); obj[safeKey] = this.logEscape(obj[key]); if (key !== safeKey) { delete obj[key]; } }); } return obj; } };