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 maincitygetCitiesAggregation: aggregation-cities

Add E2E Test

Like before, for adding getCitiesAggregation request to E2E test section, you should click on the E2E button, like bottom picture.

e2e sequence

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.

e2e sequence

The replaced country id is like below picture.

e2e sequence

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 mainusergetUserAggregation: aggregation-user

Add E2E Test

For adding getUserAggregation request to E2E test section, you should click on the E2E button, like bottom picture.

e2e sequence

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.

e2e sequence

The replaced user id is like below picture.

e2e sequence