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
JavaScript
"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;