# Monch - AI Assistant Guide Monch is a foundational MongoDB data access and validation library for TypeScript. It provides primitives that work anywhere in your codebase - import and use wherever you need MongoDB operations. ## Installation ```bash npm install @codician-team/monch mongodb zod ``` ## Core Concepts ### 1. Collection Definition ```typescript import { collection, field } from '@codician-team/monch'; const Users = collection({ name: 'users', // MongoDB collection name schema: { // Zod schema for validation _id: field.id(), name: field.string(), email: field.email(), role: field.enum(['admin', 'user']).default('user'), }, timestamps: true, // Adds createdAt/updatedAt }); ``` ### 2. Connection (Auto or Explicit) ```typescript // Auto-connect: Set MONGODB_URI environment variable // Collections connect lazily on first operation // Or explicit: const Users = collection({ name: 'users', schema: { ... }, uri: 'mongodb://localhost:27017/myapp', }); ``` ## Field Types ### Standard Zod Types - `field.string()`, `field.number()`, `field.boolean()`, `field.date()` - `field.array(field.string())`, `field.object({ ... })` - `field.enum(['a', 'b'])`, `field.union([...])` - All support `.optional()`, `.nullable()`, `.default(value)`, `.transform(fn)` ### MongoDB Types - `field.id()` - ObjectId with auto-generation - `field.objectId()` - ObjectId reference (no auto-generate) - `field.uuid()` - UUID string with auto-generation - `field.email()` - Email validation - `field.url()` - URL validation - `field.datetime()` - Date with auto-generation ### BSON Types - `field.long()` - 64-bit integer (accepts Long, bigint, number, string) - `field.decimal()` - Decimal128 (accepts Decimal128, number, string) - `field.binary()` - Binary data (accepts Binary, Buffer, Uint8Array) - `field.timestamp()` - MongoDB Timestamp ### Money Type ```typescript field.money() // Default USD field.money({ currency: 'EUR' }) // Euro field.money({ min: 0, max: 1000 }) // Constraints field.money({ allowNegative: true }) // Allow negatives // Stored as { amount: Decimal128, currency: string } ``` ## CRUD Operations ### Insert ```typescript const user = await Users.insertOne({ name: 'Alice', email: 'alice@example.com', }); const users = await Users.insertMany([ { name: 'Bob', email: 'bob@example.com' }, { name: 'Carol', email: 'carol@example.com' }, ]); ``` ### Read ```typescript const user = await Users.findOne({ email: 'alice@example.com' }); const user = await Users.findById(userId); const users = await Users.find({ role: 'admin' }).toArray(); const count = await Users.count({ status: 'active' }); const exists = await Users.exists({ email: 'test@example.com' }); ``` ### Update ```typescript const updated = await Users.updateOne( { _id: userId }, { $set: { name: 'Alicia' } } ); const count = await Users.updateMany( { status: 'pending' }, { $set: { status: 'active' } } ); ``` ### Delete ```typescript await Users.deleteOne({ _id: userId }); const count = await Users.deleteMany({ status: 'inactive' }); ``` ## Cursor Operations ```typescript const results = await Users.find({ status: 'active' }) .sort({ createdAt: -1 }) .skip(20) .limit(10) .project({ name: 1, email: 1 }) .toArray(); // Async iteration for await (const user of Users.find({ role: 'admin' })) { console.log(user.name); } ``` ## Pagination ```typescript const result = await Users.find({ status: 'active' }) .sort({ createdAt: -1 }) .paginate({ page: 1, limit: 20 }); // Returns: // { // data: [...], // total: 150, // page: 1, // limit: 20, // pages: 8, // hasNext: true, // hasPrev: false, // } ``` ## Money Utilities ```typescript import { Money } from '@codician-team/monch'; // Formatting Money.format(price); // '$99.99' Money.toCents(price); // 9999 (for Stripe) Money.fromCents(9999, 'USD'); // MoneyValue // Arithmetic Money.add(price, 10); Money.subtract(price, 5); Money.multiply(price, 1.1); Money.percentage(price, 15); // 15% of price // Comparison Money.equals(a, b); Money.greaterThan(a, b); Money.isZero(price); Money.isNegative(price); // Aggregation Money.sum([price1, price2, price3]); Money.split(total, 3); // Split evenly Money.allocate(total, [2, 1, 1]); // Split by ratios ``` ## Serialization ```typescript // For APIs and client components const user = await Users.findOne({ _id: userId }); const plain = user.serialize(); // { _id: '507f...', name: 'Alice', createdAt: '2024-01-15T12:00:00.000Z' } // Or with cursor const users = await Users.find({}).serialize(); // JSON.stringify works automatically res.json(user); // Calls .toJSON() ``` ## Hooks ```typescript const Users = collection({ name: 'users', schema: { ... }, hooks: { beforeValidate: (doc) => ({ ...doc, email: doc.email.toLowerCase() }), afterValidate: (doc) => doc, beforeInsert: (doc) => doc, afterInsert: async (doc) => { await sendWelcomeEmail(doc.email); }, beforeUpdate: (filter, update) => ({ filter, update }), afterUpdate: async (doc) => { if (doc) await invalidateCache(doc._id); }, beforeDelete: (filter) => filter, afterDelete: async (count) => { console.log(`Deleted ${count}`); }, }, }); ``` ## Instance Methods ```typescript const Users = collection({ name: 'users', schema: { _id: field.id(), firstName: field.string(), lastName: field.string(), role: field.enum(['admin', 'user']).default('user'), }, methods: { fullName: (doc) => `${doc.firstName} ${doc.lastName}`, isAdmin: (doc) => doc.role === 'admin', greet: (doc, greeting: string) => `${greeting}, ${doc.firstName}!`, }, }); const user = await Users.findById(userId); user.fullName(); // 'Alice Smith' user.isAdmin(); // false user.greet('Hello'); // 'Hello, Alice!' ``` ## Static Methods ```typescript const Users = collection({ name: 'users', schema: { ... }, statics: { findByEmail: (col, email: string) => col.findOne({ email }), findAdmins: (col) => col.find({ role: 'admin' }).toArray(), deactivateOld: async (col, days: number) => { const cutoff = new Date(Date.now() - days * 86400000); return col.updateMany( { lastLogin: { $lt: cutoff } }, { $set: { status: 'inactive' } } ); }, }, }); const user = await Users.findByEmail('alice@example.com'); const admins = await Users.findAdmins(); await Users.deactivateOld(30); ``` ## Transactions ```typescript await Users.transaction(async (session) => { const user = await Users.insertOne({ name: 'John' }, { session }); await Orders.insertOne({ userId: user._id, total: 100 }, { session }); // Auto-commits on success, auto-rolls back on error }); ``` ## Indexes ```typescript const Users = collection({ name: 'users', schema: { ... }, indexes: [ { email: 1, unique: true }, // Unique { createdAt: -1 }, // Descending { firstName: 1, lastName: 1 }, // Compound { bio: 'text' }, // Text search { location: '2dsphere' }, // Geo { expiresAt: 1, expireAfterSeconds: 0 }, // TTL { nickname: 1, sparse: true }, // Sparse ], }); // Indexes created automatically on first operation ``` ## Error Handling ```typescript import { MonchValidationError, MonchConnectionError } from '@codician-team/monch'; try { await Users.insertOne({ email: 'invalid' }); } catch (error) { if (error instanceof MonchValidationError) { error.issues; // Array of Zod issues error.toFormErrors(); // { email: 'Invalid email' } error.firstError; // 'Invalid email' error.firstField; // 'email' } } ``` ## Type Extraction ```typescript import { ModelOf, SerializedOf } from '@codician-team/monch'; type User = ModelOf; // Full model type type UserJSON = SerializedOf; // Serialized type ``` ## Common Patterns ### Soft Delete ```typescript const Users = collection({ name: 'users', schema: { _id: field.id(), name: field.string(), deletedAt: field.date().nullable().default(null), }, statics: { findActive: (col) => col.find({ deletedAt: null }).toArray(), softDelete: (col, id) => col.updateOne({ _id: id }, { $set: { deletedAt: new Date() } }), restore: (col, id) => col.updateOne({ _id: id }, { $set: { deletedAt: null } }), }, }); ``` ### Slug Generation ```typescript const Posts = collection({ name: 'posts', schema: { _id: field.id(), title: field.string(), slug: field.string().optional(), }, hooks: { beforeValidate: (doc) => ({ ...doc, slug: doc.slug || doc.title.toLowerCase().replace(/\s+/g, '-'), }), }, }); ``` ### Audit Logging ```typescript const auditLog: any[] = []; const Users = collection({ name: 'users', schema: { ... }, hooks: { afterInsert: (doc) => { auditLog.push({ action: 'INSERT', collection: 'users', id: doc._id }); }, afterUpdate: (doc) => { if (doc) auditLog.push({ action: 'UPDATE', collection: 'users', id: doc._id }); }, afterDelete: (count) => { auditLog.push({ action: 'DELETE', collection: 'users', count }); }, }, }); ``` ### E-commerce Cart ```typescript const items = [ { price: Money.from(29.99), quantity: 2 }, { price: Money.from(49.99), quantity: 1 }, ]; const lineTotals = items.map(i => Money.multiply(i.price, i.quantity)); const subtotal = Money.sum(lineTotals); const tax = Money.percentage(subtotal, 8.5); const total = Money.add(subtotal, tax); // For Stripe payment const stripeAmount = Money.toCents(total); ``` ### Stripe Webhook ```typescript // Receive cents from Stripe, convert to MoneyValue const payment = Money.fromCents(event.data.object.amount, 'USD'); await Orders.updateOne( { stripePaymentId: event.data.object.id }, { $set: { paidAmount: payment, status: 'paid' } } ); ``` ## Exports ```typescript import { // Core collection, field, z, // Money Money, CURRENCY_CONFIGS, getCurrencyConfig, // Connection resolveConnection, disconnectAll, setDebug, // Serialization serialize, serializeArray, MonchArray, // Errors MonchError, MonchConnectionError, MonchValidationError, // Types ModelOf, SerializedOf, SchemaOf, // MongoDB re-exports ObjectId, Long, Decimal128, Binary, Timestamp, MongoClient, Db, } from '@codician-team/monch'; ``` ## Tips for AI Assistants 1. **Schema-First**: Always define schema with proper Zod types before CRUD operations 2. **Type Safety**: Use `ModelOf` for type extraction 3. **Money Handling**: Use `field.money()` for financial data, never `field.number()` 4. **Serialization**: Call `.serialize()` before sending to client/API 5. **Validation Errors**: Catch `MonchValidationError` and use `.toFormErrors()` for forms 6. **Hooks Order**: beforeValidate → Zod parse → afterValidate → timestamps → beforeInsert → insert → afterInsert 7. **Lazy Connection**: Collections connect on first operation, not on definition 8. **Index Creation**: Indexes auto-create on first operation unless `createIndexes: false`