--- url: /examples/basic-crud.md --- # Basic CRUD Example A complete example showing all CRUD operations with Monch. ## Setup ```typescript // lib/models/user.model.ts import { collection, field, type ModelOf, type SerializedOf } from '@codician-team/monch'; export const Users = collection({ name: 'users', schema: { _id: field.id(), name: field.string().min(1).max(100), email: field.email(), role: field.enum(['user', 'admin', 'moderator']).default('user'), profile: field.object({ bio: field.string().max(500).optional(), avatar: field.url().optional(), }).optional(), settings: field.object({ notifications: field.boolean().default(true), theme: field.enum(['light', 'dark', 'system']).default('system'), }), }, timestamps: true, indexes: [ { key: { email: 1 }, unique: true }, { key: { role: 1 } }, { key: { createdAt: -1 } }, ], }); export type User = ModelOf; export type SerializedUser = SerializedOf; ``` ## Create ### Insert One ```typescript const user = await Users.insertOne({ name: 'Alice Johnson', email: 'alice@example.com', profile: { bio: 'Software developer', }, }); console.log(user._id); // ObjectId console.log(user.role); // 'user' (default) console.log(user.createdAt); // Date ``` ### Insert Many ```typescript const users = await Users.insertMany([ { name: 'Bob Smith', email: 'bob@example.com' }, { name: 'Carol White', email: 'carol@example.com', role: 'admin' }, { name: 'Dave Brown', email: 'dave@example.com' }, ]); console.log(users.length); // 3 ``` ## Read ### Find One ```typescript const user = await Users.findOne({ email: 'alice@example.com' }); if (user) { console.log(user.name); // 'Alice Johnson' } ``` ### Find by ID ```typescript const user = await Users.findById('507f1f77bcf86cd799439011'); // or const user = await Users.findById(user._id); ``` ### Find Many ```typescript // All users const allUsers = await Users.find().toArray(); // With filter const admins = await Users.find({ role: 'admin' }).toArray(); // With sorting and limit const recentUsers = await Users.find() .sort({ createdAt: -1 }) .limit(10) .toArray(); // With projection const emails = await Users.find() .project({ email: 1, name: 1 }) .toArray(); ``` ### Pagination ```typescript const { data, pagination } = await Users.find({ role: 'user' }) .sort({ createdAt: -1 }) .paginate({ page: 1, limit: 20 }); console.log(`Page ${pagination.page} of ${pagination.totalPages}`); console.log(`Total users: ${pagination.total}`); ``` ### Count and Exists ```typescript const userCount = await Users.count({ role: 'user' }); const hasAdmins = await Users.exists({ role: 'admin' }); const totalEstimate = await Users.estimatedDocumentCount(); ``` ### Distinct Values ```typescript const roles = await Users.distinct('role'); // ['user', 'admin', 'moderator'] ``` ## Update ### Update One ```typescript const updated = await Users.updateOne( { email: 'alice@example.com' }, { $set: { role: 'admin' } } ); if (updated) { console.log(updated.role); // 'admin' } ``` ### Update Many ```typescript const count = await Users.updateMany( { role: 'user' }, { $set: { 'settings.notifications': true } } ); console.log(`Updated ${count} users`); ``` ### Upsert ```typescript const user = await Users.updateOne( { email: 'new@example.com' }, { $set: { role: 'user' }, $setOnInsert: { name: 'New User' }, }, { upsert: true } ); ``` ### Atomic Update ```typescript const user = await Users.findOneAndUpdate( { email: 'alice@example.com' }, { $inc: { loginCount: 1 } }, { returnDocument: 'after' } ); ``` ## Delete ### Delete One ```typescript const deleted = await Users.deleteOne({ email: 'alice@example.com' }); console.log(deleted); // true or false ``` ### Delete Many ```typescript const count = await Users.deleteMany({ role: 'inactive' }); console.log(`Deleted ${count} users`); ``` ### Atomic Delete ```typescript const deletedUser = await Users.findOneAndDelete({ email: 'alice@example.com' }); if (deletedUser) { console.log(`Deleted user: ${deletedUser.name}`); } ``` ## Express API (Automatic Serialization) Documents have `toJSON()` which is called automatically by `res.json()`: ```typescript import express from 'express'; import { Users } from './models'; const app = express(); app.use(express.json()); // List users - toJSON() called automatically app.get('/api/users', async (req, res) => { const users = await Users.find({}).toArray(); res.json(users); // Just works! }); // Get single user app.get('/api/users/:id', async (req, res) => { const user = await Users.findById(req.params.id); if (!user) return res.status(404).json({ error: 'Not found' }); res.json(user); // toJSON() handles BSON types }); // Create user app.post('/api/users', async (req, res) => { const user = await Users.insertOne(req.body); res.status(201).json(user); }); // Update user app.put('/api/users/:id', async (req, res) => { const user = await Users.updateOne( { _id: req.params.id }, { $set: req.body } ); if (!user) return res.status(404).json({ error: 'Not found' }); res.json(user); }); // Delete user app.delete('/api/users/:id', async (req, res) => { const deleted = await Users.deleteOne({ _id: req.params.id }); if (!deleted) return res.status(404).json({ error: 'Not found' }); res.json({ success: true }); }); ``` ## Next.js (Manual Serialization) Next.js blocks `toJSON()` at the Server/Client boundary. Use `.serialize()`: ```typescript // Server Component export default async function UsersPage() { const users = await Users.find({ role: 'user' }) .sort({ createdAt: -1 }) .limit(10) .serialize(); // Required for Next.js return ; } ``` ```typescript // Client Component 'use client'; import type { SerializedUser } from '@/lib/models/user.model'; interface Props { users: SerializedUser[]; } export function UserList({ users }: Props) { return (
    {users.map((user) => (
  • {user.name} ({user.email}) Joined: {new Date(user.createdAt).toLocaleDateString()}
  • ))}
); } ``` ## Error Handling ```typescript import { MonchValidationError } from '@codician-team/monch'; try { await Users.insertOne({ name: '', // Invalid: too short email: 'not-an-email', // Invalid: not email format }); } catch (error) { if (error instanceof MonchValidationError) { console.log(error.toFormErrors()); // { name: 'String must contain at least 1 character(s)', email: 'Invalid email' } } } ``` --- --- url: /api/collection.md --- # collection() Creates a Monch collection with Zod validation and MongoDB integration. ## Signature ```typescript function collection(config: CollectionConfig): Collection ``` ## Configuration ```typescript const Users = collection({ // Required name: string, // MongoDB collection name schema: Schema, // Zod schema object // Connection (choose one or use auto-connect) uri?: string, // MongoDB connection string client?: MongoClient, // Existing MongoClient db?: Db, // Existing Db instance database?: string, // Database name // Features timestamps?: boolean, // Add createdAt/updatedAt (default: false) indexes?: Index[], // Index definitions createIndexes?: boolean, // Auto-create indexes (default: true) // Extensibility hooks?: Hooks, // Lifecycle hooks methods?: Methods, // Instance methods statics?: Statics, // Static methods }); ``` ## Collection Methods ### Query Methods | Method | Returns | Description | |--------|---------|-------------| | `find(filter?)` | `Cursor` | Find documents, returns chainable cursor | | `findOne(filter)` | `Promise` | Find single document | | `findById(id)` | `Promise` | Find by ObjectId (accepts string) | | `count(filter?)` | `Promise` | Count matching documents | | `estimatedDocumentCount()` | `Promise` | Fast count using metadata | | `exists(filter)` | `Promise` | Check if any documents match | | `distinct(key, filter?)` | `Promise` | Get distinct values | ### Write Methods | Method | Returns | Description | |--------|---------|-------------| | `insertOne(doc, opts?)` | `Promise` | Insert one (validated) | | `insertMany(docs, opts?)` | `Promise` | Insert multiple (validated) | | `updateOne(filter, update, opts?)` | `Promise` | Update one, return document | | `updateMany(filter, update, opts?)` | `Promise` | Update multiple, return count | | `replaceOne(filter, doc, opts?)` | `Promise` | Replace document (validated) | | `deleteOne(filter, opts?)` | `Promise` | Delete one, return success | | `deleteMany(filter, opts?)` | `Promise` | Delete multiple, return count | ### Atomic Methods | Method | Returns | Description | |--------|---------|-------------| | `findOneAndUpdate(filter, update, opts?)` | `Promise` | Atomic find and update | | `findOneAndDelete(filter, opts?)` | `Promise` | Atomic find and delete | | `findOneAndReplace(filter, doc, opts?)` | `Promise` | Atomic find and replace | ### Bulk & Aggregation | Method | Returns | Description | |--------|---------|-------------| | `bulkWrite(operations, opts?)` | `Promise` | Bulk operations | | `aggregate(pipeline?, opts?)` | `AggregationCursor` | Run aggregation | ### Utility Methods | Method | Returns | Description | |--------|---------|-------------| | `transaction(fn)` | `Promise` | Execute in transaction | | `ensureIndexes()` | `Promise` | Create configured indexes | ## Collection Properties | Property | Type | Description | |----------|------|-------------| | `$schema` | `ZodObject` | Runtime Zod schema | | `$model` | `type only` | Document type | | `$serialized` | `type only` | Serialized type | | `collection` | `Promise` | Raw MongoDB collection | | `client` | `Promise` | Raw MongoDB client | ## Examples ### Basic Collection ```typescript const Users = collection({ name: 'users', schema: { _id: field.id(), name: field.string().min(1), email: field.email(), }, }); ``` ### With Timestamps and Indexes ```typescript const Posts = collection({ name: 'posts', schema: { _id: field.id(), title: field.string(), content: field.string(), authorId: field.objectId(), }, timestamps: true, indexes: [ { key: { authorId: 1, createdAt: -1 } }, { key: { title: 'text', content: 'text' } }, ], }); ``` ### With Hooks ```typescript const Posts = collection({ name: 'posts', schema: { /* ... */ }, hooks: { beforeValidate: (doc) => ({ ...doc, slug: doc.title.toLowerCase().replace(/\s+/g, '-'), }), afterInsert: async (doc) => { await notifySubscribers(doc); }, }, }); ``` ### With Methods and Statics ```typescript const Users = collection({ name: 'users', schema: { /* ... */ }, methods: { fullName: (doc) => `${doc.firstName} ${doc.lastName}`, isAdmin: (doc) => doc.role === 'admin', }, statics: { findByEmail: (col, email: string) => col.findOne({ email }), findAdmins: (col) => col.find({ role: 'admin' }).toArray(), }, }); ``` ### With Explicit Connection ```typescript import { MongoClient } from 'mongodb'; const client = new MongoClient('mongodb://localhost:27017'); const Users = collection({ name: 'users', schema: { /* ... */ }, client, database: 'myapp', }); ``` --- --- url: /api/connection.md --- # Connection API Functions for managing MongoDB connections. ## Import ```typescript import { resolveConnection, disconnectAll, setDebug } from '@codician-team/monch'; ``` ## resolveConnection() Resolve a MongoDB connection from configuration: ```typescript async function resolveConnection(config: ConnectionConfig): Promise ``` ### ConnectionConfig ```typescript interface ConnectionConfig { uri?: string; // MongoDB connection string client?: MongoClient; // Existing MongoClient db?: Db; // Existing Db instance database?: string; // Database name } ``` ### ConnectionResult ```typescript interface ConnectionResult { client: MongoClient; // MongoDB client db: Db; // Database instance } ``` ### Resolution Order 1. Existing `Db` instance (`config.db`) 2. Existing `MongoClient` (`config.client` + `config.database`) 3. Explicit URI (`config.uri`) 4. Environment variables (`MONGODB_URI`, `MONGO_URL`, `DATABASE_URL`) ### Example ```typescript // Using environment variables (recommended) const { client, db } = await resolveConnection({}); // Explicit URI const { client, db } = await resolveConnection({ uri: 'mongodb://localhost:27017/myapp', }); // Existing client const myClient = new MongoClient('mongodb://localhost:27017'); const { client, db } = await resolveConnection({ client: myClient, database: 'myapp', }); ``` ## disconnectAll() Close all cached MongoDB connections: ```typescript async function disconnectAll(): Promise ``` ### Example ```typescript // Graceful shutdown process.on('SIGTERM', async () => { console.log('Shutting down...'); await disconnectAll(); process.exit(0); }); ``` ## setDebug() Enable or configure debug logging: ```typescript function setDebug(config: boolean | DebugConfig): void ``` ### DebugConfig ```typescript interface DebugConfig { enabled: boolean; logger?: (operation: string, collection: string, ...args: any[]) => void; } ``` ### Examples ```typescript // Enable with default logger setDebug(true); // Disable setDebug(false); // Custom logger setDebug({ enabled: true, logger: (operation, collection, ...args) => { console.log(`[DB] ${operation} ${collection}`, args); }, }); // Environment-based setDebug(process.env.NODE_ENV === 'development'); ``` ## Environment Variables ### URI Variables (checked in order) | Variable | Description | |----------|-------------| | `MONGODB_URI` | Primary connection string | | `MONGO_URL` | Alternative (common in some platforms) | | `DATABASE_URL` | Used only if it starts with `mongodb://` or `mongodb+srv://` | ### Database Name Variables | Variable | Description | |----------|-------------| | `MONGODB_DATABASE` | Database name | | `MONGO_DATABASE` | Alternative | ### Priority **URI resolution:** 1. Explicit `uri` config option 2. `MONGODB_URI` 3. `MONGO_URL` 4. `DATABASE_URL` (if MongoDB URI) **Database name resolution:** 1. Explicit `database` config option 2. Database from URI path (`mongodb://host/mydb`) 3. `MONGODB_DATABASE` 4. `MONGO_DATABASE` ## Connection Caching Monch caches connections by URI to avoid creating multiple connections to the same database: ```typescript // First call creates connection const users = await Users.find().toArray(); // Subsequent calls reuse the cached connection const posts = await Posts.find().toArray(); // Same connection is used (if same URI) ``` ## Error Handling ```typescript import { MonchConnectionError } from '@codician-team/monch'; try { const { client, db } = await resolveConnection({}); } catch (error) { if (error instanceof MonchConnectionError) { console.error('Connection failed:', error.message); // Common messages: // - 'No MongoDB connection configured...' // - 'No database specified...' // - 'Failed to connect to MongoDB...' } } ``` --- --- url: /guide/connection.md --- # Connection Options Monch provides flexible connection options from zero-config auto-connect to full manual control. ## Auto-Connect (Recommended) Set `MONGODB_URI` environment variable. Monch connects on first operation: ```bash # .env MONGODB_URI=mongodb://localhost:27017/myapp ``` ```typescript // Just use it - no setup required const user = await Users.insertOne({ name: 'Alice' }); ``` ### Environment Variable Priority Monch checks these environment variables in order: 1. `MONGODB_URI` - Primary connection string 2. `MONGO_URL` - Common alternative 3. `DATABASE_URL` - Used only if it's a MongoDB URI (`mongodb://` or `mongodb+srv://`) ### Database Name Priority 1. Explicit `database` config option 2. Database in URI path (e.g., `mongodb://localhost:27017/mydb`) 3. `MONGODB_DATABASE` or `MONGO_DATABASE` environment variable ## Explicit URI Specify the URI in the collection config: ```typescript const Users = collection({ name: 'users', schema: { /* ... */ }, uri: 'mongodb://localhost:27017/myapp', }); ``` ## Shared Client Share a single MongoDB client across collections (recommended for production): ```typescript import { MongoClient } from 'mongodb'; const client = new MongoClient('mongodb://localhost:27017'); const Users = collection({ name: 'users', schema: { /* ... */ }, client, database: 'myapp', }); const Posts = collection({ name: 'posts', schema: { /* ... */ }, client, // Same client database: 'myapp', }); ``` ### Benefits of Shared Client * Connection pooling across collections * Required for transactions across collections * Efficient resource usage ## Existing Database Instance If you already have a MongoDB `Db` instance: ```typescript const db = client.db('myapp'); const Users = collection({ name: 'users', schema: { /* ... */ }, db, }); ``` ## Connection Resolution Monch resolves connections in this order: 1. Existing `Db` instance (`config.db`) 2. Existing `MongoClient` (`config.client` + `config.database`) 3. Explicit URI (`config.uri`) 4. Environment variables (`MONGODB_URI`, etc.) ## Graceful Shutdown Close all connections when your app shuts down: ```typescript import { disconnectAll } from '@codician-team/monch'; process.on('SIGTERM', async () => { console.log('Shutting down...'); await disconnectAll(); process.exit(0); }); ``` ## Raw MongoDB Access When you need the native MongoDB driver for advanced operations: ```typescript // Get the raw MongoDB collection const raw = await Users.collection; // Use any MongoDB driver method await raw.createIndex({ email: 1 }, { unique: true }); const stats = await raw.stats(); const distinct = await raw.distinct('role'); // Aggregation const pipeline = [ { $match: { role: 'admin' } }, { $group: { _id: '$department', count: { $sum: 1 } } }, ]; const results = await raw.aggregate(pipeline).toArray(); // Get the MongoClient const client = await Users.client; const admin = client.db().admin(); ``` ## Connection Pooling MongoDB driver handles connection pooling automatically. Configure pool size in the URI: ```bash # Connection with pool settings MONGODB_URI=mongodb://localhost:27017/myapp?maxPoolSize=50&minPoolSize=10 ``` Common URI options: | Option | Default | Description | |--------|---------|-------------| | `maxPoolSize` | 100 | Maximum connections in pool | | `minPoolSize` | 0 | Minimum connections to maintain | | `maxIdleTimeMS` | 0 | Max idle time before closing | | `waitQueueTimeoutMS` | 0 | Max wait time for connection | | `connectTimeoutMS` | 30000 | Initial connection timeout | | `socketTimeoutMS` | 0 | Socket timeout | ## Error Handling Connection errors throw `MonchConnectionError`: ```typescript import { MonchConnectionError } from '@codician-team/monch'; try { const user = await Users.findOne({ email: 'alice@example.com' }); } catch (error) { if (error instanceof MonchConnectionError) { console.error('Connection failed:', error.message); // Handle reconnection or fallback } } ``` Common connection errors: * No `MONGODB_URI` configured * Invalid connection string * Network unreachable * Authentication failed * Database not found ## Next.js Considerations ### Development Mode In development, Next.js hot reloading can create multiple connections. Monch handles this by caching connections, but you may see connection warnings. ### Serverless Functions Each serverless function invocation may need to establish a connection. Monch's auto-connect handles this automatically, but for optimal performance: 1. Use connection pooling via MongoDB Atlas 2. Consider MongoDB Atlas Serverless 3. Keep warm connections with scheduled pings ### Edge Runtime Edge functions have different runtime constraints. For Edge: ```typescript // Use explicit client with Edge-compatible driver import { MongoClient } from 'mongodb'; const client = new MongoClient(process.env.MONGODB_URI!, { // Edge-compatible options }); ``` ## Multi-Database Setup Connect to multiple databases: ```typescript const client = new MongoClient('mongodb://localhost:27017'); // Users in 'auth' database const Users = collection({ name: 'users', schema: { /* ... */ }, client, database: 'auth', }); // Products in 'shop' database const Products = collection({ name: 'products', schema: { /* ... */ }, client, database: 'shop', }); ``` ## Replica Set Configuration For replica sets (required for transactions): ```bash # Replica set URI MONGODB_URI=mongodb://host1:27017,host2:27017,host3:27017/myapp?replicaSet=myrs # MongoDB Atlas (automatically configured) MONGODB_URI=mongodb+srv://user:password@cluster.mongodb.net/myapp ``` --- --- url: /guide/crud.md --- # CRUD Operations Monch provides a clean API for all database operations with automatic validation and type safety. ## Create ### insertOne Insert a single document with full Zod validation: ```typescript const user = await Users.insertOne({ name: 'Alice', email: 'alice@example.com', }); // Returns the inserted document with _id and timestamps ``` ### insertMany Insert multiple documents: ```typescript const users = await Users.insertMany([ { name: 'Alice', email: 'alice@example.com' }, { name: 'Bob', email: 'bob@example.com' }, ]); // Returns array of inserted documents ``` Each document is validated individually. If any document fails validation, the entire operation is rejected. ## Read ### find Find multiple documents: ```typescript // Basic query const users = await Users.find({ role: 'admin' }).toArray(); // With options const recentUsers = await Users.find({ createdAt: { $gte: lastWeek } }) .sort({ createdAt: -1 }) .limit(10) .toArray(); // Empty filter returns all documents const allUsers = await Users.find().toArray(); ``` ### findOne Find a single document: ```typescript const user = await Users.findOne({ email: 'alice@example.com' }); // Returns document or null ``` ### findById Find by ObjectId (accepts string or ObjectId): ```typescript const user = await Users.findById('507f1f77bcf86cd799439011'); // or const user = await Users.findById(new ObjectId('507f1f77bcf86cd799439011')); ``` ### count Count matching documents: ```typescript const count = await Users.count({ role: 'admin' }); ``` ### estimatedDocumentCount Fast count using collection metadata (doesn't filter): ```typescript const total = await Users.estimatedDocumentCount(); ``` ### exists Check if any documents match: ```typescript const hasAdmins = await Users.exists({ role: 'admin' }); // Returns boolean ``` ### distinct Get distinct values for a field: ```typescript const roles = await Users.distinct('role'); // ['user', 'admin', 'moderator'] // With filter const activeRoles = await Users.distinct('role', { status: 'active' }); ``` ## Update ### updateOne Update a single document: ```typescript const user = await Users.updateOne( { email: 'alice@example.com' }, { $set: { role: 'admin' } } ); // Returns updated document or null ``` **Partial Validation**: Only fields in `$set` and `$setOnInsert` are validated against the schema. ### updateMany Update multiple documents: ```typescript const count = await Users.updateMany( { role: 'user' }, { $set: { verified: true } } ); // Returns number of modified documents ``` ### Upsert Insert if not found, update if exists: ```typescript const user = await Users.updateOne( { email: 'alice@example.com' }, { $set: { lastLogin: new Date() }, $setOnInsert: { name: 'Alice', role: 'user' }, }, { upsert: true } ); ``` ::: info Upsert Validation For `updateOne` / `findOneAndUpdate` with `upsert: true`, Monch uses **partial validation** - only fields in `$set` and `$setOnInsert` are validated, not the entire document. This allows incremental document creation. For `replaceOne` / `findOneAndReplace` with `upsert: true`, Monch uses **full validation** - the entire replacement document must satisfy the schema. See [Validation](/guide/validation#upsert-validation) for details. ::: ### Replace Replace an entire document: ```typescript const result = await Users.replaceOne( { _id: user._id }, { name: 'Alice Smith', email: 'alice@example.com', role: 'admin' } ); // Returns UpdateResult ``` The replacement document is fully validated against the schema. ## Delete ### deleteOne Delete a single document: ```typescript const deleted = await Users.deleteOne({ email: 'alice@example.com' }); // Returns boolean (true if deleted) ``` ### deleteMany Delete multiple documents: ```typescript const count = await Users.deleteMany({ status: 'inactive' }); // Returns number of deleted documents ``` ## Atomic Operations These operations are atomic—they find and modify in a single operation. ### findOneAndUpdate ```typescript const user = await Users.findOneAndUpdate( { email: 'alice@example.com' }, { $inc: { loginCount: 1 } }, { returnDocument: 'after' } // Return updated document ); ``` ### findOneAndDelete ```typescript const deletedUser = await Users.findOneAndDelete( { email: 'alice@example.com' } ); // Returns the deleted document ``` ### findOneAndReplace ```typescript const user = await Users.findOneAndReplace( { _id: userId }, { name: 'Alice', email: 'alice@example.com', role: 'admin' }, { returnDocument: 'after' } ); ``` ::: info No Hooks Atomic operations (`findOneAndUpdate`, `findOneAndDelete`, `findOneAndReplace`) do **not** trigger lifecycle hooks. ::: ## Bulk Operations Execute multiple operations in a single request: ```typescript const result = await Users.bulkWrite([ { insertOne: { document: { name: 'Bob', email: 'bob@example.com' } } }, { updateOne: { filter: { _id: id1 }, update: { $set: { name: 'Robert' } } } }, { deleteOne: { filter: { _id: id2 } } }, { replaceOne: { filter: { _id: id3 }, replacement: { name: 'Carol', email: 'carol@example.com' } } }, ]); ``` Each operation is validated according to its type: * `insertOne`: Full validation * `updateOne`/`updateMany`: Partial validation * `replaceOne`: Full validation ::: info No Hooks `bulkWrite` uses Zod-only validation and does **not** trigger any lifecycle hooks. ::: ## Aggregation Run aggregation pipelines: ```typescript const results = await Users.aggregate([ { $match: { status: 'active' } }, { $group: { _id: '$role', count: { $sum: 1 } } }, { $sort: { count: -1 } }, ]); // Returns AggregationCursor - use .toArray() to get results const data = await results.toArray(); ``` ## Session Support All operations accept an options object with `session` for transactions: ```typescript await Users.insertOne( { name: 'Alice', email: 'alice@example.com' }, { session } ); await Users.updateOne( { email: 'alice@example.com' }, { $set: { role: 'admin' } }, { session } ); ``` See [Transactions](/guide/transactions) for more details. --- --- url: /api/cursor.md --- # Cursor Chainable query cursor for building and executing queries. ## Import ```typescript import { Cursor } from '@codician-team/monch'; ``` ## Creating a Cursor Cursors are created by calling `find()` on a collection: ```typescript const cursor = Users.find({ status: 'active' }); ``` ## Chainable Methods All chainable methods return the cursor for method chaining: ### sort(spec) Sort results by one or more fields: ```typescript cursor.sort({ createdAt: -1 }) // Descending cursor.sort({ name: 1 }) // Ascending cursor.sort({ role: 1, createdAt: -1 }) // Multiple fields ``` ### skip(n) Skip a number of documents: ```typescript cursor.skip(20) // Skip first 20 documents ``` ### limit(n) Limit the number of results: ```typescript cursor.limit(10) // Return at most 10 documents ``` ### project(spec) Select or exclude fields: ```typescript cursor.project({ name: 1, email: 1 }) // Include only these cursor.project({ password: 0 }) // Exclude this field cursor.project({ name: 1, _id: 0 }) // Include name, exclude _id ``` ## Execution Methods ### toArray() Execute and return results as an array: ```typescript const users = await cursor.toArray(); // Returns MonchArray with .serialize() method ``` The returned `MonchArray` is a regular array with an additional `.serialize()` method: ```typescript const users = await Users.find().toArray(); users.length; // Works (it's an array) users.map(...); // Works users.serialize(); // Serialize all documents ``` ### serialize() Execute and serialize in one step: ```typescript const users = await cursor.serialize(); // Returns Serialized[] - plain objects ``` Equivalent to `.toArray()` followed by `.serialize()`, but more concise. ### paginate(opts?) Execute with pagination metadata: ```typescript const result = await cursor.paginate({ page: 2, limit: 20 }); ``` **Options:** | Option | Type | Default | Description | |--------|------|---------|-------------| | `page` | `number` | `1` | Page number (1-based) | | `limit` | `number` | `20` | Items per page (max: 100) | **Returns:** ```typescript { data: Doc[], // Documents for this page pagination: { page: number, // Current page limit: number, // Items per page total: number, // Total matching documents totalPages: number, // Total pages hasNext: boolean, // Has more pages hasPrev: boolean, // Has previous pages } } ``` ::: info Limit Cap The `limit` is silently capped at 100 to prevent loading too much data. ::: ## Usage Examples ### Basic Query ```typescript const users = await Users.find({ role: 'admin' }) .sort({ name: 1 }) .toArray(); ``` ### With Pagination ```typescript const { data, pagination } = await Users.find({ status: 'active' }) .sort({ createdAt: -1 }) .paginate({ page: 1, limit: 20 }); console.log(`Page ${pagination.page} of ${pagination.totalPages}`); console.log(`Showing ${data.length} of ${pagination.total} users`); ``` ### For Next.js ```typescript // Server Component const users = await Users.find({ role: 'admin' }) .sort({ createdAt: -1 }) .limit(10) .serialize(); return ; ``` ### Complex Query ```typescript const posts = await Posts.find({ status: 'published', category: { $in: ['tech', 'science'] }, }) .sort({ publishedAt: -1, views: -1 }) .skip(20) .limit(10) .project({ title: 1, slug: 1, excerpt: 1, publishedAt: 1 }) .toArray(); ``` ## Type Safety The cursor maintains type safety throughout: ```typescript const Users = collection({ name: 'users', schema: { _id: field.id(), name: field.string(), email: field.email(), }, }); const users = await Users.find().toArray(); // users: MonchArray<{ _id: ObjectId; name: string; email: string }> const serialized = await Users.find().serialize(); // serialized: { _id: string; name: string; email: string }[] ``` ## PaginationResult Type ```typescript interface PaginationResult { data: T[]; pagination: { page: number; limit: number; total: number; totalPages: number; hasNext: boolean; hasPrev: boolean; }; } ``` ## PaginationOptions Type ```typescript interface PaginationOptions { page?: number; // Default: 1 limit?: number; // Default: 20, max: 100 } ``` --- --- url: /guide/debug.md --- # Debug Mode Enable query logging for development and debugging. ## Basic Usage ```typescript import { setDebug } from '@codician-team/monch'; // Enable debug mode setDebug(true); // Now all queries are logged await Users.findOne({ email: 'alice@example.com' }); // [Monch] findOne users { email: 'alice@example.com' } await Users.updateOne( { _id: userId }, { $set: { role: 'admin' } } ); // [Monch] updateOne users { _id: ObjectId(...) } { $set: { role: 'admin' } } ``` ## Disable Debug Mode ```typescript setDebug(false); ``` ## Custom Logger Provide a custom logging function: ```typescript setDebug({ enabled: true, logger: (operation, collection, ...args) => { console.log(`[DB] ${operation} on ${collection}`, JSON.stringify(args)); }, }); ``` ## Logger Signature ```typescript type DebugLogger = ( operation: string, // 'find', 'findOne', 'insertOne', etc. collection: string, // Collection name ...args: any[] // Operation arguments (filter, update, options, etc.) ) => void; ``` ## What Gets Logged All collection operations are logged: | Operation | Log Format | |-----------|------------| | `find` | `[Monch] find {collection} {filter}` | | `findOne` | `[Monch] findOne {collection} {filter}` | | `findById` | `[Monch] findById {collection} {id}` | | `insertOne` | `[Monch] insertOne {collection} {document}` | | `insertMany` | `[Monch] insertMany {collection} {count} documents` | | `updateOne` | `[Monch] updateOne {collection} {filter} {update}` | | `updateMany` | `[Monch] updateMany {collection} {filter} {update}` | | `deleteOne` | `[Monch] deleteOne {collection} {filter}` | | `deleteMany` | `[Monch] deleteMany {collection} {filter}` | | `count` | `[Monch] count {collection} {filter}` | | `aggregate` | `[Monch] aggregate {collection} {pipeline}` | ## Environment-Based Debug Enable debug mode based on environment: ```typescript import { setDebug } from '@codician-team/monch'; // Enable in development only setDebug(process.env.NODE_ENV === 'development'); // Or use a dedicated env var setDebug(process.env.MONCH_DEBUG === 'true'); ``` ## Integration with Logging Libraries ### Winston ```typescript import winston from 'winston'; import { setDebug } from '@codician-team/monch'; const logger = winston.createLogger({ level: 'debug', format: winston.format.json(), transports: [new winston.transports.Console()], }); setDebug({ enabled: true, logger: (operation, collection, ...args) => { logger.debug('Database operation', { operation, collection, args, }); }, }); ``` ### Pino ```typescript import pino from 'pino'; import { setDebug } from '@codician-team/monch'; const logger = pino({ level: 'debug' }); setDebug({ enabled: true, logger: (operation, collection, ...args) => { logger.debug({ operation, collection, args }, 'db query'); }, }); ``` ## Debugging Performance Combine debug mode with timing: ```typescript setDebug({ enabled: true, logger: (operation, collection, ...args) => { const start = performance.now(); console.log(`[Monch] Starting ${operation} on ${collection}`); // Note: This logs the start, not the actual duration // For actual timing, you'd need to wrap operations }, }); ``` For actual query timing, wrap operations: ```typescript async function timedQuery( name: string, fn: () => Promise ): Promise { const start = performance.now(); try { return await fn(); } finally { const duration = performance.now() - start; console.log(`[Query] ${name} took ${duration.toFixed(2)}ms`); } } // Usage const user = await timedQuery('findUser', () => Users.findOne({ email: 'alice@example.com' }) ); ``` ## Security Considerations ::: warning Sensitive Data Debug logs may contain sensitive information (passwords, tokens, personal data). Never enable debug mode in production or ensure your logger filters sensitive fields. ::: ```typescript setDebug({ enabled: process.env.NODE_ENV === 'development', logger: (operation, collection, ...args) => { // Filter sensitive fields const sanitized = args.map(arg => { if (typeof arg === 'object' && arg !== null) { const { password, token, secret, ...safe } = arg; return safe; } return arg; }); console.log(`[Monch] ${operation} ${collection}`, sanitized); }, }); ``` ## Conditional Debugging Debug specific collections or operations: ```typescript setDebug({ enabled: true, logger: (operation, collection, ...args) => { // Only log specific collections if (['users', 'orders'].includes(collection)) { console.log(`[Monch] ${operation} ${collection}`, ...args); } // Only log slow operations (after measuring) // Or only log mutations if (['insertOne', 'updateOne', 'deleteOne'].includes(operation)) { console.log(`[Monch] ${operation} ${collection}`, ...args); } }, }); ``` --- --- url: /examples/ecommerce.md --- # E-commerce Example Building a product catalog with Monch, featuring money fields, categories, and inventory. ## Models ### Product Model ```typescript // lib/models/product.model.ts import { collection, field, Money, type ModelOf, type SerializedOf } from '@codician-team/monch'; export const Products = collection({ name: 'products', schema: { _id: field.id(), name: field.string().min(1).max(200), slug: field.string(), description: field.string().max(5000).optional(), price: field.money({ currency: 'USD' }), compareAtPrice: field.money({ currency: 'USD' }).optional(), categoryId: field.objectId(), images: field.array(field.url()).default([]), inventory: field.object({ quantity: field.number().int().min(0).default(0), sku: field.string().optional(), trackInventory: field.boolean().default(true), }), status: field.enum(['draft', 'active', 'archived']).default('draft'), tags: field.array(field.string()).default([]), }, timestamps: true, indexes: [ { key: { slug: 1 }, unique: true }, { key: { categoryId: 1, status: 1 } }, { key: { status: 1, createdAt: -1 } }, { key: { name: 'text', description: 'text', tags: 'text' } }, ], hooks: { beforeValidate: (doc) => ({ ...doc, slug: doc.slug || doc.name.toLowerCase() .replace(/[^a-z0-9]+/g, '-') .replace(/^-|-$/g, ''), }), }, methods: { isOnSale: (doc) => doc.compareAtPrice && Money.greaterThan(doc.compareAtPrice, doc.price), discountPercentage: (doc) => { if (!doc.compareAtPrice) return 0; const price = Money.toNumber(doc.price); const compareAt = Money.toNumber(doc.compareAtPrice); return Math.round((1 - price / compareAt) * 100); }, isInStock: (doc) => !doc.inventory.trackInventory || doc.inventory.quantity > 0, }, statics: { findByCategory: (col, categoryId: string) => { return col.find({ categoryId, status: 'active' }) .sort({ createdAt: -1 }); }, search: (col, query: string, options?: { limit?: number }) => { return col.find({ status: 'active', $text: { $search: query }, }) .limit(options?.limit ?? 20) .toArray(); }, getLowStockProducts: async (col, threshold = 10) => { return col.find({ status: 'active', 'inventory.trackInventory': true, 'inventory.quantity': { $lte: threshold }, }).toArray(); }, }, }); export type Product = ModelOf; export type SerializedProduct = SerializedOf; ``` ### Category Model ```typescript // lib/models/category.model.ts import { collection, field, type ModelOf, type SerializedOf } from '@codician-team/monch'; export const Categories = collection({ name: 'categories', schema: { _id: field.id(), name: field.string().min(1).max(100), slug: field.string(), description: field.string().max(500).optional(), image: field.url().optional(), parentId: field.objectId().optional(), sortOrder: field.number().int().default(0), }, timestamps: true, indexes: [ { key: { slug: 1 }, unique: true }, { key: { parentId: 1, sortOrder: 1 } }, ], hooks: { beforeValidate: (doc) => ({ ...doc, slug: doc.slug || doc.name.toLowerCase().replace(/\s+/g, '-'), }), }, statics: { getRootCategories: (col) => { return col.find({ parentId: { $exists: false } }) .sort({ sortOrder: 1 }) .toArray(); }, getChildren: (col, parentId: string) => { return col.find({ parentId }) .sort({ sortOrder: 1 }) .toArray(); }, }, }); export type Category = ModelOf; export type SerializedCategory = SerializedOf; ``` ### Order Model ```typescript // lib/models/order.model.ts import { collection, field, type ModelOf, type SerializedOf } from '@codician-team/monch'; export const Orders = collection({ name: 'orders', schema: { _id: field.id(), orderNumber: field.string(), customer: field.object({ email: field.email(), name: field.string(), phone: field.string().optional(), }), items: field.array(field.object({ productId: field.objectId(), name: field.string(), quantity: field.number().int().positive(), price: field.money({ currency: 'USD' }), })), subtotal: field.money({ currency: 'USD' }), tax: field.money({ currency: 'USD' }), shipping: field.money({ currency: 'USD' }), total: field.money({ currency: 'USD' }), status: field.enum([ 'pending', 'confirmed', 'processing', 'shipped', 'delivered', 'cancelled', 'refunded' ]).default('pending'), shippingAddress: field.object({ line1: field.string(), line2: field.string().optional(), city: field.string(), state: field.string(), postalCode: field.string(), country: field.string().default('US'), }), notes: field.string().optional(), }, timestamps: true, indexes: [ { key: { orderNumber: 1 }, unique: true }, { key: { 'customer.email': 1, createdAt: -1 } }, { key: { status: 1, createdAt: -1 } }, ], hooks: { beforeValidate: async (doc) => ({ ...doc, orderNumber: doc.orderNumber || await generateOrderNumber(), }), }, statics: { findByCustomer: (col, email: string) => { return col.find({ 'customer.email': email }) .sort({ createdAt: -1 }); }, getRecentOrders: (col, days = 7) => { const since = new Date(); since.setDate(since.getDate() - days); return col.find({ createdAt: { $gte: since } }) .sort({ createdAt: -1 }); }, }, }); async function generateOrderNumber(): Promise { const date = new Date().toISOString().slice(0, 10).replace(/-/g, ''); const random = Math.random().toString(36).substring(2, 8).toUpperCase(); return `ORD-${date}-${random}`; } export type Order = ModelOf; export type SerializedOrder = SerializedOf; ``` ## Product Listing Page ```typescript // app/shop/page.tsx import { Products, Categories } from '@/lib/models'; import { ProductGrid } from '@/components/ProductGrid'; import { CategoryNav } from '@/components/CategoryNav'; interface Props { searchParams: { category?: string; page?: string; q?: string }; } export default async function ShopPage({ searchParams }: Props) { const page = Number(searchParams.page) || 1; const categorySlug = searchParams.category; const query = searchParams.q; // Build filter let filter: any = { status: 'active' }; if (categorySlug) { const category = await Categories.findOne({ slug: categorySlug }); if (category) { filter.categoryId = category._id; } } if (query) { filter.$text = { $search: query }; } // Fetch products with pagination const result = await Products.find(filter) .sort({ createdAt: -1 }) .paginate({ page, limit: 12 }); const products = result.data.map(p => p.serialize()); // Fetch categories for navigation const categories = await Categories.find() .sort({ sortOrder: 1 }) .serialize(); return (
); } ``` ## Product Detail Page ```typescript // app/shop/[slug]/page.tsx import { Products, Categories, Money } from '@/lib/models'; import { notFound } from 'next/navigation'; import { AddToCartButton } from '@/components/AddToCartButton'; interface Props { params: { slug: string }; } export default async function ProductPage({ params }: Props) { const product = await Products.findOne({ slug: params.slug, status: 'active', }); if (!product) { notFound(); } const category = await Categories.findById(product.categoryId); const serialized = product.serialize(); // Format prices for display const priceDisplay = Money.format(product.price); const compareAtDisplay = product.compareAtPrice ? Money.format(product.compareAtPrice) : null; return (
{serialized.images.map((image, i) => ( {serialized.name} ))}

{serialized.name}

{product.isOnSale() ? ( <> {priceDisplay} {compareAtDisplay} {product.discountPercentage()}% OFF ) : ( {priceDisplay} )}

{serialized.description}

{product.isInStock() ? ( ) : ( )}
); } ``` ## Checkout with Transactions ```typescript // app/actions/checkout.actions.ts 'use server'; import { Products, Orders } from '@/lib/models'; import { Money, MonchValidationError } from '@codician-team/monch'; interface CartItem { productId: string; quantity: number; } interface CheckoutData { customer: { email: string; name: string; phone?: string; }; shippingAddress: { line1: string; line2?: string; city: string; state: string; postalCode: string; }; items: CartItem[]; } export async function checkout(data: CheckoutData) { try { return await Products.transaction(async (session) => { // Fetch and validate products const products = await Promise.all( data.items.map(async (item) => { const product = await Products.findById(item.productId); if (!product) { throw new Error(`Product ${item.productId} not found`); } if (!product.isInStock()) { throw new Error(`${product.name} is out of stock`); } if (product.inventory.quantity < item.quantity) { throw new Error(`Not enough ${product.name} in stock`); } return { product, quantity: item.quantity }; }) ); // Calculate line totals using Money utility const orderItems = products.map(({ product, quantity }) => ({ productId: product._id, name: product.name, quantity, price: product.price, lineTotal: Money.multiply(product.price, quantity), })); // Calculate totals const subtotal = Money.sum(orderItems.map(item => item.lineTotal)); const tax = Money.percentage(subtotal, 8); // 8% tax const shipping = Money.greaterThanOrEqual(subtotal, 100) ? Money.from(0, 'USD') : Money.from(9.99, 'USD'); // Free shipping over $100 const total = Money.round(Money.sum([subtotal, tax, shipping])); // Update inventory for (const { product, quantity } of products) { await Products.updateOne( { _id: product._id }, { $inc: { 'inventory.quantity': -quantity } }, { session } ); } // Create order const order = await Orders.insertOne({ customer: data.customer, shippingAddress: data.shippingAddress, items: orderItems.map(({ productId, name, quantity, price }) => ({ productId, name, quantity, price, })), subtotal, tax, shipping, total, }, { session }); return { success: true, order: order.serialize() }; }); } catch (error) { if (error instanceof MonchValidationError) { return { success: false, errors: error.toFormErrors() }; } return { success: false, error: (error as Error).message }; } } ``` ## Admin Dashboard Stats ```typescript // app/admin/dashboard/page.tsx import { Products, Orders } from '@/lib/models'; export default async function AdminDashboard() { const [ totalProducts, activeProducts, lowStockProducts, recentOrders, orderStats, ] = await Promise.all([ Products.count(), Products.count({ status: 'active' }), Products.getLowStockProducts(5), Orders.getRecentOrders(7).limit(10).serialize(), Orders.aggregate([ { $match: { createdAt: { $gte: new Date(Date.now() - 30 * 24 * 60 * 60 * 1000) } } }, { $group: { _id: '$status', count: { $sum: 1 }, revenue: { $sum: '$total.amount' }, }, }, ]).toArray(), ]); return (
a + s.count, 0)} />
p.serialize())} />
); } ``` --- --- url: /api/errors.md --- # Errors Monch provides typed error classes for handling failures. ## Import ```typescript import { MonchError, MonchConnectionError, MonchValidationError, } from '@codician-team/monch'; ``` ## Error Classes ### MonchError Base class for all Monch errors: ```typescript class MonchError extends Error { name: 'MonchError'; message: string; } ``` ### MonchConnectionError Thrown when connection fails or is misconfigured: ```typescript class MonchConnectionError extends MonchError { name: 'MonchConnectionError'; } ``` **Common causes:** * No `MONGODB_URI` environment variable * Invalid connection string * Network unreachable * Authentication failed * Database not found ### MonchValidationError Thrown when Zod validation fails: ```typescript class MonchValidationError extends MonchError { name: 'MonchValidationError'; issues: ZodIssue[]; formatIssues(): string; toFormErrors(): Record; get firstError(): string; get firstField(): string; } ``` ## MonchValidationError Methods ### issues Raw Zod validation issues array: ```typescript error.issues // [ // { path: ['name'], message: 'Required', code: 'invalid_type' }, // { path: ['email'], message: 'Invalid email', code: 'invalid_string' } // ] ``` ### formatIssues() Human-readable multi-line string: ```typescript error.formatIssues() // '- name: Required\n- email: Invalid email' ``` ### toFormErrors() Field-to-error mapping for form UIs: ```typescript error.toFormErrors() // { name: 'Required', email: 'Invalid email' } ``` Only includes the first error for each field. ### firstError The first error message: ```typescript error.firstError // 'Required' ``` ### firstField Path to the first error field: ```typescript error.firstField // 'name' // For nested fields: // 'address.city' ``` ## The `_root` Key When validation errors have an empty path (top-level errors), `toFormErrors()` and `firstField` use `_root` as the key: ```typescript // Schema with refinement const schema = z.object({ password: z.string(), confirmPassword: z.string(), }).refine( (data) => data.password === data.confirmPassword, { message: 'Passwords must match' } ); // If refinement fails: error.toFormErrors() // { _root: 'Passwords must match' } error.firstField // '_root' ``` ## Usage Examples ### Basic Error Handling ```typescript import { MonchValidationError, MonchConnectionError } from '@codician-team/monch'; try { await Users.insertOne({ name: '', email: 'invalid' }); } catch (error) { if (error instanceof MonchValidationError) { console.log('Validation failed:', error.formatIssues()); } else if (error instanceof MonchConnectionError) { console.log('Connection failed:', error.message); } else { throw error; } } ``` ### Form Error Display ```typescript // Server Action export async function createUser(formData: FormData) { try { const user = await Users.insertOne({ name: formData.get('name') as string, email: formData.get('email') as string, }); return { success: true, user: user.serialize() }; } catch (error) { if (error instanceof MonchValidationError) { return { success: false, errors: error.toFormErrors() }; } throw error; } } // Client Component function UserForm() { const [errors, setErrors] = useState>({}); async function handleSubmit(formData: FormData) { const result = await createUser(formData); if (!result.success) { setErrors(result.errors); } } return (
{errors.name && {errors.name}} {errors.email && {errors.email}} {errors._root && {errors._root}}
); } ``` ### Quick Validation Check ```typescript try { await Users.insertOne(data); } catch (error) { if (error instanceof MonchValidationError) { // Quick access to first error console.log(`Error in ${error.firstField}: ${error.firstError}`); } } ``` ### Type Guard ```typescript function isValidationError(error: unknown): error is MonchValidationError { return error instanceof MonchValidationError; } try { await Users.insertOne(data); } catch (error) { if (isValidationError(error)) { // TypeScript knows error.issues exists console.log(error.issues); } } ``` ## When Errors Are Thrown | Error | Operations | Cause | |-------|------------|-------| | `MonchConnectionError` | Any | Connection failure, missing config | | `MonchValidationError` | `insertOne`, `insertMany` | Full document validation fails | | `MonchValidationError` | `updateOne`, `updateMany`, `findOneAndUpdate` | `$set`/`$setOnInsert` validation fails (partial) | | `MonchValidationError` | `replaceOne`, `findOneAndReplace` | Replacement validation fails (full) | | `MonchValidationError` | `bulkWrite` | Any operation validation fails | --- --- url: /api/exports.md --- # Exports Reference Complete list of all exports from `@codician-team/monch`. ## Functions | Export | Description | |--------|-------------| | `collection(config)` | Create a Monch collection | | `field` | Field helper namespace with type builders | | `resolveConnection(config)` | Resolve MongoDB connection | | `disconnectAll()` | Close all cached connections | | `setDebug(config)` | Enable/configure debug logging | | `serialize(doc)` | Serialize a single document | | `serializeArray(docs)` | Serialize an array of documents | | `getCurrencyConfig(code)` | Get configuration for a currency | ## Classes | Export | Description | |--------|-------------| | `Money` | Money utility class with formatting, arithmetic, and comparison methods | | `MonchError` | Base class for all Monch errors | | `MonchConnectionError` | Connection configuration or network errors | | `MonchValidationError` | Zod validation errors | | `Cursor` | Chainable query cursor | | `MonchArray` | Array subclass with `.serialize()` method | ## BSON Types Re-exported from MongoDB driver: | Export | Description | |--------|-------------| | `ObjectId` | MongoDB ObjectId | | `Long` | 64-bit integer | | `Decimal128` | 128-bit decimal | | `Binary` | Binary data | | `Timestamp` | MongoDB timestamp | | `MongoClient` | MongoDB client class | | `Db` | MongoDB database class | ## Constants | Export | Description | |--------|-------------| | `z` | Re-export of Zod | | `CURRENCY_CONFIGS` | Map of 30+ currency configurations | ## Types ### Schema & Document Types ```typescript import type { Schema, // Schema definition type Doc, // Document type from schema Input, // Input type (respects defaults/optionals) TimestampFields, // { createdAt: Date; updatedAt: Date } WithTimestamps, // Add timestamp fields to type WithTS, // Alias for WithTimestamps MonchDocument, // Document with methods and .serialize() MonchFilter, // Relaxed filter type for Zod } from '@codician-team/monch'; ``` ### Collection Types ```typescript import type { Collection, // Full collection type CollectionInstance, // Collection instance type CollectionConfig, // Collection configuration ConnectionConfig, // Connection options OperationOptions, // Options for CRUD operations Hooks, // Lifecycle hooks definition Index, // Index definition IndexOptions, // Index options IndexDirection, // 1 | -1 | 'text' | etc. Methods, // Instance methods definition Statics, // Static methods definition WithMethods, // Document with methods attached } from '@codician-team/monch'; ``` ### Serialization Types ```typescript import type { Serialized, // Serialized version of document WithSerialize, // Object with .serialize() method } from '@codician-team/monch'; ``` ### Pagination Types ```typescript import type { PaginationResult, // Paginated query result PaginationOptions, // Pagination options } from '@codician-team/monch'; ``` ### Money Types ```typescript import type { MoneyValue, // Money field value structure MoneyOptions, // Money field configuration } from '@codician-team/monch'; ``` ### Connection Types ```typescript import type { ConnectionResult, // Result from resolveConnection() DebugConfig, // Debug mode configuration } from '@codician-team/monch'; ``` ### Type Extraction Helpers ```typescript import type { ModelOf, // Extract document type from collection SerializedOf, // Extract serialized type from collection SchemaOf, // Extract schema type from collection } from '@codician-team/monch'; ``` ### Re-exported MongoDB Types ```typescript import type { Filter, UpdateFilter, AggregateOptions, BulkWriteOptions, BulkWriteResult, AnyBulkWriteOperation, FindOneAndDeleteOptions, FindOneAndReplaceOptions, FindOneAndUpdateOptions, ReplaceOptions, UpdateResult, ClientSession, } from '@codician-team/monch'; ``` ## Usage Example ```typescript // Import everything you need import { // Functions collection, field, setDebug, disconnectAll, // Classes MonchValidationError, ObjectId, // Constants z, // Types type ModelOf, type SerializedOf, type PaginationResult, } from '@codician-team/monch'; // Define collection const Users = collection({ name: 'users', schema: { _id: field.id(), name: field.string(), email: field.email(), }, timestamps: true, }); // Extract types type User = ModelOf; type SerializedUser = SerializedOf; // Use in components interface Props { user: SerializedUser; } ``` --- --- url: /api/field.md --- # Field Types The `field` helper provides type-safe field definitions built on Zod. ## Import ```typescript import { field } from '@codician-team/monch'; ``` ## Basic Types ### field.string() ```typescript field.string() // Required string field.string().min(1) // Minimum length field.string().max(100) // Maximum length field.string().length(10) // Exact length field.string().trim() // Trim whitespace field.string().toLowerCase() // Convert to lowercase field.string().toUpperCase() // Convert to uppercase field.string().regex(/pattern/) // Regex validation field.string().optional() // Optional field.string().nullable() // Can be null field.string().default('value') // Default value ``` ### field.number() ```typescript field.number() // Required number field.number().int() // Integer only field.number().positive() // > 0 field.number().nonnegative() // >= 0 field.number().negative() // < 0 field.number().nonpositive() // <= 0 field.number().min(0) // Minimum value field.number().max(100) // Maximum value field.number().multipleOf(5) // Multiple of field.number().finite() // Not Infinity ``` ### field.boolean() ```typescript field.boolean() // Required boolean field.boolean().default(false) // Default value ``` ### field.date() ```typescript field.date() // Required date field.date().min(new Date()) // Minimum date field.date().max(new Date()) // Maximum date ``` ### field.datetime() Date with default to current time: ```typescript field.datetime() // Default: new Date() ``` ## ID Types ### field.id() Auto-generating ObjectId for `_id` fields: ```typescript field.id() // Accepts string, converts to ObjectId // Generates new ObjectId if not provided ``` ### field.objectId() Required ObjectId for references: ```typescript field.objectId() // Requires a value (string or ObjectId) // Transforms strings to ObjectId ``` ### field.uuid() Auto-generating UUID v4: ```typescript field.uuid() // Generates UUID if not provided // Result: '123e4567-e89b-12d3-a456-426614174000' ``` ## Validation Types ### field.email() Email validation: ```typescript field.email() // Validates email format ``` ### field.url() URL validation: ```typescript field.url() // Validates URL format ``` ### field.enum() Enum/union type: ```typescript field.enum(['pending', 'active', 'suspended']) field.enum(['user', 'admin']).default('user') ``` ## Complex Types ### field.array() ```typescript field.array(field.string()) // string[] field.array(field.number()).min(1) // At least 1 element field.array(field.number()).max(10) // At most 10 elements field.array(field.number()).length(5) // Exactly 5 elements field.array(field.number()).nonempty() // At least 1 element ``` ### field.object() ```typescript field.object({ street: field.string(), city: field.string(), zip: field.string(), }) ``` ## BSON Types ### field.long() 64-bit integer (MongoDB Long): ```typescript field.long() // Usage import { Long } from '@codician-team/monch'; { count: Long.fromNumber(9007199254740993) } // Serializes to number (safe range) or bigint ``` ### field.decimal() High-precision decimal (MongoDB Decimal128): ```typescript field.decimal() // Usage import { Decimal128 } from '@codician-team/monch'; { price: Decimal128.fromString('99999999999999.99') } // Serializes to string ``` ### field.binary() Binary data (MongoDB Binary): ```typescript field.binary() // Usage import { Binary } from '@codician-team/monch'; { data: new Binary(Buffer.from('hello')) } // Serializes to base64 string ``` ### field.timestamp() MongoDB Timestamp: ```typescript field.timestamp() // Usage import { Timestamp } from '@codician-team/monch'; { ts: new Timestamp({ t: 1234567890, i: 1 }) } // Serializes to { t: number, i: number } ``` ## Money Type ### field.money() Currency field stored as Decimal128 for precision: ```typescript field.money() // Default: USD field.money({ currency: 'EUR' }) // Euro field.money({ currency: 'JPY' }) // Japanese Yen (0 decimals) field.money({ currency: 'BTC' }) // Bitcoin (8 decimals) field.money({ currency: 'ETH' }) // Ethereum (18 decimals) ``` **Options:** ```typescript field.money({ currency: 'USD', // Currency code (default: 'USD') min: 0, // Minimum amount max: 10000, // Maximum amount allowNegative: false, // Allow negative (default: false) }) ``` **MoneyValue structure:** ```typescript { amount: Decimal128, // High-precision decimal currency: string, // Currency code } ``` **Supported currencies:** USD, EUR, GBP, JPY, CNY, INR, AUD, CAD, CHF, HKD, SGD, SEK, KRW, NOK, NZD, MXN, TWD, ZAR, BRL, DKK, PLN, THB, ILS, IDR, CZK, AED, TRY, HUF, CLP, SAR, PHP, MYR, COP, RUB, RON, BTC, ETH ## Money Utility Class The `Money` class provides all operations for monetary values: ```typescript import { Money } from '@codician-team/monch'; ``` ### Formatting & Conversion ```typescript Money.format(price) // '$99.99' Money.format(price, { precision: 0 }) // '$100' Money.toNumber(price) // 99.99 (may lose precision) Money.toString(price) // '99.99' (exact) Money.toCents(price) // 9999 (for Stripe) Money.fromCents(9999, 'USD') // MoneyValue Money.toJSON(price) // { amount, currency, display, cents } ``` ### Creating MoneyValue ```typescript Money.from(99.99) // From number (USD) Money.from('99.99', 'EUR') // From string Money.from({ amount: 99.99, currency: 'GBP' }) // From object ``` ### Arithmetic ```typescript Money.add(a, b) // Add (same currency) Money.add(a, 10) // Add number Money.subtract(a, b) // Subtract Money.multiply(a, 1.1) // Multiply by factor Money.divide(a, 4) // Divide Money.percentage(a, 15) // 15% of value Money.round(a) // Round to currency precision ``` ### Aggregation & Splitting ```typescript Money.sum([a, b, c]) // Sum array Money.min(a, b, c) // Minimum value Money.max(a, b, c) // Maximum value Money.split(total, 3) // Split equally (handles rounding) Money.allocate(total, [2,1]) // Split by ratios ``` ### Comparison ```typescript Money.equals(a, b) // Equality Money.compare(a, b) // -1 | 0 | 1 Money.greaterThan(a, b) // a > b Money.lessThan(a, b) // a < b Money.greaterThanOrEqual(a, b) Money.lessThanOrEqual(a, b) Money.isZero(a) // Check zero Money.isPositive(a) // Check positive Money.isNegative(a) // Check negative Money.abs(a) // Absolute value Money.negate(a) // Negate ``` ## Currency Configuration ```typescript import { getCurrencyConfig, CURRENCY_CONFIGS } from '@codician-team/monch'; getCurrencyConfig('USD') // { code: 'USD', symbol: '$', precision: 2, symbolPosition: 'before' } CURRENCY_CONFIGS.JPY // { code: 'JPY', symbol: '¥', precision: 0, symbolPosition: 'before' } ``` ## Using Zod Directly The `z` export is Zod itself: ```typescript import { z } from '@codician-team/monch'; const schema = { _id: field.id(), metadata: z.record(z.string(), z.any()), tags: z.set(z.string()), data: z.union([z.string(), z.number()]), }; ``` ## Quick Reference | Method | Output Type | Description | |--------|-------------|-------------| | `field.id()` | `ObjectId` | Auto-generating ObjectId | | `field.objectId()` | `ObjectId` | Required ObjectId reference | | `field.uuid()` | `string` | Auto-generating UUID | | `field.string()` | `string` | String | | `field.number()` | `number` | Number | | `field.boolean()` | `boolean` | Boolean | | `field.date()` | `Date` | Date | | `field.datetime()` | `Date` | Date with default to now | | `field.email()` | `string` | Email validation | | `field.url()` | `string` | URL validation | | `field.enum([...])` | `union` | Enum type | | `field.array(type)` | `T[]` | Array of type | | `field.object({...})` | `object` | Nested object | | `field.long()` | `Long` | 64-bit integer | | `field.decimal()` | `Decimal128` | High-precision decimal | | `field.binary()` | `Binary` | Binary data | | `field.timestamp()` | `Timestamp` | MongoDB timestamp | | `field.money(opts?)` | `MoneyValue` | Currency with formatting | --- --- url: /guide/field-types.md --- # Field Types Monch provides a `field` helper with common field types built on Zod. All field types support the full Zod API. ## Basic Types ### String ```typescript field.string() // Required string field.string().min(1) // At least 1 character field.string().max(100) // At most 100 characters field.string().length(10) // Exactly 10 characters field.string().trim() // Trim whitespace field.string().toLowerCase() // Convert to lowercase field.string().optional() // Optional (can be undefined) field.string().nullable() // Can be null field.string().default('hello') // Default value ``` ### Number ```typescript field.number() // Required number field.number().int() // Integer only field.number().positive() // Greater than 0 field.number().negative() // Less than 0 field.number().min(0) // Minimum value field.number().max(100) // Maximum value field.number().multipleOf(5) // Must be multiple of 5 ``` ### Boolean ```typescript field.boolean() // Required boolean field.boolean().default(false) // Default value ``` ### Date ```typescript field.date() // Required date field.date().min(new Date()) // Minimum date field.date().max(new Date()) // Maximum date field.datetime() // Date with default to now ``` ## ID Types ### ObjectId ```typescript // Auto-generating ObjectId (for _id field) field.id() // Required ObjectId reference (for foreign keys) field.objectId() ``` Both accept strings and automatically convert them to ObjectId: ```typescript await Users.insertOne({ _id: '507f1f77bcf86cd799439011', // Converted to ObjectId name: 'Alice', }); ``` ### UUID ```typescript field.uuid() // Auto-generating UUID v4 // Result: '123e4567-e89b-12d3-a456-426614174000' ``` ## Validation Types ### Email ```typescript field.email() // Email validation ``` ### URL ```typescript field.url() // URL validation ``` ### Enum ```typescript // String enum field.enum(['pending', 'active', 'suspended']) // With default field.enum(['user', 'admin']).default('user') ``` ## Complex Types ### Array ```typescript // Array of strings field.array(field.string()) // Array of numbers with constraints field.array(field.number()).min(1).max(10) // Array of objects field.array(field.object({ name: field.string(), value: field.number(), })) ``` ### Object ```typescript // Nested object field.object({ street: field.string(), city: field.string(), zip: field.string(), country: field.string().default('US'), }) // Optional nested object field.object({ bio: field.string().optional(), website: field.url().optional(), }).optional() ``` ## BSON Types ### Long (64-bit Integer) ```typescript field.long() // MongoDB Long (Int64) // Usage await Stats.insertOne({ viewCount: Long.fromNumber(9007199254740993), // Beyond JS safe integer }); ``` ::: info Serialization `Long` values serialize to `number` when within JavaScript's safe integer range (±9,007,199,254,740,991). Values outside this range serialize to `bigint` to preserve precision. ::: ### Decimal128 (High-Precision Decimal) ```typescript field.decimal() // MongoDB Decimal128 // Usage - great for financial calculations await Accounts.insertOne({ balance: Decimal128.fromString('99999999999999.99'), }); ``` ### Binary ```typescript field.binary() // MongoDB Binary // Usage await Files.insertOne({ content: new Binary(Buffer.from('hello')), }); ``` ### Timestamp ```typescript field.timestamp() // MongoDB Timestamp // Usage - internal MongoDB operations await OpLog.insertOne({ ts: new Timestamp({ t: 1234567890, i: 1 }), }); ``` ::: info Serialization `Timestamp` serializes to `{ t: number, i: number }` where `t` is the timestamp and `i` is the increment. ::: ## Money Type Built-in currency support with 30+ currencies, stored as Decimal128 for precision: ```typescript field.money() // Default: USD field.money({ currency: 'EUR' }) // Euro field.money({ currency: 'JPY' }) // Japanese Yen (no decimals) field.money({ currency: 'BTC' }) // Bitcoin (8 decimals) ``` ### Money Options ```typescript field.money({ currency: 'USD', // Default currency code (default: 'USD') min: 0, // Minimum amount max: 10000, // Maximum amount allowNegative: false, // Allow negative amounts (default: false) }); ``` ### MoneyValue Structure Stored as Decimal128 for financial precision: ```typescript { amount: Decimal128, // High-precision decimal value currency: string, // Currency code (e.g., 'USD') } ``` ### Flexible Input Formats Money fields accept multiple input formats: ```typescript await Products.insertOne({ name: 'Widget', price: 29.99, // Number priceEUR: '49.99', // String priceJPY: { amount: 3500, currency: 'JPY' }, // Object }); // Stored as Decimal128: // { // price: { amount: Decimal128('29.99'), currency: 'USD' }, // priceEUR: { amount: Decimal128('49.99'), currency: 'EUR' }, // priceJPY: { amount: Decimal128('3500'), currency: 'JPY' } // } ``` ### Example Usage ```typescript import { collection, field, Money } from '@codician-team/monch'; const Products = collection({ name: 'products', schema: { _id: field.id(), name: field.string(), price: field.money({ currency: 'USD' }), }, }); // Insert with decimal amount const product = await Products.insertOne({ name: 'Widget', price: 19.99, }); // Use Money utility class for operations Money.format(product.price); // '$19.99' Money.toCents(product.price); // 1999 (for Stripe) Money.toNumber(product.price); // 19.99 ``` ### Supported Currencies USD, EUR, GBP, JPY, CNY, INR, AUD, CAD, CHF, HKD, SGD, SEK, KRW, NOK, NZD, MXN, TWD, ZAR, BRL, DKK, PLN, THB, ILS, IDR, CZK, AED, TRY, HUF, CLP, SAR, PHP, MYR, COP, RUB, RON, BTC, and ETH. ### Money Utility Class The `Money` class provides all operations for monetary values: ```typescript import { Money } from '@codician-team/monch'; // Formatting Money.format(price); // '$99.99' Money.format(price, { precision: 0 }); // '$100' // Conversions Money.toNumber(price); // 99.99 (may lose precision) Money.toString(price); // '99.99' (exact) Money.toCents(price); // 9999 (for payment APIs) Money.fromCents(9999, 'USD'); // MoneyValue // Create MoneyValue Money.from(99.99); // From number Money.from('99.99', 'EUR'); // From string Money.from({ amount: 99.99, currency: 'GBP' }); // From object // Arithmetic Money.add(price, tax); // Add two values Money.subtract(price, discount); // Subtract Money.multiply(price, 1.1); // Multiply (10% increase) Money.divide(total, 4); // Divide Money.percentage(price, 15); // Calculate 15% Money.round(result); // Round to currency precision // Aggregation Money.sum([price1, price2]); // Sum array Money.min(price1, price2); // Find minimum Money.max(price1, price2); // Find maximum // Splitting Money.split(total, 3); // Split equally [$33.34, $33.33, $33.33] Money.allocate(total, [2,1,1]); // Split by ratios [$50, $25, $25] // Comparison Money.equals(a, b); // Check equality Money.compare(a, b); // -1 | 0 | 1 Money.greaterThan(a, b); // a > b Money.lessThan(a, b); // a < b Money.isZero(price); // Check if zero Money.isPositive(price); // Check if positive Money.isNegative(price); // Check if negative Money.abs(price); // Absolute value Money.negate(price); // Negate value // JSON serialization Money.toJSON(price); // { amount: '99.99', currency: 'USD', display: '$99.99', cents: 9999 } ``` ### Currency Configuration ```typescript import { getCurrencyConfig, CURRENCY_CONFIGS } from '@codician-team/monch'; // Get currency configuration const config = getCurrencyConfig('USD'); // { code: 'USD', symbol: '$', precision: 2, symbolPosition: 'before' } // Access all currency configs CURRENCY_CONFIGS.EUR // { code: 'EUR', symbol: '€', ... } ``` ## Quick Reference | Method | Output Type | Description | |--------|-------------|-------------| | `field.id()` | `ObjectId` | Auto-generating ObjectId | | `field.objectId()` | `ObjectId` | Required ObjectId reference | | `field.uuid()` | `string` | Auto-generating UUID | | `field.string()` | `string` | String | | `field.number()` | `number` | Number | | `field.boolean()` | `boolean` | Boolean | | `field.date()` | `Date` | Date | | `field.datetime()` | `Date` | Date with default to now | | `field.email()` | `string` | Email validation | | `field.url()` | `string` | URL validation | | `field.enum([...])` | `union` | Enum type | | `field.array(type)` | `T[]` | Array of type | | `field.object({...})` | `object` | Nested object | | `field.long()` | `Long` | 64-bit integer | | `field.decimal()` | `Decimal128` | High-precision decimal | | `field.binary()` | `Binary` | Binary data | | `field.timestamp()` | `Timestamp` | MongoDB timestamp | | `field.money(opts?)` | `MoneyValue` | Currency with formatting | --- --- url: /guide/getting-started.md --- # Getting Started ## Installation ### 1. Configure GitHub Packages Create or update `.npmrc` in your project root: ```ini @codician-team:registry=https://npm.pkg.github.com //npm.pkg.github.com/:_authToken=${GITHUB_TOKEN} ``` You'll need a GitHub token with `read:packages` scope. Set it as an environment variable or replace `${GITHUB_TOKEN}` with your token. ### 2. Install the package ::: code-group ```bash [npm] npm install @codician-team/monch ``` ```bash [yarn] yarn add @codician-team/monch ``` ```bash [pnpm] pnpm add @codician-team/monch ``` ```bash [bun] bun add @codician-team/monch ``` ::: ## Quick Start ### 1. Set Environment Variable ```bash # .env MONGODB_URI=mongodb://localhost:27017/myapp ``` ### 2. Define a Collection ```typescript import { collection, field } from '@codician-team/monch'; export const Users = collection({ name: 'users', schema: { _id: field.id(), name: field.string().min(1), email: field.email(), role: field.enum(['user', 'admin']).default('user'), }, timestamps: true, // Adds createdAt, updatedAt }); ``` ### 3. Use It ```typescript // Insert (automatically validated) const user = await Users.insertOne({ name: 'Alice', email: 'alice@example.com', }); // user: { _id: ObjectId, name: 'Alice', email: '...', role: 'user', createdAt: Date, updatedAt: Date } // Query const admins = await Users.find({ role: 'admin' }).toArray(); // Update (partial validation on $set fields) await Users.updateOne( { _id: user._id }, { $set: { role: 'admin' } } ); // Serialize for JSON/Next.js const plain = user.serialize(); // { _id: '507f1f77...', name: 'Alice', ... } ``` ## How It Works ### Auto-Connection Monch automatically connects to MongoDB on first operation. No setup code needed: ```typescript // First call triggers connection const user = await Users.findOne({ email: 'alice@example.com' }); ``` The connection is cached and reused for all subsequent operations. ### Environment Variables Monch checks these environment variables in order: 1. `MONGODB_URI` - Primary connection string 2. `MONGO_URL` - Common alternative 3. `DATABASE_URL` - Used only if it's a MongoDB URI For the database name: 1. Explicit `database` config option 2. Database in URI path (e.g., `mongodb://localhost:27017/mydb`) 3. `MONGODB_DATABASE` or `MONGO_DATABASE` environment variable ### Validation Flow When you insert a document: ``` Input → beforeValidate hook → Zod validation → afterValidate hook → timestamps applied → beforeInsert hook → MongoDB insert → afterInsert hook ``` When you update: ``` Update → Partial validation of $set fields → timestamps updated → MongoDB update ``` ## Project Structure A typical project structure with Monch: ``` src/ ├── lib/ │ └── models/ │ ├── index.ts # Re-export all collections │ ├── user.model.ts │ ├── post.model.ts │ └── comment.model.ts ├── app/ │ ├── actions/ │ │ └── user.actions.ts # Server actions using collections │ └── api/ │ └── users/ │ └── route.ts # API routes using collections ``` ### Example Model File ```typescript // src/lib/models/user.model.ts import { collection, field, type ModelOf, type SerializedOf } from '@codician-team/monch'; export const Users = collection({ name: 'users', schema: { _id: field.id(), name: field.string().min(1).max(100), email: field.email(), role: field.enum(['user', 'admin', 'moderator']).default('user'), profile: field.object({ bio: field.string().max(500).optional(), avatar: field.url().optional(), }).optional(), }, timestamps: true, indexes: [ { key: { email: 1 }, unique: true }, { key: { role: 1 } }, ], }); // Export types for use in components export type User = ModelOf; export type SerializedUser = SerializedOf; ``` ### Example Index File ```typescript // src/lib/models/index.ts export { Users, type User, type SerializedUser } from './user.model'; export { Posts, type Post, type SerializedPost } from './post.model'; export { Comments, type Comment, type SerializedComment } from './comment.model'; ``` ## Next Steps * [Schema Definition](/guide/schema) - Learn about field types and schema options * [CRUD Operations](/guide/crud) - Full guide to database operations * [Validation](/guide/validation) - Understand how validation works --- --- url: /guide/indexes.md --- # Indexes Indexes improve query performance by allowing MongoDB to find documents without scanning the entire collection. ## Defining Indexes ```typescript const Users = collection({ name: 'users', schema: { _id: field.id(), email: field.email(), name: field.string(), role: field.enum(['user', 'admin']), status: field.enum(['active', 'inactive']), createdAt: field.datetime(), }, indexes: [ // Simple index { key: { email: 1 } }, // Unique index { key: { email: 1 }, unique: true }, // Compound index { key: { role: 1, createdAt: -1 } }, // Descending index { key: { createdAt: -1 } }, ], }); ``` ## Index Types ### Single Field Index ```typescript { key: { email: 1 } } // Ascending { key: { createdAt: -1 } } // Descending ``` ### Compound Index Index on multiple fields (order matters): ```typescript { key: { status: 1, createdAt: -1 } } ``` This index supports queries like: * `{ status: 'active' }` * `{ status: 'active', createdAt: { $gte: date } }` * Sorting by `{ status: 1, createdAt: -1 }` ### Unique Index Enforce unique values: ```typescript { key: { email: 1 }, unique: true } ``` Attempting to insert a duplicate throws an error. ### Sparse Index Only index documents that have the field: ```typescript { key: { optionalField: 1 }, sparse: true } ``` Useful for optional fields where you want unique constraint only on documents that have the field. ### Partial Index Index only documents matching a filter: ```typescript { key: { email: 1 }, partialFilterExpression: { status: 'active' } } ``` Only indexes documents where `status === 'active'`. ### Text Index Enable full-text search: ```typescript { key: { title: 'text', content: 'text' } } ``` Query with: ```typescript Users.find({ $text: { $search: 'mongodb tutorial' } }); ``` ### TTL Index Auto-delete documents after a time period: ```typescript { key: { expiresAt: 1 }, expireAfterSeconds: 0 // Delete when expiresAt is reached } // Or delete after fixed duration { key: { createdAt: 1 }, expireAfterSeconds: 86400 // Delete 24 hours after creation } ``` ### Wildcard Index Index all fields (use sparingly): ```typescript { key: { '$**': 1 } } // Or specific path { key: { 'metadata.$**': 1 } } ``` ## Index Options | Option | Type | Description | |--------|------|-------------| | `unique` | `boolean` | Enforce unique values | | `sparse` | `boolean` | Only index documents with the field | | `background` | `boolean` | Build index in background (deprecated in 4.2+) | | `expireAfterSeconds` | `number` | TTL - auto-delete after seconds | | `partialFilterExpression` | `object` | Only index matching documents | | `name` | `string` | Custom index name | | `collation` | `object` | Locale-specific string comparison | ## Auto-Creation By default, Monch creates indexes on first connection: ```typescript const Users = collection({ name: 'users', schema: { /* ... */ }, indexes: [ { key: { email: 1 }, unique: true }, ], createIndexes: true, // Default: true }); ``` Disable auto-creation: ```typescript const Users = collection({ name: 'users', schema: { /* ... */ }, indexes: [ /* ... */ ], createIndexes: false, // Don't auto-create }); ``` ## Manual Index Creation Create indexes explicitly: ```typescript // Create all configured indexes const indexNames = await Users.ensureIndexes(); console.log('Created indexes:', indexNames); ``` ## Common Index Patterns ### Email Lookup ```typescript indexes: [ { key: { email: 1 }, unique: true }, ] ``` ### Status + Date Queries ```typescript // For queries like: { status: 'active' } sorted by createdAt indexes: [ { key: { status: 1, createdAt: -1 } }, ] ``` ### User Activity ```typescript indexes: [ { key: { userId: 1, createdAt: -1 } }, // User's activity timeline { key: { createdAt: -1 } }, // Global activity feed ] ``` ### Search with Filters ```typescript indexes: [ { key: { title: 'text', content: 'text' } }, // Text search { key: { category: 1, createdAt: -1 } }, // Category browse ] ``` ### Geo-Spatial ```typescript // For location-based queries indexes: [ { key: { location: '2dsphere' } }, ] // Usage Users.find({ location: { $near: { $geometry: { type: 'Point', coordinates: [-73.9667, 40.78] }, $maxDistance: 1000, // meters }, }, }); ``` ## Best Practices ### 1. Index Fields Used in Queries ```typescript // If you query like this: Users.find({ role: 'admin', status: 'active' }); // Create this index: { key: { role: 1, status: 1 } } ``` ### 2. Consider Query Patterns Order compound index fields by: 1. Equality conditions first 2. Sort fields second 3. Range conditions last ```typescript // Query: { status: 'active', role: 'admin' } sorted by createdAt // Index: status (equality) + role (equality) + createdAt (sort) { key: { status: 1, role: 1, createdAt: -1 } } ``` ### 3. Don't Over-Index * Each index uses memory and disk space * Indexes slow down writes (must update indexes) * Remove unused indexes ### 4. Use Covered Queries If an index contains all fields needed by a query, MongoDB can return results from the index alone: ```typescript // Index { key: { email: 1, name: 1 } } // This query is "covered" - only needs the index Users.find({ email: 'alice@example.com' }) .project({ email: 1, name: 1, _id: 0 }); ``` ### 5. Monitor Index Usage Use MongoDB tools to analyze index usage: ```javascript // In MongoDB shell db.users.aggregate([{ $indexStats: {} }]); ``` ## Viewing Existing Indexes Access the raw collection to view indexes: ```typescript const raw = await Users.collection; const indexes = await raw.indexes(); console.log(indexes); ``` --- --- url: /guide/methods.md --- # Instance Methods Add custom methods to documents returned from queries. ## Defining Methods ```typescript const Users = collection({ name: 'users', schema: { _id: field.id(), firstName: field.string(), lastName: field.string(), email: field.email(), role: field.enum(['user', 'admin']).default('user'), }, methods: { // First argument (doc) is auto-injected fullName: (doc) => `${doc.firstName} ${doc.lastName}`, isAdmin: (doc) => doc.role === 'admin', // Methods can accept additional arguments greet: (doc, greeting = 'Hello') => `${greeting}, ${doc.firstName}!`, // Methods can be async sendEmail: async (doc, subject: string, body: string) => { await emailService.send({ to: doc.email, subject, body, }); }, }, }); ``` ## Using Methods Methods are available on documents returned from queries: ```typescript const user = await Users.findOne({ email: 'alice@example.com' }); if (user) { console.log(user.fullName()); // 'Alice Smith' console.log(user.isAdmin()); // false console.log(user.greet('Hi')); // 'Hi, Alice!' await user.sendEmail('Welcome!', 'Thanks for signing up.'); } ``` ## Method Availability Methods are attached to documents returned from: * `findOne()` * `findById()` * `find().toArray()` (each document) * `insertOne()` * `insertMany()` (each document) * `updateOne()` * `findOneAndUpdate()` * `findOneAndDelete()` * `findOneAndReplace()` ## Type Safety Methods are fully typed: ```typescript const Users = collection({ name: 'users', schema: { _id: field.id(), points: field.number().default(0), level: field.enum(['bronze', 'silver', 'gold']).default('bronze'), }, methods: { getLevel: (doc): 'bronze' | 'silver' | 'gold' => { if (doc.points >= 1000) return 'gold'; if (doc.points >= 500) return 'silver'; return 'bronze'; }, addPoints: async (doc, amount: number) => { return await Users.updateOne( { _id: doc._id }, { $inc: { points: amount } } ); }, }, }); const user = await Users.findById(id); if (user) { const level = user.getLevel(); // Type: 'bronze' | 'silver' | 'gold' await user.addPoints(100); // Type-checked argument } ``` ## Common Patterns ### Computed Properties ```typescript methods: { fullName: (doc) => `${doc.firstName} ${doc.lastName}`, age: (doc) => { const today = new Date(); const birth = doc.birthDate; let age = today.getFullYear() - birth.getFullYear(); if (today < new Date(today.getFullYear(), birth.getMonth(), birth.getDate())) { age--; } return age; }, displayPrice: (doc) => { return new Intl.NumberFormat('en-US', { style: 'currency', currency: doc.price.currency, }).format(doc.price.amount / 100); }, } ``` ### Self-Updating Methods ```typescript methods: { markAsRead: async (doc) => { return await Notifications.updateOne( { _id: doc._id }, { $set: { readAt: new Date() } } ); }, incrementViews: async (doc) => { return await Posts.updateOne( { _id: doc._id }, { $inc: { views: 1 } } ); }, softDelete: async (doc) => { return await Users.updateOne( { _id: doc._id }, { $set: { deletedAt: new Date(), status: 'deleted' } } ); }, } ``` ### Relationship Loading ```typescript methods: { getAuthor: async (doc) => { return await Users.findById(doc.authorId); }, getComments: async (doc) => { return await Comments.find({ postId: doc._id }).toArray(); }, getCommentsCount: async (doc) => { return await Comments.count({ postId: doc._id }); }, } ``` ### Validation Methods ```typescript methods: { canEdit: (doc, userId: string) => { return doc.authorId.equals(userId) || doc.editors.includes(userId); }, isExpired: (doc) => { return doc.expiresAt && doc.expiresAt < new Date(); }, hasPermission: (doc, permission: string) => { return doc.permissions.includes(permission) || doc.role === 'admin'; }, } ``` ## Methods vs Statics | Feature | Instance Methods | Static Methods | |---------|-----------------|----------------| | Access | On documents | On collection | | First arg | Document (auto-injected) | Collection | | Use case | Document operations | Collection operations | | Example | `user.fullName()` | `Users.findByEmail(email)` | Use instance methods when the operation is specific to a single document. Use static methods for operations that work with the collection as a whole. ## Serialization Note Methods are **not** included when you serialize a document: ```typescript const user = await Users.findOne({ email: 'alice@example.com' }); // Before serialization - methods available user.fullName(); // Works // After serialization - plain object, no methods const plain = user.serialize(); plain.fullName; // undefined - methods are not serialized ``` If you need computed values in serialized output, compute them before serializing: ```typescript const user = await Users.findOne({ email: 'alice@example.com' }); const serialized = { ...user.serialize(), fullName: user.fullName(), isAdmin: user.isAdmin(), }; ``` --- --- url: /guide/hooks.md --- # Lifecycle Hooks Intercept operations to transform data or trigger side effects. ## Hook Execution Order ### Insert Flow ``` 1. beforeValidate(rawInput) → Transform raw input (e.g., generate slug) 2. Zod Validation → Schema validation with defaults/transforms 3. afterValidate(validatedDoc) → Post-validation transforms 4. Timestamps Applied → createdAt/updatedAt set (if enabled) 5. beforeInsert(doc) → Final modifications before insert 6. MongoDB Insert → Document inserted 7. afterInsert(doc) → Side effects (logging, notifications) ``` ### Update Flow ``` 1. beforeUpdate(filter, update) → Modify filter/update 2. Partial Validation → Validate $set fields 3. Timestamps Applied → updatedAt set (if enabled) 4. MongoDB Update → Document updated 5. afterUpdate(doc) → Side effects (only for updateOne) ``` ### Delete Flow ``` 1. beforeDelete(filter) → Modify filter 2. MongoDB Delete → Document(s) deleted 3. afterDelete(count) → Side effects ``` ## Defining Hooks ```typescript const Posts = collection({ name: 'posts', schema: { _id: field.id(), title: field.string(), slug: field.string(), publishedAt: field.date().optional(), }, hooks: { // Transform BEFORE validation (generate slug from title) beforeValidate: (doc) => ({ ...doc, slug: doc.title.toLowerCase().replace(/\s+/g, '-'), }), // Transform AFTER validation (validated doc available) afterValidate: (doc) => ({ ...doc, // Can access validated/defaulted fields here }), // Final modifications before insert beforeInsert: (doc) => ({ ...doc, searchIndex: `${doc.title} ${doc.slug}`.toLowerCase(), }), // Side effects after insert afterInsert: async (doc) => { await notifySubscribers(doc); }, // Modify filter/update before updating beforeUpdate: (filter, update) => ({ filter, update: { ...update, $set: { ...update.$set, lastModifiedBy: getCurrentUserId() }, }, }), // React to updates afterUpdate: async (doc) => { if (doc) await logAuditTrail('update', doc._id); }, // Modify filter before delete beforeDelete: (filter) => { console.log('Deleting:', filter); return filter; }, // React to deletes afterDelete: async (count) => { console.log(`Deleted ${count} documents`); }, }, }); ``` ## Hook Reference | Hook | Triggered By | Receives | Must Return | |------|--------------|----------|-------------| | `beforeValidate` | `insertOne`, `insertMany` | raw input | document | | `afterValidate` | `insertOne`, `insertMany` | validated document | document | | `beforeInsert` | `insertOne`, `insertMany` | document (after timestamps) | document | | `afterInsert` | `insertOne`, `insertMany` | inserted document | nothing | | `beforeUpdate` | `updateOne` only | `(filter, update)` | `{ filter, update }` | | `afterUpdate` | `updateOne` only | updated document or `null` | nothing | | `beforeDelete` | `deleteOne`, `deleteMany` | filter | filter | | `afterDelete` | `deleteOne`, `deleteMany` | deleted count | nothing | ## Async Hooks All hooks can be async: ```typescript hooks: { beforeInsert: async (doc) => { const exists = await checkExternalService(doc.email); if (exists) { throw new Error('Email already registered'); } return doc; }, afterInsert: async (doc) => { await sendWelcomeEmail(doc.email); await analytics.track('user_created', { userId: doc._id }); }, } ``` ## Common Patterns ### Auto-Generate Slug ```typescript hooks: { beforeValidate: (doc) => ({ ...doc, slug: doc.slug || doc.title.toLowerCase() .replace(/[^a-z0-9]+/g, '-') .replace(/^-|-$/g, ''), }), } ``` ### Audit Trail ```typescript hooks: { beforeInsert: (doc) => ({ ...doc, createdBy: getCurrentUserId(), }), beforeUpdate: (filter, update) => ({ filter, update: { ...update, $set: { ...update.$set, updatedBy: getCurrentUserId(), }, }, }), } ``` ### Cascade Delete ```typescript hooks: { afterDelete: async (count, filter) => { // Delete related documents await Comments.deleteMany({ postId: filter._id }); await Likes.deleteMany({ postId: filter._id }); }, } ``` ### Validation with External Data ```typescript hooks: { beforeValidate: async (doc) => { // Fetch additional data const category = await Categories.findById(doc.categoryId); if (!category) { throw new Error('Invalid category'); } return { ...doc, categoryName: category.name, }; }, } ``` ### Normalize Data ```typescript hooks: { beforeValidate: (doc) => ({ ...doc, email: doc.email.toLowerCase().trim(), tags: doc.tags?.map(t => t.toLowerCase().trim()), }), } ``` ## Important Notes ::: warning Operations Without Hooks The following operations do **not** trigger any hooks: * `updateMany` - No update hooks * `findOneAndUpdate` - Atomic operation * `findOneAndDelete` - Atomic operation * `findOneAndReplace` - Atomic operation * `replaceOne` - No hooks * `bulkWrite` - No hooks (uses Zod-only validation) ::: ::: info Validation Hooks on Insert Only `beforeValidate` and `afterValidate` only run on `insertOne` and `insertMany`. They do **not** run on update operations. ::: ::: info Update Hooks on updateOne Only `beforeUpdate` and `afterUpdate` only run for `updateOne`, **not** for `updateMany`. ::: ## Error Handling in Hooks If a hook throws an error, the operation is aborted: ```typescript hooks: { beforeInsert: (doc) => { if (doc.role === 'admin' && !isCurrentUserSuperAdmin()) { throw new Error('Only super admins can create admin users'); } return doc; }, } // Usage try { await Users.insertOne({ name: 'Alice', role: 'admin' }); } catch (error) { console.log(error.message); // 'Only super admins can create admin users' } ``` --- --- url: /llms.md --- # llms.txt Alternatively, you can download [llms.txt](/llms.txt) or [llms-full.txt](/llms-full.txt) and feed it to your favorite LLMs like ChatGPT, Claude or Gemini to get a more interactive experience. --- --- url: /examples/nextjs.md --- # Next.js Integration Complete guide to using Monch with Next.js App Router. ## Project Structure ``` src/ ├── lib/ │ └── models/ │ ├── index.ts │ ├── user.model.ts │ └── post.model.ts ├── app/ │ ├── actions/ │ │ └── user.actions.ts │ ├── users/ │ │ ├── page.tsx │ │ └── [id]/ │ │ └── page.tsx │ └── api/ │ └── users/ │ └── route.ts └── components/ └── UserCard.tsx ``` ## Model Definition ```typescript // lib/models/user.model.ts import { collection, field, type ModelOf, type SerializedOf } from '@codician-team/monch'; export const Users = collection({ name: 'users', schema: { _id: field.id(), name: field.string().min(1), email: field.email(), avatar: field.url().optional(), role: field.enum(['user', 'admin']).default('user'), }, timestamps: true, indexes: [ { key: { email: 1 }, unique: true }, ], }); export type User = ModelOf; export type SerializedUser = SerializedOf; ``` ```typescript // lib/models/index.ts export { Users, type User, type SerializedUser } from './user.model'; export { Posts, type Post, type SerializedPost } from './post.model'; ``` ## Server Components ```typescript // app/users/page.tsx import { Users, type SerializedUser } from '@/lib/models'; import { UserCard } from '@/components/UserCard'; export default async function UsersPage() { const users = await Users.find({ role: 'user' }) .sort({ createdAt: -1 }) .limit(20) .serialize(); return (
{users.map((user) => ( ))}
); } ``` ```typescript // app/users/[id]/page.tsx import { Users } from '@/lib/models'; import { notFound } from 'next/navigation'; interface Props { params: { id: string }; } export default async function UserPage({ params }: Props) { const user = await Users.findById(params.id); if (!user) { notFound(); } const serialized = user.serialize(); return (

{serialized.name}

{serialized.email}

Joined: {new Date(serialized.createdAt).toLocaleDateString()}

); } ``` ## Client Components ```typescript // components/UserCard.tsx 'use client'; import type { SerializedUser } from '@/lib/models'; interface Props { user: SerializedUser; } export function UserCard({ user }: Props) { return (

{user.name}

{user.email}

{user.avatar && {user.name}} Joined {new Date(user.createdAt).toLocaleDateString()}
); } ``` ## Server Actions ```typescript // app/actions/user.actions.ts 'use server'; import { revalidatePath } from 'next/cache'; import { Users, type SerializedUser } from '@/lib/models'; import { MonchValidationError } from '@codician-team/monch'; export async function getUsers(): Promise { return Users.find() .sort({ createdAt: -1 }) .serialize(); } export async function getUser(id: string): Promise { const user = await Users.findById(id); return user?.serialize() ?? null; } export async function createUser(formData: FormData) { try { const user = await Users.insertOne({ name: formData.get('name') as string, email: formData.get('email') as string, }); revalidatePath('/users'); return { success: true, user: user.serialize() }; } catch (error) { if (error instanceof MonchValidationError) { return { success: false, errors: error.toFormErrors() }; } throw error; } } export async function updateUser(id: string, formData: FormData) { try { const user = await Users.updateOne( { _id: id }, { $set: { name: formData.get('name') as string, email: formData.get('email') as string, }, } ); revalidatePath('/users'); revalidatePath(`/users/${id}`); return { success: true, user: user?.serialize() ?? null }; } catch (error) { if (error instanceof MonchValidationError) { return { success: false, errors: error.toFormErrors() }; } throw error; } } export async function deleteUser(id: string) { await Users.deleteOne({ _id: id }); revalidatePath('/users'); return { success: true }; } ``` ## Form with Validation ```typescript // components/UserForm.tsx 'use client'; import { useState } from 'react'; import { createUser } from '@/app/actions/user.actions'; export function UserForm() { const [errors, setErrors] = useState>({}); const [pending, setPending] = useState(false); async function handleSubmit(formData: FormData) { setPending(true); setErrors({}); const result = await createUser(formData); if (!result.success) { setErrors(result.errors); } setPending(false); } return (
{errors.name && (

{errors.name}

)}
{errors.email && (

{errors.email}

)}
{errors._root && (

{errors._root}

)}
); } ``` ## API Routes ```typescript // app/api/users/route.ts import { NextRequest, NextResponse } from 'next/server'; import { Users } from '@/lib/models'; import { MonchValidationError } from '@codician-team/monch'; export async function GET(request: NextRequest) { const searchParams = request.nextUrl.searchParams; const page = Number(searchParams.get('page')) || 1; const limit = Number(searchParams.get('limit')) || 20; const result = await Users.find() .sort({ createdAt: -1 }) .paginate({ page, limit }); return NextResponse.json({ users: result.data.map(u => u.serialize()), pagination: result.pagination, }); } export async function POST(request: NextRequest) { try { const body = await request.json(); const user = await Users.insertOne(body); return NextResponse.json(user.serialize(), { status: 201 }); } catch (error) { if (error instanceof MonchValidationError) { return NextResponse.json( { errors: error.toFormErrors() }, { status: 400 } ); } throw error; } } ``` ## Pagination Component ```typescript // components/Pagination.tsx 'use client'; import Link from 'next/link'; interface Props { page: number; totalPages: number; hasNext: boolean; hasPrev: boolean; baseUrl: string; } export function Pagination({ page, totalPages, hasNext, hasPrev, baseUrl }: Props) { return ( ); } ``` ## Environment Setup ```bash # .env.local MONGODB_URI=mongodb://localhost:27017/myapp ``` ## Debug Mode in Development ```typescript // lib/db.ts import { setDebug } from '@codician-team/monch'; if (process.env.NODE_ENV === 'development') { setDebug(true); } ``` --- --- url: /guide/pagination.md --- # Pagination Monch provides built-in pagination with metadata for building paginated UIs. ## Basic Usage ```typescript const result = await Users.find({ status: 'active' }) .sort({ createdAt: -1 }) .paginate({ page: 1, limit: 20 }); ``` ## Result Structure ```typescript { data: User[], // Array of documents for this page pagination: { page: number, // Current page (1-based) limit: number, // Items per page total: number, // Total matching documents totalPages: number, // Total number of pages hasNext: boolean, // Has more pages after this hasPrev: boolean, // Has pages before this } } ``` ## Options | Option | Default | Description | |--------|---------|-------------| | `page` | `1` | Page number (1-based) | | `limit` | `20` | Items per page (capped at 100) | ::: info Limit Cap The `limit` is silently capped at 100 to prevent accidentally loading too much data. If you need more than 100 items, use multiple requests or streaming. ::: ## Examples ### Simple Pagination ```typescript const page1 = await Users.find().paginate({ page: 1 }); const page2 = await Users.find().paginate({ page: 2 }); ``` ### With Filters and Sorting ```typescript const result = await Posts.find({ status: 'published', category: 'tech', }) .sort({ publishedAt: -1 }) .paginate({ page: 3, limit: 10 }); ``` ### Cursor Methods Still Work ```typescript const result = await Users.find({ role: 'admin' }) .sort({ name: 1 }) .project({ name: 1, email: 1 }) // Select fields .paginate({ page: 1, limit: 50 }); ``` ::: warning Skip and Limit Don't use `.skip()` or `.limit()` with `.paginate()`. The pagination method handles these internally. If you do use them, they will be ignored. ::: ## Next.js Integration ### Server Component ```typescript // app/users/page.tsx import { Users } from '@/lib/models'; import { UserList } from './UserList'; import { Pagination } from '@/components/Pagination'; interface Props { searchParams: { page?: string }; } export default async function UsersPage({ searchParams }: Props) { const page = Number(searchParams.page) || 1; const result = await Users.find({ status: 'active' }) .sort({ createdAt: -1 }) .paginate({ page, limit: 20 }); const users = result.data.map(u => u.serialize()); return ( <> ); } ``` ### Server Action ```typescript // app/actions/user.actions.ts 'use server'; import { Users } from '@/lib/models'; export async function getUsers(page: number, filters?: { role?: string }) { const query = filters?.role ? { role: filters.role } : {}; const result = await Users.find(query) .sort({ createdAt: -1 }) .paginate({ page, limit: 20 }); return { users: result.data.map(u => u.serialize()), pagination: result.pagination, }; } ``` ### Pagination Component ```typescript // components/Pagination.tsx 'use client'; import Link from 'next/link'; interface Props { page: number; totalPages: number; hasNext: boolean; hasPrev: boolean; } export function Pagination({ page, totalPages, hasNext, hasPrev }: Props) { return ( ); } ``` ## Performance Considerations ### Counting Pagination requires counting total documents, which can be slow on large collections. For frequently accessed pages, consider: 1. **Caching** the total count 2. **Using `estimatedDocumentCount()`** for approximate totals 3. **Infinite scroll** instead of pagination (no total needed) ### Index Usage Ensure your filter and sort fields are indexed: ```typescript const Posts = collection({ name: 'posts', schema: { /* ... */ }, indexes: [ // Index for common queries { key: { status: 1, publishedAt: -1 } }, { key: { category: 1, publishedAt: -1 } }, ], }); ``` ## Alternative: Manual Pagination If you need more control, you can implement pagination manually: ```typescript async function getUsers(page: number, limit: number = 20) { const skip = (page - 1) * limit; const [data, total] = await Promise.all([ Users.find({ status: 'active' }) .sort({ createdAt: -1 }) .skip(skip) .limit(limit) .toArray(), Users.count({ status: 'active' }), ]); return { data, pagination: { page, limit, total, totalPages: Math.ceil(total / limit), hasNext: skip + data.length < total, hasPrev: page > 1, }, }; } ``` ## Cursor-Based Pagination For very large datasets or real-time data, consider cursor-based pagination: ```typescript async function getUsersAfter(lastId: string | null, limit: number = 20) { const query = lastId ? { _id: { $gt: new ObjectId(lastId) } } : {}; const users = await Users.find(query) .sort({ _id: 1 }) .limit(limit + 1) // Fetch one extra to check for next page .toArray(); const hasNext = users.length > limit; if (hasNext) users.pop(); // Remove the extra item return { data: users, nextCursor: hasNext ? users[users.length - 1]._id.toString() : null, }; } ``` --- --- url: /guide/querying.md --- # Querying Monch provides a chainable cursor API for building queries with full type safety. ## Basic Queries ```typescript // Find all const users = await Users.find().toArray(); // Find with filter const admins = await Users.find({ role: 'admin' }).toArray(); // Find one const user = await Users.findOne({ email: 'alice@example.com' }); // Find by ID (accepts string or ObjectId) const user = await Users.findById('507f1f77bcf86cd799439011'); ``` ## Chainable Cursor The `find()` method returns a `Cursor` that supports method chaining: ```typescript const users = await Users.find({ status: 'active' }) .sort({ createdAt: -1 }) .skip(20) .limit(10) .project({ name: 1, email: 1 }) .toArray(); ``` ### sort Sort results by one or more fields: ```typescript // Single field .sort({ createdAt: -1 }) // Descending // Multiple fields .sort({ role: 1, name: 1 }) // Ascending ``` ### skip Skip a number of documents: ```typescript .skip(20) // Skip first 20 documents ``` ### limit Limit the number of results: ```typescript .limit(10) // Return at most 10 documents ``` ### project Select specific fields: ```typescript // Include fields .project({ name: 1, email: 1 }) // Exclude fields .project({ password: 0, secret: 0 }) ``` ## Executing Queries ### toArray Execute and return results as an array: ```typescript const users = await Users.find({ role: 'admin' }).toArray(); // Returns MonchArray (array with .serialize() method) ``` The returned array has a `.serialize()` method for converting all documents: ```typescript const users = await Users.find({ role: 'admin' }).toArray(); const plain = users.serialize(); // All documents serialized for JSON/Next.js ``` ### serialize Execute and serialize in one step: ```typescript const users = await Users.find({ role: 'admin' }).serialize(); // Returns Serialized[] - ready for JSON/Next.js ``` This is equivalent to `.toArray()` followed by `.serialize()`, but more efficient. ### paginate Execute with pagination metadata: ```typescript const result = await Users.find({ status: 'active' }) .sort({ createdAt: -1 }) .paginate({ page: 2, limit: 20 }); ``` Returns: ```typescript { data: User[], // Array of documents pagination: { page: number, // Current page (1-based) limit: number, // Items per page total: number, // Total matching documents totalPages: number, // Total number of pages hasNext: boolean, // Has more pages hasPrev: boolean, // Has previous pages } } ``` **Options:** * `page` - Page number (default: 1) * `limit` - Items per page (default: 20, silently capped at 100) ## MongoDB Query Operators Monch supports all MongoDB query operators: ### Comparison ```typescript // Equal { age: 25 } { age: { $eq: 25 } } // Not equal { status: { $ne: 'deleted' } } // Greater than / less than { age: { $gt: 18 } } { age: { $gte: 18 } } { age: { $lt: 65 } } { age: { $lte: 65 } } // In array { role: { $in: ['admin', 'moderator'] } } { role: { $nin: ['banned', 'suspended'] } } ``` ### Logical ```typescript // AND (implicit) { role: 'admin', status: 'active' } // AND (explicit) { $and: [{ role: 'admin' }, { status: 'active' }] } // OR { $or: [{ role: 'admin' }, { isOwner: true }] } // NOR { $nor: [{ status: 'deleted' }, { status: 'banned' }] } // NOT { age: { $not: { $lt: 18 } } } ``` ### Element ```typescript // Field exists { deletedAt: { $exists: false } } // Type check { age: { $type: 'number' } } ``` ### Array ```typescript // Contains element { tags: 'javascript' } // Contains all elements { tags: { $all: ['javascript', 'typescript'] } } // Array size { tags: { $size: 3 } } // Element match { comments: { $elemMatch: { author: 'Alice', score: { $gt: 5 } } } } ``` ### Text Search ```typescript // Requires text index { $text: { $search: 'mongodb tutorial' } } ``` ### Regex ```typescript // Case-insensitive search { name: { $regex: /alice/i } } { name: { $regex: 'alice', $options: 'i' } } ``` ## Nested Fields Query nested fields using dot notation: ```typescript // Nested object field { 'profile.bio': { $exists: true } } { 'address.city': 'New York' } // Array element field { 'comments.0.author': 'Alice' } // Any array element { 'comments.author': 'Alice' } ``` ## MonchFilter Type Monch uses a relaxed filter type (`MonchFilter`) that works better with Zod-inferred types: ```typescript // All of these work without type casting: Users.find({ email: 'test@example.com' }); // Known field Users.find({ createdAt: { $gte: new Date() } }); // Timestamp field Users.find({ 'profile.name': 'John' }); // Nested path Users.find({ $or: [{ status: 'active' }, { role: 'admin' }] }); ``` You typically don't need to import `MonchFilter` directly—all collection methods use it internally. ## Performance Tips 1. **Use indexes** for frequently queried fields 2. **Limit results** to avoid loading too much data 3. **Project only needed fields** to reduce transfer size 4. **Use `exists()` instead of `count() > 0`** for existence checks 5. **Use `estimatedDocumentCount()`** when you don't need exact counts --- --- url: /guide/schema.md --- # Schema Definition Monch schemas are built with Zod, giving you powerful validation with full TypeScript inference. ## Basic Schema ```typescript import { collection, field } from '@codician-team/monch'; const Users = collection({ name: 'users', schema: { _id: field.id(), // Auto-generating ObjectId name: field.string(), // Required string email: field.email(), // Email validation age: field.number().optional(), // Optional number }, }); ``` ## Schema Options ### Timestamps Add automatic `createdAt` and `updatedAt` fields: ```typescript const Posts = collection({ name: 'posts', schema: { /* ... */ }, timestamps: true, }); // Documents now include: // - createdAt: Date (set on insert) // - updatedAt: Date (set on insert, updated on every update) ``` ### Indexes Define indexes for better query performance: ```typescript const Users = collection({ name: 'users', schema: { /* ... */ }, indexes: [ // Simple index { key: { email: 1 } }, // Unique index { key: { email: 1 }, unique: true }, // Compound index { key: { role: 1, createdAt: -1 } }, // Text index for search { key: { name: 'text', bio: 'text' } }, // TTL index (auto-delete after 24 hours) { key: { expiresAt: 1 }, expireAfterSeconds: 86400 }, // Sparse index (only indexes documents with the field) { key: { optionalField: 1 }, sparse: true }, // Partial filter (only index active users) { key: { email: 1 }, partialFilterExpression: { status: 'active' }, }, ], }); ``` Indexes are created automatically on first connection. Disable with `createIndexes: false`. ### Connection Options ```typescript const Users = collection({ name: 'users', schema: { /* ... */ }, // Option 1: Explicit URI uri: 'mongodb://localhost:27017/myapp', // Option 2: Existing client client: existingMongoClient, database: 'myapp', // Option 3: Existing Db instance db: existingDbInstance, // Option 4: Auto-connect (default) // Uses MONGODB_URI environment variable }); ``` ## Nested Objects Define complex nested structures: ```typescript const Users = collection({ name: 'users', schema: { _id: field.id(), name: field.string(), profile: field.object({ bio: field.string().max(500).optional(), avatar: field.url().optional(), social: field.object({ twitter: field.string().optional(), github: field.string().optional(), }).optional(), }), settings: field.object({ notifications: field.boolean().default(true), theme: field.enum(['light', 'dark', 'system']).default('system'), }), }, }); ``` ## Arrays ```typescript const Posts = collection({ name: 'posts', schema: { _id: field.id(), title: field.string(), tags: field.array(field.string()), // string[] comments: field.array(field.object({ author: field.string(), text: field.string(), createdAt: field.datetime(), })), }, }); ``` ## Enums ```typescript const Orders = collection({ name: 'orders', schema: { _id: field.id(), status: field.enum(['pending', 'processing', 'shipped', 'delivered']), priority: field.enum(['low', 'medium', 'high']).default('medium'), }, }); ``` ## Defaults ```typescript const Posts = collection({ name: 'posts', schema: { _id: field.id(), title: field.string(), // Static default status: field.enum(['draft', 'published']).default('draft'), // Dynamic default (function) slug: field.string().default(() => generateSlug()), // Optional with default views: field.number().default(0), }, }); ``` ## Transforms Apply transformations during validation: ```typescript const Users = collection({ name: 'users', schema: { _id: field.id(), // Trim whitespace name: field.string().trim(), // Lowercase email: field.email().toLowerCase(), // Custom transform username: field.string() .transform((val) => val.toLowerCase().replace(/\s+/g, '_')), }, }); ``` ## Refinements Add custom validation logic: ```typescript const Users = collection({ name: 'users', schema: { _id: field.id(), password: field.string() .min(8) .refine( (val) => /[A-Z]/.test(val) && /[0-9]/.test(val), { message: 'Password must contain uppercase letter and number' } ), confirmPassword: field.string(), }, }); ``` ## Using Zod Directly The `field` helper is just Zod with extensions. You can use Zod directly: ```typescript import { collection, z } from '@codician-team/monch'; const Users = collection({ name: 'users', schema: { _id: z.string().transform((val) => new ObjectId(val)), name: z.string().min(1).max(100), email: z.string().email(), metadata: z.record(z.string(), z.any()), // Record type tags: z.set(z.string()), // Set type }, }); ``` ## Runtime Schema Access Access the schema at runtime for validation or introspection: ```typescript const Users = collection({ name: 'users', schema: { _id: field.id(), name: field.string(), email: field.email(), }, }); // Access runtime schema const schema = Users.$schema; // Validate data manually const result = schema.safeParse({ name: 'Alice', email: 'invalid' }); if (!result.success) { console.log(result.error.issues); } // Use in other Zod operations const PartialUser = schema.partial(); const UserWithExtra = schema.extend({ extra: z.string() }); ``` --- --- url: /guide/serialization.md --- # Serialization MongoDB documents contain BSON types (`ObjectId`, `Date`, `Long`, etc.) that can't be passed directly to Client Components in Next.js or serialized to JSON. Monch provides built-in serialization to handle this. ## Automatic Serialization (Express, Fastify, etc.) For Express and other frameworks, Monch documents have a `toJSON()` method that's called automatically by `JSON.stringify()` and `res.json()`: ```typescript // Express - works automatically! app.get('/api/users', async (req, res) => { const users = await Users.find({}).toArray(); res.json(users); // toJSON() called automatically }); app.get('/api/users/:id', async (req, res) => { const user = await Users.findById(req.params.id); res.json(user); // toJSON() called automatically }); ``` No manual serialization needed - BSON types are converted to JSON-safe values automatically. ## Next.js (Manual Serialization Required) Next.js blocks objects with `toJSON` at the Server/Client boundary. Use `.serialize()` explicitly: ```typescript // Server Component const user = await Users.findOne({ email: 'alice@example.com' }); // This fails! Next.js blocks toJSON return ; // ❌ Error // Use .serialize() for Next.js const plain = user.serialize(); return ; // ✅ Works ``` ## Single Document ```typescript const user = await Users.findOne({ email: 'alice@example.com' }); // Before serialization console.log(user._id); // ObjectId('507f1f77bcf86cd799439011') console.log(user.createdAt); // Date object // After serialization const plain = user.serialize(); console.log(plain._id); // '507f1f77bcf86cd799439011' (string) console.log(plain.createdAt); // '2024-01-15T10:30:00.000Z' (ISO string) ``` ## Array of Documents ### Method 1: Serialize on Cursor (Recommended) ```typescript const users = await Users.find({ role: 'admin' }).serialize(); // Returns Serialized[] - already serialized ``` ### Method 2: Serialize After toArray ```typescript const users = await Users.find({ role: 'admin' }).toArray(); const plain = users.serialize(); // MonchArray has a .serialize() method ``` Both methods work. The first is more concise. ## Type-Safe Serialization ```typescript import { type Serialized, type ModelOf, type SerializedOf } from '@codician-team/monch'; // Document type type User = ModelOf; // { _id: ObjectId, name: string, email: string, createdAt: Date, updatedAt: Date } // Serialized type (automatic conversion) type SerializedUser = SerializedOf; // { _id: string, name: string, email: string, createdAt: string, updatedAt: string } // Or manually type ManualSerialized = Serialized; // Use in components interface UserCardProps { user: SerializedUser; // Type-safe serialized user } ``` ## Conversion Table | BSON Type | Serialized To | Example | |-----------|---------------|---------| | `ObjectId` | `string` (hex) | `'507f1f77bcf86cd799439011'` | | `Date` | `string` (ISO 8601) | `'2024-01-15T10:30:00.000Z'` | | `Long` | `number` or `bigint`\* | `9007199254740991` | | `Decimal128` | `string` | `'99999999999999.99'` | | `Binary` | `string` (base64) | `'aGVsbG8='` | | `Timestamp` | `{ t: number, i: number }` | `{ t: 1234567890, i: 1 }` | \*`Long` serializes to `number` when within JavaScript's safe integer range (±9,007,199,254,740,991). Values outside this range serialize to `bigint` to preserve precision. ## Standalone Serialization You can serialize data without fetching from a collection: ```typescript import { serialize, serializeArray } from '@codician-team/monch'; // Single document const plain = serialize(document); // Array of documents const plainArray = serializeArray(documents); ``` ## MonchArray When you call `.toArray()`, Monch returns a `MonchArray` instead of a regular array. This is an array subclass with a `.serialize()` method: ```typescript const users = await Users.find({ role: 'admin' }).toArray(); // It's still an array users.length; // Works users.map(...); // Works users.filter(...); // Works // But with extra functionality users.serialize(); // Serialize all documents ``` ## Next.js Integration ### Server Component to Client Component ```typescript // app/users/page.tsx (Server Component) import { Users } from '@/lib/models'; import { UserList } from './UserList'; export default async function UsersPage() { const users = await Users.find({ status: 'active' }) .sort({ createdAt: -1 }) .limit(20) .serialize(); // Serialize here return ; } ``` ```typescript // app/users/UserList.tsx (Client Component) 'use client'; import type { SerializedUser } from '@/lib/models'; interface Props { users: SerializedUser[]; } export function UserList({ users }: Props) { return (
    {users.map((user) => (
  • {user.name}
  • ))}
); } ``` ### Server Actions ```typescript // app/actions/user.actions.ts 'use server'; import { Users } from '@/lib/models'; export async function getUser(id: string) { const user = await Users.findById(id); return user?.serialize() ?? null; } export async function searchUsers(query: string) { const users = await Users.find({ $text: { $search: query }, }).serialize(); return users; } ``` ### Pagination with Serialization ```typescript export async function getUsers(page: number) { const result = await Users.find({ status: 'active' }) .sort({ createdAt: -1 }) .paginate({ page, limit: 20 }); return { ...result, data: result.data.map(user => user.serialize()), }; } ``` ## When to Serialize | Scenario | Method | Notes | |----------|--------|-------| | Express `res.json()` | Automatic | `toJSON()` called by `JSON.stringify()` | | Fastify response | Automatic | `toJSON()` called by `JSON.stringify()` | | Next.js Client Component | `.serialize()` | Next.js blocks `toJSON` | | Next.js Server Action | `.serialize()` | Next.js blocks `toJSON` | | `JSON.stringify()` | Automatic | `toJSON()` called automatically | | Server-only processing | None | Keep BSON types | | Storing back to MongoDB | None | Keep BSON types | ## Common Patterns ### Optional Chaining ```typescript const user = await Users.findOne({ email }); return user?.serialize() ?? null; ``` ### Conditional Serialization ```typescript async function getUser(id: string, serialize = true) { const user = await Users.findById(id); if (!user) return null; return serialize ? user.serialize() : user; } ``` ### Map with Serialization ```typescript const users = await Users.find({ role: 'admin' }).toArray(); const enriched = users.map(user => ({ ...user.serialize(), displayName: `${user.name} (${user.role})`, })); ``` --- --- url: /api/serialization.md --- # Serialization API Functions and types for converting BSON documents to JSON-safe values. ## Import ```typescript import { serialize, serializeArray, MonchArray } from '@codician-team/monch'; import type { Serialized, WithSerialize } from '@codician-team/monch'; ``` ## Automatic vs Manual Serialization Monch documents have both `toJSON()` and `serialize()` methods: | Method | When Called | Use Case | |--------|-------------|----------| | `toJSON()` | Automatically by `JSON.stringify()`, `res.json()` | Express, Fastify, etc. | | `serialize()` | Manually | Next.js (blocks `toJSON`) | ```typescript // Express - automatic (toJSON called by res.json) app.get('/api/user', async (req, res) => { const user = await Users.findById(id); res.json(user); // Just works! }); // Next.js - manual (serialize required) const user = await Users.findById(id); return ; ``` ## serialize() Serialize a single document: ```typescript function serialize(doc: T): Serialized ``` ### Example ```typescript const user = await Users.findOne({ email: 'alice@example.com' }); const plain = serialize(user); // Before: { _id: ObjectId('...'), createdAt: Date } // After: { _id: '...', createdAt: '2024-01-15T...' } ``` ## serializeArray() Serialize an array of documents: ```typescript function serializeArray(docs: T[]): Serialized[] ``` ### Example ```typescript const users = await Users.find().toArray(); const plain = serializeArray(users); ``` ## MonchArray Array subclass with built-in serialization methods: ```typescript class MonchArray extends Array { serialize(): Serialized[]; // Manual serialization toJSON(): Serialized[]; // Called by JSON.stringify() } ``` Returned by `.toArray()` on cursors: ```typescript const users = await Users.find().toArray(); // users: MonchArray users.length; // Works (it's an array) users.map(...); // Works users.filter(...); // Works users.serialize(); // Manual: Serialize all documents JSON.stringify(users); // Automatic: toJSON() called ``` ### Express Example ```typescript app.get('/api/users', async (req, res) => { const users = await Users.find({}).toArray(); res.json(users); // toJSON() serializes automatically }); ``` ## Document Methods Documents returned from queries have both `.serialize()` and `.toJSON()` methods: ```typescript const user = await Users.findOne({ email: 'alice@example.com' }); // Manual serialization (for Next.js) const plain = user?.serialize(); // Automatic serialization (for Express, JSON.stringify) res.json(user); // toJSON() called automatically ``` These methods are available on documents from: * `findOne()` * `findById()` * `insertOne()` * `insertMany()` (each document) * `updateOne()` * `findOneAndUpdate()` * `findOneAndDelete()` * `findOneAndReplace()` * `find().toArray()` (each document) ## Cursor .serialize() Method Execute query and serialize in one step: ```typescript const users = await Users.find({ role: 'admin' }).serialize(); // Returns Serialized[] directly ``` ## Types ### Serialized\ Type that represents the serialized version of a document: ```typescript type User = { _id: ObjectId; name: string; createdAt: Date; }; type SerializedUser = Serialized; // { // _id: string; // name: string; // createdAt: string; // } ``` ### WithSerialize\ Type for objects with serialization methods: ```typescript type WithSerialize = T & { serialize(): Serialized; // Manual (Next.js) toJSON(): Serialized; // Automatic (Express, JSON.stringify) }; ``` ## Conversion Table | BSON Type | Serialized To | Example | |-----------|---------------|---------| | `ObjectId` | `string` (hex) | `'507f1f77bcf86cd799439011'` | | `Date` | `string` (ISO 8601) | `'2024-01-15T10:30:00.000Z'` | | `Long` | `number` or `bigint`\* | `9007199254740991` | | `Decimal128` | `string` | `'99999999999999.99'` | | `Binary` | `string` (base64) | `'aGVsbG8='` | | `Timestamp` | `{ t: number, i: number }` | `{ t: 1234567890, i: 1 }` | | Nested objects | Recursively serialized | - | | Arrays | Each element serialized | - | \*`Long` values serialize to `number` when within JavaScript's safe integer range (±9,007,199,254,740,991). Values outside this range serialize to `bigint`. ## Usage Patterns ### Single Document ```typescript const user = await Users.findOne({ email: 'alice@example.com' }); return user?.serialize() ?? null; ``` ### Array of Documents ```typescript // Method 1: On cursor const users = await Users.find({ role: 'admin' }).serialize(); // Method 2: After toArray const users = await Users.find({ role: 'admin' }).toArray(); const plain = users.serialize(); ``` ### With Pagination ```typescript const result = await Users.find() .sort({ createdAt: -1 }) .paginate({ page: 1, limit: 20 }); return { ...result, data: result.data.map(user => user.serialize()), }; ``` ### Conditional Serialization ```typescript async function getUser(id: string, serialize = true) { const user = await Users.findById(id); if (!user) return null; return serialize ? user.serialize() : user; } ``` ### Enriched Serialization ```typescript const user = await Users.findById(id); return user ? { ...user.serialize(), fullName: `${user.firstName} ${user.lastName}`, isActive: user.status === 'active', } : null; ``` --- --- url: /guide/statics.md --- # Static Methods Add custom methods to the collection itself for reusable queries and operations. ## Defining Statics ```typescript const Users = collection({ name: 'users', schema: { _id: field.id(), name: field.string(), email: field.email(), role: field.enum(['user', 'admin', 'moderator']), status: field.enum(['active', 'inactive', 'banned']), }, statics: { // First argument (col) is auto-injected (the collection instance) findByEmail: (col, email: string) => { return col.findOne({ email }); }, findAdmins: (col) => { return col.find({ role: 'admin' }).toArray(); }, countByRole: async (col) => { const results = await col.aggregate([ { $group: { _id: '$role', count: { $sum: 1 } } }, ]).toArray(); return Object.fromEntries( results.map((r) => [r._id, r.count]) ); }, // Complex queries with multiple parameters search: (col, query: string, options?: { role?: string; limit?: number }) => { const filter: any = { $text: { $search: query } }; if (options?.role) filter.role = options.role; return col.find(filter) .limit(options?.limit ?? 20) .toArray(); }, }, }); ``` ## Using Statics Static methods are called directly on the collection: ```typescript // Find by email const user = await Users.findByEmail('alice@example.com'); // Get all admins const admins = await Users.findAdmins(); // Count by role const stats = await Users.countByRole(); // { user: 150, admin: 5, moderator: 10 } // Search with options const results = await Users.search('alice', { role: 'user', limit: 10 }); ``` ## Type Safety Static methods are fully typed: ```typescript const Users = collection({ name: 'users', schema: { _id: field.id(), email: field.email(), status: field.enum(['active', 'pending', 'banned']), }, statics: { findActiveByEmail: async (col, email: string) => { return col.findOne({ email, status: 'active' }); }, bulkActivate: async (col, ids: string[]) => { const objectIds = ids.map(id => new ObjectId(id)); return col.updateMany( { _id: { $in: objectIds } }, { $set: { status: 'active' } } ); }, }, }); // TypeScript knows the return types const user = await Users.findActiveByEmail('alice@example.com'); // user: User | null const count = await Users.bulkActivate(['id1', 'id2', 'id3']); // count: number ``` ## Common Patterns ### Query Builders ```typescript statics: { findActive: (col) => col.find({ status: 'active' }), findRecent: (col, days = 7) => { const since = new Date(); since.setDate(since.getDate() - days); return col.find({ createdAt: { $gte: since } }); }, findByRole: (col, role: string) => col.find({ role }), } // Usage - returns cursor for chaining const users = await Users.findActive() .sort({ createdAt: -1 }) .limit(10) .toArray(); ``` ### Aggregation Helpers ```typescript statics: { getStats: async (col) => { const [result] = await col.aggregate([ { $group: { _id: null, total: { $sum: 1 }, active: { $sum: { $cond: [{ $eq: ['$status', 'active'] }, 1, 0] } }, avgAge: { $avg: '$age' }, }, }, ]).toArray(); return result; }, getMonthlySignups: async (col) => { return col.aggregate([ { $group: { _id: { year: { $year: '$createdAt' }, month: { $month: '$createdAt' }, }, count: { $sum: 1 }, }, }, { $sort: { '_id.year': -1, '_id.month': -1 } }, ]).toArray(); }, } ``` ### Business Logic ```typescript statics: { createWithDefaults: async (col, data: { name: string; email: string }) => { return col.insertOne({ ...data, role: 'user', status: 'pending', settings: { notifications: true, theme: 'light', }, }); }, deactivateInactiveUsers: async (col, inactiveDays = 90) => { const cutoff = new Date(); cutoff.setDate(cutoff.getDate() - inactiveDays); return col.updateMany( { lastLoginAt: { $lt: cutoff }, status: 'active' }, { $set: { status: 'inactive' } } ); }, mergeAccounts: async (col, primaryId: string, secondaryId: string) => { return col.transaction(async (session) => { const secondary = await col.findById(secondaryId); if (!secondary) throw new Error('Secondary account not found'); // Merge data into primary await col.updateOne( { _id: primaryId }, { $addToSet: { mergedEmails: secondary.email }, $inc: { points: secondary.points }, }, { session } ); // Delete secondary await col.deleteOne({ _id: secondaryId }, { session }); return col.findById(primaryId); }); }, } ``` ### Pagination Helpers ```typescript statics: { paginated: async ( col, filter: object, options: { page?: number; limit?: number; sort?: object } = {} ) => { const { page = 1, limit = 20, sort = { createdAt: -1 } } = options; return col.find(filter) .sort(sort) .paginate({ page, limit }); }, searchPaginated: async (col, query: string, page = 1) => { return col.find({ $text: { $search: query } }) .sort({ score: { $meta: 'textScore' } }) .paginate({ page, limit: 20 }); }, } ``` ## Statics vs Methods | Feature | Static Methods | Instance Methods | |---------|---------------|------------------| | Called on | Collection | Document | | First arg | Collection (auto-injected) | Document (auto-injected) | | Use case | Queries, aggregations | Document operations | | Example | `Users.findByEmail(email)` | `user.fullName()` | Use static methods for: * Custom queries and filters * Aggregation pipelines * Bulk operations * Business logic involving multiple documents Use instance methods for: * Computed properties * Single document operations * Document-specific logic ## Accessing Raw Collection Static methods receive the Monch collection, but you can access the raw MongoDB collection if needed: ```typescript statics: { rawAggregate: async (col, pipeline: object[]) => { const raw = await col.collection; // Raw MongoDB collection return raw.aggregate(pipeline).toArray(); }, } ``` --- --- url: /guide/transactions.md --- # Transactions Execute multiple operations atomically—either all succeed or all fail. ## Basic Usage ```typescript await Users.transaction(async (session) => { // All operations must include { session } const user = await Users.insertOne( { name: 'Alice', email: 'alice@example.com' }, { session } ); await Posts.insertOne( { title: 'First Post', authorId: user._id }, { session } ); // If any operation fails, all are rolled back }); ``` ## Requirements ::: warning Replica Set Required MongoDB must be running as a replica set for transactions to work. This includes: * MongoDB Atlas (all clusters) * Local MongoDB with `--replSet` option * Docker with replica set configuration Single-node deployments without replica set configuration do not support transactions. ::: ## Shared Client Requirement All collections in a transaction must share the same MongoDB client: ```typescript import { MongoClient } from 'mongodb'; const client = new MongoClient('mongodb://localhost:27017'); const Users = collection({ name: 'users', schema: { /* ... */ }, client, database: 'myapp', }); const Posts = collection({ name: 'posts', schema: { /* ... */ }, client, // Same client database: 'myapp', }); // Now transactions work across both collections await Users.transaction(async (session) => { await Users.insertOne({ ... }, { session }); await Posts.insertOne({ ... }, { session }); }); ``` ## Return Values Transactions can return values: ```typescript const result = await Users.transaction(async (session) => { const user = await Users.insertOne( { name: 'Alice', email: 'alice@example.com' }, { session } ); const post = await Posts.insertOne( { title: 'First Post', authorId: user._id }, { session } ); return { user, post }; // Return created documents }); console.log(result.user._id); console.log(result.post._id); ``` ## Error Handling If any operation throws, the entire transaction is rolled back: ```typescript try { await Users.transaction(async (session) => { await Users.insertOne( { name: 'Alice', email: 'alice@example.com' }, { session } ); // This will fail and roll back the user insert throw new Error('Something went wrong'); }); } catch (error) { console.log('Transaction failed:', error.message); // The user was NOT inserted } ``` ## Common Patterns ### Transfer Money ```typescript async function transferMoney( fromId: string, toId: string, amount: number ) { await Accounts.transaction(async (session) => { // Debit source account const from = await Accounts.updateOne( { _id: fromId, balance: { $gte: amount } }, { $inc: { balance: -amount } }, { session } ); if (!from) { throw new Error('Insufficient funds'); } // Credit destination account await Accounts.updateOne( { _id: toId }, { $inc: { balance: amount } }, { session } ); // Log the transfer await Transfers.insertOne( { from: fromId, to: toId, amount, timestamp: new Date(), }, { session } ); }); } ``` ### Create User with Related Data ```typescript async function createUserWithProfile(data: UserInput) { return await Users.transaction(async (session) => { const user = await Users.insertOne( { name: data.name, email: data.email }, { session } ); await Profiles.insertOne( { userId: user._id, bio: data.bio, avatar: data.avatar, }, { session } ); await Settings.insertOne( { userId: user._id, notifications: true, theme: 'light', }, { session } ); return user; }); } ``` ### Atomic Counter with History ```typescript async function incrementCounter(name: string) { return await Counters.transaction(async (session) => { const counter = await Counters.updateOne( { name }, { $inc: { value: 1 } }, { session, upsert: true } ); await CounterHistory.insertOne( { counterName: name, newValue: counter?.value ?? 1, timestamp: new Date(), }, { session } ); return counter?.value ?? 1; }); } ``` ## Best Practices 1. **Keep transactions short** - Long transactions hold locks and can cause contention 2. **Only use when necessary** - Transactions have overhead. Use them only when atomicity is required 3. **Handle retries** - Transient errors can occur. Consider retry logic for production: ```typescript async function withRetry( fn: () => Promise, maxRetries = 3 ): Promise { let lastError: Error; for (let i = 0; i < maxRetries; i++) { try { return await fn(); } catch (error) { lastError = error as Error; if (!isTransientError(error)) throw error; await sleep(Math.pow(2, i) * 100); // Exponential backoff } } throw lastError!; } // Usage await withRetry(() => Users.transaction(async (session) => { // ... operations }) ); ``` 4. **Avoid transactions for single operations** - A single `insertOne` or `updateOne` is already atomic ## Limitations * Maximum transaction runtime: 60 seconds (default) * Transactions can span multiple collections but must use the same client * Some operations don't support transactions (e.g., creating indexes) * Transactions increase memory usage on the server --- --- url: /api/types.md --- # Types TypeScript type definitions exported by Monch. ## Import ```typescript import type { // Schema types Schema, Doc, Input, // Collection types Collection, CollectionConfig, CollectionInstance, MonchDocument, // Query types MonchFilter, // Timestamp types TimestampFields, WithTimestamps, WithTS, // Method types Methods, Statics, WithMethods, // Hook types Hooks, // Index types Index, IndexOptions, IndexDirection, // Connection types ConnectionConfig, ConnectionResult, DebugConfig, OperationOptions, // Serialization types Serialized, WithSerialize, // Pagination types PaginationResult, PaginationOptions, // Money types MoneyValue, MoneyOptions, // Type extraction ModelOf, SerializedOf, SchemaOf, } from '@codician-team/monch'; ``` ## Schema Types ### Schema Schema definition type (record of Zod types): ```typescript type Schema = Record; ``` ### Doc\ Document type inferred from schema: ```typescript type Doc = z.infer>; ``` ### Input\ Input type for insertions (respects defaults/optionals): ```typescript type Input = z.input>; ``` ## Collection Types ### Collection\ Full collection type with schema, methods, and statics: ```typescript type Collection = CollectionInstance, M> & ST; ``` ### CollectionInstance\ Collection instance with all methods: ```typescript interface CollectionInstance { find(filter?: MonchFilter): Cursor; findOne(filter: MonchFilter): Promise | null>; findById(id: any): Promise | null>; insertOne(doc: Input, opts?: OperationOptions): Promise>; // ... all other methods } ``` ### MonchDocument\ Document with instance methods and `.serialize()`: ```typescript type MonchDocument = T & M & WithSerialize; ``` ### CollectionConfig\ Configuration for `collection()`: ```typescript interface CollectionConfig { name: string; schema: S; uri?: string; client?: MongoClient; db?: Db; database?: string; timestamps?: boolean; indexes?: Index[]; createIndexes?: boolean; hooks?: Hooks>; methods?: M; statics?: ST; } ``` ## Query Types ### MonchFilter\ Relaxed filter type for Zod compatibility: ```typescript type MonchFilter = Filter | Record; ``` Allows queries without strict type checking on nested paths and operators. ## Timestamp Types ### TimestampFields ```typescript interface TimestampFields { createdAt: Date; updatedAt: Date; } ``` ### WithTimestamps\ Add timestamp fields to a type: ```typescript type WithTimestamps = T & TimestampFields; ``` ### WithTS\ Alias for `WithTimestamps`: ```typescript type WithTS = WithTimestamps; ``` ## Method Types ### Methods\ Instance methods definition: ```typescript type Methods = Record any>; ``` ### Statics\ Static methods definition: ```typescript type Statics = Record, ...args: any[]) => any>; ``` ### WithMethods\ Document with methods attached: ```typescript type WithMethods = T & { [K in keyof M]: M[K] extends (doc: T, ...args: infer A) => infer R ? (...args: A) => R : never }; ``` ## Hook Types ### Hooks\ Lifecycle hooks definition: ```typescript interface Hooks { beforeValidate?: (doc: any) => any | Promise; afterValidate?: (doc: T) => T | Promise; beforeInsert?: (doc: T) => T | Promise; afterInsert?: (doc: T) => void | Promise; beforeUpdate?: (filter: any, update: any) => { filter: any; update: any } | Promise<{ filter: any; update: any }>; afterUpdate?: (doc: T | null) => void | Promise; beforeDelete?: (filter: any) => any | Promise; afterDelete?: (count: number) => void | Promise; } ``` ## Index Types ### Index ```typescript interface Index { key: Record; unique?: boolean; sparse?: boolean; background?: boolean; expireAfterSeconds?: number; partialFilterExpression?: object; name?: string; collation?: object; } ``` ### IndexDirection ```typescript type IndexDirection = 1 | -1 | 'text' | '2dsphere' | '2d' | 'hashed'; ``` ### IndexOptions ```typescript interface IndexOptions { unique?: boolean; sparse?: boolean; background?: boolean; expireAfterSeconds?: number; partialFilterExpression?: object; name?: string; collation?: object; } ``` ## Connection Types ### ConnectionConfig ```typescript interface ConnectionConfig { uri?: string; client?: MongoClient; db?: Db; database?: string; } ``` ### ConnectionResult ```typescript interface ConnectionResult { client: MongoClient; db: Db; } ``` ### DebugConfig ```typescript interface DebugConfig { enabled: boolean; logger?: (operation: string, collection: string, ...args: any[]) => void; } ``` ### OperationOptions ```typescript interface OperationOptions { session?: ClientSession; } ``` ## Serialization Types ### Serialized\ Serialized version of a document: ```typescript type Serialized = { [K in keyof T]: T[K] extends ObjectId ? string : T[K] extends Date ? string : T[K] extends Long ? number | bigint : T[K] extends Decimal128 ? string : T[K] extends Binary ? string : T[K] extends Timestamp ? { t: number; i: number } : T[K] extends object ? Serialized : T[K]; }; ``` ### WithSerialize\ Object with `.serialize()` method: ```typescript interface WithSerialize { serialize(): Serialized; } ``` ## Pagination Types ### PaginationResult\ ```typescript interface PaginationResult { data: T[]; pagination: { page: number; limit: number; total: number; totalPages: number; hasNext: boolean; hasPrev: boolean; }; } ``` ### PaginationOptions ```typescript interface PaginationOptions { page?: number; limit?: number; } ``` ## Money Types ### MoneyValue ```typescript interface MoneyValue { amount: number; currency: string; formatted: string; } ``` ### MoneyOptions ```typescript interface MoneyOptions { currency?: string; decimals?: number; } ``` ## Type Extraction Helpers ### ModelOf\ Extract document type from collection: ```typescript type ModelOf = C extends Collection ? Doc : never; ``` ### SerializedOf\ Extract serialized type from collection: ```typescript type SerializedOf = C extends Collection ? Serialized> : never; ``` ### SchemaOf\ Extract schema type from collection: ```typescript type SchemaOf = C extends Collection ? S : never; ``` --- --- url: /guide/typescript.md --- # TypeScript Types Monch provides comprehensive TypeScript support with full type inference from your schemas. ## Type Inference Types flow automatically from your Zod schema: ```typescript const Users = collection({ name: 'users', schema: { _id: field.id(), name: field.string(), email: field.email(), role: field.enum(['user', 'admin']).default('user'), }, timestamps: true, }); // TypeScript automatically infers: // - Document type includes all fields + timestamps // - Input type respects defaults/optionals // - Query results are typed // - Serialized output is typed ``` ## Extracting Types from Collections The recommended way to get types: ```typescript import { collection, field, type ModelOf, type SerializedOf } from '@codician-team/monch'; export const Users = collection({ name: 'users', schema: { _id: field.id(), name: field.string(), email: field.email(), }, timestamps: true, }); // Extract types from the collection (includes timestamps!) export type User = ModelOf; // { _id: ObjectId; name: string; email: string; createdAt: Date; updatedAt: Date } export type SerializedUser = SerializedOf; // { _id: string; name: string; email: string; createdAt: string; updatedAt: string } ``` ## Type Extraction Helpers | Type Helper | Description | |-------------|-------------| | `ModelOf` | Extract document type from collection (includes timestamps) | | `SerializedOf` | Extract serialized type from collection | | `SchemaOf` | Extract the raw schema type from collection | ## Schema Types Build types from schemas directly: ```typescript import { type Doc, type Input, type Serialized } from '@codician-team/monch'; const userSchema = { _id: field.id(), name: field.string(), email: field.email(), }; // Document type (what's in MongoDB) type User = Doc; // { _id: ObjectId; name: string; email: string } // Input type (what you pass to insertOne) type UserInput = Input; // { _id?: ObjectId | string; name: string; email: string } // Serialized type (after .serialize()) type SerializedUser = Serialized; // { _id: string; name: string; email: string } ``` ## Adding Timestamps Manually ```typescript import { type WithTimestamps } from '@codician-team/monch'; type User = Doc; type UserWithTimestamps = WithTimestamps; // { _id: ObjectId; name: string; email: string; createdAt: Date; updatedAt: Date } ``` ## MonchFilter Type For filter parameters that need to work with Zod-inferred types: ```typescript import { type MonchFilter } from '@codician-team/monch'; // MonchFilter is more permissive than MongoDB's Filter type function findUsers(filter: MonchFilter) { return Users.find(filter).toArray(); } // All of these work without type errors: findUsers({ email: 'test@example.com' }); findUsers({ createdAt: { $gte: new Date() } }); findUsers({ 'profile.name': 'John' }); findUsers({ $or: [{ status: 'active' }, { role: 'admin' }] }); ``` All collection methods use `MonchFilter` internally, so you typically don't need to import it directly. ## Importing Types ```typescript import { // Schema types type Doc, // Document type from schema type Input, // Input type (respects defaults/optionals) type Schema, // Schema definition type // Operation types type Filter, // MongoDB filter type (strict) type UpdateFilter, // MongoDB update type type MonchFilter, // Relaxed filter type (recommended) // Serialization type Serialized, // BSON -> plain JS conversion // Type extraction (from collections) type ModelOf, // Extract document type from collection type SerializedOf, // Extract serialized type from collection type SchemaOf, // Extract schema type from collection // Utilities type WithTimestamps, // Add createdAt/updatedAt to type } from '@codician-team/monch'; ``` ## Using Types in Components ```typescript // lib/models/user.model.ts import { collection, field, type ModelOf, type SerializedOf } from '@codician-team/monch'; export const Users = collection({ name: 'users', schema: { _id: field.id(), name: field.string(), email: field.email(), avatar: field.url().optional(), }, timestamps: true, }); export type User = ModelOf; export type SerializedUser = SerializedOf; ``` ```typescript // components/UserCard.tsx import type { SerializedUser } from '@/lib/models/user.model'; interface UserCardProps { user: SerializedUser; } export function UserCard({ user }: UserCardProps) { return (

{user.name}

{user.email}

{user.avatar && {user.name}} Joined {new Date(user.createdAt).toLocaleDateString()}
); } ``` ## Type-Safe Server Actions ```typescript // app/actions/user.actions.ts 'use server'; import { Users, type User, type SerializedUser } from '@/lib/models'; export async function getUser(id: string): Promise { const user = await Users.findById(id); return user?.serialize() ?? null; } export async function updateUser( id: string, data: Partial> ): Promise { const user = await Users.updateOne( { _id: id }, { $set: data } ); return user?.serialize() ?? null; } ``` ## Generic Functions Create type-safe generic functions: ```typescript import { type Collection, type ModelOf } from '@codician-team/monch'; async function findOrCreate>( collection: C, filter: object, defaults: Partial> ): Promise> { const existing = await collection.findOne(filter); if (existing) return existing; return collection.insertOne({ ...defaults, ...filter, } as any); } // Usage const user = await findOrCreate(Users, { email: 'alice@example.com' }, { name: 'Alice', }); ``` ## Runtime Schema Access Access the Zod schema at runtime for validation or introspection: ```typescript const Users = collection({ name: 'users', schema: { _id: field.id(), name: field.string(), email: field.email(), }, }); // Access runtime schema const schema = Users.$schema; // Validate data manually const result = schema.safeParse({ name: 'Alice', email: 'invalid' }); if (!result.success) { console.log(result.error.issues); } // Create derived schemas const PartialUser = schema.partial(); const UserWithExtra = schema.extend({ extra: z.string() }); ``` ## Collection Properties | Property | Type | Description | |----------|------|-------------| | `$schema` | `ZodObject` | Runtime-accessible Zod schema | | `$model` | `type only` | Document type (use with `typeof`) | | `$serialized` | `type only` | Serialized document type (use with `typeof`) | --- --- url: /guide/validation.md --- # Validation Monch validates documents at multiple points to catch errors early and ensure data integrity. ## Insert Validation Full Zod schema validation on `insertOne` and `insertMany`: ```typescript // Schema const Users = collection({ name: 'users', schema: { _id: field.id(), name: field.string().min(1), email: field.email(), age: field.number().min(0).optional(), }, }); // Valid insert const user = await Users.insertOne({ name: 'Alice', email: 'alice@example.com', }); // Invalid insert - throws MonchValidationError await Users.insertOne({ name: '', // Too short email: 'invalid', // Not an email }); ``` ## Update Validation Partial validation for `$set` and `$setOnInsert` operations: ```typescript // Only the email field is validated await Users.updateOne( { _id: user._id }, { $set: { email: 'invalid-email' } } // Throws MonchValidationError ); // Valid update await Users.updateOne( { _id: user._id }, { $set: { email: 'alice@newdomain.com' } } // Works ); ``` ::: info Partial Validation Only fields in `$set` and `$setOnInsert` are validated. Other update operators like `$inc`, `$push`, `$unset` are not validated. ::: ## Upsert Validation Monch uses different validation strategies for upserts depending on the operation type: ### updateOne / findOneAndUpdate (Partial Validation) Uses the same partial validation as regular updates - only validates fields in `$set` and `$setOnInsert`: ```typescript // This works - only validates the 'role' field being set await Users.updateOne( { email: 'new@example.com' }, { $set: { role: 'admin' } }, { upsert: true } ); // Invalid field values are still rejected await Users.updateOne( { email: 'new@example.com' }, { $set: { age: -5 } }, // Throws if age has min(0) constraint { upsert: true } ); ``` ::: warning MongoDB Responsibility With partial validation, MongoDB may create documents with missing required fields. This is intentional - `updateOne` upserts are designed for incremental updates where the document may be created with minimal fields and filled in later. ::: ### replaceOne / findOneAndReplace (Full Validation) Uses full document validation since you're replacing the entire document: ```typescript // This will throw because 'name' is required await Users.replaceOne( { email: 'new@example.com' }, { email: 'new@example.com', role: 'admin' }, { upsert: true } ); // Throws: "name" is required // Valid upsert - all required fields provided await Users.replaceOne( { email: 'new@example.com' }, { email: 'new@example.com', name: 'New User', role: 'admin' }, { upsert: true } ); ``` ## Replace Validation Full validation on `replaceOne` and `findOneAndReplace`: ```typescript // Full document validation await Users.replaceOne( { _id: user._id }, { name: 'Alice', email: 'alice@example.com' } ); ``` The replacement document must satisfy the entire schema. ## Bulk Write Validation Each operation in `bulkWrite` is validated according to its type: ```typescript await Users.bulkWrite([ // Full validation { insertOne: { document: { name: 'Bob', email: 'bob@example.com' } } }, // Partial validation { updateOne: { filter: { _id: id1 }, update: { $set: { name: 'Robert' } } } }, // Full validation { replaceOne: { filter: { _id: id2 }, replacement: { name: 'Carol', email: 'carol@example.com' } } }, ]); ``` ::: warning No Hooks `bulkWrite` uses Zod-only validation and does **not** trigger any lifecycle hooks (`beforeValidate`, `afterValidate`, `beforeInsert`, etc.). ::: ## Error Handling Validation errors throw `MonchValidationError`: ```typescript import { MonchValidationError } from '@codician-team/monch'; try { await Users.insertOne({ name: '', email: 'invalid' }); } catch (error) { if (error instanceof MonchValidationError) { // Raw Zod issues console.log(error.issues); // [ // { path: ['name'], message: 'String must contain at least 1 character(s)', code: 'too_small' }, // { path: ['email'], message: 'Invalid email', code: 'invalid_string' } // ] // Human-readable format console.log(error.formatIssues()); // - name: String must contain at least 1 character(s) // - email: Invalid email // Form-friendly errors console.log(error.toFormErrors()); // { name: 'String must contain at least 1 character(s)', email: 'Invalid email' } // Quick access console.log(error.firstError); // 'String must contain at least 1 character(s)' console.log(error.firstField); // 'name' } } ``` ### The `_root` Key When `toFormErrors()` or `firstField` encounter top-level validation errors (errors with empty path), they use `_root` as the key: ```typescript // Schema with top-level refinement const schema = z.object({ password: z.string(), confirmPassword: z.string(), }).refine( (data) => data.password === data.confirmPassword, { message: 'Passwords must match' } ); // If refinement fails: error.toFormErrors(); // { _root: 'Passwords must match' } error.firstField; // '_root' ``` ## Validation with Hooks Validation hooks let you transform data before and after Zod validation: ```typescript const Posts = collection({ name: 'posts', schema: { _id: field.id(), title: field.string(), slug: field.string(), }, hooks: { // Transform BEFORE Zod validation beforeValidate: (doc) => ({ ...doc, slug: doc.title.toLowerCase().replace(/\s+/g, '-'), }), // Transform AFTER Zod validation (has access to defaults) afterValidate: (doc) => ({ ...doc, searchIndex: `${doc.title} ${doc.slug}`.toLowerCase(), }), }, }); ``` See [Lifecycle Hooks](/guide/hooks) for more details. ## Manual Validation You can validate data manually using the runtime schema: ```typescript const Users = collection({ name: 'users', schema: { _id: field.id(), name: field.string(), email: field.email(), }, }); // Access runtime schema const schema = Users.$schema; // Safe parse (returns result object) const result = schema.safeParse({ name: 'Alice', email: 'invalid' }); if (!result.success) { console.log(result.error.issues); } // Parse (throws on error) try { const data = schema.parse({ name: 'Alice', email: 'alice@example.com' }); } catch (error) { // ZodError } ``` ## Bypassing Validation In rare cases where you need to bypass validation, you can access the raw MongoDB collection: ```typescript const raw = await Users.collection; await raw.insertOne({ /* unvalidated data */ }); ``` ::: danger Use With Caution Bypassing validation can lead to invalid data in your database. Only use this for migrations, bulk imports, or other special cases where you've validated data externally. ::: --- --- url: /guide/introduction.md --- # What is Monch? Monch is a lightweight MongoDB ODM (Object Document Mapper) for TypeScript that combines the power of [Zod](https://zod.dev) validation with MongoDB operations. It provides a type-safe, developer-friendly way to work with MongoDB while ensuring data integrity through schema validation. ## Why Monch? ### The Problem Working with MongoDB in TypeScript often involves: * Manual type definitions that can drift from actual data * No validation at the database boundary * Tedious BSON serialization for frameworks like Next.js * Boilerplate for common patterns like timestamps and pagination ### The Solution Monch addresses these issues with: * **Zod Schemas as Source of Truth** - Your schema defines both validation rules and TypeScript types * **Automatic Validation** - Documents are validated before insert, with partial validation on updates * **Built-in Serialization** - Convert BSON types to JSON-safe values with a single `.serialize()` call * **Zero Configuration** - Set `MONGODB_URI` and start coding ## Core Principles ### 1. Validation at the Boundary Monch validates data at the database boundary—when documents enter MongoDB. This catches errors early and ensures data consistency: ```typescript // Invalid data is rejected before reaching MongoDB await Users.insertOne({ name: '', // Throws: String must contain at least 1 character email: 'not-an-email', // Throws: Invalid email }); ``` ### 2. Type Safety Throughout Types flow from your Zod schema through queries to serialized output: ```typescript const Users = collection({ schema: { _id: field.id(), name: field.string(), email: field.email(), }, timestamps: true, }); // TypeScript knows the shape const user = await Users.findOne({ email: 'alice@example.com' }); // user: { _id: ObjectId, name: string, email: string, createdAt: Date, updatedAt: Date } | null const serialized = user?.serialize(); // serialized: { _id: string, name: string, email: string, createdAt: string, updatedAt: string } ``` ### 3. Progressive Enhancement Monch stays out of your way. Use as much or as little as you need: ```typescript // Basic: Just schema and name const Simple = collection({ name: 'simple', schema: { _id: field.id(), value: field.string() }, }); // Full-featured: Timestamps, hooks, methods, indexes const Advanced = collection({ name: 'advanced', schema: { /* ... */ }, timestamps: true, indexes: [{ key: { email: 1 }, unique: true }], hooks: { beforeInsert: (doc) => ({ ...doc, normalized: true }) }, methods: { fullName: (doc) => `${doc.first} ${doc.last}` }, }); ``` ## Comparison | Feature | Monch | Mongoose | Prisma | |---------|-------|----------|--------| | TypeScript-first | ✅ | Partial | ✅ | | Zod validation | ✅ | ❌ | ❌ | | Zero config | ✅ | ❌ | ❌ | | Serialization helpers | ✅ | ❌ | ❌ | | Schema file required | ❌ | ❌ | ✅ | | Code generation | ❌ | ❌ | ✅ | | Bundle size | ~15KB | ~800KB | ~2MB | ## Next Steps * [Getting Started](/guide/getting-started) - Install and create your first collection * [Schema Definition](/guide/schema) - Learn about defining schemas * [CRUD Operations](/guide/crud) - Query, insert, update, and delete