The mongoose-smart-delete plugin seamlessly integrates soft delete functionality into your Mongoose models by overriding default methods like deleteOne, deleteMany, and findOneAndDelete. It also extends query methods such as find, findOne, and more, ensuring compatibility with your existing system without requiring changes to your code. When applied to a model, default delete operations automatically perform soft deletes, providing a smooth transition to using the plugin.
This plugin leverages the same Mongoose hooks (pre and post) for delete operations like deleteOne and deleteMany, making it easy to retain existing behaviors. Additionally, it introduces custom hooks like restoreOne and restoreMany for handling restore operations.
Highly customizable, the plugin allows you to define custom field names for properties such as deleted, deletedAt, and deletedBy. You can also use hooks to modify or extend the default behavior, making it a flexible and lightweight solution for managing soft deletes in your application.
Install the package using npm:
npm install mongoose-smart-delete
Import the package and apply it as a plugin to your schema:
const Mongoose = require('mongoose')
const MongooseSmartDelete = require('mongoose-smart-delete')
const Schema = new Mongoose.Schema({})
Schema.plugin(MongooseSmartDelete)
const Model = Mongoose.model('Model', Schema)
const Document = await Model.create({})
return Document.deleteOne()
Sets the field name for the soft delete flag.
Schema.plugin(MongooseSmartDelete, {
deleted: {
field: 'deleted',
},
});
Sets the field name for the deletion timestamp.
- For default behavior:
Schema.plugin(MongooseSmartDelete, {
deletedAt: true,
});
- To specify a custom field:
Schema.plugin(MongooseSmartDelete, {
deletedAt: {
field: 'deletedAt',
},
});
Tracks the user who deleted the document.
deletedBy.field (optional)
: Sets the field name for the user reference.deletedBy.ref (required)
: Sets the reference model for the user.
Example:
const Mongoose = require('mongoose')
const MongooseSmartDelete = require('mongoose-smart-delete')
const UserSchema = new Mongoose.Schema({})
const DocumentSchema = new Mongoose.Schema({})
DocumentSchema.plugin(MongooseSmartDelete, {
deletedBy: {
field: 'deletedBy',
ref: 'User',
},
})
const UserModel = Mongoose.model('User', UserSchema)
const DocumentModel = Mongoose.model('Document', DocumentSchema)
const User = await UserModel.create({})
const Document = await DocumentModel.create({})
await Document.deleteOne({deletedBy: User})
$ne (default)
- In $ne
mode, the query {deleted: {$ne: true}}
is used. For newly created and restored items, the deleted
attribute is undefined (unset).
strict
- In strict
mode, the query {deleted: false}
is used. For newly created and restored items, the deleted
attribute is explicitly set to false
.
Note: The fields deletedAt
and deletedBy
are always set to undefined
(unset) for newly created and restored documents.
Schema.plugin(MongooseSmartDelete, {
mode: '$ne|strict',
});
Important Note: For existing projects, if you want to switch to strict
mode, you must update all existing documents to explicitly set the deleted
attribute to false
.
Strict mode has better performance because it can leverage an index on the deleted
attribute.
await Model.updateMany({deleted: {$exists: false}}, {deleted: false})
Soft delete a document instance:
Document.deleteOne();
Hard delete a document instance:
Document.deleteOne({softDelete: false});
Soft delete a single document:
Model.deleteOne(query);
Hard delete a single document:
Model.deleteOne(query, {softDelete: false});
Soft delete:
Model.findOneAndDelete(query);
Hard delete:
Model.findOneAndDelete(query, {softDelete: false});
Soft delete:
Model.findByIdAndDelete(id);
Hard delete:
Model.findByIdAndDelete(id, {softDelete: false});
Soft delete multiple documents:
Model.deleteMany(query);
Hard delete multiple documents:
Model.deleteMany(query, {softDelete: false});
Use hooks to execute code before or after a delete operation.
Document-level hooks:
In document level hook, you can use options.softDelete
to determine if the operation is a soft delete or hard delete.
In pre hook, options are passed as second argument. In post - as first.
schema.pre('deleteOne', {document: true, query: false}, function (next, options) {
if (options.softDelete) {
// Code for soft delete
} else {
// Code for hard delete
}
next()
})
schema.post('deleteOne', function (options, next) {
if (options.softDelete) {
// Code for soft delete
} else {
// Code for hard delete
}
})
Query-level hooks:
In query level hook, you can use this.getOptions().softDelete
to determine if the operation is a soft delete or hard delete.
schema.pre('deleteOne', {query: true}, function (next) {
if (this.getOptions().softDelete) {
// Code for soft delete
} else {
// Code for hard delete
}
next()
})
schema.post('deleteOne', {query: true}, function (result, next) {
if (this.getOptions().softDelete) {
// Code for soft delete
} else {
// Code for hard delete
}
next()
})
schema.pre('deleteMany', function (next) {
if (this.getOptions().softDelete) {
// Code for soft delete
} else {
// Code for hard delete
}
next();
});
schema.post('deleteMany', function () {
if (this.getOptions().softDelete) {
// Code for soft delete
} else {
// Code for hard delete
}
});
// Update batchId field when soft deleting
const Mongoose = require('mongoose')
const MongooseSmartDelete = require('mongoose-smart-delete')
const Schema = new Mongoose.Schema({
batchId: String
})
Schema.plugin(MongooseSmartDelete)
Schema.pre(['deleteOne', 'deleteMany'], {document: false, query: true}, function (next) {
const options = this.getOptions()
if (options.softDelete) {
if (options.batchId) {
const update = this.getUpdate()
update.$set.batchId = options.batchId
this.setUpdate(update)
}
}
next()
})
const Model = Mongoose.model('Model', Schema)
module.exports = async function () {
const Document = await Model.create({})
await Model.deleteMany({}, {batchId: '12345'})
return Model.findOne({}).withDeleted()
}
Restore a document instance:
Document.restoreOne();
Restore a single document:
Model.restoreOne(query);
Restore multiple documents:
Model.restoreMany(query);
Use hooks to execute code before or after a restore operation.
schema.pre('restoreOne', function (next) {
// Code before restore
next();
});
schema.post('restoreOne', function () {
// Code after restore
});
schema.pre('restoreMany', function (next) {
// Code before restore
next();
});
schema.post('restoreMany', function () {
// Code after restore
});
Include deleted documents in results:
Model.find({}).withDeleted();
Include a deleted document in results:
Model.findOne({}).withDeleted();
Model.findOneAndUpdate({}).withDeleted();
Model.findOneAndReplace({}).withDeleted();
Model.updateOne({}).withDeleted();
Model.updateMany({}).withDeleted();
Model.replaceOne({}).withDeleted();
Model.countDocuments({}).withDeleted();
The MIT License
Copyright 2025 Karolis Tička https://github.com/kticka
Permission is hereby granted, free of charge, to any person obtaining a copy of this software and associated documentation files (the “Software”), to deal in the Software without restriction, including without limitation the rights to use, copy, modify, merge, publish, distribute, sublicense, and/or sell copies of the Software, and to permit persons to whom the Software is furnished to do so, subject to the following conditions:
The above copyright notice and this permission notice shall be included in all copies or substantial portions of the Software.
THE SOFTWARE IS PROVIDED “AS IS”, WITHOUT WARRANTY OF ANY KIND, EXPRESS OR IMPLIED, INCLUDING BUT NOT LIMITED TO THE WARRANTIES OF MERCHANTABILITY, FITNESS FOR A PARTICULAR PURPOSE AND NONINFRINGEMENT. IN NO EVENT SHALL THE AUTHORS OR COPYRIGHT HOLDERS BE LIABLE FOR ANY CLAIM, DAMAGES OR OTHER LIABILITY, WHETHER IN AN ACTION OF CONTRACT, TORT OR OTHERWISE, ARISING FROM, OUT OF OR IN CONNECTION WITH THE SOFTWARE OR THE USE OR OTHER DEALINGS IN THE SOFTWARE.