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.
Validators for update() and findOneAndUpdate()
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.
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.
By default this operation will succeed. However, if you set the runValidators option as shown below, you will get an error.
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.
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:
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.
You can even create a Mongoose plugin to handle this for you:
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.
The populate() function allows you to load a Band and its corresponding lead singer in one function call:
But what if you always wanted to load the lead singer every time you loaded a band? Enter find() and findOne() middleware.
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.
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!
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.