0
\$\begingroup\$

Title: Code Review Request: Mongoose Soft Delete Plugin with Query & Aggregate Middleware

Body:

I’ve implemented a soft delete plugin for Mongoose to handle REST API data safely in a MENN stack project (MongoDB, Express, Next.js, Node.js). The goal is to mark documents as deleted instead of removing them completely, and automatically exclude them from queries and aggregation pipelines.

Here’s what I’m looking for in a review:

  1. Correctness: Are my query and aggregation middlewares implemented correctly to always exclude deleted documents unless explicitly requested?
  2. Best Practices: Are there better approaches to implement soft delete in Mongoose that I might be missing?
  3. Performance: Are there any potential performance issues, especially with large collections or aggregation pipelines?
  4. Code Quality: Are there improvements I can make to make the plugin more maintainable, readable, or reusable?
  5. Edge Cases: Any scenarios where my plugin could fail, like bulk updates, transactions, or concurrent requests?

Here’s the plugin code:

// softDeletePlugin.js
export function softDeletePlugin(schema, options = {}) {
  // Default options for the plugin
  const opts = {
    deletedField: 'isDelete',  // Field to mark a document as deleted
    deletedValue: true,        // Value that indicates deletion
    indexFields: true,         // Whether to index the deleted field
    ...options
  };

  // 1. Add the deleted flag to the schema
  const schemaAdd = {};
  schemaAdd[opts.deletedField] = {
    type: Boolean,
    default: false,   // By default, documents are not deleted
    index: opts.indexFields
  };
  schema.add(schemaAdd);

  // 2. Add a deletedAt timestamp
  schema.add({
    deletedAt: { type: Date, default: null }
  });

  // 3. Pre-query middleware
  // Exclude soft-deleted documents from find queries unless explicitly requested
  schema.pre(/^find/, function (next) {
    if (!this.getOptions().includeDeleted) {
      const filter = {};
      filter[opts.deletedField] = { $ne: opts.deletedValue }; // Only non-deleted
      this.where(filter);
    }
    next();
  });

  // 4. Pre-aggregate middleware
  // Automatically exclude deleted documents from aggregation pipelines
  schema.pre("aggregate", function (next) {
    if (!this.getOptions().includeDeleted) {
      const matchStage = {};
      matchStage[opts.deletedField] = { $ne: opts.deletedValue };
      this.pipeline().unshift({ $match: matchStage }); // Insert at the beginning
    }
    next();
  });

  // 5. Instance methods
  // Soft delete a single document
  schema.methods.softDelete = function (callback) {
    this[opts.deletedField] = opts.deletedValue;
    this.deletedAt = new Date();
    return callback ? this.save(callback) : this.save();
  };

  // Restore a soft-deleted document
  schema.methods.restore = function (callback) {
    this[opts.deletedField] = false;
    this.deletedAt = null;
    return callback ? this.save(callback) : this.save();
  };

  // Check if a document is deleted
  schema.methods.isDeleted = function () {
    return this[opts.deletedField] === opts.deletedValue;
  };

  // 6. Static methods
  // Find all deleted documents
  schema.statics.findDeleted = function (conditions = {}) {
    return this.find({ ...conditions, [opts.deletedField]: opts.deletedValue });
  };

  // Find all documents including deleted ones
  schema.statics.findWithDeleted = function (conditions = {}) {
    return this.find(conditions).setOptions({ includeDeleted: true });
  };

  // Soft delete by ID
  schema.statics.softDeleteById = function (id, callback) {
    const update = {
      [opts.deletedField]: opts.deletedValue,
      deletedAt: new Date()
    };
    return callback
      ? this.updateOne({ _id: id }, update, callback)
      : this.updateOne({ _id: id }, update);
  };

  // Restore by ID
  schema.statics.restoreById = function (id, callback) {
    const update = {
      [opts.deletedField]: false,
      deletedAt: null
    };
    return callback
      ? this.updateOne({ _id: id }, update, callback)
      : this.updateOne({ _id: id }, update);
  };
}

Example usage:

const mongoose = require("mongoose");
const { softDeletePlugin } = require("../middleware/softDeletePlugin");
let groupSchema = new mongoose.Schema(
  {
    name: { type: String },
  },
  {
    timestamps: true,
  }
);
groupSchema.plugin(softDeletePlugin);
const group = mongoose.model("group", groupSchema);

module.exports = group;

I’m looking for experienced developer feedback, especially from those familiar with Mongoose best practices, database safety, and REST API design.

\$\endgroup\$

0

You must log in to answer this question.

Start asking to get answers

Find the answer to your question by asking.

Ask question

Explore related questions

See similar questions with these tags.