Skip to content

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() });

Released under the MIT License.