Mongoose 5.0.0-rc0 was released yesterday. This is the first backwards-breaking release for mongoose since 4.0.0 was released in March 2015. The major forcing functions for this release were that MongoDB 3.6 removed support for the way mongoose did authentication (which is why mongoose needed the useMongoClient option) and the 3.0.0 release of the official MongoDB Node.js driver. Another major priority for Mongoose 5 was dropping support for Node.js < 4 and MongoDB < 3.0.0. Both Node.js 0.12 and MongoDB 2.6 are about a year past their official end-of-life, and removing support for them has enabled us to make our code base much leaner.

In addition, we've made 33 other significant changes and improvements in 5.0.0-rc0 that help make Mongoose integrate better with ES6 and modern JavaScript patterns. I'd like to personally thank Varun Jayaraman and Lingke Wang for knocking out several key improvements, and Nick Baugh for his support and invaluable feedback. In this article, I'll highlight some of the improvements we're most excited about.

Native Promises By Default

The "mpromise (mongoose's default promise library) is deprecated, plug in your own promise library instead" warning has nagged Mongoose developers since May 2016. Because Mongoose 4 was released right before the formal adoption of the ES6 spec and Node.js 4, we were stuck with the legacy baggage of mpromise, Mongoose's original promise library that was written to match the mostly defunct Promises/A+ spec. The mpromise library predates the ES6 promise spec, and inconsistencies between mpromise and ES6 promises caused a lot of headache.

In Mongoose 5, mpromise is gone for good. In fact, mpromise is no longer supported, the below will not work:

// This won't work in mongoose 5
mongoose.Promise = require('mpromise');

Mongoose 5 uses Node.js' native promises by default.

mongoose.Promise === global.Promise; // true

The mongoose.Promise property is still supported. You can still make mongoose always return bluebird promises with the below code. On a related note, since mpromise is no longer supported, mongoose.Promise now contains the actual promise constructor. In other words, mongoose.Promise is no longer a compatibility layer that reconciles mpromise and ES6 promises, it is strictly equal to the promise constructor.

mongoose.Promise = require('bluebird');
mongoose.Promise === require('bluebird'); // true in 5.x, false in 4.x

Promises and Async/Await With Middleware

Mongoose's middleware pattern is similar to that of its former LearnBoost cousin Express. Asynchronous middleware in mongoose is handled using the next() function: once your asynchronous middleware wants to kick off the next middleware in the chain, it needs to call next(). When callbacks were the dominant async paradigm in Node.js this pattern made sense, but with promises and async/await we can do better.

Before you panic and start rewriting all your middleware, don't worry, mongoose hasn't gone the koa route and made next a promise. Your existing pre and post hooks should continue to work as written modulo a couple minor changes. However, with mongoose 5 you can now use async/await with middleware and not worry about next() at all.

For example, below is an example of using callback-style middleware to update an embedded vehicle property on the 'Customer' model every time a vehicle gets updated.

const mongoose = require('mongoose');

mongoose.connect('mongodb://localhost:27017/test');
mongoose.set('debug', true);

const vehicleSchema = new mongoose.Schema({
  make: String,
  model: String,
  year: Number,
  customerId: mongoose.Schema.Types.ObjectId
});

const customerSchema = new mongoose.Schema({
  vehicle: { type: vehicleSchema, required: true }
});

// Callback-style middleware, your only option in 4.x
vehicleSchema.pre('save', function(next) {
  Customer.updateMany({ 'vehicle._id': this._id }, { $set: { vehicle: this } }, (error) => {
    next(error);
  });
});

const Vehicle = mongoose.model('Vehicle', vehicleSchema);
const Customer = mongoose.model('Customer', customerSchema);

run().catch(error => console.error(error.stack));

async function run() {
  const v = await Vehicle.create({ make: 'Subaru', model: 'Crosstrek', year: 2016 });
  let customer = await Customer.create({ vehicle: v });

  v.year = 2018;
  await v.save();

  // `year` will be '2018'
  console.log(await Customer.findById(customer._id));
}

In mongoose 5.x, you can write the above middleware using async/await:

// Middleware can return a promise in 5.x. Mongoose will attach `.then(next, next)` for you.
vehicleSchema.pre('save', async function() {
  await Customer.updateMany({ 'vehicle._id': this._id }, { $set: { vehicle: this } });
});

// As an alternative, you can also call `next()` manually. The below middleware
// is equivalent to the above
vehicleSchema.pre('save', async function(next) {
  await Customer.updateMany({ 'vehicle._id': this._id }, { $set: { vehicle: this } });
  next();
});

A key change in 5.x is that next() may only be called once. Calling next() multiple times is a no-op. For example, the below middleware will not report an error, because the 2nd next() call will be ignored.

vehicleSchema.pre('save', async function(next) {
  await Customer.updateMany({ 'vehicle._id': this._id }, { $set: { vehicle: this } });
  next();
  // Ignored because calling `next()` twice is a no-op
  next(new Error('test'));
  // Ignored because mongoose handles promise rejections by calling `next(err)`
  throw new Error('test');
});

In general, if you want to use async/await with mongoose middleware, you should not use next() directly unless you have a good reason to.

Change Streams

Change streams are one of the most exciting new features in MongoDB 3.6. Change streams notify you when a given document or documents have changed. For example, in the above case, let's say someone modified the customer's vehicle from the MongoDB shell. Because the update didn't go through mongoose, your middleware won't fire and your customer document will be out of date. With change streams, you get notified of all changes affecting your documents, no matter what app they came from. Below is an example of using change streams and collection.watch() with mongoose.

const vehicleSchema = new mongoose.Schema({
  make: String,
  model: String,
  year: Number,
  customerId: mongoose.Schema.Types.ObjectId
});

const customerSchema = new mongoose.Schema({
  vehicle: { type: vehicleSchema, required: true }
});

const Vehicle = mongoose.model('Vehicle', vehicleSchema);
const Customer = mongoose.model('Customer', customerSchema);

run().catch(error => console.error(error.stack));

async function run() {
  await mongoose.connect('mongodb://localhost:27017/test');

  // Watch the entire 'vehicles' collection for changes
  Vehicle.collection.watch().stream().on('data', data => {
    // Ignore non-updates
    if (data.operationType !== 'update') {
      return;
    }
    const { documentKey, updateDescription } = data;
    // Prefix `vehicle.` in front of all keys, so updating `{ year: 2018 }`
    // becomes updating `{ 'vehicle.year': 2018 }` for customer docs
    const $set = Object.keys(updateDescription.updatedFields).
      reduce(($set, key) => {
        $set[`vehicle.${key}`] = updateDescription.updatedFields[key];
        return $set;
      }, {});

    // Execute the update. Note that this is just a proof of concept and
    // we don't handle errors here, if you want to do this in prod make
    // sure you add some error handling.
    Customer.updateMany({ 'vehicle._id': documentKey._id }, { $set }).exec();
  });

  const v = await Vehicle.create({ make: 'Subaru', model: 'Crosstrek', year: 2016 });
  let customer = await Customer.create({ vehicle: v });

  v.year = 2018;
  await v.save();

  // Wait for change stream logic to execute
  await new Promise(resolve => setTimeout(() => resolve(), 250));

  // Should get `year = 2018`
  console.log(await Customer.findById(customer._id));

Moving On

Mongoose 5.0.0-rc0 has 34 changes and improvements that will help you leverage modern JavaScript without the extra boilerplate. This is a release candidate as opposed to a formal release, so be careful running it in production. Please try it out and report any issues you find on GitHub. We're very excited to get this new release out and look forward to hearing your feedback!

Found a typo or error? Open up a pull request! This post is available as markdown on Github
comments powered by Disqus