Skip to content

Commit 663688f

Browse files
authored
Merge pull request #14467 from Automattic/vkarpov15/gh-14414
feat(document): add `validateAllPaths` option to `validate()` and `validateSync()`
2 parents b47a9fa + 7e8ebc8 commit 663688f

File tree

3 files changed

+245
-56
lines changed

3 files changed

+245
-56
lines changed

lib/document.js

+125-54
Original file line numberDiff line numberDiff line change
@@ -2756,47 +2756,7 @@ function _getPathsToValidate(doc, pathsToValidate, pathsToSkip) {
27562756

27572757
// gh-661: if a whole array is modified, make sure to run validation on all
27582758
// the children as well
2759-
for (const path of paths) {
2760-
const _pathType = doc.$__schema.path(path);
2761-
if (!_pathType) {
2762-
continue;
2763-
}
2764-
2765-
if (!_pathType.$isMongooseArray ||
2766-
// To avoid potential performance issues, skip doc arrays whose children
2767-
// are not required. `getPositionalPathType()` may be slow, so avoid
2768-
// it unless we have a case of #6364
2769-
(!Array.isArray(_pathType) &&
2770-
_pathType.$isMongooseDocumentArray &&
2771-
!(_pathType && _pathType.schemaOptions && _pathType.schemaOptions.required))) {
2772-
continue;
2773-
}
2774-
2775-
// gh-11380: optimization. If the array isn't a document array and there's no validators
2776-
// on the array type, there's no need to run validation on the individual array elements.
2777-
if (_pathType.$isMongooseArray &&
2778-
!_pathType.$isMongooseDocumentArray && // Skip document arrays...
2779-
!_pathType.$embeddedSchemaType.$isMongooseArray && // and arrays of arrays
2780-
_pathType.$embeddedSchemaType.validators.length === 0) {
2781-
continue;
2782-
}
2783-
2784-
const val = doc.$__getValue(path);
2785-
_pushNestedArrayPaths(val, paths, path);
2786-
}
2787-
2788-
function _pushNestedArrayPaths(val, paths, path) {
2789-
if (val != null) {
2790-
const numElements = val.length;
2791-
for (let j = 0; j < numElements; ++j) {
2792-
if (Array.isArray(val[j])) {
2793-
_pushNestedArrayPaths(val[j], paths, path + '.' + j);
2794-
} else {
2795-
paths.add(path + '.' + j);
2796-
}
2797-
}
2798-
}
2799-
}
2759+
_addArrayPathsToValidate(doc, paths);
28002760

28012761
const flattenOptions = { skipArrays: true };
28022762
for (const pathToCheck of paths) {
@@ -2841,12 +2801,55 @@ function _getPathsToValidate(doc, pathsToValidate, pathsToSkip) {
28412801
return [paths, doValidateOptions];
28422802
}
28432803

2804+
function _addArrayPathsToValidate(doc, paths) {
2805+
for (const path of paths) {
2806+
const _pathType = doc.$__schema.path(path);
2807+
if (!_pathType) {
2808+
continue;
2809+
}
2810+
2811+
if (!_pathType.$isMongooseArray ||
2812+
// To avoid potential performance issues, skip doc arrays whose children
2813+
// are not required. `getPositionalPathType()` may be slow, so avoid
2814+
// it unless we have a case of #6364
2815+
(!Array.isArray(_pathType) &&
2816+
_pathType.$isMongooseDocumentArray &&
2817+
!(_pathType && _pathType.schemaOptions && _pathType.schemaOptions.required))) {
2818+
continue;
2819+
}
2820+
2821+
// gh-11380: optimization. If the array isn't a document array and there's no validators
2822+
// on the array type, there's no need to run validation on the individual array elements.
2823+
if (_pathType.$isMongooseArray &&
2824+
!_pathType.$isMongooseDocumentArray && // Skip document arrays...
2825+
!_pathType.$embeddedSchemaType.$isMongooseArray && // and arrays of arrays
2826+
_pathType.$embeddedSchemaType.validators.length === 0) {
2827+
continue;
2828+
}
2829+
2830+
const val = doc.$__getValue(path);
2831+
_pushNestedArrayPaths(val, paths, path);
2832+
}
2833+
}
2834+
2835+
function _pushNestedArrayPaths(val, paths, path) {
2836+
if (val != null) {
2837+
const numElements = val.length;
2838+
for (let j = 0; j < numElements; ++j) {
2839+
if (Array.isArray(val[j])) {
2840+
_pushNestedArrayPaths(val[j], paths, path + '.' + j);
2841+
} else {
2842+
paths.add(path + '.' + j);
2843+
}
2844+
}
2845+
}
2846+
}
2847+
28442848
/*!
28452849
* ignore
28462850
*/
28472851

28482852
Document.prototype.$__validate = function(pathsToValidate, options, callback) {
2849-
28502853
if (this.$__.saveOptions && this.$__.saveOptions.pathsToSave && !pathsToValidate) {
28512854
pathsToValidate = [...this.$__.saveOptions.pathsToSave];
28522855
} else if (typeof pathsToValidate === 'function') {
@@ -2871,6 +2874,19 @@ Document.prototype.$__validate = function(pathsToValidate, options, callback) {
28712874
shouldValidateModifiedOnly = this.$__schema.options.validateModifiedOnly;
28722875
}
28732876

2877+
const validateAllPaths = options && options.validateAllPaths;
2878+
if (validateAllPaths) {
2879+
if (pathsToSkip) {
2880+
throw new TypeError('Cannot set both `validateAllPaths` and `pathsToSkip`');
2881+
}
2882+
if (pathsToValidate) {
2883+
throw new TypeError('Cannot set both `validateAllPaths` and `pathsToValidate`');
2884+
}
2885+
if (hasValidateModifiedOnlyOption && shouldValidateModifiedOnly) {
2886+
throw new TypeError('Cannot set both `validateAllPaths` and `validateModifiedOnly`');
2887+
}
2888+
}
2889+
28742890
const _this = this;
28752891
const _complete = () => {
28762892
let validationError = this.$__.validationError;
@@ -2908,11 +2924,33 @@ Document.prototype.$__validate = function(pathsToValidate, options, callback) {
29082924
};
29092925

29102926
// only validate required fields when necessary
2911-
const pathDetails = _getPathsToValidate(this, pathsToValidate, pathsToSkip);
2912-
const paths = shouldValidateModifiedOnly ?
2913-
pathDetails[0].filter((path) => this.$isModified(path)) :
2914-
pathDetails[0];
2915-
const doValidateOptionsByPath = pathDetails[1];
2927+
let paths;
2928+
let doValidateOptionsByPath;
2929+
if (validateAllPaths) {
2930+
paths = new Set(Object.keys(this.$__schema.paths));
2931+
// gh-661: if a whole array is modified, make sure to run validation on all
2932+
// the children as well
2933+
for (const path of paths) {
2934+
const schemaType = this.$__schema.path(path);
2935+
if (!schemaType || !schemaType.$isMongooseArray) {
2936+
continue;
2937+
}
2938+
const val = this.$__getValue(path);
2939+
if (!val) {
2940+
continue;
2941+
}
2942+
_pushNestedArrayPaths(val, paths, path);
2943+
}
2944+
paths = [...paths];
2945+
doValidateOptionsByPath = {};
2946+
} else {
2947+
const pathDetails = _getPathsToValidate(this, pathsToValidate, pathsToSkip);
2948+
paths = shouldValidateModifiedOnly ?
2949+
pathDetails[0].filter((path) => this.$isModified(path)) :
2950+
pathDetails[0];
2951+
doValidateOptionsByPath = pathDetails[1];
2952+
}
2953+
29162954
if (typeof pathsToValidate === 'string') {
29172955
pathsToValidate = pathsToValidate.split(' ');
29182956
}
@@ -2993,7 +3031,8 @@ Document.prototype.$__validate = function(pathsToValidate, options, callback) {
29933031
const doValidateOptions = {
29943032
...doValidateOptionsByPath[path],
29953033
path: path,
2996-
validateModifiedOnly: shouldValidateModifiedOnly
3034+
validateModifiedOnly: shouldValidateModifiedOnly,
3035+
validateAllPaths
29973036
};
29983037

29993038
schemaType.doValidate(val, function(err) {
@@ -3111,6 +3150,16 @@ Document.prototype.validateSync = function(pathsToValidate, options) {
31113150

31123151
let pathsToSkip = options && options.pathsToSkip;
31133152

3153+
const validateAllPaths = options && options.validateAllPaths;
3154+
if (validateAllPaths) {
3155+
if (pathsToSkip) {
3156+
throw new TypeError('Cannot set both `validateAllPaths` and `pathsToSkip`');
3157+
}
3158+
if (pathsToValidate) {
3159+
throw new TypeError('Cannot set both `validateAllPaths` and `pathsToValidate`');
3160+
}
3161+
}
3162+
31143163
if (typeof pathsToValidate === 'string') {
31153164
const isOnePathOnly = pathsToValidate.indexOf(' ') === -1;
31163165
pathsToValidate = isOnePathOnly ? [pathsToValidate] : pathsToValidate.split(' ');
@@ -3119,11 +3168,32 @@ Document.prototype.validateSync = function(pathsToValidate, options) {
31193168
}
31203169

31213170
// only validate required fields when necessary
3122-
const pathDetails = _getPathsToValidate(this, pathsToValidate, pathsToSkip);
3123-
const paths = shouldValidateModifiedOnly ?
3124-
pathDetails[0].filter((path) => this.$isModified(path)) :
3125-
pathDetails[0];
3126-
const skipSchemaValidators = pathDetails[1];
3171+
let paths;
3172+
let skipSchemaValidators;
3173+
if (validateAllPaths) {
3174+
paths = new Set(Object.keys(this.$__schema.paths));
3175+
// gh-661: if a whole array is modified, make sure to run validation on all
3176+
// the children as well
3177+
for (const path of paths) {
3178+
const schemaType = this.$__schema.path(path);
3179+
if (!schemaType || !schemaType.$isMongooseArray) {
3180+
continue;
3181+
}
3182+
const val = this.$__getValue(path);
3183+
if (!val) {
3184+
continue;
3185+
}
3186+
_pushNestedArrayPaths(val, paths, path);
3187+
}
3188+
paths = [...paths];
3189+
skipSchemaValidators = {};
3190+
} else {
3191+
const pathDetails = _getPathsToValidate(this, pathsToValidate, pathsToSkip);
3192+
paths = shouldValidateModifiedOnly ?
3193+
pathDetails[0].filter((path) => this.$isModified(path)) :
3194+
pathDetails[0];
3195+
skipSchemaValidators = pathDetails[1];
3196+
}
31273197

31283198
const validating = {};
31293199

@@ -3148,7 +3218,8 @@ Document.prototype.validateSync = function(pathsToValidate, options) {
31483218
const err = p.doValidateSync(val, _this, {
31493219
skipSchemaValidators: skipSchemaValidators[path],
31503220
path: path,
3151-
validateModifiedOnly: shouldValidateModifiedOnly
3221+
validateModifiedOnly: shouldValidateModifiedOnly,
3222+
validateAllPaths
31523223
});
31533224
if (err) {
31543225
const isSubdoc = p.$isSingleNested ||

lib/schema/documentArray.js

+2-2
Original file line numberDiff line numberDiff line change
@@ -279,7 +279,7 @@ SchemaDocumentArray.prototype.doValidate = function(array, fn, scope, options) {
279279
continue;
280280
}
281281

282-
doc.$__validate(callback);
282+
doc.$__validate(null, options, callback);
283283
}
284284
}
285285
};
@@ -330,7 +330,7 @@ SchemaDocumentArray.prototype.doValidateSync = function(array, scope, options) {
330330
continue;
331331
}
332332

333-
const subdocValidateError = doc.validateSync();
333+
const subdocValidateError = doc.validateSync(options);
334334

335335
if (subdocValidateError && resultError == null) {
336336
resultError = subdocValidateError;

test/document.test.js

+118
Original file line numberDiff line numberDiff line change
@@ -13087,6 +13087,124 @@ describe('document', function() {
1308713087
const savedDoc = await MainModel.findById(doc.id).orFail();
1308813088
assert.strictEqual(savedDoc.sub, null);
1308913089
});
13090+
13091+
it('validate supports validateAllPaths', async function() {
13092+
const schema = new mongoose.Schema({
13093+
name: {
13094+
type: String,
13095+
validate: v => !!v
13096+
},
13097+
age: {
13098+
type: Number,
13099+
validate: v => v == null || v < 200
13100+
},
13101+
subdoc: {
13102+
type: new Schema({
13103+
url: String
13104+
}, { _id: false }),
13105+
validate: v => v == null || v.url.length > 0
13106+
},
13107+
docArr: [{
13108+
subprop: {
13109+
type: String,
13110+
validate: v => v == null || v.length > 0
13111+
}
13112+
}]
13113+
});
13114+
13115+
const TestModel = db.model('Test', schema);
13116+
13117+
const doc = await TestModel.create({});
13118+
doc.name = '';
13119+
doc.age = 201;
13120+
doc.subdoc = { url: '' };
13121+
doc.docArr = [{ subprop: '' }];
13122+
await doc.save({ validateBeforeSave: false });
13123+
await doc.validate();
13124+
13125+
const err = await doc.validate({ validateAllPaths: true }).then(() => null, err => err);
13126+
assert.ok(err);
13127+
assert.equal(err.name, 'ValidationError');
13128+
assert.ok(err.errors['name']);
13129+
assert.ok(
13130+
err.errors['name'].message.includes('Validator failed for path `name` with value ``'),
13131+
err.errors['name'].message
13132+
);
13133+
assert.ok(err.errors['age']);
13134+
assert.ok(
13135+
err.errors['age'].message.includes('Validator failed for path `age` with value `201`'),
13136+
err.errors['age'].message
13137+
);
13138+
assert.ok(err.errors['subdoc']);
13139+
assert.ok(
13140+
err.errors['subdoc'].message.includes('Validator failed for path `subdoc` with value `{ url: \'\' }`'),
13141+
err.errors['subdoc'].message
13142+
);
13143+
assert.ok(err.errors['docArr.0.subprop']);
13144+
assert.ok(
13145+
err.errors['docArr.0.subprop'].message.includes('Validator failed for path `subprop` with value ``'),
13146+
err.errors['docArr.0.subprop'].message
13147+
);
13148+
});
13149+
13150+
it('validateSync() supports validateAllPaths', async function() {
13151+
const schema = new mongoose.Schema({
13152+
name: {
13153+
type: String,
13154+
validate: v => !!v
13155+
},
13156+
age: {
13157+
type: Number,
13158+
validate: v => v == null || v < 200
13159+
},
13160+
subdoc: {
13161+
type: new Schema({
13162+
url: String
13163+
}, { _id: false }),
13164+
validate: v => v == null || v.url.length > 0
13165+
},
13166+
docArr: [{
13167+
subprop: {
13168+
type: String,
13169+
validate: v => v == null || v.length > 0
13170+
}
13171+
}]
13172+
});
13173+
13174+
const TestModel = db.model('Test', schema);
13175+
13176+
const doc = await TestModel.create({});
13177+
doc.name = '';
13178+
doc.age = 201;
13179+
doc.subdoc = { url: '' };
13180+
doc.docArr = [{ subprop: '' }];
13181+
await doc.save({ validateBeforeSave: false });
13182+
await doc.validate();
13183+
13184+
const err = await doc.validateSync({ validateAllPaths: true });
13185+
assert.ok(err);
13186+
assert.equal(err.name, 'ValidationError');
13187+
assert.ok(err.errors['name']);
13188+
assert.ok(
13189+
err.errors['name'].message.includes('Validator failed for path `name` with value ``'),
13190+
err.errors['name'].message
13191+
);
13192+
assert.ok(err.errors['age']);
13193+
assert.ok(
13194+
err.errors['age'].message.includes('Validator failed for path `age` with value `201`'),
13195+
err.errors['age'].message
13196+
);
13197+
assert.ok(err.errors['subdoc']);
13198+
assert.ok(
13199+
err.errors['subdoc'].message.includes('Validator failed for path `subdoc` with value `{ url: \'\' }`'),
13200+
err.errors['subdoc'].message
13201+
);
13202+
assert.ok(err.errors['docArr.0.subprop']);
13203+
assert.ok(
13204+
err.errors['docArr.0.subprop'].message.includes('Validator failed for path `subprop` with value ``'),
13205+
err.errors['docArr.0.subprop'].message
13206+
);
13207+
});
1309013208
});
1309113209

1309213210
describe('Check if instance function that is supplied in schema option is availabe', function() {

0 commit comments

Comments
 (0)