πŸ› οΈ Soft Delete Plugin for Mongoose - Simplify Data Management! πŸš€

Arjun DangiArjun Dangi
3 min read

πŸ”₯ Introduction

Managing soft deletes in MongoDB using Mongoose can be tedious and repetitive. Every time you query your database, you need to remember to filter out isDeleted: false, which can lead to unnecessary complexity and potential mistakes. Manually handling this logic across multiple queries increases the risk of inconsistencies and inefficiencies. To streamline this process, I created a Mongoose Soft Delete Plugin that automatically manages soft deletes for you. This plugin ensures that queries remain clean, concise, and efficient, allowing you to focus on building features instead of handling soft delete logic manually. 😎

✨ What This Plugin Does

βœ… Automatically adds isDeleted & deletedAt fields 🏷️
βœ… Filters out soft-deleted documents in all queries πŸ”
βœ… Custom Methods: .softDeleteOne() & .softDeleteMany() πŸ› οΈ
βœ… Works with find, update, and aggregate queries seamlessly ⚑

πŸ—οΈ Implementation

Here’s how the plugin is implemented:

import { nowUTCDate } from "@/utils/helpers/date"; // new UTC Date 
import mongoose, { CallbackWithoutResultAndOptionalError } from "mongoose";

const softDeletePlugin = (schema: mongoose.Schema) => {

// Add soft delete fields to schema
  schema.add({
    isDeleted: { type: Boolean, required: true, default: false },
    deletedAt: { type: Date, default: null },
  });

  // Middleware to exclude soft-deleted documents from queries
  const queryTypes = [
    "count", "find", "findOne", "findOneAndDelete", "findOneAndRemove",
    "findOneAndUpdate", "update", "updateOne", "updateMany",
  ];

  const excludeDeletedInQueries = async function (
    this: mongoose.Query<any, any>,
    next: CallbackWithoutResultAndOptionalError
  ) {
    this.where({ isDeleted: false });
    next();
  };

  const excludeDeletedInAggregates = async function (
    this: mongoose.Aggregate<any>,
    next: CallbackWithoutResultAndOptionalError
  ) {
    this.pipeline().unshift({ $match: { isDeleted: false } });
    next();
  };

// Custom soft delete methods
  schema.statics.softDeleteOne = async function (filter, otherUpdates) {
    await this.updateOne(filter, {
      $set: { isDeleted: true, deletedAt: nowUTCDate(), ...otherUpdates },
    });
  };

// Custom soft delete methods
  schema.statics.softDeleteMany = async function (filter, otherUpdates) {
    await this.updateMany(filter, {
      $set: { isDeleted: true, deletedAt: nowUTCDate(), ...otherUpdates },
    });
  };

 // Apply middleware
  queryTypes.forEach((type) => {
    schema.pre(type as any, excludeDeletedInQueries);
  });

// Apply middleware
  schema.pre("aggregate", excludeDeletedInAggregates);
};

export { softDeletePlugin };

πŸ‘©β€πŸ’» Example: User Schema with Soft Delete

Let’s see how we can use this plugin in a User Schema:

import mongoose from "mongoose";
import { SoftDeleteModel, softDeletePlugin } from "../../softDelete";

export interface IUser {
  name: string;
  email: string;
  age: number;
}

export interface IUserDocument extends IUser, mongoose.Document {
  createdAt: Date;
  updatedAt: Date;
}

const UserSchema = new mongoose.Schema<IUserDocument>(
  {
    name: {
      type: String,
      required: true,
    },
    email: {
      type: String,
      required: true,
      unique: true,
    },
    age: {
      type: Number,
      required: false,
    },
  },
  {
    timestamps: true,
  }
);

// Apply soft delete plugin
UserSchema.plugin(softDeletePlugin);

export const User = mongoose.model<IUserDocument, SoftDeleteModel<IUserDocument>>("User", UserSchema);

const userIds = ["ids",..]

await User.softDeleteMany(
  { _id: { $in: userIds } },
  { age: { $lt:18 }, isBnboarded:false } //other fields 
);

πŸ” Querying Without Explicit Filtering

Before: (Without plugin)

const users = await User.find({ isDeleted: false });

After: (With plugin)

const users = await User.find(); // No need to manually filter πŸŽ‰

⚑ Updating Without Fetching Soft-Deleted Docs

const updatedUser = await User.findOneAndUpdate(
  { _id: userId },
  { age: 19 },
  { new: true }
);

πŸ“Š Aggregation Query (Without Manually Filtering isDeleted)

Before: (Without plugin)

const result = await User.aggregate([
  { $match: { isDeleted: false } },
  { $group: { _id: "$age", count: { $sum: 1 } } }
]);

After: (With plugin)

const result = await User.aggregate([
  { $group: { _id: "$age", count: { $sum: 1 } } }
]);

🎯 Making Soft Deletes Effortless! πŸš€

This plugin ensures that soft-deleted documents are seamlessly handled without extra work! No more manual filtering in queries, updates, or aggregations. Just plug it in and focus on your business logic. 🎯

0
Subscribe to my newsletter

Read articles from Arjun Dangi directly inside your inbox. Subscribe to the newsletter, and don't miss out.

Written by

Arjun Dangi
Arjun Dangi