mongoose 9.10.3 → 9.11.0

This diff represents the content of publicly available package versions that have been released to one of the supported registries. The information contained in this diff is provided for informational purposes only and reflects changes between package versions as they appear in their respective public registries.
package/lib/document.js CHANGED
@@ -27,7 +27,6 @@ const isInPathsToSave = require('./helpers/document/isInPathsToSave');
27
27
  const compile = require('./helpers/document/compile').compile;
28
28
  const defineKey = require('./helpers/document/compile').defineKey;
29
29
  const firstKey = require('./helpers/firstKey');
30
- const flatten = require('./helpers/common').flatten;
31
30
  const getConstructorName = require('./helpers/getConstructorName');
32
31
  const getEmbeddedDiscriminatorPath = require('./helpers/document/getEmbeddedDiscriminatorPath');
33
32
  const getKeysInSchemaOrder = require('./helpers/schema/getKeysInSchemaOrder');
@@ -48,7 +47,6 @@ const utils = require('./utils');
48
47
  const isPromise = require('./helpers/isPromise');
49
48
 
50
49
  const deepEqual = utils.deepEqual;
51
- const isMongooseObject = utils.isMongooseObject;
52
50
 
53
51
  const applyDefaultsBeforeSettersOptions = Object.freeze({ skipParentChangeTracking: true });
54
52
 
@@ -2819,24 +2817,9 @@ Document.prototype.validate = async function validate(pathsToValidate, options)
2819
2817
  let paths;
2820
2818
  let doValidateOptionsByPath;
2821
2819
  if (validateAllPaths) {
2822
- paths = new Set(Object.keys(this.$__schema.paths));
2823
- // gh-661: if a whole array is modified, make sure to run validation on all
2824
- // the children as well
2825
- for (const path of paths) {
2826
- const schemaType = this.$__schema.path(path);
2827
- if (!schemaType?.$isMongooseArray) {
2828
- continue;
2829
- }
2830
- const val = this.$__getValue(path);
2831
- if (!val) {
2832
- continue;
2833
- }
2834
- _pushNestedArrayPaths(val, paths, path);
2835
- }
2836
- paths = [...paths];
2837
- doValidateOptionsByPath = {};
2820
+ [paths, doValidateOptionsByPath] = _getAllPathsToValidate(this);
2838
2821
  } else {
2839
- const pathDetails = _getPathsToValidate(this, pathsToValidate, pathsToSkip, options?._nestedValidate);
2822
+ const pathDetails = _getPathsToValidate(this, pathsToValidate, pathsToSkip);
2840
2823
  paths = shouldValidateModifiedOnly ?
2841
2824
  pathDetails[0].filter((path) => this.$isModified(path)) :
2842
2825
  pathDetails[0];
@@ -2918,6 +2901,9 @@ Document.prototype.validate = async function validate(pathsToValidate, options)
2918
2901
  validateAllPaths,
2919
2902
  _nestedValidate: true
2920
2903
  };
2904
+ if (shouldValidateModifiedOnly) {
2905
+ doValidateOptions.validateModifiedOnly = true;
2906
+ }
2921
2907
 
2922
2908
  promises.push(
2923
2909
  schemaType.doValidate(val, scope, doValidateOptions).then(
@@ -3016,34 +3002,118 @@ function _completeValidate(doc) {
3016
3002
  return error;
3017
3003
  }
3018
3004
 
3005
+ /*!
3006
+ * Build the list for `validateAllPaths`. The document array path validates
3007
+ * every subdocument, but skips nullish elements. Add those elements separately
3008
+ * so their element-level validators still run.
3009
+ */
3010
+
3011
+ function _getAllPathsToValidate(doc) {
3012
+ const paths = Object.keys(doc.$__schema.paths).filter(path =>
3013
+ path.charAt(path.length - 1) !== '*' ||
3014
+ path.charAt(path.length - 2) !== '$' ||
3015
+ path.charAt(path.length - 3) !== '.'
3016
+ );
3017
+ const doValidateOptionsByPath = {};
3018
+
3019
+ // gh-661: if a whole array is modified, make sure to run validation on all
3020
+ // the children as well
3021
+ for (let i = 0; i < paths.length; ++i) {
3022
+ const path = paths[i];
3023
+ const schemaType = doc.$__schema.path(path);
3024
+ if (schemaType == null) {
3025
+ continue;
3026
+ }
3027
+ const val = doc.$__getValue(path);
3028
+ if (!val) {
3029
+ continue;
3030
+ }
3031
+ if (schemaType.$isSchemaMap) {
3032
+ for (const key of val.keys()) {
3033
+ paths.push(path + '.' + key);
3034
+ }
3035
+ } else if (schemaType.$isMongooseDocumentArray) {
3036
+ // The document array validates each of its subdocuments, so elements only
3037
+ // need their own path listed if the element schematype has validators of
3038
+ // its own, like `required` or `[{ type: childSchema, validate: fn }]`.
3039
+ if (schemaType.embeddedSchemaType?.validators.length) {
3040
+ for (let j = 0; j < val.length; ++j) {
3041
+ const elementPath = path + '.' + j;
3042
+ paths.push(elementPath);
3043
+ doValidateOptionsByPath[elementPath] = { skipSubdocumentValidation: true, index: j };
3044
+ }
3045
+ }
3046
+ } else if (schemaType.$isMongooseArray) {
3047
+ _pushNestedArrayPaths(val, paths, path);
3048
+ }
3049
+ }
3050
+
3051
+ return [paths, doValidateOptionsByPath];
3052
+ }
3053
+
3054
+ /*!
3055
+ * ignore
3056
+ */
3057
+
3019
3058
  const STATES_TO_VALIDATE = Object.freeze(['init', 'default', 'modify']);
3020
3059
  const STAR_CHAR_CODE = '*'.charCodeAt(0);
3021
3060
 
3022
3061
  /*!
3023
- * ignore
3062
+ * Builds the list of paths to run validation on, plus per-path options for
3063
+ * `doValidate()`.
3064
+ *
3065
+ * `activePaths` tells us which paths changed (`require`, `init`, `default`,
3066
+ * `modify`), but that is neither a complete nor a minimal list of paths to
3067
+ * validate, in four separate ways. Each phase below fixes exactly one:
3068
+ *
3069
+ * 1. Seed Collect the changed paths, dropping ones that have nothing to
3070
+ * run and ones a subdocument already validates for us.
3071
+ * 2. Resolve Some entries are not schema paths at all: a modified nested
3072
+ * object is stored as one entry (`nest`), but the paths that
3073
+ * have validators are its children (`nest.name`).
3074
+ * 3. Cover Replace changed descendants with their actual subdocument or
3075
+ * document array, which owns validation below that path.
3076
+ * 4. Augment Primitive arrays, required document array elements, and maps
3077
+ * need concrete element or key paths listed individually.
3078
+ *
3079
+ * Validating a subdocument or document array validates everything underneath
3080
+ * it, so `arr` makes `arr.0.name` redundant. Phase 3 uses the actual
3081
+ * subdocuments and their modified paths to remove descendants seeded earlier.
3082
+ * It runs for nested documents as well, so every document selects containers
3083
+ * from its own schema without relying on `$parentSchemaDocArray`.
3084
+ *
3085
+ * If a document array element is modified, for example `arr.0.name`,
3086
+ * array-level validators on `arr` still run. This behavior dates back to gh-6818.
3087
+ *
3088
+ * `pathsToValidate` / `pathsToSkip` are applied before augment, so that asking
3089
+ * for `arr` still validates its elements. Duplicates are fine; both callers
3090
+ * skip paths they have already validated.
3024
3091
  */
3025
3092
 
3026
- function _getPathsToValidate(doc, pathsToValidate, pathsToSkip, isNestedValidate) {
3027
- const doValidateOptions = {};
3093
+ function _getPathsToValidate(doc, pathsToValidate, pathsToSkip) {
3028
3094
  const schema = doc.$__schema;
3029
3095
  const schemaPaths = schema.paths;
3096
+ const singleNestedPaths = schema.singleNestedPaths;
3030
3097
  const activeStates = doc.$__.activePaths.states;
3031
-
3032
- // gh-16379: compute the modified-path set at most once and reuse it below, so
3033
- // checking many required paths doesn't rebuild it per-path (O(required x modified)).
3034
- let _modifiedPaths;
3035
- const getModifiedPaths = () => (_modifiedPaths ??= doc.modifiedPaths());
3036
-
3037
- // only validate required fields when necessary
3038
- let paths = [];
3098
+ const doValidateOptions = {};
3099
+ // Subdocument and document array paths already in `paths`, which are the
3100
+ // schema types that have a `schema` of their own. They run validation on
3101
+ // their children, so nothing below one of these should be added.
3102
+ const covered = {};
3103
+ let paths = new Set();
3104
+
3105
+ // Phase 1: seed. Every path `activePaths` reports as changed, minus the ones
3106
+ // with nothing to run and the ones a subdocument validates for us.
3039
3107
  const requireStates = activeStates.require;
3040
3108
  if (requireStates != null) {
3041
3109
  let modifiedPaths = null;
3042
3110
  for (const path in requireStates) {
3111
+ const isOwnPath = Object.hasOwn(schemaPaths, path);
3112
+ const type = isOwnPath ? schemaPaths[path] : schema.path(path);
3113
+
3043
3114
  // Evaluate function-valued `required` for every required path, before any
3044
3115
  // filtering, so `invalidate()` calls and `cachedRequired` entries stay
3045
3116
  // consistent no matter which paths end up validated.
3046
- const type = Object.hasOwn(schemaPaths, path) ? schemaPaths[path] : schema.path(path);
3047
3117
  if (typeof type?.originalRequiredValue === 'function') {
3048
3118
  const cachedRequired = doc.$__.cachedRequired ?? (doc.$__.cachedRequired = {});
3049
3119
  try {
@@ -3054,24 +3124,26 @@ function _getPathsToValidate(doc, pathsToValidate, pathsToSkip, isNestedValidate
3054
3124
  }
3055
3125
 
3056
3126
  if (!doc.$__isSelected(path)) {
3057
- if (modifiedPaths === null) {
3058
- modifiedPaths = doc.modifiedPaths();
3059
- }
3127
+ modifiedPaths ??= doc.modifiedPaths();
3060
3128
  if (!doc.$isModified(path, null, modifiedPaths)) {
3061
3129
  continue;
3062
3130
  }
3063
3131
  }
3132
+ // `.$*` paths represent map schemas, not actual document paths.
3064
3133
  if (path.charCodeAt(path.length - 1) === STAR_CHAR_CODE && path.endsWith('.$*')) {
3065
- // Skip $* paths - they represent map schemas, not actual document paths
3066
3134
  continue;
3067
3135
  }
3068
3136
  const cachedRequired = doc.$__.cachedRequired;
3069
- if (cachedRequired != null && path in cachedRequired) {
3070
- if (cachedRequired[path]) {
3071
- paths.push(path);
3072
- }
3073
- } else {
3074
- paths.push(path);
3137
+ if (cachedRequired != null && path in cachedRequired && !cachedRequired[path]) {
3138
+ continue;
3139
+ }
3140
+ if (!isOwnPath && type != null &&
3141
+ Object.hasOwn(singleNestedPaths, path)) {
3142
+ continue;
3143
+ }
3144
+ paths.add(path);
3145
+ if (type != null && type.schema != null) {
3146
+ covered[path] = true;
3075
3147
  }
3076
3148
  }
3077
3149
  }
@@ -3081,187 +3153,188 @@ function _getPathsToValidate(doc, pathsToValidate, pathsToSkip, isNestedValidate
3081
3153
  if (statePaths == null) {
3082
3154
  continue;
3083
3155
  }
3084
- for (const p in statePaths) {
3085
- if (p.charCodeAt(p.length - 1) === STAR_CHAR_CODE && p.endsWith('.$*')) {
3086
- // Skip $* paths - they represent map schemas, not actual document paths
3156
+ for (const path in statePaths) {
3157
+ // `.$*` paths represent map schemas, not actual document paths.
3158
+ if (path.charCodeAt(path.length - 1) === STAR_CHAR_CODE && path.endsWith('.$*')) {
3087
3159
  continue;
3088
3160
  }
3089
-
3090
- const _pathType = Object.hasOwn(schemaPaths, p) ? schemaPaths[p] : schema.path(p);
3091
-
3092
- // Optimization: if primitive path with no validators, or array of primitives
3093
- // with no validators, skip validating this path entirely.
3094
- // Note: paths with no _pathType (e.g. sub-paths under Mixed) must still be
3095
- // added, as they trigger validation of the parent Mixed path.
3096
- if (_pathType) {
3097
- if (!_pathType.schema &&
3098
- !_pathType.embeddedSchemaType &&
3099
- _pathType.validators.length === 0 &&
3100
- !_pathType.$parentSchemaDocArray &&
3101
- // gh-15957: skip this optimization for SchemaMap as maps can contain subdocuments
3102
- // that need validation even if the map itself has no validators
3103
- !_pathType.$isSchemaMap &&
3104
- !_pathType.$isSchemaUnion) {
3105
- continue;
3106
- } else if (_pathType.$isMongooseArray &&
3107
- !_pathType.$isMongooseDocumentArray && // Skip document arrays...
3108
- !_pathType.embeddedSchemaType.$isMongooseArray && // and arrays of arrays
3109
- _pathType.validators.length === 0 && // and arrays with top-level validators
3110
- _pathType.embeddedSchemaType.validators.length === 0) {
3111
- continue;
3112
- }
3161
+ const isOwnPath = Object.hasOwn(schemaPaths, path);
3162
+ const type = isOwnPath ? schemaPaths[path] : schema.path(path);
3163
+ // Paths with no schema type must still be added so a parent schema path
3164
+ // can handle them below.
3165
+ if (type != null &&
3166
+ (_hasNothingToValidate(type) ||
3167
+ (!isOwnPath &&
3168
+ Object.hasOwn(singleNestedPaths, path)))) {
3169
+ continue;
3170
+ }
3171
+ // `schema.path()` resolves a path under Mixed to the Mixed parent. A
3172
+ // modified Mixed child does not select the parent validator (gh-10141).
3173
+ if (type?.[schemaMixedSymbol] != null && path !== type.path) {
3174
+ continue;
3175
+ }
3176
+ paths.add(path);
3177
+ if (type != null && type.schema != null) {
3178
+ covered[path] = true;
3113
3179
  }
3114
-
3115
- paths.push(p);
3116
3180
  }
3117
3181
  }
3118
3182
 
3119
- if (paths.length === 0 && doc.$__hasOnlyPrimitiveValues()) {
3120
- return [paths, doValidateOptions];
3183
+ if (paths.size === 0 && doc.$__hasOnlyPrimitiveValues()) {
3184
+ return [[], doValidateOptions];
3185
+ }
3186
+
3187
+ // Phase 2: resolve. A modified nested object is a single entry in
3188
+ // `activePaths`, but it is not a schema path and has no validators of its
3189
+ // own, so replace it with its children. Children are added as we go, and a
3190
+ // Set visits entries added during iteration, so this same loop also resolves
3191
+ // nested objects several levels deep.
3192
+ for (const path of paths) {
3193
+ if (!schema.nested[path]) {
3194
+ continue;
3195
+ }
3196
+ paths.delete(path);
3197
+ const value = doc.$__getValue(path);
3198
+ if (value == null || Array.isArray(value)) {
3199
+ continue;
3200
+ }
3201
+ for (const key of Object.keys(value)) {
3202
+ const child = path + '.' + key;
3203
+ paths.add(child);
3204
+ if (Object.hasOwn(schemaPaths, child) && schemaPaths[child].schema != null) {
3205
+ covered[child] = true;
3206
+ }
3207
+ }
3121
3208
  }
3122
3209
 
3123
- paths = new Set(paths);
3210
+ // Phase 3: cover. Validate modified single nested documents and document
3211
+ // arrays as units, using actual subdocuments to remove redundant descendants.
3212
+ const modifiedPaths = doc.modifiedPaths();
3124
3213
 
3125
- if (!isNestedValidate) {
3126
- // If we're validating a subdocument, all this logic will run anyway on the top-level document, so skip for subdocuments.
3127
- // But only run for top-level subdocuments, because we're looking for subdocuments that are not modified at top-level but
3128
- // have a modified path. If that is the case, we will run validation on the top-level subdocument, and via that run validation
3129
- // on any subdocuments down to the modified path.
3130
- const topLevelSubdocs = [];
3131
- for (const path of Object.keys(doc.$__schema.paths)) {
3132
- const schemaType = doc.$__schema.path(path);
3133
- if (schemaType.$isSingleNested) {
3134
- const subdoc = doc.$get(path);
3135
- if (subdoc) {
3136
- topLevelSubdocs.push(subdoc);
3137
- }
3138
- } else if (schemaType.$isMongooseDocumentArray) {
3139
- const arr = doc.$get(path);
3140
- if (arr?.length) {
3141
- for (const subdoc of arr) {
3142
- if (subdoc) {
3143
- topLevelSubdocs.push(subdoc);
3144
- }
3145
- }
3146
- }
3214
+ const pathsWithSchemaTypes = Object.entries(schemaPaths);
3215
+ // Map of document arrays may have subdocuments that are not in `schemaPaths` -
3216
+ // If `team` is a map of document arrays, `team.engineering` is not a schema path
3217
+ // but may be a modified document array.
3218
+ for (const path of Object.keys(schemaPaths)) {
3219
+ const schemaType = schemaPaths[path];
3220
+ if (!schemaType.$isSchemaMap || !schemaType.$__schemaType.$isMongooseDocumentArray) {
3221
+ continue;
3222
+ }
3223
+ const val = doc.$get(path);
3224
+ if (val == null) {
3225
+ continue;
3226
+ }
3227
+ for (const key of val.keys()) {
3228
+ const mapValuePath = path + '.' + key;
3229
+ if (modifiedPaths.includes(mapValuePath)) {
3230
+ pathsWithSchemaTypes.push([mapValuePath, schemaType.$__schemaType]);
3147
3231
  }
3148
3232
  }
3149
- const modifiedPaths = getModifiedPaths();
3233
+ }
3150
3234
 
3151
- // gh-6818: if a document array that has array-level validators has any
3152
- // modified element, validate the array itself so those validators run.
3153
- for (const modifiedPath of modifiedPaths) {
3154
- const modifiedPathType = doc.$__schema.path(modifiedPath);
3155
- if (modifiedPathType != null &&
3156
- modifiedPathType.$isMongooseDocumentArray &&
3157
- modifiedPathType.validators.length > 0) {
3158
- paths.add(modifiedPath);
3235
+ for (const [path, schemaType] of pathsWithSchemaTypes) {
3236
+ let subdocs;
3237
+ if (schemaType.$isSingleNested) {
3238
+ const subdoc = doc.$get(path);
3239
+ if (subdoc == null) {
3240
+ continue;
3241
+ }
3242
+ subdocs = [subdoc];
3243
+ } else if (schemaType.$isMongooseDocumentArray) {
3244
+ subdocs = doc.$get(path);
3245
+ if (!subdocs?.length) {
3246
+ continue;
3247
+ }
3248
+ // If a document array element is modified, for example `arr.0.name`,
3249
+ // validate the whole array so array-level validators on `arr` still run.
3250
+ // This behavior dates back to gh-6818.
3251
+ if (modifiedPaths.includes(path) &&
3252
+ (schemaType.validators.length > 0 || covered[path] === true)) {
3253
+ if (covered[path] !== true && !Object.hasOwn(activeStates.modify, path)) {
3254
+ doValidateOptions[path] = { skipSchemaValidators: true };
3255
+ }
3256
+ paths.add(path);
3257
+ covered[path] = true;
3159
3258
  }
3259
+ } else {
3260
+ continue;
3160
3261
  }
3161
3262
 
3162
- for (const subdoc of topLevelSubdocs) {
3163
- if (subdoc.$basePath) {
3164
- const fullPathToSubdoc = subdoc.$__pathRelativeToParent();
3263
+ // Validating the whole array validates every element, so element paths
3264
+ // are redundant. They can be seeded before the array is, so drop them
3265
+ // here rather than relying on the array being seen first.
3266
+ const arrayCoversElements = schemaType.$isMongooseDocumentArray && covered[path] === true;
3165
3267
 
3166
- // Remove child paths for now, because we'll be validating the whole
3167
- // subdoc.
3168
- // The following is a faster take on looping through every path in `paths`
3169
- // and checking if the path starts with `fullPathToSubdoc` re: gh-13191
3170
- for (const modifiedPath of subdoc.modifiedPaths()) {
3268
+ for (const subdoc of subdocs) {
3269
+ if (subdoc == null || !subdoc.$basePath) {
3270
+ continue;
3271
+ }
3272
+ const fullPathToSubdoc = schemaType.$isMongooseDocumentArray ?
3273
+ path + '.' + subdoc.__index :
3274
+ subdoc.$__pathRelativeToParent();
3275
+ const subdocParent = subdoc.$parent();
3276
+ if (subdocParent == null) {
3277
+ throw new Error('Cannot validate subdocument that does not have a parent');
3278
+ }
3279
+ if (arrayCoversElements) {
3280
+ paths.delete(fullPathToSubdoc);
3281
+ for (const modifiedPath of subdoc.modifiedPaths({ includeChildren: true })) {
3171
3282
  paths.delete(fullPathToSubdoc + '.' + modifiedPath);
3172
3283
  }
3173
-
3174
- const subdocParent = subdoc.$parent();
3175
- if (subdocParent == null) {
3176
- throw new Error('Cannot validate subdocument that does not have a parent');
3284
+ if (path !== schemaType.path && Object.hasOwn(activeStates.modify, fullPathToSubdoc)) {
3285
+ doValidateOptions[fullPathToSubdoc] = { skipSchemaValidators: false };
3177
3286
  }
3178
- if (doc.$isModified(fullPathToSubdoc, null, modifiedPaths) &&
3179
- // Avoid using isDirectModified() here because that does additional checks on whether the parent path
3180
- // is direct modified, which can cause performance issues re: gh-14897
3181
- !Object.hasOwn(subdocParent.$__.activePaths.getStatePaths('modify'), fullPathToSubdoc) &&
3182
- !subdocParent.$isDefault(fullPathToSubdoc)) {
3183
- paths.add(fullPathToSubdoc);
3184
-
3185
- if (doc.$__.pathsToScopes == null) {
3186
- doc.$__.pathsToScopes = {};
3187
- }
3188
- doc.$__.pathsToScopes[fullPathToSubdoc] = subdoc.$isDocumentArrayElement ?
3189
- subdoc.__parentArray :
3190
- subdoc.$parent();
3287
+ }
3288
+ if (!doc.$isModified(fullPathToSubdoc, null, modifiedPaths) ||
3289
+ // Avoid using isDirectModified() here because that does additional checks on whether the parent path
3290
+ // is direct modified, which can cause performance issues re: gh-14897
3291
+ Object.hasOwn(subdocParent.$__.activePaths.getStatePaths('modify'), fullPathToSubdoc) ||
3292
+ subdocParent.$isDefault(fullPathToSubdoc)) {
3293
+ continue;
3294
+ }
3191
3295
 
3192
- doValidateOptions[fullPathToSubdoc] = { skipSchemaValidators: true };
3193
- if (subdoc.$isDocumentArrayElement && subdoc.__index != null) {
3194
- doValidateOptions[fullPathToSubdoc].index = subdoc.__index;
3195
- }
3296
+ if (!arrayCoversElements) {
3297
+ for (const modifiedPath of subdoc.modifiedPaths()) {
3298
+ paths.delete(fullPathToSubdoc + '.' + modifiedPath);
3196
3299
  }
3197
3300
  }
3198
- }
3199
- }
3200
3301
 
3201
- if (!doc.$__hasOnlyPrimitiveValues()) {
3202
- for (const path of paths) {
3203
- const _pathType = doc.$__schema.path(path);
3204
- if (_pathType && _pathType.$isMongooseDocumentArray) {
3205
- for (const p of paths) {
3206
- if (p == null || p.startsWith(_pathType.path + '.')) {
3207
- paths.delete(p);
3208
- }
3209
- }
3302
+ // The element's index and scope are still recorded even when the array
3303
+ // covers it: `_addArrayPathsToValidate` may list the element below.
3304
+ if (!arrayCoversElements && covered[path] !== true) {
3305
+ paths.add(fullPathToSubdoc);
3306
+ }
3307
+ covered[fullPathToSubdoc] = true;
3308
+ doc.$__.pathsToScopes ??= {};
3309
+ doc.$__.pathsToScopes[fullPathToSubdoc] = subdoc.$isDocumentArrayElement ?
3310
+ subdoc.__parentArray :
3311
+ subdoc.$parent();
3312
+ doValidateOptions[fullPathToSubdoc] ??= subdoc.$isDocumentArrayElement ?
3313
+ {} :
3314
+ { skipSchemaValidators: true };
3315
+ if (subdoc.$isDocumentArrayElement && subdoc.__index != null) {
3316
+ doValidateOptions[fullPathToSubdoc].index = subdoc.__index;
3210
3317
  }
3211
3318
  }
3212
3319
  }
3213
3320
 
3214
-
3215
3321
  if (Array.isArray(pathsToValidate)) {
3216
3322
  paths = _handlePathsToValidate(paths, pathsToValidate);
3217
3323
  } else if (Array.isArray(pathsToSkip)) {
3218
3324
  paths = _handlePathsToSkip(paths, pathsToSkip);
3219
3325
  }
3220
3326
 
3221
- // from here on we're not removing items from paths
3222
-
3327
+ // Phase 4: augment. Arrays and maps only run their own validators, so their
3328
+ // elements and keys need to be listed individually.
3223
3329
  // gh-661: if a whole array is modified, make sure to run validation on all
3224
3330
  // the children as well
3225
- _addArrayPathsToValidate(doc, paths);
3226
-
3227
- const flattenOptions = { skipArrays: true };
3228
- for (const pathToCheck of paths) {
3229
- if (doc.$__schema.nested[pathToCheck]) {
3230
- let _v = doc.$__getValue(pathToCheck);
3231
- if (isMongooseObject(_v)) {
3232
- _v = _v.toObject({ transform: false });
3233
- }
3234
- const flat = flatten(_v, pathToCheck, flattenOptions, doc.$__schema);
3235
- // Single nested paths (paths embedded under single nested subdocs) will
3236
- // be validated on their own when we call `validate()` on the subdoc itself.
3237
- // Re: gh-8468
3238
- const singleNestedPaths = doc.$__schema.singleNestedPaths;
3239
- for (const path of Object.keys(flat)) {
3240
- if (!Object.hasOwn(singleNestedPaths, path) && !path.endsWith('.$*')) {
3241
- paths.add(path);
3242
- }
3243
- }
3244
- }
3245
- }
3331
+ _addArrayPathsToValidate(doc, paths, doValidateOptions);
3246
3332
 
3247
3333
  for (const path of paths) {
3248
- const _pathType = doc.$__schema.path(path);
3249
-
3250
- if (!_pathType) {
3334
+ const _pathType = schema.path(path);
3335
+ if (!_pathType || !_pathType.$isSchemaMap) {
3251
3336
  continue;
3252
3337
  }
3253
-
3254
- // If underneath a document array, may need to re-validate the parent
3255
- // array re: gh-6818. Do this _after_ adding subpaths, because
3256
- // we don't want to add every array subpath.
3257
- if (_pathType.$parentSchemaDocArray && typeof _pathType.$parentSchemaDocArray.path === 'string') {
3258
- paths.add(_pathType.$parentSchemaDocArray.path);
3259
- }
3260
-
3261
- if (!_pathType.$isSchemaMap) {
3262
- continue;
3263
- }
3264
-
3265
3338
  const val = doc.$__getValue(path);
3266
3339
  if (val == null) {
3267
3340
  continue;
@@ -3271,11 +3344,35 @@ function _getPathsToValidate(doc, pathsToValidate, pathsToSkip, isNestedValidate
3271
3344
  }
3272
3345
  }
3273
3346
 
3274
- paths = Array.from(paths);
3275
- return [paths, doValidateOptions];
3347
+ // The callers index into the result, so hand them an array.
3348
+ return [Array.from(paths), doValidateOptions];
3276
3349
  }
3277
3350
 
3278
- function _addArrayPathsToValidate(doc, paths) {
3351
+ /*!
3352
+ * Optimization: skip paths that cannot produce a validation error, either a
3353
+ * primitive with no validators or an array of primitives with no validators on
3354
+ * the array or its elements.
3355
+ */
3356
+
3357
+ function _hasNothingToValidate(type) {
3358
+ if (!type.schema &&
3359
+ !type.embeddedSchemaType &&
3360
+ type.validators.length === 0 &&
3361
+ // gh-15957: maps can contain subdocuments that need validation even if
3362
+ // the map itself has no validators
3363
+ !type.$isSchemaMap &&
3364
+ !type.$isSchemaUnion) {
3365
+ return true;
3366
+ }
3367
+ // gh-11380
3368
+ return !!type.$isMongooseArray &&
3369
+ !type.$isMongooseDocumentArray && // Skip document arrays...
3370
+ !type.embeddedSchemaType.$isMongooseArray && // and arrays of arrays
3371
+ type.validators.length === 0 &&
3372
+ type.embeddedSchemaType.validators.length === 0;
3373
+ }
3374
+
3375
+ function _addArrayPathsToValidate(doc, paths, doValidateOptions) {
3279
3376
  for (const path of paths) {
3280
3377
  const _pathType = doc.$__schema.path(path);
3281
3378
  if (!_pathType) {
@@ -3283,12 +3380,12 @@ function _addArrayPathsToValidate(doc, paths) {
3283
3380
  }
3284
3381
 
3285
3382
  if (!_pathType.$isMongooseArray ||
3286
- // To avoid potential performance issues, skip doc arrays whose children
3287
- // are not required. `getPositionalPathType()` may be slow, so avoid
3288
- // it unless we have a case of #6364
3383
+ // To avoid potential performance issues, skip doc arrays whose elements
3384
+ // have no validators of their own. `getPositionalPathType()` may be
3385
+ // slow, so avoid it unless we have a case of #6364
3289
3386
  (!Array.isArray(_pathType) &&
3290
3387
  _pathType.$isMongooseDocumentArray &&
3291
- !_pathType?.schemaOptions?.required)) {
3388
+ !_pathType.embeddedSchemaType?.validators.length)) {
3292
3389
  continue;
3293
3390
  }
3294
3391
 
@@ -3302,6 +3399,23 @@ function _addArrayPathsToValidate(doc, paths) {
3302
3399
  }
3303
3400
 
3304
3401
  const val = doc.$__getValue(path);
3402
+ if (_pathType.$isMongooseDocumentArray) {
3403
+ // We only get here because the document array itself is being validated,
3404
+ // which validates every subdocument. Leave the subdocuments to it and run
3405
+ // just the element-level validators here, so nothing validates twice.
3406
+ const numElements = val?.length ?? 0;
3407
+ for (let j = 0; j < numElements; ++j) {
3408
+ const elementPath = path + '.' + j;
3409
+ paths.add(elementPath);
3410
+ doValidateOptions[elementPath] = {
3411
+ ...doValidateOptions[path],
3412
+ ...doValidateOptions[elementPath],
3413
+ skipSubdocumentValidation: true,
3414
+ index: j
3415
+ };
3416
+ }
3417
+ continue;
3418
+ }
3305
3419
  _pushNestedArrayPaths(val, paths, path);
3306
3420
  }
3307
3421
  }
@@ -3313,7 +3427,12 @@ function _pushNestedArrayPaths(val, paths, path) {
3313
3427
  if (Array.isArray(val[j])) {
3314
3428
  _pushNestedArrayPaths(val[j], paths, path + '.' + j);
3315
3429
  } else {
3316
- paths.add(path + '.' + j);
3430
+ if (Array.isArray(paths)) {
3431
+ paths.push(path + '.' + j);
3432
+ } else {
3433
+ // Set
3434
+ paths.add(path + '.' + j);
3435
+ }
3317
3436
  }
3318
3437
  }
3319
3438
  }
@@ -3359,9 +3478,21 @@ function _handlePathsToValidate(paths, pathsToValidate) {
3359
3478
  }
3360
3479
  }
3361
3480
 
3481
+ // A Set: several paths can map onto the same `parentPaths` entry.
3362
3482
  const ret = new Set();
3363
3483
  for (const path of paths) {
3364
- if (_pathsToValidate.has(path)) {
3484
+ let isUnderPathToValidate = false;
3485
+ let index = path.length;
3486
+ // Check if parent of this path is in `pathsToValidate`, e.g. if
3487
+ // `profile` is in `pathsToValidate` then we should validate `profile.age`.
3488
+ while ((index = path.lastIndexOf('.', index - 1)) !== -1) {
3489
+ if (_pathsToValidate.has(path.slice(0, index))) {
3490
+ isUnderPathToValidate = true;
3491
+ break;
3492
+ }
3493
+ }
3494
+
3495
+ if (_pathsToValidate.has(path) || isUnderPathToValidate) {
3365
3496
  ret.add(path);
3366
3497
  } else if (parentPaths.has(path)) {
3367
3498
  ret.add(parentPaths.get(path));
@@ -3376,8 +3507,12 @@ function _handlePathsToValidate(paths, pathsToValidate) {
3376
3507
 
3377
3508
  function _handlePathsToSkip(paths, pathsToSkip) {
3378
3509
  pathsToSkip = new Set(pathsToSkip);
3379
- paths = Array.from(paths).filter(p => !pathsToSkip.has(p));
3380
- return new Set(paths);
3510
+ for (const path of paths) {
3511
+ if (pathsToSkip.has(path)) {
3512
+ paths.delete(path);
3513
+ }
3514
+ }
3515
+ return paths;
3381
3516
  }
3382
3517
 
3383
3518
  /**
@@ -3457,30 +3592,15 @@ Document.prototype.$__validateSync = function(pathsToValidate, options) {
3457
3592
 
3458
3593
  // only validate required fields when necessary
3459
3594
  let paths;
3460
- let skipSchemaValidators;
3595
+ let doValidateOptionsByPath;
3461
3596
  if (validateAllPaths) {
3462
- paths = new Set(Object.keys(this.$__schema.paths));
3463
- // gh-661: if a whole array is modified, make sure to run validation on all
3464
- // the children as well
3465
- for (const path of paths) {
3466
- const schemaType = this.$__schema.path(path);
3467
- if (!schemaType?.$isMongooseArray) {
3468
- continue;
3469
- }
3470
- const val = this.$__getValue(path);
3471
- if (!val) {
3472
- continue;
3473
- }
3474
- _pushNestedArrayPaths(val, paths, path);
3475
- }
3476
- paths = [...paths];
3477
- skipSchemaValidators = {};
3597
+ [paths, doValidateOptionsByPath] = _getAllPathsToValidate(this);
3478
3598
  } else {
3479
3599
  const pathDetails = _getPathsToValidate(this, pathsToValidate, pathsToSkip);
3480
3600
  paths = shouldValidateModifiedOnly ?
3481
3601
  pathDetails[0].filter((path) => this.$isModified(path)) :
3482
3602
  pathDetails[0];
3483
- skipSchemaValidators = pathDetails[1];
3603
+ doValidateOptionsByPath = pathDetails[1];
3484
3604
  }
3485
3605
 
3486
3606
  const validating = {};
@@ -3503,11 +3623,15 @@ Document.prototype.$__validateSync = function(pathsToValidate, options) {
3503
3623
  }
3504
3624
 
3505
3625
  const val = _this.$__getValue(path);
3626
+ const pathOptions = doValidateOptionsByPath[path];
3506
3627
  const err = p.doValidateSync(val, _this, {
3507
- skipSchemaValidators: skipSchemaValidators[path],
3628
+ ...(pathOptions != null && typeof pathOptions === 'object' ? pathOptions : null),
3629
+ skipSchemaValidators: typeof pathOptions === 'object' ?
3630
+ pathOptions.skipSchemaValidators : pathOptions,
3508
3631
  path: path,
3509
3632
  validateModifiedOnly: shouldValidateModifiedOnly,
3510
- validateAllPaths
3633
+ validateAllPaths,
3634
+ _nestedValidate: true
3511
3635
  });
3512
3636
  if (err) {
3513
3637
  const isSubdoc = p.$isSingleNested ||