Why does .findByIdAndUpdate bypasses mongoose middleware?

Viewed 1358

I've searched this question on here, but as a beginner, the answers are a little complex and go over my head a bit. I'm using bcrypt to hash passwords and have set up my schema to allow me to use middleware to see if a user modifies a password (either upon initial creation or updating it) before it hashes it.

There are no problems with this on a post route, but if I use a patch route there are, and my middleware doesn't run.

Can someone help me understand in a more simple way (i.e. plain English) why this bypasses mongoose?:

const user = await User.findByIdAndUpdate(req.params.id, req.body, { new: true, runValidators: true })

This works, though:

const user = await User.findById(req.params.id)

updates.forEach((update) => user[update] = req.body[update])
await user.save()
3 Answers

It is really simple: The type of middleware you want (called query middleware) only supports / fires on certain methods. For example from the documentation:

Query middleware is supported for the following Model and Query functions. In query middleware functions, this refers to the query.

  • count
  • deleteMany
  • deleteOne
  • find
  • findOne
  • findOneAndDelete
  • findOneAndRemove
  • findOneAndUpdate
  • remove
  • update
  • updateOne
  • updateMany

So basically the method you are using is not expected to trigger any middleware. You have to use findOneAndUpdate, update, updateOne or updateMany to do so.

In your 2nd case where it actually works. This is due to the fact that there you are actually using the document middleware.

Mongoose has 4 types of middleware: document middleware, model middleware, aggregate middleware, and query middleware. Document middleware is supported for the following document functions. In document middleware functions, this refers to the document.

Document middleware is supported on:

  • validate
  • save
  • remove
  • init (note: init hooks are synchronous)

As you can see save is supported hence the middleware fires.

It should be like this:

User.update({username: oldUsername}, {
    username: newUser.username, 
    password: newUser.password
}, function(err, numberAffected, rawResponse) {
   //handle it
});

findByIdAndUpdate() is essentially a wrapper for findOneAndUpdate(). That is, it is equivalent to findOneAndUpdate({ _id: id }, ...).

As such, it does not bypasses mongoose middleware completely.

As per the documentation:

This function triggers the following middleware.

  • findOneAndUpdate()

So to hook into mongoose post middleware for findByIdAndUpdate(), the following will work:

someSchema.post('findOneAndUpdate', function (res, next) {
  console.log('This runs after findOneAndUpdate and findByIdAndUpdate');
  next();
})
Related