mongoose 9.10.4 → 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 +338 -214
- package/lib/query.js +29 -0
- package/lib/schema/documentArray.js +34 -13
- package/lib/schema/documentArrayElement.js +32 -0
- package/package.json +2 -2
- package/tstyche.matrix.json +9 -0
- package/types/connection.d.ts +1 -1
- package/types/document.d.ts +3 -1
- package/types/index.d.ts +1 -1
- package/types/models.d.ts +56 -31
- package/types/query.d.ts +84 -62
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 =
|
|
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
|
|
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
|
-
*
|
|
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
|
|
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
|
-
//
|
|
3033
|
-
//
|
|
3034
|
-
|
|
3035
|
-
const
|
|
3036
|
-
|
|
3037
|
-
|
|
3038
|
-
|
|
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
|
-
|
|
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
|
-
|
|
3071
|
-
|
|
3072
|
-
|
|
3073
|
-
|
|
3074
|
-
|
|
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
|
|
3085
|
-
|
|
3086
|
-
|
|
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
|
|
3091
|
-
|
|
3092
|
-
//
|
|
3093
|
-
|
|
3094
|
-
|
|
3095
|
-
|
|
3096
|
-
|
|
3097
|
-
|
|
3098
|
-
|
|
3099
|
-
|
|
3100
|
-
|
|
3101
|
-
|
|
3102
|
-
|
|
3103
|
-
|
|
3104
|
-
|
|
3105
|
-
|
|
3106
|
-
|
|
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.
|
|
3120
|
-
return [
|
|
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
|
-
|
|
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
|
-
|
|
3126
|
-
|
|
3127
|
-
|
|
3128
|
-
|
|
3129
|
-
|
|
3130
|
-
const
|
|
3131
|
-
|
|
3132
|
-
|
|
3133
|
-
|
|
3134
|
-
|
|
3135
|
-
|
|
3136
|
-
|
|
3137
|
-
|
|
3138
|
-
|
|
3139
|
-
|
|
3140
|
-
|
|
3141
|
-
|
|
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
|
-
|
|
3233
|
+
}
|
|
3150
3234
|
|
|
3151
|
-
|
|
3152
|
-
|
|
3153
|
-
|
|
3154
|
-
const
|
|
3155
|
-
if (
|
|
3156
|
-
|
|
3157
|
-
|
|
3158
|
-
|
|
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
|
-
|
|
3163
|
-
|
|
3164
|
-
|
|
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
|
-
|
|
3167
|
-
|
|
3168
|
-
|
|
3169
|
-
|
|
3170
|
-
|
|
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
|
-
|
|
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
|
-
|
|
3179
|
-
|
|
3180
|
-
|
|
3181
|
-
|
|
3182
|
-
|
|
3183
|
-
|
|
3184
|
-
|
|
3185
|
-
|
|
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
|
-
|
|
3193
|
-
|
|
3194
|
-
|
|
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
|
-
|
|
3202
|
-
|
|
3203
|
-
|
|
3204
|
-
|
|
3205
|
-
|
|
3206
|
-
|
|
3207
|
-
|
|
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
|
-
//
|
|
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 =
|
|
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
|
-
|
|
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
|
-
|
|
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
|
|
3287
|
-
//
|
|
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?.
|
|
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
|
-
|
|
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
|
-
|
|
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
|
-
|
|
3380
|
-
|
|
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
|
|
3595
|
+
let doValidateOptionsByPath;
|
|
3461
3596
|
if (validateAllPaths) {
|
|
3462
|
-
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
|
-
|
|
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
|
-
|
|
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 ||
|