const Provider = require('../Provider.js'); const { MongoClient } = require('mongodb'); class MongoDBProvider extends Provider { constructor(manager, config) { super(manager, { name: 'mongodb', config }); } async init() { try { this.connection = await MongoClient.connect(this.config.host+this.config.database, { useUnifiedTopology: true }); this.manager.db = await this.connection.db(this.config.database); this.db = this.manager.db; this.manager.logger.write('info', `Provider ${this.name} connected.`); this._initialized = true; } catch(err) { this.manager.logger.write('error', `Provider ${this.name} failed to connect: ${err}`); } } _query(request) { /* Query object structure { type: '', -- The function to use from this class, ex. findOne collection: '', -- Which collection to query from query: { }, -- Actual search query data: { }, -- If the operation is to update or insert something, this is what to insert upsert: bool, -- If true and no matching documents are found, insert a new one options: { }, projection: { } -- The fields that should be returned } */ if (!this._initialized) return { error: true, message: 'MongoDB not connected' }; if (!this[request.type]) return { error: true, message: `Invalid request type, got '${request.type}'` }; if (!request.collection && request.type !== 'stats') return { error: true, message: 'You must specify a collection to query!' }; return this[request.type](request); } /** * Fetches basic statistics about the database or collection * * @param {String} collection The collection to query, optional * @param {Object} options Optional options for the stats method * @returns * @memberof MongoDBProvider */ stats({ collection, options = { } }) { return new Promise((resolve, reject) => { if (!collection || collection.length) { this.db.stats(options, (error, stats) => { if (error) return reject(error); else { let { db, collections, objects, averageSize: avgObjSize, dataSize } = stats; return resolve({ db, collections, objects, averageSize: avgObjSize, dataSize }); } }); } else { this.db.collection(collection).stats(options, (err, stats) => { if (err) return reject(err); else { let { ns, size, count, averageSize: avgObjSize } = stats; return resolve({ index: ns, size, count, averageSize: avgObjSize }); } }); } }); } /** * Count the objects in * * @param {String} collection The collection to query * @param {Object} query Only documents matching this will be counted, optional * @param {Object} options Optional options, see mongo docs for these * @returns * @memberof MongoDBProvider */ count({ collection, query, options }) { return new Promise((resolve, reject) => { this.db.collection(collection).countDocuments(query, options, (error, result) => { if (error) return reject(error); else return resolve(result); }); }); } /** * Remove a document from a collection * * @param {String} collection The collection to remove from * @param {Object} query Any documents matching this will be removed * @returns * @memberof MongoDBProvider */ remove({ collection, query }) { return new Promise((resolve, reject) => { this.db.collection(collection).deleteOne(query, (err, result) => { if (err) return reject(err); return resolve(result); }); }); } /** * Insert one document to the collection * * @param {String} collection The collection to insert into * @param {Object} data The document to insert * @returns * @memberof MongoDBProvider */ insertOne({ collection, data }) { return new Promise((resolve, reject) => { this.db.collection(collection).insertOne(data, (err, result) => { if(err) reject(err); resolve(result); }); }); } /** * Find and return the first match * * @param {String} collection The collection to find from * @param {Object} query Documents matching this will be returned * @param {Object} projection Defines which fields to return, { 'key': 1, 'key2': 0 } -- key will return, key2 won't, optional * @memberof Database */ find({ collection, query, projection }) { //if(this.manager.debug) this.manager.logger.debug(`Incoming find query for ${db} with parameters ${JSON.stringify(query)}`); return new Promise((resolve, reject) => { if(!this._initialized) reject(new Error('MongoDB not connected')); this.db.collection(collection).find(query, { projection }, async (error, cursor) => { if(error) return reject(error); return resolve(await cursor.toArray()); }); }); } /** * Find and return the first match * * @param {String} collection The collection in which the data is to be updated * @param {Object} query The filter that is used to find the data * @param {Object} projection Defines which fields to return, { 'key': 1, 'key2': 0 } -- key will return, key2 won't, optional * @returns {Object} An object containing the queried data * @memberof Database */ findOne({ collection, query, projection }) { //if(this.manager.debug) this.manager.logger.debug(`Incoming findOne query for ${db} with parameters ${JSON.stringify(query)}`); return new Promise((resolve, reject) => { if(!this._initialized) reject(new Error('MongoDB not connected')); this.db.collection(collection).findOne(query, { projection }, async (error, item) => { if(error) return reject(error); return resolve(item); }); }); } /** * Update the first filter match. * * @param {String} collection The collection in which the data is to be updated * @param {Object} query The filter that is used to find the data * @param {Object} data The updated data * @param {Boolean} [upsert=true] Create a new entry if no match is found * @returns {WriteResult} Object containing the followint counts: Matched, Upserted, Modified * @memberof Database */ updateOne({ collection, query, data, upsert = true }) { //if(this.manager.debug) this.manager.logger.debug(`Incoming updateOne query for ${db} with parameters ${JSON.stringify(filter)}`); return new Promise((resolve, reject) => { if(!this._initialized) reject(new Error('MongoDB not connected')); this.db.collection(collection).updateOne(query, { $set: data }, { upsert: upsert }, async (error, result) => { if(error) return reject(error); else { //return resolve(result) let { matchedCount, upsertedCount, modifiedCount } = result; return resolve({ matched: matchedCount, upserted: upsertedCount, modified: modifiedCount }); } }); }); } /** * Remove a document property * * @param {String} collection The collection to query * @param {Object} query The filter that is used to find the data * @param {Array} data Array of fields to remove * @returns * @memberof MongoDBProvider */ removeProperty({ collection, query, data }) { return new Promise((resolve, reject) => { let unset = {}; for (let field in data) unset[field] = ''; this.db.collection(collection).updateOne(query, { $unset: unset }, async (error, result) => { if(error) return reject(error); else { let { matchedCount, modifiedCount } = result; return resolve({ matched: matchedCount, modified: modifiedCount }); } }); }); } /** * Push data to an array * * @param {string} collection The collection to query * @param {object} query The filter to find the document to update * @param {object} data The data to be pushed * @param {boolean} [upsert=true] Create a new entry if no match is found * @returns * @memberof Database */ push({ collection, query, data, upsert = true }) { //if(this.manager.debug) this.manager.logger.debug(`Incoming push query for ${db}, with upsert ${upsert} and with parameters ${JSON.stringify(filter)} and data ${JSON.stringify(data)}`); return new Promise((resolve, reject) => { if(!this._initialized) reject(new Error('MongoDB not connected')); this.db.collection(collection).updateOne(query, { $push: data }, { upsert: upsert }, async (error, result) => { if(error) return reject(error); else return resolve(result); }); }); } /** * Find a random element from a database * * @param {string} collection The collection to query * @param {object} [query={}] The filtering object to narrow down the sample pool * @param {number} [amount=1] Amount of items to return * @returns {object} * @memberof Database */ random({ collection, query = {}, amount = 1 }) { //if(this.manager.debug) this.manager.logger.debug(`Incoming random query for ${db} with parameters ${JSON.stringify(filter)} and amount ${amount}`); if(amount > 100) amount = 100; return new Promise((resolve, reject)=>{ if(!this._initialized) reject(new Error('MongoDB not connected')); this.db.collection(collection).aggregate([{ $match: query }, { $sample: {size: amount}}], function(err, item) { if(err) return reject(err); resolve(item); }); }); } } module.exports = MongoDBProvider;