Mongoose: deleteOne middleware for cascading delete not working

8.9k Views Asked by At

With remove being deprecated in Mongoose 5.7.13, I want to use deleteOne instead. I need to get the id of the deleted document so that I can then delete further related documents in other collections in a cascade. I thought that "this" within the context of the pre middleware hook was meant to refer to the removed document, but instead it's just an empty object. Is there a canonical working example of this? I'm still currently using 5.7.12 at this point - will that make a difference here?

Here is the code I'm currently working with. The issue is that I can't get the projectId at the start because the reference is completely empty. Doing this on post rather than pre, or switching the option to run on query rather than document all yield the same result.

ProjectSchema.pre("deleteOne", {document:true}, (next) => {
  const projectId = this._id;
  ListModel.find({parentProject:projectId}, (err, lists) => {
    if(err){
      console.log("error cascading project delete to lists", {err});
    }
    lists.map(list => {
      ListModel.deleteOne({_id:list._id}, (err, result) => {
        if(err) {
          console.log("error on project delete cascade", {err});
        }
      });
    });
  });
});

2

There are 2 best solutions below

3
Alex Blex On

It depends whether you call deleteOne on document or on model. The later just have no document to bind it to.

The former gives you the document as you expect:

const project = await ProjectModel.findOne();
project.deleteOne();

The later gives you the Query. There is no _id in the query, but it has this.op for example, which in this middleware will be "deleteOne":

await ProjectModel.deleteOne();

The only way to get the document id in this case is to ensure it is provided in the query:

await ProjectModel.deleteOne({_id: "alex"});

Then you can get it in the middleware from the filter:

const projectId = this.getFilter()["_id"]

You can specify query: false in second parameter of the middleware to ensure the it is not invoked when you call deleteOne on model. So the best you can do:

ProjectSchema.pre("deleteOne", {document:true, query: false}, (next) => {
    const projectId = this._id;
    ....
});

ProjectSchema.pre("deleteOne", {document:false, query: true}, (next) => {
    const projectId = this.getFilter()["_id"];
    if (typeof projectId === "undefined") {
        // no way to make cascade deletion since there is no _id
        // in the delete query
        // I would throw an exception, but it's up to you how to deal with it
        // to ensure data integrity
    }
});

Please take a look at corresponding tests on v5.7.12: https://github.com/Automattic/mongoose/blob/5.7.12/test/model.middleware.test.js#L436

5
SuleymanSah On

In the mongoose docs it says "Model.deleteOne() does not trigger pre('remove') or post('remove') hooks."

There is solution if you can refactor your delete operations with findByIdAndDelete, it triggers the findOneAndDelete middleware,

So we can add this middleware to Project Schema.

Project model:

const mongoose = require("mongoose");
const ProjectChild = require("./projectChild");

const ProjectSchema = new mongoose.Schema({
  name: String
});

ProjectSchema.post("findOneAndDelete", async function(doc) {
  console.log(doc);

  if (doc) {
    const deleteResult = await ProjectChild.deleteMany({
      parentProject: doc._id
    });

    console.log("Child delete result: ", deleteResult);
  }
});

module.exports = mongoose.model("Project", ProjectSchema);

ProjectChild model:

const mongoose = require("mongoose");

const projectChildSchema = new mongoose.Schema({
  name: String,
  parentProject: {
    type: mongoose.Schema.Types.ObjectId,
    ref: "Project"
  }
});

module.exports = mongoose.model("ProjectChild", projectChildSchema);

I created a project like this:

{
    "_id": "5dea699cb10c442260245abf",
    "name": "Project 1",
    "__v": 0
}

And created 2 project child for this project:

Child 1

{
    "_id": "5dea69c7b10c442260245ac0",
    "name": "Child 1 (project 1)",
    "parentProject": "5dea699cb10c442260245abf",
    "__v": 0
}

Child 2

{
    "_id": "5dea69e8b10c442260245ac1",
    "name": "Child 2 (project 1)",
    "parentProject": "5dea699cb10c442260245abf",
    "__v": 0
}

I created a sample route to delete a project by its id like this:

router.delete("/project/:id", async (req, res) => {
  const result = await Project.findByIdAndDelete(req.params.id);

  res.send(result);
});

When I send a DELETE request to this route, we see the following info in the console:

console.log(doc);

{ _id: 5dea699cb10c442260245abf, name: 'Project 1', __v: 0 }

console.log("Child delete result: ", deleteResult);

Child delete result:  { n: 2, ok: 1, deletedCount: 2 }

So we could deleted the 2 children of the project, when we deleted the project.

As an alternative you can also use findOneAndRemove, it triggers findOneAndRemove post middleware.

So in the ProjectSchema we replace the post middleware like this:

ProjectSchema.post("findOneAndRemove", async function(doc) {
  console.log(doc);

  if (doc) {
    const deleteResult = await ProjectChild.deleteMany({
      parentProject: doc._id
    });

    console.log("Child delete result: ", deleteResult);
  }
});

When we use a findOneAndRemove operation, the result will be the same as the first alternative:

const result = await Project.findOneAndRemove({ _id: req.params.id });