Skip to content
Merged
Show file tree
Hide file tree
Changes from 2 commits
Commits
File filter

Filter by extension

Filter by extension

Conversations
Failed to load comments.
Loading
Jump to
Jump to file
Failed to load files.
Loading
Diff view
Diff view
4 changes: 2 additions & 2 deletions lib/document.js
Original file line number Diff line number Diff line change
Expand Up @@ -806,8 +806,8 @@ function init(self, obj, doc, opts, prefix) {
reason: e
}));
}
} else if (opts.hydratedPopulatedDocs) {
doc[i] = schemaType.cast(value, self, true);
} else if (schemaType && opts.hydratedPopulatedDocs) {
doc[i] = schemaType.cast(value, self, true, undefined, { hydratedPopulatedDocs: true });

if (doc[i] && doc[i].$__ && doc[i].$__.wasPopulated) {
self.$populated(path, doc[i].$__.wasPopulated.value, doc[i].$__.wasPopulated.options);
Expand Down
4 changes: 2 additions & 2 deletions lib/model.js
Original file line number Diff line number Diff line change
Expand Up @@ -1461,7 +1461,7 @@ function getIndexesToDrop(schema, schemaIndexes, dbIndexes) {
* @param {Object} [options]
* @param {Array<String>} [options.toDrop] if specified, contains a list of index names to drop
* @param {Boolean} [options.hideIndexes=false] set to `true` to hide indexes instead of dropping. Requires MongoDB server 4.4 or higher
* @return {Promise<String>} list of dropped or hidden index names
* @return {Promise<Array<String>>} list of dropped or hidden index names
* @api public
*/

Expand Down Expand Up @@ -3688,7 +3688,7 @@ Model.castObject = function castObject(obj, options) {
}

if (schemaType.$isMongooseDocumentArray) {
const castNonArraysOption = schemaType.options?.castNonArrays ??schemaType.constructor.options.castNonArrays;
const castNonArraysOption = schemaType.options?.castNonArrays ?? schemaType.constructor.options.castNonArrays;
if (!Array.isArray(val)) {
if (!castNonArraysOption) {
if (!options.ignoreCastErrors) {
Expand Down
18 changes: 11 additions & 7 deletions lib/schema.js
Original file line number Diff line number Diff line change
Expand Up @@ -1939,13 +1939,11 @@ Schema.prototype.pre = function(name) {
* const Model = mongoose.model('Model', schema);
*
* const m = new Model(..);
* m.save(function(err) {
* console.log('this fires after the `post` hook');
* });
* await m.save();
* console.log('this fires after the `post` hook');
*
* m.find(function(err, docs) {
* console.log('this fires after the post find hook');
* });
* await m.find();
* console.log('this fires after the post find hook');
*
* @param {String|RegExp|String[]} methodName The method name or regular expression to match method name
* @param {Object} [options]
Expand Down Expand Up @@ -2382,9 +2380,15 @@ Schema.prototype.virtual = function(name, options) {
const PopulateModel = this.db.model(modelNames[0]);
for (let i = 0; i < populatedVal.length; ++i) {
if (!populatedVal[i].$__) {
populatedVal[i] = PopulateModel.hydrate(populatedVal[i]);
populatedVal[i] = PopulateModel.hydrate(populatedVal[i], null, { hydratedPopulatedDocs: true });
}
}
const foreignField = options.foreignField;
this.$populated(
name,
populatedVal.map(doc => doc == null ? doc : doc.get(typeof foreignField === 'function' ? foreignField.call(doc, doc) : foreignField)),
{ populateModelSymbol: PopulateModel }
);
}
}

Expand Down
3 changes: 3 additions & 0 deletions lib/schema/array.js
Original file line number Diff line number Diff line change
Expand Up @@ -403,6 +403,9 @@ SchemaArray.prototype.cast = function(value, doc, init, prev, options) {
opts.arrayPathIndex = i;
}
}
if (options.hydratedPopulatedDocs) {
opts.hydratedPopulatedDocs = options.hydratedPopulatedDocs;
}
rawValue[i] = caster.applySetters(rawValue[i], doc, init, void 0, opts);
}
} catch (e) {
Expand Down
4 changes: 2 additions & 2 deletions lib/schema/buffer.js
Original file line number Diff line number Diff line change
Expand Up @@ -140,7 +140,7 @@ SchemaBuffer.prototype.checkRequired = function(value, doc) {
* @api private
*/

SchemaBuffer.prototype.cast = function(value, doc, init) {
SchemaBuffer.prototype.cast = function(value, doc, init, prev, options) {
let ret;
if (SchemaType._isRef(this, value, doc, init)) {
if (value && value.isMongooseBuffer) {
Expand All @@ -167,7 +167,7 @@ SchemaBuffer.prototype.cast = function(value, doc, init) {
}

if (value == null || utils.isNonBuiltinObject(value)) {
return this._castRef(value, doc, init);
return this._castRef(value, doc, init, options);
}
}

Expand Down
4 changes: 2 additions & 2 deletions lib/schema/decimal128.js
Original file line number Diff line number Diff line change
Expand Up @@ -180,13 +180,13 @@ SchemaDecimal128.prototype.checkRequired = function checkRequired(value, doc) {
* @api private
*/

SchemaDecimal128.prototype.cast = function(value, doc, init) {
SchemaDecimal128.prototype.cast = function(value, doc, init, prev, options) {
if (SchemaType._isRef(this, value, doc, init)) {
if (isBsonType(value, 'Decimal128')) {
return value;
}

return this._castRef(value, doc, init);
return this._castRef(value, doc, init, options);
}

let castDecimal128;
Expand Down
4 changes: 2 additions & 2 deletions lib/schema/number.js
Original file line number Diff line number Diff line change
Expand Up @@ -354,10 +354,10 @@ SchemaNumber.prototype.enum = function(values, message) {
* @api private
*/

SchemaNumber.prototype.cast = function(value, doc, init) {
SchemaNumber.prototype.cast = function(value, doc, init, prev, options) {
if (typeof value !== 'number' && SchemaType._isRef(this, value, doc, init)) {
if (value == null || utils.isNonBuiltinObject(value)) {
return this._castRef(value, doc, init);
return this._castRef(value, doc, init, options);
}
}

Expand Down
4 changes: 2 additions & 2 deletions lib/schema/objectId.js
Original file line number Diff line number Diff line change
Expand Up @@ -223,15 +223,15 @@ SchemaObjectId.prototype.checkRequired = function checkRequired(value, doc) {
* @api private
*/

SchemaObjectId.prototype.cast = function(value, doc, init) {
SchemaObjectId.prototype.cast = function(value, doc, init, prev, options) {
if (!(isBsonType(value, 'ObjectId')) && SchemaType._isRef(this, value, doc, init)) {
// wait! we may need to cast this to a document
if ((getConstructorName(value) || '').toLowerCase() === 'objectid') {
return new oid(value.toHexString());
}

if (value == null || utils.isNonBuiltinObject(value)) {
return this._castRef(value, doc, init);
return this._castRef(value, doc, init, options);
}
}

Expand Down
4 changes: 2 additions & 2 deletions lib/schema/string.js
Original file line number Diff line number Diff line change
Expand Up @@ -586,9 +586,9 @@ SchemaString.prototype.checkRequired = function checkRequired(value, doc) {
* @api private
*/

SchemaString.prototype.cast = function(value, doc, init) {
SchemaString.prototype.cast = function(value, doc, init, prev, options) {
if (typeof value !== 'string' && SchemaType._isRef(this, value, doc, init)) {
return this._castRef(value, doc, init);
return this._castRef(value, doc, init, options);
}

let castString;
Expand Down
4 changes: 2 additions & 2 deletions lib/schema/uuid.js
Original file line number Diff line number Diff line change
Expand Up @@ -268,10 +268,10 @@ SchemaUUID.prototype.checkRequired = function checkRequired(value) {
* @api private
*/

SchemaUUID.prototype.cast = function(value, doc, init) {
SchemaUUID.prototype.cast = function(value, doc, init, prev, options) {
if (utils.isNonBuiltinObject(value) &&
SchemaType._isRef(this, value, doc, init)) {
return this._castRef(value, doc, init);
return this._castRef(value, doc, init, options);
}

let castFn;
Expand Down
4 changes: 2 additions & 2 deletions lib/schemaType.js
Original file line number Diff line number Diff line change
Expand Up @@ -1555,7 +1555,7 @@ SchemaType._isRef = function(self, value, doc, init) {
* ignore
*/

SchemaType.prototype._castRef = function _castRef(value, doc, init) {
SchemaType.prototype._castRef = function _castRef(value, doc, init, options) {
if (value == null) {
return value;
}
Expand Down Expand Up @@ -1587,7 +1587,7 @@ SchemaType.prototype._castRef = function _castRef(value, doc, init) {
!doc.$__.populated[path].options.options ||
!doc.$__.populated[path].options.options.lean) {
const PopulatedModel = pop ? pop.options[populateModelSymbol] : doc.constructor.db.model(this.options.ref);
ret = new PopulatedModel(value);
ret = PopulatedModel.hydrate(value, null, options);
ret.$__.wasPopulated = { value: ret._doc._id, options: { [populateModelSymbol]: PopulatedModel } };
}

Expand Down
124 changes: 124 additions & 0 deletions test/model.hydrate.test.js
Original file line number Diff line number Diff line change
Expand Up @@ -198,5 +198,129 @@ describe('model', function() {
assert.ok(c.populated('users'));
assert.ok(c.users[0] instanceof User);
});

it('marks deeply nested docs as hydrated underneath virtuals (gh-15110)', async function() {
const ArticleSchema = new Schema({ title: String });

const StorySchema = new Schema({
title: String,
userId: Schema.Types.ObjectId,
article: {
type: Schema.Types.ObjectId,
ref: 'Article'
}
});

const UserSchema = new Schema({
name: String
});

UserSchema.virtual('stories', {
ref: 'Story',
localField: '_id',
foreignField: 'userId'
});

db.deleteModel(/User/);
db.deleteModel(/Story/);
db.deleteModel(/Article/);
const User = db.model('User', UserSchema);
const Story = db.model('Story', StorySchema);
const Article = db.model('Article', ArticleSchema);
await Promise.all([
User.deleteMany({}),
Story.deleteMany({}),
Article.deleteMany({})
]);

const article = await Article.create({ title: 'Cinema' });
const user = await User.create({ name: 'Alex' });
await Story.create({ title: 'Ticket 1', userId: user._id, article });
await Story.create({ title: 'Ticket 2', userId: user._id });

const populated = await User.findOne({ name: 'Alex' }).populate({
path: 'stories',
populate: ['article']
}).lean();

const hydrated = User.hydrate(
JSON.parse(JSON.stringify(populated)),
null,
{ hydratedPopulatedDocs: true }
);

assert.ok(hydrated.populated('stories'));
assert.ok(hydrated.stories[0].populated('article'));
assert.equal(hydrated.stories[0].article._id.toString(), article._id.toString());
assert.ok(typeof hydrated.stories[0].article._id === 'object');
assert.ok(hydrated.stories[0].article._id instanceof mongoose.Types.ObjectId);
assert.equal(hydrated.stories[0].article.title, 'Cinema');

assert.ok(!hydrated.stories[1].article);
});

it('marks deeply nested docs as hydrated underneath conventional (gh-15110)', async function() {
const ArticleSchema = new Schema({
title: {
type: String
}
});

const StorySchema = new Schema({
title: {
type: String
},
article: {
type: Schema.Types.ObjectId,
ref: 'Article'
}
});

const UserSchema = new Schema({
name: String,
stories: [{
type: Schema.Types.ObjectId,
ref: 'Story'
}]
});

db.deleteModel(/User/);
db.deleteModel(/Story/);
db.deleteModel(/Article/);
const User = db.model('User', UserSchema);
const Story = db.model('Story', StorySchema);
const Article = db.model('Article', ArticleSchema);
await Promise.all([
User.deleteMany({}),
Story.deleteMany({}),
Article.deleteMany({})
]);

const article = await Article.create({ title: 'Cinema' });
const story1 = await Story.create({ title: 'Ticket 1', article });
const story2 = await Story.create({ title: 'Ticket 2' });

await User.create({ name: 'Alex', stories: [story1, story2] });

const populated = await User.findOne({ name: 'Alex' }).populate({
path: 'stories',
populate: ['article']
}).lean();

const hydrated = User.hydrate(
JSON.parse(JSON.stringify(populated)),
null,
{ hydratedPopulatedDocs: true }
);

assert.ok(hydrated.populated('stories'));
assert.ok(hydrated.stories[0].populated('article'));
assert.equal(hydrated.stories[0].article._id.toString(), article._id.toString());
assert.ok(typeof hydrated.stories[0].article._id === 'object');
assert.ok(hydrated.stories[0].article._id instanceof mongoose.Types.ObjectId);
assert.equal(hydrated.stories[0].article.title, 'Cinema');

assert.ok(!hydrated.stories[1].article);
});
});
});
Loading