7

First of all, this doesn't help.

Let's say, we have a User model:

const schema = new mongoose.Schema({
    active: { type: Boolean },
    avatar: { type: String }
});

const User = mongoose.model('User', schema);

When we update it (set an avatar):

// This should pass validation
User.update({ _id: id }, { $set: { avatar: 'user1.png' } });

We want to validate it based on current (or changed) active attribute value.

Case #1

  • active is false
  • we should not be able to set avatar - it should not pass the validation

Case #2

  • active is true
  • we should be able to set avatar - it should pass the validation

Ideas

  1. Use a custom validator
const schema = new mongoose.Schema({
    active: { type: Boolean },
    avatar: { type: String, validate: [validateAvatar, 'User is not active'] }
});

function validateAvatar (value) {
    console.log(value); // user.avatar
    console.log(this.active); // undefined
}

So this will not work as we don't have an access to active field.

  1. Use pre "validate" hook
schema.pre('validate', function (next) {
    // this will never be called
});

This hook doesn't work with update method.

  1. Use pre "update" hook
schema.pre('update', function (next) {
    console.log(this.active); // undefined
});

This will not work for us as it doesn't have an access to model fields.

  1. Use post "update" hook
schema.post('update', function (next) {
    console.log(this.active); // false
});

This one works, but in terms of validation is not quite good choice, as the function is being called only when model was already saved.

Question

So is there a way to validate the model based on several fields (both saved in DB and new ones) before saving it, while using model.update() method?

As a summary:

  1. Initial user object
{ active: false, avatar: null }
  1. Update
User.update({ _id: id }, { $set: { avatar: 'user1.png' } });
  1. Validation should have an access to
{ active: false, avatar: 'user1.png' }
  1. If validation fails, changes should not be passed to DB
Community
  • 1
  • 1
Nazar
  • 1,769
  • 1
  • 15
  • 31

5 Answers5

4

Due to limitation of working with update() I've decided to solve the problem this way:

  • Use custom validators (idea #1 mentioned in the question)
  • Don't use update()

So instead of

User.update({ _id: id }, { $set: { avatar: 'user1.png' } });

I use

User.findOne({ _id: id })
    .then((user) => {
        user.avatar = 'user1.png';
        user.save();
    });

In this case custom validators work as expected.

P.S. I choose this answer as a correct one for me, but I will give bounty to the most relevant answer.

Nazar
  • 1,769
  • 1
  • 15
  • 31
1

You can do this with the context option specified in the mongoose documentation.

The context option

The context option lets you set the value of this in update validators to the underlying query.


So in your code you can define your validator on the path like this:
function validateAvatar (value) {
    // When running update validators with the `context` option set to
    // 'query', `this` refers to the query object.
    return this.getUpdate().$set.active;
}

schema.path('avatar').validate(validateAvatar, 'User is not active');

And while updating you need to enter two options runValidators and context. So your update query becomes:

var opts = { runValidators: true, context: 'query' };
user.update({ _id: id }, { $set: { avatar: 'user1.png' }, opts });
Nazar
  • 1,769
  • 1
  • 15
  • 31
Naeem Shaikh
  • 15,331
  • 6
  • 50
  • 88
  • 1
    Thanks for suggestion, but it doesn't work. `this.getUpdate().$set` is an update query object, which in this case is `{ avatar: 'user1.png' }`. So it still doesn't have current `active` attribute. – Nazar Aug 30 '16 at 06:29
1

Did you try giving active a default value so it would not be undefined in mongodb.

const schema = new mongoose.Schema({
active: { type: Boolean, 'default': false },
avatar: { type: String,
          trim: true,
          'default': '',
          validate: [validateAvatar, 'User is not active']
}});

function validateAvatar (value) {
    console.log(value); // user.avatar
    console.log(this.active); // undefined
}

When creating do you set the user in this way

  var User = mongoose.model('User');
  var user_1 = new User({ active: false, avatar: ''});
  user_1.save(function (err) {
            if (err) {
                return res.status(400).send({message: 'err'});
            }               
            res.json(user_1);                
        });
ramon22
  • 3,528
  • 2
  • 35
  • 48
0

You can try with pre "save" hook. I used it before and can get the value in "this".

schema.pre('save', function (next) {
    console.log(this.active);
});

Hope this work with you too !

Tan Le
  • 187
  • 4
0

You have to use an asynchronous custom validator for that:

const schema = new mongoose.Schema({
  active: { type: Boolean },
  avatar: {
    type     : String,
    validate : {
      validator : validateAvatar,
      message   : 'User is not active'
    }
  }
});

function validateAvatar(v, cb) {
  this.model.findOne({ _id : this.getQuery()._id }).then(user => {
    if (user && ! user.active) {
      return cb(false);
    } else {
      cb();
    }
  });
}

(and pass the runValidators and context options to update(), as suggested in the answer from Naeem).

However, this will require an extra query for each update, which isn't ideal.

As an alternative, you could also consider using something like this (if the constraint of not being able to update inactive users is more important than actually validating for it):

user.update({ _id : id, active : true }, { ... }, ...);
robertklep
  • 198,204
  • 35
  • 394
  • 381
  • 1
    "Both update statements are exactly the same" - Please take a look at `active` attribute described in both of cases. It's different, that's the main thing there. – Nazar Aug 30 '16 at 11:31
  • @Leestex but as you may have noticed, your question is confusing because of that. – robertklep Aug 30 '16 at 11:34
  • I've updated that part of question, hope it isn't such confusing right now – Nazar Aug 30 '16 at 11:39
  • When I am creating a record it says getQuery() is not a function – Ali Abbas May 19 '17 at 11:51
  • @AliAbbas I think `getQuery()` is only available in update validators. But you should probably create a new question if you can't solve your problem. – robertklep May 19 '17 at 12:28