Skip to main content

Find and findOne

When you want to penetrate one step into the depth of relationships, find and findOne are the best choices. In a traditional Mongo setup you'd need an aggregation with a $lookup for a left join โ€” but since Lesan embeds all relations, a plain find/findOne already returns one level of relations: the document and its directly-related children.

findOne โ€” find a single documentโ€‹

Add a getUser act:

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

const getUser: ActFn = async (body) => {
const {
set: { userId },
get,
} = body.details;

return await users.findOne({
filters: { _id: new ObjectId(userId) },
projection: get,
});
};

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

findOne accepts three inputs:

  • filters โ€” the MongoDB findOne query.
  • projection โ€” the MongoDB projection operation.
  • options (optional) โ€” the MongoDB findOptions.

Per-relation depth in selectStructโ€‹

Finding a city or country is identical โ€” the only difference is the second argument of selectStruct. Instead of a single number, pass an object whose keys are relation names and whose values specify how deep this act may penetrate that specific relation:

const getCountryValidator = () => {
return object({
set: object({
countryId: objectIdValidation,
}),
get: coreApp.schemas.selectStruct("country", {
citiesByPopulation: 1,
users: 1,
capital: 1,
}),
});
};

const getCountry: ActFn = async (body) => {
const {
set: { countryId },
get,
} = body.details;

return await countries.findOne({
filters: { _id: new ObjectId(countryId) },
projection: get,
});
};

const getCityValidator = () => {
return object({
set: object({
cityId: objectIdValidation,
}),
get: coreApp.schemas.selectStruct("city", { country: 1, lovedByUser: 1 }),
});
};

const getCity: ActFn = async (body) => {
const {
set: { cityId },
get,
} = body.details;

return await cities.findOne({
filters: { _id: new ObjectId(cityId) },
projection: get,
});
};

The full example is examples/document/06-1-find-one.ts.

Executing main โ†’ user โ†’ getUser:

getUser act in the playground

Executing main โ†’ city โ†’ getCity:

getCity act in the playground

Executing main โ†’ country โ†’ getCountry:

getCountry act in the playground

find โ€” find many documentsโ€‹

Add a getUsers act for paginated lists:

const getUsersValidator = () => {
return object({
set: object({
page: number(),
limit: number(),
}),
get: coreApp.schemas.selectStruct("user", 1),
});
};

const getUsers: ActFn = async (body) => {
let {
set: { page, limit },
get,
} = body.details;

page = page || 1;
limit = limit || 50;
const skip = limit * (page - 1);

return await users
.find({ projection: get, filters: {} })
.skip(skip)
.limit(limit)
.toArray();
};

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

find accepts the same three inputs as findOne, and returns a MongoDB cursor โ€” so you can chain .skip(), .limit(), .sort(), and end with .toArray().

Just like with findOne, per-relation depth is set through the selectStruct second argument:

get: coreApp.schemas.selectStruct("country", {
citiesByPopulation: 1,
users: 1,
capital: 1,
}),

The full example is examples/document/06-2-find-methods.ts.

Executing main โ†’ user โ†’ getUsers:

getUsers act in the playground

Executing main โ†’ city โ†’ getCities:

getCities act in the playground

Executing main โ†’ country โ†’ getCountries:

getCountries act in the playground

When do you need aggregation?โ€‹

find/findOne give you one embedded level. If the client asks for country โ†’ cities โ†’ users (two steps), the projection can no longer be satisfied from the embedded snapshot alone โ€” Lesan has to $lookup into other collections. That's when you switch to aggregation, which is one step behind the request and resolves the rest automatically.

Next stepsโ€‹