Skip to content
New issue

Have a question about this project? Sign up for a free GitHub account to open an issue and contact its maintainers and the community.

By clicking “Sign up for GitHub”, you agree to our terms of service and privacy statement. We’ll occasionally send you account related emails.

Already on GitHub? Sign in to your account

8.3 #14470

Merged
merged 36 commits into from
Apr 3, 2024
Merged

8.3 #14470

Show file tree
Hide file tree
Changes from all commits
Commits
Show all changes
36 commits
Select commit Hold shift + click to select a range
9d2121e
types: add overwriteMiddlewareResult and skipMiddlewareFunction to types
vkarpov15 Feb 2, 2024
4beb8bc
feat(query): add `options` parameter to `Query.prototype.sort()`
vkarpov15 Feb 24, 2024
7218969
feat: `pathsToSave` option to `save()` function
IslandRhythms Feb 26, 2024
62655fc
move filtering to handle save
IslandRhythms Feb 26, 2024
fae80d3
fix: lint
IslandRhythms Feb 26, 2024
e8ce022
Merge pull request #14375 from Automattic/vkarpov15/gh-14365
vkarpov15 Feb 27, 2024
5635a49
Support subdocs, doc arrays, and validation
IslandRhythms Feb 28, 2024
0d8189a
types: use Kareem exported OverwriteMiddlewareResult and SkipWrappedF…
vkarpov15 Mar 1, 2024
f6ea526
Merge branch 'master' into vkarpov15/gh-14289
vkarpov15 Mar 1, 2024
79bd298
chore: try alternative approach to install kareem branch
vkarpov15 Mar 1, 2024
f007581
perf: clean up a couple of unnecessarty pre() definitions
vkarpov15 Mar 1, 2024
9c335a8
perf: remove a couple of unnecessary pre<> signatures to improve Type…
vkarpov15 Mar 4, 2024
35aae25
feat: upgrade kareem to 2.6.0 for TypeScript types
vkarpov15 Mar 4, 2024
f6619d9
Merge branch 'master' into 8.3
vkarpov15 Mar 4, 2024
268d7b8
Merge pull request #14328 from Automattic/vkarpov15/gh-14289
vkarpov15 Mar 4, 2024
cf62382
write test
IslandRhythms Mar 13, 2024
82aee66
feat: `validateAll` function on `SchemaType`
IslandRhythms Mar 13, 2024
cab97cc
types+docs(schematype): add types and jsdoc for SchemaType.prototype.…
vkarpov15 Mar 14, 2024
55774c6
Update test/schematype.test.js
vkarpov15 Mar 14, 2024
399531b
types: propagate DocType down to validators for validate() and valida…
vkarpov15 Mar 14, 2024
92b2544
Merge branch 'master' into 8.3
vkarpov15 Mar 18, 2024
dc9810b
Merge pull request #14434 from Automattic/IslandRhythms/gh-6910
vkarpov15 Mar 18, 2024
a8d5c98
fix: array schema definitions with `of` keyword
IslandRhythms Mar 19, 2024
1f53635
Merge pull request #14447 from Automattic/IslandRhythms/gh-14416
vkarpov15 Mar 20, 2024
c96b5bb
fix: better handling for paths that contain other paths
vkarpov15 Mar 20, 2024
ebedffc
Update lib/document.js
vkarpov15 Mar 20, 2024
5950c86
Merge branch '8.3' into IslandRhythms/gh-9583
vkarpov15 Mar 20, 2024
aeb061e
Merge branch 'IslandRhythms/gh-9583' of github.com:Automattic/mongoos…
vkarpov15 Mar 20, 2024
2edf2ff
style: fix lint
vkarpov15 Mar 20, 2024
aabddf4
style: cleanup
vkarpov15 Mar 20, 2024
276765d
Merge pull request #14385 from Automattic/IslandRhythms/gh-9583
vkarpov15 Mar 20, 2024
c8dcaef
Merge branch 'master' into 8.3
vkarpov15 Mar 22, 2024
b47a9fa
feat: upgrade mongodb and bson -> 6.5.0
vkarpov15 Mar 22, 2024
7e8ebc8
feat(document): add `validateAllPaths` option to `validate()` and `va…
vkarpov15 Mar 26, 2024
663688f
Merge pull request #14467 from Automattic/vkarpov15/gh-14414
vkarpov15 Mar 27, 2024
52d1486
Merge branch 'master' into 8.3
vkarpov15 Apr 2, 2024
File filter

Filter by extension

Filter by extension


Conversations
Failed to load comments.
Loading
Jump to
Jump to file
Failed to load files.
Loading
Diff view
Diff view
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
Loading