UNPKG

mongo-pipeliner

Version:

A practical and userful set of tools to help you build and test MongoDB aggregation pipelines.

436 lines (435 loc) 14.7 kB
"use strict"; Object.defineProperty(exports, "__esModule", { value: true }); exports.AggregationPipelineBuilder = void 0; /** * A class that helps you build aggregation pipelines. * It can be used in two ways: * * 1. Extending this class and defining your own methods * 2. Using the methods directly and managing the pipeline on your own * * @class * @implements {IDetailedAggregationPipelineBuilder} * * @example * * import { AggregationPipelineBuilder } from '../lib/base/pipeline-builder'; * import mongoose from 'mongoose'; * import { UserSchema } from '../shared/schema'; * * const User = mongoose.model('User', UserSchema); * * class UserPipeliner extends AggregationPipelineBuilder { * listPaginated(filter: any, page: number, limit: number) { * return this.match(filter).paginate(page, limit).execute(); * } * } * */ class AggregationPipelineBuilder { constructor(collection, pipeline) { this.pipeline = pipeline || []; this.collection = collection; } /** * Filters the documents to pass only * those that match the specified conditions. * * @param match - The query in the $match stage * @returns {} * */ match(match) { this.pipeline.push({ $match: match }); return this; } /** * Groups documents by some specified key * and can perform various aggregate * operations such as adding, counting, * obtaining averages, etc. * * @param group - The query in the $group stage * @returns {AggregationPipelineBuilder} * */ group(group) { this.pipeline.push({ $group: group }); return this; } /** * Sort documents by one or more fields. * * @param {*} sort - The query in the $sort stage * @returns {AggregationPipelineBuilder} * */ sort(sort) { this.pipeline.push({ $sort: sort }); return this; } /** * Limits the number of documents passed * to the next stage in the pipeline. * * @param {number} limit - The query in the $limit stage * @returns {AggregationPipelineBuilder} * */ limit(limit) { this.pipeline.push({ $limit: limit }); return this; } /** * Skips over a specified number of documents * and passes the remaining documents * to the next stage in the pipeline. * * @param {number} skip - The query in the $skip stage * @returns {AggregationPipelineBuilder} * */ skip(skip) { this.pipeline.push({ $skip: skip }); return this; } /** * Adds new fields to documents. * * @param {*} payload - The query in the $set stage * @returns {AggregationPipelineBuilder} * */ set(payload) { this.pipeline.push({ $set: payload }); return this; } /** * Removes/excludes fields from documents. * * @param {string | string[]} prop - The query in the $unset stage * @returns {AggregationPipelineBuilder} * */ unset(prop) { this.pipeline.push({ $unset: prop }); return this; } /** * Reshapes each document in the stream, * such as by adding new fields or removing existing fields. * * @param {*} projection - The query in the $project stage * @returns {AggregationPipelineBuilder} * */ project(projection) { this.pipeline.push({ $project: projection }); return this; } /** * Counts the number of documents input to the stage. * * @param {string} count - The query in the $count stage * @returns {AggregationPipelineBuilder} * */ count(count) { this.pipeline.push({ $count: count }); return this; } /** * Process a set of input documents in multiple different ways, * all in a single aggregation stage. * * @param params - The query in the $lookup stage * @returns {AggregationPipelineBuilder} * */ facet(params) { this.pipeline.push({ $facet: params }); return this; } /** * Writes the resulting documents of the aggregation pipeline * to a collection. The stage can incorporate the results * into an existing collection or write to a new collection. * * @param {string} collectionName - The query in the $out stage * @returns {AggregationPipelineBuilder} * */ out(collectionName) { this.pipeline.push({ $out: collectionName }); return this; } /** * Writes the resulting documents of the aggregation pipeline * to a collection. The stage can incorporate the results * into an existing collection or write to a new collection. * * IMPORTANT: * * - If the collection does not exist, the $merge stage creates the collection. * - If the collection does exist, the $merge stage combines the documents from the input * and the specified collection. * * @param {string} into - Into which collection the results will be written * @param {string} on - Optional. Field or fields that act as a unique identifier * for a document. * @param {string} whenMatched - Optional. The behavior of $merge if a result document and * an existing document in the collection have the same value * for the specified on field(s). * @param {string} whenNotMatched - Optional. The behavior of $merge if a result document does * not match an existing document in the out collection. * @returns {AggregationPipelineBuilder} */ merge(params) { this.pipeline.push({ $merge: { into: params.into, on: params.on, whenMatched: params.whenMatched || 'merge', whenNotMatched: params.whenNotMatched || 'insert', }, }); return this; } /** * Performs a union of two collections. $unionWith combines pipeline results from two collections * into a single result set. The stage outputs the combined result set (including duplicates) * to the next stage. * * @param {string} collectionName - The name of the collection to union with * @param {any[]} pipeline - The pipeline to execute on the unioned collection * * @returns {AggregationPipelineBuilder} */ unionWith(collectionName, pipeline) { this.pipeline.push({ $unionWith: { coll: collectionName, pipeline } }); return this; } /** * Perform a join with another collection. * It can be used to combine documents from two collections. * * @param {string} from - Collection to join * @param {string} localField - Field from the input documents * @param {string} foreignField - Field from the documents of the "from" collection * @param {string} as - Name of the new array field to add to the input documents * @returns {AggregationPipelineBuilder} * */ lookup(params) { this.pipeline.push({ $lookup: { from: params.from, localField: params.localField, foreignField: params.foreignField, as: params.as, }, }); return this; } /** * Deconstructs an array field from the input documents to output a document * for each element. * Each output document is the input document with the value of the array * field replaced by the element. * * @param {string} path - Path to unwind * @param {boolean} preserveNullAndEmptyArrays - If true, if the path is null or empty, it will be preserved * @returns {AggregationPipelineBuilder} */ unwind(path, preserveNullAndEmptyArrays = false) { this.pipeline.push({ $unwind: { path, preserveNullAndEmptyArrays } }); return this; } /** * Adds new fields to documents. $addFields outputs documents that contain all * existing fields from the input documents and newly added fields. * * @param {object} fields - The fields to add * @returns {AggregationPipelineBuilder} */ addFields(fields) { this.pipeline.push({ $addFields: fields }); return this; } /** * Perform a join with another collection. * It can be used to combine documents from two collections. * This method allows you to specify a custom pipeline * to execute on the joined collection. * * @param {string} collectionName - Collection to join * @param {string} localField - Field from the input documents * @param {string} matchExpression - Filter condition for the documents of the "from" collection * @param {string} projection - Specifies the fields to return in the documents of the "from" collection * @param {string} as - Name of the new array field to add to the input documents * @returns {AggregationPipelineBuilder} * * @example * * const pipeliner = new AggregationPipelineBuilder(); * const result = pipeliner * .customLookup({ * collectionName: 'bookings', * localField: 'bookingId', * matchExpression: { $eq: ['$_id', '$$bookingId'] }, * projection: { _id: 0, name: 1, date: 1 }, * as: 'bookings', * }) * * // In case you need to implement an alias for 'composed' localField * // Just ensure to use the 'alias' property in the matchExpression object * * const pipeliner = new AggregationPipelineBuilder(); * const result = pipeliner * .customLookup({ * collectionName: 'authors', * localField: { * ref: 'author.refId', * alias: 'authorId', * }, * matchExpression: { $eq: ['$_id', '$$authorId'] }, * projection: { _id: 0, name: 1, date: 1 }, * as: 'author', * }) * */ customLookup(params) { const lookup_pipeline = []; if (params.matchExpression) { lookup_pipeline.push({ $match: { $expr: params.matchExpression } }); } if (params.projection) { lookup_pipeline.push({ $project: params.projection }); } this.pipeline.push({ $lookup: { from: params.collectionName, let: { [typeof params.localField === 'string' ? params.localField : params.localField.alias]: `$${typeof params.localField === 'string' ? params.localField : params.localField.ref}`, }, pipeline: lookup_pipeline, as: params.as, }, }); return this; } /** * Perform a join with another collection. * It can be used to combine documents from two collections. * This method allows you to specify a custom pipeline * to execute on the joined collection. * * It also unwind the joined collection automatically. * * * @param {string} collectionName - Collection to join * @param {string} localField - Field from the input documents * @param {string} matchExpression - Filter condition for the documents of the "from" collection * @param {string} projection - Specifies the fields to return in the documents of the "from" collection * @param {string} as - Name of the new array field to add to the input documents * @returns {AggregationPipelineBuilder} * * @example * * const pipeliner = new AggregationPipelineBuilder(); * const result = pipeliner * .customUnwindLookup({ * collectionName: 'bookings', * localField: 'bookingId', * matchExpression: { $eq: ['$_id', '$$bookingId'] }, * projection: { _id: 0, name: 1, date: 1 }, * as: 'bookings', * }) * * // In case you need to implement an alias for 'composed' localField * // Just ensure to use the 'alias' property in the matchExpression object * * const pipeliner = new AggregationPipelineBuilder(); * const result = pipeliner * .customLookup({ * collectionName: 'authors', * localField: { * ref: 'author.refId', * alias: 'authorId', * }, * matchExpression: { $eq: ['$_id', '$$authorId'] }, * projection: { _id: 0, name: 1, date: 1 }, * as: 'author', * }) * */ customUnwindLookup(params) { this.customLookup(params); this.pipeline.push({ $unwind: { path: `$${params.as}`, preserveNullAndEmptyArrays: true } }); return this; } /** * Builds a pipeline that will paginate the results * according to a limit and page number (skip). * * @param {number} limit - The query in the $limit stage * @param {number} page - The query in the $skip stage * @returns {AggregationPipelineBuilder} */ paginate(limit = 10, page = 1) { this.pipeline.push({ $skip: limit * (page - 1) }); this.pipeline.push({ $limit: limit }); return this; } /** * Allows you to add a custom stage to the pipeline. * * NOTE: This method is not type-safe and should be used with caution. * * @param {PipelineStage} stage - The custom stage to add to the pipeline * @returns {AggregationPipelineBuilder} */ addCustom(stage) { this.pipeline.push(stage); return this; } /** * Performs aggregation using the specified * aggregation pipeline. * * @param params - The query in the $lookup stage * @returns {Promise<any[]>} * */ async execute() { if (!this.collection) { throw new Error('No collection defined for this AggregationBuilder.'); } return this.collection.aggregate(this.pipeline).exec(); } /** * Returns the aggregation pipeline. * If reset is true, the pipeline will be reseted. * Otherwise, it will keep the pipeline. * * @param {boolean} reset - If true, the pipeline will be reseted * @returns {PipelineStage[]} - The aggregation pipeline * */ assemble(reset = true) { const pipeline = this.pipeline; if (reset) { this.reset(); } return pipeline; } /** * Resets the aggregation pipeline. * @returns {void} * */ reset() { this.pipeline = []; } } exports.AggregationPipelineBuilder = AggregationPipelineBuilder;