Mongoose 3.8 brings with it significant additions to our GeoJSON, Promise, and Query building support as well as dozens of bug fixes and lastly, some important deprecations and changes you should be aware of as detailed below.

In case you missed the 3.7 release announcement, the Mongoose project has moved to a release versioning approach similar to the MongoDB and Node.js projects. All odd numbered minor versions are to be considered unstable where you should expect larger, potentially breaking changes to occur during iteration towards the next even numbered (stable) release.

We feel this approach provides a cleaner, more consistent way to describe and iterate on new code branches than tagging pre-releases as “beta0”, “beta1”, etc.

Summary:

  • X.OddNumber.X is an unstable release
  • X.EvenNumber.X is a stable release

For example: 3.7.0, 3.7.1 and 4.11.398 are unstable, whereas 3.8.0, 3.8.1, and 3.20.1 are stable.

For more information on this versioning scheme, please see the MongoDB example.


The details

Mongoose 3.8 brings with it some significant changes and improvements under the hood:

geoSearch

Mongoose now supports new geoSearch command in MongoDB.

To use geoSearch, you must have a haystack index defined, which can be done as shown below:

  1. var schema = new Schema({
  2. pos : [Number],
  3. complex : {},
  4. type: String
  5. });
  6. schema.index({ "pos" : "geoHaystack", type : 1},{ bucketSize : 1});
  7. mongoose.model('Geo', schema);

You can use the geoSearch command much like you would use mapreduce().

  1. Geo.geoSearch({ type : "place" }, { near : [9,9], maxDistance : 5 }, function (err, results, stats) {
  2. console.log(results);
  3. });

For information on the required options and parameters, please see the MongoDB docs

geoNear

We now have support for the geoNear command.

To use geoNear, your model must have a 2d or 2dsphere index. For more information on these, see the MongoDB docs

  1. var schema = new Schema({
  2. pos : [Number],
  3. type: String
  4. });
  5. schema.index({ "pos" : "2dsphere"});
  6. mongoose.model('Geo', schema);

Once that has been defined, you can access the geoNear method on the model.

  1. Geo.geoNear([9,9], { spherical : true, maxDistance : .1 }, function (err, results, stats) {
  2. console.log(results);
  3. });

For more information on the geoNear command and required options, see the MongoDB docs

mquery

mquery is a fluent query building library for MongoDB. It’s API is very similar to Mongoose’s old query syntax, but it does have some improvements and is much more standardized. Any query you can construct with mquery, can now be constructed with Mongoose using the same syntax. At the same time, all old Mongoose syntax that mquery does not support should still be available (with 2 notable exceptions, see below). If any old syntax (that was using public APIs— we do not guarantee support for any internal APIs used, and they have changed) does not work, please submit a report.

This change reduces the size of the Mongoose code base by a couple thousand lines. Additionally, it allows for you to integrate your own query engine should you desire.

within() changes

In 3.6.x, a within query was constructed like this:

  1. Model.where('loc').within.geometry(geojsonPoly);

The syntax has now changed to:

  1. Model.where('loc').within().geometry(geojsonPoly);

A shim to revert these changes and return to the old syntax can be found here

intersects() changes

In 3.6.x, a within query was constructed like this:

  1. Model.where('line').intersects.geometry(geojsonLine)

The syntax has now changed to:

  1. Model.where('line').intersects().geometry(geojsonLine)

A shim to revert these changes and return to the old syntax can be found here

Query#geometry no longer accepts path

geometry no longer accepts a path argument. This makes the Mongoose query building much more semantically consistent.

A query like this:

  1. Geo.within().geometry('loc', { type: 'Polygon', coordinates : coords });

Now turns into:

  1. Geo.where('loc').within().geometry({ type: 'Polygon', coordinates : coords });

geoWithin changes

If you are running a version of MongoDB < 2.4 this affects you.

In MongoDB 2.4, $within was deprecated and $geoWithin was introduced which is 100% backward compatible with $within. For .within() queries, we now internally use $geoWithin by default. However, you can change this to remain backward compatible with old releases of MongoDB and force Mongoose to continue using $within by setting a flag. To toggle this flag, simply change the use$geoWithin property on the Query object.

  1. var mongoose = require('mongoose');
  2. mongoose.Query.use$geoWithin = false;

aggregation builder

There is now an API for building aggregation pipelines, courtesy of njoyard. It works very similar to query building in Mongoose.

Example

  1. Model.aggregate({ $match: { age: { $gte: 21 }}}).unwind('tags').exec(cb);

All items returned will be plain objects and not mongoose documents.

Supported Operations

Links

model.aggregate arguments

The Model.aggregate() argument signature has changed; now no longer accepting an options argument. Now that we are wrapping the driver’s aggregate method, we can provide a cleaner, less error prone approach through the use of the new aggregation builders read method.

  1. // old way of specifying the read preference option
  2. Model.aggregate({ $match: {..}}, .. , { readPreference: 'primary' }, callback);
  3. // new approach
  4. Model.aggregate({ $match: {..}}, ..).read('primary').exec(callback)

query.toConstructor

Say you have a common set of criteria you query for repeatedly throughout your application, like selecting only accounts that have not made a payment in the past 30 days:

  1. Accounts.find({ lastPayment: { $lt: 30DaysAgo }}).select('name lastPayment').exec(callback)

To make managing these common sets of query criteria easier, Mongoose now supports creating reusable base queries.

  1. var Late = Accounts.find({ lastPayment: { $lt: 30DaysAgo }}).select('name lastPayment').toConstructor();

Now, utilizing the Late query constructor, we can clean up our codebase a bit:

  1. Late().exec(callback)

Since Late is a query constructor, not just an instance, we can make modifications to the returned query instance without any impacts to the original it was based on.

  1. Late().where({ active: true }).lean().exec(callback)

And since Late is a stand-alone subclass of mongoose.Query, we can make customizations to the constructor itself without impacting any global queries either.

  1. Late.prototype.startsWith = function (prefix) {
  2. this.where({ name: new RegExp('^' + prefix) });
  3. return this;
  4. }
  5. Late().startsWith('Fra').exec(callback)

Custom error message support for built-in validators

All built-in validators now support error message customization. Error messages can be defined in the schema or globally.

  1. // in the schema
  2. new Schema({ x: { type: Number, min: [0, '{VALUE} is less than the minimum required ({MIN}) for path: {PATH}'] }})
  3. // globally
  4. var messages = require('mongoose').Error.messages;
  5. messages.String.enum = "Your custom message for {PATH}.";

As seen in the above example, error messages now also support templating (if that’s really a term):

  1. var schema = new Schema({ name: { type: String, match: [/^T/, '{PATH} must start with "T". You provided: `{VALUE}`'] }});
  2. var M = mongoose.model('M', schema);
  3. var m = new M({ name: 'Nope' });
  4. m.save(function (err) {
  5. console.log(err);
  6. // prints..
  7. // { message: 'Validation failed',
  8. // name: 'ValidationError',
  9. // errors:
  10. // { name:
  11. // { message: 'name must start with "T". You provided: `Nope`',
  12. // name: 'ValidatorError',
  13. // path: 'name',
  14. // type: 'regexp',
  15. // value: 'Nope' } } }
  16. })

The following tokens are replaced as follows:

{PATH} is replaced with the invalid document path {VALUE} is replaced with the invalid value {TYPE} is replaced with the validator type such as “regexp”, “min”, or “user defined” {MIN} is replaced with the declared min value for the Number.min validator {MAX} is replaced with the declared max value for the Number.max validator

One change to be aware of for custom validators is that in previous versions, the error message was assigned to the type property of the ValidationError. Going forward, the type property is assigned the value user defined.

  1. function validator () { return false }
  2. Schema({ name: { type: String, validate: validator }})
  3. doc.save(function (err) {
  4. console.log(err.type) // 'user defined'
  5. })

See issue #747

GeoJSON Support for query.near()

The mongoose query builders near() method now supports passing GeoJSON objects as well:

  1. Test.where('loc').near({ center: { type: 'Point', coordinates: [11,20]}, maxDistance : 1000000 })

query.remove() behavior change

Previously, Query#remove() always executed the operation and did not accept query conditions. To be consistent with other Query operations, query conditions and/or callback are now accepted. The operation is executed only if the callback is passed.

  1. query.remove(conds, fn); // executes
  2. query.remove(conds) // no execution
  3. query.remove(fn) // executes
  4. query.remove() // no execution

To execute a remove query without passing a callback, use remove() followed by exec().

  1. var promise = query.remove().exec()

child schemas transform options now respected

Previousy, when a schema and its subdocument both had transforms defined, the top-level transform was used for the subdocuments as well. This behavior has now been corrected.

See #1412

subdoc and populated docs toJSON options now respected

Previously, subdocuments and populated child documents toJSON options were ignored when calling parent.toJSON() and the parents toJSON options were used instead. This behavior has now been corrected.

See #1376

disabling safe mode now also disables versioning support

When { w: 0 } or safe: false is used in your schema to force non-acknowledged writes (rare but some people use this), no response is received from MongoDB causing an error to occur in the versioning handler. Because mongoose doesn’t know if the write was successful under these conditions, versioning must be disabled as well. In 3.8 we now disable it automatically when using non-acknowledged writes.

See #1520

pluralization optional

Mongoose has historically pluralized collection names. While some feel its a nice feature it often has lead to confusion when later exploring the database in the MongoDB shell. This release includes optionally disabling pluralization of collection names.

  1. // disable globally
  2. mongoose.set('pluralization', false)
  3. // disable at the model/collection level
  4. var schema = Schema({..}, { pluralization: false })

Eventually, in v4, all pluralization will be removed.

See #1707

pluralization no longer applied to non-alphabetic characters

In previous versions of mongoose, a model named “plan_“ would get pluralized to “plan_s”. This is no longer the case. Now, if the name ends with a non-alphabetic character, it will not be pluralized.

See #1703

mixed types now support being required

Previously we were unable to make Mixed types required. This has been fixed.

See #1722

toObject/toJSON no longer ignore minimize option

See #1744 and #1607

remove node 0.6.x support

We have discontinued support of running mongoose on node < 0.8.x. While we made no actual code changes related to this announcement, the node project itself isn’t supporting it, so neither will we.

connection pool sharing

Mongoose connection objects can now share their underlying connection pool across multiple databases. This helps avoid excessive numbers of open connections when working with multiple databases.

For example, an application working with 5 databases in a 3 node replica-set previously required creating 5 separate mongoose connection instances. Since each mongoose connection instance opens 6 internal connections (by default) to each node of your replica set, a total of 90 (635) connections were opened. Now, utilizing connection.useDb(dbname), we are able to just reuse the existing 18 (6*3) connections.

  1. // create a connection to a database
  2. var db = mongoose.createConnection(uri);
  3. // use another database without creating additional connections
  4. var db2 = db.useDb('someDbName');
  5. // proceed as you would normally
  6. var Model1 = db.model('Model1', m1Schema);
  7. var Model2 = db2.model('Model2', m2Schema);

Since both db and db2 use the same connection pool, when either of them close, both will be closed. Same goes for open() and other events like connecting, disconnecting, etc.

See 1124.

model.update() now supports overwrite

You can now pass the overwrite option to a model in order to override default update semantics.

overwrite is passed in via setOptions

  1. var q = Model.where({ _id: id }).setOptions({ overwrite: true });

This will cause the update to ignore the built-in protections in Mongoose and simply send the update as-is. This means that passing {} to update with overwrite will result in an empty document. This feature is useful when you need to override the default $set update behavior.

  1. Model.findOne(id, function (err, doc) {
  2. console.log(doc); // { _id: 108, name: 'cajon' })
  3. Model.where({ _id : id }).setOptions({ overwrite: true }).update({ changed: true }, function (err) {
  4. base.findOne(function (err, doc) {
  5. console.log(doc); // { _id: 108, changed: true }) - the doc was overwritten
  6. });
  7. });
  8. })

Better support for Promises

The following model methods now return promises:

nearSphere deprecated

While nearSphere still exists in the API, it will be removed in a future release. In its place, use the near() method with the { spherical : true } option.

IE.

  1. Model.nearSphere({ center : [10,10] });
  2. // becomes...
  3. Model.near({ center : [10,10], spherical : true })

Validation against nonsensical queries

The new query engine will now throw an error when nonsensical or invalid combinations are attempted. Some of these include:

  • using slice with distinct
  • using limit with distinct
  • using mod without a preceding where
  • using maxScan with count
  • and more

Important to note that these types of queries never worked before. They would either not return results that were truly representative of the query or would fail silently.

Query#center is deprecated

Please use Query#circle instead.

  1. var area = { center: [50, 50], radius: 10, unique: true }
  2. query.where('loc').within().circle(area)

Query#centerSphere is deprecated

Please use Query#circle instead with the spherical option set.

  1. var area = { center: [50, 50], radius: 10, unique: true, spherical: true }
  2. query.where('loc').within().circle(area)

Query#slaveOk is deprecated

Use Query#read instead with secondaryPreferred.

Promise#addBack is deprecated

Use Promise#onResolve instead.

Promise#addCallback is deprecated

Use Promise#onFulFill instead.

Promise#addErrback is deprecated

Use Promise#onReject instead.

Promise#complete is deprecated

Use Promise#fulfill instead.

document.remove callback arguments

Callbacks passed to document.remove() now receive the document which was removed.

  1. model.findOne(function (err, doc1) {
  2. doc1.remove(function (err, doc2) {
  3. // doc2 == doc1
  4. })
  5. })

MongooseBuffer#subtype

MongooseBuffer has a new subtype() method which sets it’s subtype option and marks the buffer modified if necessary.

  1. var bson = require('bson');
  2. doc.buf.subtype(bson.BSON_BINARY_SUBTYPE_UUID)

See this page for more detail.

awaitdata support

Query#tailable() now supports the awaitdata option.

  1. query.tailable({ awaitdata: true })

Query#update and Query#remove only executes with callback or explicit true

Previously, update() and remove() would execute an unsafe update/delete if no callback was passed. This has been changed in this release. You now must either pass a callback or explicitly tell Mongoose to execute the query in an unsafe manner. This can be done by passing true for the callback value to the query.

  1. query.update({...},{...}, true);
  2. query.remove(true);