Skip to content

Commit 7803d80

Browse files
authored
Merge pull request #14470 from Automattic/8.3
8.3
2 parents 3aba5bf + 52d1486 commit 7803d80

19 files changed

+506
-113
lines changed

lib/document.js

+141-56
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,58 @@ 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-
if (typeof pathsToValidate === 'function') {
2853+
if (this.$__.saveOptions && this.$__.saveOptions.pathsToSave && !pathsToValidate) {
2854+
pathsToValidate = [...this.$__.saveOptions.pathsToSave];
2855+
} else if (typeof pathsToValidate === 'function') {
28502856
callback = pathsToValidate;
28512857
options = null;
28522858
pathsToValidate = null;
@@ -2868,6 +2874,19 @@ Document.prototype.$__validate = function(pathsToValidate, options, callback) {
28682874
shouldValidateModifiedOnly = this.$__schema.options.validateModifiedOnly;
28692875
}
28702876

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+
28712890
const _this = this;
28722891
const _complete = () => {
28732892
let validationError = this.$__.validationError;
@@ -2905,11 +2924,33 @@ Document.prototype.$__validate = function(pathsToValidate, options, callback) {
29052924
};
29062925

29072926
// only validate required fields when necessary
2908-
const pathDetails = _getPathsToValidate(this, pathsToValidate, pathsToSkip);
2909-
const paths = shouldValidateModifiedOnly ?
2910-
pathDetails[0].filter((path) => this.$isModified(path)) :
2911-
pathDetails[0];
2912-
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+
29132954
if (typeof pathsToValidate === 'string') {
29142955
pathsToValidate = pathsToValidate.split(' ');
29152956
}
@@ -2929,8 +2970,19 @@ Document.prototype.$__validate = function(pathsToValidate, options, callback) {
29292970
const validated = {};
29302971
let total = 0;
29312972

2932-
for (const path of paths) {
2933-
validatePath(path);
2973+
let pathsToSave = this.$__.saveOptions?.pathsToSave;
2974+
if (Array.isArray(pathsToSave)) {
2975+
pathsToSave = new Set(pathsToSave);
2976+
for (const path of paths) {
2977+
if (!pathsToSave.has(path)) {
2978+
continue;
2979+
}
2980+
validatePath(path);
2981+
}
2982+
} else {
2983+
for (const path of paths) {
2984+
validatePath(path);
2985+
}
29342986
}
29352987

29362988
function validatePath(path) {
@@ -2979,7 +3031,8 @@ Document.prototype.$__validate = function(pathsToValidate, options, callback) {
29793031
const doValidateOptions = {
29803032
...doValidateOptionsByPath[path],
29813033
path: path,
2982-
validateModifiedOnly: shouldValidateModifiedOnly
3034+
validateModifiedOnly: shouldValidateModifiedOnly,
3035+
validateAllPaths
29833036
};
29843037

29853038
schemaType.doValidate(val, function(err) {
@@ -3097,6 +3150,16 @@ Document.prototype.validateSync = function(pathsToValidate, options) {
30973150

30983151
let pathsToSkip = options && options.pathsToSkip;
30993152

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+
31003163
if (typeof pathsToValidate === 'string') {
31013164
const isOnePathOnly = pathsToValidate.indexOf(' ') === -1;
31023165
pathsToValidate = isOnePathOnly ? [pathsToValidate] : pathsToValidate.split(' ');
@@ -3105,11 +3168,32 @@ Document.prototype.validateSync = function(pathsToValidate, options) {
31053168
}
31063169

31073170
// only validate required fields when necessary
3108-
const pathDetails = _getPathsToValidate(this, pathsToValidate, pathsToSkip);
3109-
const paths = shouldValidateModifiedOnly ?
3110-
pathDetails[0].filter((path) => this.$isModified(path)) :
3111-
pathDetails[0];
3112-
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+
}
31133197

31143198
const validating = {};
31153199

@@ -3134,7 +3218,8 @@ Document.prototype.validateSync = function(pathsToValidate, options) {
31343218
const err = p.doValidateSync(val, _this, {
31353219
skipSchemaValidators: skipSchemaValidators[path],
31363220
path: path,
3137-
validateModifiedOnly: shouldValidateModifiedOnly
3221+
validateModifiedOnly: shouldValidateModifiedOnly,
3222+
validateAllPaths
31383223
});
31393224
if (err) {
31403225
const isSubdoc = p.$isSingleNested ||

lib/model.js

+13-2
Original file line numberDiff line numberDiff line change
@@ -298,7 +298,6 @@ Model.prototype.$__handleSave = function(options, callback) {
298298
if (!saveOptions.hasOwnProperty('session') && session != null) {
299299
saveOptions.session = session;
300300
}
301-
302301
if (this.$isNew) {
303302
// send entire doc
304303
const obj = this.toObject(saveToObjectOptions);
@@ -335,6 +334,18 @@ Model.prototype.$__handleSave = function(options, callback) {
335334
// since it already exists
336335
this.$__.inserting = false;
337336
const delta = this.$__delta();
337+
338+
if (options.pathsToSave) {
339+
for (const key in delta[1]['$set']) {
340+
if (options.pathsToSave.includes(key)) {
341+
continue;
342+
} else if (options.pathsToSave.some(pathToSave => key.slice(0, pathToSave.length) === pathToSave && key.charAt(pathToSave.length) === '.')) {
343+
continue;
344+
} else {
345+
delete delta[1]['$set'][key];
346+
}
347+
}
348+
}
338349
if (delta) {
339350
if (delta instanceof MongooseError) {
340351
callback(delta);
@@ -521,6 +532,7 @@ function generateVersionError(doc, modifiedPaths) {
521532
* @param {Number} [options.wtimeout] sets a [timeout for the write concern](https://www.mongodb.com/docs/manual/reference/write-concern/#wtimeout). Overrides the [schema-level `writeConcern` option](https://mongoosejs.com/docs/guide.html#writeConcern).
522533
* @param {Boolean} [options.checkKeys=true] the MongoDB driver prevents you from saving keys that start with '$' or contain '.' by default. Set this option to `false` to skip that check. See [restrictions on field names](https://docs.mongodb.com/manual/reference/limits/#mongodb-limit-Restrictions-on-Field-Names)
523534
* @param {Boolean} [options.timestamps=true] if `false` and [timestamps](https://mongoosejs.com/docs/guide.html#timestamps) are enabled, skip timestamps for this `save()`.
535+
* @param {Array} [options.pathsToSave] An array of paths that tell mongoose to only validate and save the paths in `pathsToSave`.
524536
* @throws {DocumentNotFoundError} if this [save updates an existing document](https://mongoosejs.com/docs/api/document.html#Document.prototype.isNew) but the document doesn't exist in the database. For example, you will get this error if the document is [deleted between when you retrieved the document and when you saved it](documents.html#updating).
525537
* @return {Promise}
526538
* @api public
@@ -747,7 +759,6 @@ function handleAtomics(self, where, delta, data, value) {
747759

748760
Model.prototype.$__delta = function() {
749761
const dirty = this.$__dirty();
750-
751762
const optimisticConcurrency = this.$__schema.options.optimisticConcurrency;
752763
if (optimisticConcurrency) {
753764
if (Array.isArray(optimisticConcurrency)) {

lib/query.js

+11-3
Original file line numberDiff line numberDiff line change
@@ -2861,19 +2861,27 @@ Query.prototype.distinct = function(field, conditions) {
28612861
* Cannot be used with `distinct()`
28622862
*
28632863
* @param {Object|String|Array<Array<(string | number)>>} arg
2864+
* @param {Object} [options]
2865+
* @param {Boolean} [options.override=false] If true, replace existing sort options with `arg`
28642866
* @return {Query} this
28652867
* @see cursor.sort https://www.mongodb.com/docs/manual/reference/method/cursor.sort/
28662868
* @api public
28672869
*/
28682870

2869-
Query.prototype.sort = function(arg) {
2870-
if (arguments.length > 1) {
2871-
throw new Error('sort() only takes 1 Argument');
2871+
Query.prototype.sort = function(arg, options) {
2872+
if (arguments.length > 2) {
2873+
throw new Error('sort() takes at most 2 arguments');
2874+
}
2875+
if (options != null && typeof options !== 'object') {
2876+
throw new Error('sort() options argument must be an object or nullish');
28722877
}
28732878

28742879
if (this.options.sort == null) {
28752880
this.options.sort = {};
28762881
}
2882+
if (options && options.override) {
2883+
this.options.sort = {};
2884+
}
28772885
const sort = this.options.sort;
28782886
if (typeof arg === 'string') {
28792887
const properties = arg.indexOf(' ') === -1 ? [arg] : arg.split(' ');

lib/schema.js

+8-4
Original file line numberDiff line numberDiff line change
@@ -1300,6 +1300,7 @@ Schema.prototype.interpretAsType = function(path, obj, options) {
13001300
return clone;
13011301
}
13021302

1303+
13031304
// If this schema has an associated Mongoose object, use the Mongoose object's
13041305
// copy of SchemaTypes re: gh-7158 gh-6933
13051306
const MongooseTypes = this.base != null ? this.base.Schema.Types : Schema.Types;
@@ -1365,9 +1366,13 @@ Schema.prototype.interpretAsType = function(path, obj, options) {
13651366
}
13661367
return new MongooseTypes.DocumentArray(path, cast[options.typeKey], obj, cast);
13671368
}
1368-
1369-
if (Array.isArray(cast)) {
1370-
return new MongooseTypes.Array(path, this.interpretAsType(path, cast, options), obj);
1369+
if (typeof cast !== 'undefined') {
1370+
if (Array.isArray(cast) || cast.type === Array || cast.type == 'Array') {
1371+
if (cast && cast.type == 'Array') {
1372+
cast.type = Array;
1373+
}
1374+
return new MongooseTypes.Array(path, this.interpretAsType(path, cast, options), obj);
1375+
}
13711376
}
13721377

13731378
// Handle both `new Schema({ arr: [{ subpath: String }] })` and `new Schema({ arr: [{ type: { subpath: string } }] })`
@@ -1418,7 +1423,6 @@ Schema.prototype.interpretAsType = function(path, obj, options) {
14181423
type = cast[options.typeKey] && (options.typeKey !== 'type' || !cast.type.type)
14191424
? cast[options.typeKey]
14201425
: cast;
1421-
14221426
if (Array.isArray(type)) {
14231427
return new MongooseTypes.Array(path, this.interpretAsType(path, type, options), obj);
14241428
}

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;

0 commit comments

Comments
 (0)