import mongoose from 'mongoose'; import cfenv from 'cfenv'; import _ from 'lodash'; import ChangelogModel from './models/changelog'; // database urls, prod db url is retrieved automatically const TESTING_URL = 'mongodb://localhost/dfopdb_test'; const DEV_URL = 'mongodb://localhost/dfopdb'; const debugging = false; 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(); }); } 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 {} Object.keys(data).forEach(key => { if (key[0] === '$') { data[key.substr(1)] = data[key]; delete data[key]; } }); new ChangelogModel({ 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(err => { if (err) console.error(err); }); } else { // (req, collection, conditions, data) new ChangelogModel({ 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; } };