Aggregation Pipelines with Client Projections
Lesan's aggregation method lets you run a raw MongoDB pipeline and then appends the projection stages needed to satisfy the client's get. You write the hard logic ($match, $sort, $group, $skip/$limit); Lesan writes the $lookup, $unwind, and $project.
The Methodโ
const cursor = cities.aggregation({
pipeline: [ /* your raw stages */ ],
projection: get, // optional client-driven shape
options: {}, // optional AggregateOptions
});
const result = await cursor.toArray();
The projection is appended to your pipeline as generated stages, so embedded relations resolve automatically.
Basic Aggregationโ
const getCitiesAggregation: ActFn = async (body) => {
const { set: { page, take, countryId }, get } = body.details;
const pipeline: any[] = [];
pipeline.push({ $skip: (page - 1) * take });
pipeline.push({ $limit: take });
if (countryId) {
pipeline.push({ $match: { "country._id": new ObjectId(countryId) } });
}
return await cities.aggregation({ pipeline, projection: get }).toArray();
};
Note the $match on "country._id" โ because relations are embedded snapshots, dot-notation filters work directly on the stored field without any join.
Aggregating Across the Whole Collectionโ
Grouping, counting, and averaging over all documents:
const getStats: ActFn = async (body) => {
return await cities.aggregation({
pipeline: [
{
$group: {
_id: "$country.name",
totalPopulation: { $sum: "$population" },
cityCount: { $sum: 1 },
avgPopulation: { $avg: "$population" },
},
},
{ $sort: { totalPopulation: -1 } },
],
}).toArray();
};
A Single Document via Aggregationโ
const getUserAggregation: ActFn = async (body) => {
const { set: { userId }, get } = body.details;
const pipeline = [{ $match: { _id: new ObjectId(userId) } }];
return await users.aggregation({ pipeline, projection: get }).toArray();
};
How Projection Stages Are Generatedโ
For shallow projections (fields only, no nested relations), Lesan appends a single $project. When a client penetrates a relation, generateProjection adds:
| Stage | Purpose |
|---|---|
$lookup | Join the related collection (from = the related schema's collection) |
$unwind | Flatten single relations (preserveNullAndEmptyArrays: true); multiple arrays stay arrays |
$project | Shape the final output |
Because the first level of every relation is already embedded in the document, no $lookup is needed until the client goes deeper than one level โ that's the key to the fifteen-to-hundreds-of-times-faster reads.
Validator with selectStructโ
Use selectStruct so the client can only request valid projections:
const validator = () =>
object({
set: object({ page: number(), take: number(), countryId: optional(objectIdValidation) }),
get: coreApp.schemas.selectStruct("city", 3),
});
Performance Notesโ
- Prefer
$matchearly in the pipeline to reduce rows before$skip/$limit. - Create indexes on fields you
$matchor$sort(seecreateIndexin Model Options). - The
optionsargument is passed straight to MongoDB'saggregate, soallowDiskUse,maxTimeMS,collation, etc. all work.