UNPKG

@valkyriestudios/mongo

Version:
480 lines (479 loc) 19.4 kB
import { isBoolean } from '@valkyriestudios/utils/boolean/is'; import { isAsyncFn, isFn } from '@valkyriestudios/utils/function'; import { isNeString } from '@valkyriestudios/utils/string'; import { isObject, isNeObject } from '@valkyriestudios/utils/object'; import { isArray, isNeArray, dedupe } from '@valkyriestudios/utils/array'; import { Mongo } from './index'; import { LogLevel } from './Types'; class Query { /* Instance of @valkyriestudios/mongo the query is running against */ #instance; /* Mongo collection the query is for */ #col; /* Log Function */ #log; constructor(instance, col) { if (!(instance instanceof Mongo)) throw new Error('MongoQuery: Expected instance of Mongo'); if (!isNeString(col)) throw new Error('MongoQuery: Expected collection to be a non-empty string'); this.#instance = instance; this.#col = col; this.#log = instance.log; } /** * Counts the number of records. Pass pipeline to run a count with filters. * * Take Note: This will automatically add a $count stage to the pipeline when running an aggregation pipeline * * @param {Filter<Document>|Document[]?} filter - Optional filter object or aggregation pipeline to run * @param {CountOptions|AggregateOptions?} options - Options to use when running a filter/pipeline */ async count(filter, options = {}) { if (!isNeArray(filter) && !isObject(filter) && filter !== undefined) throw new Error('MongoQuery@count: Invalid filter passed'); if (options !== undefined && !isObject(options)) throw new Error('MongoQuery@count: Options should be an object'); try { let result; if (isNeArray(filter)) { result = await this.aggregate([ ...filter, { $count: 'count' }, ], options); } else { /* Connect */ const db = await this.#instance.connect(); /* Run query */ result = [{ count: await db.collection(this.#col) .countDocuments(isObject(filter) ? filter : undefined, options), }]; } /* Validate result */ if (!isNeArray(result) || !isObject(result[0]) || !Number.isInteger(result[0].count) || result[0].count < 0) throw new Error('Unexpected result'); return result[0].count; } catch (err) { const msg = err instanceof Error ? err.message.replace('MongoQuery@aggregate: Failed - ', '') : 'Unknown Error'; throw new Error(`MongoQuery@count: Failed - ${msg}`); } } /** * Quickly checks if a document exists matching the filter * * @param {Filter<TModel>} filter - Filter to match */ async exists(filter = {}) { const count = await this.count(filter, { limit: 1 }); return count > 0; } /** * Run an aggregation pipeline against the collection and return its results * * @param {Document[]} pipeline - Pipeline array to run * @param {AggregateOptions} options - (default={}) Aggregation options * @returns {Promise<Document[]>} Array of documents - Null is returned when aggregation fails */ async aggregate(pipeline, options = {}) { if (!isNeArray(pipeline)) throw new Error('MongoQuery@aggregrate: Pipeline should be an array with content'); if (!isObject(options)) throw new Error('MongoQuery@aggregate: Options should be an object'); /* Sanitize pipeline */ const normalized_pipeline = dedupe(pipeline, { filter_fn: isNeObject }); if (!normalized_pipeline.length) throw new Error('MongoQuery@aggregate: Pipeline is empty after sanitization'); /* Run pipeline */ try { /* Connect */ const db = await this.#instance.connect(); /* Run query */ const result = await db.collection(this.#col).aggregate(normalized_pipeline, options).toArray(); if (!isArray(result)) throw new Error('Unexpected result'); this.#log({ level: LogLevel.DEBUG, fn: 'MongoQuery@aggregate', msg: 'Pipeline run', data: { pipeline: normalized_pipeline, options }, }); return result; } catch (err) { this.#log({ level: LogLevel.ERROR, fn: 'MongoQuery@aggregate', msg: 'Failed to run aggregation', err: err, data: { pipeline: normalized_pipeline, options }, }); return []; } } /** * Find unique values for a specified field across the collection * * @param {string} key - Field name to get distinct values for * @param {Filter<TModel>} filter - Optional filter to narrow scope * @returns {Promise<any[]>} Array of unique values */ async distinct(key, filter = {}, options = {}) { if (!isNeString(key)) throw new Error('MongoQuery@distinct: Key should be a non-empty string'); try { /* Connect */ const db = await this.#instance.connect(); /* Run query */ const result = await db.collection(this.#col).distinct(key, filter, options); if (!isArray(result)) throw new Error('Unexpected result'); this.#log({ level: LogLevel.DEBUG, fn: 'MongoQuery@distinct', msg: 'Distinct run', data: { key, filter }, }); return result; } catch (err) { this.#log({ level: LogLevel.ERROR, fn: 'MongoQuery@distinct', msg: 'Failed to run distinct', err: err, data: { key, filter }, }); return []; } } /** * Find the first document matching the provided query * Take Note: when passing no query it will simply return the first document it finds * * @param {Filter<Document>?} query - Optional Query that matches the document to find * @param {Document?} projection - Optional projection to use, if not passed will return entire object * @returns {Promise<Document|null>} The found document or null * @throws {Error} when provided query or connection fails */ async findOne(query, projection) { if (query !== undefined && !isObject(query)) throw new Error('MongoQuery@findOne: If passed, query should be an object'); if (projection !== undefined && !isObject(projection)) throw new Error('MongoQuery@findOne: If passed, projection should be an object'); try { /* Connect */ const db = await this.#instance.connect(); /* Run query */ const result = await db.collection(this.#col).findOne(query, projection ? { projection } : undefined); return isObject(result) ? result : null; } catch (err) { const msg = err instanceof Error ? err.message : 'Unknown Error'; throw new Error(`MongoQuery@findOne: Failed - ${msg}`); } } /** * Remove the first document matching the provided query * * @param {Filter<Document>} query - Query that matches the document to be removed * @param {DeleteOptions} options - (default={}) Remove options * @returns {Promise<boolean>} Result of the query * @throws {Error} When provided options are invalid or connection fails */ async removeOne(query, options = {}) { if (!isNeObject(query)) throw new Error('MongoQuery@removeOne: Query should be an object with content'); if (!isObject(options)) throw new Error('MongoQuery@removeOne: Options should be an object'); try { /* Connect */ const db = await this.#instance.connect(); /* Run query */ const result = await db.collection(this.#col).deleteOne(query, options); if (!result?.acknowledged) throw new Error('Unacknowledged'); this.#log({ level: LogLevel.DEBUG, fn: 'MongoQuery@removeOne', msg: 'Removal succeeded', data: { query, options, result }, }); return true; } catch (err) { this.#log({ level: LogLevel.ERROR, fn: 'MongoQuery@removeOne', msg: 'Failed to remove', err: err, data: { query, options }, }); return false; } } /** * Remove all documents matching the provided query * * @param {Filter<Document>} query - Query that matches the documents to be removed * @param {DeleteOptions} options - (default={}) Remove options * @returns {Promise<boolean>} Result of the query * @throws {Error} When provided options are invalid or connection fails */ async removeMany(query, options = {}) { if (!isNeObject(query)) throw new Error('MongoQuery@removeMany: Query should be an object with content'); if (!isObject(options)) throw new Error('MongoQuery@removeMany: Options should be an object'); try { /* Connect */ const db = await this.#instance.connect(); /* Run query */ const result = await db.collection(this.#col).deleteMany(query, options); if (!result?.acknowledged) throw new Error('Unacknowledged'); this.#log({ level: LogLevel.DEBUG, fn: 'MongoQuery@removeMany', msg: 'Removal succeeded', data: { query, options, result }, }); return true; } catch (err) { this.#log({ level: LogLevel.ERROR, fn: 'MongoQuery@removeMany', msg: 'Failed to remove', err: err, data: { query, options }, }); return false; } } /** * Update the first document matching the provided query * * @param {Filter<Document>} query - Query that matches the documents to be updated * @param {UpdateFilter<Document>} data - Update to run * @param {UpdateOptions} options - Update Options * @returns {Promise<boolean>} Result of the query * @throws {Error} When provided options are invalid or connection fails */ async updateOne(query, data, options = {}) { if (!isNeObject(query)) throw new Error('MongoQuery@updateOne: Query should be an object with content'); if (!isNeObject(data) && !isNeArray(data)) throw new Error('MongoQuery@updateOne: Data should be an object/array with content'); if (!isObject(options)) throw new Error('MongoQuery@updateOne: Options should be an object'); /* Check if all entries are at least objects with content when passed an update pipeline */ if (isArray(data) && !data.every(isNeObject)) throw new Error('MongoQuery@updateOne: Data pipeline is invalid'); try { /* Connect */ const db = await this.#instance.connect(); /* Run query */ const result = await db.collection(this.#col).updateOne(query, data, options); if (!result?.acknowledged) throw new Error('Unacknowledged'); this.#log({ level: LogLevel.DEBUG, fn: 'MongoQuery@updateOne', msg: 'Update succeeded', data: { query, options, result }, }); return true; } catch (err) { this.#log({ level: LogLevel.ERROR, fn: 'MongoQuery@updateOne', msg: 'Failed', err: err, data: { query, data, options }, }); return false; } } /** * Update all documents matching the provided query * * @param {Filter<Document>} query - Query that matches the documents to be updated * @param {UpdateFilter<Document>} data - Update to run * @param {UpdateOptions} options - Update Options * @returns {Promise<boolean>} Result of the query * @throws {Error} When provided options are invalid or connection fails */ async updateMany(query, data, options = {}) { if (!isNeObject(query)) throw new Error('MongoQuery@updateMany: Query should be an object with content'); if (!isNeObject(data) && !isNeArray(data)) throw new Error('MongoQuery@updateMany: Data should be an object/array with content'); if (!isObject(options)) throw new Error('MongoQuery@updateMany: Options should be an object'); /* Check if all entries are at least objects with content when passed an update pipeline */ if (isArray(data) && !data.every(isNeObject)) throw new Error('MongoQuery@updateMany: Data pipeline is invalid'); try { /* Connect */ const db = await this.#instance.connect(); /* Run query */ const result = await db.collection(this.#col).updateMany(query, data, options); if (!result?.acknowledged) throw new Error('Unacknowledged'); this.#log({ level: LogLevel.DEBUG, fn: 'MongoQuery@updateMany', msg: 'Updated succeeded', data: { query, options, result }, }); return true; } catch (err) { this.#log({ level: LogLevel.ERROR, fn: 'MongoQuery@updateMany', msg: 'Failed to update', err: err, data: { query, options }, }); return false; } } /** * Insert a document into a specific collection * * @param {Document} document - Document to insert * @param {InsertOneOptions} options - Update Options * @returns {Promise<InsertOneResult>} Result of the query * @throws {Error} When provided options are invalid or connection fails */ async insertOne(document, options = {}) { if (!isNeObject(document)) throw new Error('MongoQuery@insertOne: Document should be a non-empty object'); if (!isObject(options)) throw new Error('MongoQuery@insertOne: Options should be an object'); try { /* Connect */ const db = await this.#instance.connect(); /* Run query */ const result = await db.collection(this.#col).insertOne(document, options); if (!result?.acknowledged) throw new Error('Unacknowledged'); this.#log({ level: LogLevel.DEBUG, fn: 'MongoQuery@insertOne', msg: 'Insert succeeded', data: { options, result }, }); return result.insertedId; } catch (err) { this.#log({ level: LogLevel.ERROR, fn: 'MongoQuery@insertOne', msg: 'Failed to insert', err: err, }); return null; } } /** * Insert multiple documents into a specific collection * * @param {Document[]} documents - Array of documents to insert * @returns {Promise<boolean>} Result of the query * @throws {Error} When provided options are invalid or connection fails */ async insertMany(documents) { if (!isNeArray(documents)) throw new Error('MongoQuery@insertMany: Documents should be an array with content'); const normalized_documents = dedupe(documents, { filter_fn: isNeObject }); if (!isNeArray(normalized_documents)) throw new Error('MongoQuery@insertMany: Documents is empty after sanitization'); try { const db = await this.#instance.connect(); const result = await db.collection(this.#col).insertMany(normalized_documents); if (result.insertedCount !== normalized_documents.length) throw new Error('Not all documents were inserted'); this.#log({ level: LogLevel.DEBUG, fn: 'MongoQuery@insertMany', msg: 'Insert succeeded', data: { result }, }); return true; } catch (err) { this.#log({ level: LogLevel.ERROR, fn: 'MongoQuery@insertMany', msg: 'Failed to insert', err: err, }); return false; } } /** * Run bulk operations * * @param {BulkOperatorFunction} fn - Bulk operations callback function * @param {boolean} sorted - Whether or not an unordered (false) or ordered (true) bulk operation should be used * @returns {Promise<BulkWriteResult|null>} Result of the query * @throws {Error} When provided options are invalid or connection fails */ async bulkOps(fn, sorted = false) { if (!isFn(fn)) throw new Error('MongoQuery@bulkOps: Fn should be a function'); if (!isBoolean(sorted)) throw new Error('MongoQuery@bulkOps: Sorted should be a boolean'); try { /* Connect */ const db = await this.#instance.connect(); /* Instantiate bulk operator */ const bulk_operator = db.collection(this.#col)[sorted === false ? 'initializeUnorderedBulkOp' : 'initializeOrderedBulkOp'](); if (!isObject(bulk_operator) || !isFn(bulk_operator.execute)) throw new Error('Not able to acquire bulk operation'); /* Pass bulk operator to fn */ if (!isAsyncFn(fn)) { fn(bulk_operator); } else { await fn(bulk_operator); } /* Execute operations */ const result = await bulk_operator.execute(); if (!isObject(result)) throw new Error('Unexpected result'); this.#log({ level: LogLevel.DEBUG, fn: 'MongoQuery@bulkOps', msg: 'Ran bulk operation', data: { sorted }, }); return result; } catch (err) { this.#log({ level: LogLevel.ERROR, fn: 'MongoQuery@bulkOps', msg: 'Failed to run bulk operation', err: err, data: { sorted }, }); return null; } } } export { Query, Query as default };