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
MongoDB Indexes
Configure indexes to improve query performance:
export const dbPosts = new Store('posts', {
schema: {
title: schema.string(),
authorId: schema.userId(),
publishedAt: schema.date(),
status: schema.string(),
},
indexes: [
// Single field index
{ key: { authorId: 1 } },
// Compound index
{ key: { status: 1, publishedAt: -1 } },
// Named index
{
key: { title: 'text' },
name: 'title_text_index'
},
// Unique index
{
key: { slug: 1 },
unique: true
}
]
});
Modelence automatically creates these indexes when the application starts.
MongoDB Atlas Search Indexes
For advanced full-text search capabilities using MongoDB Atlas Search, configure search indexes:
export const dbArticles = new Store('articles', {
schema: {
title: schema.string(),
content: schema.string(),
tags: schema.array(schema.string()),
category: schema.string(),
authorId: schema.userId(),
},
indexes: [
{ key: { authorId: 1 } },
],
searchIndexes: [
{
name: 'article_search',
definition: {
mappings: {
dynamic: false,
fields: {
title: {
type: 'string',
analyzer: 'lucene.standard'
},
content: {
type: 'string',
analyzer: 'lucene.standard'
},
tags: {
type: 'string',
analyzer: 'lucene.keyword'
},
category: {
type: 'string',
analyzer: 'lucene.keyword'
}
}
}
}
}
]
});
Using Search Indexes
Once configured, use the search indexes with MongoDB’s aggregation pipeline:
// Full-text search across title and content
const results = await dbArticles.aggregate([
{
$search: {
index: 'article_search',
text: {
query: 'machine learning',
path: ['title', 'content']
}
}
},
{
$limit: 10
}
]).toArray();
// Search with filters
const filteredResults = await dbArticles.aggregate([
{
$search: {
index: 'article_search',
compound: {
must: [
{
text: {
query: 'tutorial',
path: 'title'
}
}
],
filter: [
{
text: {
query: 'javascript',
path: 'category'
}
}
]
}
}
}
]).toArray();
Search indexes require MongoDB Atlas. They are automatically created when your application starts, similar to regular indexes.
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 },
{ sort: { createdAt: -1 }, limit: 10 }
);
// 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.
Best Practices
- Define stores per domain - Keep related data in the same module
- Use indexes wisely - Index fields used frequently in queries
- Leverage custom methods - Encapsulate business logic in document methods
- Type safety - Let TypeScript guide you with schema-based types
- Search indexes for Atlas - Use search indexes for advanced full-text search capabilities
API Reference
For a complete list of available methods and detailed API documentation, see the Store API Reference.