diff --git a/lib/document.js b/lib/document.js index 56b029c2891..fd159ffbb96 100644 --- a/lib/document.js +++ b/lib/document.js @@ -268,19 +268,26 @@ Document.prototype.populate = function populate () { * @api private */ -Document.prototype.init = function (doc, query, fn) { - // backwards compat. remove in 4.x - if ('function' == typeof query) { - fn = query; - query = null; +Document.prototype.init = function (doc, opts, fn) { + if ('function' == typeof opts) { + fn = opts; + opts = null; } - // end backwards compat this.isNew = false; init(this, doc, this._doc); this._storeShard(); + // handle docs with populated paths + if (opts && opts.populated && opts.populated.length) { + var id = this.id; + for (var i = 0; i < opts.populated.length; ++i) { + var item = opts.populated[i]; + this.populated(item.path, item._docs[id]); + } + } + this.emit('init', this); if (fn) fn(null); return this; diff --git a/lib/internal.js b/lib/internal.js index a63fbf06073..1bcb09c565d 100644 --- a/lib/internal.js +++ b/lib/internal.js @@ -20,6 +20,7 @@ function InternalCache () { this.getters = {}; this._id = undefined; this.populate = undefined; + this.populated = undefined; this.scope = undefined; this.activePaths = new ActiveRoster; } diff --git a/lib/model.js b/lib/model.js index a1da5f76966..7bf5b6ea4b3 100644 --- a/lib/model.js +++ b/lib/model.js @@ -1636,6 +1636,10 @@ function populate (model, docs, options, cb) { ret = utils.getValue(path, doc); } + options._docs[doc._id] = Array.isArray(ret) + ? ret.slice() + : ret; + if (isDocument) { // cache original populated _ids doc.populated(path, Array.isArray(ret) ? ret.slice() : ret); diff --git a/lib/query.js b/lib/query.js index 2ec5dc20bd2..28dd07e75ba 100644 --- a/lib/query.js +++ b/lib/query.js @@ -61,7 +61,10 @@ Query.prototype.setOptions = function (options, overwrite) { // overwrite is internal use only if (overwrite) { options = this.options = options || {}; - this.safe = options.safe + this.safe = options.safe; + if ('populate' in options) { + this.populate(this.options.populate); + } return this; } @@ -89,6 +92,7 @@ Query.prototype.setOptions = function (options, overwrite) { this.options[method] = options[method]; } } + return this; } @@ -1523,7 +1527,7 @@ Query.prototype.execFind = function (callback) { if (!self.options.populate) { return true === options.lean ? promise.complete(docs) - : completeMany(model, docs, fields, self, promise); + : completeMany(model, docs, fields, self, null, promise); } var pop = utils.object.vals(self.options.populate); @@ -1531,7 +1535,7 @@ Query.prototype.execFind = function (callback) { if (err) return promise.error(err); return true === options.lean ? promise.complete(docs) - : completeMany(model, docs, fields, self, promise); + : completeMany(model, docs, fields, self, pop, promise); }); } @@ -1545,18 +1549,22 @@ Query.prototype.execFind = function (callback) { * @param {Array} docs * @param {Object} fields * @param {Query} self + * @param {Array} [pop] array of paths used in population * @param {Promise} promise */ -function completeMany (model, docs, fields, self, promise) { +function completeMany (model, docs, fields, self, pop, promise) { var arr = []; var count = docs.length; var len = count; var i = 0; + var opts = pop ? + { populated: pop } + : undefined; for (; i < len; ++i) { arr[i] = new model(undefined, fields, true); - arr[i].init(docs[i], function (err) { + arr[i].init(docs[i], opts, function (err) { if (err) return promise.error(err); --count || promise.complete(arr); }); @@ -1618,7 +1626,7 @@ Query.prototype.findOne = function (callback) { if (!self.options.populate) { return true === options.lean ? promise.complete(doc) - : completeOne(model, doc, fields, self, promise); + : completeOne(model, doc, fields, self, null, promise); } var pop = utils.object.vals(self.options.populate); @@ -1626,7 +1634,7 @@ Query.prototype.findOne = function (callback) { if (err) return promise.error(err); return true === options.lean ? promise.complete(doc) - : completeOne(model, doc, fields, self, promise); + : completeOne(model, doc, fields, self, pop, promise); }) })); @@ -1640,12 +1648,17 @@ Query.prototype.findOne = function (callback) { * @param {Document} doc * @param {Object} fields * @param {Query} self + * @param {Array} [pop] array of paths used in population * @param {Promise} promise */ -function completeOne (model, doc, fields, self, promise) { +function completeOne (model, doc, fields, self, pop, promise) { + var opts = pop ? + { populated: pop } + : undefined; + var casted = new model(undefined, fields, true); - casted.init(doc, function (err) { + casted.init(doc, opts, function (err) { if (err) return promise.error(err); promise.complete(casted); }); @@ -2274,7 +2287,7 @@ Query.prototype._findAndModify = function (type, callback) { } var casted = new model(undefined, fields, true); - casted.init(doc, self, function (err) { + casted.init(doc, function (err) { if (err) return promise.error(err); promise.complete(casted); }); @@ -2323,7 +2336,7 @@ Query.prototype.populate = function populate () { var res = utils.populate.apply(null, arguments); var opts = this.options; - if (!opts.populate) { + if (!utils.isObject(opts.populate)) { opts.populate = {}; } diff --git a/lib/querystream.js b/lib/querystream.js index 69726c458be..75974baf948 100644 --- a/lib/querystream.js +++ b/lib/querystream.js @@ -212,7 +212,7 @@ QueryStream.prototype._onNextObject = function _onNextObject (err, doc) { var instance = new this.query.model(undefined, this._fields, true); var self = this; - instance.init(doc, this.query, function (err) { + instance.init(doc, function (err) { if (err) return self.destroy(err); self.emit('data', instance); diff --git a/lib/utils.js b/lib/utils.js index 5195ed69103..9cd1a31d225 100644 --- a/lib/utils.js +++ b/lib/utils.js @@ -488,6 +488,7 @@ function PopulateOptions (path, select, match, options, model) { this.select = select; this.options = options; this.model = model; + this._docs = {}; } // make it compatible with utils.clone diff --git a/test/model.populate.test.js b/test/model.populate.test.js index 1f175792b0a..2643932b6cf 100644 --- a/test/model.populate.test.js +++ b/test/model.populate.test.js @@ -60,7 +60,7 @@ mongoose.model('RefAlternateUser', User); * Tests. */ -describe('model: ref:', function(){ +describe('model: populate:', function(){ it('populating a single ref', function(done){ var db = start() , BlogPost = db.model('RefBlogPost', posts) @@ -1840,6 +1840,7 @@ describe('model: ref:', function(){ }); }) }) + }) describe('populating combined with lean (gh-1260)', function(){ @@ -1923,4 +1924,86 @@ describe('model: ref:', function(){ }); }) }) + + describe('records paths and _ids used in population', function(){ + var db; + var B; + var U; + var u1, u2; + var b1, b2 + + before(function(done){ + db = start() + B = db.model('RefBlogPost', posts + random()) + U = db.model('RefUser', users + random()); + + U.create({ + name : 'Fan 1' + , email : 'fan1@learnboost.com' + }, { + name : 'Fan 2' + , email : 'fan2@learnboost.com' + }, function (err, fan1, fan2) { + assert.ifError(err); + u1 = fan1; + u2 = fan2; + + B.create({ + title : 'Woot' + , fans : [fan1, fan2] + , _creator: fan1 + }, { + title : 'Woot2' + , fans : [fan2, fan1] + , _creator: fan2 + }, function (err, post1, post2) { + assert.ifError(err); + b1 = post1; + b2 = post2; + done(); + }); + }); + }) + + after(function(){ + db.close() + }) + + it('with findOne', function(done){ + B.findById(b1).populate('fans _creator').exec(function (err, doc) { + assert.ifError(err); + assert.ok(Array.isArray(doc.populated('fans'))); + assert.equal(2, doc.populated('fans').length); + assert.equal(doc.populated('fans')[0], String(u1._id)); + assert.equal(doc.populated('fans')[1], String(u2._id)); + assert.equal(doc.populated('_creator'), String(u1._id)); + done(); + }) + }) + + it('with find', function(done){ + B.find().populate('fans _creator').exec(function (err, docs) { + assert.ifError(err); + assert.equal(2, docs.length); + + var doc1 = docs[0]; + var doc2 = docs[1]; + + assert.ok(Array.isArray(doc1.populated('fans'))); + assert.equal(2, doc1.populated('fans').length); + assert.equal(doc1.populated('fans')[0], String(u1._id)); + assert.equal(doc1.populated('fans')[1], String(u2._id)); + assert.equal(doc1.populated('_creator'), String(u1._id)); + + assert.ok(Array.isArray(doc2.populated('fans'))); + assert.equal(2, doc2.populated('fans').length); + assert.equal(doc2.populated('fans')[0], String(u2._id)); + assert.equal(doc2.populated('fans')[1], String(u1._id)); + assert.equal(doc2.populated('_creator'), String(u2._id)); + done(); + }) + }) + }) + + }); diff --git a/test/query.test.js b/test/query.test.js index f08d3d1857e..f7b0eaddcaa 100644 --- a/test/query.test.js +++ b/test/query.test.js @@ -732,6 +732,7 @@ describe('Query', function(){ , select: undefined , model: undefined , options: undefined + , _docs: {} } q.populate(o); assert.deepEqual(o, q.options.populate['yellow.brick']); @@ -746,6 +747,7 @@ describe('Query', function(){ , select: undefined , model: undefined , options: undefined + , _docs: {} } q.populate(o); assert.equal(1, Object.keys(q.options.populate).length); @@ -766,6 +768,7 @@ describe('Query', function(){ , select: undefined , model: undefined , options: undefined + , _docs: {} } assert.equal(2, Object.keys(q.options.populate).length); assert.deepEqual(o, q.options.populate['yellow.brick']); @@ -1258,7 +1261,7 @@ describe('Query', function(){ q.setOptions({ read: ['s', [{dc:'eu'}]]}); assert.equal(q.options.thing, 'cat'); - assert.deepEqual(q.options.populate.fans, { path: 'fans', select: undefined, match: undefined, options: undefined, model: undefined }); + assert.deepEqual(q.options.populate.fans, { path: 'fans', select: undefined, match: undefined, options: undefined, model: undefined, _docs: {} }); assert.equal(q.options.batchSize, 10); assert.equal(q.options.limit, 4); assert.equal(q.options.skip, 3);