The MongoDB NodeJS team is excited to announce the latest version, 4.0.0, of the popular Mongoose ODM. In addition to supporting MongoDB server 3.0, Mongoose 4 is packed with exciting new features and improvements. I've already covered some of the highlights, including an improved custom validator API and schema validation in the browser. In this article, you'll learn about two more important features: schema validation for update() and query middleware.

In version 3.x, Mongoose did not run validation on update() operations. It only did type casting. For example, Mongoose would convert { $set: { name: 1 } } to { $set: { name: "1" } } if your schema said name was a string. However, Mongoose would allow you to $unset your name field, even if your schema said it was required.

Mongoose 4.0 introduces an option to run validators on update() and findOneAndUpdate() calls. Turning this option on will run validators for all fields that your update() call tries to $set or $unset . For example, suppose you have a schema for breakfasts as shown below.

var breakfastSchema = new mongoose.Schema({ steak: { type: String, required: true, enum: ['flank', 'ribeye'] }, eggs: { type: Number, required: true, min: 2 } }); <p>var Breakfast = mongoose.model('breakfast', breakfastSchema, 'breakfasts');

This schema has 4 validators. Both the steak and eggs fields must be specified. Furthermore, steak must be either "flank" or "ribeye", and eggs must be at least 2. Suppose you run the following update operation.

var updates = { $unset: { steak: true }, $set: { eggs: 0 } }; Breakfast.update({}, updates, callback);

By default this operation will succeed. However, if you set the runValidators option as shown below, you will get an error.

var updates = { $unset: { steak: true }, $set: { eggs: 0 } }; // Note the `runValidators` option Breakfast.update({}, updates, { runValidators: true }, function(err) { console.log(err.errors['steak'].message); console.log(err.errors['eggs'].message); /* * The above error messages output: * "Path `steak` is required." * "Path `eggs` (0) is less than minimum allowed value (2)." */ });

Why do you need to opt-in to run validators on update() ? Update validators don't have access to the entire document - the document being updated might not be in your application's memory at all. Because of this, update validators have two subtle differences that are sufficiently important to warrant a flag.

First, update validators only check $set and $unset operations. Update validators will not check $push or $inc operations. The below update will succeed even if eggs is 2.

var updates = { $inc: { eggs: -1 } }; Breakfast.update({}, updates, callback);

The second and most important difference lies in the fact that, in document validators, this refers to the document being updated. In the case of update validators, there is no underlying document, so this will be null in your custom validators. Built-in validators do not rely on this convention. However, suppose you add a custom validator to your schema:

var breakfastSchema = new mongoose.Schema({ steak: { type: String, required: true, enum: ['flank', 'ribeye'], validate: function(v) { // Woops! `this` is equal to the global object! if (this.eggs >= 4) { return v === 'flank'; } } }, eggs: { type: Number, required: true, min: 2 } }); <p>var Breakfast = mongoose.model('breakfast', breakfastSchema, 'breakfasts');</p> <p>var updates = { $set: { steak: 'ribeye', eggs: 4 } }; Breakfast.findOneAndUpdate({}, updates, { runValidators: true }, function(err) { // No err! });

Why does the above validation pass? Update validators are run using .call(null) , so in the custom validator, this (otherwise known as the function's context) refers to the global object rather than a document. If update validators were enabled by default, they would break because many custom validators use this .

In Mongoose 4.x, you will need to specify the runValidators option every time you call update() to run update validators. But what if you're sure you want to run update validators on every single update operation? Thankfully, setting default update options is just one application of the next feature you will learn about: query middleware.

Pre and Post Hooks for Queries

The other new Mongoose 4 feature you will learn about in this article is pre and post hooks for count() , find() , findOne() , findOneAndUpdate() , and update() . Much like the existing middleware for save() , validate() , and remove() , query middleware allows you to define business logic for handling queries at the schema level. Query middleware will also enable plugins to transform queries. If this sounds vague, don't worry, things will be more clear after you see an example.

Remember when I told you that query middleware would allow you to enable update validators by default? Unlike Arnold Schwarzenegger in the film Commando, I didn't lie. With a small addition to the breakfastSchema from the previous section, you no longer have to specify the runValidators flag for each call to findOneAndUpdate .

var breakfastSchema = new mongoose.Schema({ steak: { type: String, required: true, enum: ['flank', 'ribeye'], }, eggs: { type: Number, required: true, min: 2 } }); <p>// Pre hook for <code>findOneAndUpdate breakfastSchema.pre('findOneAndUpdate', function(next) { this.options.runValidators = true; next(); }); var Breakfast = db.model('breakfast', breakfastSchema, 'breakfasts'); var updates = { $set: { steak: 'ribeye', eggs: 1 } }; // Note the lack of the runValidators option Breakfast.findOneAndUpdate({}, updates, function(err) { // "ValidationError: Path eggs (1) is less than minimum allowed value (2)." console.log(err); });

You can even create a Mongoose plugin to handle this for you:

/* * Any schema with this plugin will run validators on * `findOneAndUpdate()` by default. */ var runValidatorsPlugin = function(schema, options) { schema.pre('findOneAndUpdate', function(next) { this.options.runValidators = true; next(); }); }; <p>var breakfastSchema = new mongoose.Schema({ steak: { type: String, required: true, enum: ['flank', 'ribeye'], }, eggs: { type: Number, required: true, min: 2 } });</p> <p>// Attach the plugin to <code>breakfastSchema breakfastSchema.plugin(runValidatorsPlugin);

Using Query Hooks to Automatically populate()

One of the most requested Mongoose features is the ability to automatically populate() documents. The populate() function is, at a surface level, analogous to an SQL join. Suppose you have two separate schemas for two separate collections, one for bands and one for lead singers of these bands, as shown below.

var personSchema = new mongoose.Schema({ name: String }); <p>var bandSchema = new mongoose.Schema({ name: String, lead: { type: mongoose.Schema.Types.ObjectId, ref: 'person' } });</p> <p>var Person = mongoose.model('person', personSchema, 'people'); var Band = mongoose.model('band', bandSchema, 'bands');</p> <p>var axl = new Person({ name: 'Axl Rose' }); var gnr = new Band({ name: "Guns N' Roses", lead: axl._id }); // Save Person and Band

The populate() function allows you to load a Band and its corresponding lead singer in one function call:

Band. findOne({ name: "Guns N' Roses" }). populate('lead'). exec(function(err, band) { console.log(band.lead.name); // "Axl Rose" });

But what if you always wanted to load the lead singer every time you loaded a band? Enter find() and findOne() middleware.

var bandSchema = new mongoose.Schema({ name: String, lead: { type: mongoose.Schema.Types.ObjectId, ref: 'person' } }); <p>var autoPopulateLead = function(next) { this.populate('lead'); next(); };</p> <p>bandSchema. pre('findOne', autoPopulateLead). pre('find', autoPopulateLead);</p> <p>var Band = mongoose.model('band', bandSchema, 'bands');

Once you add this middleware, every time you load a Band , Mongoose will also pull in the lead singer for you. If you don’t want to implement this yourself, don’t worry, there’s a mongoose-autopopulate plugin available on npm.

Band. findOne({ name: "Guns N' Roses" }). exec(function(err, band) { console.log(band.lead.name); // "Axl Rose" });

Conclusion

I hope this post has gotten you as excited about Mongoose 4 as we at MongoDB are. In this article, you barely scratched the surface of what you can do with query middleware. Some other great applications include performance profiling, last modified fields for update() , and disallowing certain update() options at the schema level. Update validators will allow you to make your applications more performant by allowing you to run validators without loading the whole document into memory. Try these features out by running npm install mongoose and open up any issues you find on Mongoose's GitHub. I look forward to seeing what plugins and applications you're going to build with Mongoose 4!

If you’re interested in learning more about the architecture of MongoDB, download our guide:

About the Author - Valeri

Valeri Karpov is a NodeJS Engineer at MongoDB, where he maintains mongoose, mongoskin, connect-mongodb-session, and several other MongoDB-related NodeJS modules. He's also the author of Professional AngularJS, a blogger for StrongLoop, and gave the MEAN stack its name. He blogs about NodeJS, MongoDB, and related topics at www.thecodebarbarian.com.