Skip to content

Queries & CRUD

Mondel collection methods mirror the driver while adding validation, timestamps, and typed schema access.

typescript
const db = await connect(process.env.MONGODB_URI!);

Create

typescript
// Field defaults (e.g. role: "USER") apply when validation is strict/loose
const result = await db.users.create({
  email: "john@example.com",
  name: "John",
});
// result is InsertOneResult — use result.insertedId (not a full document)

await db.users.createMany([
  { email: "a@test.com" },
  { email: "b@test.com" },
]);

Options: session, timestamps (default follows schema), plus native insert options.

Unknown keys are preserved (not stripped) on validated creates.

Read

typescript
const user = await db.users.findOne({ email: "john@example.com" });

const users = await db.users.findMany(
  { role: "ADMIN" },
  {
    sort: { createdAt: -1 },
    skip: 0,
    limit: 20,
    select: { email: true, role: true, _id: true }, // runtime projection
  }
);

const byId = await db.users.findById("507f1f77bcf86cd799439011");

Native operators:

typescript
await db.users.findMany({
  age: { $gte: 18 },
  role: { $in: ["ADMIN", "MODERATOR"] },
});

Projection vs TypeScript

select maps to MongoDB projection at runtime.
The TypeScript return type remains the full document type today — it is not narrowed by select. Access only fields you projected.

Driver options such as session, timeoutMS, and maxTimeMS are forwarded on find helpers.

Update

typescript
// Plain object → wrapped in $set
await db.users.updateOne({ email: "john@example.com" }, { name: "Johnny" });

// Operators
await db.users.updateOne(
  { _id: userId },
  { $set: { name: "Johnny" }, $inc: { loginCount: 1 } }
);

await db.users.updateById(userId, { $set: { name: "Johnny" } });

// Atomic find + update
await db.users.findOneAndUpdate(
  { email: "john@example.com" },
  { $set: { lastSeenAt: new Date() } },
  { returnDocument: "after" }
);

Validation: plain objects and $set / $setOnInsert payloads are validated.
$inc, $push, $pull, $unset, etc. are not schema-validated.

Partial updates do not inject field defaults (only create does).

Timestamps on update

With timestamps: true on the schema:

  • Updates set updatedAt
  • Upserts also set createdAt via $setOnInsert
  • Disable with { timestamps: false }

Delete

typescript
// Hard delete (default)
await db.users.deleteOne({ email: "gone@example.com" });
await db.users.deleteMany({ isActive: false });
await db.users.deleteById(userId);

// Soft delete — sets deletedAt (and updatedAt when timestamps enabled)
await db.users.deleteOne({ email: "gone@example.com" }, { soft: true });

See Soft delete recipe.

Utilities

typescript
const n = await db.users.count({ role: "ADMIN" });
const ok = await db.users.exists({ email: "john@example.com" });

Aggregation

typescript
const stats = await db.users.aggregate([
  { $match: { isActive: true } },
  { $group: { _id: "$role", count: { $sum: 1 } } },
  { $sort: { count: -1 } },
]);

Bulk write

typescript
await db.users.bulkWrite([
  { insertOne: { document: { email: "a@test.com" } } },
  {
    updateOne: {
      filter: { email: "b@test.com" },
      update: { $set: { active: true } },
    },
  },
]);

Payloads are not re-validated. Prefer create / updateOne when you need Zod.

Raw collection

typescript
const col = db.users.getCollection();
await col.distinct("role");

Transactions

Pass { session } into CRUD methods. Full guide: Transactions.

Released under the MIT License. · llms.txt