mongoose 8.6.4 → 8.7.0

This diff represents the content of publicly available package versions that have been released to one of the supported registries. The information contained in this diff is provided for informational purposes only and reflects changes between package versions as they appear in their respective public registries.
package/lib/connection.js CHANGED
@@ -106,6 +106,15 @@ Object.setPrototypeOf(Connection.prototype, EventEmitter.prototype);
106
106
 
107
107
  Object.defineProperty(Connection.prototype, 'readyState', {
108
108
  get: function() {
109
+ // If connection thinks it is connected, but we haven't received a heartbeat in 2 heartbeat intervals,
110
+ // that likely means the connection is stale (potentially due to frozen AWS Lambda container)
111
+ if (
112
+ this._readyState === STATES.connected &&
113
+ this._lastHeartbeatAt != null &&
114
+ typeof this.client?.topology?.s?.description?.heartbeatFrequencyMS === 'number' &&
115
+ Date.now() - this._lastHeartbeatAt >= this.client.topology.s.description.heartbeatFrequencyMS * 2) {
116
+ return STATES.disconnected;
117
+ }
109
118
  return this._readyState;
110
119
  },
111
120
  set: function(val) {
@@ -23,6 +23,11 @@ const utils = require('../../utils');
23
23
  function NativeConnection() {
24
24
  MongooseConnection.apply(this, arguments);
25
25
  this._listening = false;
26
+ // Tracks the last time (as unix timestamp) the connection received a
27
+ // serverHeartbeatSucceeded or serverHeartbeatFailed event from the underlying MongoClient.
28
+ // If we haven't received one in a while (like due to a frozen AWS Lambda container) then
29
+ // `readyState` is likely stale.
30
+ this._lastHeartbeatAt = null;
26
31
  }
27
32
 
28
33
  /**
@@ -106,6 +111,7 @@ NativeConnection.prototype.useDb = function(name, options) {
106
111
  _opts.noListener = options.noListener;
107
112
  }
108
113
  newConn.db = _this.client.db(name, _opts);
114
+ newConn._lastHeartbeatAt = _this._lastHeartbeatAt;
109
115
  newConn.onOpen();
110
116
  }
111
117
 
@@ -409,6 +415,9 @@ function _setClient(conn, client, options, dbName) {
409
415
  }
410
416
  });
411
417
  }
418
+ client.on('serverHeartbeatSucceeded', () => {
419
+ conn._lastHeartbeatAt = Date.now();
420
+ });
412
421
 
413
422
  if (options.monitorCommands) {
414
423
  client.on('commandStarted', (data) => conn.emit('commandStarted', data));
@@ -417,6 +426,9 @@ function _setClient(conn, client, options, dbName) {
417
426
  }
418
427
 
419
428
  conn.onOpen();
429
+ if (client.topology?.s?.state === 'connected') {
430
+ conn._lastHeartbeatAt = Date.now();
431
+ }
420
432
 
421
433
  for (const i in conn.collections) {
422
434
  if (utils.object.hasOwnProperty(conn.collections, i)) {
@@ -0,0 +1,44 @@
1
+ /*!
2
+ * Module dependencies.
3
+ */
4
+
5
+ 'use strict';
6
+
7
+ const MongooseError = require('./mongooseError');
8
+
9
+
10
+ /**
11
+ * If the underwriting `bulkWrite()` for `bulkSave()` succeeded, but wasn't able to update or
12
+ * insert all documents, we throw this error.
13
+ *
14
+ * @api private
15
+ */
16
+
17
+ class MongooseBulkSaveIncompleteError extends MongooseError {
18
+ constructor(modelName, documents, bulkWriteResult) {
19
+ const matchedCount = bulkWriteResult?.matchedCount ?? 0;
20
+ const insertedCount = bulkWriteResult?.insertedCount ?? 0;
21
+ let preview = documents.map(doc => doc._id).join(', ');
22
+ if (preview.length > 100) {
23
+ preview = preview.slice(0, 100) + '...';
24
+ }
25
+
26
+ const numDocumentsNotUpdated = documents.length - matchedCount - insertedCount;
27
+ super(`${modelName}.bulkSave() was not able to update ${numDocumentsNotUpdated} of the given documents due to incorrect version or optimistic concurrency, document ids: ${preview}`);
28
+
29
+ this.modelName = modelName;
30
+ this.documents = documents;
31
+ this.bulkWriteResult = bulkWriteResult;
32
+ this.numDocumentsNotUpdated = numDocumentsNotUpdated;
33
+ }
34
+ }
35
+
36
+ Object.defineProperty(MongooseBulkSaveIncompleteError.prototype, 'name', {
37
+ value: 'MongooseBulkSaveIncompleteError'
38
+ });
39
+
40
+ /*!
41
+ * exports
42
+ */
43
+
44
+ module.exports = MongooseBulkSaveIncompleteError;
@@ -0,0 +1,146 @@
1
+ 'use strict';
2
+
3
+ const mpath = require('mpath');
4
+
5
+ module.exports = applyVirtuals;
6
+
7
+ /**
8
+ * Apply a given schema's virtuals to a given POJO
9
+ *
10
+ * @param {Schema} schema
11
+ * @param {Object} obj
12
+ * @param {Array<string>} [virtuals] optional whitelist of virtuals to apply
13
+ * @returns
14
+ */
15
+
16
+ function applyVirtuals(schema, obj, virtuals) {
17
+ if (obj == null) {
18
+ return obj;
19
+ }
20
+
21
+ let virtualsForChildren = virtuals;
22
+ let toApply = null;
23
+
24
+ if (Array.isArray(virtuals)) {
25
+ virtualsForChildren = [];
26
+ toApply = [];
27
+ for (const virtual of virtuals) {
28
+ if (virtual.length === 1) {
29
+ toApply.push(virtual[0]);
30
+ } else {
31
+ virtualsForChildren.push(virtual);
32
+ }
33
+ }
34
+ }
35
+
36
+ applyVirtualsToChildren(schema, obj, virtualsForChildren);
37
+ return applyVirtualsToDoc(schema, obj, toApply);
38
+ }
39
+
40
+ /**
41
+ * Apply virtuals to any subdocuments
42
+ *
43
+ * @param {Schema} schema subdocument schema
44
+ * @param {Object} res subdocument
45
+ * @param {Array<String>} [virtuals] optional whitelist of virtuals to apply
46
+ */
47
+
48
+ function applyVirtualsToChildren(schema, res, virtuals) {
49
+ let attachedVirtuals = false;
50
+ for (const childSchema of schema.childSchemas) {
51
+ const _path = childSchema.model.path;
52
+ const _schema = childSchema.schema;
53
+ if (!_path) {
54
+ continue;
55
+ }
56
+ const _obj = mpath.get(_path, res);
57
+ if (_obj == null || (Array.isArray(_obj) && _obj.flat(Infinity).length === 0)) {
58
+ continue;
59
+ }
60
+
61
+ let virtualsForChild = null;
62
+ if (Array.isArray(virtuals)) {
63
+ virtualsForChild = [];
64
+ for (const virtual of virtuals) {
65
+ if (virtual[0] == _path) {
66
+ virtualsForChild.push(virtual.slice(1));
67
+ }
68
+ }
69
+
70
+ if (virtualsForChild.length === 0) {
71
+ continue;
72
+ }
73
+ }
74
+
75
+ applyVirtuals(_schema, _obj, virtualsForChild);
76
+ attachedVirtuals = true;
77
+ }
78
+
79
+ if (virtuals && virtuals.length && !attachedVirtuals) {
80
+ applyVirtualsToDoc(schema, res, virtuals);
81
+ }
82
+ }
83
+
84
+ /**
85
+ * Apply virtuals to a given document. Does not apply virtuals to subdocuments: use `applyVirtualsToChildren` instead
86
+ *
87
+ * @param {Schema} schema
88
+ * @param {Object} doc
89
+ * @param {Array<String>} [virtuals] optional whitelist of virtuals to apply
90
+ * @returns
91
+ */
92
+
93
+ function applyVirtualsToDoc(schema, obj, virtuals) {
94
+ if (obj == null || typeof obj !== 'object') {
95
+ return;
96
+ }
97
+ if (Array.isArray(obj)) {
98
+ for (const el of obj) {
99
+ applyVirtualsToDoc(schema, el, virtuals);
100
+ }
101
+ return;
102
+ }
103
+
104
+ if (schema.discriminators && Object.keys(schema.discriminators).length > 0) {
105
+ for (const discriminatorKey of Object.keys(schema.discriminators)) {
106
+ const discriminator = schema.discriminators[discriminatorKey];
107
+ const key = discriminator.discriminatorMapping.key;
108
+ const value = discriminator.discriminatorMapping.value;
109
+ if (obj[key] == value) {
110
+ schema = discriminator;
111
+ break;
112
+ }
113
+ }
114
+ }
115
+
116
+ if (virtuals == null) {
117
+ virtuals = Object.keys(schema.virtuals);
118
+ }
119
+ for (const virtual of virtuals) {
120
+ if (schema.virtuals[virtual] == null) {
121
+ continue;
122
+ }
123
+ const virtualType = schema.virtuals[virtual];
124
+ const sp = Array.isArray(virtual)
125
+ ? virtual
126
+ : virtual.indexOf('.') === -1
127
+ ? [virtual]
128
+ : virtual.split('.');
129
+ let cur = obj;
130
+ for (let i = 0; i < sp.length - 1; ++i) {
131
+ cur[sp[i]] = sp[i] in cur ? cur[sp[i]] : {};
132
+ cur = cur[sp[i]];
133
+ }
134
+ let val = virtualType.applyGetters(cur[sp[sp.length - 1]], obj);
135
+ const isPopulateVirtual =
136
+ virtualType.options && (virtualType.options.ref || virtualType.options.refPath);
137
+ if (isPopulateVirtual && val === undefined) {
138
+ if (virtualType.options.justOne) {
139
+ val = null;
140
+ } else {
141
+ val = [];
142
+ }
143
+ }
144
+ cur[sp[sp.length - 1]] = val;
145
+ }
146
+ }
@@ -2,6 +2,7 @@
2
2
 
3
3
  const CastError = require('../../error/cast');
4
4
  const MongooseError = require('../../error/mongooseError');
5
+ const SchemaString = require('../../schema/string');
5
6
  const StrictModeError = require('../../error/strict');
6
7
  const ValidationError = require('../../error/validation');
7
8
  const castNumber = require('../../cast/number');
@@ -307,6 +308,20 @@ function walkUpdatePath(schema, obj, op, options, context, filter, pref) {
307
308
  continue;
308
309
  }
309
310
 
311
+ hasKeys = true;
312
+ } else if (op === '$rename') {
313
+ const schematype = new SchemaString(`${prefix}${key}.$rename`);
314
+ try {
315
+ obj[key] = castUpdateVal(schematype, val, op, key, context, prefix + key);
316
+ } catch (error) {
317
+ aggregatedError = _appendError(error, context, key, aggregatedError);
318
+ }
319
+
320
+ if (obj[key] === void 0) {
321
+ delete obj[key];
322
+ continue;
323
+ }
324
+
310
325
  hasKeys = true;
311
326
  } else {
312
327
  const pathToCheck = (prefix + key);
@@ -372,10 +387,12 @@ function walkUpdatePath(schema, obj, op, options, context, filter, pref) {
372
387
  delete obj[key];
373
388
  }
374
389
  } else {
375
- // gh-1845 temporary fix: ignore $rename. See gh-3027 for tracking
376
- // improving this.
377
390
  if (op === '$rename') {
378
- hasKeys = true;
391
+ if (obj[key] == null) {
392
+ throw new CastError('String', obj[key], `${prefix}${key}.$rename`);
393
+ }
394
+ const schematype = new SchemaString(`${prefix}${key}.$rename`);
395
+ obj[key] = schematype.castForQuery(null, obj[key], context);
379
396
  continue;
380
397
  }
381
398
 
package/lib/model.js CHANGED
@@ -31,6 +31,7 @@ const applySchemaCollation = require('./helpers/indexes/applySchemaCollation');
31
31
  const applyStaticHooks = require('./helpers/model/applyStaticHooks');
32
32
  const applyStatics = require('./helpers/model/applyStatics');
33
33
  const applyWriteConcern = require('./helpers/schema/applyWriteConcern');
34
+ const applyVirtualsHelper = require('./helpers/document/applyVirtuals');
34
35
  const assignVals = require('./helpers/populate/assignVals');
35
36
  const castBulkWrite = require('./helpers/model/castBulkWrite');
36
37
  const clone = require('./helpers/clone');
@@ -64,6 +65,7 @@ const STATES = require('./connectionState');
64
65
  const util = require('util');
65
66
  const utils = require('./utils');
66
67
  const minimize = require('./helpers/minimize');
68
+ const MongooseBulkSaveIncompleteError = require('./error/bulkSaveIncompleteError');
67
69
 
68
70
  const modelCollectionSymbol = Symbol('mongoose#Model#collection');
69
71
  const modelDbSymbol = Symbol('mongoose#Model#db');
@@ -3418,11 +3420,10 @@ Model.bulkSave = async function bulkSave(documents, options) {
3418
3420
 
3419
3421
  const matchedCount = bulkWriteResult?.matchedCount ?? 0;
3420
3422
  const insertedCount = bulkWriteResult?.insertedCount ?? 0;
3421
- if (writeOperations.length > 0 && matchedCount + insertedCount === 0 && !bulkWriteError) {
3422
- throw new DocumentNotFoundError(
3423
- writeOperations.filter(op => op.updateOne).map(op => op.updateOne.filter),
3423
+ if (writeOperations.length > 0 && matchedCount + insertedCount < writeOperations.length && !bulkWriteError) {
3424
+ throw new MongooseBulkSaveIncompleteError(
3424
3425
  this.modelName,
3425
- writeOperations.length,
3426
+ documents,
3426
3427
  bulkWriteResult
3427
3428
  );
3428
3429
  }
@@ -3488,6 +3489,9 @@ function handleSuccessfulWrite(document) {
3488
3489
  */
3489
3490
 
3490
3491
  Model.applyDefaults = function applyDefaults(doc) {
3492
+ if (doc == null) {
3493
+ return doc;
3494
+ }
3491
3495
  if (doc.$__ != null) {
3492
3496
  applyDefaultsHelper(doc, doc.$__.fields, doc.$__.exclude);
3493
3497
 
@@ -3503,6 +3507,40 @@ Model.applyDefaults = function applyDefaults(doc) {
3503
3507
  return doc;
3504
3508
  };
3505
3509
 
3510
+ /**
3511
+ * Apply this model's virtuals to a given POJO. Virtuals execute with the POJO as the context `this`.
3512
+ *
3513
+ * #### Example:
3514
+ *
3515
+ * const userSchema = new Schema({ name: String });
3516
+ * userSchema.virtual('upper').get(function() { return this.name.toUpperCase(); });
3517
+ * const User = mongoose.model('User', userSchema);
3518
+ *
3519
+ * const obj = { name: 'John' };
3520
+ * User.applyVirtuals(obj);
3521
+ * obj.name; // 'John'
3522
+ * obj.upper; // 'JOHN', Mongoose applied the return value of the virtual to the given object
3523
+ *
3524
+ * @param {Object} obj object or document to apply virtuals on
3525
+ * @param {Array<string>} [virtualsToApply] optional whitelist of virtuals to apply
3526
+ * @returns {Object} obj
3527
+ * @api public
3528
+ */
3529
+
3530
+ Model.applyVirtuals = function applyVirtuals(obj, virtualsToApply) {
3531
+ if (obj == null) {
3532
+ return obj;
3533
+ }
3534
+ // Nothing to do if this is already a hydrated document - it should already have virtuals
3535
+ if (obj.$__ != null) {
3536
+ return obj;
3537
+ }
3538
+
3539
+ applyVirtualsHelper(this.schema, obj, virtualsToApply);
3540
+
3541
+ return obj;
3542
+ };
3543
+
3506
3544
  /**
3507
3545
  * Cast the given POJO to the model's schema
3508
3546
  *
package/lib/schemaType.js CHANGED
@@ -1724,6 +1724,25 @@ SchemaType.prototype.clone = function() {
1724
1724
  return schematype;
1725
1725
  };
1726
1726
 
1727
+ /**
1728
+ * Returns the embedded schema type, if any. For arrays, document arrays, and maps, `getEmbeddedSchemaType()`
1729
+ * returns the schema type of the array's elements (or map's elements). For other types, `getEmbeddedSchemaType()`
1730
+ * returns `undefined`.
1731
+ *
1732
+ * #### Example:
1733
+ *
1734
+ * const schema = new Schema({ name: String, tags: [String] });
1735
+ * schema.path('name').getEmbeddedSchemaType(); // undefined
1736
+ * schema.path('tags').getEmbeddedSchemaType(); // SchemaString { path: 'tags', ... }
1737
+ *
1738
+ * @returns {SchemaType} embedded schematype
1739
+ * @api public
1740
+ */
1741
+
1742
+ SchemaType.prototype.getEmbeddedSchemaType = function getEmbeddedSchemaType() {
1743
+ return this.$embeddedSchemaType;
1744
+ };
1745
+
1727
1746
  /*!
1728
1747
  * Module exports.
1729
1748
  */
@@ -410,6 +410,7 @@ const methods = {
410
410
 
411
411
  addToSet() {
412
412
  _checkManualPopulation(this, arguments);
413
+ _depopulateIfNecessary(this, arguments);
413
414
 
414
415
  const values = [].map.call(arguments, this._mapCast, this);
415
416
  const added = [];
@@ -691,6 +692,7 @@ const methods = {
691
692
  }
692
693
 
693
694
  _checkManualPopulation(this, values);
695
+ _depopulateIfNecessary(this, values);
694
696
 
695
697
  values = [].map.call(values, this._mapCast, this);
696
698
  let ret;
@@ -1009,6 +1011,30 @@ function _checkManualPopulation(arr, docs) {
1009
1011
  }
1010
1012
  }
1011
1013
 
1014
+ /*!
1015
+ * If `docs` isn't all instances of the right model, depopulate `arr`
1016
+ */
1017
+
1018
+ function _depopulateIfNecessary(arr, docs) {
1019
+ const ref = arr == null ?
1020
+ null :
1021
+ arr[arraySchemaSymbol] && arr[arraySchemaSymbol].caster && arr[arraySchemaSymbol].caster.options && arr[arraySchemaSymbol].caster.options.ref || null;
1022
+ const parentDoc = arr[arrayParentSymbol];
1023
+ const path = arr[arrayPathSymbol];
1024
+ if (!ref || !parentDoc.populated(path)) {
1025
+ return;
1026
+ }
1027
+ for (const doc of docs) {
1028
+ if (doc == null) {
1029
+ continue;
1030
+ }
1031
+ if (typeof doc !== 'object' || doc instanceof String || doc instanceof Number || doc instanceof Buffer || utils.isMongooseType(doc)) {
1032
+ parentDoc.depopulate(path);
1033
+ break;
1034
+ }
1035
+ }
1036
+ }
1037
+
1012
1038
  const returnVanillaArrayMethods = [
1013
1039
  'filter',
1014
1040
  'flat',
package/package.json CHANGED
@@ -1,7 +1,7 @@
1
1
  {
2
2
  "name": "mongoose",
3
3
  "description": "Mongoose MongoDB ODM",
4
- "version": "8.6.4",
4
+ "version": "8.7.0",
5
5
  "author": "Guillermo Rauch <guillermo@learnboost.com>",
6
6
  "keywords": [
7
7
  "mongodb",
@@ -21,7 +21,7 @@
21
21
  "dependencies": {
22
22
  "bson": "^6.7.0",
23
23
  "kareem": "2.6.3",
24
- "mongodb": "6.8.0",
24
+ "mongodb": "6.9.0",
25
25
  "mpath": "0.9.0",
26
26
  "mquery": "5.0.0",
27
27
  "ms": "2.1.3",
@@ -24,9 +24,6 @@ declare module 'mongoose' {
24
24
  /** This documents _id. */
25
25
  _id: T;
26
26
 
27
- /** This documents __v. */
28
- __v?: any;
29
-
30
27
  /** Assert that a given path or paths is populated. Throws an error if not populated. */
31
28
  $assertPopulated<Paths = {}>(path: string | string[], values?: Partial<Paths>): Omit<this, keyof Paths> & Paths;
32
29
 
package/types/index.d.ts CHANGED
@@ -138,6 +138,10 @@ declare module 'mongoose' {
138
138
  ? IfAny<U, T & { _id: Types.ObjectId }, T & Required<{ _id: U }>>
139
139
  : T & { _id: Types.ObjectId };
140
140
 
141
+ export type Default__v<T> = T extends { __v?: infer U }
142
+ ? T
143
+ : T & { __v?: number };
144
+
141
145
  /** Helper type for getting the hydrated document type from the raw document type. The hydrated document type is what `new MyModel()` returns. */
142
146
  export type HydratedDocument<
143
147
  DocType,
@@ -147,12 +151,12 @@ declare module 'mongoose' {
147
151
  DocType,
148
152
  any,
149
153
  TOverrides extends Record<string, never> ?
150
- Document<unknown, TQueryHelpers, DocType> & Require_id<DocType> :
154
+ Document<unknown, TQueryHelpers, DocType> & Default__v<Require_id<DocType>> :
151
155
  IfAny<
152
156
  TOverrides,
153
- Document<unknown, TQueryHelpers, DocType> & Require_id<DocType>,
157
+ Document<unknown, TQueryHelpers, DocType> & Default__v<Require_id<DocType>>,
154
158
  Document<unknown, TQueryHelpers, DocType> & MergeType<
155
- Require_id<DocType>,
159
+ Default__v<Require_id<DocType>>,
156
160
  TOverrides
157
161
  >
158
162
  >
package/types/models.d.ts CHANGED
@@ -290,6 +290,9 @@ declare module 'mongoose' {
290
290
  applyDefaults(obj: AnyObject): AnyObject;
291
291
  applyDefaults(obj: TRawDocType): TRawDocType;
292
292
 
293
+ /* Apply virtuals to the given POJO. */
294
+ applyVirtuals(obj: AnyObject, virtalsToApply?: string[]): AnyObject;
295
+
293
296
  /**
294
297
  * Sends multiple `insertOne`, `updateOne`, `updateMany`, `replaceOne`,
295
298
  * `deleteOne`, and/or `deleteMany` operations to the MongoDB server in one
@@ -232,6 +232,9 @@ declare module 'mongoose' {
232
232
  /** Adds a getter to this schematype. */
233
233
  get(fn: Function): this;
234
234
 
235
+ /** Gets this SchemaType's embedded SchemaType, if any */
236
+ getEmbeddedSchemaType<T = any, DocType = any>(): SchemaType<T, DocType> | undefined;
237
+
235
238
  /**
236
239
  * Defines this path as immutable. Mongoose prevents you from changing
237
240
  * immutable paths unless the parent document has [`isNew: true`](/docs/api/document.html#document_Document-isNew).