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:
- Correctness: Are my query and aggregation middlewares implemented correctly to always exclude deleted documents unless explicitly requested?
- Best Practices: Are there better approaches to implement soft delete in Mongoose that I might be missing?
- Performance: Are there any potential performance issues, especially with large collections or aggregation pipelines?
- Code Quality: Are there improvements I can make to make the plugin more maintainable, readable, or reusable?
- 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.