From fbfe28368722e6ef11a119f4b6166d14af711180 Mon Sep 17 00:00:00 2001 From: Valeri Karpov Date: Sun, 8 Oct 2017 10:23:24 -0700 Subject: [PATCH 01/78] chore: release 4.12.1 --- History.md | 9 +++++++++ package.json | 2 +- 2 files changed, 10 insertions(+), 1 deletion(-) diff --git a/History.md b/History.md index 2757ee5744f..812b3bdf17a 100644 --- a/History.md +++ b/History.md @@ -1,3 +1,12 @@ +4.12.1 / 2017-10-08 +=================== + * fix(document): create new doc when setting single nested, no more set() on copy of priorVal #5693 + * fix(model): recursively call applyMethods on child schemas for global plugins #5690 + * docs: fix bad promise lib example on home page #5686 + * fix(query): handle false when checking for inclusive/exclusive projection #5685 + * fix(discriminator): allow reusing child schema #5684 + * fix: make addToSet() on empty array with subdoc trigger manual population #5504 + 4.12.0 / 2017-10-02 =================== * docs(validation): add docs coverage for ValidatorError.reason #5681 diff --git a/package.json b/package.json index d1a3c9c26a1..2eeee7856cb 100644 --- a/package.json +++ b/package.json @@ -1,7 +1,7 @@ { "name": "mongoose", "description": "Mongoose MongoDB ODM", - "version": "4.12.1-pre", + "version": "4.12.1", "author": "Guillermo Rauch ", "keywords": [ "mongodb", From 4bb72ddb8ba037e05ec0eeef44abaa8918f30c8d Mon Sep 17 00:00:00 2001 From: Valeri Karpov Date: Sun, 8 Oct 2017 10:27:22 -0700 Subject: [PATCH 02/78] chore: now working on 4.12.2 --- package.json | 2 +- 1 file changed, 1 insertion(+), 1 deletion(-) diff --git a/package.json b/package.json index 2eeee7856cb..07d368d93ce 100644 --- a/package.json +++ b/package.json @@ -1,7 +1,7 @@ { "name": "mongoose", "description": "Mongoose MongoDB ODM", - "version": "4.12.1", + "version": "4.12.2-pre", "author": "Guillermo Rauch ", "keywords": [ "mongodb", From c5ec9a9ae3e2fd9f697faca7cb0e21d8c29ca5c2 Mon Sep 17 00:00:00 2001 From: Valeri Karpov Date: Sun, 8 Oct 2017 17:47:47 -0700 Subject: [PATCH 03/78] docs(connection): clarify that poolSize is max --- docs/connections.jade | 26 +++++++++++++------------- 1 file changed, 13 insertions(+), 13 deletions(-) diff --git a/docs/connections.jade b/docs/connections.jade index 974edc9b04b..56f55fc4c82 100644 --- a/docs/connections.jade +++ b/docs/connections.jade @@ -13,7 +13,7 @@ block content on the default port (27017). If the local connection fails then try using 127.0.0.1 instead of localhost. Sometimes issues may arise when the local hostname has been changed. - + You can also specify several more parameters in the `uri`: :js @@ -26,28 +26,28 @@ block content :markdown Mongoose lets you start using your models immediately, without waiting for mongoose to establish a connection to MongoDB. - + :js mongoose.connect('mongodb://localhost/myapp'); var MyModel = mongoose.model('Test', new Schema({ name: String })); // Works MyModel.findOne(function(error, result) { /* ... */ }); - + :markdown That's because mongoose buffers model function calls internally. This buffering is convenient, but also a common source of confusion. Mongoose will *not* throw any errors by default if you use a model without connecting. - + :js var MyModel = mongoose.model('Test', new Schema({ name: String })); // Will just hang until mongoose successfully connects MyModel.findOne(function(error, result) { /* ... */ }); - + setTimeout(function() { mongoose.connect('mongodb://localhost/myapp'); }, 60000); - + :markdown To disable buffering, turn off the [`bufferCommands` option on your schema](http://mongoosejs.com/docs/guide.html#bufferCommands). If you have `bufferCommands` on and your connection is hanging, try turning @@ -93,7 +93,7 @@ block content :js // Good way to make sure mongoose never stops trying to reconnect mongoose.connect(uri, { server: { reconnectTries: Number.MAX_VALUE } }); - + h3#callback Callback :markdown The `connect()` function also accepts a callback parameter and returns a [promise](http://mongoosejs.com/docs/promises.html). @@ -101,7 +101,7 @@ block content mongoose.connect(uri, options, function(error) { // Check error in initial connection. There is no 2nd param to the callback. }); - + // Or using promises mongoose.connect(uri, options).then( () => { /** ready to use. The `mongoose.connect()` promise resolves to undefined. */ }, @@ -111,7 +111,7 @@ block content h4#connection-string-options Connection String Options :markdown Mongoose supports the following options in the connection string. - + * [ssl](http://mongodb.github.io/node-mongodb-native/2.1/api/Server.html) * [poolSize](http://mongodb.github.io/node-mongodb-native/2.1/api/Server.html) * [autoReconnect](http://mongodb.github.io/node-mongodb-native/2.1/api/Server.html) @@ -155,7 +155,7 @@ block content :markdown To connect to a single node replica set, specify the `replicaSet` option. - + :js mongoose.connect('mongodb://host1:port1/?replicaSet=rsName'); @@ -179,7 +179,7 @@ block content h3#connection_pools Connection pools :markdown - Each `connection`, whether created with `mongoose.connect` or `mongoose.createConnection` are all backed by an internal configurable connection pool defaulting to a size of 5. Adjust the pool size using your connection options: + Each `connection`, whether created with `mongoose.connect` or `mongoose.createConnection` are all backed by an internal configurable connection pool defaulting to a maximum size of 5. Adjust the pool size using your connection options: :js // single server @@ -192,7 +192,7 @@ block content // passing the option in the URI works with single or replica sets var uri = 'mongodb://localhost/test?poolSize=4'; mongoose.createConnection(uri); - + h3#use-mongo-client The `useMongoClient` Option :markdown Mongoose's default connection logic is deprecated as of 4.11.0. Please opt @@ -261,7 +261,7 @@ block content keepAlive: true, reconnectTries: 30 }); - + :markdown This deprecation is because the [MongoDB driver](https://www.npmjs.com/package/mongodb) has deprecated an API that is critical to mongoose's connection logic to From 76f14feca511e651635248c5b271cc87b6074f54 Mon Sep 17 00:00:00 2001 From: Valeri Karpov Date: Mon, 9 Oct 2017 07:59:21 -0700 Subject: [PATCH 04/78] test(model): refactor tests to reuse same db connetion Re: #5575 --- test/model.indexes.test.js | 113 ++++++++++--------------------------- 1 file changed, 29 insertions(+), 84 deletions(-) diff --git a/test/model.indexes.test.js b/test/model.indexes.test.js index 6606d36f6b0..43f13e8961d 100644 --- a/test/model.indexes.test.js +++ b/test/model.indexes.test.js @@ -10,6 +10,16 @@ var start = require('./common'), ObjectId = Schema.Types.ObjectId; describe('model', function() { + var db; + + before(function() { + db = start(); + }); + + after(function(done) { + db.close(done); + }); + describe('indexes', function() { it('are created when model is compiled', function(done) { var Indexed = new Schema({ @@ -22,9 +32,8 @@ describe('model', function() { Indexed.index({last: 1, email: 1}, {unique: true}); Indexed.index({date: 1}, {expires: 10}); - var db = start(), - IndexedModel = db.model('IndexedModel', Indexed, 'indexedmodel' + random()), - assertions = 0; + var IndexedModel = db.model('IndexedModel1', Indexed, 'indexedmodel' + random()); + var assertions = 0; IndexedModel.on('index', function() { IndexedModel.collection.getIndexes({full: true}, function(err, indexes) { @@ -45,7 +54,7 @@ describe('model', function() { }); assert.equal(assertions, 4); - db.close(done); + done(); }); }); }); @@ -62,9 +71,8 @@ describe('model', function() { blogposts: [BlogPosts] }); - var db = start(), - UserModel = db.model('DeepIndexedModel', User, 'deepindexedmodel' + random()), - assertions = 0; + var UserModel = db.model('DeepIndexedModel2', User, 'deepindexedmodel' + random()); + var assertions = 0; UserModel.on('index', function() { UserModel.collection.getIndexes(function(err, indexes) { @@ -87,7 +95,7 @@ describe('model', function() { } assert.equal(assertions, 3); - db.close(done); + done(); }); }); }); @@ -105,8 +113,7 @@ describe('model', function() { featured: [BlogPosts] }); - var db = start(); - var UserModel = db.model('DeepIndexedModel', User, 'gh-2322'); + var UserModel = db.model('DeepIndexedModelMulti3', User, 'gh2322'); var assertions = 0; UserModel.on('index', function() { @@ -136,7 +143,7 @@ describe('model', function() { } assert.equal(assertions, 5); - db.close(done); + done(); }); }); }); @@ -154,9 +161,8 @@ describe('model', function() { blogposts: [BlogPosts] }); - var db = start(), - UserModel = db.model('DeepCompoundIndexModel', User, 'deepcompoundindexmodel' + random()), - found = 0; + var UserModel = db.model('DeepCompoundIndexModel4', User, 'deepcompoundindexmodel' + random()); + var found = 0; UserModel.on('index', function() { UserModel.collection.getIndexes(function(err, indexes) { @@ -171,7 +177,6 @@ describe('model', function() { } } - db.close(); assert.equal(found, 2); done(); }); @@ -222,9 +227,8 @@ describe('model', function() { }); it('error should emit on the model', function(done) { - var db = start(); var schema = new Schema({name: {type: String}}); - var Test = db.model('IndexError', schema, 'x' + random()); + var Test = db.model('IndexError5', schema, 'x' + random()); Test.create({name: 'hi'}, {name: 'hi'}, function(err) { assert.strictEqual(err, null); @@ -232,7 +236,6 @@ describe('model', function() { Test.schema.index({other: 1}); Test.on('index', function(err) { - db.close(); assert.ok(/E11000 duplicate key error/.test(err.message), err); done(); }); @@ -244,11 +247,10 @@ describe('model', function() { describe('auto creation', function() { it('can be disabled', function(done) { - var db = start(); var schema = new Schema({name: {type: String, index: true}}); schema.set('autoIndex', false); - var Test = db.model('AutoIndexing', schema, 'autoindexing-disable'); + var Test = db.model('AutoIndexing6', schema, 'autoindexing-disable'); Test.on('index', function() { assert.ok(false, 'Model.ensureIndexes() was called'); }); @@ -262,7 +264,7 @@ describe('model', function() { assert.ifError(err); // Only default _id index should exist assert.deepEqual(['_id_'], Object.keys(indexes)); - db.close(done); + done(); }); }, 100); }); @@ -270,16 +272,15 @@ describe('model', function() { describe('global autoIndexes (gh-1875)', function() { it('will create indexes as a default', function(done) { - var db = start(); var schema = new Schema({name: {type: String, index: true}}); - var Test = db.model('GlobalAutoIndex', schema, 'gh-1875-1'); + var Test = db.model('GlobalAutoIndex7', schema, 'gh-1875-1'); Test.on('index', function(error) { assert.ifError(error); assert.ok(true, 'Model.ensureIndexes() was called'); Test.collection.getIndexes(function(err, indexes) { assert.ifError(err); assert.equal(Object.keys(indexes).length, 2); - db.close(done); + done(); }); }); }); @@ -287,7 +288,7 @@ describe('model', function() { it('will not create indexes if the global auto index is false and schema option isnt set (gh-1875)', function(done) { var db = start({config: {autoIndex: false}}); var schema = new Schema({name: {type: String, index: true}}); - var Test = db.model('GlobalAutoIndex', schema, 'x' + random()); + var Test = db.model('GlobalAutoIndex8', schema, 'x' + random()); Test.on('index', function() { assert.ok(false, 'Model.ensureIndexes() was called'); }); @@ -306,61 +307,6 @@ describe('model', function() { }); }); - it('do not trigger "MongoError: cannot add index with a background operation in progress" (gh-1365) LONG', function(done) { - this.timeout(90000); - - var db = start({uri: 'mongodb://localhost/mongoose_test_indexing'}); - - var schema = new Schema({ - name: {type: String, index: true}, - furryness: {type: Number, index: true} - }, {autoIndex: false}); - - schema.index({name: 1, furryness: 1}); - - var K = db.model('Kitten', schema); - K.on('index', function(err) { - assert.ifError(err); - db.close(done); - }); - - var neededKittens = 30000; - - db.on('open', function() { - K.count({}, function(err, n) { - assert.ifError(err); - if (n >= neededKittens) { - return index(); - } - var pending = neededKittens - n; - - function callback(err) { - assert.ifError(err); - if (--pending) { - return; - } - index(); - } - - function iter(i) { - K.create({name: 'kitten' + i, furryness: i}, callback); - } - - for (var i = n; i < neededKittens; ++i) { - iter(i); - } - }); - - function index() { - K.collection.dropAllIndexes(function(err) { - assert.ifError(err); - K.ensureIndexes(); - }); - } - }); - }); - - describe('model.ensureIndexes()', function() { it('is a function', function(done) { var schema = mongoose.Schema({x: 'string'}); @@ -378,9 +324,8 @@ describe('model', function() { }); it('creates indexes', function(done) { - var db = start(); - var schema = new Schema({name: {type: String}}), - Test = db.model('ManualIndexing', schema, 'x' + random()); + var schema = new Schema({name: {type: String}}); + var Test = db.model('ManualIndexing' + random(), schema, 'x' + random()); Test.schema.index({name: 1}, {sparse: true}); @@ -392,7 +337,7 @@ describe('model', function() { Test.ensureIndexes(function(err) { assert.ifError(err); assert.ok(called); - db.close(done); + done(); }); }); }); From 78c8a3dcd1d360bb459421e4e4846f947de42293 Mon Sep 17 00:00:00 2001 From: Valeri Karpov Date: Mon, 9 Oct 2017 08:47:38 -0700 Subject: [PATCH 05/78] feat(schema): add excludeIndexes option to optionally prevent collecting indexes from nested schemas Fix #5575 --- lib/schema.js | 6 ++++-- test/model.indexes.test.js | 34 ++++++++++++++++++++++++++++++++++ 2 files changed, 38 insertions(+), 2 deletions(-) diff --git a/lib/schema.js b/lib/schema.js index e4517730287..a3ed2ff6693 100644 --- a/lib/schema.js +++ b/lib/schema.js @@ -569,7 +569,7 @@ Schema.interpretAsType = function(path, obj, options) { if (cast && cast[options.typeKey] && cast[options.typeKey].instanceOfSchema) { - return new MongooseTypes.DocumentArray(path, cast[options.typeKey], obj); + return new MongooseTypes.DocumentArray(path, cast[options.typeKey], cast); } if (Array.isArray(cast)) { @@ -1408,7 +1408,9 @@ Schema.prototype.indexes = function() { path = schema.paths[key]; if ((path instanceof MongooseTypes.DocumentArray) || path.$isSingleNested) { - collectIndexes(path.schema, prefix + key + '.'); + if (path.options.excludeIndexes !== true) { + collectIndexes(path.schema, prefix + key + '.'); + } } else { index = path._index || (path.caster && path.caster._index); diff --git a/test/model.indexes.test.js b/test/model.indexes.test.js index 43f13e8961d..7d1255740fb 100644 --- a/test/model.indexes.test.js +++ b/test/model.indexes.test.js @@ -100,6 +100,40 @@ describe('model', function() { }); }); + it('of embedded documents unless excludeIndexes (gh-5575)', function(done) { + var BlogPost = new Schema({ + _id: {type: ObjectId}, + title: {type: String, index: true}, + desc: String + }); + + var User = new Schema({ + name: {type: String, index: true}, + blogposts: { + type: [BlogPost], + excludeIndexes: true + }, + otherblogposts: [{ type: BlogPost, excludeIndexes: true }], + blogpost: { + type: BlogPost, + excludeIndexes: true + } + }); + + var UserModel = db.model('gh5575', User); + + UserModel.on('index', function() { + UserModel.collection.getIndexes(function(err, indexes) { + assert.ifError(err); + + // Should only have _id and name indexes + var indexNames = Object.keys(indexes); + assert.deepEqual(indexNames.sort(), ['_id_', 'name_1']); + done(); + }); + }); + }); + it('of multiple embedded documents with same schema', function(done) { var BlogPosts = new Schema({ _id: {type: ObjectId, index: true}, From 053a118042f8a8153c3852c5c116a30f5c025e93 Mon Sep 17 00:00:00 2001 From: Valeri Karpov Date: Tue, 10 Oct 2017 16:22:46 -0700 Subject: [PATCH 06/78] fix: upgrade muri for more detailed error messages Re: https://github.com/Automattic/mongoose/issues/2912#issuecomment-335242685 --- package.json | 2 +- 1 file changed, 1 insertion(+), 1 deletion(-) diff --git a/package.json b/package.json index 07d368d93ce..2349bccf1ac 100644 --- a/package.json +++ b/package.json @@ -28,7 +28,7 @@ "mpromise": "0.5.5", "mquery": "2.3.2", "ms": "2.0.0", - "muri": "1.2.2", + "muri": "1.3.0", "regexp-clone": "0.0.1", "sliced": "1.0.1" }, From 23967d713b7e90643734e70374ba15d29a0a57ca Mon Sep 17 00:00:00 2001 From: Valeri Karpov Date: Wed, 11 Oct 2017 09:51:20 -0700 Subject: [PATCH 07/78] test(schema): add coverage for childSchemas re: #5695 --- test/schema.test.js | 20 ++++++++++++++++++++ 1 file changed, 20 insertions(+) diff --git a/test/schema.test.js b/test/schema.test.js index 286dcb3636e..8f21b8e40bc 100644 --- a/test/schema.test.js +++ b/test/schema.test.js @@ -1700,5 +1700,25 @@ describe('schema', function() { ]); done(); }); + + it('childSchemas prop (gh-5695)', function(done) { + var schema1 = new Schema({ name: String }); + var schema2 = new Schema({ test: String }); + var schema = new Schema({ + arr: [schema1], + single: schema2 + }); + + assert.equal(schema.childSchemas.length, 2); + assert.equal(schema.childSchemas[0].schema, schema1); + assert.equal(schema.childSchemas[1].schema, schema2); + + schema = schema.clone(); + assert.equal(schema.childSchemas.length, 2); + assert.equal(schema.childSchemas[0].schema, schema1); + assert.equal(schema.childSchemas[1].schema, schema2); + + done(); + }); }); }); From 6df478284003019187af9fc0a10d0376f97776d8 Mon Sep 17 00:00:00 2001 From: Valeri Karpov Date: Wed, 11 Oct 2017 09:51:25 -0700 Subject: [PATCH 08/78] docs(schema): document the childSchemas property and add to public API Fix #5695 --- lib/schema.js | 17 +++++++++++++++++ 1 file changed, 17 insertions(+) diff --git a/lib/schema.js b/lib/schema.js index e4517730287..52ec2bae076 100644 --- a/lib/schema.js +++ b/lib/schema.js @@ -224,6 +224,23 @@ Object.defineProperty(Schema.prototype, '_defaultMiddleware', { ] }); +/** + * Array of child schemas (from document arrays and single nested subdocs) + * and their corresponding compiled models. Each element of the array is + * an object with 2 properties: `schema` and `model`. + * + * This property is typically only useful for plugin authors and advanced users. + * You do not need to interact with this property at all to use mongoose. + * + * @api public + * @property childSchemas + */ + +Object.defineProperty(Schema.prototype, 'childSchemas', { + configurable: false, + enumerable: true, + writable: true +}); /** * The original object passed to the schema constructor From 4594c5dacc722b9aa4980ebd55575835ce377402 Mon Sep 17 00:00:00 2001 From: Valeri Karpov Date: Wed, 11 Oct 2017 10:50:19 -0700 Subject: [PATCH 09/78] test(query): repro #5669 --- test/services.query.test.js | 58 +++++++++++++++++++++++++++++++++++++ 1 file changed, 58 insertions(+) create mode 100644 test/services.query.test.js diff --git a/test/services.query.test.js b/test/services.query.test.js new file mode 100644 index 00000000000..89bc5e2a1d7 --- /dev/null +++ b/test/services.query.test.js @@ -0,0 +1,58 @@ +'use strict'; + +var Query = require('../lib/query'); +var Schema = require('../lib/schema'); +var assert = require('assert'); +var selectPopulatedFields = require('../lib/services/query/selectPopulatedFields'); + +describe('Query helpers', function() { + describe('selectPopulatedFields', function() { + it('handles nested populate if parent key is projected in (gh-5669)', function(done) { + var schema = new Schema({ + nested: { + key1: String, + key2: String + } + }); + + var q = new Query({}); + q.schema = schema; + + assert.strictEqual(q._fields, void 0); + + q.select('nested'); + q.populate('nested.key1'); + assert.deepEqual(q._fields, { nested: 1 }); + + selectPopulatedFields(q); + + assert.deepEqual(q._fields, { nested: 1 }); + + done(); + }); + + it('handles nested populate if parent key is projected out (gh-5669)', function(done) { + var schema = new Schema({ + nested: { + key1: String, + key2: String + } + }); + + var q = new Query({}); + q.schema = schema; + + assert.strictEqual(q._fields, void 0); + + q.select('-nested'); + q.populate('nested.key1'); + assert.deepEqual(q._fields, { nested: 0 }); + + selectPopulatedFields(q); + + assert.deepEqual(q._fields, { nested: 0 }); + + done(); + }); + }); +}); From ccae0a91dd97b47c8daf1c73c865ff91095f0265 Mon Sep 17 00:00:00 2001 From: Valeri Karpov Date: Wed, 11 Oct 2017 10:54:34 -0700 Subject: [PATCH 10/78] fix(query): don't project in populated field if parent field is already projected in Fix #5669 --- lib/services/query/selectPopulatedFields.js | 19 ++++++++++++++++++- 1 file changed, 18 insertions(+), 1 deletion(-) diff --git a/lib/services/query/selectPopulatedFields.js b/lib/services/query/selectPopulatedFields.js index f706f4552c0..bb69f9d3f05 100644 --- a/lib/services/query/selectPopulatedFields.js +++ b/lib/services/query/selectPopulatedFields.js @@ -13,7 +13,7 @@ module.exports = function selectPopulatedFields(query) { var userProvidedFields = query._userProvidedFields || {}; if (query.selectedInclusively()) { for (i = 0; i < paths.length; ++i) { - if (userProvidedFields[paths[i]] == null) { + if (!isPathInFields(userProvidedFields, paths[i])) { query.select(paths[i]); } } @@ -26,3 +26,20 @@ module.exports = function selectPopulatedFields(query) { } } }; + +/*! + * ignore + */ + +function isPathInFields(userProvidedFields, path) { + var pieces = path.split('.'); + var len = pieces.length; + var cur = pieces[0]; + for (var i = 1; i < len; ++i) { + if (userProvidedFields[cur] != null) { + return true; + } + cur += '.' + pieces[i]; + } + return false; +} From e0632116103f4c076b58fb9ae161737df0472fad Mon Sep 17 00:00:00 2001 From: Valeri Karpov Date: Fri, 13 Oct 2017 12:26:42 -0700 Subject: [PATCH 11/78] fix: bump mongodb -> 2.2.33 Fix #4513 --- package-lock.json | 36 +++++++++++++++++++----------------- package.json | 2 +- 2 files changed, 20 insertions(+), 18 deletions(-) diff --git a/package-lock.json b/package-lock.json index 54aa52a74c0..98a55a69610 100644 --- a/package-lock.json +++ b/package-lock.json @@ -1,6 +1,6 @@ { "name": "mongoose", - "version": "4.12.0-pre", + "version": "4.12.2-pre", "lockfileVersion": 1, "requires": true, "dependencies": { @@ -2055,22 +2055,24 @@ } }, "mongodb": { - "version": "2.2.31", - "resolved": "https://registry.npmjs.org/mongodb/-/mongodb-2.2.31.tgz", - "integrity": "sha1-GUBEXGYeGSF7s7+CRdmFSq71SNs=", + "version": "2.2.33", + "resolved": "https://registry.npmjs.org/mongodb/-/mongodb-2.2.33.tgz", + "integrity": "sha1-tTfEcdNKZlG0jzb9vyl1A0Dgi1A=", "requires": { "es6-promise": "3.2.1", - "mongodb-core": "2.1.15", + "mongodb-core": "2.1.17", "readable-stream": "2.2.7" - } - }, - "mongodb-core": { - "version": "2.1.15", - "resolved": "https://registry.npmjs.org/mongodb-core/-/mongodb-core-2.1.15.tgz", - "integrity": "sha1-hB9TuH//9MdFgYnDXIroJ+EWl2Q=", - "requires": { - "bson": "1.0.4", - "require_optional": "1.0.1" + }, + "dependencies": { + "mongodb-core": { + "version": "2.1.17", + "resolved": "https://registry.npmjs.org/mongodb-core/-/mongodb-core-2.1.17.tgz", + "integrity": "sha1-pBizN6FKFJkPtRC5I97mqBMXPfg=", + "requires": { + "bson": "1.0.4", + "require_optional": "1.0.1" + } + } } }, "mongodb-topology-manager": { @@ -2148,9 +2150,9 @@ "integrity": "sha1-VgiurfwAvmwpAd9fmGF4jeDVl8g=" }, "muri": { - "version": "1.2.2", - "resolved": "https://registry.npmjs.org/muri/-/muri-1.2.2.tgz", - "integrity": "sha1-YxmBMmUNsIoEzHnM0A3Tia/SYxw=" + "version": "1.3.0", + "resolved": "https://registry.npmjs.org/muri/-/muri-1.3.0.tgz", + "integrity": "sha512-FiaFwKl864onHFFUV/a2szAl7X0fxVlSKNdhTf+BM8i8goEgYut8u5P9MqQqIYwvaMxjzVESsoEm/2kfkFH1rg==" }, "mute-stream": { "version": "0.0.5", diff --git a/package.json b/package.json index 2349bccf1ac..9711fa054ff 100644 --- a/package.json +++ b/package.json @@ -23,7 +23,7 @@ "bson": "~1.0.4", "hooks-fixed": "2.0.0", "kareem": "1.5.0", - "mongodb": "2.2.31", + "mongodb": "2.2.33", "mpath": "0.3.0", "mpromise": "0.5.5", "mquery": "2.3.2", From aca06b1ae2651e3b13f6e82fd6dbf7fb860769dd Mon Sep 17 00:00:00 2001 From: Valeri Karpov Date: Fri, 13 Oct 2017 12:49:27 -0700 Subject: [PATCH 12/78] docs(faq): add warnings about using arrow functions for getters/setters, virtuals, and methods Fix #5700 --- docs/faq.jade | 60 +++++++++++++++++++++++++++++++++++++-------------- 1 file changed, 44 insertions(+), 16 deletions(-) diff --git a/docs/faq.jade b/docs/faq.jade index fc45f5a5ca3..feb006bdf2c 100644 --- a/docs/faq.jade +++ b/docs/faq.jade @@ -30,11 +30,11 @@ block content doc.array[3] = 'changed'; doc.markModified('array'); doc.save(); - + hr#unique-doesnt-work :markdown **Q**. I declared a schema property as `unique` but I can still save duplicates. What gives? - + **A**. Mongoose doesn't handle `unique` on it's own, `{ name: { type: String, unique: true } }` just a shorthand for creating a [MongoDB unique index on `name`](https://docs.mongodb.com/manual/core/index-unique/). For example, if MongoDB doesn't already have a unique index on `name`, the below code will not error despite the fact that `unique` is true. @@ -61,7 +61,7 @@ block content console.log(err); }); }); - + // Promise based alternative. `init()` returns a promise that resolves // when the indexes have finished building successfully. The `init()` // function is idempotent, so don't worry about triggering an index rebuild. @@ -77,7 +77,7 @@ block content you should [create your indexes using the MongoDB shell])(https://docs.mongodb.com/manual/reference/method/db.collection.createIndex/) rather than relying on mongoose to do it for you. The `unique` option for schemas is convenient for development and documentation, but mongoose is *not* an index management solution. - + hr#nested-properties :markdown **Q**. When I have a nested property in a schema, mongoose adds empty objects by default. Why? @@ -88,7 +88,7 @@ block content } }); var Model = db.model('Test', schema); - + // The below prints `{ _id: /* ... */, nested: {} }`, mongoose assigns // `nested` to an empty object `{}` by default. console.log(new Model()); @@ -96,7 +96,7 @@ block content **A**. This is a performance optimization. These empty objects are not saved to the database, nor are they in the result `toObject()`, nor do they show up in `JSON.stringify()` output unless you turn off the [`minimize` option](http://mongoosejs.com/docs/guide.html#minimize). - + The reason for this behavior is that Mongoose's change detection and getters/setters are based on [`Object.defineProperty()`](https://developer.mozilla.org/en-US/docs/Web/JavaScript/Reference/Global_Objects/Object/defineProperty). In order to support change detection on nested properties without incurring @@ -105,7 +105,35 @@ block content Because mongoose needs to define getters and setters for `nested.prop`, `nested` must always be defined as an object on a mongoose document, even if `nested` is undefined on the underlying [POJO](http://mongoosejs.com/docs/guide.html#minimize). - + + hr#arrow-functions + :markdown + **Q**. I'm using an arrow function for a [virtual](http://mongoosejs.com/docs/guide.html#virtuals), getter/setter, or [method](http://mongoosejs.com/docs/guide.html#methods) and the value of `this` is wrong. + + **A**. Arrow functions [handle the `this` keyword much differently than conventional functions](https://developer.mozilla.org/en-US/docs/Web/JavaScript/Reference/Functions/Arrow_functions#No_binding_of_this). + Mongoose getters/setters depend on `this` to give you access to the document that you're writing to, but this functionality does not work with arrow functions. Do **not** use arrow functions for mongoose getters/setters unless do not intend to access the document in the getter/setter. + :js + // Do **NOT** use arrow functions as shown below unless you're certain + // that's what you want. If you're reading this FAQ, odds are you should + // just be using a conventional function. + var schema = new mongoose.Schema({ + propWithGetter: { + type: String, + get: v => { + // Will **not** be the doc, do **not** use arrow functions for getters/setters + console.log(this); + return v; + } + } + }); + + // `this` will **not** be the doc, do **not** use arrow functions for methods + schema.method.arrowMethod = () => this; + schema.virtual('virtualWithArrow').get(() => { + // `this` will **not** be the doc, do **not** use arrow functions for virtuals + console.log(this); + }); + hr#date_changes :markdown **Q**. Why don't in-place modifications to date objects @@ -125,11 +153,11 @@ block content :js doc.createdAt = new Date(2011, 5, 1).setHours(4); doc.save(); // Works - + hr#populate_sort_order :markdown **Q**. I'm populating a nested property under an array like the below code: - + ``` new Schema({ arr: [{ @@ -137,21 +165,21 @@ block content }] }); ``` - + `.populate({ path: 'arr.child', options: { sort: 'name' } })` won't sort by `arr.child.name`? - + :markdown **A**. See [this GitHub issue](https://github.com/Automattic/mongoose/issues/2202). It's a known issue but one that's exceptionally difficult to fix. - + hr#model_functions_hanging :markdown **Q**. All function calls on my models hang, what am I doing wrong? - + **A**. By default, mongoose will buffer your function calls until it can connect to MongoDB. Read the [buffering section of the connection docs](http://mongoosejs.com/docs/connections.html#buffering) for more information. - + hr#enable_debugging :markdown **Q**. How can I enable debugging? @@ -183,7 +211,7 @@ block content :markdown **Q**. Why do I get "OverwriteModelError: Cannot overwrite .. model once compiled" when I use nodemon / a testing framework? - + **A**. `mongoose.model('ModelName', schema)` requires 'ModelName' to be unique, so you can access the model by using `mongoose.model('ModelName')`. If you put `mongoose.model('ModelName', schema);` in a @@ -198,7 +226,7 @@ block content // use mongoose.Schema var kittySchema = mongoose.Schema({ name: String }); - + // use connection.model var Kitten = connection.model('Kitten', kittySchema); From f2170ee2a6bab60982f04ee54a153314d7ebe405 Mon Sep 17 00:00:00 2001 From: Valeri Karpov Date: Sat, 14 Oct 2017 08:01:35 -0700 Subject: [PATCH 13/78] chore: release 4.12.2 --- History.md | 7 +++++++ package.json | 2 +- 2 files changed, 8 insertions(+), 1 deletion(-) diff --git a/History.md b/History.md index 812b3bdf17a..550a4754ab3 100644 --- a/History.md +++ b/History.md @@ -1,3 +1,10 @@ +4.12.2 / 2017-10-13 +=================== + * docs(faq): add FAQ about using arrow functions for getters/setters, virtuals, and methods #5700 + * docs(schema): document the childSchemas property and add to public API #5695 + * fix(query): don't project in populated field if parent field is already projected in #5669 + * fix: bump mongodb -> 2.2.33 for issue with autoReconnect #4513 + 4.12.1 / 2017-10-08 =================== * fix(document): create new doc when setting single nested, no more set() on copy of priorVal #5693 diff --git a/package.json b/package.json index 9711fa054ff..a21e47c2805 100644 --- a/package.json +++ b/package.json @@ -1,7 +1,7 @@ { "name": "mongoose", "description": "Mongoose MongoDB ODM", - "version": "4.12.2-pre", + "version": "4.12.2", "author": "Guillermo Rauch ", "keywords": [ "mongodb", From 75a55af19b625435362a25bf26b87943368847c7 Mon Sep 17 00:00:00 2001 From: Valeri Karpov Date: Sat, 14 Oct 2017 11:46:33 -0700 Subject: [PATCH 14/78] test(query): repro #5702 --- test/model.findOneAndUpdate.test.js | 59 +++++++++++++++++++++++++++++ 1 file changed, 59 insertions(+) diff --git a/test/model.findOneAndUpdate.test.js b/test/model.findOneAndUpdate.test.js index c2ff235878e..6edccb61356 100644 --- a/test/model.findOneAndUpdate.test.js +++ b/test/model.findOneAndUpdate.test.js @@ -1910,6 +1910,65 @@ describe('model: findOneAndUpdate:', function() { }); }); + it('avoids edge case with middleware cloning buffers (gh-5702)', function(done) { + var uuidParse = require('uuid-parse'); + + function toUUID(string) { + if (!string) { + return null; + } + if (Buffer.isBuffer(string) || Buffer.isBuffer(string.buffer)) { + return string; + } + var buffer = uuidParse.parse(string); + return new mongoose.Types.Buffer(buffer).toObject(0x04); + } + + function fromUUID(buffer) { + if (!buffer || buffer.length !== 16) { + return null; + } + return uuidParse.unparse(buffer); + } + + var UserSchema = new mongoose.Schema({ + name: String, + lastUpdate: {type: Date}, + friends: [{ + _id: false, + status: {type: String, required: true}, + id: { + type: mongoose.Schema.Types.Buffer, + get: fromUUID, + set: toUUID + } + }] + }, { collection: 'users', runSettersOnQuery: true }); + + UserSchema.pre('findOneAndUpdate', function() { + this.update({},{ $set: {lastUpdate: new Date()} }); + }); + + var User = db.model('gh5702', UserSchema); + + var friendId = uuid.v4(); + var user = { + name: 'Sean', + friends: [{status: 'New', id: friendId}] + }; + + User.create(user, function(error, user) { + assert.ifError(error); + + var q = { _id: user._id, 'friends.id': friendId }; + var upd = {'friends.$.status': 'Active'}; + User.findOneAndUpdate(q, upd, {new: true}).lean().exec(function(error) { + assert.ifError(error); + done(); + }); + }); + }); + it('setting subtype when saving (gh-5551)', function(done) { if (parseInt(process.version.substr(1).split('.')[0], 10) < 4) { // Don't run on node 0.x because of `const` issues From 63692e3353bfbf9aed81e53a443c9ac480968cee Mon Sep 17 00:00:00 2001 From: Valeri Karpov Date: Sat, 14 Oct 2017 11:46:44 -0700 Subject: [PATCH 15/78] fix(query): avoid infinite recursion edge case when cloning a buffer Fix #5702 --- lib/types/buffer.js | 15 ++++++++++++--- 1 file changed, 12 insertions(+), 3 deletions(-) diff --git a/lib/types/buffer.js b/lib/types/buffer.js index 9d7d4f26bf5..c61459eca7b 100644 --- a/lib/types/buffer.js +++ b/lib/types/buffer.js @@ -46,9 +46,18 @@ function MongooseBuffer(value, encode, offset) { // make sure these internal props don't show up in Object.keys() Object.defineProperties(buf, { - validators: {value: []}, - _path: {value: path}, - _parent: {value: doc} + validators: { + value: [], + enumerable: false + }, + _path: { + value: path, + enumerable: false + }, + _parent: { + value: doc, + enumerable: false + } }); if (doc && typeof path === 'string') { From 10882668528fdc58e9305b55aff041a68e40f0c5 Mon Sep 17 00:00:00 2001 From: Valeri Karpov Date: Sat, 14 Oct 2017 15:55:17 -0700 Subject: [PATCH 16/78] chore: now working on 4.12.3 --- package.json | 2 +- 1 file changed, 1 insertion(+), 1 deletion(-) diff --git a/package.json b/package.json index a21e47c2805..2df39dd808a 100644 --- a/package.json +++ b/package.json @@ -1,7 +1,7 @@ { "name": "mongoose", "description": "Mongoose MongoDB ODM", - "version": "4.12.2", + "version": "4.12.3-pre", "author": "Guillermo Rauch ", "keywords": [ "mongodb", From 191ff1d588590feb92a27a62848d2c99da524927 Mon Sep 17 00:00:00 2001 From: Valeri Karpov Date: Sat, 14 Oct 2017 15:57:55 -0700 Subject: [PATCH 17/78] style: fix lint --- test/model.findOneAndUpdate.test.js | 30 ++++++++++++++--------------- 1 file changed, 15 insertions(+), 15 deletions(-) diff --git a/test/model.findOneAndUpdate.test.js b/test/model.findOneAndUpdate.test.js index 6edccb61356..c35c2215aec 100644 --- a/test/model.findOneAndUpdate.test.js +++ b/test/model.findOneAndUpdate.test.js @@ -1914,14 +1914,14 @@ describe('model: findOneAndUpdate:', function() { var uuidParse = require('uuid-parse'); function toUUID(string) { - if (!string) { + if (!string) { return null; } - if (Buffer.isBuffer(string) || Buffer.isBuffer(string.buffer)) { + if (Buffer.isBuffer(string) || Buffer.isBuffer(string.buffer)) { return string; } - var buffer = uuidParse.parse(string); - return new mongoose.Types.Buffer(buffer).toObject(0x04); + var buffer = uuidParse.parse(string); + return new mongoose.Types.Buffer(buffer).toObject(0x04); } function fromUUID(buffer) { @@ -1932,17 +1932,17 @@ describe('model: findOneAndUpdate:', function() { } var UserSchema = new mongoose.Schema({ - name: String, - lastUpdate: {type: Date}, - friends: [{ - _id: false, - status: {type: String, required: true}, - id: { - type: mongoose.Schema.Types.Buffer, - get: fromUUID, - set: toUUID - } - }] + name: String, + lastUpdate: {type: Date}, + friends: [{ + _id: false, + status: {type: String, required: true}, + id: { + type: mongoose.Schema.Types.Buffer, + get: fromUUID, + set: toUUID + } + }] }, { collection: 'users', runSettersOnQuery: true }); UserSchema.pre('findOneAndUpdate', function() { From 16475f3b7959563cca4978011b62098ab44ecb0a Mon Sep 17 00:00:00 2001 From: Valeri Karpov Date: Sat, 14 Oct 2017 16:19:01 -0700 Subject: [PATCH 18/78] test(query): repro #5710 --- test/model.findOneAndUpdate.test.js | 15 +++++++++++++++ 1 file changed, 15 insertions(+) diff --git a/test/model.findOneAndUpdate.test.js b/test/model.findOneAndUpdate.test.js index c35c2215aec..8538eadac38 100644 --- a/test/model.findOneAndUpdate.test.js +++ b/test/model.findOneAndUpdate.test.js @@ -1910,6 +1910,21 @@ describe('model: findOneAndUpdate:', function() { }); }); + it('update validators with pushing null (gh-5710)', function(done) { + var schema = new mongoose.Schema({ + arr: [String] + }); + + var Model = db.model('gh5710', schema); + + var update = { $addToSet: { arr: null } }; + var options = { runValidators: true }; + Model.findOneAndUpdate({}, update, options, function(error) { + assert.ifError(error); + done(); + }); + }); + it('avoids edge case with middleware cloning buffers (gh-5702)', function(done) { var uuidParse = require('uuid-parse'); From b7ac49b05e89cea3cee74c31aa753e397a22dd3b Mon Sep 17 00:00:00 2001 From: Valeri Karpov Date: Sat, 14 Oct 2017 16:19:16 -0700 Subject: [PATCH 19/78] fix(query): handle null with addToSet/push/pull/pullAll update validators Fix #5710 --- lib/services/updateValidators.js | 8 +++++--- 1 file changed, 5 insertions(+), 3 deletions(-) diff --git a/lib/services/updateValidators.js b/lib/services/updateValidators.js index 64d49b69cbc..326d0b169a2 100644 --- a/lib/services/updateValidators.js +++ b/lib/services/updateValidators.js @@ -29,6 +29,7 @@ module.exports = function(query, schema, castedDoc, options) { var numKeys = keys.length; var hasDollarUpdate = false; var modified = {}; + var currentUpdate; for (var i = 0; i < numKeys; ++i) { if (keys[i].charAt(0) === '$') { @@ -36,12 +37,13 @@ module.exports = function(query, schema, castedDoc, options) { keys[i] === '$pull' || keys[i] === '$pullAll') { _keys = Object.keys(castedDoc[keys[i]]); for (var ii = 0; ii < _keys.length; ++ii) { - if (castedDoc[keys[i]][_keys[ii]].$each) { + currentUpdate = castedDoc[keys[i]][_keys[ii]]; + if (currentUpdate && currentUpdate.$each) { arrayAtomicUpdates[_keys[ii]] = (arrayAtomicUpdates[_keys[ii]] || []). - concat(castedDoc[keys[i]][_keys[ii]].$each); + concat(currentUpdate.$each); } else { arrayAtomicUpdates[_keys[ii]] = (arrayAtomicUpdates[_keys[ii]] || []). - concat([castedDoc[keys[i]][_keys[ii]]]); + concat([currentUpdate]); } } continue; From 2410add849a5ac76269527893e3bbf9e17387103 Mon Sep 17 00:00:00 2001 From: Valeri Karpov Date: Sat, 14 Oct 2017 16:49:08 -0700 Subject: [PATCH 20/78] fix(connection): allow passing in `autoIndex` as top-level option rather than requiring `config.autoIndex` Re: #5711 --- lib/connection.js | 8 ++++++-- test/connection.test.js | 2 +- 2 files changed, 7 insertions(+), 3 deletions(-) diff --git a/lib/connection.js b/lib/connection.js index 07530bb6b70..1f3cc2c5c89 100644 --- a/lib/connection.js +++ b/lib/connection.js @@ -763,9 +763,13 @@ Connection.prototype.openUri = function(uri, options, callback) { if (options) { options = utils.clone(options, { retainKeyOrder: true }); delete options.useMongoClient; - if (options.config && options.config.autoIndex != null) { - this.config.autoIndex = options.config.autoIndex !== false; + var autoIndex = options.config && options.config.autoIndex != null ? + options.config.autoIndex : + options.autoIndex; + if (autoIndex != null) { + this.config.autoIndex = autoIndex !== false; delete options.config; + delete options.autoIndex; } // Backwards compat diff --git a/test/connection.test.js b/test/connection.test.js index d682d4fff81..50fea157b60 100644 --- a/test/connection.test.js +++ b/test/connection.test.js @@ -65,7 +65,7 @@ describe('connections:', function() { it('with autoIndex (gh-5423)', function(done) { var promise = mongoose.createConnection('mongodb://localhost:27017/mongoosetest', { useMongoClient: true, - config: { autoIndex: false } + autoIndex: false }); promise.then(function(conn) { From 69e44805873fe1391eeecb76eb1438d4aa40ad3a Mon Sep 17 00:00:00 2001 From: Valeri Karpov Date: Sat, 14 Oct 2017 16:50:01 -0700 Subject: [PATCH 21/78] docs(connection): improve docs regarding reconnectTries, autoReconnect, and bufferMaxEntries Fix #5711 --- docs/connections.jade | 54 ++++++++++++++++++++----------------------- 1 file changed, 25 insertions(+), 29 deletions(-) diff --git a/docs/connections.jade b/docs/connections.jade index 56f55fc4c82..305b25aeaab 100644 --- a/docs/connections.jade +++ b/docs/connections.jade @@ -55,44 +55,40 @@ block content h3#options Options :markdown - The `connect` method also accepts an `options` object which will be passed on to the underlying driver. All options included here take precedence over options passed in the connection string. + The `connect` method also accepts an `options` object which will be passed on to the underlying MongoDB driver. :js mongoose.connect(uri, options); :markdown - The following option keys are available: - - db - passed to the [underlying driver's db instance](http://mongodb.github.io/node-mongodb-native/2.1/api/Db.html) - server - passed to the [underlying driver's server instance(s)](http://mongodb.github.io/node-mongodb-native/2.1/api/Server.html) - replset - passed to the [underlying driver's ReplSet instance](http://mongodb.github.io/node-mongodb-native/2.1/api/ReplSet.html) - user - username for authentication (if not specified in uri) - pass - password for authentication (if not specified in uri) - auth - options for authentication - mongos - passed to the [underlying driver's mongos options](http://mongodb.github.io/node-mongodb-native/2.1/api/Mongos.html) - promiseLibrary - sets the [underlying driver's promise library](http://mongodb.github.io/node-mongodb-native/2.1/api/MongoClient.html) + A full list of options can be found on the [MongoDB Node.js driver docs for `connect()`](http://mongodb.github.io/node-mongodb-native/2.2/api/MongoClient.html#connect). + Mongoose passes options to the driver without modification, modulo three exceptions that are explained below. + + * `useMongoClient` - This is a mongoose-specific option (not passed to the MongoDB driver) that opts in to mongoose 4.11's new connection logic. If you are writing a new application, you **should** set this to `true`. + * `user`/`pass` - The username and password for authentication. These options are mongoose-specific, they are equivalent to the MongoDB driver's `auth.user` and `auth.password` options. + * `autoIndex` - By default, mongoose will automatically build indexes defined in your schema when it connects. This is great for development, but not ideal for large production deployments, because index builds can cause performance degradation. If you set `autoIndex` to false, mongoose will not automatically build indexes for **any** model associated with this connection. + + Below are some of the options that are important for tuning mongoose. + + * `autoReconnect` - The underlying MongoDB driver will automatically try to reconnect when it loses connection to MongoDB. Unless you are an extremely advanced user that wants to manage their own connection pool, do **not** set this option to `false`. + * `reconnectTries` - If you're connected to a single server or mongos proxy (as opposed to a replica set), the MongoDB driver will try to reconnect every `reconnectInterval` milliseconds for `reconnectTries` times, and give up afterward. When the driver gives up, the mongoose connection emits a `reconnectFailed` event. This option does nothing for replica set connections. + * `reconnectInterval` - See `reconnectTries` + * `promiseLibrary` - sets the [underlying driver's promise library](http://mongodb.github.io/node-mongodb-native/2.1/api/MongoClient.html) + * `poolSize` - The maximum number of sockets the MongoDB driver will keep open for this connection. By default, `poolSize` is 5. Keep in mind that, as of MongoDB 3.4, MongoDB only allows one operation per socket at a time, so you may want to increase this if you find you have a few slow queries that are blocking faster queries from proceeding. + * `bufferMaxEntries` - The MongoDB driver also has its own buffering mechanism that kicks in when the driver is disconnected. Set this option to 0 and set `bufferCommands` to `false` on your schemas if you want your database operations to fail immediately when the driver is not connected, as opposed to waiting for reconnection. + Example: :js var options = { - db: { native_parser: true }, - server: { poolSize: 5 }, - replset: { rs_name: 'myReplicaSetName' }, - user: 'myUserName', - pass: 'myPassword' - } + useMongoClient: true, + autoIndex: false, // Don't build indexes + reconnectTries: Number.MAX_VALUE, // Never stop trying to reconnect + reconnectInterval: 500, // Reconnect every 500ms + poolSize: 10, // Maintain up to 10 socket connections + // If not connected, return errors immediately rather than waiting for reconnect + bufferMaxEntries: 0 + }; mongoose.connect(uri, options); - :markdown - **Note:** - The server option `auto_reconnect` is defaulted to true which _can_ be overridden. - The db option `forceServerObjectId` is set to false which _cannot_ be overridden. - - See the [driver](https://github.com/mongodb/node-mongodb-native) for more information about available options. - :markdown - **Note:** - If `auto_reconnect` is on, mongoose will give up trying to reconnect after a certain number of failures. Set the [`server.reconnectTries` and `server.reconnectInterval` options](http://mongodb.github.io/node-mongodb-native/2.1/api/Server.html) to increase the number of times mongoose will try to reconnect. - :js - // Good way to make sure mongoose never stops trying to reconnect - mongoose.connect(uri, { server: { reconnectTries: Number.MAX_VALUE } }); h3#callback Callback :markdown From 480098dcd894b471c7a660296c91184bdc22a4db Mon Sep 17 00:00:00 2001 From: Valeri Karpov Date: Sat, 14 Oct 2017 17:00:02 -0700 Subject: [PATCH 22/78] test: fix 0.10+0.12 const issue --- test/model.findOneAndUpdate.test.js | 5 +++++ 1 file changed, 5 insertions(+) diff --git a/test/model.findOneAndUpdate.test.js b/test/model.findOneAndUpdate.test.js index 8538eadac38..23c2f3cb4d0 100644 --- a/test/model.findOneAndUpdate.test.js +++ b/test/model.findOneAndUpdate.test.js @@ -1926,6 +1926,11 @@ describe('model: findOneAndUpdate:', function() { }); it('avoids edge case with middleware cloning buffers (gh-5702)', function(done) { + if (parseInt(process.version.substr(1).split('.')[0], 10) < 4) { + // Don't run on node 0.x because of `const` issues + this.skip(); + } + var uuidParse = require('uuid-parse'); function toUUID(string) { From 7c45e7ea87c8fab537a5b750ad4536a099224353 Mon Sep 17 00:00:00 2001 From: Valeri Karpov Date: Sat, 14 Oct 2017 17:05:54 -0700 Subject: [PATCH 23/78] chore: correct 4.12.2 release date --- History.md | 2 +- 1 file changed, 1 insertion(+), 1 deletion(-) diff --git a/History.md b/History.md index 550a4754ab3..1cf45cfd5db 100644 --- a/History.md +++ b/History.md @@ -1,4 +1,4 @@ -4.12.2 / 2017-10-13 +4.12.2 / 2017-10-14 =================== * docs(faq): add FAQ about using arrow functions for getters/setters, virtuals, and methods #5700 * docs(schema): document the childSchemas property and add to public API #5695 From 76d8ffeb0ccc8bce49b549a4286a35606148b007 Mon Sep 17 00:00:00 2001 From: Valeri Karpov Date: Sat, 14 Oct 2017 17:45:55 -0700 Subject: [PATCH 24/78] refactor(query): make setDefaultsOnInsert take a filter as opposed to a query Re: #5708 --- lib/query.js | 4 ++-- lib/services/setDefaultsOnInsert.js | 8 ++++---- 2 files changed, 6 insertions(+), 6 deletions(-) diff --git a/lib/query.js b/lib/query.js index 829e94ffad1..28e8a648586 100644 --- a/lib/query.js +++ b/lib/query.js @@ -2227,7 +2227,7 @@ Query.prototype._findAndModify = function(type, callback) { }; } else { castedDoc = castDoc(this, opts.overwrite); - castedDoc = setDefaultsOnInsert(this, schema, castedDoc, opts); + castedDoc = setDefaultsOnInsert(this._conditions, schema, castedDoc, opts); if (!castedDoc) { if (opts.upsert) { // still need to do the upsert to empty doc @@ -2939,7 +2939,7 @@ function _update(query, op, conditions, doc, options, callback) { } } - castedDoc = setDefaultsOnInsert(query, query.schema, castedDoc, options); + castedDoc = setDefaultsOnInsert(query._conditions, query.schema, castedDoc, options); if (!castedDoc) { // Make sure promises know that this is still an update, see gh-2796 query.op = op; diff --git a/lib/services/setDefaultsOnInsert.js b/lib/services/setDefaultsOnInsert.js index 26ead96d73d..265d4c8fd0f 100644 --- a/lib/services/setDefaultsOnInsert.js +++ b/lib/services/setDefaultsOnInsert.js @@ -5,7 +5,7 @@ var modifiedPaths = require('./common').modifiedPaths; /** * Applies defaults to update and findOneAndUpdate operations. * - * @param {Query} query + * @param {Object} filter * @param {Schema} schema * @param {Object} castedDoc * @param {Object} options @@ -13,7 +13,7 @@ var modifiedPaths = require('./common').modifiedPaths; * @api private */ -module.exports = function(query, schema, castedDoc, options) { +module.exports = function(filter, schema, castedDoc, options) { var keys = Object.keys(castedDoc || {}); var updatedKeys = {}; var updatedValues = {}; @@ -33,11 +33,11 @@ module.exports = function(query, schema, castedDoc, options) { modifiedPaths(castedDoc, '', modified); } - var paths = Object.keys(query._conditions); + var paths = Object.keys(filter); var numPaths = paths.length; for (i = 0; i < numPaths; ++i) { var path = paths[i]; - var condition = query._conditions[path]; + var condition = filter[path]; if (condition && typeof condition === 'object') { var conditionKeys = Object.keys(condition); var numConditionKeys = conditionKeys.length; From 23da8c9eaae8388eb98aeb8fdb94b51827c16895 Mon Sep 17 00:00:00 2001 From: Valeri Karpov Date: Sun, 15 Oct 2017 09:52:57 -0700 Subject: [PATCH 25/78] test(model): repro #5708 --- test/model.test.js | 31 +++++++++++++++++++++++++++++++ 1 file changed, 31 insertions(+) diff --git a/test/model.test.js b/test/model.test.js index 6a07644a0ea..ebe2ffe8d6d 100644 --- a/test/model.test.js +++ b/test/model.test.js @@ -5611,6 +5611,37 @@ describe('Model', function() { }); }); + it('bulkWrite with setDefaultsOnInsert (gh-5708)', function(done) { + var schema = new Schema({ + str: { type: String, default: 'test' }, + num: Number + }); + + var M = db.model('gh5708', schema); + + var ops = [ + { + updateOne: { + filter: { num: 0 }, + update: { + $inc: { num: 1 } + }, + upsert: true, + setDefaultsOnInsert: true + } + } + ]; + M.bulkWrite(ops, function(error) { + assert.ifError(error); + M.findOne({}).lean().exec(function(error, doc) { + assert.ifError(error); + assert.strictEqual(doc.str, 'test'); + assert.strictEqual(doc.num, 1); + done(); + }); + }); + }); + it('insertMany with Decimal (gh-5190)', function(done) { start.mongodVersion(function(err, version) { if (err) { From 1452950e6f001268d3cd26e72e32b91db60620c8 Mon Sep 17 00:00:00 2001 From: Valeri Karpov Date: Sun, 15 Oct 2017 09:54:05 -0700 Subject: [PATCH 26/78] fix(model): handle setDefaultsOnInsert option for bulkWrite updateOne and updateMany Fix #5708 --- lib/model.js | 27 ++++++++++++++++++++------- 1 file changed, 20 insertions(+), 7 deletions(-) diff --git a/lib/model.js b/lib/model.js index 2c58e588bea..64a078aa917 100644 --- a/lib/model.js +++ b/lib/model.js @@ -23,6 +23,7 @@ var isPathSelectedInclusive = require('./services/projection/isPathSelectedInclu var mpath = require('mpath'); var parallel = require('async/parallel'); var parallelLimit = require('async/parallelLimit'); +var setDefaultsOnInsert = require('./services/setDefaultsOnInsert'); var util = require('util'); var utils = require('./utils'); @@ -2239,12 +2240,18 @@ Model.bulkWrite = function(ops, options, callback) { }); }; } else if (op['updateOne']) { + op = op['updateOne']; return function(callback) { try { - op['updateOne']['filter'] = cast(_this.schema, - op['updateOne']['filter']); - op['updateOne']['update'] = castUpdate(_this.schema, - op['updateOne']['update'], _this.schema.options.strict); + op['filter'] = cast(_this.schema, op['filter']); + op['update'] = castUpdate(_this.schema, op['update'], + _this.schema.options.strict); + if (op.setDefaultsOnInsert) { + setDefaultsOnInsert(op['filter'], _this.schema, op['update'], { + setDefaultsOnInsert: true, + upsert: op.upsert + }); + } } catch (error) { return callback(error); } @@ -2252,14 +2259,20 @@ Model.bulkWrite = function(ops, options, callback) { callback(null); }; } else if (op['updateMany']) { + op = op['updateMany']; return function(callback) { try { - op['updateMany']['filter'] = cast(_this.schema, - op['updateMany']['filter']); - op['updateMany']['update'] = castUpdate(_this.schema, op['updateMany']['update'], { + op['filter'] = cast(_this.schema, op['filter']); + op['update'] = castUpdate(_this.schema, op['update'], { strict: _this.schema.options.strict, overwrite: false }); + if (op.setDefaultsOnInsert) { + setDefaultsOnInsert(op['filter'], _this.schema, op['update'], { + setDefaultsOnInsert: true, + upsert: op.upsert + }); + } } catch (error) { return callback(error); } From 74f2493320e3ee457729624c1cbb63a3060e59dc Mon Sep 17 00:00:00 2001 From: Valeri Karpov Date: Sun, 15 Oct 2017 15:18:09 -0700 Subject: [PATCH 27/78] feat(connection): add createCollection() helper Fix #5712 --- lib/connection.js | 28 ++++++++++++++++++++++++++-- test/connection.test.js | 26 ++++++++++++++++++++++++++ 2 files changed, 52 insertions(+), 2 deletions(-) diff --git a/lib/connection.js b/lib/connection.js index 1f3cc2c5c89..ac118ad7fcd 100644 --- a/lib/connection.js +++ b/lib/connection.js @@ -368,6 +368,28 @@ Connection.prototype._openWithoutPromise = function() { }); }; +/** + * Helper for `createCollection()`. Will explicitly create the given collection + * with specified options. Used to create [capped collections](https://docs.mongodb.com/manual/core/capped-collections/) + * and [views](https://docs.mongodb.com/manual/core/views/) from mongoose. + * + * Options are passed down without modification to the [MongoDB driver's `createCollection()` function](http://mongodb.github.io/node-mongodb-native/2.2/api/Db.html#createCollection) + * + * @param {string} collection The collection to delete + * @param {Object} [options] see [MongoDB driver docs](http://mongodb.github.io/node-mongodb-native/2.2/api/Db.html#createCollection) + * @param {Function} [callback] + * @return {Promise} + * @api public + */ + +Connection.prototype.createCollection = _wrapConnHelper(function createCollection(collection, options, cb) { + if (typeof options === 'function') { + cb = options; + options = {}; + } + this.db.createCollection(collection, options, cb); +}); + /** * Helper for `dropCollection()`. Will delete the given collection, including * all documents and indexes. @@ -403,8 +425,10 @@ function _wrapConnHelper(fn) { return function() { var _this = this; var Promise = PromiseProvider.get(); - var argsWithoutCb = Array.prototype.slice.call(arguments, 0, fn.length - 1); - var cb = arguments[arguments.length - 1]; + var cb = arguments.length > 0 ? arguments[arguments.length - 1] : []; + var argsWithoutCb = typeof cb === 'function' ? + Array.prototype.slice.call(arguments, 0, arguments.length - 1) : + Array.prototype.slice.call(arguments); var promise = new Promise.ES6(function(resolve, reject) { if (_this.readyState !== STATES.connected) { _this.on('open', function() { diff --git a/test/connection.test.js b/test/connection.test.js index 50fea157b60..fc2c5aa2a4b 100644 --- a/test/connection.test.js +++ b/test/connection.test.js @@ -297,6 +297,32 @@ describe('connections:', function() { assert.ok(!doc); }); }); + + it('createCollection()', function() { + return conn.dropDatabase(). + then(function() { + return conn.createCollection('gh5712', { + capped: true, + size: 1024 + }); + }). + then(function() { + return conn.db.listCollections().toArray(); + }). + then(function(collections) { + var names = collections.map(function(c) { return c.name; }); + assert.ok(names.indexOf('gh5712') !== -1); + assert.ok(collections[names.indexOf('gh5712')].options.capped); + return conn.createCollection('gh5712_0'); + }). + then(function() { + return conn.db.listCollections().toArray(); + }). + then(function(collections) { + var names = collections.map(function(c) { return c.name; }); + assert.ok(names.indexOf('gh5712') !== -1); + }); + }); }); it('should allow closing a closed connection', function(done) { From fa6bce36e5eb30f979c8bc88886447466ebadc8c Mon Sep 17 00:00:00 2001 From: Valeri Karpov Date: Sun, 15 Oct 2017 21:10:53 -0700 Subject: [PATCH 28/78] refactor(error): move error.js -> error/index.js for sanity --- lib/error/browserMissingSchema.js | 2 +- lib/error/cast.js | 2 +- lib/error/disconnected.js | 2 +- lib/error/divergentArray.js | 2 +- lib/{error.js => error/index.js} | 18 +++++++++--------- lib/error/missingSchema.js | 2 +- lib/error/notFound.js | 2 +- lib/error/objectExpected.js | 2 +- lib/error/objectParameter.js | 2 +- lib/error/overwriteModel.js | 2 +- lib/error/strict.js | 2 +- lib/error/validation.js | 2 +- lib/error/validator.js | 2 +- lib/error/version.js | 2 +- 14 files changed, 22 insertions(+), 22 deletions(-) rename lib/{error.js => error/index.js} (66%) diff --git a/lib/error/browserMissingSchema.js b/lib/error/browserMissingSchema.js index 898915596db..24140f58a59 100644 --- a/lib/error/browserMissingSchema.js +++ b/lib/error/browserMissingSchema.js @@ -2,7 +2,7 @@ * Module dependencies. */ -var MongooseError = require('../error.js'); +var MongooseError = require('./'); /*! * MissingSchema Error constructor. diff --git a/lib/error/cast.js b/lib/error/cast.js index 59e6c180736..9bd9bc91160 100644 --- a/lib/error/cast.js +++ b/lib/error/cast.js @@ -2,7 +2,7 @@ * Module dependencies. */ -var MongooseError = require('../error.js'); +var MongooseError = require('./'); var util = require('util'); /** diff --git a/lib/error/disconnected.js b/lib/error/disconnected.js index a8c3e12f5a0..e17f71ce0e6 100644 --- a/lib/error/disconnected.js +++ b/lib/error/disconnected.js @@ -2,7 +2,7 @@ * Module dependencies. */ -var MongooseError = require('../error.js'); +var MongooseError = require('./'); /** * Casting Error constructor. diff --git a/lib/error/divergentArray.js b/lib/error/divergentArray.js index 473f7dc446f..de949cb0206 100644 --- a/lib/error/divergentArray.js +++ b/lib/error/divergentArray.js @@ -3,7 +3,7 @@ * Module dependencies. */ -var MongooseError = require('../error.js'); +var MongooseError = require('./'); /*! * DivergentArrayError constructor. diff --git a/lib/error.js b/lib/error/index.js similarity index 66% rename from lib/error.js rename to lib/error/index.js index b0e8ff53f82..2dbf4963880 100644 --- a/lib/error.js +++ b/lib/error/index.js @@ -37,7 +37,7 @@ module.exports = exports = MongooseError; * @api public */ -MongooseError.messages = require('./error/messages'); +MongooseError.messages = require('./messages'); // backward compat MongooseError.Messages = MongooseError.messages; @@ -51,16 +51,16 @@ MongooseError.Messages = MongooseError.messages; * @api public */ -MongooseError.DocumentNotFoundError = require('./error/notFound'); +MongooseError.DocumentNotFoundError = require('./notFound'); /*! * Expose subclasses */ -MongooseError.CastError = require('./error/cast'); -MongooseError.ValidationError = require('./error/validation'); -MongooseError.ValidatorError = require('./error/validator'); -MongooseError.VersionError = require('./error/version'); -MongooseError.OverwriteModelError = require('./error/overwriteModel'); -MongooseError.MissingSchemaError = require('./error/missingSchema'); -MongooseError.DivergentArrayError = require('./error/divergentArray'); +MongooseError.CastError = require('./cast'); +MongooseError.ValidationError = require('./validation'); +MongooseError.ValidatorError = require('./validator'); +MongooseError.VersionError = require('./version'); +MongooseError.OverwriteModelError = require('./overwriteModel'); +MongooseError.MissingSchemaError = require('./missingSchema'); +MongooseError.DivergentArrayError = require('./divergentArray'); diff --git a/lib/error/missingSchema.js b/lib/error/missingSchema.js index a2445b40364..dacfb1c7abe 100644 --- a/lib/error/missingSchema.js +++ b/lib/error/missingSchema.js @@ -3,7 +3,7 @@ * Module dependencies. */ -var MongooseError = require('../error.js'); +var MongooseError = require('./'); /*! * MissingSchema Error constructor. diff --git a/lib/error/notFound.js b/lib/error/notFound.js index f63f8f1bacb..fecbd8c584a 100644 --- a/lib/error/notFound.js +++ b/lib/error/notFound.js @@ -4,7 +4,7 @@ * Module dependencies. */ -var MongooseError = require('../error.js'); +var MongooseError = require('./'); var util = require('util'); /*! diff --git a/lib/error/objectExpected.js b/lib/error/objectExpected.js index eb764fc2df3..5a64c9debee 100644 --- a/lib/error/objectExpected.js +++ b/lib/error/objectExpected.js @@ -2,7 +2,7 @@ * Module dependencies. */ -var MongooseError = require('../error.js'); +var MongooseError = require('./'); /** * Strict mode error constructor diff --git a/lib/error/objectParameter.js b/lib/error/objectParameter.js index bf3f74a06c7..450771b671d 100644 --- a/lib/error/objectParameter.js +++ b/lib/error/objectParameter.js @@ -2,7 +2,7 @@ * Module dependencies. */ -var MongooseError = require('../error.js'); +var MongooseError = require('./'); /** * Constructor for errors that happen when a parameter that's expected to be diff --git a/lib/error/overwriteModel.js b/lib/error/overwriteModel.js index 85c29c8c935..27ce7fe7069 100644 --- a/lib/error/overwriteModel.js +++ b/lib/error/overwriteModel.js @@ -3,7 +3,7 @@ * Module dependencies. */ -var MongooseError = require('../error.js'); +var MongooseError = require('./'); /*! * OverwriteModel Error constructor. diff --git a/lib/error/strict.js b/lib/error/strict.js index e0e94eded7b..2d0c108fda9 100644 --- a/lib/error/strict.js +++ b/lib/error/strict.js @@ -2,7 +2,7 @@ * Module dependencies. */ -var MongooseError = require('../error.js'); +var MongooseError = require('./'); /** * Strict mode error constructor diff --git a/lib/error/validation.js b/lib/error/validation.js index e51af657fdd..2202d82a97b 100644 --- a/lib/error/validation.js +++ b/lib/error/validation.js @@ -2,7 +2,7 @@ * Module requirements */ -var MongooseError = require('../error.js'); +var MongooseError = require('./'); var utils = require('../utils'); /** diff --git a/lib/error/validator.js b/lib/error/validator.js index d05ea491471..d934b524a14 100644 --- a/lib/error/validator.js +++ b/lib/error/validator.js @@ -2,7 +2,7 @@ * Module dependencies. */ -var MongooseError = require('../error.js'); +var MongooseError = require('./'); /** * Schema validator error diff --git a/lib/error/version.js b/lib/error/version.js index e509b9b5fa5..719c07f1254 100644 --- a/lib/error/version.js +++ b/lib/error/version.js @@ -4,7 +4,7 @@ * Module dependencies. */ -var MongooseError = require('../error.js'); +var MongooseError = require('./'); /** * Version Error constructor. From d9dce1bfeebe188004df58773866f14f73fea8d4 Mon Sep 17 00:00:00 2001 From: Valeri Karpov Date: Sun, 15 Oct 2017 21:34:51 -0700 Subject: [PATCH 29/78] fix: correctly bubble up left/joined events for replica set Fix #5718 --- lib/drivers/node-mongodb-native/connection.js | 8 ++++++++ 1 file changed, 8 insertions(+) diff --git a/lib/drivers/node-mongodb-native/connection.js b/lib/drivers/node-mongodb-native/connection.js index 609889564b9..17e96e7f1ac 100644 --- a/lib/drivers/node-mongodb-native/connection.js +++ b/lib/drivers/node-mongodb-native/connection.js @@ -204,6 +204,14 @@ NativeConnection.prototype.doOpenSet = function(fn) { : new ReplSetServers(servers, this.options.replset || this.options.replSet); this.db = new Db(this.name, server, this.options.db); + this.db.s.topology.on('left', function(data) { + _this.emit('left', data); + }); + + this.db.s.topology.on('joined', function(data) { + _this.emit('joined', data); + }); + this.db.on('fullsetup', function() { _this.emit('fullsetup'); }); From dfab80c26d8086e9f535a67075da3b5e3af7f541 Mon Sep 17 00:00:00 2001 From: Valeri Karpov Date: Mon, 16 Oct 2017 08:32:24 -0700 Subject: [PATCH 30/78] fix(connection): emit 'reconnect' event as well as 'reconnected' for consistency with driver These two events are interchangeable, 'reconnected' is for backwards compat because that's what mongoose has always supported, but 'reconnect' is the event the driver uses. Fix #5719 --- lib/connection.js | 1 + lib/drivers/node-mongodb-native/connection.js | 2 ++ test/connection.test.js | 7 +++++++ 3 files changed, 10 insertions(+) diff --git a/lib/connection.js b/lib/connection.js index 1f3cc2c5c89..8919115629c 100644 --- a/lib/connection.js +++ b/lib/connection.js @@ -799,6 +799,7 @@ Connection.prototype.openUri = function(uri, options, callback) { // Backwards compat for mongoose 4.x db.on('reconnect', function() { _this.readyState = STATES.connected; + _this.emit('reconnect'); _this.emit('reconnected'); }); db.s.topology.on('reconnectFailed', function() { diff --git a/lib/drivers/node-mongodb-native/connection.js b/lib/drivers/node-mongodb-native/connection.js index 17e96e7f1ac..b92d8254716 100644 --- a/lib/drivers/node-mongodb-native/connection.js +++ b/lib/drivers/node-mongodb-native/connection.js @@ -162,6 +162,7 @@ function listen(conn) { }); conn.db.on('reconnect', function() { conn.readyState = STATES.connected; + conn.emit('reconnect'); conn.emit('reconnected'); conn.onOpen(); }); @@ -171,6 +172,7 @@ function listen(conn) { conn.db.on('open', function(err, db) { if (STATES.disconnected === conn.readyState && db && db.databaseName) { conn.readyState = STATES.connected; + conn.emit('reconnect'); conn.emit('reconnected'); } }); diff --git a/test/connection.test.js b/test/connection.test.js index 50fea157b60..3d97056ba27 100644 --- a/test/connection.test.js +++ b/test/connection.test.js @@ -94,6 +94,7 @@ describe('connections:', function() { var numConnected = 0; var numDisconnected = 0; var numReconnected = 0; + var numReconnect = 0; var numClose = 0; conn = mongoose.createConnection('mongodb://localhost:27000/mongoosetest', { useMongoClient: true @@ -105,6 +106,10 @@ describe('connections:', function() { conn.on('disconnected', function() { ++numDisconnected; }); + conn.on('reconnect', function() { + ++numReconnect; + }); + // Same as `reconnect`, just for backwards compat conn.on('reconnected', function() { ++numReconnected; }); @@ -127,6 +132,7 @@ describe('connections:', function() { assert.equal(conn.readyState, conn.states.disconnected); assert.equal(numDisconnected, 1); assert.equal(numReconnected, 0); + assert.equal(numReconnect, 0); }). then(function() { return server.start(); @@ -140,6 +146,7 @@ describe('connections:', function() { assert.equal(conn.readyState, conn.states.connected); assert.equal(numDisconnected, 1); assert.equal(numReconnected, 1); + assert.equal(numReconnect, 1); assert.equal(numClose, 0); conn.close(); From 12b2087adbedde4e5599bcdd2fbea10f60457f8c Mon Sep 17 00:00:00 2001 From: Valeri Karpov Date: Mon, 16 Oct 2017 09:10:38 -0700 Subject: [PATCH 31/78] chore: release 4.12.3 --- History.md | 10 ++++++++++ package.json | 2 +- 2 files changed, 11 insertions(+), 1 deletion(-) diff --git a/History.md b/History.md index 1cf45cfd5db..ffff568ac93 100644 --- a/History.md +++ b/History.md @@ -1,3 +1,13 @@ +4.12.3 / 2017-10-16 +=================== + * fix(connection): emit 'reconnect' event as well as 'reconnected' for consistency with driver #5719 + * fix: correctly bubble up left/joined events for replica set #5718 + * fix(connection): allow passing in `autoIndex` as top-level option rather than requiring `config.autoIndex` #5711 + * docs(connection): improve docs regarding reconnectTries, autoReconnect, and bufferMaxEntries #5711 + * fix(query): handle null with addToSet/push/pull/pullAll update validators #5710 + * fix(model): handle setDefaultsOnInsert option for bulkWrite updateOne and updateMany #5708 + * fix(query): avoid infinite recursion edge case when cloning a buffer #5702 + 4.12.2 / 2017-10-14 =================== * docs(faq): add FAQ about using arrow functions for getters/setters, virtuals, and methods #5700 diff --git a/package.json b/package.json index 2df39dd808a..96878cb9b90 100644 --- a/package.json +++ b/package.json @@ -1,7 +1,7 @@ { "name": "mongoose", "description": "Mongoose MongoDB ODM", - "version": "4.12.3-pre", + "version": "4.12.3", "author": "Guillermo Rauch ", "keywords": [ "mongodb", From 1aa4390c52b216ff7f3210b1802d4c68488fd82d Mon Sep 17 00:00:00 2001 From: Valeri Karpov Date: Mon, 16 Oct 2017 09:12:04 -0700 Subject: [PATCH 32/78] chore: now working on 4.12.4 --- package.json | 2 +- 1 file changed, 1 insertion(+), 1 deletion(-) diff --git a/package.json b/package.json index 96878cb9b90..7ee3ffe6b3e 100644 --- a/package.json +++ b/package.json @@ -1,7 +1,7 @@ { "name": "mongoose", "description": "Mongoose MongoDB ODM", - "version": "4.12.3", + "version": "4.12.4-pre", "author": "Guillermo Rauch ", "keywords": [ "mongodb", From db63ef53f8f3eed1ea79c945bc0d1b6988096a23 Mon Sep 17 00:00:00 2001 From: Valeri Karpov Date: Tue, 17 Oct 2017 14:12:52 -0700 Subject: [PATCH 33/78] test(model): repro #5721 --- test/model.discriminator.test.js | 22 ++++++++++++++++++++++ 1 file changed, 22 insertions(+) diff --git a/test/model.discriminator.test.js b/test/model.discriminator.test.js index db68b68b717..e473571b659 100644 --- a/test/model.discriminator.test.js +++ b/test/model.discriminator.test.js @@ -512,6 +512,28 @@ describe('model', function() { done(); }); + it('clone() allows reusing with different models (gh-5721)', function(done) { + var schema = new mongoose.Schema({ + name: String + }); + + var schemaExt = new mongoose.Schema({ + nameExt: String + }); + + var ModelA = db.model('gh5721_a0', schema); + ModelA.discriminator('gh5721_a1', schemaExt); + + ModelA.findOneAndUpdate({}, { $set: { name: 'test' } }, function(error) { + assert.ifError(error); + + var ModelB = db.model('gh5721_b0', schema.clone()); + ModelB.discriminator('gh5721_b1', schemaExt.clone()); + + done(); + }); + }); + it('copies query hooks (gh-5147)', function(done) { var options = { discriminatorKey: 'kind' }; From d2a4904d298c174ba9186a1ec8d18f3123ef66bf Mon Sep 17 00:00:00 2001 From: Valeri Karpov Date: Tue, 17 Oct 2017 14:13:10 -0700 Subject: [PATCH 34/78] fix(model): avoid copying recursive $$context object when creating discriminator after querying Fix #5721 --- lib/schematype.js | 7 +++++++ 1 file changed, 7 insertions(+) diff --git a/lib/schematype.js b/lib/schematype.js index 779588159d2..4be2d132fa5 100644 --- a/lib/schematype.js +++ b/lib/schematype.js @@ -42,6 +42,13 @@ function SchemaType(path, options, instance) { this[i].apply(this, opts); } } + + Object.defineProperty(this, '$$context', { + enumerable: false, + configurable: false, + writable: true, + value: null + }); } /** From 2e326f0774824ead0a1291cc79cd18f6a4b00417 Mon Sep 17 00:00:00 2001 From: Valeri Karpov Date: Tue, 17 Oct 2017 14:49:58 -0700 Subject: [PATCH 35/78] fix(collection): ensure queued operations run on the next tick Fix #5562 --- lib/collection.js | 5 ++++- 1 file changed, 4 insertions(+), 1 deletion(-) diff --git a/lib/collection.js b/lib/collection.js index d894173e440..7bc5145ecc8 100644 --- a/lib/collection.js +++ b/lib/collection.js @@ -80,7 +80,10 @@ Collection.prototype.conn; Collection.prototype.onOpen = function() { this.buffer = false; - this.doQueue(); + var _this = this; + setImmediate(function() { + _this.doQueue(); + }); }; /** From 006cb7163d23699fe4ab0eaf7a3cf67b7536a48a Mon Sep 17 00:00:00 2001 From: Sheyi Folorunsho Adekoya Date: Wed, 18 Oct 2017 13:02:25 +0100 Subject: [PATCH 36/78] Fixes typo in schematypes.jade Fixes typo in schematypes.jade about adding indexes --- docs/schematypes.jade | 2 +- 1 file changed, 1 insertion(+), 1 deletion(-) diff --git a/docs/schematypes.jade b/docs/schematypes.jade index 2cceb02ad2b..0411d5394e5 100644 --- a/docs/schematypes.jade +++ b/docs/schematypes.jade @@ -143,7 +143,7 @@ block content using schema type options. :markdown - * `index`: boolean, whether to define an on this property. + * `index`: boolean, whether to define an [index](https://docs.mongodb.com/manual/indexes/) on this property. * `unique`: boolean, whether to define a [unique index](https://docs.mongodb.com/manual/core/index-unique/) on this property. * `sparse`: boolean, whether to define a [sparse index](https://docs.mongodb.com/manual/core/index-sparse/) on this property. :js From 53856939f2e56fbf37a0dec98be3a11840acb324 Mon Sep 17 00:00:00 2001 From: Valeri Karpov Date: Wed, 18 Oct 2017 09:05:51 -0700 Subject: [PATCH 37/78] refactor(aggregate): clean up exec() for fewer LoD violations --- lib/aggregate.js | 49 +++++++++++++++++++++++------------------------- 1 file changed, 23 insertions(+), 26 deletions(-) diff --git a/lib/aggregate.js b/lib/aggregate.js index 9d0d00f66e5..c56fec66fe7 100644 --- a/lib/aggregate.js +++ b/lib/aggregate.js @@ -626,25 +626,25 @@ Aggregate.prototype.exec = function(callback) { } var _this = this; var Promise = PromiseProvider.get(); - var options = utils.clone(this.options); + var options = utils.clone(this.options || {}); + var pipeline = this._pipeline; + var collection = this._model.collection; if (options && options.cursor) { if (options.cursor.async) { delete options.cursor.async; return new Promise.ES6(function(resolve) { - if (!_this._model.collection.buffer) { + if (!collection.buffer) { process.nextTick(function() { - var cursor = _this._model.collection. - aggregate(_this._pipeline, options || {}); + var cursor = collection.aggregate(pipeline, options); decorateCursor(cursor); resolve(cursor); callback && callback(null, cursor); }); return; } - _this._model.collection.emitter.once('queue', function() { - var cursor = _this._model.collection. - aggregate(_this._pipeline, options || {}); + collection.emitter.once('queue', function() { + var cursor = collection.aggregate(pipeline, options); decorateCursor(cursor); resolve(cursor); callback && callback(null, cursor); @@ -654,14 +654,13 @@ Aggregate.prototype.exec = function(callback) { delete options.cursor.useMongooseAggCursor; return new AggregationCursor(this); } - var cursor = this._model.collection. - aggregate(this._pipeline, this.options || {}); + var cursor = collection.aggregate(pipeline, options); decorateCursor(cursor); return cursor; } return new Promise.ES6(function(resolve, reject) { - if (!_this._pipeline.length) { + if (!pipeline.length) { var err = new Error('Aggregate has empty pipeline'); if (callback) { callback(err); @@ -672,22 +671,20 @@ Aggregate.prototype.exec = function(callback) { prepareDiscriminatorPipeline(_this); - _this._model - .collection - .aggregate(_this._pipeline, _this.options || {}, function(error, result) { - if (error) { - if (callback) { - callback(error); - } - reject(error); - return; - } + collection.aggregate(pipeline, options, function(error, result) { + if (error) { + if (callback) { + callback(error); + } + reject(error); + return; + } - if (callback) { - callback(null, result); - } - resolve(result); - }); + if (callback) { + callback(null, result); + } + resolve(result); + }); }); }; @@ -781,7 +778,7 @@ function prepareDiscriminatorPipeline(aggregate) { } else { var match = {}; match[discriminatorKey] = discriminatorValue; - aggregate._pipeline = [{$match: match}].concat(originalPipeline); + aggregate._pipeline.unshift({ $match: match }); } } } From ae2b5c9bc063235c9aac831e0796ef036b7a4fa7 Mon Sep 17 00:00:00 2001 From: Valeri Karpov Date: Wed, 18 Oct 2017 09:06:37 -0700 Subject: [PATCH 38/78] feat(AggregationCursor): run pre aggregate hooks with agg cursor --- lib/cursor/AggregationCursor.js | 15 +++++++++++---- lib/schema.js | 1 + package-lock.json | 2 +- 3 files changed, 13 insertions(+), 5 deletions(-) diff --git a/lib/cursor/AggregationCursor.js b/lib/cursor/AggregationCursor.js index a56d6a54d0c..2f360731224 100644 --- a/lib/cursor/AggregationCursor.js +++ b/lib/cursor/AggregationCursor.js @@ -14,6 +14,9 @@ var util = require('util'); * in addition to several other mechanisms for loading documents from MongoDB * one at a time. * + * Creating an AggregationCursor executes the model's pre aggregate hooks, + * but **not** the model's post aggregate hooks. + * * Unless you're an advanced user, do **not** instantiate this class directly. * Use [`Aggregate#cursor()`](/docs/api.html#aggregate_Aggregate-cursor) instead. * @@ -47,13 +50,17 @@ util.inherits(AggregationCursor, Readable); function _init(model, c, agg) { if (!model.collection.buffer) { - c.cursor = model.collection.aggregate(agg._pipeline, agg.options || {}); - c.emit('cursor', c.cursor); - } else { - model.collection.emitter.once('queue', function() { + model.hooks.execPre('aggregate', agg, function() { c.cursor = model.collection.aggregate(agg._pipeline, agg.options || {}); c.emit('cursor', c.cursor); }); + } else { + model.collection.emitter.once('queue', function() { + model.hooks.execPre('aggregate', agg, function() { + c.cursor = model.collection.aggregate(agg._pipeline, agg.options || {}); + c.emit('cursor', c.cursor); + }); + }); } } diff --git a/lib/schema.js b/lib/schema.js index 177f4f012ba..49160f41d71 100644 --- a/lib/schema.js +++ b/lib/schema.js @@ -13,6 +13,7 @@ var SchemaType = require('./schematype'); var mpath = require('mpath'); var IS_KAREEM_HOOK = { + aggregate: true, count: true, find: true, findOne: true, diff --git a/package-lock.json b/package-lock.json index 98a55a69610..9b614be3b2b 100644 --- a/package-lock.json +++ b/package-lock.json @@ -1,6 +1,6 @@ { "name": "mongoose", - "version": "4.12.2-pre", + "version": "4.13.0-pre", "lockfileVersion": 1, "requires": true, "dependencies": { From 6478176e0aced0450aee0d395524f5ab9f8ef05c Mon Sep 17 00:00:00 2001 From: Valeri Karpov Date: Wed, 18 Oct 2017 09:41:34 -0700 Subject: [PATCH 39/78] feat(aggregate): add pre/post aggregate middleware Fix #5251 --- lib/aggregate.js | 39 ++++++++---- test/aggregate.test.js | 137 +++++++++++++++++++++++++++++++++++++++-- 2 files changed, 159 insertions(+), 17 deletions(-) diff --git a/lib/aggregate.js b/lib/aggregate.js index c56fec66fe7..a7d57d0b462 100644 --- a/lib/aggregate.js +++ b/lib/aggregate.js @@ -625,6 +625,7 @@ Aggregate.prototype.exec = function(callback) { throw new Error('Aggregate not bound to any Model'); } var _this = this; + var model = this._model; var Promise = PromiseProvider.get(); var options = utils.clone(this.options || {}); var pipeline = this._pipeline; @@ -671,19 +672,34 @@ Aggregate.prototype.exec = function(callback) { prepareDiscriminatorPipeline(_this); - collection.aggregate(pipeline, options, function(error, result) { + model.hooks.execPre('aggregate', _this, function(error) { if (error) { - if (callback) { - callback(error); - } - reject(error); - return; + var _opts = { error: error }; + return model.hooks.execPost('aggregate', _this, [null], _opts, function(error) { + if (callback) { + callback(error); + } + reject(error); + }); } - if (callback) { - callback(null, result); - } - resolve(result); + collection.aggregate(pipeline, options, function(error, result) { + var _opts = { error: error }; + model.hooks.execPost('aggregate', _this, [result], _opts, function(error, result) { + if (error) { + if (callback) { + callback(error); + } + reject(error); + return; + } + + if (callback) { + callback(null, result); + } + resolve(result); + }); + }); }); }); }; @@ -755,6 +771,8 @@ function isOperator(obj) { * @param {Aggregate} aggregate Aggregate to prepare */ +Aggregate._prepareDiscriminatorPipeline = prepareDiscriminatorPipeline; + function prepareDiscriminatorPipeline(aggregate) { var schema = aggregate._model.schema, discriminatorMapping = schema && schema.discriminatorMapping; @@ -783,7 +801,6 @@ function prepareDiscriminatorPipeline(aggregate) { } } - /*! * Exports */ diff --git a/test/aggregate.test.js b/test/aggregate.test.js index 2af32acb83c..38a6f06dc26 100644 --- a/test/aggregate.test.js +++ b/test/aggregate.test.js @@ -337,9 +337,7 @@ describe('aggregate: ', function() { assert.equal(aggregate.near({ a: 1 }), aggregate); // Run exec so we apply discriminator pipeline - assert.throws(function() { - aggregate.exec(); - }, /Cannot read property 'aggregate' of undefined|Cannot call method 'aggregate' of undefined/); + Aggregate._prepareDiscriminatorPipeline(aggregate); assert.deepEqual(aggregate._pipeline, [{ $geoNear: { a: 1, query: { __t: 'subschema' } } }]); @@ -347,9 +345,7 @@ describe('aggregate: ', function() { aggregate._model = stub; aggregate.near({ b: 2, query: { x: 1 } }); - assert.throws(function() { - aggregate.exec(); - }, /Cannot read property 'aggregate' of undefined|Cannot call method 'aggregate' of undefined/); + Aggregate._prepareDiscriminatorPipeline(aggregate); assert.deepEqual(aggregate._pipeline, [{ $geoNear: { b: 2, query: { x: 1, __t: 'subschema' } } }]); @@ -863,6 +859,135 @@ describe('aggregate: ', function() { }); }); + describe('middleware (gh-5251)', function() { + var db; + + before(function() { + db = start(); + }); + + after(function(done) { + db.close(done); + }); + + it('pre', function(done) { + var s = new Schema({ name: String }); + + var called = 0; + s.pre('aggregate', function(next) { + ++called; + next(); + }); + + var M = db.model('gh5251', s); + + M.aggregate([{ $match: { name: 'test' } }], function(error, res) { + assert.ifError(error); + assert.deepEqual(res, []); + assert.equal(called, 1); + done(); + }); + }); + + it('post', function(done) { + var s = new Schema({ name: String }); + + var calledWith = []; + s.post('aggregate', function(res, next) { + calledWith.push(res); + next(); + }); + + var M = db.model('gh5251_post', s); + + M.aggregate([{ $match: { name: 'test' } }], function(error, res) { + assert.ifError(error); + assert.deepEqual(res, []); + assert.equal(calledWith.length, 1); + assert.deepEqual(calledWith[0], []); + done(); + }); + }); + + it('error handler with agg error', function(done) { + var s = new Schema({ name: String }); + + var calledWith = []; + s.post('aggregate', function(error, res, next) { + calledWith.push(error); + next(); + }); + + var M = db.model('gh5251_error_agg', s); + + M.aggregate([{ $fakeStage: { name: 'test' } }], function(error, res) { + assert.ok(error); + assert.equal(error.message, + 'Unrecognized pipeline stage name: \'$fakeStage\''); + assert.equal(res, null); + assert.equal(calledWith.length, 1); + assert.equal(calledWith[0], error); + done(); + }); + }); + + it('error handler with pre error', function(done) { + var s = new Schema({ name: String }); + + var calledWith = []; + s.pre('aggregate', function(next) { + next(new Error('woops')); + }); + s.post('aggregate', function(error, res, next) { + calledWith.push(error); + next(); + }); + + var M = db.model('gh5251_error', s); + + M.aggregate([{ $match: { name: 'test' } }], function(error, res) { + assert.ok(error); + assert.equal(error.message, 'woops'); + assert.equal(res, null); + assert.equal(calledWith.length, 1); + assert.equal(calledWith[0], error); + done(); + }); + }); + + it('with agg cursor', function(done) { + var s = new Schema({ name: String }); + + var calledPre = 0; + var calledPost = 0; + s.pre('aggregate', function(next) { + ++calledPre; + next(); + }); + s.post('aggregate', function(res, next) { + ++calledPost; + next(); + }); + + var M = db.model('gh5251_cursor', s); + + var numDocs = 0; + M. + aggregate([{ $match: { name: 'test' } }]). + cursor({ useMongooseAggCursor: true }). + exec(). + eachAsync(function() { + ++numDocs; + }). + then(function() { + assert.equal(numDocs, 0); + assert.equal(calledPre, 1); + assert.equal(calledPost, 0); + done(); + }); + }); + }); + it('readPref from schema (gh-5522)', function(done) { var schema = new Schema({ name: String }, { read: 'secondary' }); var M = db.model('gh5522', schema); From 46188e60e96176cff579d4fa517735cdbf5da35e Mon Sep 17 00:00:00 2001 From: Valeri Karpov Date: Wed, 18 Oct 2017 09:46:14 -0700 Subject: [PATCH 40/78] test(aggregate): fix brittle test that depends on server-produced error message --- test/aggregate.test.js | 4 ++-- 1 file changed, 2 insertions(+), 2 deletions(-) diff --git a/test/aggregate.test.js b/test/aggregate.test.js index 38a6f06dc26..80e01a5a9ce 100644 --- a/test/aggregate.test.js +++ b/test/aggregate.test.js @@ -922,8 +922,8 @@ describe('aggregate: ', function() { M.aggregate([{ $fakeStage: { name: 'test' } }], function(error, res) { assert.ok(error); - assert.equal(error.message, - 'Unrecognized pipeline stage name: \'$fakeStage\''); + assert.ok(error.message.indexOf('Unrecognized pipeline stage') !== -1, + error.message); assert.equal(res, null); assert.equal(calledWith.length, 1); assert.equal(calledWith[0], error); From aac3bd2edad017a597e9ff0d109e10a8f7bf5369 Mon Sep 17 00:00:00 2001 From: wlingke Date: Thu, 19 Oct 2017 11:43:46 -0700 Subject: [PATCH 41/78] idGetter plugin tests - confirms #5713 has been solved --- package-lock.json | 2 +- test/plugin.idGetter.test.js | 58 ++++++++++++++++++++++++++++++++++++ 2 files changed, 59 insertions(+), 1 deletion(-) create mode 100644 test/plugin.idGetter.test.js diff --git a/package-lock.json b/package-lock.json index 98a55a69610..26e7972d6b0 100644 --- a/package-lock.json +++ b/package-lock.json @@ -1,6 +1,6 @@ { "name": "mongoose", - "version": "4.12.2-pre", + "version": "4.12.4-pre", "lockfileVersion": 1, "requires": true, "dependencies": { diff --git a/test/plugin.idGetter.test.js b/test/plugin.idGetter.test.js new file mode 100644 index 00000000000..875f58d6635 --- /dev/null +++ b/test/plugin.idGetter.test.js @@ -0,0 +1,58 @@ + +/** + * Module dependencies. + */ + +var start = require('./common'), + mongoose = start.mongoose, + assert = require('power-assert'), + Schema = mongoose.Schema; + +describe('id virtual getter', function() { + it('should work as expected with an ObjectId', function(done) { + var db = start(); + + var schema = new Schema({}); + + var S = db.model('Basic', schema); + S.create({}, function(err, s) { + assert.ifError(err); + // Comparing with aliases + assert.equal(s._id.toString(), s.id); + done(); + }); + }); + + it('should be turned off when `id` option is set to false', function(done) { + var db = start(); + + var schema = new Schema({}, {id: false}); + + var S = db.model('NoIdGetter', schema); + S.create({}, function(err, s) { + assert.ifError(err); + + // Comparing with aliases + assert.equal(s.id, undefined); + done(); + }); + }); + + + it('should be turned off when the schema has a set `id` path', function(done) { + var db = start(); + + var schema = new Schema({ + id: String, + }); + + var S = db.model('NoIdGetter', schema); + S.create({ id: 'test'}, function(err, s) { + assert.ifError(err); + + // Comparing with aliases + assert.equal(s.id, 'test'); + done(); + }); + }); +}); From c6fac06295235a6e1cdcac4fb4a7631ce597a2b2 Mon Sep 17 00:00:00 2001 From: wlingke Date: Thu, 19 Oct 2017 11:50:55 -0700 Subject: [PATCH 42/78] clean up comments --- test/plugin.idGetter.test.js | 7 ++++--- 1 file changed, 4 insertions(+), 3 deletions(-) diff --git a/test/plugin.idGetter.test.js b/test/plugin.idGetter.test.js index 875f58d6635..2ed2df03cba 100644 --- a/test/plugin.idGetter.test.js +++ b/test/plugin.idGetter.test.js @@ -17,7 +17,8 @@ describe('id virtual getter', function() { var S = db.model('Basic', schema); S.create({}, function(err, s) { assert.ifError(err); - // Comparing with aliases + + // Comparing with virtual getter assert.equal(s._id.toString(), s.id); done(); }); @@ -32,7 +33,7 @@ describe('id virtual getter', function() { S.create({}, function(err, s) { assert.ifError(err); - // Comparing with aliases + // Comparing with virtual getter assert.equal(s.id, undefined); done(); }); @@ -50,7 +51,7 @@ describe('id virtual getter', function() { S.create({ id: 'test'}, function(err, s) { assert.ifError(err); - // Comparing with aliases + // Comparing with expected value assert.equal(s.id, 'test'); done(); }); From a38203c4504f6237d9ef1c62f613aaf7816d4a66 Mon Sep 17 00:00:00 2001 From: Valeri Karpov Date: Thu, 19 Oct 2017 12:41:26 -0700 Subject: [PATCH 43/78] style: fix lint --- test/plugin.idGetter.test.js | 2 +- 1 file changed, 1 insertion(+), 1 deletion(-) diff --git a/test/plugin.idGetter.test.js b/test/plugin.idGetter.test.js index 2ed2df03cba..f68d930db14 100644 --- a/test/plugin.idGetter.test.js +++ b/test/plugin.idGetter.test.js @@ -44,7 +44,7 @@ describe('id virtual getter', function() { var db = start(); var schema = new Schema({ - id: String, + id: String }); var S = db.model('NoIdGetter', schema); From 90f4782cb332727a551c9111d1c5c57de11dd2e1 Mon Sep 17 00:00:00 2001 From: Valeri Karpov Date: Thu, 19 Oct 2017 14:11:31 -0700 Subject: [PATCH 44/78] docs(schema): add notes about runSettersOnQuery to schema setters Fix #5705 --- lib/schema/string.js | 24 +++++++++++++++++++++--- lib/schematype.js | 28 ++++++++++++++++++++-------- 2 files changed, 41 insertions(+), 11 deletions(-) diff --git a/lib/schema/string.js b/lib/schema/string.js index b9bf296e88d..8b4a1842728 100644 --- a/lib/schema/string.js +++ b/lib/schema/string.js @@ -116,7 +116,7 @@ SchemaString.prototype.enum = function() { }; /** - * Adds a lowercase setter. + * Adds a lowercase [setter](http://mongoosejs.com/docs/api.html#schematype_SchemaType-set). * * ####Example: * @@ -125,6 +125,12 @@ SchemaString.prototype.enum = function() { * var m = new M({ email: 'SomeEmail@example.COM' }); * console.log(m.email) // someemail@example.com * + * NOTE: Setters do not run on queries by default. Use the `runSettersOnQuery` option: + * + * // Must use `runSettersOnQuery` as shown below, otherwise `email` will + * // **not** be lowercased. + * M.updateOne({}, { $set: { email: 'SomeEmail@example.COM' } }, { runSettersOnQuery: true }); + * * @api public * @return {SchemaType} this */ @@ -145,7 +151,7 @@ SchemaString.prototype.lowercase = function(shouldApply) { }; /** - * Adds an uppercase setter. + * Adds an uppercase [setter](http://mongoosejs.com/docs/api.html#schematype_SchemaType-set). * * ####Example: * @@ -154,6 +160,12 @@ SchemaString.prototype.lowercase = function(shouldApply) { * var m = new M({ caps: 'an example' }); * console.log(m.caps) // AN EXAMPLE * + * NOTE: Setters do not run on queries by default. Use the `runSettersOnQuery` option: + * + * // Must use `runSettersOnQuery` as shown below, otherwise `email` will + * // **not** be lowercased. + * M.updateOne({}, { $set: { email: 'SomeEmail@example.COM' } }, { runSettersOnQuery: true }); + * * @api public * @return {SchemaType} this */ @@ -174,7 +186,7 @@ SchemaString.prototype.uppercase = function(shouldApply) { }; /** - * Adds a trim setter. + * Adds a trim [setter](http://mongoosejs.com/docs/api.html#schematype_SchemaType-set). * * The string value will be trimmed when set. * @@ -187,6 +199,12 @@ SchemaString.prototype.uppercase = function(shouldApply) { * var m = new M({ name: string }) * console.log(m.name.length) // 9 * + * NOTE: Setters do not run on queries by default. Use the `runSettersOnQuery` option: + * + * // Must use `runSettersOnQuery` as shown below, otherwise `email` will + * // **not** be lowercased. + * M.updateOne({}, { $set: { email: 'SomeEmail@example.COM' } }, { runSettersOnQuery: true }); + * * @api public * @return {SchemaType} this */ diff --git a/lib/schematype.js b/lib/schematype.js index 4be2d132fa5..d841a5b5ccd 100644 --- a/lib/schematype.js +++ b/lib/schematype.js @@ -244,25 +244,37 @@ SchemaType.prototype.sparse = function(bool) { * * You can set up email lower case normalization easily via a Mongoose setter. * - * function toLower (v) { + * function toLower(v) { * return v.toLowerCase(); * } * * var UserSchema = new Schema({ * email: { type: String, set: toLower } - * }) + * }); * - * var User = db.model('User', UserSchema) + * var User = db.model('User', UserSchema); * - * var user = new User({email: 'AVENUE@Q.COM'}) + * var user = new User({email: 'AVENUE@Q.COM'}); * console.log(user.email); // 'avenue@q.com' * * // or - * var user = new User - * user.email = 'Avenue@Q.com' - * console.log(user.email) // 'avenue@q.com' + * var user = new User(); + * user.email = 'Avenue@Q.com'; + * console.log(user.email); // 'avenue@q.com' + * + * As you can see above, setters allow you to transform the data before it stored in MongoDB. * - * As you can see above, setters allow you to transform the data before it gets to the raw mongodb document and is set as a value on an actual key. + * NOTE: setters by default do **not** run on queries by default. + * + * // Will **not** run the `toLower()` setter by default. + * User.updateOne({ _id: _id }, { $set: { email: 'AVENUE@Q.COM' } }); + * + * Use the `runSettersOnQuery` option to opt-in to running setters on `User.update()`: + * + * // Turn on `runSettersOnQuery` to run the setters from your schema. + * User.updateOne({ _id: _id }, { $set: { email: 'AVENUE@Q.COM' } }, { + * runSettersOnQuery: true + * }); * * _NOTE: we could have also just used the built-in `lowercase: true` SchemaType option instead of defining our own function._ * From d8e023ef39433ede9a35b297f54f9d9290575180 Mon Sep 17 00:00:00 2001 From: Valeri Karpov Date: Thu, 19 Oct 2017 14:56:23 -0700 Subject: [PATCH 45/78] docs(middleware): add docs coverage for aggregation middleware Re: #5251 --- docs/middleware.jade | 16 +++++++++++----- 1 file changed, 11 insertions(+), 5 deletions(-) diff --git a/docs/middleware.jade b/docs/middleware.jade index 6820563484a..a4a4779ae5b 100644 --- a/docs/middleware.jade +++ b/docs/middleware.jade @@ -6,10 +6,10 @@ block content Middleware (also called pre and post *hooks*) are functions which are passed control during execution of asynchronous functions. Middleware is specified on the schema level and is useful for writing [plugins](./plugins.html). - Mongoose 4.x has 3 types - of middleware: document middleware, model middleware, and query middleware. + Mongoose 4.x has 4 types + of middleware: document middleware, model middleware, aggregate middleware, and query middleware. Document middleware is supported for the following document functions. - In document middleware functions, `this` refers to the document. + In document middleware functions, `this` refers to the document. * [init](./api.html#document_Document-init) * [validate](./api.html#document_Document-validate) @@ -25,10 +25,16 @@ block content * [findOneAndRemove](./api.html#query_Query-findOneAndRemove) * [findOneAndUpdate](./api.html#query_Query-findOneAndUpdate) * [update](./api.html#query_Query-update) - + + Aggregate middleware is for `MyModel.aggregate()`. Aggregate middleware + executes when you call `exec()` on an aggregate object. + In aggregate middleware, `this` refers to the [aggregation object](http://mongoosejs.com/docs/api.html#model_Model.aggregate). + + * [aggregate](http://mongoosejs.com/docs/api.html#model_Model.aggregate) + Model middleware is supported for the following model functions. In model middleware functions, `this` refers to the model. - + * [insertMany](./api.html#model_Model.insertMany) All middleware types support pre and post hooks. From e3473b51d03ef50b5730810cebe3acff684569b0 Mon Sep 17 00:00:00 2001 From: Valeri Karpov Date: Thu, 19 Oct 2017 15:27:22 -0700 Subject: [PATCH 46/78] feat(connection): add connection-level bufferCommands Fix #5720 --- lib/connection.js | 6 ++++++ lib/model.js | 23 +++++++++++++++++++++-- lib/schema.js | 4 ++++ test/connection.test.js | 21 +++++++++++++++++++++ 4 files changed, 52 insertions(+), 2 deletions(-) diff --git a/lib/connection.js b/lib/connection.js index b71b2bdbcc5..c19f3e2d0ae 100644 --- a/lib/connection.js +++ b/lib/connection.js @@ -806,6 +806,12 @@ Connection.prototype.openUri = function(uri, options, callback) { this.user = options.auth.user; this.pass = options.auth.password; } + + if (options.bufferCommands != null) { + options.bufferMaxEntries = 0; + this.config.bufferCommands = options.bufferCommands; + delete options.bufferCommands; + } } this._connectionOptions = options; diff --git a/lib/model.js b/lib/model.js index 64a078aa917..8f87b4ae33b 100644 --- a/lib/model.js +++ b/lib/model.js @@ -3812,8 +3812,17 @@ Model.compile = function compile(name, schema, collectionName, connection, base) model.prototype.$__setSchema(schema); + var _userProvidedOptions = schema._userProvidedOptions || {}; + var bufferCommands = true; + if (connection.config.bufferCommands != null) { + bufferCommands = connection.config.bufferCommands; + } + if (_userProvidedOptions.bufferCommands != null) { + bufferCommands = _userProvidedOptions.bufferCommands; + } + var collectionOptions = { - bufferCommands: schema.options.bufferCommands, + bufferCommands: bufferCommands, capped: schema.options.capped }; @@ -3908,14 +3917,24 @@ Model.__subclass = function subclass(conn, schema, collection) { : _this.prototype.schema; var options = s.options || {}; + var _userProvidedOptions = s._userProvidedOptions || {}; if (!collection) { collection = _this.prototype.schema.get('collection') || utils.toCollectionName(_this.modelName, options); } + var bufferCommands = true; + if (s) { + if (conn.config.bufferCommands != null) { + bufferCommands = conn.config.bufferCommands; + } + if (_userProvidedOptions.bufferCommands != null) { + bufferCommands = _userProvidedOptions.bufferCommands; + } + } var collectionOptions = { - bufferCommands: s ? options.bufferCommands : true, + bufferCommands: bufferCommands, capped: s && options.capped }; diff --git a/lib/schema.js b/lib/schema.js index 49160f41d71..0bf6694b8b1 100644 --- a/lib/schema.js +++ b/lib/schema.js @@ -326,6 +326,10 @@ Schema.prototype.defaultOptions = function(options) { options.versionKey = false; } + this._userProvidedOptions = utils.clone(options, { + retainKeyOrder: true + }); + options = utils.options({ strict: true, bufferCommands: true, diff --git a/test/connection.test.js b/test/connection.test.js index be23ba57240..7193d7f9c67 100644 --- a/test/connection.test.js +++ b/test/connection.test.js @@ -1378,6 +1378,27 @@ describe('connections:', function() { }); }); + it('bufferCommands (gh-5720)', function(done) { + var opts = { useMongoClient: true, bufferCommands: false }; + var db = mongoose.createConnection('mongodb://localhost:27017/test', opts); + + var M = db.model('gh5720', new Schema({})); + assert.ok(!M.collection.buffer); + db.close(); + + opts = { useMongoClient: true, bufferCommands: true }; + db = mongoose.createConnection('mongodb://localhost:27017/test', opts); + M = db.model('gh5720', new Schema({}, { bufferCommands: false })); + assert.ok(!M.collection.buffer); + db.close(); + + opts = { useMongoClient: true, bufferCommands: true }; + db = mongoose.createConnection('mongodb://localhost:27017/test', opts); + M = db.model('gh5720', new Schema({})); + assert.ok(M.collection.buffer); + db.close(done); + }); + describe('connecting to multiple mongos nodes (gh-1037)', function() { var mongos = process.env.MONGOOSE_MULTI_MONGOS_TEST_URI; if (!mongos) { From 8e7dfb00787bf406c588c259a6b02896e1e677d9 Mon Sep 17 00:00:00 2001 From: Valeri Karpov Date: Fri, 20 Oct 2017 12:13:59 -0700 Subject: [PATCH 47/78] test(connection): repro #5714 --- test/connection.test.js | 18 ++++++++++++++++++ 1 file changed, 18 insertions(+) diff --git a/test/connection.test.js b/test/connection.test.js index 3d97056ba27..c5fa2eebbf3 100644 --- a/test/connection.test.js +++ b/test/connection.test.js @@ -3,6 +3,7 @@ */ var Promise = require('bluebird'); +var Q = require('q'); var assert = require('power-assert'); var muri = require('muri'); var random = require('../lib/utils').random; @@ -75,6 +76,23 @@ describe('connections:', function() { }).catch(done); }); + it('resolving with q (gh-5714)', function(done) { + var bootMongo = Q.defer(); + + var conn = mongoose.createConnection('mongodb://localhost:27017/mongoosetest', { + useMongoClient: true + }); + + conn.on('connected', function() { + bootMongo.resolve(this); + }); + + bootMongo.promise.then(function(_conn) { + assert.equal(_conn, conn); + done(); + }).catch(done); + }); + describe('connection events', function() { beforeEach(function() { this.timeout(10000); From 2445ca4116c41026c1ccc479a9b441baadbe15b3 Mon Sep 17 00:00:00 2001 From: Valeri Karpov Date: Fri, 20 Oct 2017 12:14:45 -0700 Subject: [PATCH 48/78] fix(connection): ensure connection promise helpers are removed before emitting 'connected' Fix #5714 --- lib/connection.js | 5 +++-- package-lock.json | 6 +++--- package.json | 2 +- 3 files changed, 7 insertions(+), 6 deletions(-) diff --git a/lib/connection.js b/lib/connection.js index 8919115629c..fc47b2c8d90 100644 --- a/lib/connection.js +++ b/lib/connection.js @@ -813,6 +813,9 @@ Connection.prototype.openUri = function(uri, options, callback) { _this.emit('timeout'); }); + delete _this.then; + delete _this.catch; + _this.db = db; _this.readyState = STATES.connected; @@ -823,8 +826,6 @@ Connection.prototype.openUri = function(uri, options, callback) { } callback && callback(null, _this); - delete _this.then; - delete _this.catch; resolve(_this); _this.emit('open'); }); diff --git a/package-lock.json b/package-lock.json index 26e7972d6b0..2a439c4b0b0 100644 --- a/package-lock.json +++ b/package-lock.json @@ -2786,9 +2786,9 @@ } }, "q": { - "version": "1.4.1", - "resolved": "https://registry.npmjs.org/q/-/q-1.4.1.tgz", - "integrity": "sha1-VXBbzZPF82c1MMLCy8DCs63cKG4=", + "version": "1.5.1", + "resolved": "https://registry.npmjs.org/q/-/q-1.5.1.tgz", + "integrity": "sha1-fjL3W0E4EpHQRhHxvxQQmsAGUdc=", "dev": true }, "qs": { diff --git a/package.json b/package.json index 7ee3ffe6b3e..cfee0371a68 100644 --- a/package.json +++ b/package.json @@ -52,7 +52,7 @@ "node-static": "0.7.7", "nsp": "~2.8.1", "power-assert": "1.4.1", - "q": "1.4.1", + "q": "1.5.1", "tbd": "0.6.4", "uglify-js": "2.7.0", "uuid": "2.0.3", From e071b575d9a791282138c54837e2a8f1d1a44c9a Mon Sep 17 00:00:00 2001 From: Valeri Karpov Date: Fri, 20 Oct 2017 18:42:34 -0700 Subject: [PATCH 49/78] feat(populate): support setting localField and foreignField to functions Fix #5704 --- lib/model.js | 21 ++++++++++++--- test/model.populate.test.js | 53 +++++++++++++++++++++++++++++++++++++ 2 files changed, 71 insertions(+), 3 deletions(-) diff --git a/lib/model.js b/lib/model.js index 8f87b4ae33b..8f71a4cb412 100644 --- a/lib/model.js +++ b/lib/model.js @@ -3547,9 +3547,18 @@ function getModelsMapForPopulate(model, docs, options) { } virtual = model.schema._getVirtual(options.path); - var localField = virtual && virtual.options ? - (virtual.$nestedSchemaPath ? virtual.$nestedSchemaPath + '.' : '') + virtual.options.localField : - options.path; + var localField; + if (virtual && virtual.options) { + var virtualPrefix = virtual.$nestedSchemaPath ? + virtual.$nestedSchemaPath + '.' : ''; + if (typeof virtual.options.localField === 'function') { + localField = virtualPrefix + virtual.options.localField.call(doc); + } else { + localField = virtualPrefix + virtual.options.localField; + } + } else { + localField = options.path; + } var foreignField = virtual && virtual.options ? virtual.options.foreignField : '_id'; @@ -3564,6 +3573,12 @@ function getModelsMapForPopulate(model, docs, options) { } options.isVirtual = isVirtual; + if (typeof localField === 'function') { + localField = localField.call(doc); + } + if (typeof foreignField === 'function') { + foreignField = foreignField.call(doc); + } var ret = convertTo_id(utils.getValue(localField, doc)); var id = String(utils.getValue(foreignField, doc)); options._docs[id] = Array.isArray(ret) ? ret.slice() : ret; diff --git a/test/model.populate.test.js b/test/model.populate.test.js index b04c788258a..8bc1aa99cd0 100644 --- a/test/model.populate.test.js +++ b/test/model.populate.test.js @@ -4448,6 +4448,59 @@ describe('model: populate:', function() { catch(done); }); + it('with functions for localField and foreignField (gh-5704)', function(done) { + var ASchema = new Schema({ + name: String + }); + + var BSchema = new Schema({ + name: String, + localField: String, + firstId: ObjectId, + secondId: ObjectId + }, { + toObject: { virtuals: true }, + toJSON: { virtuals: true } + }); + + BSchema.virtual('a', { + ref: 'gh5704', + localField: function() { return this.localField; }, + foreignField: function() { return '_id'; }, + justOne: true + }); + + var A = db.model('gh5704', ASchema); + var B = db.model('gh5704_0', BSchema); + + A.create([{ name: 'test1' }, { name: 'test2' }]). + then(function(arr) { + return B.create([ + { + name: 'b1', + localField: 'firstId', + firstId: arr[0]._id, + secondId: arr[1]._id + }, + { + name: 'b2', + localField: 'secondId', + firstId: arr[0]._id, + secondId: arr[1]._id + } + ]); + }). + then(function(b) { + return B.find().populate('a').sort([['name', 1]]).exec(); + }). + then(function(bs) { + assert.equal(bs[0].a.name, 'test1'); + assert.equal(bs[1].a.name, 'test2'); + done(); + }). + catch(done); + }); + it('with no results (gh-4284)', function(done) { var PersonSchema = new Schema({ name: String, From 3416fcc1233ebc70e428a146e6c46209a4886be7 Mon Sep 17 00:00:00 2001 From: Valeri Karpov Date: Fri, 20 Oct 2017 18:48:59 -0700 Subject: [PATCH 50/78] style: fix lint --- test/model.populate.test.js | 2 +- 1 file changed, 1 insertion(+), 1 deletion(-) diff --git a/test/model.populate.test.js b/test/model.populate.test.js index 8bc1aa99cd0..0f434322b30 100644 --- a/test/model.populate.test.js +++ b/test/model.populate.test.js @@ -4490,7 +4490,7 @@ describe('model: populate:', function() { } ]); }). - then(function(b) { + then(function() { return B.find().populate('a').sort([['name', 1]]).exec(); }). then(function(bs) { From 30394cde14e6a47d00897febd0cacb3165964e2c Mon Sep 17 00:00:00 2001 From: Valeri Karpov Date: Sat, 21 Oct 2017 16:14:29 -0700 Subject: [PATCH 51/78] chore: release 4.12.4 --- History.md | 8 ++++++++ package.json | 2 +- 2 files changed, 9 insertions(+), 1 deletion(-) diff --git a/History.md b/History.md index ffff568ac93..5e8424d3c8a 100644 --- a/History.md +++ b/History.md @@ -1,3 +1,11 @@ +4.12.4 / 2017-10-21 +=================== + * test(plugins): add coverage for idGetter with id as a schema property #5713 [wlingke](https://github.com/wlingke) + * fix(model): avoid copying recursive $$context object when creating discriminator after querying #5721 + * fix(connection): ensure connection promise helpers are removed before emitting 'connected' #5714 + * docs(schema): add notes about runSettersOnQuery to schema setters #5705 + * fix(collection): ensure queued operations run on the next tick #5562 + 4.12.3 / 2017-10-16 =================== * fix(connection): emit 'reconnect' event as well as 'reconnected' for consistency with driver #5719 diff --git a/package.json b/package.json index cfee0371a68..63e026c197c 100644 --- a/package.json +++ b/package.json @@ -1,7 +1,7 @@ { "name": "mongoose", "description": "Mongoose MongoDB ODM", - "version": "4.12.4-pre", + "version": "4.12.4", "author": "Guillermo Rauch ", "keywords": [ "mongodb", From cf8ef5f3f5856cc3c15e6120b87c815b44927ea6 Mon Sep 17 00:00:00 2001 From: Valeri Karpov Date: Sat, 21 Oct 2017 16:23:28 -0700 Subject: [PATCH 52/78] chore: now working on 4.12.5 --- package.json | 2 +- 1 file changed, 1 insertion(+), 1 deletion(-) diff --git a/package.json b/package.json index 63e026c197c..6e84144fbae 100644 --- a/package.json +++ b/package.json @@ -1,7 +1,7 @@ { "name": "mongoose", "description": "Mongoose MongoDB ODM", - "version": "4.12.4", + "version": "4.12.5-pre", "author": "Guillermo Rauch ", "keywords": [ "mongodb", From e527264bfb81991ba04de845ad920d34f393ac57 Mon Sep 17 00:00:00 2001 From: Valeri Karpov Date: Sat, 21 Oct 2017 16:53:05 -0700 Subject: [PATCH 53/78] feat(populate): allow passing a function to virtual ref Fix #5602 --- lib/model.js | 17 +++++++++----- package-lock.json | 5 +++++ package.json | 1 + test/model.populate.test.js | 45 +++++++++++++++++++++++++++++++++++++ 4 files changed, 62 insertions(+), 6 deletions(-) diff --git a/lib/model.js b/lib/model.js index 8f71a4cb412..a38b9f10dc6 100644 --- a/lib/model.js +++ b/lib/model.js @@ -20,6 +20,7 @@ var cast = require('./cast'); var castUpdate = require('./services/query/castUpdate'); var discriminator = require('./services/model/discriminator'); var isPathSelectedInclusive = require('./services/projection/isPathSelectedInclusive'); +var get = require('lodash.get'); var mpath = require('mpath'); var parallel = require('async/parallel'); var parallelLimit = require('async/parallelLimit'); @@ -3523,10 +3524,14 @@ function getModelsMapForPopulate(model, docs, options) { } var virtual = modelForCurrentDoc.schema._getVirtual(options.path); - if (schemaForCurrentDoc && schemaForCurrentDoc.options && schemaForCurrentDoc.options.ref) { - modelNames = [schemaForCurrentDoc.options.ref]; - } else if (virtual && virtual.options && virtual.options.ref) { - modelNames = [virtual && virtual.options && virtual.options.ref]; + var ref; + if ((ref = get(schemaForCurrentDoc, 'options.ref')) != null) { + modelNames = [ref]; + } else if ((ref = get(virtual, 'options.ref')) != null) { + if (typeof ref === 'function') { + ref = ref.call(doc, doc); + } + modelNames = [ref]; isVirtual = true; } else { // We may have a discriminator, in which case we don't want to @@ -3552,7 +3557,7 @@ function getModelsMapForPopulate(model, docs, options) { var virtualPrefix = virtual.$nestedSchemaPath ? virtual.$nestedSchemaPath + '.' : ''; if (typeof virtual.options.localField === 'function') { - localField = virtualPrefix + virtual.options.localField.call(doc); + localField = virtualPrefix + virtual.options.localField.call(doc, doc); } else { localField = virtualPrefix + virtual.options.localField; } @@ -3574,7 +3579,7 @@ function getModelsMapForPopulate(model, docs, options) { options.isVirtual = isVirtual; if (typeof localField === 'function') { - localField = localField.call(doc); + localField = localField.call(doc, doc); } if (typeof foreignField === 'function') { foreignField = foreignField.call(doc); diff --git a/package-lock.json b/package-lock.json index 078ad99841c..1947b7c2ab1 100644 --- a/package-lock.json +++ b/package-lock.json @@ -1867,6 +1867,11 @@ "lodash._isiterateecall": "3.0.9" } }, + "lodash.get": { + "version": "4.4.2", + "resolved": "https://registry.npmjs.org/lodash.get/-/lodash.get-4.4.2.tgz", + "integrity": "sha1-LRd/ZS+jHpObRDjVNBSZ36OCXpk=" + }, "lodash.isarguments": { "version": "3.1.0", "resolved": "https://registry.npmjs.org/lodash.isarguments/-/lodash.isarguments-3.1.0.tgz", diff --git a/package.json b/package.json index 51c5f990270..bc669264fbd 100644 --- a/package.json +++ b/package.json @@ -23,6 +23,7 @@ "bson": "~1.0.4", "hooks-fixed": "2.0.0", "kareem": "1.5.0", + "lodash.get": "4.4.2", "mongodb": "2.2.33", "mpath": "0.3.0", "mpromise": "0.5.5", diff --git a/test/model.populate.test.js b/test/model.populate.test.js index 0f434322b30..485c9b50508 100644 --- a/test/model.populate.test.js +++ b/test/model.populate.test.js @@ -4501,6 +4501,51 @@ describe('model: populate:', function() { catch(done); }); + it('with functions for ref (gh-5602)', function(done) { + var ASchema = new Schema({ + name: String + }); + + var BSchema = new Schema({ + referencedModel: String, + aId: ObjectId + }); + + BSchema.virtual('a', { + ref: function() { return this.referencedModel; }, + localField: 'aId', + foreignField: '_id', + justOne: true + }); + + var A1 = db.model('gh5602_1', ASchema); + var A2 = db.model('gh5602_2', ASchema); + var B = db.model('gh5602_0', BSchema); + + A1.create({ name: 'a1' }). + then(function(a1) { + return A2.create({ name: 'a2' }).then(function(res) { + return [a1].concat(res); + }); + }). + then(function(as) { + return B.create([ + { name: 'test1', referencedModel: 'gh5602_1', aId: as[0]._id }, + { name: 'test2', referencedModel: 'gh5602_2', aId: as[1]._id } + ]); + }). + then(function() { + return B.find().populate('a').sort([['name', 1]]); + }). + then(function(bs) { + assert.equal(bs.length, 2); + assert.equal(bs[0].a.name, 'a1'); + assert.equal(bs[1].a.name, 'a2'); + done(); + }). + catch(done); + }); + it('with no results (gh-4284)', function(done) { var PersonSchema = new Schema({ name: String, From 245cbbc87d78eaa69d097c7f51ee0622dec4e815 Mon Sep 17 00:00:00 2001 From: Valeri Karpov Date: Sun, 22 Oct 2017 11:12:18 -0700 Subject: [PATCH 54/78] feat(query): add multipleCastError option for aggregating cast errors when casting update Fix #5609 --- lib/query.js | 4 ++ lib/services/query/castUpdate.js | 72 ++++++++++++++++++++++------- test/model.findOneAndUpdate.test.js | 20 ++++++++ 3 files changed, 79 insertions(+), 17 deletions(-) diff --git a/lib/query.js b/lib/query.js index 28e8a648586..f885eee7daf 100644 --- a/lib/query.js +++ b/lib/query.js @@ -1981,6 +1981,7 @@ function prepareDiscriminatorCriteria(query) { * @param {Object} [options] * @param {Boolean} [options.passRawResult] if true, passes the [raw result from the MongoDB driver as the third callback parameter](http://mongodb.github.io/node-mongodb-native/2.0/api/Collection.html#findAndModify) * @param {Boolean|String} [options.strict] overwrites the schema's [strict mode option](http://mongoosejs.com/docs/guide.html#strict) + * @param {Boolean} [options.multipleCastError] by default, mongoose only returns the first error that occurred in casting the query. Turn on this option to aggregate all the cast errors. * @param {Function} [callback] optional params are (error, doc), _unless_ `passRawResult` is used, in which case params are (error, doc, writeOpResult) * @see mongodb http://www.mongodb.org/display/DOCS/findAndModify+Command * @see writeOpResult http://mongodb.github.io/node-mongodb-native/2.2/api/Collection.html#~WriteOpResult @@ -2676,6 +2677,7 @@ Query.prototype._replaceOne = function(callback) { * @param {Object} [criteria] * @param {Object} [doc] the update command * @param {Object} [options] + * @param {Boolean} [options.multipleCastError] by default, mongoose only returns the first error that occurred in casting the query. Turn on this option to aggregate all the cast errors. * @param {Function} [callback] optional, params are (error, writeOpResult) * @return {Query} this * @see Model.update #model_Model.update @@ -2727,6 +2729,7 @@ Query.prototype.update = function(conditions, doc, options, callback) { * @param {Object} [criteria] * @param {Object} [doc] the update command * @param {Object} [options] + @param {Boolean} [options.multipleCastError] by default, mongoose only returns the first error that occurred in casting the query. Turn on this option to aggregate all the cast errors. * @param {Function} [callback] optional params are (error, writeOpResult) * @return {Query} this * @see Model.update #model_Model.update @@ -2777,6 +2780,7 @@ Query.prototype.updateMany = function(conditions, doc, options, callback) { * @param {Object} [criteria] * @param {Object} [doc] the update command * @param {Object} [options] + @param {Boolean} [options.multipleCastError] by default, mongoose only returns the first error that occurred in casting the query. Turn on this option to aggregate all the cast errors. * @param {Function} [callback] params are (error, writeOpResult) * @return {Query} this * @see Model.update #model_Model.update diff --git a/lib/services/query/castUpdate.js b/lib/services/query/castUpdate.js index 9417e71be18..52b11f4f82c 100644 --- a/lib/services/query/castUpdate.js +++ b/lib/services/query/castUpdate.js @@ -1,6 +1,7 @@ 'use strict'; var StrictModeError = require('../../error/strict'); +var ValidationError = require('../../error/validation'); var utils = require('../../utils'); /*! @@ -102,13 +103,16 @@ module.exports = function castUpdate(schema, obj, options, context) { */ function walkUpdatePath(schema, obj, op, strict, context, pref) { - var prefix = pref ? pref + '.' : '', - keys = Object.keys(obj), - i = keys.length, - hasKeys = false, - schematype, - key, - val; + var prefix = pref ? pref + '.' : ''; + var keys = Object.keys(obj); + var i = keys.length; + var hasKeys = false; + var schematype; + var key; + var val; + + var hasError = false; + var aggregatedError = new ValidationError(); var useNestedStrict = schema.options.useNestedStrict; @@ -124,9 +128,14 @@ function walkUpdatePath(schema, obj, op, strict, context, pref) { hasKeys = true; if ('$each' in val) { - obj[key] = { - $each: castUpdateVal(schematype, val.$each, op, context) - }; + try { + obj[key] = { + $each: castUpdateVal(schematype, val.$each, op, context) + }; + } catch (error) { + hasError = true; + _handleCastError(error, context, key, aggregatedError); + } if (val.$slice != null) { obj[key].$slice = val.$slice | 0; @@ -140,14 +149,22 @@ function walkUpdatePath(schema, obj, op, strict, context, pref) { obj[key].$position = val.$position; } } else { - obj[key] = castUpdateVal(schematype, val, op, context); + try { + obj[key] = castUpdateVal(schematype, val, op, context); + } catch (error) { + hasError = true; + _handleCastError(error, context, key, aggregatedError); + } } - } else if (op === '$currentDate') { + } else if ((op === '$currentDate') || (op in castOps && schematype)) { // $currentDate can take an object - obj[key] = castUpdateVal(schematype, val, op, context); - hasKeys = true; - } else if (op in castOps && schematype) { - obj[key] = castUpdateVal(schematype, val, op, context); + try { + obj[key] = castUpdateVal(schematype, val, op, context); + } catch (error) { + hasError = true; + _handleCastError(error, context, key, aggregatedError); + } + hasKeys = true; } else { var pathToCheck = (prefix + key); @@ -208,13 +225,34 @@ function walkUpdatePath(schema, obj, op, strict, context, pref) { } hasKeys = true; - obj[key] = castUpdateVal(schematype, val, op, key, context); + try { + obj[key] = castUpdateVal(schematype, val, op, key, context); + } catch (error) { + hasError = true; + _handleCastError(error, context, key, aggregatedError); + } } } } + + if (hasError) { + throw aggregatedError; + } + return hasKeys; } +/*! + * ignore + */ + +function _handleCastError(error, query, key, aggregatedError) { + if (!query.options.multipleCastError) { + throw error; + } + aggregatedError.addError(key, error); +} + /*! * These operators should be cast to numbers instead * of their path schema type. diff --git a/test/model.findOneAndUpdate.test.js b/test/model.findOneAndUpdate.test.js index 23c2f3cb4d0..c1aadd4421e 100644 --- a/test/model.findOneAndUpdate.test.js +++ b/test/model.findOneAndUpdate.test.js @@ -1910,6 +1910,26 @@ describe('model: findOneAndUpdate:', function() { }); }); + it('multi cast error (gh-5609)', function(done) { + var schema = new mongoose.Schema({ + num1: Number, + num2: Number + }); + + var Model = db.model('gh5609', schema); + + var opts = { multipleCastError: true }; + Model.findOneAndUpdate({}, { num1: 'fail', num2: 'fail' }, opts, function(error) { + assert.ok(error); + assert.equal(error.name, 'ValidationError'); + assert.ok(error.errors['num1']); + assert.equal(error.errors['num1'].name, 'CastError'); + assert.ok(error.errors['num2']); + assert.equal(error.errors['num2'].name, 'CastError'); + done(); + }); + }); + it('update validators with pushing null (gh-5710)', function(done) { var schema = new mongoose.Schema({ arr: [String] From 5d9c8581e23cc8ec753407078489f3e7c245d734 Mon Sep 17 00:00:00 2001 From: Valeri Karpov Date: Mon, 23 Oct 2017 17:57:02 -0700 Subject: [PATCH 55/78] fix(model): throw sane error when customer calls `mongoose.Model()` over `mongoose.model()` Fix #2005 --- lib/model.js | 5 +++++ 1 file changed, 5 insertions(+) diff --git a/lib/model.js b/lib/model.js index 64a078aa917..00d6d2ff7c3 100644 --- a/lib/model.js +++ b/lib/model.js @@ -46,6 +46,11 @@ var VERSION_WHERE = 1, */ function Model(doc, fields, skipId) { + if (fields instanceof Schema) { + throw new TypeError('2nd argument to `Model` must be a POJO or string, ' + + '**not** a schema. Make sure you\'re calling `mongoose.model()`, not ' + + '`mongoose.Model()`.'); + } Document.call(this, doc, fields, skipId, true); } From 174ad6c63174971546cb68161990fddbf977fd74 Mon Sep 17 00:00:00 2001 From: wlingke Date: Mon, 23 Oct 2017 19:52:57 -0700 Subject: [PATCH 56/78] Fixes #5706. Separately also adds length check for ObjectId hex string checker. --- lib/schema/documentarray.js | 3 +++ lib/schema/embedded.js | 4 ++++ lib/schema/objectid.js | 2 +- 3 files changed, 8 insertions(+), 1 deletion(-) diff --git a/lib/schema/documentarray.js b/lib/schema/documentarray.js index 97ee114262c..f27542f22e9 100644 --- a/lib/schema/documentarray.js +++ b/lib/schema/documentarray.js @@ -11,6 +11,7 @@ var EventEmitter = require('events').EventEmitter; var MongooseDocumentArray = require('../types/documentarray'); var SchemaType = require('../schematype'); var Subdocument = require('../types/embedded'); +var applyHooks = require('../services/model/applyHooks'); var discriminator = require('../services/model/discriminator'); var util = require('util'); var utils = require('../utils'); @@ -119,6 +120,8 @@ DocumentArray.prototype.discriminator = function(name, schema) { this.casterConstructor.discriminators[name] = EmbeddedDocument; + applyHooks(EmbeddedDocument, schema); + return this.casterConstructor.discriminators[name]; }; diff --git a/lib/schema/embedded.js b/lib/schema/embedded.js index e9705626d59..7697caf6630 100644 --- a/lib/schema/embedded.js +++ b/lib/schema/embedded.js @@ -8,6 +8,7 @@ var $exists = require('./operators/exists'); var EventEmitter = require('events').EventEmitter; var SchemaType = require('../schematype'); var Subdocument = require('../types/subdocument'); +var applyHooks = require('../services/model/applyHooks'); var castToNumber = require('./operators/helpers').castToNumber; var discriminator = require('../services/model/discriminator'); var geospatial = require('./operators/geospatial'); @@ -253,5 +254,8 @@ Embedded.prototype.discriminator = function(name, schema) { discriminator(this.caster, name, schema); this.caster.discriminators[name] = _createConstructor(schema); + + applyHooks(this.caster.discriminators[name], schema); + return this.caster.discriminators[name]; }; diff --git a/lib/schema/objectid.js b/lib/schema/objectid.js index c75d82dfbb5..83c0d1ab510 100644 --- a/lib/schema/objectid.js +++ b/lib/schema/objectid.js @@ -20,7 +20,7 @@ var SchemaType = require('../schematype'), */ function ObjectId(key, options) { - var isKeyHexStr = typeof key === 'string' && /^a-f0-9$/i.test(key); + var isKeyHexStr = typeof key === 'string' && key.length === 24 && /^a-f0-9$/i.test(key); var suppressWarning = options && options.suppressWarning; if ((isKeyHexStr || typeof key === 'undefined') && !suppressWarning) { console.warn('mongoose: To create a new ObjectId please try ' + From 15ee3b59f8aac979df04799fc8e117af943d9fce Mon Sep 17 00:00:00 2001 From: Ayush Gupta Date: Tue, 24 Oct 2017 08:57:49 +0530 Subject: [PATCH 57/78] addFields operator and tests added --- lib/aggregate.js | 35 +++++++++++++++++++++++++++++++++++ package-lock.json | 36 ++++++++++++++++++------------------ test/aggregate.test.js | 14 ++++++++++++++ 3 files changed, 67 insertions(+), 18 deletions(-) diff --git a/lib/aggregate.js b/lib/aggregate.js index 9d0d00f66e5..c98ff13aacb 100644 --- a/lib/aggregate.js +++ b/lib/aggregate.js @@ -106,6 +106,41 @@ Aggregate.prototype.append = function() { return this; }; +/** + * Appends a new $addFields operator to this aggregate pipeline. + * Requires MongoDB v3.4+ to work + * + * ####Examples: + * + * // adding new fields based on existing fields + * aggregate.addFields({ + * newField: '$b.nested' + * , plusTen: { $add: ['$val', 10]} + * , sub: { + * name: '$a' + * } + * }) + * + * // etc + * aggregate.addFields({ salary_k: { $divide: [ "$salary", 1000 ] } }); + * + * @param {Object} arg field specification + * @see projection http://docs.mongodb.org/manual/reference/aggregation/project/ + * @return {Aggregate} + * @api public + */ +Aggregate.prototype.addFields = function(arg) { + var fields = {}; + if (typeof arg === 'object' && !util.isArray(arg)) { + Object.keys(arg).forEach(function(field) { + fields[field] = arg[field]; + }); + } else { + throw new Error('Invalid addFields() argument. Must be an object'); + } + return this.append({$addFields: fields}); +}; + /** * Appends a new $project operator to this aggregate pipeline. * diff --git a/package-lock.json b/package-lock.json index 2a439c4b0b0..ace1960d1db 100644 --- a/package-lock.json +++ b/package-lock.json @@ -1,6 +1,6 @@ { "name": "mongoose", - "version": "4.12.4-pre", + "version": "4.12.5-pre", "lockfileVersion": 1, "requires": true, "dependencies": { @@ -2866,15 +2866,6 @@ "is-finite": "1.0.2" } }, - "require_optional": { - "version": "1.0.1", - "resolved": "https://registry.npmjs.org/require_optional/-/require_optional-1.0.1.tgz", - "integrity": "sha512-qhM/y57enGWHAe3v/NcwML6a3/vfESLe/sGM2dII+gEO0BpKRUkWZow/tyloNqJyN6kXSl3RyyM8Ll5D/sJP8g==", - "requires": { - "resolve-from": "2.0.0", - "semver": "5.4.1" - } - }, "require-uncached": { "version": "1.0.3", "resolved": "https://registry.npmjs.org/require-uncached/-/require-uncached-1.0.3.tgz", @@ -2893,6 +2884,15 @@ } } }, + "require_optional": { + "version": "1.0.1", + "resolved": "https://registry.npmjs.org/require_optional/-/require_optional-1.0.1.tgz", + "integrity": "sha512-qhM/y57enGWHAe3v/NcwML6a3/vfESLe/sGM2dII+gEO0BpKRUkWZow/tyloNqJyN6kXSl3RyyM8Ll5D/sJP8g==", + "requires": { + "resolve-from": "2.0.0", + "semver": "5.4.1" + } + }, "requirejs": { "version": "2.3.5", "resolved": "https://registry.npmjs.org/requirejs/-/requirejs-2.3.5.tgz", @@ -3097,14 +3097,6 @@ "integrity": "sha1-+vUbnrdKrvOzrPStX2Gr8ky3uT4=", "dev": true }, - "string_decoder": { - "version": "1.0.3", - "resolved": "https://registry.npmjs.org/string_decoder/-/string_decoder-1.0.3.tgz", - "integrity": "sha512-4AH6Z5fzNNBcH+6XDMfA/BTt87skxqJlO0lAh3Dker5zThcAxG6mKz+iGu308UKoPPQ8Dcqx/4JhujzltRa+hQ==", - "requires": { - "safe-buffer": "5.1.1" - } - }, "string-width": { "version": "1.0.2", "resolved": "https://registry.npmjs.org/string-width/-/string-width-1.0.2.tgz", @@ -3116,6 +3108,14 @@ "strip-ansi": "3.0.1" } }, + "string_decoder": { + "version": "1.0.3", + "resolved": "https://registry.npmjs.org/string_decoder/-/string_decoder-1.0.3.tgz", + "integrity": "sha512-4AH6Z5fzNNBcH+6XDMfA/BTt87skxqJlO0lAh3Dker5zThcAxG6mKz+iGu308UKoPPQ8Dcqx/4JhujzltRa+hQ==", + "requires": { + "safe-buffer": "5.1.1" + } + }, "stringifier": { "version": "1.3.0", "resolved": "https://registry.npmjs.org/stringifier/-/stringifier-1.3.0.tgz", diff --git a/test/aggregate.test.js b/test/aggregate.test.js index 2af32acb83c..c6aad586408 100644 --- a/test/aggregate.test.js +++ b/test/aggregate.test.js @@ -448,6 +448,20 @@ describe('aggregate: ', function() { }); }); + describe('project', function() { + it('(object)', function(done) { + var aggregate = new Aggregate(); + + assert.equal(aggregate.addFields({ a: 1, b: 1, c: 0 }), aggregate); + assert.deepEqual(aggregate._pipeline, [{ $addFields: { a: 1, b: 1, c: 0 } }]); + + aggregate.addFields({ d: {$add: ['$a','$b']} }); + assert.deepEqual(aggregate._pipeline, [{ $addFields: { a: 1, b: 1, c: 0 } }, { $addFields: { d: {$add: ['$a','$b']} } }]); + + done(); + }); + }); + describe('facet', function() { it('works', function(done) { var aggregate = new Aggregate(); From 45b749a526cfaf6309c6196a2dfcba1baf721822 Mon Sep 17 00:00:00 2001 From: Ayush Gupta Date: Tue, 24 Oct 2017 09:02:43 +0530 Subject: [PATCH 58/78] fix test name --- test/aggregate.test.js | 2 +- 1 file changed, 1 insertion(+), 1 deletion(-) diff --git a/test/aggregate.test.js b/test/aggregate.test.js index c6aad586408..a5b3806762f 100644 --- a/test/aggregate.test.js +++ b/test/aggregate.test.js @@ -448,7 +448,7 @@ describe('aggregate: ', function() { }); }); - describe('project', function() { + describe('addFields', function() { it('(object)', function(done) { var aggregate = new Aggregate(); From 273f489bf3c804ad8bea150a1f3da2849d0464c0 Mon Sep 17 00:00:00 2001 From: Ayush Gupta Date: Tue, 24 Oct 2017 09:04:56 +0530 Subject: [PATCH 59/78] (docs) fix addFields link --- lib/aggregate.js | 2 +- 1 file changed, 1 insertion(+), 1 deletion(-) diff --git a/lib/aggregate.js b/lib/aggregate.js index c98ff13aacb..7927cb21aa4 100644 --- a/lib/aggregate.js +++ b/lib/aggregate.js @@ -125,7 +125,7 @@ Aggregate.prototype.append = function() { * aggregate.addFields({ salary_k: { $divide: [ "$salary", 1000 ] } }); * * @param {Object} arg field specification - * @see projection http://docs.mongodb.org/manual/reference/aggregation/project/ + * @see $addFields https://docs.mongodb.com/manual/reference/operator/aggregation/addFields/ * @return {Aggregate} * @api public */ From 541fb36a699edb079cc1ff5f091943eb2bef2cc6 Mon Sep 17 00:00:00 2001 From: Ayush Gupta Date: Tue, 24 Oct 2017 09:13:34 +0530 Subject: [PATCH 60/78] (lint): fix trailing spaces --- test/aggregate.test.js | 5 ++--- 1 file changed, 2 insertions(+), 3 deletions(-) diff --git a/test/aggregate.test.js b/test/aggregate.test.js index a5b3806762f..57bc2fdcda8 100644 --- a/test/aggregate.test.js +++ b/test/aggregate.test.js @@ -451,13 +451,12 @@ describe('aggregate: ', function() { describe('addFields', function() { it('(object)', function(done) { var aggregate = new Aggregate(); - + assert.equal(aggregate.addFields({ a: 1, b: 1, c: 0 }), aggregate); assert.deepEqual(aggregate._pipeline, [{ $addFields: { a: 1, b: 1, c: 0 } }]); - + aggregate.addFields({ d: {$add: ['$a','$b']} }); assert.deepEqual(aggregate._pipeline, [{ $addFields: { a: 1, b: 1, c: 0 } }, { $addFields: { d: {$add: ['$a','$b']} } }]); - done(); }); }); From 8d09a20c79852cb80ca72cca50d01ab4f9225a30 Mon Sep 17 00:00:00 2001 From: wlingke Date: Thu, 26 Oct 2017 10:21:49 -0700 Subject: [PATCH 61/78] Added test cases for #5706 --- test/model.discriminator.test.js | 121 +++++++++++++++++++++++++++++++ 1 file changed, 121 insertions(+) diff --git a/test/model.discriminator.test.js b/test/model.discriminator.test.js index e473571b659..769115dc686 100644 --- a/test/model.discriminator.test.js +++ b/test/model.discriminator.test.js @@ -842,5 +842,126 @@ describe('model', function() { }). catch(done); }); + describe('embedded discriminators + hooks (gh-5706)', function(){ + var counters = { + eventPreSave: 0, + eventPostSave: 0, + purchasePreSave: 0, + purchasePostSave: 0, + eventPreValidate: 0, + eventPostValidate: 0, + purchasePreValidate: 0, + purchasePostValidate: 0, + }; + var eventSchema = new Schema( + { message: String }, + { discriminatorKey: 'kind', _id: false } + ); + eventSchema.pre('validate', (next) => { + counters.eventPreValidate++; + next(); + }); + + eventSchema.post('validate', (doc) => { + counters.eventPostValidate++; + }); + + eventSchema.pre('save', (next) => { + counters.eventPreSave++; + next(); + }); + + eventSchema.post('save', (doc) => { + counters.eventPostSave++; + }); + + var purchasedSchema = new Schema({ + product: String, + }, { _id: false }); + + purchasedSchema.pre('validate', (next) => { + counters.purchasePreValidate++; + next(); + }); + + purchasedSchema.post('validate', (doc) => { + counters.purchasePostValidate++; + }); + + purchasedSchema.pre('save', (next) => { + counters.purchasePreSave++; + next(); + }); + + purchasedSchema.post('save', (doc) => { + counters.purchasePostSave++; + }); + + beforeEach(function() { + Object.keys(counters).forEach(function(i){ + counters[i]=0; + }) + }); + + it('should call the hooks on the embedded document defined by both the parent and discriminated schemas', function(done){ + var trackSchema = new Schema({ + event: eventSchema, + }); + + var embeddedEventSchema = trackSchema.path('event'); + embeddedEventSchema.discriminator('Purchased', purchasedSchema) + + var TrackModel = db.model('Track', trackSchema); + var doc = new TrackModel({ + event: { + message: 'Test', + kind: 'Purchased' + } + }); + doc.save(function(err){ + assert.ok(!err); + assert.equal(doc.event.message, 'Test') + assert.equal(doc.event.kind, 'Purchased') + Object.keys(counters).forEach(function(i){ + assert.equal(counters[i], 1); + }); + done(); + }) + }) + + it('should call the hooks on the embedded document in an embedded array defined by both the parent and discriminated schemas', function(done){ + var trackSchema = new Schema({ + events: [eventSchema], + }); + + var embeddedEventSchema = trackSchema.path('events'); + embeddedEventSchema.discriminator('Purchased', purchasedSchema) + + var TrackModel = db.model('Track2', trackSchema); + var doc = new TrackModel({ + events: [ + { + message: 'Test', + kind: 'Purchased' + }, + { + message: 'TestAgain', + kind: 'Purchased' + } + ] + }); + doc.save(function(err){ + assert.ok(!err); + assert.equal(doc.events[0].kind, 'Purchased'); + assert.equal(doc.events[0].message, 'Test'); + assert.equal(doc.events[1].kind, 'Purchased'); + assert.equal(doc.events[1].message, 'TestAgain'); + Object.keys(counters).forEach(function(i){ + assert.equal(counters[i], 2); + }); + done(); + }) + }) + }) }); }); From a51e1f520185beb46a0bde5236a4bf18a5fadf13 Mon Sep 17 00:00:00 2001 From: Valeri Karpov Date: Thu, 26 Oct 2017 10:56:57 -0700 Subject: [PATCH 62/78] fix(connection): handle no cb correctly for connection helpers --- lib/connection.js | 2 +- 1 file changed, 1 insertion(+), 1 deletion(-) diff --git a/lib/connection.js b/lib/connection.js index ecc4726a4bf..eb8e61133e1 100644 --- a/lib/connection.js +++ b/lib/connection.js @@ -425,7 +425,7 @@ function _wrapConnHelper(fn) { return function() { var _this = this; var Promise = PromiseProvider.get(); - var cb = arguments.length > 0 ? arguments[arguments.length - 1] : []; + var cb = arguments.length > 0 ? arguments[arguments.length - 1] : null; var argsWithoutCb = typeof cb === 'function' ? Array.prototype.slice.call(arguments, 0, arguments.length - 1) : Array.prototype.slice.call(arguments); From f0088950c01c7bfdb2f2b5b8bf746c0a0ce2b7dc Mon Sep 17 00:00:00 2001 From: Valeri Karpov Date: Thu, 26 Oct 2017 16:22:13 -0700 Subject: [PATCH 63/78] chore: bump lockfile --- package-lock.json | 2 +- 1 file changed, 1 insertion(+), 1 deletion(-) diff --git a/package-lock.json b/package-lock.json index 2a439c4b0b0..d9d818f0af1 100644 --- a/package-lock.json +++ b/package-lock.json @@ -1,6 +1,6 @@ { "name": "mongoose", - "version": "4.12.4-pre", + "version": "4.12.5-pre", "lockfileVersion": 1, "requires": true, "dependencies": { From f02641c173a60e5e1e5822118b12791157d1ae1b Mon Sep 17 00:00:00 2001 From: Valeri Karpov Date: Thu, 26 Oct 2017 17:27:36 -0700 Subject: [PATCH 64/78] test(populate): repro #5737 --- test/model.populate.test.js | 36 ++++++++++++++++++++++++++++++++++++ 1 file changed, 36 insertions(+) diff --git a/test/model.populate.test.js b/test/model.populate.test.js index b04c788258a..0a19a0eec4a 100644 --- a/test/model.populate.test.js +++ b/test/model.populate.test.js @@ -3251,6 +3251,42 @@ describe('model: populate:', function() { db.close(done); }); + it('populating an array of refs, slicing, and fetching many (gh-5737)', function(done) { + var BlogPost = db.model('gh5737_0', new Schema({ + title: String, + fans: [{ type: ObjectId, ref: 'gh5737' }] + })); + var User = db.model('gh5737', new Schema({ name: String })); + + User.create([{ name: 'Fan 1' }, { name: 'Fan 2' }], function(error, fans) { + assert.ifError(error); + var posts = [ + { title: 'Test 1', fans: [fans[0]._id, fans[1]._id] }, + { title: 'Test 2', fans: [fans[1]._id, fans[0]._id] } + ]; + BlogPost.create(posts, function(error) { + assert.ifError(error); + BlogPost. + find({}). + slice('fans', [0, 5]). + populate('fans'). + exec(function(err, blogposts) { + assert.ifError(error); + + assert.equal(blogposts[0].title, 'Test 1'); + assert.equal(blogposts[1].title, 'Test 2'); + + assert.equal(blogposts[0].fans[0].name, 'Fan 1'); + assert.equal(blogposts[0].fans[1].name, 'Fan 2'); + + assert.equal(blogposts[1].fans[0].name, 'Fan 2'); + assert.equal(blogposts[1].fans[1].name, 'Fan 1'); + done(); + }); + }); + }); + }); + it('maps results back to correct document (gh-1444)', function(done) { var articleSchema = new Schema({ body: String, From 1d87987bec5934fcdec4ce5361a1ef6087faba83 Mon Sep 17 00:00:00 2001 From: Valeri Karpov Date: Thu, 26 Oct 2017 17:27:57 -0700 Subject: [PATCH 65/78] fix(populate): handle slice projections correctly when automatically selecting populated fields Fix #5737 --- lib/query.js | 44 +++++++++++++++++++++ lib/services/query/selectPopulatedFields.js | 2 +- 2 files changed, 45 insertions(+), 1 deletion(-) diff --git a/lib/query.js b/lib/query.js index 28e8a648586..9db55decd67 100644 --- a/lib/query.js +++ b/lib/query.js @@ -15,6 +15,7 @@ var mquery = require('mquery'); var readPref = require('./drivers').ReadPreference; var selectPopulatedFields = require('./services/query/selectPopulatedFields'); var setDefaultsOnInsert = require('./services/setDefaultsOnInsert'); +var slice = require('sliced'); var updateValidators = require('./services/updateValidators'); var util = require('util'); var utils = require('./utils'); @@ -263,6 +264,49 @@ Query.prototype.toConstructor = function toConstructor() { * @api public */ +Query.prototype.slice = function() { + if (arguments.length === 0) { + return this; + } + + this._validate('slice'); + + var path; + var val; + + if (arguments.length === 1) { + var arg = arguments[0]; + if (typeof arg === 'object' && !Array.isArray(arg)) { + var keys = Object.keys(arg); + var numKeys = keys.length; + for (var i = 0; i < numKeys; ++i) { + this.slice(keys[i], arg[keys[i]]); + } + return this; + } + this._ensurePath('slice'); + path = this._path; + val = arguments[0]; + } else if (arguments.length === 2) { + if ('number' === typeof arguments[0]) { + this._ensurePath('slice'); + path = this._path; + val = slice(arguments); + } else { + path = arguments[0]; + val = arguments[1]; + } + } else if (arguments.length === 3) { + path = arguments[0]; + val = slice(arguments, 1); + } + + var p = {}; + p[path] = { $slice: val }; + return this.select(p); +}; + + /** * Specifies the complementary comparison value for paths specified with `where()` * diff --git a/lib/services/query/selectPopulatedFields.js b/lib/services/query/selectPopulatedFields.js index bb69f9d3f05..88b3551cbe7 100644 --- a/lib/services/query/selectPopulatedFields.js +++ b/lib/services/query/selectPopulatedFields.js @@ -41,5 +41,5 @@ function isPathInFields(userProvidedFields, path) { } cur += '.' + pieces[i]; } - return false; + return userProvidedFields[cur] != null; } From 9e3427a1df462eef8f73b98f7ac8aab5a4030fa6 Mon Sep 17 00:00:00 2001 From: Valeri Karpov Date: Thu, 26 Oct 2017 20:48:01 -0700 Subject: [PATCH 66/78] fix(document): catch sync errors in document pre hooks and report as error Fix #5738 --- package-lock.json | 6 +++--- package.json | 2 +- test/document.hooks.test.js | 20 ++++++++++++++++++++ 3 files changed, 24 insertions(+), 4 deletions(-) diff --git a/package-lock.json b/package-lock.json index d9d818f0af1..8b0b2db2b57 100644 --- a/package-lock.json +++ b/package-lock.json @@ -1402,9 +1402,9 @@ } }, "hooks-fixed": { - "version": "2.0.0", - "resolved": "https://registry.npmjs.org/hooks-fixed/-/hooks-fixed-2.0.0.tgz", - "integrity": "sha1-oB2JTVKsf2WZu7H2PfycQR33DLo=" + "version": "2.0.2", + "resolved": "https://registry.npmjs.org/hooks-fixed/-/hooks-fixed-2.0.2.tgz", + "integrity": "sha512-YurCM4gQSetcrhwEtpQHhQ4M7Zo7poNGqY4kQGeBS6eZtOcT3tnNs01ThFa0jYBByAiYt1MjMjP/YApG0EnAvQ==" }, "http-errors": { "version": "1.6.2", diff --git a/package.json b/package.json index 6e84144fbae..55f7e69bb87 100644 --- a/package.json +++ b/package.json @@ -21,7 +21,7 @@ "dependencies": { "async": "2.1.4", "bson": "~1.0.4", - "hooks-fixed": "2.0.0", + "hooks-fixed": "2.0.2", "kareem": "1.5.0", "mongodb": "2.2.33", "mpath": "0.3.0", diff --git a/test/document.hooks.test.js b/test/document.hooks.test.js index c73ff0ccd6c..6c47abdb2a3 100644 --- a/test/document.hooks.test.js +++ b/test/document.hooks.test.js @@ -763,6 +763,26 @@ describe('document: hooks:', function() { done(); }); + it('sync exceptions get passed as errors (gh-5738)', function(done) { + var bookSchema = new Schema({ title: String }); + + /* eslint-disable no-unused-vars */ + bookSchema.pre('save', function(next) { + throw new Error('woops!'); + }); + + var Book = mongoose.model('gh5738', bookSchema); + + var book = new Book({}); + + book.title = 'Professional AngularJS'; + book.save(function(error) { + assert.ok(error); + assert.equal(error.message, 'woops!'); + done(); + }); + }); + it('nested subdocs only fire once (gh-3281)', function(done) { var L3Schema = new Schema({ title: String From b9ab446706d8af0ba6d0181af7ca38e599b5d303 Mon Sep 17 00:00:00 2001 From: Valeri Karpov Date: Fri, 27 Oct 2017 18:59:06 -0700 Subject: [PATCH 67/78] refactor(test): remove unnecessary code --- test/document.hooks.test.js | 4 +--- 1 file changed, 1 insertion(+), 3 deletions(-) diff --git a/test/document.hooks.test.js b/test/document.hooks.test.js index 6c47abdb2a3..948831f8f39 100644 --- a/test/document.hooks.test.js +++ b/test/document.hooks.test.js @@ -773,9 +773,7 @@ describe('document: hooks:', function() { var Book = mongoose.model('gh5738', bookSchema); - var book = new Book({}); - - book.title = 'Professional AngularJS'; + var book = new Book({ title: 'Professional AngularJS' }); book.save(function(error) { assert.ok(error); assert.equal(error.message, 'woops!'); From 3b4211ea8b01dd76a6170a0a9504d04109a91daa Mon Sep 17 00:00:00 2001 From: Valeri Karpov Date: Sat, 28 Oct 2017 00:23:44 -0700 Subject: [PATCH 68/78] test(query): repro #5744 --- test/model.update.test.js | 32 +++++++++++++++++++++++++++++++- 1 file changed, 31 insertions(+), 1 deletion(-) diff --git a/test/model.update.test.js b/test/model.update.test.js index 69c3c977ea1..0659d61f8f3 100644 --- a/test/model.update.test.js +++ b/test/model.update.test.js @@ -2711,7 +2711,7 @@ describe('model: update:', function() { User.update({}, update, opts).exec(function(error) { assert.ok(error); - assert.ok(error.errors['notifications']); + assert.ok(error.errors['notifications.message']); update.$pull.notifications.message = 'test'; User.update({ _id: doc._id }, update, opts).exec(function(error) { @@ -2726,6 +2726,36 @@ describe('model: update:', function() { }); }); + it('$pull with updateValidators and $in (gh-5744)', function(done) { + var exampleSchema = mongoose.Schema({ + subdocuments: [{ + name: String + }] + }); + var ExampleModel = db.model('gh5744', exampleSchema); + var exampleDocument = { + subdocuments: [{ name: 'First' }, { name: 'Second' }] + }; + + ExampleModel.create(exampleDocument, function(error, doc) { + assert.ifError(error); + ExampleModel.updateOne({ _id: doc._id }, { + $pull: { + subdocuments: { + _id: { $in: [doc.subdocuments[0]._id] } + } + } + }, { runValidators: true }, function(error) { + assert.ifError(error); + ExampleModel.findOne({ _id: doc._id }, function(error, doc) { + assert.ifError(error); + assert.equal(doc.subdocuments.length, 1); + done(); + }); + }); + }); + }); + it('update with Decimal type (gh-5361)', function(done) { start.mongodVersion(function(err, version) { if (err) { From 85821d9cd07a78310eaafa5f4357d25f730224b6 Mon Sep 17 00:00:00 2001 From: Valeri Karpov Date: Sat, 28 Oct 2017 00:23:51 -0700 Subject: [PATCH 69/78] fix(query): correctly handle `$in` and required for $pull and update validators Fix #5744 --- lib/services/updateValidators.js | 8 ++++---- 1 file changed, 4 insertions(+), 4 deletions(-) diff --git a/lib/services/updateValidators.js b/lib/services/updateValidators.js index 326d0b169a2..f283fd7e3c2 100644 --- a/lib/services/updateValidators.js +++ b/lib/services/updateValidators.js @@ -33,8 +33,8 @@ module.exports = function(query, schema, castedDoc, options) { for (var i = 0; i < numKeys; ++i) { if (keys[i].charAt(0) === '$') { - if (keys[i] === '$push' || keys[i] === '$addToSet' || - keys[i] === '$pull' || keys[i] === '$pullAll') { + hasDollarUpdate = true; + if (keys[i] === '$push' || keys[i] === '$addToSet') { _keys = Object.keys(castedDoc[keys[i]]); for (var ii = 0; ii < _keys.length; ++ii) { currentUpdate = castedDoc[keys[i]][_keys[ii]]; @@ -55,14 +55,14 @@ module.exports = function(query, schema, castedDoc, options) { for (var j = 0; j < numPaths; ++j) { var updatedPath = paths[j].replace('.$.', '.0.'); updatedPath = updatedPath.replace(/\.\$$/, '.0'); - if (keys[i] === '$set' || keys[i] === '$setOnInsert') { + if (keys[i] === '$set' || keys[i] === '$setOnInsert' || + keys[i] === '$pull' || keys[i] === '$pullAll') { updatedValues[updatedPath] = flat[paths[j]]; } else if (keys[i] === '$unset') { updatedValues[updatedPath] = undefined; } updatedKeys[updatedPath] = true; } - hasDollarUpdate = true; } } From 9c709e1730592d695955a5f67c8459fa0f846537 Mon Sep 17 00:00:00 2001 From: Valeri Karpov Date: Sat, 28 Oct 2017 13:03:34 -0700 Subject: [PATCH 70/78] refactor: reduce number of array accesses --- lib/services/updateValidators.js | 8 +++++--- 1 file changed, 5 insertions(+), 3 deletions(-) diff --git a/lib/services/updateValidators.js b/lib/services/updateValidators.js index f283fd7e3c2..0453f7eac53 100644 --- a/lib/services/updateValidators.js +++ b/lib/services/updateValidators.js @@ -30,6 +30,7 @@ module.exports = function(query, schema, castedDoc, options) { var hasDollarUpdate = false; var modified = {}; var currentUpdate; + var key; for (var i = 0; i < numKeys; ++i) { if (keys[i].charAt(0) === '$') { @@ -55,10 +56,11 @@ module.exports = function(query, schema, castedDoc, options) { for (var j = 0; j < numPaths; ++j) { var updatedPath = paths[j].replace('.$.', '.0.'); updatedPath = updatedPath.replace(/\.\$$/, '.0'); - if (keys[i] === '$set' || keys[i] === '$setOnInsert' || - keys[i] === '$pull' || keys[i] === '$pullAll') { + key = keys[i]; + if (keys === '$set' || keys === '$setOnInsert' || + keys === '$pull' || keys === '$pullAll') { updatedValues[updatedPath] = flat[paths[j]]; - } else if (keys[i] === '$unset') { + } else if (key === '$unset') { updatedValues[updatedPath] = undefined; } updatedKeys[updatedPath] = true; From 411db6192d46a711e4bc33c06e3328969ba2b626 Mon Sep 17 00:00:00 2001 From: Valeri Karpov Date: Sat, 28 Oct 2017 13:06:57 -0700 Subject: [PATCH 71/78] fix: remove typo that was breaking tests --- lib/services/updateValidators.js | 4 ++-- 1 file changed, 2 insertions(+), 2 deletions(-) diff --git a/lib/services/updateValidators.js b/lib/services/updateValidators.js index 0453f7eac53..0d0be0857f2 100644 --- a/lib/services/updateValidators.js +++ b/lib/services/updateValidators.js @@ -57,8 +57,8 @@ module.exports = function(query, schema, castedDoc, options) { var updatedPath = paths[j].replace('.$.', '.0.'); updatedPath = updatedPath.replace(/\.\$$/, '.0'); key = keys[i]; - if (keys === '$set' || keys === '$setOnInsert' || - keys === '$pull' || keys === '$pullAll') { + if (key === '$set' || key === '$setOnInsert' || + key === '$pull' || key === '$pullAll') { updatedValues[updatedPath] = flat[paths[j]]; } else if (key === '$unset') { updatedValues[updatedPath] = undefined; From 4d9d71b37fec057c40f2943d74439844db7eeeb3 Mon Sep 17 00:00:00 2001 From: Valeri Karpov Date: Sat, 28 Oct 2017 19:16:18 -0700 Subject: [PATCH 72/78] test(discriminator): fix tests re: #5757 --- test/model.discriminator.test.js | 30 +++++++++++++++--------------- 1 file changed, 15 insertions(+), 15 deletions(-) diff --git a/test/model.discriminator.test.js b/test/model.discriminator.test.js index 769115dc686..8f3eed47316 100644 --- a/test/model.discriminator.test.js +++ b/test/model.discriminator.test.js @@ -857,21 +857,21 @@ describe('model', function() { { message: String }, { discriminatorKey: 'kind', _id: false } ); - eventSchema.pre('validate', (next) => { + eventSchema.pre('validate', function(next) { counters.eventPreValidate++; next(); }); - eventSchema.post('validate', (doc) => { + eventSchema.post('validate', function(doc) { counters.eventPostValidate++; }); - eventSchema.pre('save', (next) => { + eventSchema.pre('save', function(next) { counters.eventPreSave++; next(); }); - eventSchema.post('save', (doc) => { + eventSchema.post('save', function(doc) { counters.eventPostSave++; }); @@ -879,28 +879,28 @@ describe('model', function() { product: String, }, { _id: false }); - purchasedSchema.pre('validate', (next) => { + purchasedSchema.pre('validate', function(next) { counters.purchasePreValidate++; next(); }); - purchasedSchema.post('validate', (doc) => { + purchasedSchema.post('validate', function(doc) { counters.purchasePostValidate++; }); - purchasedSchema.pre('save', (next) => { + purchasedSchema.pre('save', function(next) { counters.purchasePreSave++; next(); }); - purchasedSchema.post('save', (doc) => { + purchasedSchema.post('save', function(doc) { counters.purchasePostSave++; }); beforeEach(function() { - Object.keys(counters).forEach(function(i){ - counters[i]=0; - }) + Object.keys(counters).forEach(function(i) { + counters[i] = 0; + }); }); it('should call the hooks on the embedded document defined by both the parent and discriminated schemas', function(done){ @@ -909,7 +909,7 @@ describe('model', function() { }); var embeddedEventSchema = trackSchema.path('event'); - embeddedEventSchema.discriminator('Purchased', purchasedSchema) + embeddedEventSchema.discriminator('Purchased', purchasedSchema.clone()); var TrackModel = db.model('Track', trackSchema); var doc = new TrackModel({ @@ -922,7 +922,7 @@ describe('model', function() { assert.ok(!err); assert.equal(doc.event.message, 'Test') assert.equal(doc.event.kind, 'Purchased') - Object.keys(counters).forEach(function(i){ + Object.keys(counters).forEach(function(i) { assert.equal(counters[i], 1); }); done(); @@ -935,7 +935,7 @@ describe('model', function() { }); var embeddedEventSchema = trackSchema.path('events'); - embeddedEventSchema.discriminator('Purchased', purchasedSchema) + embeddedEventSchema.discriminator('Purchased', purchasedSchema.clone()); var TrackModel = db.model('Track2', trackSchema); var doc = new TrackModel({ @@ -956,7 +956,7 @@ describe('model', function() { assert.equal(doc.events[0].message, 'Test'); assert.equal(doc.events[1].kind, 'Purchased'); assert.equal(doc.events[1].message, 'TestAgain'); - Object.keys(counters).forEach(function(i){ + Object.keys(counters).forEach(function(i) { assert.equal(counters[i], 2); }); done(); From ae78242fb4fecdafabd269c0f3d4e58aead2bbb5 Mon Sep 17 00:00:00 2001 From: Valeri Karpov Date: Sat, 28 Oct 2017 19:33:48 -0700 Subject: [PATCH 73/78] chore: release 4.12.5 --- History.md | 9 +++++++++ package.json | 2 +- 2 files changed, 10 insertions(+), 1 deletion(-) diff --git a/History.md b/History.md index 5e8424d3c8a..cf09366f5f6 100644 --- a/History.md +++ b/History.md @@ -1,3 +1,12 @@ +4.12.5 / 2017-10-28 +=================== + * fix(query): correctly handle `$in` and required for $pull and update validators #5744 + * feat(aggegate): add $addFields pipeline operator #5740 [AyushG3112](https://github.com/AyushG3112) + * fix(document): catch sync errors in document pre hooks and report as error #5738 + * fix(populate): handle slice projections correctly when automatically selecting populated fields #5737 + * fix(discriminator): fix hooks for embedded discriminators #5706 [wlingke](https://github.com/wlingke) + * fix(model): throw sane error when customer calls `mongoose.Model()` over `mongoose.model()` #2005 + 4.12.4 / 2017-10-21 =================== * test(plugins): add coverage for idGetter with id as a schema property #5713 [wlingke](https://github.com/wlingke) diff --git a/package.json b/package.json index 55f7e69bb87..5d6c7fb1b81 100644 --- a/package.json +++ b/package.json @@ -1,7 +1,7 @@ { "name": "mongoose", "description": "Mongoose MongoDB ODM", - "version": "4.12.5-pre", + "version": "4.12.5", "author": "Guillermo Rauch ", "keywords": [ "mongodb", From c83b4161f5bbf77fc6e2689bfb1e2067dfc01400 Mon Sep 17 00:00:00 2001 From: Valeri Karpov Date: Sun, 29 Oct 2017 22:44:28 -0700 Subject: [PATCH 74/78] chore: fix date --- History.md | 2 +- 1 file changed, 1 insertion(+), 1 deletion(-) diff --git a/History.md b/History.md index cf09366f5f6..cfd80ff6aea 100644 --- a/History.md +++ b/History.md @@ -1,4 +1,4 @@ -4.12.5 / 2017-10-28 +4.12.5 / 2017-10-29 =================== * fix(query): correctly handle `$in` and required for $pull and update validators #5744 * feat(aggegate): add $addFields pipeline operator #5740 [AyushG3112](https://github.com/AyushG3112) From 65cc9099f1b046d9403edcee3eb92a27df5f019d Mon Sep 17 00:00:00 2001 From: Valeri Karpov Date: Sun, 29 Oct 2017 22:44:44 -0700 Subject: [PATCH 75/78] chore: now working on 4.12.6 --- package.json | 2 +- 1 file changed, 1 insertion(+), 1 deletion(-) diff --git a/package.json b/package.json index 5d6c7fb1b81..a0c5e67a96e 100644 --- a/package.json +++ b/package.json @@ -1,7 +1,7 @@ { "name": "mongoose", "description": "Mongoose MongoDB ODM", - "version": "4.12.5", + "version": "4.12.6-pre", "author": "Guillermo Rauch ", "keywords": [ "mongodb", From 4138ce5723e9fb58286c5ede57d805dfdbf17e2d Mon Sep 17 00:00:00 2001 From: Valeri Karpov Date: Mon, 30 Oct 2017 12:33:03 -0700 Subject: [PATCH 76/78] chore: bump lockfile --- package-lock.json | 2 +- 1 file changed, 1 insertion(+), 1 deletion(-) diff --git a/package-lock.json b/package-lock.json index 8b0b2db2b57..ac5a35b7c1c 100644 --- a/package-lock.json +++ b/package-lock.json @@ -1,6 +1,6 @@ { "name": "mongoose", - "version": "4.12.5-pre", + "version": "4.12.6-pre", "lockfileVersion": 1, "requires": true, "dependencies": { From 43fb08626fa788b397801a8e9d7824a9960d85fe Mon Sep 17 00:00:00 2001 From: Valeri Karpov Date: Mon, 30 Oct 2017 12:36:07 -0700 Subject: [PATCH 77/78] test(schema): repro #5752 --- test/schema.test.js | 14 ++++++++++++++ 1 file changed, 14 insertions(+) diff --git a/test/schema.test.js b/test/schema.test.js index 8f21b8e40bc..a378e2c6543 100644 --- a/test/schema.test.js +++ b/test/schema.test.js @@ -1669,6 +1669,20 @@ describe('schema', function() { done(); }); + it('clone() copies methods, statics, and query helpers (gh-5752)', function(done) { + var schema = new Schema({}); + + schema.methods.fakeMethod = function() { return 'fakeMethod'; }; + schema.statics.fakeStatic = function() { return 'fakeStatic'; }; + schema.query.fakeQueryHelper = function() { return 'fakeQueryHelper'; }; + + var clone = schema.clone(); + assert.equal(clone.methods.fakeMethod, schema.methods.fakeMethod); + assert.equal(clone.statics.fakeStatic, schema.statics.fakeStatic); + assert.equal(clone.query.fakeQueryHelper, schema.query.fakeQueryHelper); + done(); + }); + it('clone() copies validators declared with validate() (gh-5607)', function(done) { var schema = new Schema({ num: Number From f43a984e31049bc2c8b65df617e0779d4c6b3178 Mon Sep 17 00:00:00 2001 From: Valeri Karpov Date: Mon, 30 Oct 2017 12:36:19 -0700 Subject: [PATCH 78/78] fix(schema): make clone() copy query helpers correctly Fix #5752 --- lib/schema.js | 8 +++++--- 1 file changed, 5 insertions(+), 3 deletions(-) diff --git a/lib/schema.js b/lib/schema.js index 52ec2bae076..48acab99cb2 100644 --- a/lib/schema.js +++ b/lib/schema.js @@ -298,11 +298,13 @@ Schema.prototype.tree; Schema.prototype.clone = function() { var s = new Schema(this.paths, this.options); // Clone the call queue + var cloneOpts = { retainKeyOrder: true }; s.callQueue = this.callQueue.map(function(f) { return f; }); - s.methods = utils.clone(this.methods); - s.statics = utils.clone(this.statics); + s.methods = utils.clone(this.methods, cloneOpts); + s.statics = utils.clone(this.statics, cloneOpts); + s.query = utils.clone(this.query, cloneOpts); s.plugins = Array.prototype.slice.call(this.plugins); - s._indexes = utils.clone(this._indexes); + s._indexes = utils.clone(this._indexes, cloneOpts); s.s.hooks = this.s.hooks.clone(); return s; };