Mongoose 5.5 was released earlier this week. This release includes 12 new features and a performance improvement. The two features I'm most excited about are hooks for user-defined static functions and the ability to pass a function to populate's match option. In this article, I'll introduce these two new features and show how they can save you some design headache.

Hooks for Custom Statics

Statics are Mongoose's implementation of OOP static functions. You add a static function to your schema, and Mongoose attaches it to any model you compile with that schema.

const mongoose = require ( 'mongoose' ); const schema = new mongoose.Schema({ name: String }); schema.statics.findByName = function ( name ) { return this .find({ name }); }; const User = mongoose.model( 'User' , schema); User.findByName( 'test' );

Mongoose 5.5 introduces the ability to add hooks for custom statics, like schema.pre('findByName') . In a custom static's middleware function, this is the model, like insertMany middleware.

schema.pre( 'findByName' , function ( next, name ) { console .log( ` ${this.modelName} .findByName(' ${name} ')` ); next(); }); schema.post( 'findByName' , function ( docs ) { console .log( 'Found' , docs.length, 'docs' ); }); const User = mongoose.model( 'User' , schema); User.findByName( 'test' ). then(() => console .log( 'done' ));

Statics help you consolidate core business logic into reusable functions. Hooks for statics lets you handle cross-cutting concerns that affect all your statics. This is especially powerful when combined with plugins. For example, here's how you can add debug logging to all your statics:

const mongoose = require ( 'mongoose' ); const debug = require ( 'debug' )( 'mongoose:statics' ); const schema = new mongoose.Schema({ name: String , email: String }); schema.statics.findByName = function ( name ) { return this .find({ name }); }; schema.statics.findByEmail = function ( email ) { return this .find({ email }); }; Object .keys(schema.statics).forEach(name => { schema.pre(name, function ( next, arg ) { debug( ` ${this.modelName} . ${name} ( ${arg} )` ); next(); }); }); ( async () => { await mongoose.connect( 'mongodb://localhost:27017/test' , { useNewUrlParser: true }); const User = mongoose.model( 'User' , schema); await User.findByName( 'test' ); await User.findByEmail( 'test@test.co' ); })();

Populate Match Function

The match option for populate lets you add an additional filter to populate() . For example, suppose you have two models, BlogPost and Comment , and you're using soft deletes for comments. When you load a blog post, you want to load the corresponding comments that do not have the deleted property set. Here's how you would ensure that populate() always ignores deleted comments.

const blogPostSchema = new mongoose.Schema({ title: String , authorId: Number }); blogPostSchema.virtual( 'comments' , { ref: 'Comment' , localField: '_id' , foreignField: 'blogPostId' , options: { match: { deleted: { $ne: true } } }, }); const commentSchema = new mongoose.Schema({ _id: Number , blogPostId: mongoose.ObjectId, authorId: Number , deleted: Boolean });

The match option lets you filter out deleted comments. But how about a trickier challenge: finding all self-comments. That is, comments whose authorId is equal to the corresponding blog post's authorId ? Below is an example of populating comments , excluding soft-deleted comments and comments by anyone other than the author of the blog post.

const BlogPost = mongoose.model( 'BlogPost' , blogPostSchema); const Comment = mongoose.model( 'Comment' , commentSchema); let post = await BlogPost.create({ title: 'Mongoose 5.5.0' , authorId: 1 }); await Comment.create([ { _id: 1 , blogPostId: post._id, authorId: 2 }, { _id: 2 , blogPostId: post._id, authorId: 1 }, { _id: 3 , blogPostId: post._id, authorId: 1 , deleted: true } ]); post = await BlogPost.findOne().populate({ path: 'comments' , match: doc => ({ authorId: doc.authorId, deleted: { $ne: true } }) }); console .log(post.comments.length);

Internally, Mongoose relies on the excellent Sift library to power match functions.

Match functions are particularly useful when it comes to roles and hiding results the end user doesn't have permission to see. For example, suppose you want only the author of the blog post to see deleted comments. Here's how you can do that with match functions:

const authorId = 1 post = await BlogPost.find().populate({ path: 'comments' , match: doc => (doc.authorId === authorId ? {} : { deleted: { $ne: true } }) });

Moving On

Hooks for custom statics and populate match functions are just two of the 12 new features in Mongoose 5.5. Some of the other new features are connection-scoped plugins, hooks for Query#distinct() , and the new Query#projection() function. Make sure you upgrade and take advantage of all the new features in Mongoose 5.5!

Are you new to Mongoose and looking to learn? Sign up for a free trial of Pluralsight and check out Mark Scott's excellent video course Introduction to Mongoose for Node.js and MongoDB.