Skip to content
Merged
Show file tree
Hide file tree
Changes from all 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
34 changes: 28 additions & 6 deletions lib/document.js
Original file line number Diff line number Diff line change
Expand Up @@ -2721,12 +2721,33 @@ function _getPathsToValidate(doc, pathsToValidate, pathsToSkip, isNestedValidate
function addToPaths(p) { paths.add(p); }

if (!isNestedValidate) {
// If we're validating a subdocument, all this logic will run anyway on the top-level document, so skip for subdocuments
const subdocs = doc.$getAllSubdocs({ useCache: true });
// If we're validating a subdocument, all this logic will run anyway on the top-level document, so skip for subdocuments.
// But only run for top-level subdocuments, because we're looking for subdocuments that are not modified at top-level but
// have a modified path. If that is the case, we will run validation on the top-level subdocument, and via that run validation
// on any subdocuments down to the modified path.
const topLevelSubdocs = [];
for (const path of Object.keys(doc.$__schema.paths)) {
const schemaType = doc.$__schema.path(path);
if (schemaType.$isSingleNested) {
const subdoc = doc.$get(path);
if (subdoc) {
topLevelSubdocs.push(subdoc);
}
} else if (schemaType.$isMongooseDocumentArray) {
const arr = doc.$get(path);
if (arr && arr.length) {
for (const subdoc of arr) {
if (subdoc) {
topLevelSubdocs.push(subdoc);
}
}
}
}
}
const modifiedPaths = doc.modifiedPaths();
for (const subdoc of subdocs) {
for (const subdoc of topLevelSubdocs) {
if (subdoc.$basePath) {
const fullPathToSubdoc = subdoc.$isSingleNested ? subdoc.$__pathRelativeToParent() : subdoc.$__fullPathWithIndexes();
const fullPathToSubdoc = subdoc.$__pathRelativeToParent();

// Remove child paths for now, because we'll be validating the whole
// subdoc.
Expand All @@ -2736,11 +2757,12 @@ function _getPathsToValidate(doc, pathsToValidate, pathsToSkip, isNestedValidate
paths.delete(fullPathToSubdoc + '.' + modifiedPath);
}

const subdocParent = subdoc.$parent();
if (doc.$isModified(fullPathToSubdoc, null, modifiedPaths) &&
// Avoid using isDirectModified() here because that does additional checks on whether the parent path
// is direct modified, which can cause performance issues re: gh-14897
!doc.$__.activePaths.getStatePaths('modify').hasOwnProperty(fullPathToSubdoc) &&
!doc.$isDefault(fullPathToSubdoc)) {
!subdocParent.$__.activePaths.getStatePaths('modify').hasOwnProperty(fullPathToSubdoc) &&
!subdocParent.$isDefault(fullPathToSubdoc)) {
paths.add(fullPathToSubdoc);

if (doc.$__.pathsToScopes == null) {
Expand Down
47 changes: 47 additions & 0 deletions test/document.test.js
Original file line number Diff line number Diff line change
Expand Up @@ -14455,6 +14455,53 @@ describe('document', function() {
assert.strictEqual(car.vin, undefined);
assert.strictEqual(car.sunroof, true);
});

it('avoids double validating document arrays underneath single nested (gh-15335)', async function() {
let arraySubdocValidateCalls = 0;
let strValidateCalls = 0;

const embeddedSchema = new mongoose.Schema({
arrObj: {
type: [{
name: {
type: String,
validate: {
validator: () => {
++arraySubdocValidateCalls;
return true;
}
}
}
}]
},
arrStr: {
type: [{
type: String,
validate: {
validator: () => {
++strValidateCalls;
return true;
}
}
}]
}
});

const TestModel = db.model('Test', new Schema({ child: embeddedSchema }));
await TestModel.create({
child: {
arrObj: [
{
name: 'arrObj'
}
],
arrStr: ['arrStr']
}
});
assert.strictEqual(arraySubdocValidateCalls, 1);
assert.strictEqual(strValidateCalls, 1);

});
});

describe('Check if instance function that is supplied in schema option is available', function() {
Expand Down