mongoose 9.10.4 → 9.11.1

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