diff --git a/lib/document.js b/lib/document.js index 731be3301c7..b9f39a82c12 100644 --- a/lib/document.js +++ b/lib/document.js @@ -694,7 +694,6 @@ Document.prototype.$__init = function(doc, opts) { init(this, doc, this._doc, opts); markArraySubdocsPopulated(this, opts.populated); - this.$emit('init', this); this.constructor.emit('init', this); @@ -703,7 +702,6 @@ Document.prototype.$__init = function(doc, opts) { null; applyDefaults(this, this.$__.selected, this.$__.exclude, hasIncludedChildren, false, this.$__.skipDefaults); - return this; }; @@ -746,7 +744,6 @@ function init(self, obj, doc, opts, prefix) { } path = prefix + i; schemaType = docSchema.path(path); - // Should still work if not a model-level discriminator, but should not be // necessary. This is *only* to catch the case where we queried using the // base model and the discriminated model has a projection @@ -770,15 +767,14 @@ function init(self, obj, doc, opts, prefix) { } } else { // Retain order when overwriting defaults - if (doc.hasOwnProperty(i) && obj[i] !== void 0) { + if (doc.hasOwnProperty(i) && obj[i] !== void 0 && !opts.hydratedPopulatedDocs) { delete doc[i]; } if (obj[i] === null) { doc[i] = schemaType._castNullish(null); } else if (obj[i] !== undefined) { const wasPopulated = obj[i].$__ == null ? null : obj[i].$__.wasPopulated; - - if (schemaType && !wasPopulated) { + if ((schemaType && !wasPopulated) && !opts.hydratedPopulatedDocs) { try { if (opts && opts.setters) { // Call applySetters with `init = false` because otherwise setters are a noop diff --git a/lib/model.js b/lib/model.js index 93e34c2eb33..a596c864833 100644 --- a/lib/model.js +++ b/lib/model.js @@ -3830,6 +3830,7 @@ Model.buildBulkWriteOperations = function buildBulkWriteOperations(documents, op * @param {Object|String|String[]} [projection] optional projection containing which fields should be selected for this document * @param {Object} [options] optional options * @param {Boolean} [options.setters=false] if true, apply schema setters when hydrating + * @param {Boolean} [options.hydratedPopulatedDocs=false] if true, populates the docs if passing pre-populated data * @return {Document} document instance * @api public */ @@ -3843,7 +3844,6 @@ Model.hydrate = function(obj, projection, options) { } obj = applyProjection(obj, projection); } - const document = require('./queryHelpers').createModel(this, obj, projection); document.$init(obj, options); return document; diff --git a/test/model.hydrate.test.js b/test/model.hydrate.test.js index 6cdc333ce8d..bc6632f5b15 100644 --- a/test/model.hydrate.test.js +++ b/test/model.hydrate.test.js @@ -99,5 +99,23 @@ describe('model', function() { assert.equal(hydrated.test, 'test'); assert.deepEqual(hydrated.schema.tree, C.schema.tree); }); + it('should deeply hydrate the document with the `hydratedPopulatedDocs` option (gh-4727)', async function() { + const userSchema = new Schema({ + name: String + }); + const companySchema = new Schema({ + name: String, + users: [{ ref: 'User', type: Schema.Types.ObjectId }] + }); + + db.model('UserTestHydrate', userSchema); + const Company = db.model('CompanyTestHyrdrate', companySchema); + + const users = [{ _id: new mongoose.Types.ObjectId(), name: 'Val' }]; + const company = { _id: new mongoose.Types.ObjectId(), name: 'Booster', users: [users[0]] }; + + const C = Company.hydrate(company, null, { hydratedPopulatedDocs: true }); + assert.equal(C.users[0].name, 'Val'); + }); }); }); diff --git a/test/types/models.test.ts b/test/types/models.test.ts index 9bf423f136f..61298be836d 100644 --- a/test/types/models.test.ts +++ b/test/types/models.test.ts @@ -880,3 +880,23 @@ async function gh13999() { } } } + +function gh4727() { + const userSchema = new mongoose.Schema({ + name: String + }); + const companySchema = new mongoose.Schema({ + name: String, + users: [{ ref: 'User', type: mongoose.Schema.Types.ObjectId }] + }); + + mongoose.model('UserTestHydrate', userSchema); + const Company = mongoose.model('CompanyTestHyrdrate', companySchema); + + const users = [{ _id: new mongoose.Types.ObjectId(), name: 'Val' }]; + const company = { _id: new mongoose.Types.ObjectId(), name: 'Booster', users: [users[0]] }; + + return Company.hydrate(company, {}, { hydratedPopulatedDocs: true }); + + +} diff --git a/types/models.d.ts b/types/models.d.ts index 67731cc3eb0..a77aa25525e 100644 --- a/types/models.d.ts +++ b/types/models.d.ts @@ -37,6 +37,11 @@ declare module 'mongoose' { skipValidation?: boolean; } + interface HydrateOptions { + setters?: boolean; + hydratedPopulatedDocs?: boolean; + } + interface InsertManyOptions extends PopulateOption, SessionOption { @@ -371,7 +376,7 @@ declare module 'mongoose' { * Shortcut for creating a new Document from existing raw data, pre-saved in the DB. * The document returned has no paths marked as modified initially. */ - hydrate(obj: any, projection?: AnyObject, options?: { setters?: boolean }): THydratedDocumentType; + hydrate(obj: any, projection?: AnyObject, options?: HydrateOptions): THydratedDocumentType; /** * This function is responsible for building [indexes](https://www.mongodb.com/docs/manual/indexes/),