Skip to content

Commit

Permalink
Merge pull request #14470 from Automattic/8.3
Browse files Browse the repository at this point in the history
8.3
  • Loading branch information
vkarpov15 committed Apr 3, 2024
2 parents 3aba5bf + 52d1486 commit 7803d80
Show file tree
Hide file tree
Showing 19 changed files with 506 additions and 113 deletions.
197 changes: 141 additions & 56 deletions lib/document.js
Original file line number Diff line number Diff line change
Expand Up @@ -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) {
Expand Down Expand Up @@ -2841,12 +2801,58 @@ 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 (typeof pathsToValidate === 'function') {
if (this.$__.saveOptions && this.$__.saveOptions.pathsToSave && !pathsToValidate) {
pathsToValidate = [...this.$__.saveOptions.pathsToSave];
} else if (typeof pathsToValidate === 'function') {
callback = pathsToValidate;
options = null;
pathsToValidate = null;
Expand All @@ -2868,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;
Expand Down Expand Up @@ -2905,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(' ');
}
Expand All @@ -2929,8 +2970,19 @@ Document.prototype.$__validate = function(pathsToValidate, options, callback) {
const validated = {};
let total = 0;

for (const path of paths) {
validatePath(path);
let pathsToSave = this.$__.saveOptions?.pathsToSave;
if (Array.isArray(pathsToSave)) {
pathsToSave = new Set(pathsToSave);
for (const path of paths) {
if (!pathsToSave.has(path)) {
continue;
}
validatePath(path);
}
} else {
for (const path of paths) {
validatePath(path);
}
}

function validatePath(path) {
Expand Down Expand Up @@ -2979,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) {
Expand Down Expand Up @@ -3097,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(' ');
Expand All @@ -3105,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 = {};

Expand All @@ -3134,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 ||
Expand Down
15 changes: 13 additions & 2 deletions lib/model.js
Original file line number Diff line number Diff line change
Expand Up @@ -298,7 +298,6 @@ Model.prototype.$__handleSave = function(options, callback) {
if (!saveOptions.hasOwnProperty('session') && session != null) {
saveOptions.session = session;
}

if (this.$isNew) {
// send entire doc
const obj = this.toObject(saveToObjectOptions);
Expand Down Expand Up @@ -335,6 +334,18 @@ Model.prototype.$__handleSave = function(options, callback) {
// since it already exists
this.$__.inserting = false;
const delta = this.$__delta();

if (options.pathsToSave) {
for (const key in delta[1]['$set']) {
if (options.pathsToSave.includes(key)) {
continue;
} else if (options.pathsToSave.some(pathToSave => key.slice(0, pathToSave.length) === pathToSave && key.charAt(pathToSave.length) === '.')) {
continue;
} else {
delete delta[1]['$set'][key];
}
}
}
if (delta) {
if (delta instanceof MongooseError) {
callback(delta);
Expand Down Expand Up @@ -521,6 +532,7 @@ function generateVersionError(doc, modifiedPaths) {
* @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).
* @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)
* @param {Boolean} [options.timestamps=true] if `false` and [timestamps](https://mongoosejs.com/docs/guide.html#timestamps) are enabled, skip timestamps for this `save()`.
* @param {Array} [options.pathsToSave] An array of paths that tell mongoose to only validate and save the paths in `pathsToSave`.
* @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).
* @return {Promise}
* @api public
Expand Down Expand Up @@ -747,7 +759,6 @@ function handleAtomics(self, where, delta, data, value) {

Model.prototype.$__delta = function() {
const dirty = this.$__dirty();

const optimisticConcurrency = this.$__schema.options.optimisticConcurrency;
if (optimisticConcurrency) {
if (Array.isArray(optimisticConcurrency)) {
Expand Down
14 changes: 11 additions & 3 deletions lib/query.js
Original file line number Diff line number Diff line change
Expand Up @@ -2861,19 +2861,27 @@ Query.prototype.distinct = function(field, conditions) {
* Cannot be used with `distinct()`
*
* @param {Object|String|Array<Array<(string | number)>>} arg
* @param {Object} [options]
* @param {Boolean} [options.override=false] If true, replace existing sort options with `arg`
* @return {Query} this
* @see cursor.sort https://www.mongodb.com/docs/manual/reference/method/cursor.sort/
* @api public
*/

Query.prototype.sort = function(arg) {
if (arguments.length > 1) {
throw new Error('sort() only takes 1 Argument');
Query.prototype.sort = function(arg, options) {
if (arguments.length > 2) {
throw new Error('sort() takes at most 2 arguments');
}
if (options != null && typeof options !== 'object') {
throw new Error('sort() options argument must be an object or nullish');
}

if (this.options.sort == null) {
this.options.sort = {};
}
if (options && options.override) {
this.options.sort = {};
}
const sort = this.options.sort;
if (typeof arg === 'string') {
const properties = arg.indexOf(' ') === -1 ? [arg] : arg.split(' ');
Expand Down
12 changes: 8 additions & 4 deletions lib/schema.js
Original file line number Diff line number Diff line change
Expand Up @@ -1300,6 +1300,7 @@ Schema.prototype.interpretAsType = function(path, obj, options) {
return clone;
}


// If this schema has an associated Mongoose object, use the Mongoose object's
// copy of SchemaTypes re: gh-7158 gh-6933
const MongooseTypes = this.base != null ? this.base.Schema.Types : Schema.Types;
Expand Down Expand Up @@ -1365,9 +1366,13 @@ Schema.prototype.interpretAsType = function(path, obj, options) {
}
return new MongooseTypes.DocumentArray(path, cast[options.typeKey], obj, cast);
}

if (Array.isArray(cast)) {
return new MongooseTypes.Array(path, this.interpretAsType(path, cast, options), obj);
if (typeof cast !== 'undefined') {
if (Array.isArray(cast) || cast.type === Array || cast.type == 'Array') {
if (cast && cast.type == 'Array') {
cast.type = Array;
}
return new MongooseTypes.Array(path, this.interpretAsType(path, cast, options), obj);
}
}

// Handle both `new Schema({ arr: [{ subpath: String }] })` and `new Schema({ arr: [{ type: { subpath: string } }] })`
Expand Down Expand Up @@ -1418,7 +1423,6 @@ Schema.prototype.interpretAsType = function(path, obj, options) {
type = cast[options.typeKey] && (options.typeKey !== 'type' || !cast.type.type)
? cast[options.typeKey]
: cast;

if (Array.isArray(type)) {
return new MongooseTypes.Array(path, this.interpretAsType(path, type, options), obj);
}
Expand Down
4 changes: 2 additions & 2 deletions lib/schema/documentArray.js
Original file line number Diff line number Diff line change
Expand Up @@ -279,7 +279,7 @@ SchemaDocumentArray.prototype.doValidate = function(array, fn, scope, options) {
continue;
}

doc.$__validate(callback);
doc.$__validate(null, options, callback);
}
}
};
Expand Down Expand Up @@ -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;
Expand Down

0 comments on commit 7803d80

Please sign in to comment.