Adam Zerner
Adam Zerner

Reputation: 19178

How do you validate that a property of a nested document is present when that nested document exists?

user.schema.js

var Schema = require('mongoose').Schema;
var uniqueValidator = require('mongoose-unique-validator');
var _ = require('lodash');

var userSchema = new Schema({
  local: {
    username: String, // should exist when local exists
    role: String,
    hashedPassword: { type: String, select: false }
  },

  facebook: {
    id: String,
    token: { type: String, select: false }
  },

  twitter: {
    id: String,
    token: { type: String, select: false }
  },

  google: {
    id: String,
    token: { type: String, select: false }
  }
});

userSchema.path('local').validate(function(local) {
  var empty = _.isEmpty(local);
  if (empty) {
    return true;
  }
  else if (!empty && local.username) {
    return true;
  }
  else if (!empty && !local.username) {
    return false;
  }
}, 'Local auth requires a username.');

module.exports = userSchema;

I'm trying to validate that username is present when local isn't empty. Ie. when using local authentication, username should be present.

// should validate
user = {
  local: {
    username: 'foo';
    hashedPassword: 'sfsdfs'
  }
};

// shouldn't validate
user = {
  local: {
    hashedPassword: 'sdfsdfs'
  }
};

// should validate (because local isn't being used)
user = {
  local: {},
  facebook {
    ...
  }
};

I get this error:

/Users/azerner/code/mean-starter/server/api/users/user.schema.js:51
userSchema.path('local').validate(function(local) {
                        ^
TypeError: Cannot read property 'validate' of undefined

It seems that you can't get the path of objects. I learned here that Schemas have a paths property. When I console.log(userSchema.paths):

{ 'local.username':
   { enumValues: [],
     regExp: null,
     path: 'local.username',
     instance: 'String',
     validators: [],
     setters: [],
     getters: [],
     options: { type: [Function: String] },
     _index: null },
  'local.role':
   { enumValues: [],
     regExp: null,
     path: 'local.role',
     instance: 'String',
     validators: [],
     setters: [],
     getters: [],
     options: { type: [Function: String] },
     _index: null },
  'local.hashedPassword':
   { enumValues: [],
     regExp: null,
     path: 'local.hashedPassword',
     instance: 'String',
     validators: [],
     setters: [],
     getters: [],
     options: { type: [Function: String], select: false },
     _index: null,
     selected: false },
  'facebook.id':
   { enumValues: [],
     regExp: null,
     path: 'facebook.id',
     instance: 'String',
     validators: [],
     setters: [],
     getters: [],
     options: { type: [Function: String] },
     _index: null },
  'facebook.token':
   { enumValues: [],
     regExp: null,
     path: 'facebook.token',
     instance: 'String',
     validators: [],
     setters: [],
     getters: [],
     options: { type: [Function: String], select: false },
     _index: null,
     selected: false },
  'twitter.id':
   { enumValues: [],
     regExp: null,
     path: 'twitter.id',
     instance: 'String',
     validators: [],
     setters: [],
     getters: [],
     options: { type: [Function: String] },
     _index: null },
  'twitter.token':
   { enumValues: [],
     regExp: null,
     path: 'twitter.token',
     instance: 'String',
     validators: [],
     setters: [],
     getters: [],
     options: { type: [Function: String], select: false },
     _index: null,
     selected: false },
  'google.id':
   { enumValues: [],
     regExp: null,
     path: 'google.id',
     instance: 'String',
     validators: [],
     setters: [],
     getters: [],
     options: { type: [Function: String] },
     _index: null },
  'google.token':
   { enumValues: [],
     regExp: null,
     path: 'google.token',
     instance: 'String',
     validators: [],
     setters: [],
     getters: [],
     options: { type: [Function: String], select: false },
     _index: null,
     selected: false },
  _id:
   { path: '_id',
     instance: 'ObjectID',
     validators: [],
     setters: [ [Function: resetId] ],
     getters: [],
     options: { type: [Object], auto: true },
     _index: null,
     defaultValue: [Function: defaultId] } }

So it seems that paths like local.username and facebook.token exist, but not "top level" paths like local and facebook.

If I try to validate the local.username path, it doesn't work like I want it to.

userSchema.path('local.username').validate(function(username) {
  return !!username
}, 'Local auth requires a username.');

The validation is only applied when local.username exists. I want to validate that it exists. So when it doesn't exist, the validation isn't applied, and thus it's considered valid and gets saved.

I also tried the following approach, but the outcome is the same as the local.username approach (validation doesn't get hit when the username isn't present, and it gets labeled as valid).

var Schema = require('mongoose').Schema;
var uniqueValidator = require('mongoose-unique-validator');
var _ = require('lodash');

var userSchema = new Schema({
  local: {
    username: {
      type: String,
      validate: [validateUsernameRequired, 'Local auth requires a username.']
    },
    role: String,
    hashedPassword: { type: String, select: false }
  },

  facebook: {
    id: String,
    token: { type: String, select: false }
  },

  twitter: {
    id: String,
    token: { type: String, select: false }
  },

  google: {
    id: String,
    token: { type: String, select: false }
  }
});

function validateUsernameRequired(username) {
  return !!username;
}

module.exports = userSchema;

Upvotes: 4

Views: 306

Answers (2)

Zeke Nierenberg
Zeke Nierenberg

Reputation: 2206

Adam, why don't you try a pre-validate hook that conditionally passes an error to the next function. I think this'll give you the flexibility you're looking for. Let me know if it doesn't work.

For example

schema.pre('validate', function(next) {
  if(/*your error case */){ next('validation error text') }
  else { next() }
})

This will cause mongoose to send a ValidationError back to whoever tried to save the document.

Upvotes: 1

hswets
hswets

Reputation: 61

Looks like you are trying to create a custom validation. Not sure if you implemented everything you need for it. It looks like this:

// make sure every value is equal to "something"
function validator (val) {
  return val == 'something';
}
new Schema({ name: { type: String, validate: validator }});

// with a custom error message

var custom = [validator, 'Uh oh, {PATH} does not equal "something".']
new Schema({ name: { type: String, validate: custom }});

// adding many validators at a time

var many = [
    { validator: validator, msg: 'uh oh' }
  , { validator: anotherValidator, msg: 'failed' }
]
new Schema({ name: { type: String, validate: many }});

// or utilizing SchemaType methods directly:

var schema = new Schema({ name: 'string' });
schema.path('name').validate(validator, 'validation of `{PATH}` failed with 
value `{VALUE}`');

here is the link: mongoose custom validation

Upvotes: 0

Related Questions