5

I have mongoose-schema called UserSchema, which stores information about all users. I want to enable the user to change his information, which I try by using .findByIdAndUpdate. This is the relevant code:

router.post("/updateprofile", function(req,res,next) {
    const {id, org, tel, email, firstName, lastName} = req.body;
    Users.findByIdAndUpdate(id, {org : org, tel : tel, email : email, firstName : firstName , lastName : lastName}, function (err, response) {
        if (err) throw err
        res.json(response);
    });

});

However, when trying to change the info, I get the following error-message: Cannot read property 'password' of undefined. I'm pretty sure this is caused by a pre-update hook, but I can not remove it because I need it for my "forgot-password"-functionality. Here's the code:

UserSchema.pre('findOneAndUpdate', function (next) {
    this.update({},{ $set: { password: 
    bcrypt.hashSync(this.getUpdate().$set.password, 10)}} )
    next();
});

I'm confused by why it used that prehook anyway, since in the hook it's looking for findOneandUpdate and when I try to change the data I'm using findByIdAndUpdate.

I tried using .update() instead but that doesn't work either. Does anyone know what I'm doing wrong and how to fix it?

A.S.J
  • 627
  • 3
  • 14
  • 38

3 Answers3

17

Looks like getUpdate isn't what you want, try it like this:

    UserSchema.pre('findOneAndUpdate', function (next) {
    this._update.password = bcrypt.hashSync(this._update.password, 10)
    next();
});

With regards to your second question, findByIdAndUpdate is a wrapper around findOneAndUpdate. Here's the code straight from Mongoose's source code for your reference

Model.findByIdAndUpdate = function(id, update, options, callback) {
  if (callback) {
    callback = this.$wrapCallback(callback);
  }
  if (arguments.length === 1) {
    if (typeof id === 'function') {
      var msg = 'Model.findByIdAndUpdate(): First argument must not be a function.\n\n'
          + '  ' + this.modelName + '.findByIdAndUpdate(id, callback)\n'
          + '  ' + this.modelName + '.findByIdAndUpdate(id)\n'
          + '  ' + this.modelName + '.findByIdAndUpdate()\n';
      throw new TypeError(msg);
    }
    return this.findOneAndUpdate({_id: id}, undefined);
  }

The comments in the code read:

/**
 * Issues a mongodb findAndModify update command by a document's _id field.
 * `findByIdAndUpdate(id, ...)` is equivalent to `findOneAndUpdate({ _id: id }, ...)`.
 *

You can read the source code for yourself here: https://github.com/Automattic/mongoose/blob/9ec32419fb38b74b240280aaba162f9ee4416674/lib/model.js

Daniel Gary
  • 507
  • 4
  • 10
  • 1
    Thank you very much for your response. I changed my code accordin gly. So, are you saying that I defined my function wrong? I'm not exactly sure about what to do with the second bit of code ( `Model-findByIdAndUpdate` ) – A.S.J Mar 05 '18 at 16:25
  • You asked why the findOneAndUpdate hook was being called. It's being called because findByIdAndUpdate calls findOneAndUpdate. Also, if my code fixed your issue would you please mark as the correct answer. – Daniel Gary Mar 05 '18 at 16:26
  • oh now it makes sense to me. I will mark your answer as correct, however, my issue is not quite fixed since my problem was that I somehow need to get around the fact that, when I update the profile info, that it checks for a password. I tried checking if there was a password by wrapping `this._update.$set.password = ....` in an if-statement, but that didn't work either – A.S.J Mar 05 '18 at 16:29
  • I did not include any $set in my code. You should not need $set in the pre findOneAndUpdate hook. You should be able to set your desired property directly on this._update.password. – Daniel Gary Mar 05 '18 at 16:31
  • I tried it exactly the way you described and it didn't work, that's why I included $set – A.S.J Mar 05 '18 at 16:31
  • Instead of just calling next at the end of your function, try return next() – Daniel Gary Mar 05 '18 at 16:32
  • `data and salt arguments required` I get this error message – A.S.J Mar 05 '18 at 16:34
  • Let us [continue this discussion in chat](https://chat.stackoverflow.com/rooms/166260/discussion-between-daniel-gary-and-a-s-j). – Daniel Gary Mar 05 '18 at 16:34
0

Assuming you plan to pass the entire document in as an update (such as with upsert: true - I think this is also assumed in the accepted answer) and want to use one function, this works:

async function validation(next, self) {
    // validation code here 
}

YourSchema.pre('validate', async function(next) { validation(next, this) });
YourSchema.pre('findOneAndUpdate', async function(next) { validation(next, this._update) });

You just replace this with this._update to inform the function what it's validating.

SuperCodeBrah
  • 2,874
  • 2
  • 19
  • 33
0

Since I wasn't able to access _update property, this worked to me:

UserSchema.pre('findOneAndUpdate', async function (this) {
  let update = {...this.getUpdate()};

  // Only run this function if password was modified
  if (update.password){

  // Hash the password
  const salt = genSaltSync();
  update.password = await hash(this.getUpdate().password, salt);
  this.setUpdate(update);
  }
})