Skip to main content

Models & ODM

The Models & ODM (Object Document Mapper) system is the heart of Lesan's data layer. It provides type-safe schema definitions, automatic bi-directional relationships, and a rich set of MongoDB operations with built-in projection support.

Schema Definitionโ€‹

Schemas in Lesan define the structure of your data models. Each schema consists of pure fields (scalar values) and relations (links to other schemas).

Pure Fieldsโ€‹

Pure fields are the intrinsic properties of a model โ€” values that belong directly to the document.

import { string, number, boolean, date, optional, array } from "@hemedani/lesan";

const userPure = {
name: string(),
email: string(),
age: number(),
isActive: boolean(),
avatar: optional(string()),
tags: array(string()),
};

Lesan uses Superstruct for schema validation. Available types include:

TypeDescriptionExample
string()String validationname: string()
number()Number validationage: number()
boolean()Boolean validationisActive: boolean()
date()Date validationcreatedAt: date()
optional(type)Optional fieldavatar: optional(string())
array(type)Array of typetags: array(string())
object(shape)Nested objectaddress: object({ city: string() })
instance(Class)Class instance_id: instance(ObjectId)

Creating a Modelโ€‹

const users = coreApp.odm.newModel("user", userPure, {});

Model Optionsโ€‹

You can pass an optional third argument to configure indexing and field exclusion:

const users = coreApp.odm.newModel("user", userPure, {}, {
createIndex: {
indexSpec: { email: 1 },
options: { unique: true },
},
excludes: ["password", "secretToken"],
});
OptionTypeDescription
createIndex{ indexSpec, options }Create a MongoDB index on model creation
excludesstring[]Fields to exclude from projections by default

Relationsโ€‹

Lesan's most powerful feature is automatic bi-directional relationships. You define a relation on one schema, and Lesan automatically maintains the reverse relation on the other schema.

Relation Typesโ€‹

TypeDescriptionUse Case
singleOne-to-one or many-to-oneA city belongs to one province
multipleOne-to-many or many-to-manyA province has many cities

Defining Relationsโ€‹

import { RelationDataType, RelationSortOrderType } from "@hemedani/lesan";

const cityRelations = {
province: {
schemaName: "province",
type: "single" as RelationDataType,
optional: false,
relatedRelations: {
cities: {
type: "multiple" as RelationDataType,
limit: 50,
sort: {
field: "_id",
order: "desc" as RelationSortOrderType,
},
},
},
},
};

const cities = coreApp.odm.newModel("city", cityPure, cityRelations);

Relation Configurationโ€‹

Main Relation (on the source schema)โ€‹

PropertyTypeRequiredDescription
schemaNamestringโœ…Target schema name
type"single" | "multiple"โœ…Relation cardinality
optionalbooleanโœ…Whether the relation can be null
excludesstring[]โŒFields to exclude when embedding
limitnumber | nullโŒMax items for multiple relations
sort{ field, order }โŒDefault sort for related items
relatedRelationsobjectโœ…Back-references to create on target
PropertyTypeDescription
type"single" | "multiple"Back-reference cardinality
limitnumber | nullMax items
sort{ field, order }Default sort
excludesstring[]Fields to exclude

How Relations Workโ€‹

When you define a relation like city -> province, Lesan:

  1. Embeds the related document โ€” When you insert a city, the province's pure fields are embedded directly in the city document
  2. Maintains back-references โ€” The province document gets a cities array containing embedded city documents
  3. Keeps both sides in sync โ€” addRelation, removeRelation, and insertOne handle both sides automatically
City Document:
{
_id: ObjectId("..."),
name: "Tehran",
population: 9000000,
province: {
_id: ObjectId("..."),
name: "Tehran Province",
population: 13000000
}
}

Province Document:
{
_id: ObjectId("..."),
name: "Tehran Province",
population: 13000000,
cities: [
{ _id: ObjectId("..."), name: "Tehran", population: 9000000 },
{ _id: ObjectId("..."), name: "Karaj", population: 2000000 }
]
}

Inserting with Relationsโ€‹

tip

Go deeper

For advanced relationship patterns โ€” multiple relatedRelations to one schema, limit/sort/excludes, addRelation/removeRelation, and hardCascade cascading deletes โ€” see Relations in Depth.

await cities.insertOne({
doc: { name: "Tehran", population: 9000000 },
relations: {
province: {
_ids: new ObjectId(provinceId),
relatedRelations: {
cities: true, // Add to province.cities
citiesByPopulation: false, // Skip this back-reference
},
},
},
});

Database Connectionโ€‹

Before creating models, you must connect to MongoDB:

import { MongoClient } from "@hemedani/lesan";

const client = await new MongoClient("mongodb://localhost:27017/").connect();
const db = client.db("myapp");

coreApp.odm.setDb(db);

setDb()โ€‹

coreApp.odm.setDb(db: Db): Db

Sets the MongoDB database instance. Must be called before any model operations.

getCollection()โ€‹

coreApp.odm.getCollection(collection: string): Collection<Document>

Get a raw MongoDB collection by name. Validates that the collection name matches a defined schema.


CRUD Operationsโ€‹

Each model returned by newModel() provides a full set of CRUD operations.

insertOne() โ€” Insert a Single Documentโ€‹

await cities.insertOne({
doc: { name: "Tehran", population: 9000000 },
relations: {
province: {
_ids: new ObjectId(provinceId),
relatedRelations: { cities: true },
},
},
options: { writeConcern: { w: "majority" } },
projection: { name: 1, population: 1 },
});
ParameterTypeRequiredDescription
docobjectโœ…Document to insert
relationsTInsertRelationsโŒRelations to establish
optionsInsertOneOptionsโŒMongoDB insert options
projectionProjectionโŒFields to return

Returns: Promise<Document \| { _id: ObjectId } \| null>

insertMany() โ€” Insert Multiple Documentsโ€‹

await cities.insertMany({
docs: [
{ name: "Tehran", population: 9000000 },
{ name: "Karaj", population: 2000000 },
],
relations: { /* ... */ },
options: { ordered: true },
});

find() โ€” Query Multiple Documentsโ€‹

const cursor = await cities.find({
filters: { population: { $gt: 1000000 } },
projection: { name: 1, population: 1 },
options: { limit: 10, sort: { population: -1 } },
});

const results = await cursor.toArray();
ParameterTypeRequiredDescription
filtersFilter<Document>โœ…MongoDB query filter
projectionProjectionโŒFields to include/exclude
optionsFindOptionsโŒMongoDB find options

Returns: FindCursor<Document>

findOne() โ€” Query a Single Documentโ€‹

const city = await cities.findOne({
filters: { _id: new ObjectId(cityId) },
projection: { name: 1, "province.name": 1 },
});

Returns: Promise<Document \| null>

findOneAndUpdate() โ€” Update and Returnโ€‹

const result = await cities.findOneAndUpdate({
filter: { _id: new ObjectId(cityId) },
update: { $set: { population: 9500000 } },
options: { includeResultMetadata: true },
projection: { name: 1, population: 1 },
});
ParameterTypeRequiredDescription
filterFilter<Document>โœ…Query filter
updateUpdateFilter<Document>โœ…Update operations
optionsFindOneAndUpdateOptionsโŒMongoDB options
projectionProjectionโŒFields to return

deleteOne() โ€” Delete a Documentโ€‹

await cities.deleteOne({
filter: { _id: new ObjectId(cityId) },
hardCascade: true, // Also remove from all relation back-references
});
ParameterTypeRequiredDescription
filterFilter<Document>โœ…Query filter
optionsDeleteOptionsโŒMongoDB delete options
hardCascadebooleanโŒRemove from all back-references

countDocument() โ€” Count Documentsโ€‹

const count = await cities.countDocument({
filter: { population: { $gt: 1000000 } },
});

Relation Operationsโ€‹

addRelation() โ€” Add Relations to Existing Documentsโ€‹

Attaches one or more relations to an existing document, keeping both sides in sync.

await cities.addRelation({
filters: { _id: new ObjectId(cityId) },
relations: {
province: {
_ids: new ObjectId(newProvinceId),
relatedRelations: {
cities: true,
},
},
},
replace: true, // Replace existing single relation
projection: { name: 1, "province.name": 1 },
});
ParameterTypeRequiredDescription
filtersFilter<Document>โœ…Target document filter
relationsTInsertRelationsโœ…Relations to add
replacebooleanโŒReplace existing single relation
projectionProjectionโŒFields to return
caution

Single Relation Replacement

For single type relations, if the relation already exists and replace is not true, an error is thrown.

removeRelation() โ€” Remove Relationsโ€‹

Removes relations from a document and cleans up back-references.

await cities.removeRelation({
filters: { _id: new ObjectId(cityId) },
relations: {
province: {
_ids: new ObjectId(provinceId),
relatedRelations: {
cities: true, // Remove from province.cities
},
},
},
});

Aggregationโ€‹

Lesan provides aggregation with automatic projection pipeline generation.

const results = await cities.aggregation({
pipeline: [
{ $match: { population: { $gt: 1000000 } } },
{ $group: { _id: "$province.name", total: { $sum: "$population" } } },
],
projection: { name: 1, population: 1 },
options: { allowDiskUse: true },
}).toArray();

When you provide a projection, Lesan automatically generates $lookup, $unwind, and $project stages based on your schema relations.


Schema Introspectionโ€‹

Lesan provides rich schema introspection functions:

getSchemas() โ€” Get All Schemasโ€‹

const allSchemas = coreApp.schemas.getSchemas();
// Returns: Record<string, IModel>

getSchema() โ€” Get a Specific Schemaโ€‹

const citySchema = coreApp.schemas.getSchema("city");
// Returns: { pure: {...}, relations: {...}, mainRelations: {...}, relatedRelations: {...} }

getSchemasKeys() โ€” Get All Schema Namesโ€‹

const schemaNames = coreApp.schemas.getSchemasKeys();
// Returns: ["country", "province", "city", "user"]

getPureSchema() โ€” Get Pure Fields Onlyโ€‹

const pureFields = coreApp.schemas.getPureSchema("city");
// Returns: { name: Struct, population: Struct, ... }

createStruct() โ€” Create a Superstruct Validatorโ€‹

const cityStruct = coreApp.schemas.createStruct("city");
// Returns: Superstruct struct combining pure + embedded relation fields

createEmbedded() โ€” Create Embedded Fieldsโ€‹

const embedded = coreApp.schemas.createEmbedded("city");
// Returns: Pure fields of all related schemas (for nesting in other structs)

createProjection() โ€” Generate MongoDB Projectionsโ€‹

const projection = coreApp.schemas.createProjection("city", "PureMainRelations");
// Returns: { name: 1, population: 1, province: { name: 1, ... } }

Available projection types:

TypeDescription
PureOnly pure (scalar) fields
MainRelationsOnly main relation embedded fields
RelatedRelationsOnly related relation embedded fields
PureMainRelationsPure + main relations
PureRelatedRelationsPure + related relations
MainRelationsRelatedRelationsAll relation fields (no pure)
PureMainRelationsRelatedRelationsEverything

Complete Model Exampleโ€‹

import {
lesan, MongoClient,
string, number, object, ObjectId,
RelationDataType, RelationSortOrderType,
ActFn,
} from "@hemedani/lesan";

const coreApp = lesan();
const client = await new MongoClient("mongodb://localhost:27017/").connect();
coreApp.odm.setDb(client.db("geo"));

// --- Country Model ---
const countryPure = { name: string(), population: number() };
const countries = coreApp.odm.newModel("country", countryPure, {});

// --- Province Model ---
const provinceRelations = {
country: {
schemaName: "country",
type: "single" as RelationDataType,
optional: false,
relatedRelations: {
provinces: {
type: "multiple" as RelationDataType,
limit: 50,
sort: { field: "_id", order: "desc" as RelationSortOrderType },
},
},
},
};
const provinces = coreApp.odm.newModel("province", countryPure, provinceRelations);

// --- City Model ---
const cityRelations = {
province: {
schemaName: "province",
type: "single" as RelationDataType,
optional: false,
relatedRelations: {
cities: {
type: "multiple" as RelationDataType,
limit: 50,
sort: { field: "_id", order: "desc" as RelationSortOrderType },
},
},
},
};
const cities = coreApp.odm.newModel("city", countryPure, cityRelations);

// --- Actions ---
const addCity: ActFn = async (body) => {
const { name, population, provinceId } = body.details.set;
return await cities.insertOne({
doc: { name, population },
relations: {
province: {
_ids: new ObjectId(provinceId),
relatedRelations: { cities: true },
},
},
projection: body.details.get,
});
};

coreApp.acts.setAct({
schema: "city",
actName: "addCity",
validator: object({
set: object({ name: string(), population: number(), provinceId: objectIdValidation }),
get: object(),
}),
fn: addCity,
});

API Reference Tableโ€‹

ODM Functionsโ€‹

FunctionDescription
odm.setDb(db)Set MongoDB database
odm.getCollection(name)Get raw MongoDB collection
odm.newModel(name, pure, relations, options?)Create a new model

Model Methodsโ€‹

MethodDescription
model.find(args)Query multiple documents
model.findOne(args)Query single document
model.insertOne(args)Insert single document
model.insertMany(args)Insert multiple documents
model.addRelation(args)Add relations to document
model.removeRelation(args)Remove relations from document
model.findOneAndUpdate(args)Update and return document
model.deleteOne(args)Delete document
model.aggregation(args)Run aggregation pipeline
model.countDocument(args)Count matching documents

Schema Functionsโ€‹

FunctionDescription
schemas.getSchemas()Get all schemas
schemas.getSchema(name)Get specific schema
schemas.getSchemasKeys()Get all schema names
schemas.getPureSchema(name)Get pure fields
schemas.createStruct(name)Create validator struct
schemas.createEmbedded(name)Create embedded fields
schemas.createProjection(name, type)Generate projection