aggregation functions
We should use Aggregation
when we want to penetrate more than one step in the depth of relationships, that is, if we want to go from father to grandson or vice versa.
Don't worry, all the commands you need to penetrate the depths of the relationship and select their fields are automatically generated by Lesan.
The great thing about Aggregation
in Lesan is that relationship penetration is always one step behind the client request. For more information about this please see here
We can use aggregation
instead of find
and findOne
.
Get list of documents with aggregation
Pay attention to the code written below:
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,
});
In the code above, we have used aggregation
to find the cities.
As you can see, we have added two pipelines
to create pagination by using page
and take
inputs, but these two pipelines
are not all that is sent to the database. Lesan automatically creates lookup
, unwind
and projection
pipelines based on get
input. So that we can establish a join
between the schemas
and select
and return
the data requested by the user.
If this request
is sent to the server:
{
"body": {
"method": "POST",
"headers": {
"Content-Type": "application/json",
"Authorization": ""
},
"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
}
}
}
}
}
these pipelines
will be created:
[
{
"$project": {
"_id": 1,
"name": 1,
"country": { "_id": 1, "name": 1 },
"users": { "_id": 1, "name": 1 }
}
}
]
And if this request
is sent to the server:
{
"body": {
"method": "POST",
"headers": {
"Content-Type": "application/json",
"Authorization": ""
},
"body": {
"service": "main",
"model": "city",
"act": "getCitiesAggregation",
"details": {
"get": {
"_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
}
},
"set": {
"page": 1,
"take": 10
}
}
}
}
}
these pipelines
will be created:
[
{
"$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
}
}
}
]
Note that pipelines
are always one step behind the request
, and send indexed
lookup with _id
for anything. because we embed
all relations.
Because we have given the second input 3
in the coreApp.schemas.selectStruct("city", 3)
function, we can penetrate one more step in the depth of relationships, you can send more complex queries in the playground.
You can find full example here and test the aggregation
method in local computer.
executing main
→ city
→ getCitiesAggregation
:
Add E2E Test
Like before, for adding getCitiesAggregation
request to E2E test section, you should click on the E2E button, like bottom picture.
Then, in the E2E section and getCitiesAggregation
sequence, you should replace the country id that you set capture in own sequence with default country id. default country id in getCitiesAggregation
sequence is like below picture.
The replaced country id is like below picture.
Get a one document with aggregation
Because we may request the relations of a document more than one step and if we want to use lookup
between two schemas, we have to use aggregation
even to receive a document.
Enter the following code to find a user
:
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 that the returned information will still be in an array
but with one member.
You can find full example here and test the aggregation
method in local computer.
executing main
→ user
→ getUserAggregation
:
Add E2E Test
For adding getUserAggregation
request to E2E test section, you should click on the E2E button, like bottom picture.
Then, in the E2E section and getUserAggregation
sequence, you should replace the user id that you set capture in own sequence with default user id. default user id in getUserAggregation
sequence is like below picture.
The replaced user id is like below picture.