Mongoose 5.9.0 was released on February 13, 2020. As a semver minor version, Mongoose 5.9 introduces several important new features. In this article, I'll provide an overview of 2 of my favorite features: default options per SchemaType and the perDocumentLimit option for populate() .

Default Options Per SchemaType

Mongoose schema types have several handy options. For example, Mongoose strings have a handy trim option that removes leading and trailing whitespace from the string automatically using JavaScript's built-in String#trim() method:

const mongoose = require ( 'mongoose' ); const schema = mongoose.Schema({ email: { type: String , trim: true } }); const User = mongoose.model( 'User' , schema); const doc = new User({ email: 'john@mongoosejs.com ' }); doc.email;

The trim option is useful enough that often people want to add it to every string path in their schema. Before Mongoose 5.9, you would have to add trim: true to every string path in every schema you define. With Mongoose 5.9, that is now a one liner:

const mongoose = require ( 'mongoose' ); mongoose.Schema.Types.String.set( 'trim' , true ); const schema = mongoose.Schema({ email: String }); const User = mongoose.model( 'User' , schema); const doc = new User({ email: 'john@mongoosejs.com ' }); doc.email;

Suppose you want to trim almost every string, but have one or two exceptions where you don't want to set trim . Mongoose lets you set trim: false to overwrite the default.

mongoose.Schema.Types.String.set( 'trim' , true ); const schema = mongoose.Schema({ code: { type: String , trim: false } }); const Solution = mongoose.model( 'Solution' , schema); const doc = new Solution({ code: ' <div>Hello</div>' }); doc.code;

There are several other potential use cases for defaults per SchemaType. For example:

Setting required: true for all strings.

for all strings. Setting unique: true on ObjectIds to build a unique index on every ObjectId property by default.

on ObjectIds to build a unique index on every ObjectId property by default. Setting default: 0 on numbers so all number paths get set to 0 by default.

on numbers so all number paths get set to 0 by default. Setting the transform option on dates so Mongoose formats all dates as unix timestamps rather than ISO date strings.

New Populate Option: perDocumentLimit

Mongoose populate() has a known issue when populating with limit . When you call Article.find().populate('authors') , Mongoose executes 2 queries: Article.find() to get all the articles, and then User.find() to find all users whose _id is in one of the articles' authors field.

Now suppose you call populate() with limit :

const res = await Article.find().populate( 'authors' , { limit: 1 });

Mongoose still only executes one query to populate authors . It just increases the limit to match the number of articles and then applies the original limit after the fact.

const res = await Article.find().populate( 'authors' , { limit: 1 }); const res = await Article.find(); const limit = 1 * res.length; const authorIds = res.reduce((arr, article) => arr.concat(article.authors), []); const authors = await Author.find({ _id: { $ in : authorIds } }).limit(limit); for ( const article of res) { article.authors = authors. filter(author => { return article.authors.map(a => a.toString()).includes(author._id.toString()); }). slice( 0 , 1 ); }

This approach sometimes works, but can cause unpredictable behavior when one article in the result has way more authors than another. If you have 2 articles, both with 2 authors, populate() with limit: 1 may end up giving 0 authors to the 2nd article.

const mongoose = require ( 'mongoose' ); const Article = mongoose.model( 'Article' , mongoose.Schema({ authors: [{ type: Number , ref: 'User' }] })); const User = mongoose.model( 'User' , mongoose.Schema({ _id: Number })); run().catch(err => console .log(err)); async function run ( ) { await mongoose.connect( 'mongodb://localhost:27017/test' , { useNewUrlParser: true , useUnifiedTopology: true }); await Article.deleteMany({}); await User.deleteMany({}); await Article.create([ { authors: [ 1 , 2 ] }, { authors: [ 3 , 4 ] } ]); await User.create({ _id: 1 }, { _id: 2 }, { _id: 3 }, { _id: 4 }); const res = await Article.find().populate({ path: 'authors' , options: { limit: 1 } }); console .log(res); }

In Mongoose 5.9, we introduced a separate option called perDocumentLimit that handles limit in a more intuitive way. If you replace limit: 1 with perDocumentLimit: 1 in the above example, both articles get exactly 1 author.

const res = await Article.find().populate({ path: 'authors' , perDocumentLimit: 1 }); console .log(res);

The difference is that Mongoose will now execute a separate query to populate() every article. Since Article.find() fetches 2 articles in the above example, under the hood Mongoose will execute 2 Author.find() queries (each with limit = 1 ) in parallel to populate authors for each document. For most apps, executing a separate query per document may not be a problem. But if you are populating hundreds of documents with a limit , you may end up with slow trains.

Moving On