mongoose 9.2.0 → 9.2.2

This diff represents the content of publicly available package versions that have been released to one of the supported registries. The information contained in this diff is provided for informational purposes only and reflects changes between package versions as they appear in their respective public registries.
package/lib/document.js CHANGED
@@ -22,6 +22,7 @@ const $__hasIncludedChildren = require('./helpers/projection/hasIncludedChildren
22
22
  const applyDefaults = require('./helpers/document/applyDefaults');
23
23
  const cleanModifiedSubpaths = require('./helpers/document/cleanModifiedSubpaths');
24
24
  const clone = require('./helpers/clone');
25
+ const isInPathsToSave = require('./helpers/document/isInPathsToSave');
25
26
  const compile = require('./helpers/document/compile').compile;
26
27
  const defineKey = require('./helpers/document/compile').defineKey;
27
28
  const firstKey = require('./helpers/firstKey');
@@ -1428,11 +1429,25 @@ Document.prototype.$set = function $set(path, val, type, options) {
1428
1429
  }
1429
1430
 
1430
1431
  // Check refPath
1431
- const refPath = schema.options.refPath;
1432
+ let refPath = schema.options.refPath;
1432
1433
  if (refPath == null) {
1433
1434
  return false;
1434
1435
  }
1435
- const modelName = val.get(refPath);
1436
+
1437
+ if (typeof refPath === 'function' && !refPath[modelSymbol]) {
1438
+ let fullPath = path;
1439
+ const fullPathWithIndexes = this.$__fullPathWithIndexes?.();
1440
+ if (fullPathWithIndexes?.length) {
1441
+ fullPath = fullPathWithIndexes + '.' + path;
1442
+ }
1443
+ refPath = refPath.call(this, this, fullPath);
1444
+ }
1445
+
1446
+ if (typeof refPath !== 'string') {
1447
+ throw new MongooseError('`refPath` must be a string or a function that returns a string, got ' + inspect(refPath));
1448
+ }
1449
+
1450
+ const modelName = this.ownerDocument().get(refPath);
1436
1451
  return modelName === model.modelName || modelName === model.baseModelName;
1437
1452
  })();
1438
1453
 
@@ -4998,26 +5013,47 @@ Document.prototype.getChanges = function() {
4998
5013
  /**
4999
5014
  * Produces a special query document of the modified properties used in updates.
5000
5015
  *
5016
+ * @param {string[]|null} [pathsToSave] paths to include in delta generation
5017
+ * @param {Set<string>|null} [pathsToSaveSet] pre-built Set of pathsToSave for O(1) exact lookup
5001
5018
  * @api private
5002
5019
  * @method $__delta
5003
5020
  * @memberOf Document
5004
5021
  * @instance
5005
5022
  */
5006
5023
 
5007
- Document.prototype.$__delta = function $__delta() {
5008
- const dirty = this.$__dirty();
5024
+ Document.prototype.$__delta = function $__delta(pathsToSave, pathsToSaveSet) {
5025
+ const allDirty = this.$__dirty();
5026
+ let dirty = allDirty;
5027
+ let unsavedDirty = null;
5028
+ if (pathsToSave != null) {
5029
+ dirty = [];
5030
+ unsavedDirty = [];
5031
+ for (const data of allDirty) {
5032
+ if (isInPathsToSave(data.path, pathsToSaveSet, pathsToSave)) {
5033
+ dirty.push(data);
5034
+ } else {
5035
+ unsavedDirty.push(data);
5036
+ }
5037
+ }
5038
+ }
5009
5039
  const optimisticConcurrency = this.$__schema.options.optimisticConcurrency;
5010
5040
  if (optimisticConcurrency) {
5011
5041
  if (Array.isArray(optimisticConcurrency)) {
5012
5042
  const optCon = new Set(optimisticConcurrency);
5013
5043
  const modPaths = this.modifiedPaths();
5014
- if (modPaths.some(path => optCon.has(path))) {
5044
+ const hasRelevantModPaths = pathsToSave == null ?
5045
+ modPaths.find(path => optCon.has(path)) :
5046
+ modPaths.find(path => optCon.has(path) && isInPathsToSave(path, pathsToSaveSet, pathsToSave));
5047
+ if (hasRelevantModPaths) {
5015
5048
  this.$__.version = dirty.length ? VERSION_ALL : VERSION_WHERE;
5016
5049
  }
5017
5050
  } else if (Array.isArray(optimisticConcurrency?.exclude)) {
5018
5051
  const excluded = new Set(optimisticConcurrency.exclude);
5019
5052
  const modPaths = this.modifiedPaths();
5020
- if (modPaths.some(path => !excluded.has(path))) {
5053
+ const hasRelevantModPaths = pathsToSave == null ?
5054
+ modPaths.find(path => !excluded.has(path)) :
5055
+ modPaths.find(path => !excluded.has(path) && isInPathsToSave(path, pathsToSaveSet, pathsToSave));
5056
+ if (hasRelevantModPaths) {
5021
5057
  this.$__.version = dirty.length ? VERSION_ALL : VERSION_WHERE;
5022
5058
  }
5023
5059
  } else {
@@ -5123,10 +5159,10 @@ Document.prototype.$__delta = function $__delta() {
5123
5159
  }
5124
5160
 
5125
5161
  if (utils.hasOwnKeys(delta) === false) {
5126
- return [where, null];
5162
+ return [where, null, unsavedDirty];
5127
5163
  }
5128
5164
 
5129
- return [where, delta];
5165
+ return [where, delta, unsavedDirty];
5130
5166
  };
5131
5167
 
5132
5168
  /**
@@ -0,0 +1,24 @@
1
+ 'use strict';
2
+
3
+ /**
4
+ * Returns true if `path` is included by the `pathsToSave` filter.
5
+ * Matches exact paths and child paths (e.g. 'metadata.views' is included by 'metadata').
6
+ *
7
+ * @param {string} path
8
+ * @param {Set<string>} pathsToSaveSet - pre-built Set of pathsToSave for O(1) exact lookup
9
+ * @param {string[]} pathsToSave - original array, used for prefix matching
10
+ * @returns {boolean}
11
+ */
12
+ module.exports = function isInPathsToSave(path, pathsToSaveSet, pathsToSave) {
13
+ if (pathsToSaveSet.has(path)) {
14
+ return true;
15
+ }
16
+
17
+ for (const pathToSave of pathsToSave) {
18
+ if (path.slice(0, pathToSave.length) === pathToSave && path.charAt(pathToSave.length) === '.') {
19
+ return true;
20
+ }
21
+ }
22
+
23
+ return false;
24
+ };
@@ -17,6 +17,9 @@ const modelSymbol = require('../symbols').modelSymbol;
17
17
  const populateModelSymbol = require('../symbols').populateModelSymbol;
18
18
  const schemaMixedSymbol = require('../../schema/symbols').schemaMixedSymbol;
19
19
  const StrictPopulate = require('../../error/strictPopulate');
20
+ const numericPathSegmentPattern = '\\.\\d+(?=\\.|$)';
21
+ const hasNumericPathSegmentRE = new RegExp(numericPathSegmentPattern);
22
+ const numericPathSegmentRE = new RegExp(numericPathSegmentPattern, 'g');
20
23
 
21
24
  module.exports = function getModelsMapForPopulate(model, docs, options) {
22
25
  let doc;
@@ -43,7 +46,8 @@ module.exports = function getModelsMapForPopulate(model, docs, options) {
43
46
  return _virtualPopulate(model, docs, options, _virtualRes);
44
47
  }
45
48
 
46
- let allSchemaTypes = getSchemaTypes(model, modelSchema, null, options.path);
49
+ const parts = modelSchema.paths[options.path]?.splitPath() ?? options.path.split('.');
50
+ let allSchemaTypes = getSchemaTypes(model, modelSchema, null, options.path, parts);
47
51
  allSchemaTypes = Array.isArray(allSchemaTypes) ? allSchemaTypes : [allSchemaTypes].filter(v => v != null);
48
52
 
49
53
  const isStrictPopulateDisabled = options.strictPopulate === false || options.options?.strictPopulate === false;
@@ -63,7 +67,7 @@ module.exports = function getModelsMapForPopulate(model, docs, options) {
63
67
  }
64
68
 
65
69
  const docSchema = doc?.$__ != null ? doc.$__schema : modelSchema;
66
- schema = getSchemaTypes(model, docSchema, doc, options.path);
70
+ schema = getSchemaTypes(model, docSchema, doc, options.path, parts);
67
71
 
68
72
  // Special case: populating a path that's a DocumentArray unless
69
73
  // there's an explicit `ref` or `refPath` re: gh-8946
@@ -201,8 +205,11 @@ module.exports = function getModelsMapForPopulate(model, docs, options) {
201
205
  data.modelNamesInOrder = modelNamesInOrder;
202
206
 
203
207
  if (isRefPath) {
208
+ const normalizedRefPathForDiscriminators = typeof normalizedRefPath === 'string' ?
209
+ normalizedRefPath.replace(numericPathSegmentRE, '') :
210
+ normalizedRefPath;
204
211
  const embeddedDiscriminatorModelNames = _findRefPathForDiscriminators(doc,
205
- modelSchema, data, options, normalizedRefPath, ret);
212
+ modelSchema, data, options, normalizedRefPathForDiscriminators, ret);
206
213
 
207
214
  modelNames = embeddedDiscriminatorModelNames || modelNames;
208
215
  }
@@ -215,23 +222,23 @@ module.exports = function getModelsMapForPopulate(model, docs, options) {
215
222
  }
216
223
  return map;
217
224
 
218
- function _getModelNames(doc, schema, modelNameFromQuery, model) {
225
+ function _getModelNames(doc, schemaType, modelNameFromQuery, model) {
219
226
  let modelNames;
220
227
  let isRefPath = false;
221
228
  let justOne = null;
222
229
 
223
- const originalSchema = schema;
224
- if (schema?.instance === 'Array') {
225
- schema = schema.embeddedSchemaType;
230
+ const originalSchema = schemaType;
231
+ if (schemaType?.instance === 'Array') {
232
+ schemaType = schemaType.embeddedSchemaType;
226
233
  }
227
- if (schema?.$isSchemaMap) {
228
- schema = schema.$__schemaType;
234
+ if (schemaType?.$isSchemaMap) {
235
+ schemaType = schemaType.$__schemaType;
229
236
  }
230
237
 
231
- const ref = schema?.options?.ref;
232
- refPath = schema?.options?.refPath;
233
- if (schema != null &&
234
- schema[schemaMixedSymbol] &&
238
+ const ref = schemaType?.options?.ref;
239
+ refPath = schemaType?.options?.refPath;
240
+ if (schemaType != null &&
241
+ schemaType[schemaMixedSymbol] &&
235
242
  !ref &&
236
243
  !refPath &&
237
244
  !modelNameFromQuery) {
@@ -242,19 +249,9 @@ module.exports = function getModelsMapForPopulate(model, docs, options) {
242
249
  modelNames = [modelNameFromQuery]; // query options
243
250
  } else if (refPath != null) {
244
251
  if (typeof refPath === 'function') {
245
- const subdocPath = options.path.slice(0, options.path.length - schema.path.length - 1);
246
- const vals = mpath.get(subdocPath, doc, lookupLocalFields);
247
- const subdocsBeingPopulated = Array.isArray(vals) ?
248
- utils.array.flatten(vals) :
249
- (vals ? [vals] : []);
250
-
251
- modelNames = new Set();
252
- for (const subdoc of subdocsBeingPopulated) {
253
- refPath = refPath.call(subdoc, subdoc, options.path);
254
- modelNamesFromRefPath(refPath, doc, options.path, modelSchema, options._queryProjection).
255
- forEach(name => modelNames.add(name));
256
- }
257
- modelNames = Array.from(modelNames);
252
+ const res = _getModelNamesFromFunctionRefPath(refPath, doc, schemaType, options.path, modelSchema, options._queryProjection);
253
+ modelNames = res.modelNames;
254
+ refPath = res.refPath;
258
255
  } else {
259
256
  modelNames = modelNamesFromRefPath(refPath, doc, options.path, modelSchema, options._queryProjection);
260
257
  }
@@ -268,7 +265,7 @@ module.exports = function getModelsMapForPopulate(model, docs, options) {
268
265
  let modelForCurrentDoc = model;
269
266
  const discriminatorKey = model.schema.options.discriminatorKey;
270
267
 
271
- if (!schema && discriminatorKey && (discriminatorValue = utils.getValue(discriminatorKey, doc))) {
268
+ if (!schemaType && discriminatorKey && (discriminatorValue = utils.getValue(discriminatorKey, doc))) {
272
269
  // `modelNameForFind` is the discriminator value, so we might need
273
270
  // find the discriminated model name
274
271
  const discriminatorModel = getDiscriminatorByValue(model.discriminators, discriminatorValue) || model;
@@ -288,7 +285,7 @@ module.exports = function getModelsMapForPopulate(model, docs, options) {
288
285
  schemaForCurrentDoc = schemaForCurrentDoc.embeddedSchemaType;
289
286
  }
290
287
  } else {
291
- schemaForCurrentDoc = schema;
288
+ schemaForCurrentDoc = schemaType;
292
289
  }
293
290
 
294
291
  if (originalSchema && originalSchema.path.endsWith('.$*')) {
@@ -322,22 +319,12 @@ module.exports = function getModelsMapForPopulate(model, docs, options) {
322
319
  ref = handleRefFunction(ref, doc);
323
320
  modelNames = [ref];
324
321
  }
325
- } else if ((schemaForCurrentDoc = get(schema, 'options.refPath')) != null) {
322
+ } else if ((refPath = get(schemaForCurrentDoc, 'options.refPath')) != null) {
326
323
  isRefPath = true;
327
324
  if (typeof refPath === 'function') {
328
- const subdocPath = options.path.slice(0, options.path.length - schemaForCurrentDoc.path.length - 1);
329
- const vals = mpath.get(subdocPath, doc, lookupLocalFields);
330
- const subdocsBeingPopulated = Array.isArray(vals) ?
331
- utils.array.flatten(vals) :
332
- (vals ? [vals] : []);
333
-
334
- modelNames = new Set();
335
- for (const subdoc of subdocsBeingPopulated) {
336
- refPath = refPath.call(subdoc, subdoc, options.path);
337
- modelNamesFromRefPath(refPath, doc, options.path, modelSchema, options._queryProjection).
338
- forEach(name => modelNames.add(name));
339
- }
340
- modelNames = Array.from(modelNames);
325
+ const res = _getModelNamesFromFunctionRefPath(refPath, doc, schemaForCurrentDoc, options.path, modelSchema, options._queryProjection);
326
+ modelNames = res.modelNames;
327
+ refPath = res.refPath;
341
328
  } else {
342
329
  modelNames = modelNamesFromRefPath(refPath, doc, options.path, modelSchema, options._queryProjection);
343
330
  }
@@ -361,6 +348,137 @@ module.exports = function getModelsMapForPopulate(model, docs, options) {
361
348
  }
362
349
  };
363
350
 
351
+ /**
352
+ * Resolve model names for function-style `refPath` and return the first
353
+ * resolved refPath value so discriminator handling can inspect it later.
354
+ *
355
+ * @param {Function} refPath
356
+ * @param {Document|Object} doc
357
+ * @param {SchemaType} schema
358
+ * @param {String} populatePath
359
+ * @param {Schema} modelSchema
360
+ * @param {Object} queryProjection
361
+ * @returns {{modelNames: String[], refPath: String|null}}
362
+ * @private
363
+ */
364
+ function _getModelNamesFromFunctionRefPath(refPath, doc, schemaType, populatePath, modelSchema, queryProjection) {
365
+ const modelNames = [];
366
+ let normalizedRefPath = null;
367
+ const schemaPath = schemaType?.path;
368
+
369
+ if (schemaPath != null &&
370
+ populatePath.length > schemaPath.length + 1 &&
371
+ populatePath.charAt(populatePath.length - schemaPath.length - 1) === '.' &&
372
+ populatePath.slice(populatePath.length - schemaPath.length) === schemaPath
373
+ ) {
374
+ const subdocPath = populatePath.slice(0, populatePath.length - schemaPath.length - 1);
375
+ const segments = subdocPath.indexOf('.') === -1 ? [subdocPath] : subdocPath.split('.');
376
+ let hasSubdoc = false;
377
+
378
+ walkSubdocs(doc, '', 0);
379
+ if (hasSubdoc) {
380
+ return { modelNames, refPath: normalizedRefPath };
381
+ }
382
+
383
+ function walkSubdocs(currentSubdoc, indexedPathPrefix, segmentIndex) {
384
+ if (currentSubdoc == null) {
385
+ return;
386
+ }
387
+
388
+ if (segmentIndex >= segments.length) {
389
+ hasSubdoc = true;
390
+ const indexedPath = indexedPathPrefix + '.' + schemaPath;
391
+ const subdocRefPath = refPath.call(currentSubdoc, currentSubdoc, indexedPath);
392
+ normalizedRefPath = normalizedRefPath || subdocRefPath;
393
+ modelNames.push(..._getModelNamesFromRefPath(subdocRefPath, doc, populatePath, indexedPath, modelSchema, queryProjection));
394
+ return;
395
+ }
396
+
397
+ const segment = segments[segmentIndex];
398
+ const source = currentSubdoc?._doc ?? currentSubdoc;
399
+
400
+ if (segment === '$*') {
401
+ if (source instanceof Map) {
402
+ for (const [key, value] of source.entries()) {
403
+ if (value != null) {
404
+ const valuePath = indexedPathPrefix.length === 0 ?
405
+ key :
406
+ indexedPathPrefix + '.' + key;
407
+ walkSubdocs(value, valuePath, segmentIndex + 1);
408
+ }
409
+ }
410
+ return;
411
+ }
412
+
413
+ if (source != null && typeof source === 'object') {
414
+ for (const key of Object.keys(source)) {
415
+ const value = source[key];
416
+ if (value != null) {
417
+ const valuePath = indexedPathPrefix.length === 0 ?
418
+ key :
419
+ indexedPathPrefix + '.' + key;
420
+ walkSubdocs(value, valuePath, segmentIndex + 1);
421
+ }
422
+ }
423
+ }
424
+ return;
425
+ }
426
+
427
+ const child = source[segment];
428
+ const childPath = indexedPathPrefix.length === 0 ?
429
+ segment :
430
+ indexedPathPrefix + '.' + segment;
431
+
432
+ if (Array.isArray(child)) {
433
+ for (let i = 0; i < child.length; ++i) {
434
+ if (child[i] != null) {
435
+ walkSubdocs(child[i], childPath + '.' + i, segmentIndex + 1);
436
+ }
437
+ }
438
+ } else if (child != null) {
439
+ walkSubdocs(child, childPath, segmentIndex + 1);
440
+ }
441
+ }
442
+ }
443
+
444
+ const topLevelRefPath = refPath.call(doc, doc, populatePath);
445
+ normalizedRefPath = normalizedRefPath || topLevelRefPath;
446
+ modelNames.push(...modelNamesFromRefPath(topLevelRefPath, doc, populatePath, modelSchema, queryProjection));
447
+
448
+ return { modelNames, refPath: normalizedRefPath };
449
+ }
450
+
451
+ /**
452
+ * Resolve model names from a refPath, preferring the indexed populated path
453
+ * for array subdocuments and falling back to the original populate path if
454
+ * normalization fails.
455
+ *
456
+ * @param {String|Function} refPath
457
+ * @param {Document|Object} doc
458
+ * @param {String} populatePath
459
+ * @param {String} indexedPath
460
+ * @param {Schema} modelSchema
461
+ * @param {Object} queryProjection
462
+ * @returns {String[]}
463
+ * @private
464
+ */
465
+ function _getModelNamesFromRefPath(refPath, doc, populatePath, indexedPath, modelSchema, queryProjection) {
466
+ const populatedPath = typeof refPath === 'string' && hasNumericPathSegmentRE.test(refPath) ?
467
+ populatePath :
468
+ indexedPath;
469
+
470
+ try {
471
+ return modelNamesFromRefPath(refPath, doc, populatedPath, modelSchema, queryProjection);
472
+ } catch (error) {
473
+ if (populatedPath === indexedPath &&
474
+ typeof error?.message === 'string' &&
475
+ error.message.startsWith('Could not normalize ref path')) {
476
+ return modelNamesFromRefPath(refPath, doc, populatePath, modelSchema, queryProjection);
477
+ }
478
+ throw error;
479
+ }
480
+ }
481
+
364
482
  /*!
365
483
  * ignore
366
484
  */
@@ -22,10 +22,11 @@ const populateModelSymbol = require('../symbols').populateModelSymbol;
22
22
  * @param {Schema} schema
23
23
  * @param {Object} doc POJO
24
24
  * @param {string} path
25
+ * @param {string[]} [parts] pass in pre-split `path` to avoid extra splitting
25
26
  * @api private
26
27
  */
27
28
 
28
- module.exports = function getSchemaTypes(model, schema, doc, path) {
29
+ module.exports = function getSchemaTypes(model, schema, doc, path, parts) {
29
30
  const pathschema = schema.path(path);
30
31
  const topLevelDoc = doc;
31
32
  if (pathschema) {
@@ -217,7 +218,7 @@ module.exports = function getSchemaTypes(model, schema, doc, path) {
217
218
  }
218
219
  }
219
220
  // look for arrays
220
- const parts = path.split('.');
221
+ parts = parts || path.split('.');
221
222
  for (let i = 0; i < parts.length; ++i) {
222
223
  if (parts[i] === '$') {
223
224
  // Re: gh-5628, because `schema.path()` doesn't take $ into account.
@@ -14,7 +14,11 @@ module.exports = function modelNamesFromRefPath(refPath, doc, populatedPath, mod
14
14
  return [];
15
15
  }
16
16
 
17
- if (typeof refPath === 'string' && queryProjection != null && isPathExcluded(queryProjection, refPath)) {
17
+ if (typeof refPath !== 'string') {
18
+ throw new MongooseError('`refPath` must be a string or a function that returns a string, got ' + util.inspect(refPath));
19
+ }
20
+
21
+ if (queryProjection != null && isPathExcluded(queryProjection, refPath)) {
18
22
  throw new MongooseError('refPath `' + refPath + '` must not be excluded in projection, got ' +
19
23
  util.inspect(queryProjection));
20
24
  }
@@ -35,7 +39,11 @@ module.exports = function modelNamesFromRefPath(refPath, doc, populatedPath, mod
35
39
  // 2nd, 4th, etc. will be numeric props. For example: `[ 'a', '.0.', 'b' ]`
36
40
  for (let i = 0; i < chunks.length; i += 2) {
37
41
  const chunk = chunks[i];
38
- if (_remaining.startsWith(chunk + '.')) {
42
+ if (
43
+ _remaining.length >= chunk.length + 1 &&
44
+ _remaining.charAt(chunk.length) === '.' &&
45
+ _remaining.slice(0, chunk.length) === chunk
46
+ ) {
39
47
  _refPath += _remaining.substring(0, chunk.length) + chunks[i + 1];
40
48
  _remaining = _remaining.substring(chunk.length + 1);
41
49
  } else if (i === chunks.length - 1) {
@@ -22,7 +22,7 @@ module.exports = function(filter, schema, castedDoc, options) {
22
22
  }
23
23
 
24
24
  const keys = Object.keys(castedDoc || {});
25
- const updatedKeys = {};
25
+ const updatedKeys = Object.create(null);
26
26
  const updatedValues = {};
27
27
  const numKeys = keys.length;
28
28
 
@@ -55,6 +55,20 @@ module.exports = function(filter, schema, castedDoc, options) {
55
55
  }
56
56
  }
57
57
  updatedKeys[path] = true;
58
+ if (path.indexOf('.') === -1) {
59
+ continue;
60
+ }
61
+ // Also mark all parent prefixes so child-path lookups are O(1).
62
+ // e.g. 'extraProps.location' also marks 'extraProps'
63
+ const pieces = schema.paths[path] ?
64
+ // If the SchemaType already split for us, use that to avoid the extra overhead
65
+ schema.paths[path].splitPath() :
66
+ path.split('.');
67
+ let cur = pieces[0];
68
+ for (let j = 1; j < pieces.length; ++j) {
69
+ updatedKeys[cur] = true;
70
+ cur += '.' + pieces[j];
71
+ }
58
72
  }
59
73
 
60
74
  if (options?.overwrite && !hasDollarUpdate) {
package/lib/model.js CHANGED
@@ -21,6 +21,7 @@ const ValidationError = require('./error/validation');
21
21
  const VersionError = require('./error/version');
22
22
  const ParallelSaveError = require('./error/parallelSave');
23
23
  const applyDefaultsHelper = require('./helpers/document/applyDefaults');
24
+ const isInPathsToSave = require('./helpers/document/isInPathsToSave');
24
25
  const applyDefaultsToPOJO = require('./helpers/model/applyDefaultsToPOJO');
25
26
  const applyEmbeddedDiscriminators = require('./helpers/discriminator/applyEmbeddedDiscriminators');
26
27
  const applyHooks = require('./helpers/model/applyHooks');
@@ -73,7 +74,11 @@ const ObjectExpectedError = require('./error/objectExpected');
73
74
  const decorateBulkWriteResult = require('./helpers/model/decorateBulkWriteResult');
74
75
  const modelCollectionSymbol = Symbol('mongoose#Model#collection');
75
76
  const modelDbSymbol = Symbol('mongoose#Model#db');
76
- const modelSymbol = require('./helpers/symbols').modelSymbol;
77
+ const {
78
+ arrayAtomicsBackupSymbol,
79
+ arrayAtomicsSymbol,
80
+ modelSymbol
81
+ } = require('./helpers/symbols');
77
82
  const subclassedSymbol = Symbol('mongoose#Model#subclassed');
78
83
 
79
84
  const { VERSION_INC, VERSION_WHERE, VERSION_ALL } = Document;
@@ -405,19 +410,14 @@ Model.prototype.$__save = async function $__save(options) {
405
410
  // Make sure we don't treat it as a new object on error,
406
411
  // since it already exists
407
412
  this.$__.inserting = false;
408
- const delta = this.$__delta();
409
-
410
- if (options.pathsToSave) {
411
- for (const key in delta[1]['$set']) {
412
- if (options.pathsToSave.includes(key)) {
413
- continue;
414
- } else if (options.pathsToSave.some(pathToSave => key.slice(0, pathToSave.length) === pathToSave && key.charAt(pathToSave.length) === '.')) {
415
- continue;
416
- } else {
417
- delete delta[1]['$set'][key];
418
- }
419
- }
420
- }
413
+ const pathsToSave = Array.isArray(options.pathsToSave) ? options.pathsToSave : null;
414
+ const pathsToSaveSet = pathsToSave != null ? new Set(pathsToSave) : null;
415
+ const delta = this.$__delta(pathsToSave, pathsToSaveSet);
416
+ const unsavedDirty = pathsToSave != null ? (delta != null ? delta[2] : this.$__dirty()) : null;
417
+ const unsavedDefaultPaths = pathsToSave != null
418
+ ? Object.keys(this.$__.activePaths.getStatePaths('default')).filter(path => !isInPathsToSave(path, pathsToSaveSet, pathsToSave))
419
+ : null;
420
+
421
421
  if (delta) {
422
422
  where = this.$__where(delta[0]);
423
423
  _applyCustomWhere(this, where);
@@ -448,6 +448,7 @@ Model.prototype.$__save = async function $__save(options) {
448
448
  // store the modified paths before the document is reset
449
449
  this.$__.modifiedPaths = this.modifiedPaths();
450
450
  this.$__reset();
451
+ restoreUnsavedState(this, unsavedDirty, unsavedDefaultPaths);
451
452
 
452
453
  _setIsNew(this, false);
453
454
  result = await this[modelCollectionSymbol].updateOne(where, update, saveOptions).catch(err => {
@@ -526,6 +527,33 @@ Model.prototype.$__save = async function $__save(options) {
526
527
  await this._execDocumentPostHooks('save', options);
527
528
  };
528
529
 
530
+ /*!
531
+ * Restores $__.activePaths state and any atomics for paths that failed
532
+ * to save.
533
+ *
534
+ * @param {Document} doc
535
+ * @param {object[]} unsavedDirty
536
+ * @param {string[]} unsavedDefaultPaths
537
+ */
538
+
539
+ function restoreUnsavedState(doc, unsavedDirty, unsavedDefaultPaths) {
540
+ if (unsavedDirty == null) {
541
+ return;
542
+ }
543
+
544
+ for (const dirty of unsavedDirty) {
545
+ doc.$__.activePaths.modify(dirty.path);
546
+ if (dirty.value?.[arrayAtomicsBackupSymbol]) {
547
+ dirty.value[arrayAtomicsSymbol] = dirty.value[arrayAtomicsBackupSymbol];
548
+ dirty.value[arrayAtomicsBackupSymbol] = null;
549
+ }
550
+ }
551
+
552
+ for (const path of unsavedDefaultPaths) {
553
+ doc.$__.activePaths.default(path);
554
+ }
555
+ }
556
+
529
557
  /*!
530
558
  * ignore
531
559
  */
package/lib/mongoose.js CHANGED
@@ -243,6 +243,8 @@ Mongoose.prototype.setDriver = function setDriver(driver) {
243
243
  * - `timestamps.createdAt.immutable`: `true` by default. If `false`, it will change the `createdAt` field to be [`immutable: false`](https://mongoosejs.com/docs/api/schematype.html#SchemaType.prototype.immutable) which means you can update the `createdAt`
244
244
  * - `toJSON`: `{ transform: true, flattenDecimals: true }` by default. Overwrites default objects to [`toJSON()`](https://mongoosejs.com/docs/api/document.html#Document.prototype.toJSON()), for determining how Mongoose documents get serialized by `JSON.stringify()`
245
245
  * - `toObject`: `{ transform: true, flattenDecimals: true }` by default. Overwrites default objects to [`toObject()`](https://mongoosejs.com/docs/api/document.html#Document.prototype.toObject())
246
+ * - `transactionAsyncLocalStorage`: `false` by default. If `true`, Mongoose will automatically pass the `session` to any operation executing within a transaction executor function. See [transaction AsyncLocalStorage documentation](https://mongoosejs.com/docs/transactions.html#asynclocalstorage)
247
+ * - `translateAliases`: `false` by default. If `true`, Mongoose will automatically translate aliases to their original paths before sending the query to MongoDB.
246
248
  * - `updatePipeline`: `false` by default. If `true`, allows passing update pipelines (arrays) to update operations by default without explicitly setting `updatePipeline: true` in each query.
247
249
  *
248
250
  * @param {String|Object} key The name of the option or a object of multiple key-value pairs
package/lib/schemaType.js CHANGED
@@ -170,7 +170,6 @@ SchemaType.prototype.path;
170
170
  * const schematype = schema.path('name');
171
171
  * console.log(schematype.toJSON());
172
172
  *
173
- * @function toJSON
174
173
  * @memberOf SchemaType
175
174
  * @instance
176
175
  * @api public
package/package.json CHANGED
@@ -1,7 +1,7 @@
1
1
  {
2
2
  "name": "mongoose",
3
3
  "description": "Mongoose MongoDB ODM",
4
- "version": "9.2.0",
4
+ "version": "9.2.2",
5
5
  "author": "Guillermo Rauch <guillermo@learnboost.com>",
6
6
  "keywords": [
7
7
  "mongodb",
@@ -35,6 +35,7 @@
35
35
  "acquit-ignore": "0.2.2",
36
36
  "acquit-require": "0.1.1",
37
37
  "ajv": "8.17.1",
38
+ "c8": "10.1.3",
38
39
  "cheerio": "^1.2",
39
40
  "dox": "1.0.0",
40
41
  "eslint": "9.39.2",
@@ -54,9 +55,9 @@
54
55
  "mongodb-memory-server": "11.0.1",
55
56
  "mongodb-runner": "^6.0.0",
56
57
  "ncp": "^2.0.0",
57
- "c8": "10.1.3",
58
58
  "pug": "3.0.3",
59
59
  "sinon": "21.0.1",
60
+ "tstyche": "^6.2.0",
60
61
  "typescript": "5.9.3",
61
62
  "typescript-eslint": "^8.31.1",
62
63
  "uuid": "11.1.0"
@@ -98,7 +99,7 @@
98
99
  "test-deno:ci": "npm run test-deno -- --reporter min",
99
100
  "test-rs": "START_REPLICA_SET=1 mocha --timeout 30000 --exit ./test/*.test.js",
100
101
  "test-rs:ci": "npm run test-rs -- --reporter min",
101
- "test:types": "tsc --project test/types/tsconfig.json",
102
+ "test:types": "tstyche",
102
103
  "setup-test-encryption": "node scripts/setup-encryption-tests.js",
103
104
  "test-encryption": "mocha --exit ./test/encryption/*.test.js",
104
105
  "test-encryption:ci": "npm run test-encryption -- --reporter min",
@@ -0,0 +1,6 @@
1
+ {
2
+ "$schema": "https://tstyche.org/schemas/config.json",
3
+ "testFileMatch": [
4
+ "test/types/*.test.*"
5
+ ]
6
+ }
package/types/index.d.ts CHANGED
@@ -945,29 +945,6 @@ declare module 'mongoose' {
945
945
  : BufferToBinary<T[K]>;
946
946
  } : T;
947
947
 
948
- /**
949
- * Converts any UUID properties into strings for JSON serialization
950
- */
951
- export type UUIDToJSON<T> = T extends Types.UUID
952
- ? string
953
- : T extends mongodb.UUID
954
- ? string
955
- : T extends Document
956
- ? T
957
- : T extends TreatAsPrimitives
958
- ? T
959
- : T extends Record<string, any> ? {
960
- [K in keyof T]: T[K] extends Types.UUID
961
- ? string
962
- : T[K] extends mongodb.UUID
963
- ? string
964
- : T[K] extends Types.DocumentArray<infer ItemType>
965
- ? Types.DocumentArray<UUIDToJSON<ItemType>>
966
- : T[K] extends Types.Subdocument<unknown, unknown, infer SubdocType>
967
- ? HydratedSingleSubdocument<UUIDToJSON<SubdocType>>
968
- : UUIDToJSON<T[K]>;
969
- } : T;
970
-
971
948
  /**
972
949
  * Converts any UUID properties into strings for JSON serialization
973
950
  */
package/types/query.d.ts CHANGED
@@ -20,8 +20,18 @@ declare module 'mongoose' {
20
20
 
21
21
  export type ApplyBasicQueryCasting<T> = QueryTypeCasting<T> | QueryTypeCasting<T[]> | (T extends (infer U)[] ? QueryTypeCasting<U> : T) | null;
22
22
 
23
- type _QueryFilter<T> = ({ [P in keyof T]?: mongodb.Condition<ApplyBasicQueryCasting<T[P]>>; } & mongodb.RootFilterOperators<{ [P in keyof mongodb.WithId<T>]?: ApplyBasicQueryCasting<mongodb.WithId<T>[P]>; }>);
24
- type QueryFilter<T> = IsItRecordAndNotAny<T> extends true ? _QueryFilter<WithLevel1NestedPaths<T>> : _QueryFilter<Record<string, any>>;
23
+ type _QueryFilter<T> = (
24
+ { [P in keyof T]?: mongodb.Condition<ApplyBasicQueryCasting<T[P]>>; } &
25
+ mongodb.RootFilterOperators<{ [P in keyof mongodb.WithId<T>]?: ApplyBasicQueryCasting<mongodb.WithId<T>[P]>; }>
26
+ );
27
+ type _QueryFilterLooseId<T> = (
28
+ { [P in keyof T]?: mongodb.Condition<ApplyBasicQueryCasting<T[P]>>; } &
29
+ mongodb.RootFilterOperators<
30
+ { [P in keyof T]?: ApplyBasicQueryCasting<T[P]>; } &
31
+ { _id?: any; }
32
+ >
33
+ );
34
+ type QueryFilter<T> = IsItRecordAndNotAny<T> extends true ? _QueryFilter<WithLevel1NestedPaths<T>> : _QueryFilterLooseId<Record<string, any>>;
25
35
 
26
36
  type MongooseBaseQueryOptionKeys =
27
37
  | 'context'
package/types/types.d.ts CHANGED
@@ -84,7 +84,7 @@ declare module 'mongoose' {
84
84
  class ObjectId extends mongodb.ObjectId {
85
85
  }
86
86
 
87
- class Subdocument<IdType = any, TQueryHelpers = any, DocType = any> extends Document<IdType, TQueryHelpers, DocType> {
87
+ class Subdocument<IdType = any, TQueryHelpers = any, DocType = any, TVirtuals = {}, TSchemaOptions = {}> extends Document<IdType, TQueryHelpers, DocType, TVirtuals, TSchemaOptions> {
88
88
  $isSingleNested: true;
89
89
 
90
90
  /** Returns the top level document of this sub-document. */
@@ -97,7 +97,7 @@ declare module 'mongoose' {
97
97
  $parent(): Document;
98
98
  }
99
99
 
100
- class ArraySubdocument<IdType = any, TQueryHelpers = unknown, DocType = unknown> extends Subdocument<IdType, TQueryHelpers, DocType> {
100
+ class ArraySubdocument<IdType = any, TQueryHelpers = unknown, DocType = unknown, TVirtuals = {}, TSchemaOptions = {}> extends Subdocument<IdType, TQueryHelpers, DocType, TVirtuals, TSchemaOptions> {
101
101
  /** Returns this sub-documents parent array. */
102
102
  parentArray(): Types.DocumentArray<unknown>;
103
103
  }