javascriptnode.jsmongodbmongoosemongoose-plugins

Mongoose Plugin to filter soft deleted in array docs and root docs using find() and perform soft delete on array docs and root docs


Implementing a Mongoose Plugin for Soft Delete Array docs and root docs and Filtering soft deleted Array docs and root docs.

I am developing a Mongoose plugin to handle soft deletion and filtering of soft-deleted items using find() in a MongoDB database. The plugin needs to:

  1. Automatically filter out soft-deleted documents and nested array items in find() queries.
  2. Provide a method to perform soft deletion on both top-level documents and nested array items.
  3. Allow the option to include soft-deleted items in queries when needed.

My main challenges are:

I am particularly interested in:

  1. Handling of nested documents and arrays.

Any guidance or example patterns would be greatly appreciated!

Data :

 {
      "_id": {
        "$oid": "66c883a5e6ddbf5f720ae1b6"
      },
      "name": "Some Name",
      "email": "email@email.com",
      "age": 48,
      "nestedArr": [
        {
          "id": 7032086,
          "value": "apple0",
          "isDeleted": false
        },
        {
          "id": 4086837,
          "value": "apple0",
          "isDeleted": true
        },
        {
          "id": 6683277,
          "value": "apple0",
          "isDeleted": false
        },
        {
          "id": 2870389,
          "value": "apple0",
          "isDeleted": true
        }
      ],
      "isDeleted": true,
    }

Solution

  • Below are logics made to just understand mechanism of projection & filter in pre-hook mongoose schema in case to create soft-delete plugin:

    const mongoose = require("mongoose");
    
    // Define User Schema
    const userSchema = new mongoose.Schema({
      name: {
        type: String,
        required: true,
        minlength: 2,
        maxlength: 50,
      },
      createdAt: {
        type: Date,
        default: Date.now,
      },
      nestedArr: [
        {
          id: Number,
          value: String,
          isDeleted: Boolean,
        },
      ],
    
      isDeleted: Boolean,
    });
    
    // User Plugin
    function softDeletePlugin(schema, options) {
      schema.add({
        isDeleted: {
          type: Boolean,
          required: true,
          default: false,
        },
      });
    
      schema.pre("find", function (next) {
        const filter = this.getQuery();
        const options = this.getOptions();
    
        //In case we need to show soft deleted items  (We can also put in getOptions() but I have put in filter)
        if (filter.showSoftDeleted) {
          delete filter["showSoftDeleted"];
          this.setQuery(filter);
          return next();
        }
    
        // Filter out root (top-level) documents where isDeleted is true
        if (!filter.hasOwnProperty("isDeleted")) {
          filter.isDeleted = { $ne: true };
        }
    
        // For Array documents. Using $filter will bring all matching records unlike $elemMatch which returns 1st matching result
        if (!options.projection) {
          options.projection = {};
        }
        options.projection.nestedArr = {
          $filter: {
            input: "$nestedArr",
            as: "item",
            cond: { $ne: ["$$item.isDeleted", true] },
          },
        };
    
        this.setQuery(filter);
        this.setOptions(options);
    
        return next();
      });
    
      // Static soft delete method
      schema.statics.softDelete = function (
        conditionFilter,
        nestedRecord = false,
        nestedRecordKey = "nestedArr"
      ) {
        let softDeleteInstance;
        if (nestedRecord) {
          const updateQuery = {
            $set: {
              [`${nestedRecordKey}.$[elem].isDeleted`]: true,
            },
          };
    
          softDeleteInstance = this.updateMany(conditionFilter, updateQuery, {
            arrayFilters: [
              { [`elem._id`]: conditionFilter[`${nestedRecordKey}._id`] },
            ],
          }).exec();
        }
    
        //check if _id is given to soft delete root document
        //update root documents (top-level documents) if required to delete.
        if (conditionFilter._id) {
          const updateQuery = {
            $set: {
              isDeleted: true,
            },
          };
          softDeleteInstance = this.updateMany(conditionFilter, updateQuery).exec();
        }
    
        return softDeleteInstance;
      };
    }
    
    // Apply the plugin to the schema
    userSchema.plugin(softDeletePlugin);
    
    // User model
    const User = mongoose.model("User", userSchema);
    
    // Connect to MongoDB
    mongoose
      .connect("mongodb://localhost/userdb")
      .then(() => console.log("Connected to MongoDB"))
      .catch((err) => console.error("Could not connect to MongoDB", err));
    
    // How to use :
    
    //soft deletes only array documents
    User.softDelete(
      {
        "nestedArr.id": 4086837,
      },
      true
    ).then(() => console.log("Soft deleted"));
    
    // to soft delete both root & array documents
    User.softDelete(
      {
        _id: "66c883a5e6ddbf5f720ae1b6",
        "nestedArr.id": 4086837,
      },
      true
    ).then(() => console.log("Soft deleted"));
    
    User.find({})
      .lean()
      .then((data) => {
        console.log(data);
      });
    
    /*
      1st argument : conditionFilter
      2nd argument : projection
      3rd argument : to provide Options -> getOptions() in mongoose schema
    
      // showSoftDeleted to include soft deleted items
      User.find({showSoftDeleted : true}, null, {});
    
      */