Queries & Projections
Lesan's query system combines the power of MongoDB with client-driven projections โ the client decides exactly what data to fetch, reducing over-fetching and under-fetching without the complexity of GraphQL.
Client-Driven Projectionsโ
Unlike traditional REST APIs where the server decides the response shape, Lesan lets the client specify exactly which fields and relations to return.
The get Objectโ
Every Lesan request includes a details.get object that defines the desired output shape:
{
service: "main",
model: "city",
act: "getCity",
details: {
set: { _id: "507f1f77bcf86cd799439011" },
get: {
name: 1,
population: 1,
province: {
name: 1,
country: {
name: 1
}
}
}
}
}
This request says: "Give me the city's name and population, plus its province's name, plus that province's country's name."
Projection Syntaxโ
Projections follow MongoDB's projection syntax:
| Value | Meaning |
|---|---|
1 | Include this field |
0 | Exclude this field |
| Nested object | Include nested relation fields |
// Include only name and population
{ name: 1, population: 1 }
// Include name and province relation (with province's name)
{ name: 1, province: { name: 1 } }
// Exclude password field
{ password: 0 }
How Projections Workโ
When you provide a projection to a model method, Lesan:
- Parses the projection against your schema
- Generates an aggregation pipeline with
$lookupstages for relations - Executes the pipeline on MongoDB
- Returns shaped data exactly matching the projection
Client Request: { name: 1, province: { name: 1 } }
โ
Lesan generates: [ { $match: {...} },
{ $lookup: { from: "province", ... } },
{ $unwind: "$province" },
{ $project: { name: 1, "province.name": 1 } } ]
โ
MongoDB returns: { name: "Tehran", province: { name: "Tehran Province" } }
Query Operationsโ
Basic Find with Projectionโ
const cities = coreApp.odm.newModel("city", cityPure, cityRelations);
// In your action:
const getCity: ActFn = async (body) => {
return await cities.findOne({
filters: { _id: new ObjectId(body.details.set._id) },
projection: body.details.get, // Client decides what to fetch!
});
};
Deep Nestingโ
Projections can traverse multiple levels of relations:
// City โ Province โ Country โ Continent
const getCityDeep: ActFn = async (body) => {
return await cities.findOne({
filters: { _id: new ObjectId(body.details.set._id) },
projection: {
name: 1,
province: {
name: 1,
country: {
name: 1,
continent: {
name: 1
}
}
}
},
});
};
Array Relationsโ
For multiple type relations, projections automatically handle arrays:
// Get province with its cities (array)
const getProvince: ActFn = async (body) => {
return await provinces.findOne({
filters: { _id: new ObjectId(body.details.set._id) },
projection: {
name: 1,
cities: { // This is a multiple relation
name: 1,
population: 1
}
},
});
};
Deeper examples
For copyable advanced patterns โ per-relation selectStruct depths, dynamic projections, and deep nesting โ see Client-Driven Projections, Aggregation Pipelines, Filtering, and Pagination.
Aggregation Pipelineโ
Lesan's aggregation method combines raw MongoDB pipelines with automatic projection generation.
Basic Aggregationโ
const results = await cities.aggregation({
pipeline: [
{ $match: { population: { $gt: 1000000 } } },
{ $sort: { population: -1 } },
{ $limit: 10 },
],
}).toArray();
Aggregation with Projectionโ
const results = await cities.aggregation({
pipeline: [
{ $match: { population: { $gt: 1000000 } } },
],
projection: {
name: 1,
population: 1,
province: {
name: 1,
},
},
}).toArray();
When projection is provided, Lesan appends the generated pipeline stages to your custom pipeline.
Generated Pipeline Stagesโ
Lesan generates the following stages based on your projection:
| Stage | Purpose |
|---|---|
$lookup | Join related collections |
$unwind | Flatten single-type relation arrays |
$project | Shape the final output |
$addFields | Compute derived fields |
$sort | Sort relation arrays |
$slice | Limit relation array size |
Filteringโ
Lesan uses standard MongoDB filters with full operator support.
Comparison Operatorsโ
await cities.find({
filters: {
population: { $gt: 1000000 }, // Greater than
name: { $ne: "Unknown" }, // Not equal
code: { $in: ["TEH", "KAR"] }, // In array
},
});
Logical Operatorsโ
await cities.find({
filters: {
$and: [
{ population: { $gt: 1000000 } },
{ isCapital: true },
],
$or: [
{ name: { $regex: "^T" } },
{ name: { $regex: "^K" } },
],
},
});
Relation Filteringโ
Filter on embedded relation fields using dot notation:
await cities.find({
filters: {
"province.name": "Tehran Province",
"province.country.name": "Iran",
},
});
Sorting and Paginationโ
Sortingโ
await cities.find({
filters: {},
options: {
sort: { population: -1 }, // Descending by population
limit: 20,
},
});
Paginationโ
const page = 2;
const pageSize = 20;
await cities.find({
filters: {},
options: {
sort: { _id: -1 },
skip: (page - 1) * pageSize,
limit: pageSize,
},
});
Cursor-Based Paginationโ
For better performance with large datasets:
const lastId = "..."; // Last seen _id from previous page
await cities.find({
filters: {
_id: { $lt: new ObjectId(lastId) },
},
options: {
sort: { _id: -1 },
limit: 20,
},
});
Select Struct for Validationโ
Lesan provides selectStruct for creating validators that match your schema's shape at specific depths.
// Create a validator that accepts projections up to depth 2
const validator = object({
set: object({ _id: objectIdValidation }),
get: coreApp.schemas.selectStruct("city", 2),
});
The selectStruct generates a Superstruct validator that accepts any valid projection for the schema up to the specified depth.
Advanced Query Patternsโ
Conditional Projectionsโ
Build projections dynamically based on client input:
const getUser: ActFn = async (body) => {
const { _id, includeProfile, includeOrders } = body.details.set;
const projection: any = {
name: 1,
email: 1,
};
if (includeProfile) {
projection.profile = { bio: 1, avatar: 1 };
}
if (includeOrders) {
projection.orders = { total: 1, status: 1, items: { name: 1, price: 1 } };
}
return await users.findOne({
filters: { _id: new ObjectId(_id) },
projection,
});
};
Count with Filtersโ
const getStats: ActFn = async (body) => {
const filter = body.details.set.filter || {};
const [total, items] = await Promise.all([
cities.countDocument({ filter }),
cities.find({ filters: filter, options: { limit: 10 } }).toArray(),
]);
return { total, items };
};
Aggregation with Groupingโ
const getStats: ActFn = async (body) => {
return await cities.aggregation({
pipeline: [
{ $match: { "province.name": body.details.set.provinceName } },
{
$group: {
_id: "$province.name",
totalPopulation: { $sum: "$population" },
cityCount: { $sum: 1 },
avgPopulation: { $avg: "$population" },
},
},
],
}).toArray();
};
Performance Considerationsโ
Indexingโ
Create indexes for fields you frequently filter or sort by:
const cities = coreApp.odm.newModel("city", cityPure, cityRelations, {
createIndex: {
indexSpec: { population: -1 },
options: {},
},
});
Projection Limitsโ
- Use
limitonmultiplerelations in schema definitions to prevent huge arrays - Use
excludesto omit large fields (like base64 images) from automatic projections - Use
FindOptions.limitto cap query results
Embedded vs. Referencedโ
Lesan uses denormalization (embedded documents) rather than foreign keys. This means:
- Reads are extremely fast โ no joins needed for single-document lookups
- Writes are managed โ relation updates handle both sides automatically
- Storage is traded for speed โ related data is stored in multiple places
Complete Query Exampleโ
// Client sends this request:
const requestBody = {
service: "main",
model: "city",
act: "searchCities",
details: {
set: {
minPopulation: 1000000,
provinceName: "Tehran Province",
},
get: {
name: 1,
population: 1,
province: {
name: 1,
country: {
name: 1,
flag: 1,
},
},
},
},
};
// Server action:
const searchCities: ActFn = async (body) => {
const { minPopulation, provinceName } = body.details.set;
return await cities.find({
filters: {
population: { $gte: minPopulation },
"province.name": provinceName,
},
projection: body.details.get,
options: {
sort: { population: -1 },
limit: 20,
},
}).toArray();
};
// Result:
[
{
name: "Tehran",
population: 9000000,
province: {
name: "Tehran Province",
country: {
name: "Iran",
flag: "๐ฎ๐ท"
}
}
},
// ...
]
API Reference Tableโ
Query Methodsโ
| Method | Description |
|---|---|
model.find({ filters, projection, options }) | Query with cursor |
model.findOne({ filters, projection, options }) | Single document |
model.aggregation({ pipeline, projection, options }) | Aggregation pipeline |
model.countDocument({ filter, options }) | Count documents |
Optionsโ
| Option | Description |
|---|---|
filters | MongoDB filter object |
projection | Client-driven shape definition |
options.limit | Max documents to return |
options.skip | Documents to skip |
options.sort | Sort specification |
Projection Typesโ
type Projection = { [key: string]: number | Projection };
// Examples:
{ name: 1 }
{ name: 1, province: { name: 1 } }
{ password: 0 }