import { ObjectId, MongoClient } from 'mongodb'; let db; let dbo; /** * Manages database operations, управление на операциите към базата от данни */ class Db { name = 'db'; /** * Initializes the database plugin, инициализация * @param {App} app The application instance, апликация */ init(app){ } /** * Starts the database plugin, стартиране * @param {App} app The application instance, апликация */ async start(app){ db = await MongoClient.connect(app.config.db.url, {maxPoolSize: 256}); try { dbo = db.db(app.config.db.name); this.instance = dbo; for (let c of ['users', 'user_sessions', 'history', 'log', 'assets']){ try { await dbo.createCollection(c); }catch(err){} } }finally{ } } /** * Inserts a record in a db collection, добавяне на запис в базата от данни * @param {string} collection The name of the collection, име на колекцията, в която да бъде добавен записа * @param {Object} value The object to insert, стойност на записа * @returns {ObjectId} Inserted Id, идентификатор на новия запис */ async create(collection, value){ try { delete value._id; return await dbo.collection(collection).insertOne(value); }finally{ } } /** * Loads a record from db collection, извличане на запис от базата от данни * @param {string} collection The name of the collection, име на колекцията, съдържаща записа * @param {Object} key Record identifier, идентификатор на записа * @param {Object} projection What data to take from the object, проекция на очаквания резултат * @returns {Object} A record, запис */ async get(collection, key, projection){ try { let res = await dbo.collection(collection).findOne(key, projection ? {projection} : undefined) return res; }finally{ } } /** * Performs a database query, проста заявка за търсене в базата от данни * @param {string} collection Collection name, име на колекция * @param {Object} query A mongo db query, заявка * @returns {Object[]} Array of records, масив от записи */ async list(collection, query){ try { let cursor = dbo.collection(collection).find(query.query, query.project ? {projection: query.project} : undefined); query.sort && cursor.sort(query.sort); query.skip && cursor.skip(query.skip); query.limit && cursor.limit(query.limit); let count = await dbo.collection(collection).countDocuments(query.query); let result = await cursor.toArray(); return {data: result, count:count}; }finally{ } } /** * Performs a database aggregation according to a given pipeline, сложна заявка (агрегираща) към базата от данни * @param {string} collection Database collection name, име на колекция * @param {Object} specs aggregation definition (the pipeline), дефиниция на заявката * @returns {Object[]} Array of records, списък от записи */ async aggregate(collection, specs){ try { let cursor = dbo.collection(collection); let aggCursor = cursor.aggregate(specs); let result = await aggCursor.toArray(); return result.length == 1 ? result[0] : result; }finally{ } } /** * Finds the distinct values for a specified field across a single collection, извличане на списък от уникални записи в колекция по зададени критерии * @param {string} collection Database collection name, име на колекцията * @param {Object} key The target field for the distinction, целеви атрибут, по който се търси уникалност * @param {Object} query filter to be applied, филтър на записите в колекцията * @returns {Object[]} Array of records, списък от записи */ async distinct(collection, key, query){ try { return await dbo.collection(collection).distinct(key, query); }finally{ } } /** * Updates a record in database by given key and value, обновяване на запис в базата данни по дадени ключ и стойност * @param {Object} collection DB collection, име на колекцията * @param {Object} key The key/query which identifies the record to be updated, ключ или заявка за идентификация на съществуващия запис * @param {Object} value The new value for the record, нова стойност на записа * @returns {Object} The result from the update operation, резултат от операцията */ async update(collection, key, value){ let r; try { delete value._id; r = await dbo.collection(collection).replaceOne(key, value, {upsert:true}); }finally{ return r; } } /** * Performs partial update on a record by given key and partial value, частично обновяване на запис в базата от данни * @param {Object} collection Database collection, име на колекция * @param {Object} key The key/query which identifies the record to be updated, ключ или заявка за идентификация на целевите обекти * @param {Object} value The partial value to be updated, дефиниция на частичното обновяване * @returns {Object} The result from the update operation, резултат от операцията */ async updateSet(collection, key, value){ let r; try { r = await dbo.collection(collection).updateMany(key, value); }finally{ return r; } } /** * Removes a record from the database by given key, изтриване на запис от базата от данни по зададен критерий * @param {Object} collection Database collection, име на колекцията * @param {Object} key The key/query which identifies the record to be updated, ключ/заявка за идентификация на целевите записи */ async remove(collection, key){ try { await dbo.collection(collection).deleteMany(key); }finally{ } } // convertToObjectId(object, key, recursive, result){ // if (object && object[key]){ // if (Array.isArray(object[key])){ // object[key].forEach((v, i, a)=>{ // a[i] = this.ObjectId(v); // result && result.push(a[i]); // }) // }else{ // let oid = this.ObjectId(object[key]) // object[key] = oid; // result && result.push(oid); // } // } // if (recursive){ // for (var k in object){ // if (typeof(object[k]) == 'object'){ // this.convertToObjectId(object[k], key, recursive, result); // } // } // } // } sanitizeQuery(q){ if (!q) return; Object.getOwnPropertyNames(q).forEach(n=>{ if (n.startsWith('$')){ //sanitize $ console.warn('Deleting suspicious query key', n) delete q[n]; } if (typeof(q[n]) == 'object'){ this.sanitizeQuery(q[n]); } //prepare for DB if (n.startsWith('*')){ let n1 = '$' + n.slice(1); q[n1] = q[n]; delete q[n]; n = n1; } if (['$where', '$group', '$merge', '$lookup', '$accumulator', '$function'].includes(n)){ delete q[n]; console.warn('Deleting suspicious query key', n); } }) if (q.$oid){ q.$oid.forEach(o=>{ q[o] = this.ObjectId(q[o]); }) delete q.$oid; } return q; } sanitizeProjection(p){ if (!p) return { _id: 1 }; let props = Object.getOwnPropertyNames(p); props.forEach(n=>{ if (typeof p[n] == 'object'){ console.warn('Deleting suspicious projection key, object', n) delete p[n] }else if (!n.match(/^[0-9a-zA-Z\._\$\#\-\:]*$/)){ console.warn('Deleting suspicious projection key', n) delete p[n] }else if (typeof p[n] !== 'number' && !p[n].match(/^[0-9a-zA-Z\._\$\#\-]*$/)){ console.warn('Deleting suspicious projection value', n, p[n]) delete p[n] } }) props = Object.getOwnPropertyNames(p); if (props.length == 0){ return { _id: 1 }; } } checkLimit(q){ if (!q) return; if (!q.limit) q.limit = 100; if (q.limit == 'all') delete q.limit; } ObjectId(id){ return new ObjectId(id); } } export { Db };