Skip to main content

The addRelation Function

In addition to the usual CRUD functions, every model exposes two relation-management functions: addRelation and removeRelation. Use them to change the relations of an existing document. This tutorial covers addRelation โ€” both the many-to-many and the one-to-many cases.

Update a many-to-many relationโ€‹

The most common case: add one or more cities to the livedCities of an existing user.

const addUserLivedCityValidator = () => {
return object({
set: object({
_id: objectIdValidation,
livedCities: array(objectIdValidation),
}),
get: coreApp.schemas.selectStruct("user", 1),
});
};

const addUserLivedCity: ActFn = async (body) => {
const { livedCities, _id } = body.details.set;
const obIdLivedCities = livedCities.map((lc: string) => new ObjectId(lc));

return await users.addRelation({
filters: { _id: new ObjectId(_id) },
projection: body.details.get,
relations: {
livedCities: {
_ids: obIdLivedCities,
relatedRelations: {
users: true,
},
},
},
});
};

coreApp.acts.setAct({
schema: "user",
actName: "addUserLivedCities",
validator: addUserLivedCityValidator(),
fn: addUserLivedCity,
});

addRelation takes an object with these keys:

  • filters โ€” a MongoDB findOne filter that selects the single document whose relations we change.
  • relations โ€” the relation input, exactly like the one we used in insertOne (see Add a new act with a relation). Keys are this model's relation names; each value has _ids and an optional relatedRelations map.
  • projection โ€” shapes the written data, same as everywhere else.
  • replace (optional) โ€” a boolean. Defaults to false; we use it below for single relations.

In the act above, the user id and an array of city ids come in through validation. We convert the string ids to ObjectId, then hand everything to addRelation. On the user side, the cities are added to livedCities; on the city side, the user is added to each city's users list. Both sides, one call.

Update a one-to-many (single) relationโ€‹

What if the relation field is an object instead of an array โ€” a single relation like the country on a user? Look at this:

const addUserCountryValidator = () => {
return object({
set: object({
_id: objectIdValidation,
country: objectIdValidation,
}),
get: coreApp.schemas.selectStruct("user", 1),
});
};

const addUserCountry: ActFn = async (body) => {
const { country, _id } = body.details.set;

return await users.addRelation({
filters: { _id: new ObjectId(_id) },
projection: body.details.get,
relations: {
country: {
_ids: new ObjectId(country),
relatedRelations: {
users: true,
},
},
},
replace: true,
});
};

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

Because country is not optional and is defined with type: "single", changing a user's country means removing the user from the old country's users list and adding them to the new one. That's why we pass replace: true. If replace is false (or omitted) while a value already exists, Lesan refuses the operation rather than silently leaving both sides out of sync.

What happens under the hoodโ€‹

The bottom line is a bit involved, but it's all automatic:

  1. Find the user.
  2. Find the user's old country.
  3. Find the user's new country.
  4. Check whether the user appears in any list of the old country (a country can keep several lists โ€” e.g. cities, citiesByPopulation, users).
  5. Build a command to remove the user from every list they were found in.
  6. If a list has a limit and we've reached it, find the next documents that qualify and build the commands to refill, unify, sort, and trim the list.
  7. Build a command to add the user to all relevant lists of the new country (again honoring any limit).
  8. Run all the accumulated commands.
  9. Run the command that stores the new country in the user.

The next three lessons (findOneAndUpdate, deleteOne) show the same engine at work in other directions.

Run the codeโ€‹

The complete runnable example is examples/document/05-1-add-relation-fn-1.ts (many-to-many) and examples/document/05-1-add-relation-fn-2.ts (one-to-many) in the Lesan repository.

Run the code and open the playground โ€” you'll see addUserLivedCities and addUserCountry under main โ†’ user, and you can test them against the data created in the previous tutorials.

Before running addUserLivedCities:

addUserLivedCities act in the playground, before running

While running:

addUserLivedCities act in the playground, executing

After running โ€” the city is now in livedCities:

addUserLivedCities act in the playground, after running

For addUserCountry (the single relation with replace: true), the same before/executing/after flow applies:

addUserCountry act in the playground, before running

addUserCountry act in the playground, executing

addUserCountry act in the playground, after running

Next stepsโ€‹