From f8c705e17c442a73489c27f6789eed9e76bd2a0b Mon Sep 17 00:00:00 2001 From: Valeri Karpov Date: Thu, 5 Dec 2024 17:24:24 -0500 Subject: [PATCH 1/3] fix: mark documents that are populated using hydratedPopulatedDocs option as populated in top-level doc Fix #15048 --- lib/document.js | 8 ++++++++ lib/schemaType.js | 5 +++-- test/model.hydrate.test.js | 28 ++++++++++++++++++++++++++-- 3 files changed, 37 insertions(+), 4 deletions(-) diff --git a/lib/document.js b/lib/document.js index 06204519db9..9ae9fc8758b 100644 --- a/lib/document.js +++ b/lib/document.js @@ -805,6 +805,14 @@ function init(self, obj, doc, opts, prefix) { reason: e })); } + } else if (opts.hydratedPopulatedDocs) { + doc[i] = schemaType.cast(value, self, true); + + if (doc[i] && doc[i].$__ && doc[i].$__.wasPopulated) { + self.$populated(path, doc[i].$__.wasPopulated.value, doc[i].$__.wasPopulated.options); + } else if (Array.isArray(doc[i]) && doc[i].length && doc[i][0]?.$__?.wasPopulated) { + self.$populated(path, doc[i].map(populatedDoc => populatedDoc?.$__?.wasPopulated?.value).filter(val => val != null), doc[i][0].$__.wasPopulated.options); + } } else { doc[i] = value; } diff --git a/lib/schemaType.js b/lib/schemaType.js index e5aa476468f..c15ce35faf7 100644 --- a/lib/schemaType.js +++ b/lib/schemaType.js @@ -1567,8 +1567,9 @@ SchemaType.prototype._castRef = function _castRef(value, doc, init) { !doc.$__.populated[path].options || !doc.$__.populated[path].options.options || !doc.$__.populated[path].options.options.lean) { - ret = new pop.options[populateModelSymbol](value); - ret.$__.wasPopulated = { value: ret._doc._id }; + const PopulatedModel = pop ? pop.options[populateModelSymbol] : doc.constructor.db.model(this.options.ref); + ret = new PopulatedModel(value); + ret.$__.wasPopulated = { value: ret._doc._id, options: { [populateModelSymbol]: PopulatedModel } }; } return ret; diff --git a/test/model.hydrate.test.js b/test/model.hydrate.test.js index 63947f95961..331f41908df 100644 --- a/test/model.hydrate.test.js +++ b/test/model.hydrate.test.js @@ -108,8 +108,10 @@ describe('model', function() { users: [{ ref: 'User', type: Schema.Types.ObjectId }] }); - db.model('UserTestHydrate', userSchema); - const Company = db.model('CompanyTestHyrdrate', companySchema); + db.deleteModel(/User/); + db.deleteModel(/Company/); + db.model('User', userSchema); + const Company = db.model('Company', companySchema); const users = [{ _id: new mongoose.Types.ObjectId(), name: 'Val' }]; const company = { _id: new mongoose.Types.ObjectId(), name: 'Booster', users: [users[0]] }; @@ -144,6 +146,7 @@ describe('model', function() { count: true }); + db.deleteModel(/User/); const User = db.model('User', UserSchema); const Story = db.model('Story', StorySchema); @@ -173,5 +176,26 @@ describe('model', function() { assert.strictEqual(hydrated.storiesCount, 2); }); + + it('sets hydrated docs as populated (gh-15048)', async function() { + const userSchema = new Schema({ + name: String + }); + const companySchema = new Schema({ + name: String, + users: [{ ref: 'User', type: Schema.Types.ObjectId }] + }); + + db.deleteModel(/User/); + const User = db.model('User', userSchema); + const Company = db.model('Company', companySchema); + + const users = [{ _id: new mongoose.Types.ObjectId(), name: 'Val' }]; + const company = { _id: new mongoose.Types.ObjectId(), name: 'Acme', users: [users[0]] }; + + const c = Company.hydrate(company, null, { hydratedPopulatedDocs: true }); + assert.ok(c.populated('users')); + assert.ok(c.users[0] instanceof User); + }); }); }); From 233e767213eedf18b10d632d778bccc87d14d938 Mon Sep 17 00:00:00 2001 From: Valeri Karpov Date: Wed, 11 Dec 2024 11:59:46 -0500 Subject: [PATCH 2/3] Update test/model.hydrate.test.js Co-authored-by: hasezoey --- test/model.hydrate.test.js | 1 + 1 file changed, 1 insertion(+) diff --git a/test/model.hydrate.test.js b/test/model.hydrate.test.js index 331f41908df..447cc2be85b 100644 --- a/test/model.hydrate.test.js +++ b/test/model.hydrate.test.js @@ -187,6 +187,7 @@ describe('model', function() { }); db.deleteModel(/User/); + db.deleteModel(/Company/); const User = db.model('User', userSchema); const Company = db.model('Company', companySchema); From 874b4f96fc6c8ce9b61808ed8f6b8e99c155d328 Mon Sep 17 00:00:00 2001 From: Valeri Karpov Date: Wed, 11 Dec 2024 12:03:14 -0500 Subject: [PATCH 3/3] docs(document): add hydratedPopulatedDocs option info and other improvements to init() docs --- lib/document.js | 7 ++++--- 1 file changed, 4 insertions(+), 3 deletions(-) diff --git a/lib/document.js b/lib/document.js index 9ae9fc8758b..85a65d355d8 100644 --- a/lib/document.js +++ b/lib/document.js @@ -624,16 +624,17 @@ Document.prototype.toBSON = function() { }; /** - * Initializes the document without setters or marking anything modified. + * Hydrates this document with the data in `doc`. Does not run setters or mark any paths modified. * - * Called internally after a document is returned from mongodb. Normally, + * Called internally after a document is returned from MongoDB. Normally, * you do **not** need to call this function on your own. * * This function triggers `init` [middleware](https://mongoosejs.com/docs/middleware.html). * Note that `init` hooks are [synchronous](https://mongoosejs.com/docs/middleware.html#synchronous). * - * @param {Object} doc document returned by mongo + * @param {Object} doc raw document returned by mongo * @param {Object} [opts] + * @param {Boolean} [opts.hydratedPopulatedDocs=false] If true, hydrate and mark as populated any paths that are populated in the raw document * @param {Function} [fn] * @api public * @memberOf Document