From 24e4d80cab202e31b773b3f71a557c32a4a12506 Mon Sep 17 00:00:00 2001 From: Mike Noseworthy Date: Sat, 29 Jan 2022 01:06:08 -0330 Subject: [PATCH] fix: set default for dotted path projection When a schema included a subdocument that had a default value of empty object and that subdocument itself included some defaults, the subdocument was always null if a query was run with a dotted path projection to the subdocument's default values. Check not only that the curent path being evaluated to have defaults applied, but also check if the current path is part of the included children in the projection. --- lib/document.js | 2 +- test/document.test.js | 62 +++++++++++++++++++++++++++++++++++++++++++ 2 files changed, 63 insertions(+), 1 deletion(-) 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'); + }); });