Skip to main content
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

  1. Define stores per domain - Keep related data in the same module
  2. Plan index strategy - See Indexes for configuration patterns and startup mode tradeoffs
  3. Leverage custom methods - Encapsulate business logic in document methods
  4. Type safety - Let TypeScript guide you with schema-based types
  5. Use Atlas Search intentionally - Use searchIndexes for advanced full-text search use cases
  6. Extend system collections early - Extend system collections like dbUsers before using them in your application
  7. 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.