5

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});
        }
      });
    });
  });
});
notnot
  • 4,472
  • 12
  • 46
  • 57

2 Answers2

12

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

Alex Blex
  • 34,704
  • 7
  • 48
  • 75
  • I've tried it both via the model and via the document, and there's no difference. In both cases, "this" is just an empty object. Have you gotten it to work? – notnot Dec 09 '19 at 06:14
  • But of course. I have added a link to the tests. `this` should never be an empty object. It's either a document or a query. Please add your code that reproduces the problem. The middleware code alone is not enough. As I explained it matters how you delete documents. – Alex Blex Dec 09 '19 at 11:43
  • @notnot I have the exact same issue, the middleware is not triggered at all; did you fix it? – Joel Peltonen Jul 06 '22 at 14:39
5

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 });
SuleymanSah
  • 17,153
  • 5
  • 33
  • 54
  • So does this mean that usage with deleteOne is not yet fully supported? – notnot Dec 07 '19 at 21:42
  • @notnot actually I didn’t tried that. But with findByIdAndDelete it works. Tomorrow I will check. – SuleymanSah Dec 07 '19 at 21:47
  • 1
    @notnot in the docs it says deleteOne does not trigger pre('remove') or post('remove') hooks. https://mongoosejs.com/docs/api/model.html#model_Model.deleteOne – SuleymanSah Dec 08 '19 at 08:08
  • Here the issue is that the hook *is* being triggered, it just doesn't include any reference that I can find to the document that was removed. – notnot Dec 09 '19 at 06:01
  • @notnot we can be sure if the doc is not null in middleware. I will update the answer. – SuleymanSah Dec 09 '19 at 06:33