Mongoose: Mongoose findOneAndUpdate and runValidators not working

Created on 30 Jun 2015  路  9Comments  路  Source: Automattic/mongoose

I am having issues trying to get the 'runValidators' option to work. My user schema has an email field that has required set to true but each time a new user gets added to the database (using the 'upsert' option) and the email field is empty it does not complain:

 var userSchema = new mongoose.Schema({
   facebookId: {type: Number, required: true},
   activated: {type: Boolean, required: true, default: false},
   email: {type: String, required: true}
});

findOneAndUpdate code:

model.user.user.findOneAndUpdate(
      {facebookId: request.params.facebookId},
      {
          $setOnInsert: {
              facebookId: request.params.facebookId,
              email: request.payload.email,
          }
      },
      {upsert: true, 
       new: true, 
       runValidators: true, 
       setDefaultsOnInsert: true
      }, function (err, user) {
          if (err) {
              console.log(err);
              return reply(boom.badRequest(authError));
          }
          return reply(user);
      });

I have no idea what I am doing wrong, I just followed the docs: http://mongoosejs.com/docs/validation.html

In the docs is says the following:

Note that in mongoose 4.x, update validators only run on $set and $unset operations. For instance, the below update will succeed, regardless of the value of number.

I replaced the $setOnInsert with $set but had the same result.

won't fix

Most helpful comment

So the below script:

var assert = require('assert');
var mongoose = require('mongoose');
var Schema = mongoose.Schema;
mongoose.set('debug', true);
mongoose.connect('mongodb://localhost:27017/gh3124');

var userSchema = new mongoose.Schema({
   facebookId: {type: Number, required: true},
   activated: {type: Boolean, required: true, default: false},
   email: {type: String, required: true}
});

var User = mongoose.model('gh3124', userSchema);

User.findOneAndUpdate(
  {facebookId: 123},
  {
    $setOnInsert: {
      facebookId: 123,
      email: undefined,
    }
  },
  {upsert: true,     
   'new': true,   
   runValidators: true,     
   setDefaultsOnInsert: true
  }, function (err, user) {
    console.log(err);
    process.exit(0);
  });

Gives me a validation error as expected in 4.0.6. Can you run that script locally and double check that it works for you @Jdruwe ?

All 9 comments

So the below script:

var assert = require('assert');
var mongoose = require('mongoose');
var Schema = mongoose.Schema;
mongoose.set('debug', true);
mongoose.connect('mongodb://localhost:27017/gh3124');

var userSchema = new mongoose.Schema({
   facebookId: {type: Number, required: true},
   activated: {type: Boolean, required: true, default: false},
   email: {type: String, required: true}
});

var User = mongoose.model('gh3124', userSchema);

User.findOneAndUpdate(
  {facebookId: 123},
  {
    $setOnInsert: {
      facebookId: 123,
      email: undefined,
    }
  },
  {upsert: true,     
   'new': true,   
   runValidators: true,     
   setDefaultsOnInsert: true
  }, function (err, user) {
    console.log(err);
    process.exit(0);
  });

Gives me a validation error as expected in 4.0.6. Can you run that script locally and double check that it works for you @Jdruwe ?

This gives me the following error:

{ email:
{ [ValidatorError: Path email is required.]

What I think is weird is the following: email is required but if I don't mention it in the setOnInsert is does not complain, if its value is undefined it does validate.

Ah yeah that's a good point. Update validators right now only run on fields that are mentioned in the update (and the query, if upsert is specified), because mongoose really has no way of knowing if the documents being updated on the server side have an email field or not. There are definitely ways to be smarter about this, but right now update validators are somewhat limited.

I created a plugin to validate required model properties before doing update operations in mongoose.

var mongoose = require('mongoose');
var _ = require('lodash');
var s = require('underscore.string');

function validateExtra(schema, options){
    schema.methods.validateRequired = function(){
        var deferred = Promise.defer();
        var self = this;
        try {
            _.forEach(this.schema.paths, function (val, key) {
                if (val.isRequired && _.isUndefined(self[key])) {
                    throw new Error(s.humanize(key) + ' is not set and is required');
                }
            });
            deferred.resolve();
        } catch (err){
            deferred.reject(err);
        }
        return deferred.promise;
    }
}

module.exports = validateExtra;

Must be called explicitly as a method from the model, so I recommend chaining it a .then chain prior to the update call.

fuelOrderModel(postVars.fuelOrder).validateRequired()
        .then(function(){
            return fuelOrderModel.findOneAndUpdate({_id: postVars.fuelOrder.fuelOrderId}, postVars.fuelOrder,
                {runValidators: true, upsert: true, setDefaultsOnInsert: true, new: true})
                    .then(function(doc) {
                        res.json({fuelOrderId: postVars.fuelOrder.fuelOrderId});
                    });
        }, function(err){
                global.saveError(err, 'server', req.user);
                res.status(500).json(err);
        });

@jeremyml glad that works for you :) I'd recommend only running the validation if upsert is true, since otherwise the underlying doc may have the field already set.

so this is close but the issue still exists.

@mylastore please open a new issue and follow the issue template.

Was this page helpful?
0 / 5 - 0 ratings