NodeJS MongoDB : Schema Validation, Middleware

In this blog post we will see advance usage of mongoose library.

In the previous blog post we saw how to perform various operations on mongodb in fast way. We didn’t define any schema , validation and other feature. In this post we will see how to to use all mongoose features.

Collection Schema

Each collection on your mongo database is represented using a schema. Schema are basically column definitions of your collection, which you provide to mongoose to help out in validations, type checking and other operations.

Lets first define a very simple Schema and use it

  
var userSchema = mongoose.Schema({
      
firstname: String,
      
lastname: String,
      
created_at: Date,
      
is_active: Boolean
  
});

var User = conn.model('User', userSchema);

var u1 = new User({
      
firstname: 'Manish',
      
lastname: 'Prakash',
      
created_at: new Date(),
      
is_active: true
  
});
  
u1.save(function (err) {
      
if (err) {
          
console.log(err);
      
} else {
          
console.log('Done');
      
}
  
});
  

This is how it gets saved in mongodb, using all proper data types.

  
{
  
"_id": ObjectId("548d0b64fbcb585c0b8292c4"),
     
"firstname": "Manish",
     
"lastname": "Prakash",
     
"created_at": ISODate("2014-12-14T04:00:36.811Z"),
     
"is_active": true,
     
"__v": NumberInt(0)
  
}
  

Schema Validations

We can also extend schema for validation of our objects. I will expand our user schema to include more options

  
var allowedTypes = ['Level1′,'Level2′,'Level3'];
  
var userSchema = mongoose.Schema({
      
firstname: String,
      
lastname: String,
      
created_at: Date,
      
is_active: Boolean,
      
meta: {
          
friends: Number,
          
likes: Number,
          
votes: Number,
          
dislikes: Number
      
},
      
comments: [{body: String, date: Date}],
      
updated_at: {type: Date, default: Date.now},
      
age: {type: Number, min: 5, max: 40},
      
type: {type: String, enum: allowedTypes},
      
username: {type: String, lowercase: true, required: true, trim: true},
      
internal\_name: {type: String, match: /int\_/},
      
last\_payment\_date: {type: Date, default: Date.now, expires: 60 \* 60 \* 31}
  
});
  
var User = conn.model('User', userSchema);
  
var u1 = new User({
      
firstname: 'Manish',
      
lastname: 'Prakash',
      
created_at: new Date(),
      
is_active: true,
      
username : 'manish_iitg'
  
});
  
u1.save(function (err) {
      
if (err) {
          
console.log(err);
      
} else {
          
console.log('Done');
          
process.exit();
      
}
  
});
  

This is what we see in our database

   
{
     
"_id": ObjectId("548d0f141b41baf82259d815"),
     
"firstname": "Manish",
     
"lastname": "Prakash",
     
"created_at": ISODate("2014-12-14T04:16:20.542Z"),
     
"is_active": true,
     
"username": "manish_iitg",
     
"last\_payment\_date": ISODate("2014-12-14T04:16:20.544Z"),
     
"updated_at": ISODate("2014-12-14T04:16:20.543Z"),
     
"comments": [

],
     
"__v": NumberInt(0)
  
}
  

As you can see ‘last_payment_date’ and ‘updated_at’ have been filled up with default values.

If we try to save this model

  
var User = conn.model('User', userSchema);
  
var u1 = new User({
      
firstname: 'Manish',
      
lastname: 'Prakash',
      
created_at: new Date(),
      
is_active: true,
      
type : 'Invalid',
      
age : 2,
      
internal_name : 'xyz'
  
});
  
u1.save(function (err) {
      
if (err) {
          
console.log(err);
      
} else {
          
console.log('Done');
          
process.exit();
      
}
  
});
  

We get a bunch of error message due to the validation applied.

    
{ [ValidationError: User validation failed]
    
message: 'User validation failed',
    
name: 'ValidationError',
    
errors:
     
{ username:
        
{ [ValidatorError: Path \`username\` is required.]
          
properties: [Object],
          
message: 'Path \`username\` is required.',
          
name: 'ValidatorError',
          
kind: 'required',
          
path: 'username',
          
value: undefined },
       
internal_name:
        
{ [ValidatorError: Path \`internal_name\` is invalid (xyz).]
          
properties: [Object],
          
message: 'Path \`internal_name\` is invalid (xyz).',
          
name: 'ValidatorError',
          
kind: 'regexp',
          
path: 'internal_name',
          
value: 'xyz' },
       
age:
        
{ [ValidatorError: Path \`age\` (2) is less than minimum allowed value (5).]
          
properties: [Object],
          
message: 'Path \`age\` (2) is less than minimum allowed value (5).',
          
name: 'ValidatorError',
          
kind: 'min',
          
path: 'age',
          
value: 2 },
       
type:
        
{ [ValidatorError: \`Invalid\` is not a valid enum value for path \`type\`.]
          
properties: [Object],
          
message: '\`Invalid\` is not a valid enum value for path \`type\`.',
          
name: 'ValidatorError',
          
kind: 'enum',
          
path: 'type',
          
value: 'Invalid' } } }

We have also used another schema option ‘expires’ in ‘last_payment_date’, this basically sets the seconds after which a document should be deleted automatically by mongo. It works on version v2.4 and later. More details here http://docs.mongodb.org/manual/tutorial/expire-data/

There are also custom mongoose schema type which you can include in your collection like Email, URL more details here https://www.npmjs.com/package/openifyit-mongoose-types

Schema Custom Validation

Lets add a custom validation for 10 digit mobile number.

  
var allowedTypes = ['Level1′,'Level2′,'Level3'];
  
var userSchema = mongoose.Schema({
      
firstname: String,
      
lastname: String,
      
created_at: Date,
      
is_active: Boolean,
      
meta: {
          
friends: Number,
          
likes: Number,
          
votes: Number,
          
dislikes: Number
      
},
      
comments: [{body: String, date: Date}],
      
updated_at: {type: Date, default: Date.now},
      
age: {type: Number, min: 5, max: 40},
      
type: {type: String, enum: allowedTypes},
      
username: {type: String, lowercase: true, required: true, trim: true},
      
internal\_name: {type: String, match: /int\_/},
      
last\_payment\_date: {type: Date, default: Date.now, expires: 60 \* 60 \* 31},
      
phone: {
          
type: String,
          
required: true,
          
validate: {
              
validator: function(v) {
                  
return /^[0-9]{10}$/.test(v);
              
},
              
message: '{VALUE} is not a valid phone number!'
          
}
      
},
  
});

var User = conn.model('User', userSchema);
  
var u1 = new User({
      
firstname: 'Manish',
      
lastname: 'Prakash',
      
created_at: new Date(),
      
is_active: true,
      
username : 'manish_iitg',
      
phone : '987654'
  
});
  
u1.save(function (err) {
      
if (err) {
          
console.log(err);
      
} else {
          
console.log('Done');
          
process.exit();
      
}
  
});
  

The above will throw a validation error. Since 10 digit phone number is required.

Below code with 10 digit phone number will work.

  
var u1 = new User({
      
firstname: 'Manish',
      
lastname: 'Prakash',
      
created_at: new Date(),
      
is_active: true,
      
username : 'manish_iitg',
      
phone : '9876543210'
  
});
  

Schema Instance Methods and Static Methods

We can set different functions to our Schema as static and instance methods. Static method apply to model directly and instance methods apply to model documents. Let see how it works

Instance Methods

  
userSchema.methods.isLevelAllowed = function (cb) {
      
var age = this.age;
      
var type = this.type;
      
var model = this.model('User');
      
//can perform any database operation on the model as well
      
if (age == 10 && (type == 'Level2' || type == 'Level3')) {
          
cb(false, false);
      
} else {
          
cb(false, true);
      
}
  
}
  
var User = conn.model('User', userSchema);
  
//make sure model is created after instance method is defined
  
var u1 = new User({
      
firstname: 'Manish',
      
lastname: 'Prakash',
      
created_at: new Date(),
      
is_active: true,
      
username: 'manish_iitg',
      
type: 'Level3',
      
age: 5,
      
internal\_name: 'int\_xetro',
      
meta: {
          
likes: 1
      
}
  
});
  
//isLevelAllowed is an instance method, hence used on a document.
  
u1.isLevelAllowed(function (err, allowed) {
      
if (allowed) {
          
console.log('Allowed');
      
} else {
          
console.log('Not Allowed');
      
}
      
process.exit();
  
});
  

Static Method

  
userSchema.statics.findAllByLevel = function (level, cb) {
      
this.find({
          
level: level
      
}, function (err, result) {
          
cb(err, result);
      
})
  
}
  
var User = conn.model('User', userSchema);
  
User.findAllByLevel('Level1', function (err, results) {

})
  
//static methods get directly called on model itself.
  

Schema Indexes

Mongodb also supports many indexes which we can apply using schemas

  
var userSchema = mongoose.Schema({
      
firstname: String,
      
lastname: String,
      
created_at: Date,
      
is_active: Boolean,
      
email : {type:String,required:true,index:{unique:true}}, //field level
      
meta: {
          
friends: Number,
          
likes: {type: Number, default: -1},
          
votes: Number,
          
dislikes: Number
      
},
      
comments: [{body: String, date: Date}],
      
updated_at: {type: Date, default: Date.now},
      
age: {type: Number, min: 5, max: 40},
      
type: {type: String, enum: allowedTypes},
      
username: {type: String, lowercase: true, required: true, trim: true},
      
internal\_name: {type: String, match: /int\_/},
      
last\_payment\_date: {type: Date, default: Date.now, expires: 60 \* 60 \* 31}
  
});
  
userSchema.index({firstname:-1,lastname:-1}); //schema level
  

If for some reason indexes are not getting created, restart mongodb server and check if index constraints are not failing already. Which means for index unique on email field, if you db already has duplicate emails indexes won’t work.
You can view indexes in rockmongo from DB -> Collection > More -> Indexes

Mongoose also tries to create indexes on each type application start, which causes overheads. To stop this do

  
userSchema.set('autoIndex',false);
  

There are few other schema options which can be seen here http://mongoosejs.com/docs/guide.html#autoIndex

Schema Middleware

Middleware are basically functions which are called during the flow of execution of a model. There are two types of middleware ‘pre’ and ‘post’ and 4 different execution flow methods init, validate, save and remove.

  
userSchema.pre('save', function (next) {
    
// do stuff
    
next();
  
});
  

if some async operation is required

  
userSchema.pre('save',true, function (next,done) {
    
// do stuff
    
next();
    
asyncOperation(done);
  
});
  

so model ‘save’ will only be called when all asyncOperations are done. We can also pass error objects to middleware

  
userSchema.pre('save', function (next) {
    
var err = new Error('something went wrong');
    
next(err);
  
});
  

More details can be seen here http://mongoosejs.com/docs/middleware.html

Schema Population

Mongodb doesn’t have a concept of joins, but when we need to store relationships we use multiple collection and ObjectIds for the same. Mongoose provides an easy way to load collections with relationships, if we define it in scheme.

I will create two schema to demonstrate the same

  
var userSchema = mongoose.Schema({
      
firstname: String,
      
lastname: String,
      
created_at: {type: Date, default: Date.now},
      
is_active: {type: Boolean, default: true},
      
email: {type: String, required: true, index: {unique: true}},
      
comments: [{type: Schema.Types.ObjectId, ref: 'Comment'}]
  
});
  
var User = conn.model('User', userSchema);

var commentSchema = mongoose.Schema({
      
text: String,
      
created_at: {type: Date, default: Date.now},
      
by: {type: Schema.Types.ObjectId, ref: 'User'}
  
});

var Comment = conn.model('Comment', commentSchema);
  

We use the ‘ref’ parameter to define relationships. With the code below i will insert data into database as per the relationship.

Till now everything is normal, we have inserted 2 users and 2 comments. Now we can use the ‘populate’ method to load relationship automatically e.g

  
User.find({
      
email: '[email protected]'
  
}).populate('comments').lean().exec(function (err, result) {
      
console.log(result);
  
});
  

This will load replace comment_ids with comment objects automatically. We can also add extra where condition in the populate function as below

  
User.find({
      
email: '[email protected]'
  
}).populate({
      
path: 'comments',
      
match: {text: /Comment/i}
  
}).lean().exec(function (err, result) {
      
console.log(result);
  
});
  

As we can easily populate comment_ids with comment objects, we can easily do the reverse.

  
var u1 = new User({
      
firstname: 'Manish',
      
lastname: 'Prakash',
      
email: '[email protected]',
      
comments: []
  
});

u1.comments.push(new Comment({
      
text: 'Text1'
  
}));
  
u1.comments.push(new Comment({
      
text: 'Text2'
  
}));

u1.save();
  

This will automatically replace ‘comments’ with ObjectIds.

excellence-social-linkdin
excellence-social-facebook
excellence-social-instagram
excellence-social-skype