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