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 });
// `findByName` is a static function. `this` is the model in this context.
schema.statics.findByName = function(name) {
return this.find({ name });
};
const User = mongoose.model('User', schema);
User.findByName('test'); // Equivalent to `User.find({ name: '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.
// Can define pre and post hooks for `findByName` in Mongoose 5.5
schema.pre('findByName', function(next, name) {
// Prints "User.findByName('test')"
console.log(`${this.modelName}.findByName('${name}')`);
next();
});
schema.post('findByName', function(docs) {
// Prints "Found 0 docs"
console.log('Found', docs.length, 'docs');
});
// Compile the model and run `findByName()`
const User = mongoose.model('User', schema);
User.findByName('test').
then(() => console.log('done')); // Hooked `findByName` returns a promise
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');
// Create schema
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 });
};
// Add debug hooks for every static
Object.keys(schema.statics).forEach(name => {
schema.pre(name, function(next, arg) {
debug(`${this.modelName}.${name}(${arg})`);
next();
});
});
// Use the model
(async () => {
await mongoose.connect('mongodb://localhost:27017/test', { useNewUrlParser: true });
const User = mongoose.model('User', schema);
await User.findByName('test'); // Prints "mongoose:statics User.findByName(test) +0ms"
await User.findByEmail('test@test.co'); // Prints "mongoose:statics User.findByEmail(test@test.co) +10ms"
})();
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.
// Create schemas
const blogPostSchema = new mongoose.Schema({
title: String,
authorId: Number
});
blogPostSchema.virtual('comments', {
ref: 'Comment',
localField: '_id',
foreignField: 'blogPostId',
// When populating comments, exclude comments that have `deleted`
// set to `true`
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',
// If match is a function, it receives the blog post doc as a parameter
match: doc => ({ authorId: doc.authorId, deleted: { $ne: true } })
});
console.log(post.comments.length); // 1
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!
Want to become your team's MongoDB expert? "Mastering Mongoose" distills 8 years of hard-earned lessons building Mongoose apps at scale into 153 pages. That means you can learn what you need to know to build production-ready full-stack apps with Node.js and MongoDB in a few days. Get your copy!