A type-safe, fluent builder for MongoDB aggregation pipelines. Build complex aggregations with full TypeScript autocomplete and compile-time error checking — no more fragile raw JSON blobs.
npm install mongodb-typesafe-aggregationPeer dependency: requires
mongoose >= 7.
import { PipelineBuilder } from 'mongodb-typesafe-aggregation/builder';
import { filter } from 'mongodb-typesafe-aggregation/types/filter';
interface Order {
_id: string;
status: 'pending' | 'completed';
total: number;
customerId: string;
}
const pipeline = new PipelineBuilder<Order>()
.match({ status: filter<string>().eq('pending').build() })
.sort({ total: -1 })
.limit(20)
.build();
// Pass to your collection
const results = await db.collection<Order>('orders').aggregate(pipeline).toArray();Raw aggregation pipelines have no type safety:
// Before — easy to mistype, no autocomplete, fails only at runtime
[
{ $match: { satuts: 'pending' } },
{
$addFields: {
isLate: { $cond: { if: { $gt: ['$dueDate', new Date()] }, then: true, else: false } },
},
},
];With mongodb-typesafe-aggregation:
// After — typos caught at compile time, full autocomplete
new PipelineBuilder<Order>()
.match({ status: filter<string>().eq('pending').build() })
.addFields({
isLate: filter<boolean>()
.cond({ $gt: ['$dueDate', new Date()] }, { $literal: true }, false)
.build(),
})
.build();Import:
import { PipelineBuilder } from 'mongodb-typesafe-aggregation/builder';All methods return this (for same-type stages) or a new PipelineBuilder instance (for stages that change the document shape). Call .build() at the end to get the PipelineStage[] array.
| Method | MongoDB Stage | Notes |
|---|---|---|
.match(filter) |
$match |
Accepts FilterOperator values |
.lookup(spec) |
$lookup |
Returns PipelineBuilder<T & U> |
.group(spec) |
$group |
Returns PipelineBuilder<G> |
.addFields(spec) |
$addFields |
Returns PipelineBuilder<T & U> |
.set(spec) |
$set |
Alias for $addFields (Mongo 4.2+) |
.project(spec) |
$project |
Returns PipelineBuilder<G> |
.unset(fields) |
$unset |
Accepts string or string[] |
.sort(spec) |
$sort |
Keys must be keyof T |
.skip(n) |
$skip |
|
.limit(n) |
$limit |
|
.unwind(spec) |
$unwind |
Accepts string path or UnwindSpec |
.replaceRoot(spec) |
$replaceRoot |
Returns PipelineBuilder<G> |
.count(fieldName) |
$count |
Returns PipelineBuilder<{[k]: number}> |
.facet(spec) |
$facet |
Returns PipelineBuilder<G> |
.unionWith(spec) |
$unionWith |
|
.graphLookup(spec) |
$graphLookup |
|
.bucket(spec) |
$bucket |
|
.bucketAuto(spec) |
$bucketAuto |
|
.redact(spec) |
$redact |
|
.merge(spec) |
$merge |
Terminal — write to another collection |
.raw(stage) |
any | Escape hatch for unsupported stages |
.build() |
— | Returns the PipelineStage[] array |
Import:
import { FilterOperator, filter } from 'mongodb-typesafe-aggregation/types/filter';
// These two are equivalent
new FilterOperator<number>().gt(100).build();
filter<number>().gt(100).build();| Method | Operator |
|---|---|
.eq(value) |
$eq |
.ne(value) |
$ne |
.gt(value) |
$gt |
.gte(value) |
$gte |
.lt(value) |
$lt |
.lte(value) |
$lte |
| Method | Operator |
|---|---|
.in(values) |
$in |
.nin(values) |
$nin |
.all(values) |
$all |
.size(n) |
$size |
| Method | Operator |
|---|---|
.exists(bool) |
$exists |
.regex(pattern) |
$regex |
| Method | Operator | Use case |
|---|---|---|
.fieldEq(field, value) |
$eq |
[$field, value] form for $cond |
.fieldNe(field, value) |
$ne |
[$field, value] form for $cond |
.cond(if, then, else) |
$cond |
Ternary expression |
Methods can be chained. Multiple comparison operators on the same instance produce a combined object:
filter<number>().gte(100).lte(999).build();
// → { $gte: 100, $lte: 999 }new PipelineBuilder<Product>()
.match({
price: filter<number>().gte(100).lte(999).build(),
category: filter<string>().in(['electronics', 'furniture']).build(),
stock: filter<number>().exists(true).build(),
})
.build();new PipelineBuilder<Product>()
.lookup({ from: 'orders', localField: '_id', foreignField: 'productId', as: 'orders' })
.addFields({
orderedQuantity: { $sum: '$orders.quantity' },
remainingStock: { $subtract: ['$stock', { $sum: '$orders.quantity' }] },
})
.project({ _id: 0, name: 1, remainingStock: 1 })
.build();new PipelineBuilder<Order>()
.group({ _id: '$status', totalOrders: { $sum: 1 }, totalQty: { $sum: '$quantity' } })
.sort({ _id: 1 })
.build();new PipelineBuilder<Product>()
.addFields({
isExpensive: filter<boolean>()
.cond({ $gt: ['$price', 500] }, { $literal: true }, false)
.build(),
})
.build();new PipelineBuilder<Product>()
.facet({
expensive: [{ $match: { price: { $gte: 700 } } }],
lowStock: [{ $match: { stock: { $lt: 20 } } }],
})
.build();new PipelineBuilder<Product>()
.graphLookup({
from: 'products',
startWith: '$relatedIds',
connectFromField: 'relatedIds',
connectToField: '_id',
as: 'relatedProducts',
maxDepth: 2,
})
.build();Contributions are welcome. See CONTRIBUTING.md for setup and guidelines.
See CHANGELOG.md for version history.