Skip to main content

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:

StagePurpose
$lookupJoin the related collection (from = the related schema's collection)
$unwindFlatten single relations (preserveNullAndEmptyArrays: true); multiple arrays stay arrays
$projectShape 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 $match early in the pipeline to reduce rows before $skip/$limit.
  • Create indexes on fields you $match or $sort (see createIndex in Model Options).
  • The options argument is passed straight to MongoDB's aggregate, so allowDiskUse, maxTimeMS, collation, etc. all work.