Stores in Modelence provide a type-safe interface for MongoDB collections with built-in schema validation, custom methods, and indexing support.
Overview
A Store represents a MongoDB collection with:
- Type-safe schemas using Modelence schema types (based on Zod)
- Custom document methods for business logic
- MongoDB indexes for query performance
- Search indexes for MongoDB Atlas Search
- CRUD operations with full TypeScript support
Creating a Store
Define a Store by specifying a collection name and configuration:
import { Store, schema } from 'modelence/server';
export const dbTodos = new Store('todos', {
schema: {
title: schema.string(),
isCompleted: schema.boolean(),
dueDate: schema.date().optional(),
userId: schema.userId(),
createdAt: schema.date(),
},
indexes: [
{ key: { userId: 1 } },
{ key: { dueDate: 1 } },
],
methods: {
isOverdue() {
return this.dueDate ? this.dueDate < new Date() : false;
}
}
});
Schema Definition
Modelence schemas are based on and closely resemble Zod types. Available schema types include:
{
// Primitive types
name: schema.string(),
age: schema.number(),
isActive: schema.boolean(),
createdAt: schema.date(),
// Optional fields
description: schema.string().optional(),
// Arrays
tags: schema.array(schema.string()),
// Objects
metadata: schema.object({
key: schema.string(),
value: schema.string(),
}),
// Built-in Modelence types
userId: schema.userId(), // References a user ID
}
Custom Methods
Add custom methods to documents for business logic:
export const dbProducts = new Store('products', {
schema: {
name: schema.string(),
price: schema.number(),
discount: schema.number().optional(),
},
methods: {
getFinalPrice() {
return this.discount
? this.price * (1 - this.discount / 100)
: this.price;
},
hasDiscount() {
return !!this.discount && this.discount > 0;
}
}
});
// Usage
const product = await dbProducts.findById(productId);
console.log(product.getFinalPrice()); // Custom method available
Indexes
For index configuration and examples, see Indexes. The indexes
guide covers:
- MongoDB
indexes
- Atlas
searchIndexes
indexCreationMode (blocking vs background) and startup behavior
with migrations
CRUD Operations
Stores provide comprehensive methods for data operations:
Finding Documents
// Find one document
const todo = await dbTodos.findOne({ userId: user.id });
// Find by ID
const todo = await dbTodos.findById(todoId);
// Require one (throws error if not found)
const todo = await dbTodos.requireById(todoId);
// Fetch multiple documents
const todos = await dbTodos.fetch(
{ userId: user.id },
{
projection: { title: 1, isCompleted: 1, createdAt: 1 },
sort: { createdAt: -1 },
limit: 10
}
);
// Exclude heavy fields when they are not needed
const chunks = await dbDocumentChunks.fetch(
{ documentId },
{ projection: { embedding: 0 } }
);
// Count documents
const count = await dbTodos.countDocuments({ isCompleted: false });
Inserting Documents
// Insert one
const { insertedId } = await dbTodos.insertOne({
title: 'Buy groceries',
isCompleted: false,
userId: user.id,
createdAt: new Date()
});
// Insert many
const result = await dbTodos.insertMany([
{ title: 'Task 1', isCompleted: false, userId: user.id, createdAt: new Date() },
{ title: 'Task 2', isCompleted: false, userId: user.id, createdAt: new Date() }
]);
Updating Documents
// Update one
await dbTodos.updateOne(
{ id: todoId },
{ $set: { isCompleted: true } }
);
// Update one with convenience selector (by ID string)
await dbTodos.updateOne(
todoId,
{ $set: { isCompleted: true } }
);
// Upsert (update or insert)
await dbTodos.upsertOne(
{ userId: user.id, title: 'Unique task' },
{ $set: { isCompleted: false } }
);
// Update many
await dbTodos.updateMany(
{ userId: user.id },
{ $set: { isArchived: true } }
);
Deleting Documents
// Delete one
await dbTodos.deleteOne({ id: todoId });
// Delete many
await dbTodos.deleteMany({ isCompleted: true });
Advanced Operations
// Aggregation pipeline
const stats = await dbTodos.aggregate([
{ $match: { userId: user.id } },
{ $group: {
_id: '$isCompleted',
count: { $sum: 1 }
}}
]).toArray();
// Bulk write operations
await dbTodos.bulkWrite([
{ insertOne: { document: { title: 'New task', /* ... */ } } },
{ updateOne: { filter: { id: todoId }, update: { $set: { isCompleted: true } } } },
{ deleteOne: { filter: { id: oldTodoId } } }
]);
Including Stores in Modules
Register stores in your module to automatically provision them:
import { Module } from 'modelence/server';
import { dbTodos } from './db';
export default new Module('todo', {
stores: [dbTodos],
queries: {
async getAll() {
return await dbTodos.fetch({});
}
}
});
When your application starts, Modelence will:
- Provision the collection in MongoDB
- Create all configured indexes
- Create all configured search indexes
Stores handle all MongoDB connection management automatically. Just define your Store and include it in a module - Modelence takes care of the rest.
Extending Stores
Use the extend() method to add custom schema fields, indexes, methods, and search indexes to any store, including system collections:
import { schema, dbUsers } from 'modelence/server';
// Extend the users collection
export const extendedDbUsers = dbUsers.extend({
schema: {
firstName: schema.string(),
lastName: schema.string(),
companyId: schema.objectId().optional(),
},
indexes: [
{ key: { companyId: 1 } },
{ key: { lastName: 1, firstName: 1 } },
],
methods: {
getFullName() {
return `${this.firstName} ${this.lastName}`;
}
}
});
// Fully typed with new fields and methods!
const user = await extendedDbUsers.findOne({ firstName: 'John' });
console.log(user?.getFullName()); // ✅ Custom methods work
console.log(user?.companyId); // ✅ Type-safe fields
console.log(user?.handle); // ✅ Original fields preserved
The extend() method creates a new Store instance with merged schema, methods, indexes, and search indexes. The extended store shares the same MongoDB collection as the original.
Best Practices
- Define stores per domain - Keep related data in the same module
- Plan index strategy - See Indexes for configuration patterns and startup mode tradeoffs
- Leverage custom methods - Encapsulate business logic in document methods
- Type safety - Let TypeScript guide you with schema-based types
- Use Atlas Search intentionally - Use
searchIndexes for advanced full-text search use cases
- Extend system collections early - Extend system collections like
dbUsers before using them in your application
- Use sparse indexes - For optional fields with low cardinality, use
sparse: true to save space
API Reference
For a complete list of available methods and detailed API documentation, see the Store API Reference.