diff --git a/lib/helpers/populate/getModelsMapForPopulate.js b/lib/helpers/populate/getModelsMapForPopulate.js index d767c369578..e554f4810a8 100644 --- a/lib/helpers/populate/getModelsMapForPopulate.js +++ b/lib/helpers/populate/getModelsMapForPopulate.js @@ -207,6 +207,7 @@ module.exports = function getModelsMapForPopulate(model, docs, options) { let isRefPath = false; let justOne = null; + const originalSchema = schema; if (schema && schema.instance === 'Array') { schema = schema.caster; } @@ -277,7 +278,9 @@ module.exports = function getModelsMapForPopulate(model, docs, options) { schemaForCurrentDoc = schema; } - if (schemaForCurrentDoc != null) { + if (originalSchema && originalSchema.path.endsWith('.$*')) { + justOne = !originalSchema.$isMongooseArray && !originalSchema._arrayPath; + } else if (schemaForCurrentDoc != null) { justOne = !schemaForCurrentDoc.$isMongooseArray && !schemaForCurrentDoc._arrayPath; } diff --git a/lib/model.js b/lib/model.js index 8fd7bdfbdd2..27dd89ee5a7 100644 --- a/lib/model.js +++ b/lib/model.js @@ -4784,7 +4784,11 @@ function populate(model, docs, options, callback) { for (const val of vals) { mod.options._childDocs.push(val); } - _assign(model, vals, mod, assignmentOpts); + try { + _assign(model, vals, mod, assignmentOpts); + } catch (err) { + return callback(err); + } } for (const arr of params) { diff --git a/lib/types/map.js b/lib/types/map.js index 6516d2e4038..4c571160fb8 100644 --- a/lib/types/map.js +++ b/lib/types/map.js @@ -1,6 +1,7 @@ 'use strict'; const Mixed = require('../schema/mixed'); +const MongooseError = require('../error/mongooseError'); const clone = require('../helpers/clone'); const deepEqual = require('../utils').deepEqual; const getConstructorName = require('../helpers/getConstructorName'); @@ -102,6 +103,12 @@ class MongooseMap extends Map { const priorVal = this.get(key); if (populated != null) { + if (this.$__schemaType.$isSingleNested) { + throw new MongooseError( + 'Cannot manually populate single nested subdoc underneath Map ' + + `at path "${this.$__path}". Try using an array instead of a Map.` + ); + } if (Array.isArray(value) && this.$__schemaType.$isMongooseArray) { value = value.map(v => { if (v.$__ == null) { diff --git a/test/document.test.js b/test/document.test.js index df17aa9da77..1a757bbde32 100644 --- a/test/document.test.js +++ b/test/document.test.js @@ -11016,6 +11016,54 @@ describe('document', function() { assert.equal(foo.get('bar.another'), 2); }); + it('populating subdocument refs underneath maps throws (gh-12494) (gh-10856)', async function() { + // Bar model, has a name property and some other properties that we are interested in + const BarSchema = new Schema({ + name: String, + more: String, + another: Number + }); + const Bar = db.model('Bar', BarSchema); + + // Denormalised Bar schema with just the name, for use on the Foo model + const BarNameSchema = new Schema({ + _id: { + type: Schema.Types.ObjectId, + ref: 'Bar' + }, + name: String + }); + + // Foo model, which contains denormalized bar data (just the name) + const FooSchema = new Schema({ + something: String, + other: Number, + map: { + type: Map, + of: { + type: BarNameSchema, + ref: 'Bar' + } + } + }); + const Foo = db.model('Foo', FooSchema); + + const bar = await Bar.create({ + name: 'I am Bar', + more: 'With more data', + another: 2 + }); + const { _id } = await Foo.create({ + something: 'I am Foo', + other: 1, + map: { test: bar } + }); + + const err = await Foo.findById(_id).populate('map').then(() => null, err => err); + assert.ok(err); + assert.ok(err.message.includes('Cannot manually populate single nested subdoc underneath Map'), err.message); + }); + it('handles save with undefined nested doc under subdoc (gh-11110)', async function() { const testSchema = new Schema({ level_1_array: [new Schema({ diff --git a/test/types.map.test.js b/test/types.map.test.js index c3415a6c3bf..fe895ed228d 100644 --- a/test/types.map.test.js +++ b/test/types.map.test.js @@ -1080,13 +1080,24 @@ describe('Map', function() { } }).save(); - const query = UserModel.findById(_id); - + // Using `.$*` + let query = UserModel.findById(_id); query.populate({ path: 'addresses.$*' }); - const doc = await query.exec(); + let doc = await query.exec(); + assert.ok(Array.isArray(doc.addresses.get('home'))); + assert.equal(doc.addresses.get('home').length, 1); + assert.equal(doc.addresses.get('home')[0].city, 'London'); + + // Populating just one path in the map + query = UserModel.findById(_id); + query.populate({ + path: 'addresses.home' + }); + + doc = await query.exec(); assert.ok(Array.isArray(doc.addresses.get('home'))); assert.equal(doc.addresses.get('home').length, 1); assert.equal(doc.addresses.get('home')[0].city, 'London');