Skip to content

A Mongoose plugin for implementing soft delete functionality, allowing documents to be marked as deleted without being removed from the database.

License

Notifications You must be signed in to change notification settings

kticka/mongoose-smart-delete

Repository files navigation

Mongoose Smart Delete

Node.js CI

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.

Documentation

Installation

Install the package using npm:

npm install mongoose-smart-delete

Usage

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()

Options

deleted (optional)

Sets the field name for the soft delete flag.

Schema.plugin(MongooseSmartDelete, {
  deleted: {
    field: 'deleted',
  },
});

deletedAt (optional)

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',
  },
});

deletedBy (optional)

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})

mode (optional)

$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})

Delete

Document.deleteOne()

Soft delete a document instance:

Document.deleteOne();

Hard delete a document instance:

Document.deleteOne({softDelete: false});

Model.deleteOne()

Soft delete a single document:

Model.deleteOne(query);

Hard delete a single document:

Model.deleteOne(query, {softDelete: false});

Model.findOneAndDelete()

Soft delete:

Model.findOneAndDelete(query);

Hard delete:

Model.findOneAndDelete(query, {softDelete: false});

Model.findByIdAndDelete()

Soft delete:

Model.findByIdAndDelete(id);

Hard delete:

Model.findByIdAndDelete(id, {softDelete: false});

Model.deleteMany()

Soft delete multiple documents:

Model.deleteMany(query);

Hard delete multiple documents:

Model.deleteMany(query, {softDelete: false});

Delete Hooks

Use hooks to execute code before or after a delete operation.

deleteOne Hooks

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()
})

deleteMany Hooks

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
  }
});

Real life example with custom batchId attribute:

// 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

Document.restoreOne()

Restore a document instance:

Document.restoreOne();

Model.restoreOne()

Restore a single document:

Model.restoreOne(query);

Model.restoreMany()

Restore multiple documents:

Model.restoreMany(query);

Restore Hooks

Use hooks to execute code before or after a restore operation.

restoreOne Hooks

schema.pre('restoreOne', function (next) {
  // Code before restore
  next();
});

schema.post('restoreOne', function () {
  // Code after restore
});

restoreMany Hooks

schema.pre('restoreMany', function (next) {
  // Code before restore
  next();
});

schema.post('restoreMany', function () {
  // Code after restore
});

Queries

Model.find()

Include deleted documents in results:

Model.find({}).withDeleted();

Model.findOne()

Include a deleted document in results:

Model.findOne({}).withDeleted();

Model.findOneAndUpdate()

Model.findOneAndUpdate({}).withDeleted();

Model.findOneAndReplace()

Model.findOneAndReplace({}).withDeleted();

Model.updateOne()

Model.updateOne({}).withDeleted();

Model.updateMany()

Model.updateMany({}).withDeleted();

Model.replaceOne()

Model.replaceOne({}).withDeleted();

Model.countDocuments()

Model.countDocuments({}).withDeleted();

License

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.

About

A Mongoose plugin for implementing soft delete functionality, allowing documents to be marked as deleted without being removed from the database.

Resources

License

Stars

Watchers

Forks

Releases

No releases published

Packages

No packages published