@depup/mongoose 9.4.1-depup.0 → 9.8.1-depup.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/README.md +3 -9
- package/changes.json +3 -8
- package/eslint.config.mjs +4 -1
- package/lib/aggregate.js +54 -24
- package/lib/cast.js +1 -1
- package/lib/connection.js +1 -1
- package/lib/cursor/aggregationCursor.js +47 -16
- package/lib/cursor/queryCursor.js +23 -7
- package/lib/document.js +140 -61
- package/lib/drivers/node-mongodb-native/collection.js +10 -4
- package/lib/error/cast.js +18 -9
- package/lib/error/divergentArray.js +1 -1
- package/lib/error/messages.js +1 -0
- package/lib/helpers/clone.js +22 -5
- package/lib/helpers/document/applyDefaults.js +5 -0
- package/lib/helpers/populate/createPopulateQueryFilter.js +9 -1
- package/lib/helpers/populate/getModelsMapForPopulate.js +8 -6
- package/lib/helpers/populate/splitPopulateQuery.js +81 -0
- package/lib/helpers/query/castUpdate.js +31 -10
- package/lib/helpers/schema/getKeysInSchemaOrder.js +13 -16
- package/lib/helpers/update/applyTimestampsToUpdate.js +4 -2
- package/lib/model.js +153 -38
- package/lib/mongoose.js +3 -3
- package/lib/options/schemaTypeOptions.js +14 -0
- package/lib/plugins/saveSubdocs.js +16 -13
- package/lib/query.js +208 -102
- package/lib/queryHelpers.js +13 -12
- package/lib/schema/array.js +3 -3
- package/lib/schema/date.js +7 -5
- package/lib/schema/documentArray.js +1 -1
- package/lib/schema/objectId.js +7 -1
- package/lib/schema/string.js +19 -2
- package/lib/schema/subdocument.js +1 -1
- package/lib/schema/union.js +2 -2
- package/lib/schema.js +38 -12
- package/lib/schemaType.js +58 -10
- package/lib/standardSchema/convertErrorToIssues.js +20 -0
- package/lib/stateMachine.js +11 -6
- package/lib/tracing.js +38 -0
- package/lib/types/documentArray/methods/index.js +117 -3
- package/lib/types/subdocument.js +4 -2
- package/lib/utils.js +3 -2
- package/lib/validOptions.js +1 -0
- package/package.json +27 -30
- package/tstyche.json +2 -1
- package/types/augmentations.d.ts +2 -2
- package/types/document.d.ts +21 -4
- package/types/expressions.d.ts +16 -0
- package/types/index.d.ts +15 -10
- package/types/inferhydrateddoctype.d.ts +1 -1
- package/types/inferrawdoctype.d.ts +6 -3
- package/types/inferschematype.d.ts +10 -2
- package/types/models.d.ts +38 -12
- package/types/mongooseoptions.d.ts +9 -1
- package/types/populate.d.ts +8 -0
- package/types/query.d.ts +13 -3
- package/types/schemaoptions.d.ts +5 -0
- package/types/schematypes.d.ts +10 -0
- package/types/tracing.d.ts +10 -0
- package/types/utility.d.ts +9 -0
package/lib/document.js
CHANGED
|
@@ -143,7 +143,7 @@ function Document(obj, fields, options) {
|
|
|
143
143
|
this.$__.strictMode = schema.options.strict;
|
|
144
144
|
}
|
|
145
145
|
|
|
146
|
-
const requiredPaths = schema.requiredPaths(
|
|
146
|
+
const requiredPaths = schema.requiredPaths();
|
|
147
147
|
for (const path of requiredPaths) {
|
|
148
148
|
this.$__.activePaths.require(path);
|
|
149
149
|
}
|
|
@@ -562,27 +562,26 @@ function $applyDefaultsToNested(val, path, doc) {
|
|
|
562
562
|
Document.prototype.$__buildDoc = function(obj, fields, skipId, exclude, hasIncludedChildren) {
|
|
563
563
|
const doc = {};
|
|
564
564
|
|
|
565
|
-
const paths = Object.keys(this.$__schema.paths)
|
|
566
|
-
// Don't build up any paths that are underneath a map, we don't know
|
|
567
|
-
// what the keys will be
|
|
568
|
-
filter(p => !p.includes('$*'));
|
|
565
|
+
const paths = Object.keys(this.$__schema.paths);
|
|
569
566
|
const plen = paths.length;
|
|
570
567
|
let ii = 0;
|
|
571
568
|
|
|
572
569
|
for (; ii < plen; ++ii) {
|
|
573
570
|
const p = paths[ii];
|
|
574
571
|
|
|
575
|
-
|
|
576
|
-
|
|
577
|
-
|
|
578
|
-
|
|
579
|
-
if (obj && '_id' in obj) {
|
|
580
|
-
continue;
|
|
581
|
-
}
|
|
572
|
+
// Don't build up any paths that are underneath a map, we don't know
|
|
573
|
+
// what the keys will be
|
|
574
|
+
if (p.includes('$*')) {
|
|
575
|
+
continue;
|
|
582
576
|
}
|
|
583
577
|
|
|
584
578
|
const path = this.$__schema.paths[p].splitPath();
|
|
585
579
|
const len = path.length;
|
|
580
|
+
// This loop only creates intermediate objects for nested paths: it never
|
|
581
|
+
// sets any leaf values, so single-segment paths are no-ops.
|
|
582
|
+
if (len === 1) {
|
|
583
|
+
continue;
|
|
584
|
+
}
|
|
586
585
|
const last = len - 1;
|
|
587
586
|
let curPath = '';
|
|
588
587
|
let doc_ = doc;
|
|
@@ -757,6 +756,7 @@ function init(self, obj, doc, opts, prefix) {
|
|
|
757
756
|
let i;
|
|
758
757
|
const strict = self.$__.strictMode;
|
|
759
758
|
const docSchema = self.$__schema;
|
|
759
|
+
const strictRead = docSchema.options.strictRead;
|
|
760
760
|
|
|
761
761
|
for (let index = 0; index < len; ++index) {
|
|
762
762
|
i = keys[index];
|
|
@@ -774,6 +774,14 @@ function init(self, obj, doc, opts, prefix) {
|
|
|
774
774
|
}
|
|
775
775
|
|
|
776
776
|
const value = obj[i];
|
|
777
|
+
if (!schemaType && strictRead && docSchema.pathType(path) === 'adhocOrUndefined') {
|
|
778
|
+
if (strictRead === 'throw') {
|
|
779
|
+
throw new StrictModeError(path, 'Field `' + path + '` is not in schema and strictRead is set to throw.');
|
|
780
|
+
} else if (strictRead === true) {
|
|
781
|
+
continue;
|
|
782
|
+
}
|
|
783
|
+
}
|
|
784
|
+
|
|
777
785
|
if (!schemaType && utils.isPOJO(value)) {
|
|
778
786
|
// assume nested object
|
|
779
787
|
if (!doc[i]) {
|
|
@@ -786,6 +794,13 @@ function init(self, obj, doc, opts, prefix) {
|
|
|
786
794
|
}
|
|
787
795
|
init(self, value, doc[i], opts, path + '.');
|
|
788
796
|
} else if (!schemaType) {
|
|
797
|
+
// Handle strictRead: filter unknown fields during document hydration from DB
|
|
798
|
+
if (strictRead === 'throw') {
|
|
799
|
+
throw new StrictModeError(path, 'Field `' + path + '` is not in schema and strictRead is set to throw.');
|
|
800
|
+
} else if (strictRead === true) {
|
|
801
|
+
// Skip this field - do not store in _doc
|
|
802
|
+
continue;
|
|
803
|
+
}
|
|
789
804
|
doc[i] = value;
|
|
790
805
|
if (!strict && !prefix) {
|
|
791
806
|
self[i] = value;
|
|
@@ -1133,7 +1148,11 @@ Document.prototype.$set = function $set(path, val, type, options) {
|
|
|
1133
1148
|
return this;
|
|
1134
1149
|
}
|
|
1135
1150
|
|
|
1136
|
-
|
|
1151
|
+
// Only need to clone if we're overwriting `_skipMinimizeTopLevel`, recursive
|
|
1152
|
+
// calls treat a missing `_skipMinimizeTopLevel` as `false`
|
|
1153
|
+
if (_skipMinimizeTopLevel) {
|
|
1154
|
+
options = Object.assign({}, options, { _skipMinimizeTopLevel: false });
|
|
1155
|
+
}
|
|
1137
1156
|
|
|
1138
1157
|
for (let i = 0; i < len; ++i) {
|
|
1139
1158
|
key = keys[i];
|
|
@@ -1220,15 +1239,9 @@ Document.prototype.$set = function $set(path, val, type, options) {
|
|
|
1220
1239
|
val = handleSpreadDoc(val, true);
|
|
1221
1240
|
|
|
1222
1241
|
// if this doc is being constructed we should not trigger getters
|
|
1223
|
-
const priorVal =
|
|
1224
|
-
|
|
1225
|
-
|
|
1226
|
-
}
|
|
1227
|
-
if (constructing) {
|
|
1228
|
-
return void 0;
|
|
1229
|
-
}
|
|
1230
|
-
return this.$__getValue(path);
|
|
1231
|
-
})();
|
|
1242
|
+
const priorVal = this.$__.priorDoc != null ?
|
|
1243
|
+
this.$__.priorDoc.$__getValue(path) :
|
|
1244
|
+
(constructing ? void 0 : this.$__getValue(path));
|
|
1232
1245
|
|
|
1233
1246
|
if (pathType === 'nested' && val) {
|
|
1234
1247
|
if (typeof val === 'object' && val != null) {
|
|
@@ -1410,13 +1423,10 @@ Document.prototype.$set = function $set(path, val, type, options) {
|
|
|
1410
1423
|
try {
|
|
1411
1424
|
// If the user is trying to set a ref path to a document with
|
|
1412
1425
|
// the correct model name, treat it as populated
|
|
1413
|
-
const refMatches = (() => {
|
|
1426
|
+
const refMatches = !(val instanceof Document) ? false : (() => {
|
|
1414
1427
|
if (schema.options == null) {
|
|
1415
1428
|
return false;
|
|
1416
1429
|
}
|
|
1417
|
-
if (!(val instanceof Document)) {
|
|
1418
|
-
return false;
|
|
1419
|
-
}
|
|
1420
1430
|
const model = val.constructor;
|
|
1421
1431
|
|
|
1422
1432
|
// Check ref
|
|
@@ -1954,7 +1964,9 @@ Document.prototype.get = function(path, type, options) {
|
|
|
1954
1964
|
|
|
1955
1965
|
// Fast path if we know we're just accessing top-level path on the document:
|
|
1956
1966
|
// just get the schema path, avoid `$__path()` because that does string manipulation
|
|
1957
|
-
let schema = noDottedPath ?
|
|
1967
|
+
let schema = noDottedPath ?
|
|
1968
|
+
Object.hasOwn(this.$__schema.paths, path) ? this.$__schema.paths[path] : undefined :
|
|
1969
|
+
this.$__path(path);
|
|
1958
1970
|
if (schema == null) {
|
|
1959
1971
|
schema = this.$__schema.virtualpath(path);
|
|
1960
1972
|
|
|
@@ -1964,7 +1976,7 @@ Document.prototype.get = function(path, type, options) {
|
|
|
1964
1976
|
}
|
|
1965
1977
|
|
|
1966
1978
|
if (noDottedPath) {
|
|
1967
|
-
let obj = this._doc[path];
|
|
1979
|
+
let obj = Object.hasOwn(this._doc, path) ? this._doc[path] : undefined;
|
|
1968
1980
|
if (adhoc) {
|
|
1969
1981
|
obj = adhoc.cast(obj);
|
|
1970
1982
|
}
|
|
@@ -1991,6 +2003,10 @@ Document.prototype.get = function(path, type, options) {
|
|
|
1991
2003
|
}
|
|
1992
2004
|
|
|
1993
2005
|
for (let i = 0, l = pieces.length; i < l; i++) {
|
|
2006
|
+
if (specialProperties.has(pieces[i])) {
|
|
2007
|
+
return undefined;
|
|
2008
|
+
}
|
|
2009
|
+
|
|
1994
2010
|
if (obj?._doc) {
|
|
1995
2011
|
obj = obj._doc;
|
|
1996
2012
|
}
|
|
@@ -2012,7 +2028,7 @@ Document.prototype.get = function(path, type, options) {
|
|
|
2012
2028
|
|
|
2013
2029
|
if (schema != null && options.getters !== false) {
|
|
2014
2030
|
obj = schema.applyGetters(obj, this);
|
|
2015
|
-
} else if (this.$__schema.nested
|
|
2031
|
+
} else if (Object.hasOwn(this.$__schema.nested, path) && options.virtuals) {
|
|
2016
2032
|
// Might need to apply virtuals if this is a nested path
|
|
2017
2033
|
return applyVirtuals(this, clone(obj) || {}, { path: path });
|
|
2018
2034
|
}
|
|
@@ -2870,7 +2886,8 @@ Document.prototype.validate = async function validate(pathsToValidate, options)
|
|
|
2870
2886
|
|
|
2871
2887
|
await this._execDocumentPostHooks('validate', options, error);
|
|
2872
2888
|
} finally {
|
|
2873
|
-
delete
|
|
2889
|
+
// Assign rather than `delete` to avoid putting `$__` in dictionary mode
|
|
2890
|
+
this.$__.validateModifiedOnly = undefined;
|
|
2874
2891
|
this.$op = null;
|
|
2875
2892
|
this.$__.validating = null;
|
|
2876
2893
|
}
|
|
@@ -2913,7 +2930,7 @@ function _completeValidate(doc) {
|
|
|
2913
2930
|
}
|
|
2914
2931
|
}
|
|
2915
2932
|
|
|
2916
|
-
doc.$__.cachedRequired =
|
|
2933
|
+
doc.$__.cachedRequired = null;
|
|
2917
2934
|
doc.$emit('validate', doc);
|
|
2918
2935
|
doc.constructor.emit('validate', doc);
|
|
2919
2936
|
|
|
@@ -2958,9 +2975,13 @@ function _getPathsToValidate(doc, pathsToValidate, pathsToSkip, isNestedValidate
|
|
|
2958
2975
|
const doValidateOptions = {};
|
|
2959
2976
|
|
|
2960
2977
|
_evaluateRequiredFunctions(doc);
|
|
2978
|
+
// gh-16379: compute the modified-path set at most once and reuse it below, so
|
|
2979
|
+
// checking many required paths doesn't rebuild it per-path (O(required x modified)).
|
|
2980
|
+
let _modifiedPaths;
|
|
2981
|
+
const getModifiedPaths = () => (_modifiedPaths ??= doc.modifiedPaths());
|
|
2961
2982
|
// only validate required fields when necessary
|
|
2962
2983
|
let paths = new Set(Object.keys(doc.$__.activePaths.getStatePaths('require')).filter(function(path) {
|
|
2963
|
-
if (!doc.$__isSelected(path) && !doc.$isModified(path)) {
|
|
2984
|
+
if (!doc.$__isSelected(path) && !doc.$isModified(path, null, getModifiedPaths())) {
|
|
2964
2985
|
return false;
|
|
2965
2986
|
}
|
|
2966
2987
|
if (path.endsWith('.$*')) {
|
|
@@ -3039,7 +3060,7 @@ function _getPathsToValidate(doc, pathsToValidate, pathsToSkip, isNestedValidate
|
|
|
3039
3060
|
}
|
|
3040
3061
|
}
|
|
3041
3062
|
}
|
|
3042
|
-
const modifiedPaths =
|
|
3063
|
+
const modifiedPaths = getModifiedPaths();
|
|
3043
3064
|
for (const subdoc of topLevelSubdocs) {
|
|
3044
3065
|
if (subdoc.$basePath) {
|
|
3045
3066
|
const fullPathToSubdoc = subdoc.$__pathRelativeToParent();
|
|
@@ -3204,7 +3225,7 @@ function _pushNestedArrayPaths(val, paths, path) {
|
|
|
3204
3225
|
* ignore
|
|
3205
3226
|
*/
|
|
3206
3227
|
|
|
3207
|
-
Document.prototype._execDocumentPreHooks =
|
|
3228
|
+
Document.prototype._execDocumentPreHooks = function _execDocumentPreHooks(opName, options, argsForHooks) {
|
|
3208
3229
|
const filter = buildMiddlewareFilter(options, 'pre');
|
|
3209
3230
|
return this.$__middleware.execPre(opName, this, argsForHooks || [], { filter });
|
|
3210
3231
|
};
|
|
@@ -3213,7 +3234,7 @@ Document.prototype._execDocumentPreHooks = async function _execDocumentPreHooks(
|
|
|
3213
3234
|
* ignore
|
|
3214
3235
|
*/
|
|
3215
3236
|
|
|
3216
|
-
Document.prototype._execDocumentPostHooks =
|
|
3237
|
+
Document.prototype._execDocumentPostHooks = function _execDocumentPostHooks(opName, options, error) {
|
|
3217
3238
|
const filter = buildMiddlewareFilter(options, 'post');
|
|
3218
3239
|
return this.$__middleware.execPost(opName, this, [this], { error, filter });
|
|
3219
3240
|
};
|
|
@@ -3264,6 +3285,9 @@ function _handlePathsToSkip(paths, pathsToSkip) {
|
|
|
3264
3285
|
/**
|
|
3265
3286
|
* Executes registered validation rules (skipping asynchronous validators) for this document.
|
|
3266
3287
|
*
|
|
3288
|
+
* **Deprecated.** Use [`validate()`](https://mongoosejs.com/docs/api/document.html#Document.prototype.validate()) instead.
|
|
3289
|
+
* `validateSync()` does not run validation middleware and will be removed in Mongoose 10.
|
|
3290
|
+
*
|
|
3267
3291
|
* #### Note:
|
|
3268
3292
|
*
|
|
3269
3293
|
* This method is useful if you need synchronous validation.
|
|
@@ -3281,14 +3305,21 @@ function _handlePathsToSkip(paths, pathsToSkip) {
|
|
|
3281
3305
|
* @param {object} [options] options for validation
|
|
3282
3306
|
* @param {boolean} [options.validateModifiedOnly=false] If `true`, Mongoose will only validate modified paths, as opposed to modified paths and `required` paths.
|
|
3283
3307
|
* @param {Array|string} [options.pathsToSkip] list of paths to skip. If set, Mongoose will validate every modified path that is not in this list.
|
|
3284
|
-
* @param {boolean|object} [options.middleware=true] set to `false` to skip all user-defined middleware
|
|
3285
|
-
* @param {boolean} [options.middleware.pre=true] set to `false` to skip only pre hooks
|
|
3286
|
-
* @param {boolean} [options.middleware.post=true] set to `false` to skip only post hooks
|
|
3287
3308
|
* @return {ValidationError|undefined} ValidationError if there are errors during validation, or undefined if there is no error.
|
|
3309
|
+
* @deprecated Use `validate()` instead. `validateSync()` does not run middleware and will be removed in Mongoose 10.
|
|
3288
3310
|
* @api public
|
|
3289
3311
|
*/
|
|
3290
3312
|
|
|
3291
|
-
Document.prototype.validateSync = function(
|
|
3313
|
+
Document.prototype.validateSync = function() {
|
|
3314
|
+
utils.warn('Mongoose: `Document.prototype.validateSync()` is deprecated and will be removed in Mongoose 10. Use `Document.prototype.validate()` instead.');
|
|
3315
|
+
return this.$__validateSync.apply(this, arguments);
|
|
3316
|
+
};
|
|
3317
|
+
|
|
3318
|
+
/*!
|
|
3319
|
+
* ignore
|
|
3320
|
+
*/
|
|
3321
|
+
|
|
3322
|
+
Document.prototype.$__validateSync = function(pathsToValidate, options) {
|
|
3292
3323
|
const _this = this;
|
|
3293
3324
|
|
|
3294
3325
|
if (arguments.length === 1 && typeof arguments[0] === 'object' && !Array.isArray(arguments[0])) {
|
|
@@ -3556,6 +3587,10 @@ function _checkImmutableSubpaths(subdoc, schematype, priorVal) {
|
|
|
3556
3587
|
* @param {number} [options.wtimeout] sets a [timeout for the write concern](https://www.mongodb.com/docs/manual/reference/write-concern/#wtimeout). Overrides the [schema-level `writeConcern` option](https://mongoosejs.com/docs/guide.html#writeConcern).
|
|
3557
3588
|
* @param {boolean} [options.checkKeys=true] the MongoDB driver prevents you from saving keys that start with '$' or contain '.' by default. Set this option to `false` to skip that check. See [restrictions on field names](https://www.mongodb.com/docs/manual/reference/limits/#Restrictions-on-Field-Names)
|
|
3558
3589
|
* @param {boolean} [options.timestamps=true] if `false` and [timestamps](https://mongoosejs.com/docs/guide.html#timestamps) are enabled, skip timestamps for this `save()`.
|
|
3590
|
+
* @param {Array} [options.pathsToSave] An array of paths that tell mongoose to only validate and save the paths in `pathsToSave`.
|
|
3591
|
+
* @param {boolean|object} [options.middleware=true] set to `false` to skip all user-defined middleware
|
|
3592
|
+
* @param {boolean} [options.middleware.pre=true] set to `false` to skip only pre hooks
|
|
3593
|
+
* @param {boolean} [options.middleware.post=true] set to `false` to skip only post hooks
|
|
3559
3594
|
* @method save
|
|
3560
3595
|
* @memberOf Document
|
|
3561
3596
|
* @instance
|
|
@@ -3620,7 +3655,11 @@ Document.prototype.$__reset = function reset() {
|
|
|
3620
3655
|
// Clear atomics on dirty paths. Walk the modified and default paths
|
|
3621
3656
|
// directly instead of calling $__dirty(), which builds intermediate
|
|
3622
3657
|
// arrays, a Map, and does parent-path deduplication we don't need here.
|
|
3623
|
-
|
|
3658
|
+
// Skip entirely if the doc only has primitive values, because only arrays
|
|
3659
|
+
// and maps have atomics.
|
|
3660
|
+
if (!onlyPrimitiveValues) {
|
|
3661
|
+
this.$__resetAtomics();
|
|
3662
|
+
}
|
|
3624
3663
|
|
|
3625
3664
|
this.$__.backup = {};
|
|
3626
3665
|
this.$__.backup.activePaths = {
|
|
@@ -3750,15 +3789,17 @@ Document.prototype.$__dirty = function() {
|
|
|
3750
3789
|
}
|
|
3751
3790
|
|
|
3752
3791
|
let top = null;
|
|
3792
|
+
let foundParent = false;
|
|
3753
3793
|
|
|
3754
3794
|
const array = parentPaths(item.path);
|
|
3755
3795
|
for (let i = 0; i < array.length - 1; i++) {
|
|
3756
3796
|
if (allPaths.has(array[i])) {
|
|
3757
3797
|
top = allPaths.get(array[i]);
|
|
3798
|
+
foundParent = true;
|
|
3758
3799
|
break;
|
|
3759
3800
|
}
|
|
3760
3801
|
}
|
|
3761
|
-
if (
|
|
3802
|
+
if (!foundParent) {
|
|
3762
3803
|
minimal.push(item);
|
|
3763
3804
|
} else if (top != null &&
|
|
3764
3805
|
top[arrayAtomicsSymbol] != null &&
|
|
@@ -4701,6 +4742,9 @@ Document.prototype.equals = function(doc) {
|
|
|
4701
4742
|
* @param {boolean} [options.getters=false] if true, Mongoose will call any getters defined on the `localField`. By default, Mongoose gets the raw value of `localField`. For example, you would need to set this option to `true` if you wanted to [add a `lowercase` getter to your `localField`](https://mongoosejs.com/docs/schematypes.html#schematype-options).
|
|
4702
4743
|
* @param {boolean} [options.clone=false] When you do `BlogPost.find().populate('author')`, blog posts with the same author will share 1 copy of an `author` doc. Enable this option to make Mongoose clone populated docs before assigning them.
|
|
4703
4744
|
* @param {object|Function} [options.match=null] Add an additional filter to the populate query. Can be a filter object containing [MongoDB query syntax](https://www.mongodb.com/docs/manual/tutorial/query-documents/), or a function that returns a filter object.
|
|
4745
|
+
* @param {boolean} [options.skipInvalidIds=false] By default, Mongoose throws a cast error if `localField` and `foreignField` schemas don't line up. If you enable this option, Mongoose will instead filter out any `localField` properties that cannot be casted to `foreignField`'s schema type.
|
|
4746
|
+
* @param {number} [options.perDocumentLimit=null] For legacy reasons, `limit` with `populate()` may give incorrect results when populating multiple parent documents because it executes a single query for all parents. If you set `perDocumentLimit`, Mongoose will execute a separate query per parent document to ensure correct per-document `limit`.
|
|
4747
|
+
* @param {boolean} [options.strictPopulate=true] Set to false to allow populating paths that aren't defined in the given model's schema.
|
|
4704
4748
|
* @param {Function} [options.transform=null] Function that Mongoose will call on every populated document that allows you to transform the populated document.
|
|
4705
4749
|
* @param {object} [options.options=null] Additional options like `limit` and `lean`.
|
|
4706
4750
|
* @param {boolean} [options.forceRepopulate=true] Set to `false` to prevent Mongoose from repopulating paths that are already populated
|
|
@@ -5112,20 +5156,26 @@ Document.prototype.$__delta = function $__delta(pathsToSave, pathsToSaveSet) {
|
|
|
5112
5156
|
const optimisticConcurrency = this.$__schema.options.optimisticConcurrency;
|
|
5113
5157
|
if (optimisticConcurrency) {
|
|
5114
5158
|
if (Array.isArray(optimisticConcurrency)) {
|
|
5115
|
-
|
|
5116
|
-
|
|
5159
|
+
if (!this.$__schema.options._optimisticConcurrencySet) {
|
|
5160
|
+
this.$__schema.options._optimisticConcurrencySet = new Set(optimisticConcurrency);
|
|
5161
|
+
}
|
|
5162
|
+
const optimisticConcurrencySet = this.$__schema.options._optimisticConcurrencySet;
|
|
5163
|
+
const modPaths = this.directModifiedPaths();
|
|
5117
5164
|
const hasRelevantModPaths = pathsToSave == null ?
|
|
5118
|
-
modPaths.find(path =>
|
|
5119
|
-
modPaths.find(path =>
|
|
5165
|
+
modPaths.find(path => _pathOverlapsSet(path, optimisticConcurrencySet)) :
|
|
5166
|
+
modPaths.find(path => _pathOverlapsSet(path, optimisticConcurrencySet) && isInPathsToSave(path, pathsToSaveSet, pathsToSave));
|
|
5120
5167
|
if (hasRelevantModPaths) {
|
|
5121
5168
|
this.$__.version = dirty.length ? VERSION_ALL : VERSION_WHERE;
|
|
5122
5169
|
}
|
|
5123
5170
|
} else if (Array.isArray(optimisticConcurrency?.exclude)) {
|
|
5124
|
-
|
|
5125
|
-
|
|
5171
|
+
if (!this.$__schema.options._optimisticConcurrencyExcludeSet) {
|
|
5172
|
+
this.$__schema.options._optimisticConcurrencyExcludeSet = new Set(optimisticConcurrency.exclude);
|
|
5173
|
+
}
|
|
5174
|
+
const optimisticConcurrencyExcludeSet = this.$__schema.options._optimisticConcurrencyExcludeSet;
|
|
5175
|
+
const modPaths = this.directModifiedPaths();
|
|
5126
5176
|
const hasRelevantModPaths = pathsToSave == null ?
|
|
5127
|
-
modPaths.find(path => !
|
|
5128
|
-
modPaths.find(path => !
|
|
5177
|
+
modPaths.find(path => !_pathOrAncestorInSet(path, optimisticConcurrencyExcludeSet)) :
|
|
5178
|
+
modPaths.find(path => !_pathOrAncestorInSet(path, optimisticConcurrencyExcludeSet) && isInPathsToSave(path, pathsToSaveSet, pathsToSave));
|
|
5129
5179
|
if (hasRelevantModPaths) {
|
|
5130
5180
|
this.$__.version = dirty.length ? VERSION_ALL : VERSION_WHERE;
|
|
5131
5181
|
}
|
|
@@ -5258,6 +5308,12 @@ function checkDivergentArray(doc, path, array) {
|
|
|
5258
5308
|
// If any array was selected using an $elemMatch projection, we deny the update.
|
|
5259
5309
|
// NOTE: MongoDB only supports projected $elemMatch on top level array.
|
|
5260
5310
|
const top = path.split('.')[0];
|
|
5311
|
+
if (doc.$__.selected[top] && doc.$__.selected[top].$slice != null && utils.isMongooseArray(array)) {
|
|
5312
|
+
const atomics = array[arrayAtomicsSymbol];
|
|
5313
|
+
if (atomics.$set) {
|
|
5314
|
+
return top;
|
|
5315
|
+
}
|
|
5316
|
+
}
|
|
5261
5317
|
if (doc.$__.selected[top + '.$']) {
|
|
5262
5318
|
return top;
|
|
5263
5319
|
}
|
|
@@ -5633,24 +5689,47 @@ Document.prototype._applyVersionIncrement = function _applyVersionIncrement() {
|
|
|
5633
5689
|
};
|
|
5634
5690
|
|
|
5635
5691
|
/*!
|
|
5636
|
-
*
|
|
5692
|
+
* Module exports.
|
|
5637
5693
|
*/
|
|
5638
5694
|
|
|
5639
|
-
|
|
5640
|
-
|
|
5641
|
-
|
|
5642
|
-
|
|
5643
|
-
|
|
5644
|
-
|
|
5645
|
-
|
|
5646
|
-
|
|
5647
|
-
|
|
5695
|
+
/*!
|
|
5696
|
+
* Check if `path` or any of its ancestor paths exist in `pathSet`.
|
|
5697
|
+
* For example:
|
|
5698
|
+
* _pathOrAncestorInSet('profile.firstName', Set(['profile'])) === true
|
|
5699
|
+
* _pathOrAncestorInSet('profile', Set(['profile.firstName'])) === false
|
|
5700
|
+
*/
|
|
5701
|
+
function _pathOrAncestorInSet(path, pathSet) {
|
|
5702
|
+
if (pathSet.has(path)) {
|
|
5703
|
+
return true;
|
|
5648
5704
|
}
|
|
5649
|
-
|
|
5705
|
+
let idx = path.indexOf('.');
|
|
5706
|
+
while (idx !== -1) {
|
|
5707
|
+
if (pathSet.has(path.substring(0, idx))) {
|
|
5708
|
+
return true;
|
|
5709
|
+
}
|
|
5710
|
+
idx = path.indexOf('.', idx + 1);
|
|
5711
|
+
}
|
|
5712
|
+
return false;
|
|
5713
|
+
}
|
|
5650
5714
|
|
|
5651
5715
|
/*!
|
|
5652
|
-
*
|
|
5716
|
+
* Check if `path`, any of its ancestor paths, or any of its descendant paths
|
|
5717
|
+
* exist in `pathSet`.
|
|
5718
|
+
* For example:
|
|
5719
|
+
* _pathOverlapsSet('profile.firstName', Set(['profile'])) === true
|
|
5720
|
+
* _pathOverlapsSet('profile', Set(['profile.firstName'])) === true
|
|
5653
5721
|
*/
|
|
5722
|
+
function _pathOverlapsSet(path, pathSet) {
|
|
5723
|
+
if (_pathOrAncestorInSet(path, pathSet)) {
|
|
5724
|
+
return true;
|
|
5725
|
+
}
|
|
5726
|
+
for (const p of pathSet) {
|
|
5727
|
+
if (p.length > path.length + 1 && p[path.length] === '.' && p.slice(0, path.length) === path) {
|
|
5728
|
+
return true;
|
|
5729
|
+
}
|
|
5730
|
+
}
|
|
5731
|
+
return false;
|
|
5732
|
+
}
|
|
5654
5733
|
|
|
5655
5734
|
Document.VERSION_WHERE = VERSION_WHERE;
|
|
5656
5735
|
Document.VERSION_INC = VERSION_INC;
|
|
@@ -172,7 +172,8 @@ function iter(i) {
|
|
|
172
172
|
} else {
|
|
173
173
|
const color = debug.color == null ? true : debug.color;
|
|
174
174
|
const shell = debug.shell == null ? false : debug.shell;
|
|
175
|
-
|
|
175
|
+
const timestamp = debug.timestamp == null ? false : debug.timestamp;
|
|
176
|
+
this.$print(_this.name, i, args, color, shell, timestamp);
|
|
176
177
|
}
|
|
177
178
|
}
|
|
178
179
|
|
|
@@ -256,7 +257,12 @@ for (const key of Object.getOwnPropertyNames(Collection.prototype)) {
|
|
|
256
257
|
* @method $print
|
|
257
258
|
*/
|
|
258
259
|
|
|
259
|
-
NativeCollection.prototype.$print = function(name, i, args, color, shell) {
|
|
260
|
+
NativeCollection.prototype.$print = function(name, i, args, color, shell, timestamp) {
|
|
261
|
+
let prefix = '';
|
|
262
|
+
if (timestamp) {
|
|
263
|
+
const ts = new Date().toISOString();
|
|
264
|
+
prefix = color ? `\x1B[0;90m[${ts}]\x1B[0m ` : `[${ts}] `;
|
|
265
|
+
}
|
|
260
266
|
const moduleName = color ? '\x1B[0;36mMongoose:\x1B[0m ' : 'Mongoose: ';
|
|
261
267
|
const functionCall = [name, i].join('.');
|
|
262
268
|
const _args = [];
|
|
@@ -267,14 +273,14 @@ NativeCollection.prototype.$print = function(name, i, args, color, shell) {
|
|
|
267
273
|
}
|
|
268
274
|
const params = '(' + _args.join(', ') + ')';
|
|
269
275
|
|
|
270
|
-
console.info(moduleName + functionCall + params);
|
|
276
|
+
console.info(prefix + moduleName + functionCall + params);
|
|
271
277
|
};
|
|
272
278
|
|
|
273
279
|
/**
|
|
274
280
|
* Debug print helper
|
|
275
281
|
*
|
|
276
282
|
* @api public
|
|
277
|
-
* @method $
|
|
283
|
+
* @method $printToStream
|
|
278
284
|
*/
|
|
279
285
|
|
|
280
286
|
NativeCollection.prototype.$printToStream = function(name, i, args, stream) {
|
package/lib/error/cast.js
CHANGED
|
@@ -42,6 +42,7 @@ class CastError extends MongooseError {
|
|
|
42
42
|
message: this.message
|
|
43
43
|
};
|
|
44
44
|
}
|
|
45
|
+
|
|
45
46
|
/*!
|
|
46
47
|
* ignore
|
|
47
48
|
*/
|
|
@@ -76,7 +77,7 @@ class CastError extends MongooseError {
|
|
|
76
77
|
*/
|
|
77
78
|
setModel(model) {
|
|
78
79
|
this.message = formatMessage(model, this.kind, this.value, this.path,
|
|
79
|
-
this.messageFormat, this.valueType);
|
|
80
|
+
this.messageFormat, this.valueType, this.reason);
|
|
80
81
|
}
|
|
81
82
|
}
|
|
82
83
|
|
|
@@ -122,10 +123,11 @@ function getMessageFormat(schemaType) {
|
|
|
122
123
|
function formatMessage(model, kind, value, path, messageFormat, valueType, reason) {
|
|
123
124
|
if (typeof messageFormat === 'string') {
|
|
124
125
|
const stringValue = getStringValue(value);
|
|
125
|
-
let ret = messageFormat
|
|
126
|
-
replace('{KIND}', kind)
|
|
127
|
-
replace('{VALUE}', stringValue)
|
|
128
|
-
replace('{PATH}', path);
|
|
126
|
+
let ret = messageFormat
|
|
127
|
+
.replace('{KIND}', kind)
|
|
128
|
+
.replace('{VALUE}', stringValue)
|
|
129
|
+
.replace('{PATH}', path);
|
|
130
|
+
|
|
129
131
|
if (model != null) {
|
|
130
132
|
ret = ret.replace('{MODEL}', model.modelName);
|
|
131
133
|
}
|
|
@@ -136,17 +138,24 @@ function formatMessage(model, kind, value, path, messageFormat, valueType, reaso
|
|
|
136
138
|
} else {
|
|
137
139
|
const stringValue = getStringValue(value);
|
|
138
140
|
const valueTypeMsg = valueType ? ' (type ' + valueType + ')' : '';
|
|
141
|
+
|
|
139
142
|
let ret = 'Cast to ' + kind + ' failed for value ' +
|
|
140
143
|
stringValue + valueTypeMsg + ' at path "' + path + '"';
|
|
144
|
+
|
|
141
145
|
if (model != null) {
|
|
142
146
|
ret += ' for model "' + model.modelName + '"';
|
|
143
147
|
}
|
|
144
|
-
|
|
145
|
-
|
|
146
|
-
|
|
147
|
-
|
|
148
|
+
|
|
149
|
+
if (
|
|
150
|
+
reason != null &&
|
|
151
|
+
typeof reason.constructor === 'function' &&
|
|
152
|
+
reason.constructor.name !== 'AssertionError' &&
|
|
153
|
+
reason.constructor.name !== 'Error' &&
|
|
154
|
+
reason.constructor.name !== 'BSONError'
|
|
155
|
+
) {
|
|
148
156
|
ret += ' because of "' + reason.constructor.name + '"';
|
|
149
157
|
}
|
|
158
|
+
|
|
150
159
|
return ret;
|
|
151
160
|
}
|
|
152
161
|
}
|
|
@@ -17,7 +17,7 @@ class DivergentArrayError extends MongooseError {
|
|
|
17
17
|
|
|
18
18
|
constructor(paths) {
|
|
19
19
|
const msg = 'For your own good, using `document.save()` to update an array '
|
|
20
|
-
+ 'which was selected using an $elemMatch projection OR '
|
|
20
|
+
+ 'which was selected using an $elemMatch or $slice projection OR '
|
|
21
21
|
+ 'populated using skip, limit, query conditions, or exclusion of '
|
|
22
22
|
+ 'the _id field when the operation results in a $pop or $set of '
|
|
23
23
|
+ 'the entire array is not supported. The following '
|
package/lib/error/messages.js
CHANGED
|
@@ -30,6 +30,7 @@ msg.DocumentNotFoundError = null;
|
|
|
30
30
|
msg.general = {};
|
|
31
31
|
msg.general.default = 'Validator failed for path `{PATH}` with value `{VALUE}`';
|
|
32
32
|
msg.general.required = 'Path `{PATH}` is required.';
|
|
33
|
+
msg.general.allowNull = 'Path `{PATH}` does not allow null values.';
|
|
33
34
|
|
|
34
35
|
msg.Number = {};
|
|
35
36
|
msg.Number.min = 'Path `{PATH}` ({VALUE}) is less than minimum allowed value ({MIN}).';
|
package/lib/helpers/clone.js
CHANGED
|
@@ -202,10 +202,15 @@ function cloneArray(arr, options) {
|
|
|
202
202
|
|
|
203
203
|
let ret = null;
|
|
204
204
|
if (options?.retainDocuments) {
|
|
205
|
+
// Use the cloned parent doc (options.parentDoc) when available so the new
|
|
206
|
+
// array is wired up to the clone, not the source. Falling back to the
|
|
207
|
+
// source's parent preserves the previous behavior for callers that don't
|
|
208
|
+
// pass parentDoc.
|
|
209
|
+
const newParent = options.parentDoc ?? (arr.isMongooseArray ? arr.$parent() : undefined);
|
|
205
210
|
if (arr.isMongooseDocumentArray) {
|
|
206
|
-
ret = new (arr.$schemaType().schema.base.Types.DocumentArray)([], arr.$path(),
|
|
211
|
+
ret = new (arr.$schemaType().schema.base.Types.DocumentArray)([], arr.$path(), newParent, arr.$schemaType());
|
|
207
212
|
} else if (arr.isMongooseArray) {
|
|
208
|
-
ret = new (arr.$parent().schema.base.Types.Array)([], arr.$path(),
|
|
213
|
+
ret = new (arr.$parent().schema.base.Types.Array)([], arr.$path(), newParent, arr.$schemaType());
|
|
209
214
|
} else {
|
|
210
215
|
ret = new Array(len);
|
|
211
216
|
}
|
|
@@ -218,9 +223,21 @@ function cloneArray(arr, options) {
|
|
|
218
223
|
// Create new options object to avoid mutating the shared options.
|
|
219
224
|
// Subdocs need parentArray to point to their own cloned array.
|
|
220
225
|
options = { ...options, parentArray: ret };
|
|
221
|
-
|
|
222
|
-
|
|
223
|
-
|
|
226
|
+
// Skip change-tracking while populating the freshly cloned array: every
|
|
227
|
+
// index assignment would otherwise call markModified on the cloned parent
|
|
228
|
+
// doc and (before this guard) on the source parent doc.
|
|
229
|
+
for (i = 0; i < len; ++i) {
|
|
230
|
+
const doc = clone(arr[i], options, true);
|
|
231
|
+
// Document arrays can contain null entries, so only restamp actual subdocs.
|
|
232
|
+
if (typeof doc?.$setIndex === 'function') {
|
|
233
|
+
doc.$setIndex(i);
|
|
234
|
+
}
|
|
235
|
+
ret.set(i, doc, true);
|
|
236
|
+
}
|
|
237
|
+
} else {
|
|
238
|
+
for (i = 0; i < len; ++i) {
|
|
239
|
+
ret[i] = clone(arr[i], options, true);
|
|
240
|
+
}
|
|
224
241
|
}
|
|
225
242
|
|
|
226
243
|
return ret;
|
|
@@ -17,6 +17,11 @@ module.exports = function applyDefaults(doc, fields, exclude, hasIncludedChildre
|
|
|
17
17
|
}
|
|
18
18
|
|
|
19
19
|
const type = doc.$__schema.paths[p];
|
|
20
|
+
// `getDefault()` returns undefined when `defaultValue` is undefined, in which
|
|
21
|
+
// case this loop never modifies the doc, so skip traversing the path entirely.
|
|
22
|
+
if (type.defaultValue === undefined) {
|
|
23
|
+
continue;
|
|
24
|
+
}
|
|
20
25
|
const path = type.splitPath();
|
|
21
26
|
const len = path.length;
|
|
22
27
|
if (path[len - 1] === '$*') {
|
|
@@ -25,7 +25,15 @@ module.exports = function createPopulateQueryFilter(ids, _match, _foreignField,
|
|
|
25
25
|
for (let i = 0; i < _parentPaths.length - 1; ++i) {
|
|
26
26
|
const cur = _parentPaths[i];
|
|
27
27
|
if (match[cur] != null && match[cur].$elemMatch != null) {
|
|
28
|
-
|
|
28
|
+
// Copy rather than mutate so the user's `match` stays unchanged and split populate
|
|
29
|
+
// queries (gh-5890) each get their own `$in` rather than sharing one object
|
|
30
|
+
match[cur] = {
|
|
31
|
+
...match[cur],
|
|
32
|
+
$elemMatch: {
|
|
33
|
+
...match[cur].$elemMatch,
|
|
34
|
+
[foreignField.slice(cur.length + 1)]: trusted({ $in: ids })
|
|
35
|
+
}
|
|
36
|
+
};
|
|
29
37
|
delete match[foreignField];
|
|
30
38
|
break;
|
|
31
39
|
}
|
|
@@ -649,11 +649,7 @@ function addModelNamesToMap(model, map, available, modelNames, options, data, re
|
|
|
649
649
|
ids = matchIdsToRefPaths(ret, modelNamesForRefPath, modelName);
|
|
650
650
|
}
|
|
651
651
|
|
|
652
|
-
|
|
653
|
-
get(options, 'options.perDocumentLimit', null) :
|
|
654
|
-
options.perDocumentLimit;
|
|
655
|
-
|
|
656
|
-
if (!available[modelName] || perDocumentLimit != null) {
|
|
652
|
+
if (!available[modelName]) {
|
|
657
653
|
const currentOptions = {
|
|
658
654
|
model: Model
|
|
659
655
|
};
|
|
@@ -682,6 +678,7 @@ function addModelNamesToMap(model, map, available, modelNames, options, data, re
|
|
|
682
678
|
isVirtual: data.isVirtual,
|
|
683
679
|
virtual: data.virtual,
|
|
684
680
|
count: data.count,
|
|
681
|
+
isRefPath: !!data.isRefPath,
|
|
685
682
|
[populateModelSymbol]: Model
|
|
686
683
|
};
|
|
687
684
|
map.push(available[modelName]);
|
|
@@ -692,6 +689,7 @@ function addModelNamesToMap(model, map, available, modelNames, options, data, re
|
|
|
692
689
|
available[modelName].ids.push(ids);
|
|
693
690
|
available[modelName].allIds.push(ret);
|
|
694
691
|
available[modelName].unpopulatedValues.push(unpopulatedValue);
|
|
692
|
+
available[modelName].isRefPath = available[modelName].isRefPath || !!data.isRefPath;
|
|
695
693
|
if (data.hasMatchFunction) {
|
|
696
694
|
available[modelName].match.push(data.match);
|
|
697
695
|
}
|
|
@@ -873,7 +871,11 @@ function _findRefPathForDiscriminators(doc, modelSchema, data, options, normaliz
|
|
|
873
871
|
}
|
|
874
872
|
|
|
875
873
|
/**
|
|
876
|
-
* Throw an error if there are any $where keys
|
|
874
|
+
* Throw an error if there are any $where keys to defend against [CVE-2024-53900](https://nvd.nist.gov/vuln/detail/CVE-2024-53900)
|
|
875
|
+
*
|
|
876
|
+
* Note that this is ONLY for $where because sift executes $where in Node.js memory.
|
|
877
|
+
* Other forms of MongoDB server-side execution, like $expr, are NOT filtered out.
|
|
878
|
+
* This function is not meant to protect against server-side execution in MongoDB.
|
|
877
879
|
*/
|
|
878
880
|
|
|
879
881
|
function throwOn$where(match) {
|