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/cast.js +9 -19
- package/lib/cursor/queryCursor.js +4 -1
- package/lib/document.js +344 -219
- package/lib/helpers/discriminator/getDiscriminatorValueFromFilter.js +30 -0
- package/lib/helpers/document/toObjectRecursive.js +49 -0
- package/lib/helpers/query/getEmbeddedDiscriminatorPath.js +4 -3
- package/lib/helpers/update/castArrayFilters.js +1 -1
- package/lib/query.js +29 -0
- package/lib/schema/array.js +13 -0
- package/lib/schema/documentArray.js +38 -17
- 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 +2 -2
- package/types/models.d.ts +66 -35
- 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
|
|
|
@@ -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
|
-
|
|
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 =
|
|
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
|
|
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
|
-
*
|
|
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
|
|
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
|
-
//
|
|
3033
|
-
//
|
|
3034
|
-
|
|
3035
|
-
const
|
|
3036
|
-
|
|
3037
|
-
|
|
3038
|
-
|
|
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
|
-
|
|
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
|
-
|
|
3071
|
-
|
|
3072
|
-
|
|
3073
|
-
|
|
3074
|
-
|
|
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
|
|
3085
|
-
|
|
3086
|
-
|
|
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
|
|
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
|
-
}
|
|
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.
|
|
3120
|
-
return [
|
|
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
|
-
|
|
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
|
-
|
|
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
|
-
}
|
|
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
|
-
|
|
3232
|
+
}
|
|
3150
3233
|
|
|
3151
|
-
|
|
3152
|
-
|
|
3153
|
-
|
|
3154
|
-
const
|
|
3155
|
-
if (
|
|
3156
|
-
|
|
3157
|
-
|
|
3158
|
-
|
|
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
|
-
|
|
3163
|
-
|
|
3164
|
-
|
|
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
|
-
|
|
3167
|
-
|
|
3168
|
-
|
|
3169
|
-
|
|
3170
|
-
|
|
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
|
-
|
|
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
|
-
|
|
3179
|
-
|
|
3180
|
-
|
|
3181
|
-
|
|
3182
|
-
|
|
3183
|
-
|
|
3184
|
-
|
|
3185
|
-
|
|
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
|
-
|
|
3193
|
-
|
|
3194
|
-
|
|
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
|
-
|
|
3202
|
-
|
|
3203
|
-
|
|
3204
|
-
|
|
3205
|
-
|
|
3206
|
-
|
|
3207
|
-
|
|
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
|
-
//
|
|
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 =
|
|
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
|
-
|
|
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
|
-
|
|
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
|
|
3287
|
-
//
|
|
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?.
|
|
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
|
-
|
|
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
|
-
|
|
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
|
-
|
|
3380
|
-
|
|
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
|
|
3594
|
+
let doValidateOptionsByPath;
|
|
3461
3595
|
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 = {};
|
|
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
|
-
|
|
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
|
-
|
|
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 = {};
|