Skip to content

Commit bbb234d

Browse files
authoredJan 31, 2020
Merge pull request #8548 from taxilian/feature/single_validate_subpaths_and_instrumentation
Feature/single validate subpaths and instrumentation
2 parents 3f1f1de + 01fd34f commit bbb234d

7 files changed

+100
-49
lines changed
 

‎lib/document.js

+61-41
Original file line numberDiff line numberDiff line change
@@ -2030,9 +2030,12 @@ Document.prototype.validate = function(pathsToValidate, options, callback) {
20302030
let parallelValidate;
20312031

20322032
if (this.$__.validating) {
2033-
parallelValidate = new ParallelValidateError(this);
2033+
parallelValidate = new ParallelValidateError(this, {
2034+
parentStack: options && options.parentStack,
2035+
conflictStack: this.$__.validating.stack
2036+
});
20342037
} else {
2035-
this.$__.validating = new ParallelValidateError(this);
2038+
this.$__.validating = new ParallelValidateError(this, {parentStack: options && options.parentStack});
20362039
}
20372040

20382041
if (typeof pathsToValidate === 'function') {
@@ -2076,53 +2079,54 @@ function _evaluateRequiredFunctions(doc) {
20762079
*/
20772080

20782081
function _getPathsToValidate(doc) {
2079-
let i;
2080-
let len;
20812082
const skipSchemaValidators = {};
20822083

20832084
_evaluateRequiredFunctions(doc);
20842085

20852086
// only validate required fields when necessary
2086-
let paths = Object.keys(doc.$__.activePaths.states.require).filter(function(path) {
2087+
let paths = new Set(Object.keys(doc.$__.activePaths.states.require).filter(function(path) {
20872088
if (!doc.isSelected(path) && !doc.isModified(path)) {
20882089
return false;
20892090
}
20902091
if (path in doc.$__.cachedRequired) {
20912092
return doc.$__.cachedRequired[path];
20922093
}
20932094
return true;
2094-
});
2095+
}));
2096+
20952097

2096-
paths = paths.concat(Object.keys(doc.$__.activePaths.states.init));
2097-
paths = paths.concat(Object.keys(doc.$__.activePaths.states.modify));
2098-
paths = paths.concat(Object.keys(doc.$__.activePaths.states.default));
2098+
function addToPaths(p) { paths.add(p); }
2099+
Object.keys(doc.$__.activePaths.states.init).forEach(addToPaths);
2100+
Object.keys(doc.$__.activePaths.states.modify).forEach(addToPaths);
2101+
Object.keys(doc.$__.activePaths.states.default).forEach(addToPaths);
20992102

21002103
const subdocs = doc.$__getAllSubdocs();
2101-
let subdoc;
2102-
len = subdocs.length;
21032104
const modifiedPaths = doc.modifiedPaths();
2104-
for (i = 0; i < len; ++i) {
2105-
subdoc = subdocs[i];
2106-
if (subdoc.$basePath &&
2107-
doc.isModified(subdoc.$basePath, modifiedPaths) &&
2108-
!doc.isDirectModified(subdoc.$basePath) &&
2109-
!doc.$isDefault(subdoc.$basePath)) {
2105+
for (const subdoc of subdocs) {
2106+
if (subdoc.$basePath) {
21102107
// Remove child paths for now, because we'll be validating the whole
21112108
// subdoc
2112-
paths = paths.filter(function(p) {
2113-
return p != null && p.indexOf(subdoc.$basePath + '.') !== 0;
2114-
});
2115-
paths.push(subdoc.$basePath); // This can cause duplicates, make unique below
2116-
skipSchemaValidators[subdoc.$basePath] = true;
2109+
for (const p of paths) {
2110+
if (p === null || p.startsWith(subdoc.$basePath + '.')) {
2111+
paths.delete(p);
2112+
}
2113+
}
2114+
2115+
if (doc.isModified(subdoc.$basePath, modifiedPaths) &&
2116+
!doc.isDirectModified(subdoc.$basePath) &&
2117+
!doc.$isDefault(subdoc.$basePath)) {
2118+
paths.add(subdoc.$basePath);
2119+
2120+
skipSchemaValidators[subdoc.$basePath] = true;
2121+
}
21172122
}
21182123
}
21192124

2125+
// from here on we're not removing items from paths
2126+
21202127
// gh-661: if a whole array is modified, make sure to run validation on all
21212128
// the children as well
2122-
len = paths.length;
2123-
for (i = 0; i < len; ++i) {
2124-
const path = paths[i];
2125-
2129+
for (const path of paths) {
21262130
const _pathType = doc.schema.path(path);
21272131
if (!_pathType ||
21282132
!_pathType.$isMongooseArray ||
@@ -2144,34 +2148,33 @@ function _getPathsToValidate(doc) {
21442148
if (Array.isArray(val[j])) {
21452149
_pushNestedArrayPaths(val[j], paths, path + '.' + j);
21462150
} else {
2147-
paths.push(path + '.' + j);
2151+
paths.add(path + '.' + j);
21482152
}
21492153
}
21502154
}
21512155
}
21522156

21532157
const flattenOptions = { skipArrays: true };
2154-
len = paths.length;
2155-
for (i = 0; i < len; ++i) {
2156-
const pathToCheck = paths[i];
2158+
for (const pathToCheck of paths) {
21572159
if (doc.schema.nested[pathToCheck]) {
21582160
let _v = doc.$__getValue(pathToCheck);
21592161
if (isMongooseObject(_v)) {
21602162
_v = _v.toObject({ transform: false });
21612163
}
21622164
const flat = flatten(_v, pathToCheck, flattenOptions, doc.schema);
2163-
paths = paths.concat(Object.keys(flat));
2165+
Object.keys(flat).forEach(addToPaths);
21642166
}
21652167
}
21662168

2167-
// Single nested paths (paths embedded under single nested subdocs) will
2168-
// be validated on their own when we call `validate()` on the subdoc itself.
2169-
// Re: gh-8468
2170-
paths = paths.filter(p => !doc.schema.singleNestedPaths.hasOwnProperty(p));
21712169

2172-
len = paths.length;
2173-
for (i = 0; i < len; ++i) {
2174-
const path = paths[i];
2170+
for (const path of paths) {
2171+
// Single nested paths (paths embedded under single nested subdocs) will
2172+
// be validated on their own when we call `validate()` on the subdoc itself.
2173+
// Re: gh-8468
2174+
if (doc.schema.singleNestedPaths.hasOwnProperty(path)) {
2175+
paths.delete(path);
2176+
continue;
2177+
}
21752178
const _pathType = doc.schema.path(path);
21762179
if (!_pathType || !_pathType.$isSchemaMap) {
21772180
continue;
@@ -2182,11 +2185,11 @@ function _getPathsToValidate(doc) {
21822185
continue;
21832186
}
21842187
for (const key of val.keys()) {
2185-
paths.push(path + '.' + key);
2188+
paths.add(path + '.' + key);
21862189
}
21872190
}
21882191

2189-
paths = Array.from(new Set(paths));
2192+
paths = Array.from(paths);
21902193
return [paths, skipSchemaValidators];
21912194
}
21922195

@@ -2208,6 +2211,15 @@ Document.prototype.$__validate = function(pathsToValidate, options, callback) {
22082211
(typeof options === 'object') &&
22092212
('validateModifiedOnly' in options);
22102213

2214+
const saveDeepStacktrace = (
2215+
options &&
2216+
(typeof options === 'object') &&
2217+
!!options['deepStackTrace'])
2218+
|| !!this.schema.options.deepStackTrace;
2219+
const parentStack = saveDeepStacktrace && options &&
2220+
(typeof options === 'object') &&
2221+
options.parentStack || [];
2222+
22112223
let shouldValidateModifiedOnly;
22122224
if (hasValidateModifiedOnlyOption) {
22132225
shouldValidateModifiedOnly = !!options.validateModifiedOnly;
@@ -2294,6 +2306,9 @@ Document.prototype.$__validate = function(pathsToValidate, options, callback) {
22942306
validated[path] = true;
22952307
total++;
22962308

2309+
const stackToHere = saveDeepStacktrace ?
2310+
[(new Error().stack), path].concat(parentStack) : void 0;
2311+
22972312
process.nextTick(function() {
22982313
const p = _this.schema.path(path);
22992314

@@ -2320,6 +2335,11 @@ Document.prototype.$__validate = function(pathsToValidate, options, callback) {
23202335
_this.$__.pathsToScopes[path] :
23212336
_this;
23222337

2338+
const doValidateOptions = {
2339+
skipSchemaValidators: skipSchemaValidators[path],
2340+
path: path,
2341+
parentStack: stackToHere
2342+
};
23232343
p.doValidate(val, function(err) {
23242344
if (err && (!p.$isMongooseDocumentArray || err.$isArrayValidatorError)) {
23252345
if (p.$isSingleNested &&
@@ -2330,7 +2350,7 @@ Document.prototype.$__validate = function(pathsToValidate, options, callback) {
23302350
_this.invalidate(path, err, undefined, true);
23312351
}
23322352
--total || complete();
2333-
}, scope, { skipSchemaValidators: skipSchemaValidators[path], path: path });
2353+
}, scope, doValidateOptions);
23342354
});
23352355
};
23362356

‎lib/error/parallelValidate.js

+9-1
Original file line numberDiff line numberDiff line change
@@ -13,10 +13,18 @@ const MongooseError = require('./mongooseError');
1313
* @api private
1414
*/
1515

16-
function ParallelValidateError(doc) {
16+
function ParallelValidateError(doc, opts) {
1717
const msg = 'Can\'t validate() the same doc multiple times in parallel. Document: ';
1818
MongooseError.call(this, msg + doc._id);
1919
this.name = 'ParallelValidateError';
20+
if (opts && opts.parentStack) {
21+
// Provide a full async stack, most recent first
22+
this.stack = this.stack + '\n\n' + opts.parentStack.join('\n\n');
23+
}
24+
// You need to know to look for this, but having it can be very helpful
25+
// for tracking down issues when combined with the deepStackTrace schema
26+
// option
27+
this.conflictStack = opts && opts.conflictStack;
2028
}
2129

2230
/*!

‎lib/schema.js

+3
Original file line numberDiff line numberDiff line change
@@ -402,6 +402,9 @@ Schema.prototype.defaultOptions = function(options) {
402402
strict: 'strict' in baseOptions ? baseOptions.strict : true,
403403
bufferCommands: true,
404404
capped: false, // { size, max, autoIndexId }
405+
// Setting to true may help with debugging but will have performance
406+
// consequences
407+
deepStackTrace: false,
405408
versionKey: '__v',
406409
discriminatorKey: '__t',
407410
minimize: true,

‎lib/schema/SingleNestedPath.js

+15-4
Original file line numberDiff line numberDiff line change
@@ -237,9 +237,17 @@ SingleNestedPath.prototype.doValidate = function(value, fn, scope, options) {
237237
if (!(value instanceof Constructor)) {
238238
value = new Constructor(value, null, scope);
239239
}
240-
241-
return value.validate(fn);
240+
try {
241+
return value.validate(fn);
242+
} catch (err) {
243+
// Save the parent stack on the error
244+
if (options.parentStack) {
245+
err.parentStack = options.parentStack;
246+
}
247+
throw err;
248+
}
242249
}
250+
const parentStack = options && options.parentStack;
243251

244252
SchemaType.prototype.doValidate.call(this, value, function(error) {
245253
if (error) {
@@ -249,8 +257,11 @@ SingleNestedPath.prototype.doValidate = function(value, fn, scope, options) {
249257
return fn(null);
250258
}
251259

252-
value.validate(fn);
253-
}, scope);
260+
value.validate(fn, {
261+
deepStackTrace: !!parentStack,
262+
parentStack: parentStack,
263+
});
264+
}, scope, options);
254265
};
255266

256267
/**

‎lib/schema/documentarray.js

+5-2
Original file line numberDiff line numberDiff line change
@@ -197,7 +197,7 @@ DocumentArrayPath.prototype.doValidate = function(array, fn, scope, options) {
197197

198198
const _this = this;
199199
try {
200-
SchemaType.prototype.doValidate.call(this, array, cb, scope);
200+
SchemaType.prototype.doValidate.call(this, array, cb, scope, options);
201201
} catch (err) {
202202
err.$isArrayValidatorError = true;
203203
return fn(err);
@@ -251,7 +251,10 @@ DocumentArrayPath.prototype.doValidate = function(array, fn, scope, options) {
251251
doc = array[i] = new Constructor(doc, array, undefined, undefined, i);
252252
}
253253

254-
doc.$__validate(callback);
254+
doc.$__validate(null, {
255+
parentStack: options && options.parentStack,
256+
deepStackTrace: !!(options && options.parentStack)
257+
}, callback);
255258
}
256259
}
257260
};

‎lib/schematype.js

+6
Original file line numberDiff line numberDiff line change
@@ -1060,6 +1060,9 @@ SchemaType.prototype.doValidate = function(value, fn, scope, options) {
10601060
const ErrorConstructor = validatorProperties.ErrorConstructor || ValidatorError;
10611061
err = new ErrorConstructor(validatorProperties);
10621062
err[validatorErrorSymbol] = true;
1063+
if (options && options.parentStack) {
1064+
err.parentStack = options.parentStack;
1065+
}
10631066
immediate(function() {
10641067
fn(err);
10651068
});
@@ -1101,6 +1104,9 @@ SchemaType.prototype.doValidate = function(value, fn, scope, options) {
11011104
if (error.message) {
11021105
validatorProperties.message = error.message;
11031106
}
1107+
if (options && options.parentStack) {
1108+
validatorProperties.deepStack = options.parentStack;
1109+
}
11041110
}
11051111
if (ok != null && typeof ok.then === 'function') {
11061112
ok.then(

‎test/document.test.js

+1-1
Original file line numberDiff line numberDiff line change
@@ -586,7 +586,7 @@ describe('document', function() {
586586
const Model = db.model('gh8468-2', Schema({
587587
name: String,
588588
keys: [keySchema],
589-
}));
589+
}, {deepStackTrace: true}));
590590
const doc = new Model({
591591
name: 'test',
592592
keys: [

0 commit comments

Comments
 (0)
Please sign in to comment.