diff --git a/lib/document.js b/lib/document.js index 234e32f18d4..fcfb6026ec6 100644 --- a/lib/document.js +++ b/lib/document.js @@ -2756,47 +2756,7 @@ function _getPathsToValidate(doc, pathsToValidate, pathsToSkip) { // gh-661: if a whole array is modified, make sure to run validation on all // the children as well - for (const path of paths) { - const _pathType = doc.$__schema.path(path); - if (!_pathType) { - continue; - } - - if (!_pathType.$isMongooseArray || - // To avoid potential performance issues, skip doc arrays whose children - // are not required. `getPositionalPathType()` may be slow, so avoid - // it unless we have a case of #6364 - (!Array.isArray(_pathType) && - _pathType.$isMongooseDocumentArray && - !(_pathType && _pathType.schemaOptions && _pathType.schemaOptions.required))) { - continue; - } - - // gh-11380: optimization. If the array isn't a document array and there's no validators - // on the array type, there's no need to run validation on the individual array elements. - if (_pathType.$isMongooseArray && - !_pathType.$isMongooseDocumentArray && // Skip document arrays... - !_pathType.$embeddedSchemaType.$isMongooseArray && // and arrays of arrays - _pathType.$embeddedSchemaType.validators.length === 0) { - continue; - } - - const val = doc.$__getValue(path); - _pushNestedArrayPaths(val, paths, path); - } - - function _pushNestedArrayPaths(val, paths, path) { - if (val != null) { - const numElements = val.length; - for (let j = 0; j < numElements; ++j) { - if (Array.isArray(val[j])) { - _pushNestedArrayPaths(val[j], paths, path + '.' + j); - } else { - paths.add(path + '.' + j); - } - } - } - } + _addArrayPathsToValidate(doc, paths); const flattenOptions = { skipArrays: true }; for (const pathToCheck of paths) { @@ -2841,12 +2801,55 @@ function _getPathsToValidate(doc, pathsToValidate, pathsToSkip) { return [paths, doValidateOptions]; } +function _addArrayPathsToValidate(doc, paths) { + for (const path of paths) { + const _pathType = doc.$__schema.path(path); + if (!_pathType) { + continue; + } + + if (!_pathType.$isMongooseArray || + // To avoid potential performance issues, skip doc arrays whose children + // are not required. `getPositionalPathType()` may be slow, so avoid + // it unless we have a case of #6364 + (!Array.isArray(_pathType) && + _pathType.$isMongooseDocumentArray && + !(_pathType && _pathType.schemaOptions && _pathType.schemaOptions.required))) { + continue; + } + + // gh-11380: optimization. If the array isn't a document array and there's no validators + // on the array type, there's no need to run validation on the individual array elements. + if (_pathType.$isMongooseArray && + !_pathType.$isMongooseDocumentArray && // Skip document arrays... + !_pathType.$embeddedSchemaType.$isMongooseArray && // and arrays of arrays + _pathType.$embeddedSchemaType.validators.length === 0) { + continue; + } + + const val = doc.$__getValue(path); + _pushNestedArrayPaths(val, paths, path); + } +} + +function _pushNestedArrayPaths(val, paths, path) { + if (val != null) { + const numElements = val.length; + for (let j = 0; j < numElements; ++j) { + if (Array.isArray(val[j])) { + _pushNestedArrayPaths(val[j], paths, path + '.' + j); + } else { + paths.add(path + '.' + j); + } + } + } +} + /*! * ignore */ Document.prototype.$__validate = function(pathsToValidate, options, callback) { - if (this.$__.saveOptions && this.$__.saveOptions.pathsToSave && !pathsToValidate) { pathsToValidate = [...this.$__.saveOptions.pathsToSave]; } else if (typeof pathsToValidate === 'function') { @@ -2871,6 +2874,19 @@ Document.prototype.$__validate = function(pathsToValidate, options, callback) { shouldValidateModifiedOnly = this.$__schema.options.validateModifiedOnly; } + const validateAllPaths = options && options.validateAllPaths; + if (validateAllPaths) { + if (pathsToSkip) { + throw new TypeError('Cannot set both `validateAllPaths` and `pathsToSkip`'); + } + if (pathsToValidate) { + throw new TypeError('Cannot set both `validateAllPaths` and `pathsToValidate`'); + } + if (hasValidateModifiedOnlyOption && shouldValidateModifiedOnly) { + throw new TypeError('Cannot set both `validateAllPaths` and `validateModifiedOnly`'); + } + } + const _this = this; const _complete = () => { let validationError = this.$__.validationError; @@ -2908,11 +2924,33 @@ Document.prototype.$__validate = function(pathsToValidate, options, callback) { }; // only validate required fields when necessary - const pathDetails = _getPathsToValidate(this, pathsToValidate, pathsToSkip); - const paths = shouldValidateModifiedOnly ? - pathDetails[0].filter((path) => this.$isModified(path)) : - pathDetails[0]; - const doValidateOptionsByPath = pathDetails[1]; + let paths; + let doValidateOptionsByPath; + if (validateAllPaths) { + paths = new Set(Object.keys(this.$__schema.paths)); + // gh-661: if a whole array is modified, make sure to run validation on all + // the children as well + for (const path of paths) { + const schemaType = this.$__schema.path(path); + if (!schemaType || !schemaType.$isMongooseArray) { + continue; + } + const val = this.$__getValue(path); + if (!val) { + continue; + } + _pushNestedArrayPaths(val, paths, path); + } + paths = [...paths]; + doValidateOptionsByPath = {}; + } else { + const pathDetails = _getPathsToValidate(this, pathsToValidate, pathsToSkip); + paths = shouldValidateModifiedOnly ? + pathDetails[0].filter((path) => this.$isModified(path)) : + pathDetails[0]; + doValidateOptionsByPath = pathDetails[1]; + } + if (typeof pathsToValidate === 'string') { pathsToValidate = pathsToValidate.split(' '); } @@ -2993,7 +3031,8 @@ Document.prototype.$__validate = function(pathsToValidate, options, callback) { const doValidateOptions = { ...doValidateOptionsByPath[path], path: path, - validateModifiedOnly: shouldValidateModifiedOnly + validateModifiedOnly: shouldValidateModifiedOnly, + validateAllPaths }; schemaType.doValidate(val, function(err) { @@ -3111,6 +3150,16 @@ Document.prototype.validateSync = function(pathsToValidate, options) { let pathsToSkip = options && options.pathsToSkip; + const validateAllPaths = options && options.validateAllPaths; + if (validateAllPaths) { + if (pathsToSkip) { + throw new TypeError('Cannot set both `validateAllPaths` and `pathsToSkip`'); + } + if (pathsToValidate) { + throw new TypeError('Cannot set both `validateAllPaths` and `pathsToValidate`'); + } + } + if (typeof pathsToValidate === 'string') { const isOnePathOnly = pathsToValidate.indexOf(' ') === -1; pathsToValidate = isOnePathOnly ? [pathsToValidate] : pathsToValidate.split(' '); @@ -3119,11 +3168,32 @@ Document.prototype.validateSync = function(pathsToValidate, options) { } // only validate required fields when necessary - const pathDetails = _getPathsToValidate(this, pathsToValidate, pathsToSkip); - const paths = shouldValidateModifiedOnly ? - pathDetails[0].filter((path) => this.$isModified(path)) : - pathDetails[0]; - const skipSchemaValidators = pathDetails[1]; + let paths; + let skipSchemaValidators; + if (validateAllPaths) { + paths = new Set(Object.keys(this.$__schema.paths)); + // gh-661: if a whole array is modified, make sure to run validation on all + // the children as well + for (const path of paths) { + const schemaType = this.$__schema.path(path); + if (!schemaType || !schemaType.$isMongooseArray) { + continue; + } + const val = this.$__getValue(path); + if (!val) { + continue; + } + _pushNestedArrayPaths(val, paths, path); + } + paths = [...paths]; + skipSchemaValidators = {}; + } else { + const pathDetails = _getPathsToValidate(this, pathsToValidate, pathsToSkip); + paths = shouldValidateModifiedOnly ? + pathDetails[0].filter((path) => this.$isModified(path)) : + pathDetails[0]; + skipSchemaValidators = pathDetails[1]; + } const validating = {}; @@ -3148,7 +3218,8 @@ Document.prototype.validateSync = function(pathsToValidate, options) { const err = p.doValidateSync(val, _this, { skipSchemaValidators: skipSchemaValidators[path], path: path, - validateModifiedOnly: shouldValidateModifiedOnly + validateModifiedOnly: shouldValidateModifiedOnly, + validateAllPaths }); if (err) { const isSubdoc = p.$isSingleNested || diff --git a/lib/schema/documentArray.js b/lib/schema/documentArray.js index 5e5a9b4b592..50ae0e12a6b 100644 --- a/lib/schema/documentArray.js +++ b/lib/schema/documentArray.js @@ -279,7 +279,7 @@ SchemaDocumentArray.prototype.doValidate = function(array, fn, scope, options) { continue; } - doc.$__validate(callback); + doc.$__validate(null, options, callback); } } }; @@ -330,7 +330,7 @@ SchemaDocumentArray.prototype.doValidateSync = function(array, scope, options) { continue; } - const subdocValidateError = doc.validateSync(); + const subdocValidateError = doc.validateSync(options); if (subdocValidateError && resultError == null) { resultError = subdocValidateError; diff --git a/test/document.test.js b/test/document.test.js index 73361a49574..f5d1a0b1fc6 100644 --- a/test/document.test.js +++ b/test/document.test.js @@ -13087,6 +13087,124 @@ describe('document', function() { const savedDoc = await MainModel.findById(doc.id).orFail(); assert.strictEqual(savedDoc.sub, null); }); + + it('validate supports validateAllPaths', async function() { + const schema = new mongoose.Schema({ + name: { + type: String, + validate: v => !!v + }, + age: { + type: Number, + validate: v => v == null || v < 200 + }, + subdoc: { + type: new Schema({ + url: String + }, { _id: false }), + validate: v => v == null || v.url.length > 0 + }, + docArr: [{ + subprop: { + type: String, + validate: v => v == null || v.length > 0 + } + }] + }); + + const TestModel = db.model('Test', schema); + + const doc = await TestModel.create({}); + doc.name = ''; + doc.age = 201; + doc.subdoc = { url: '' }; + doc.docArr = [{ subprop: '' }]; + await doc.save({ validateBeforeSave: false }); + await doc.validate(); + + const err = await doc.validate({ validateAllPaths: true }).then(() => null, err => err); + assert.ok(err); + assert.equal(err.name, 'ValidationError'); + assert.ok(err.errors['name']); + assert.ok( + err.errors['name'].message.includes('Validator failed for path `name` with value ``'), + err.errors['name'].message + ); + assert.ok(err.errors['age']); + assert.ok( + err.errors['age'].message.includes('Validator failed for path `age` with value `201`'), + err.errors['age'].message + ); + assert.ok(err.errors['subdoc']); + assert.ok( + err.errors['subdoc'].message.includes('Validator failed for path `subdoc` with value `{ url: \'\' }`'), + err.errors['subdoc'].message + ); + assert.ok(err.errors['docArr.0.subprop']); + assert.ok( + err.errors['docArr.0.subprop'].message.includes('Validator failed for path `subprop` with value ``'), + err.errors['docArr.0.subprop'].message + ); + }); + + it('validateSync() supports validateAllPaths', async function() { + const schema = new mongoose.Schema({ + name: { + type: String, + validate: v => !!v + }, + age: { + type: Number, + validate: v => v == null || v < 200 + }, + subdoc: { + type: new Schema({ + url: String + }, { _id: false }), + validate: v => v == null || v.url.length > 0 + }, + docArr: [{ + subprop: { + type: String, + validate: v => v == null || v.length > 0 + } + }] + }); + + const TestModel = db.model('Test', schema); + + const doc = await TestModel.create({}); + doc.name = ''; + doc.age = 201; + doc.subdoc = { url: '' }; + doc.docArr = [{ subprop: '' }]; + await doc.save({ validateBeforeSave: false }); + await doc.validate(); + + const err = await doc.validateSync({ validateAllPaths: true }); + assert.ok(err); + assert.equal(err.name, 'ValidationError'); + assert.ok(err.errors['name']); + assert.ok( + err.errors['name'].message.includes('Validator failed for path `name` with value ``'), + err.errors['name'].message + ); + assert.ok(err.errors['age']); + assert.ok( + err.errors['age'].message.includes('Validator failed for path `age` with value `201`'), + err.errors['age'].message + ); + assert.ok(err.errors['subdoc']); + assert.ok( + err.errors['subdoc'].message.includes('Validator failed for path `subdoc` with value `{ url: \'\' }`'), + err.errors['subdoc'].message + ); + assert.ok(err.errors['docArr.0.subprop']); + assert.ok( + err.errors['docArr.0.subprop'].message.includes('Validator failed for path `subprop` with value ``'), + err.errors['docArr.0.subprop'].message + ); + }); }); describe('Check if instance function that is supplied in schema option is availabe', function() {