diff --git a/backend/constants/base-configuration.js b/backend/constants/base-configuration.js index dc4e751c2..977578588 100644 --- a/backend/constants/base-configuration.js +++ b/backend/constants/base-configuration.js @@ -3,7 +3,7 @@ * To add a new configuration. Simply add it to this object. * When changing this template, please follow the principle of "Secure by default" (https://en.wikipedia.org/wiki/Secure_by_default). */ -const BASE_CONFIGURATION = { +const BASE_CONFIGURATION = Object.freeze({ maintenance: false, quoteReport: { enabled: false, @@ -16,6 +16,6 @@ const BASE_CONFIGURATION = { resultObjectHashCheck: { enabled: false, }, -}; +}); module.exports = BASE_CONFIGURATION; diff --git a/backend/dao/configuration.js b/backend/dao/configuration.js index 28c157605..3f887f00c 100644 --- a/backend/dao/configuration.js +++ b/backend/dao/configuration.js @@ -1,12 +1,10 @@ const _ = require("lodash"); -const { mongoDB } = require("../init/mongodb"); +const db = require("../init/db"); const BASE_CONFIGURATION = require("../constants/base-configuration"); const Logger = require("../handlers/logger.js"); const CONFIG_UPDATE_INTERVAL = 10 * 60 * 1000; // 10 Minutes -let databaseConfigurationUpdated = false; - function mergeConfigurations(baseConfiguration, liveConfiguration) { if ( !_.isPlainObject(baseConfiguration) || @@ -22,9 +20,21 @@ function mergeConfigurations(baseConfiguration, liveConfiguration) { const baseValue = base[key]; const sourceValue = source[key]; - if (_.isPlainObject(baseValue) && _.isPlainObject(sourceValue)) { + const isBaseValueObject = _.isPlainObject(baseValue); + const isSourceValueObject = _.isPlainObject(sourceValue); + const isBaseValueArray = _.isArray(baseValue); + const isSourceValueArray = _.isArray(sourceValue); + + const arrayObjectMismatch = + (isBaseValueObject && isSourceValueArray) || + (isBaseValueArray && isSourceValueObject); + + if (isBaseValueObject && isSourceValueObject) { merge(baseValue, sourceValue); - } else if (typeof baseValue === typeof sourceValue) { + } else if ( + typeof baseValue === typeof sourceValue && + !arrayObjectMismatch // typeof {} = "object", typeof [] = "object" + ) { base[key] = sourceValue; } }); @@ -34,8 +44,9 @@ function mergeConfigurations(baseConfiguration, liveConfiguration) { } class ConfigurationDAO { - static configuration = Object.freeze(BASE_CONFIGURATION); + static configuration = BASE_CONFIGURATION; static lastFetchTime = 0; + static databaseConfigurationUpdated = false; static async getCachedConfiguration(attemptCacheUpdate = false) { if ( @@ -51,28 +62,23 @@ class ConfigurationDAO { static async getLiveConfiguration() { this.lastFetchTime = Date.now(); + const configurationCollection = db.collection("configuration"); + try { - const liveConfiguration = await mongoDB() - .collection("configuration") - .findOne(); + const liveConfiguration = await configurationCollection.findOne(); if (liveConfiguration) { const baseConfiguration = _.cloneDeep(BASE_CONFIGURATION); mergeConfigurations(baseConfiguration, liveConfiguration); - this.configuration = baseConfiguration; - - if (!databaseConfigurationUpdated) { - await mongoDB() - .collection("configuration") - .updateOne({}, { $set: Object.assign({}, this.configuration) }); - databaseConfigurationUpdated = true; - } + this.pushConfiguration(baseConfiguration); + this.configuration = Object.freeze(baseConfiguration); } else { - await mongoDB() - .collection("configuration") - .insertOne(Object.assign({}, BASE_CONFIGURATION)); // Seed the base configuration. + await configurationCollection.insertOne( + Object.assign({}, BASE_CONFIGURATION) + ); // Seed the base configuration. } + Logger.log( "fetch_configuration_success", "Successfully fetched live configuration." @@ -84,10 +90,27 @@ class ConfigurationDAO { ); } - this.configuration = Object.freeze(this.configuration); - return this.configuration; } + + static async pushConfiguration(configuration) { + if (this.databaseConfigurationUpdated) { + return; + } + + const configurationCollection = db.collection("configuration"); + + try { + await configurationCollection.replaceOne({}, configuration); + + this.databaseConfigurationUpdated = true; + } catch (error) { + Logger.log( + "push_configuration_failure", + `Could not push configuration: ${error.message}` + ); + } + } } module.exports = ConfigurationDAO; diff --git a/backend/handlers/logger.js b/backend/handlers/logger.js index 5d6c95b5f..350724639 100644 --- a/backend/handlers/logger.js +++ b/backend/handlers/logger.js @@ -1,8 +1,10 @@ -const { mongoDB } = require("../init/mongodb"); +const db = require("../init/db"); async function log(event, message, uid) { + const logsCollection = db.collection("logs"); + console.log(new Date(), "\t", event, "\t", uid, "\t", message); - await mongoDB().collection("logs").insertOne({ + await logsCollection.insertOne({ timestamp: Date.now(), uid, event, diff --git a/backend/init/db.js b/backend/init/db.js new file mode 100644 index 000000000..e88bc979d --- /dev/null +++ b/backend/init/db.js @@ -0,0 +1,75 @@ +const { MongoClient } = require("mongodb"); + +class DatabaseClient { + static mongoClient = null; + static db = null; + static collections = {}; + static connected = false; + + static async connect() { + const { + DB_USERNAME, + DB_PASSWORD, + DB_AUTH_MECHANISM, + DB_AUTH_SOURCE, + DB_URI, + DB_NAME, + } = process.env; + + const connectionOptions = { + useNewUrlParser: true, + useUnifiedTopology: true, + connectTimeoutMS: 2000, + serverSelectionTimeoutMS: 2000, + }; + + if (DB_USERNAME && DB_PASSWORD) { + connectionOptions.auth = { + username: DB_USERNAME, + password: DB_PASSWORD, + }; + } + + if (DB_AUTH_MECHANISM) { + connectionOptions.authMechanism = DB_AUTH_MECHANISM; + } + + if (DB_AUTH_SOURCE) { + connectionOptions.authSource = DB_AUTH_SOURCE; + } + + this.mongoClient = new MongoClient(DB_URI, connectionOptions); + + try { + await this.mongoClient.connect(); + this.db = this.mongoClient.db(DB_NAME); + this.connected = true; + } catch (error) { + console.error(e.message); + console.error( + "Failed to connect to database. Exiting with exit status code 1." + ); + process.exit(1); + } + } + + static async close() { + if (this.connected) { + await this.mongoClient.close(); + } + } + + static collection(collectionName) { + if (!this.connected) { + return null; + } + + if (!(collectionName in this.collections)) { + this.collections[collectionName] = this.db.collection(collectionName); + } + + return this.collections[collectionName]; + } +} + +module.exports = DatabaseClient; diff --git a/backend/init/mongodb.js b/backend/init/mongodb.js index 23d2688a8..5bd5f6e4b 100644 --- a/backend/init/mongodb.js +++ b/backend/init/mongodb.js @@ -1,42 +1,7 @@ -const { MongoClient } = require("mongodb"); - -let mongoClient; +const db = require("./db"); module.exports = { - async connectDB() { - let options = { - useNewUrlParser: true, - useUnifiedTopology: true, - connectTimeoutMS: 2000, - serverSelectionTimeoutMS: 2000, - }; - - if (process.env.DB_USERNAME && process.env.DB_PASSWORD) { - options.auth = { - username: process.env.DB_USERNAME, - password: process.env.DB_PASSWORD, - }; - } - - if (process.env.DB_AUTH_MECHANISM) { - options.authMechanism = process.env.DB_AUTH_MECHANISM; - } - - if (process.env.DB_AUTH_SOURCE) { - options.authSource = process.env.DB_AUTH_SOURCE; - } - - return MongoClient.connect(process.env.DB_URI, options) - .then((client) => { - mongoClient = client; - }) - .catch((e) => { - console.error(e.message); - console.error("FAILED TO CONNECT TO DATABASE. EXITING..."); - process.exit(1); - }); - }, mongoDB() { - return mongoClient.db(process.env.DB_NAME); + return db; }, }; diff --git a/backend/server.js b/backend/server.js index 412ac3b52..fa5fb8043 100644 --- a/backend/server.js +++ b/backend/server.js @@ -7,7 +7,7 @@ const cors = require("cors"); const admin = require("firebase-admin"); const Logger = require("./handlers/logger.js"); const serviceAccount = require("./credentials/serviceAccountKey.json"); -const { connectDB, mongoDB } = require("./init/mongodb"); +const db = require("./init/db"); const jobs = require("./jobs"); const addApiRoutes = require("./api/routes"); const contextMiddleware = require("./middlewares/context"); @@ -15,7 +15,7 @@ const ConfigurationDAO = require("./dao/configuration"); const PORT = process.env.PORT || 5005; -// MIDDLEWARE & SETUP +// MIDDLEWARE & SETUP const app = express(); app.use(express.urlencoded({ extended: true })); app.use(express.json()); @@ -62,7 +62,7 @@ app.use(function (e, req, res, _next) { `${monkeyError.status} ${monkeyError.message}`, monkeyError.uid ); - mongoDB().collection("errors").insertOne({ + db.collection("errors").insertOne({ _id: monkeyError.errorID, timestamp: Date.now(), status: monkeyError.status, @@ -80,12 +80,15 @@ app.use(function (e, req, res, _next) { console.log("Starting server..."); app.listen(PORT, async () => { console.log(`Listening on port ${PORT}`); + console.log("Connecting to database..."); - await connectDB(); + await db.connect(); console.log("Database connected"); + admin.initializeApp({ credential: admin.credential.cert(serviceAccount), }); + await ConfigurationDAO.getLiveConfiguration(); console.log("Starting cron jobs..."); diff --git a/backend/worker.js b/backend/worker.js index 98c8c21b9..852b5c816 100644 --- a/backend/worker.js +++ b/backend/worker.js @@ -1,19 +1,13 @@ -const express = require("express"); const { config } = require("dotenv"); const path = require("path"); -const MonkeyError = require("./handlers/error"); config({ path: path.join(__dirname, ".env") }); -const cors = require("cors"); +const db = require("./init/db"); const admin = require("firebase-admin"); - const serviceAccount = require("./credentials/serviceAccountKey.json"); -const { connectDB, mongoDB } = require("./init/mongodb"); - -const PORT = process.env.PORT || 5005; async function main() { - await connectDB(); + await db.connect(); await admin.initializeApp({ credential: admin.credential.cert(serviceAccount), }); @@ -25,8 +19,11 @@ main(); async function refactor() { console.log("getting all users"); - let users = await mongoDB().collection("users").find({}).toArray(); + + const usersCollection = db.collection("users"); + let users = await usersCollection.find({}).toArray(); console.log(users.length); + for (let user of users) { let obj = user.personalBests; @@ -67,9 +64,10 @@ async function refactor() { }); } - await mongoDB() - .collection("users") - .updateOne({ _id: user._id }, { $set: { lbPersonalBests: lbPb } }); + await usersCollection.updateOne( + { _id: user._id }, + { $set: { lbPersonalBests: lbPb } } + ); console.log(`updated ${user.name}`); } console.log("done");