Skip to main content

Aggregation

Use aggregation when you need to penetrate more than one step in the depth of relationships โ€” going from father to grandson, or vice versa. Don't worry: every $lookup, $unwind, and $project you need is generated for you. You only write the filtering logic.

The neat trick: Lesan's aggregation is always one step behind the client request. Because relations are embedded, deep paths that can't be read from a snapshot get $lookuped on demand. For the conceptual background, see Why NoSQL? โ€” Lesan pipeline example.

Get a list of documents with aggregationโ€‹

const getCitiesAggregationValidator = () => {
return object({
set: object({
page: number(),
take: number(),
countryId: optional(objectIdValidation),
}),
get: coreApp.schemas.selectStruct("city", 3),
});
};

const getCitiesAggregation: ActFn = async (body) => {
const {
set: { page, take, countryId },
get,
} = body.details;
const pipeline = [];

pipeline.push({ $skip: (page - 1) * take });
pipeline.push({ $limit: take });

countryId &&
pipeline.push({ $match: { "country._id": new ObjectId(countryId) } });

return await cities
.aggregation({
pipeline,
projection: get,
})
.toArray();
};

coreApp.acts.setAct({
schema: "city",
actName: "getCitiesAggregation",
validator: getCitiesAggregationValidator(),
fn: getCitiesAggregation,
});

We add two stages for pagination ($skip, $limit) and one optional $match on the embedded country._id. But those aren't all that reaches the database โ€” Lesan appends the $lookup, $unwind, and $project stages it needs to satisfy the get shape.

For this request:

{
"body": {
"service": "main",
"model": "city",
"act": "getCitiesAggregation",
"details": {
"get": {
"_id": 1,
"name": 1,
"country": { "_id": 1, "name": 1 },
"users": { "_id": 1, "name": 1 }
},
"set": { "page": 1, "take": 10 }
}
}
}

Only a single $project stage is appended (both country and users are already embedded one level deep):

[
{
"$project": {
"_id": 1,
"name": 1,
"country": { "_id": 1, "name": 1 },
"users": { "_id": 1, "name": 1 }
}
}
]

But for a deeper request โ€” country.cities, country.citiesByPopulation, country.capital, users.livedCities, users.country โ€” Lesan appends $lookup/$unwind for each one-step relation, then a $project:

[
{ "$lookup": { "from": "country", "localField": "country._id", "foreignField": "_id", "as": "country" } },
{ "$unwind": { "path": "$country", "preserveNullAndEmptyArrays": true } },
{ "$lookup": { "from": "user", "localField": "users._id", "foreignField": "_id", "as": "users" } },
{ "$project": { "_id": 1, "name": 1, "country": { "_id": 1, "name": 1, "cities": { "_id": 1, "name": 1 }, "citiesByPopulation": { "name": 1, "_id": 1 }, "capital": { "_id": 1, "name": 1 } }, "users": { "_id": 1, "name": 1, "livedCities": { "name": 1, "_id": 1 }, "country": { "_id": 1, "name": 1 } }, "lovedByUser": { "_id": 1, "name": 1 } } }
]

Because we passed 3 to selectStruct("city", 3), the pipeline allows penetrating two extra steps; try deeper get shapes in the playground to see the $lookups grow. The full example is examples/document/07-1-aggregation.ts.

Executing main โ†’ city โ†’ getCitiesAggregation:

getCitiesAggregation act in the playground

Get a single document with aggregationโ€‹

If the client needs a single document's relations more than one step deep, aggregation is required even for one document:

const getUserAggregationValidator = () => {
return object({
set: object({
userId: objectIdValidation,
}),
get: coreApp.schemas.selectStruct("user", 2),
});
};

const getUserAggregation: ActFn = async (body) => {
const {
set: { userId },
get,
} = body.details;
const pipeline = [];

pipeline.push({ $match: { _id: new ObjectId(userId) } });

return await users
.aggregation({
pipeline,
projection: get,
})
.toArray();
};

coreApp.acts.setAct({
schema: "user",
actName: "getUserAggregation",
validator: getUserAggregationValidator(),
fn: getUserAggregation,
});

Note the result is still an array โ€” but with a single member. The full example is examples/document/07-2-aggregation.ts.

Executing main โ†’ user โ†’ getUserAggregation:

getUserAggregation act in the playground

Next stepsโ€‹