Recipe: Soft delete
Mondel supports opt-in soft delete on delete helpers.
Behavior
typescript
// Hard delete (default) — document removed
await db.users.deleteOne({ email: "a@b.com" });
// Soft delete — sets deletedAt: Date
// If schema timestamps enabled, also bumps updatedAt
await db.users.deleteOne({ email: "a@b.com" }, { soft: true });
await db.users.deleteMany({ role: "GUEST" }, { soft: true });
await db.users.deleteById(id, { soft: true });deletedCount on the result reflects matched documents for soft deletes (how many docs were targeted by the filter).
Schema tip
Optionally model the field:
typescript
export const userSchema = defineSchema("users", {
timestamps: true,
fields: {
email: s.string().required(),
deletedAt: s.date(), // optional
},
});Choosing the field name
deletedAt is the default. Point Mondel at an existing column instead — useful when migrating a collection that already uses another name — with softDelete:
typescript
export const userSchema = defineSchema("users", {
timestamps: true,
softDelete: { field: "removedAt" },
fields: {
email: s.string().required(),
removedAt: s.date(),
},
});
// stamps removedAt, not deletedAt
await db.users.deleteOne({ email: "a@b.com" }, { soft: true });The field is stamped whether or not you declare it in fields — Mondel does not require the schema to model it.
Always filter soft-deleted rows
Soft delete does not change default reads:
typescript
// Active only
await db.users.findMany({ deletedAt: { $exists: false } });
// Or explicit null
await db.users.findMany({ deletedAt: null });Helper pattern:
typescript
const notDeleted = { deletedAt: { $exists: false } } as const;
await db.users.findMany({ ...notDeleted, role: "USER" });Restore
typescript
await db.users.updateOne(
{ email: "a@b.com" },
{ $unset: { deletedAt: "" } }
);$unset is not Zod-validated (operator passthrough).
Indexes
Partial index for active emails:
typescript
indexes: [
{
fields: { email: 1 },
options: {
unique: true,
partialFilterExpression: { deletedAt: { $exists: false } },
},
},
],