diff --git a/lib/document.js b/lib/document.js index bde713cc63c..7e2e2c8f8d2 100644 --- a/lib/document.js +++ b/lib/document.js @@ -479,7 +479,7 @@ function $__applyDefaults(doc, fields, exclude, hasIncludedChildren, isBeforeSet break; } } else if (exclude === false && fields && !included) { - if (curPath in fields) { + if (curPath in fields || type.$isSingleNested && hasIncludedChildren[curPath]) { included = true; } else if (!hasIncludedChildren[curPath]) { break; diff --git a/test/document.test.js b/test/document.test.js index 86034fd19b2..46478482676 100644 --- a/test/document.test.js +++ b/test/document.test.js @@ -10998,4 +10998,66 @@ describe('document', function() { assert.equal(typeof doc.somethingElse, 'string'); delete String.type; }); + + it('applies subdocument defaults when projecting dotted subdocument fields', async function() { + const grandChildSchema = new mongoose.Schema({ + name: { + type: mongoose.Schema.Types.String, + default: () => 'grandchild' + } + }); + + const childSchema = new mongoose.Schema({ + name: { + type: mongoose.Schema.Types.String, + default: () => 'child' + }, + grandChild: { + type: grandChildSchema, + default: () => ({}) + } + }); + + const parentSchema = new mongoose.Schema({ + name: mongoose.Schema.Types.String, + child: { + type: childSchema, + default: () => ({}) + } + }); + + const ParentModel = db.model('Parent', parentSchema); + // insert an object without mongoose adding missing defaults + const result = await db.collection('Parent').insertOne({ name: 'parent' }); + + // ensure that the defaults are populated when no projections are used + const doc = await ParentModel.findById(result.insertedId).exec(); + assert.equal(doc.name, 'parent'); + assert.equal(doc.child.name, 'child'); + assert.equal(doc.child.grandChild.name, 'grandchild'); + + // ensure that defaults are populated when using an object projection + const projectedDoc = await ParentModel.findById(result.insertedId, { + name: 1, + child: { + name: 1, + grandChild: { + name: 1 + } + } + }).exec(); + assert.equal(projectedDoc.name, 'parent'); + assert.equal(projectedDoc.child.name, 'child'); + assert.equal(projectedDoc.child.grandChild.name, 'grandchild'); + + // ensure that defaults are populated when using dotted path projections + const dottedProjectedDoc = await ParentModel.findById(result.insertedId, { + name: 1, + 'child.name': 1, + 'child.grandChild.name': 1 + }).exec(); + assert.equal(dottedProjectedDoc.name, 'parent'); + assert.equal(dottedProjectedDoc.child.name, 'child'); + assert.equal(dottedProjectedDoc.child.grandChild.name, 'grandchild'); + }); });