46

I want to use mongoose custom validation to validate if endDate is greater than startDate. How can I access startDate value? When using this.startDate, it doesn't work; I get undefined.

var a = new Schema({
  startDate: Date,
  endDate: Date
});

var A = mongoose.model('A', a);

A.schema.path('endDate').validate(function (value) {
  return diff(this.startDate, value) >= 0;
}, 'End Date must be greater than Start Date');

diff is a function that compares two dates.

Talha Awan
  • 4,573
  • 4
  • 25
  • 40
amgohan
  • 1,358
  • 3
  • 14
  • 24

6 Answers6

106

You can do that using Mongoose 'validate' middleware so that you have access to all fields:

ASchema.pre('validate', function(next) {
    if (this.startDate > this.endDate) {
        next(new Error('End Date must be greater than Start Date'));
    } else {
        next();
    }
});

Note that you must wrap your validation error message in a JavaScript Error object when calling next to report a validation failure. 

Austen
  • 1,931
  • 2
  • 19
  • 28
JohnnyHK
  • 305,182
  • 66
  • 621
  • 471
  • I have in my controller in nodejs exports.create = function (req, res, next) { var a = new A(req.body); // I added your code here but it does'nt work newEvent.save(function(err) { if (err) { return res.json(400, err); } return res.json({message:'success'}); }); @JohnnyHK : Where am I supposed to add your code example Thnks – amgohan May 21 '14 at 00:47
  • 2
    This is a much cleaner approach than the accepted answer, thanks @JohnnyHK – Dana Woodman Feb 19 '16 at 19:23
  • I tried it but the error is not returned to the `entity.save()` callback. – Adrien G Sep 05 '16 at 11:45
  • 1
    @AdrienG Go ahead and post a new question with the full details if you still need help. – JohnnyHK Sep 05 '16 at 18:45
  • 2
    Maybe it's my fault but I found another way to do it: I used instead `this.invalidate('myField', 'my error message.', this.myField);` so it's ok thanks :) – Adrien G Sep 06 '16 at 09:09
  • In case the error is not showing up for anyone, make sure to add `new` before `Error` when creating the error object. See [MDN](https://developer.mozilla.org/en-US/docs/Web/JavaScript/Reference/Global_Objects/Error). – Austen Nov 10 '17 at 23:59
  • @Austen The `new` is optional in the node.js implementation of the `Error` constructor, so either way is fine. – JohnnyHK Nov 11 '17 at 00:35
  • 1
    Be aware `pre('validate',...` middleware does not execute for `findOneAndUpdate()` https://github.com/Automattic/mongoose/issues/964 – steampowered Feb 22 '18 at 14:46
38

An an alternative to the accepted answer for the original question is:

var mongoose = require('mongoose'),
  Schema = mongoose.Schema;

// schema definition
var ASchema = new Schema({
  startDate: {
    type: Date,
    required: true
  },
  endDate: {
    type: Date,
    required: true,
    validate: [dateValidator, 'Start Date must be less than End Date']
  }
});

// function that validate the startDate and endDate
function dateValidator(value) {
  // `this` is the mongoose document
  return this.startDate <= value;
}
Jason Cust
  • 10,743
  • 2
  • 33
  • 45
  • Great! And if you require one of two fields you can create the validation on a required field, but check what you need via `this`. – webjay Apr 23 '15 at 00:34
  • 10
    this option fails when use .findOneAndUpdate with runValidators options. this has wrong context so .startDate will not be available :( – Denis May 17 '16 at 10:40
  • Same problem when using findByIdAndUpdate with runValidators:true as Denis mentioned – Julius Jul 08 '22 at 10:30
  • @Denis For me [this](https://stackoverflow.com/a/70087952/4222504) solution resolved the issue that you mentioned. Also, check out my comment under the linked answer. Hope this help! – Véger Lóránd Oct 20 '22 at 12:41
31

I wanted to expand upon the solid answer from @JohnnyHK (thank you) by tapping into this.invalidate:

Schema.pre('validate', function (next) {
  if (this.startDate > this.endDate) {
    this.invalidate('startDate', 'Start date must be less than end date.', this.startDate);
  }

  next();
});

This keeps all of the validation errors inside of a mongoose.Error.ValidationError error. Helps to keep error handlers standardized. Hope this helps.

kognizant
  • 311
  • 3
  • 3
  • Just what I needed! Now I just need to figure out why mongoose isn't hitting the validate hook before saving... – Corbfon Sep 13 '17 at 00:06
  • 2
    Nix the above, this solution is perfect. It should be the accepted. Thanks @kognizant !!! – Corbfon Sep 13 '17 at 00:37
29

You could try nesting your date stamps in a parent object and then validate the parent. For example something like:

//create a simple object defining your dates
var dateStampSchema = {
  startDate: {type:Date},
  endDate: {type:Date}
};

//validation function
function checkDates(value) {
   return value.endDate < value.startDate; 
}

//now pass in the dateStampSchema object as the type for a schema field
var schema = new Schema({
   dateInfo: {type:dateStampSchema, validate:checkDates}
});
Tom Makin
  • 3,203
  • 23
  • 23
6

Using 'this' within the validator works for me - in this case when checking the uniqueness of email address I need to access the id of the current object so that I can exclude it from the count:

var userSchema = new mongoose.Schema({
  id: String,
  name: { type: String, required: true},
  email: {
    type: String,
    index: {
      unique: true, dropDups: true
    },
    validate: [
      { validator: validator.isEmail, msg: 'invalid email address'},
      { validator: isEmailUnique, msg: 'Email already exists'}
    ]},
  facebookId: String,
  googleId: String,
  admin: Boolean
});

function isEmailUnique(value, done) {
  if (value) {
    mongoose.models['users'].count({ _id: {'$ne': this._id }, email: value }, function (err, count) {
      if (err) {
        return done(err);
      }
      // If `count` is greater than zero, "invalidate"
      done(!count);
    });
  }
}
prule
  • 2,536
  • 31
  • 32
1

This is the solution I used (thanks to @shakinfree for the hint) :

var mongoose = require('mongoose'),
Schema = mongoose.Schema;

// schema definition
var ASchema = new Schema({
  dateSchema : {
                type:{
                    startDate:{type:Date, required: true}, 
                    endDate:{type:Date, required: true}
                }, 
                required: true, 
                validate: [dateValidator, 'Start Date must be less than End Date']
            }
});

// function that validate the startDate and endDate
function dateValidator (value) {
    return value.startDate <= value.endDate;
}

module.exports = mongoose.model('A', ASchema);
amgohan
  • 1,358
  • 3
  • 14
  • 24
  • Be carefully with this aproach because there is bug in 4.01 https://github.com/Automattic/mongoose/issues/2814 – Erik Apr 02 '15 at 13:55