@valkyriestudios/mongo
Version:
MongoDB Adapter Library
484 lines (483 loc) • 20.2 kB
JavaScript
"use strict";
Object.defineProperty(exports, "__esModule", { value: true });
exports.default = exports.Query = void 0;
const is_1 = require("@valkyriestudios/utils/boolean/is");
const function_1 = require("@valkyriestudios/utils/function");
const string_1 = require("@valkyriestudios/utils/string");
const object_1 = require("@valkyriestudios/utils/object");
const array_1 = require("@valkyriestudios/utils/array");
const index_1 = require("./index");
const Types_1 = require("./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 index_1.Mongo))
throw new Error('MongoQuery: Expected instance of Mongo');
if (!(0, string_1.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 (!(0, array_1.isNeArray)(filter) &&
!(0, object_1.isObject)(filter) &&
filter !== undefined)
throw new Error('MongoQuery@count: Invalid filter passed');
if (options !== undefined &&
!(0, object_1.isObject)(options))
throw new Error('MongoQuery@count: Options should be an object');
try {
let result;
if ((0, array_1.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((0, object_1.isObject)(filter) ? filter : undefined, options),
}];
}
/* Validate result */
if (!(0, array_1.isNeArray)(result) ||
!(0, object_1.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 (!(0, array_1.isNeArray)(pipeline))
throw new Error('MongoQuery@aggregrate: Pipeline should be an array with content');
if (!(0, object_1.isObject)(options))
throw new Error('MongoQuery@aggregate: Options should be an object');
/* Sanitize pipeline */
const normalized_pipeline = (0, array_1.dedupe)(pipeline, { filter_fn: object_1.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 (!(0, array_1.isArray)(result))
throw new Error('Unexpected result');
this.#log({
level: Types_1.LogLevel.DEBUG,
fn: 'MongoQuery@aggregate',
msg: 'Pipeline run',
data: { pipeline: normalized_pipeline, options },
});
return result;
}
catch (err) {
this.#log({
level: Types_1.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 (!(0, string_1.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 (!(0, array_1.isArray)(result))
throw new Error('Unexpected result');
this.#log({
level: Types_1.LogLevel.DEBUG,
fn: 'MongoQuery@distinct',
msg: 'Distinct run',
data: { key, filter },
});
return result;
}
catch (err) {
this.#log({
level: Types_1.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 &&
!(0, object_1.isObject)(query))
throw new Error('MongoQuery@findOne: If passed, query should be an object');
if (projection !== undefined &&
!(0, object_1.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 (0, object_1.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 (!(0, object_1.isNeObject)(query))
throw new Error('MongoQuery@removeOne: Query should be an object with content');
if (!(0, object_1.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: Types_1.LogLevel.DEBUG,
fn: 'MongoQuery@removeOne',
msg: 'Removal succeeded',
data: { query, options, result },
});
return true;
}
catch (err) {
this.#log({
level: Types_1.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 (!(0, object_1.isNeObject)(query))
throw new Error('MongoQuery@removeMany: Query should be an object with content');
if (!(0, object_1.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: Types_1.LogLevel.DEBUG,
fn: 'MongoQuery@removeMany',
msg: 'Removal succeeded',
data: { query, options, result },
});
return true;
}
catch (err) {
this.#log({
level: Types_1.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 (!(0, object_1.isNeObject)(query))
throw new Error('MongoQuery@updateOne: Query should be an object with content');
if (!(0, object_1.isNeObject)(data) && !(0, array_1.isNeArray)(data))
throw new Error('MongoQuery@updateOne: Data should be an object/array with content');
if (!(0, object_1.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 ((0, array_1.isArray)(data) && !data.every(object_1.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: Types_1.LogLevel.DEBUG,
fn: 'MongoQuery@updateOne',
msg: 'Update succeeded',
data: { query, options, result },
});
return true;
}
catch (err) {
this.#log({
level: Types_1.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 (!(0, object_1.isNeObject)(query))
throw new Error('MongoQuery@updateMany: Query should be an object with content');
if (!(0, object_1.isNeObject)(data) && !(0, array_1.isNeArray)(data))
throw new Error('MongoQuery@updateMany: Data should be an object/array with content');
if (!(0, object_1.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 ((0, array_1.isArray)(data) && !data.every(object_1.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: Types_1.LogLevel.DEBUG,
fn: 'MongoQuery@updateMany',
msg: 'Updated succeeded',
data: { query, options, result },
});
return true;
}
catch (err) {
this.#log({
level: Types_1.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 (!(0, object_1.isNeObject)(document))
throw new Error('MongoQuery@insertOne: Document should be a non-empty object');
if (!(0, object_1.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: Types_1.LogLevel.DEBUG,
fn: 'MongoQuery@insertOne',
msg: 'Insert succeeded',
data: { options, result },
});
return result.insertedId;
}
catch (err) {
this.#log({
level: Types_1.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 (!(0, array_1.isNeArray)(documents))
throw new Error('MongoQuery@insertMany: Documents should be an array with content');
const normalized_documents = (0, array_1.dedupe)(documents, { filter_fn: object_1.isNeObject });
if (!(0, array_1.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: Types_1.LogLevel.DEBUG,
fn: 'MongoQuery@insertMany',
msg: 'Insert succeeded',
data: { result },
});
return true;
}
catch (err) {
this.#log({
level: Types_1.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 (!(0, function_1.isFn)(fn))
throw new Error('MongoQuery@bulkOps: Fn should be a function');
if (!(0, is_1.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 (!(0, object_1.isObject)(bulk_operator) || !(0, function_1.isFn)(bulk_operator.execute))
throw new Error('Not able to acquire bulk operation');
/* Pass bulk operator to fn */
if (!(0, function_1.isAsyncFn)(fn)) {
fn(bulk_operator);
}
else {
await fn(bulk_operator);
}
/* Execute operations */
const result = await bulk_operator.execute();
if (!(0, object_1.isObject)(result))
throw new Error('Unexpected result');
this.#log({
level: Types_1.LogLevel.DEBUG,
fn: 'MongoQuery@bulkOps',
msg: 'Ran bulk operation',
data: { sorted },
});
return result;
}
catch (err) {
this.#log({
level: Types_1.LogLevel.ERROR,
fn: 'MongoQuery@bulkOps',
msg: 'Failed to run bulk operation',
err: err,
data: { sorted },
});
return null;
}
}
}
exports.Query = Query;
exports.default = Query;