mongoose 8.5.5 → 8.6.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.
@@ -5,6 +5,7 @@
5
5
  */
6
6
 
7
7
  const EventEmitter = require('events').EventEmitter;
8
+ const MongooseError = require('../error/mongooseError');
8
9
 
9
10
  /*!
10
11
  * ignore
@@ -25,6 +26,7 @@ class ChangeStream extends EventEmitter {
25
26
  this.bindedEvents = false;
26
27
  this.pipeline = pipeline;
27
28
  this.options = options;
29
+ this.errored = false;
28
30
 
29
31
  if (options && options.hydrate && !options.model) {
30
32
  throw new Error(
@@ -33,16 +35,36 @@ class ChangeStream extends EventEmitter {
33
35
  );
34
36
  }
35
37
 
36
- // This wrapper is necessary because of buffering.
37
- changeStreamThunk((err, driverChangeStream) => {
38
- if (err != null) {
38
+ let syncError = null;
39
+ this.$driverChangeStreamPromise = new Promise((resolve, reject) => {
40
+ // This wrapper is necessary because of buffering.
41
+ try {
42
+ changeStreamThunk((err, driverChangeStream) => {
43
+ if (err != null) {
44
+ this.errored = true;
45
+ this.emit('error', err);
46
+ return reject(err);
47
+ }
48
+
49
+ this.driverChangeStream = driverChangeStream;
50
+ this.emit('ready');
51
+ resolve();
52
+ });
53
+ } catch (err) {
54
+ syncError = err;
55
+ this.errored = true;
39
56
  this.emit('error', err);
40
- return;
57
+ reject(err);
41
58
  }
42
-
43
- this.driverChangeStream = driverChangeStream;
44
- this.emit('ready');
45
59
  });
60
+
61
+ // Because a ChangeStream is an event emitter, there's no way to register an 'error' handler
62
+ // that catches errors which occur in the constructor, unless we force sync errors into async
63
+ // errors with setImmediate(). For cleaner stack trace, we just immediately throw any synchronous
64
+ // errors that occurred with changeStreamThunk().
65
+ if (syncError != null) {
66
+ throw syncError;
67
+ }
46
68
  }
47
69
 
48
70
  _bindEvents() {
@@ -53,20 +75,23 @@ class ChangeStream extends EventEmitter {
53
75
  this.bindedEvents = true;
54
76
 
55
77
  if (this.driverChangeStream == null) {
56
- this.once('ready', () => {
57
- this.driverChangeStream.on('close', () => {
58
- this.closed = true;
59
- });
78
+ this.$driverChangeStreamPromise.then(
79
+ () => {
80
+ this.driverChangeStream.on('close', () => {
81
+ this.closed = true;
82
+ });
60
83
 
61
- driverChangeStreamEvents.forEach(ev => {
62
- this.driverChangeStream.on(ev, data => {
63
- if (data != null && data.fullDocument != null && this.options && this.options.hydrate) {
64
- data.fullDocument = this.options.model.hydrate(data.fullDocument);
65
- }
66
- this.emit(ev, data);
84
+ driverChangeStreamEvents.forEach(ev => {
85
+ this.driverChangeStream.on(ev, data => {
86
+ if (data != null && data.fullDocument != null && this.options && this.options.hydrate) {
87
+ data.fullDocument = this.options.model.hydrate(data.fullDocument);
88
+ }
89
+ this.emit(ev, data);
90
+ });
67
91
  });
68
- });
69
- });
92
+ },
93
+ () => {} // No need to register events if opening change stream failed
94
+ );
70
95
 
71
96
  return;
72
97
  }
@@ -86,10 +111,16 @@ class ChangeStream extends EventEmitter {
86
111
  }
87
112
 
88
113
  hasNext(cb) {
114
+ if (this.errored) {
115
+ throw new MongooseError('Cannot call hasNext() on errored ChangeStream');
116
+ }
89
117
  return this.driverChangeStream.hasNext(cb);
90
118
  }
91
119
 
92
120
  next(cb) {
121
+ if (this.errored) {
122
+ throw new MongooseError('Cannot call next() on errored ChangeStream');
123
+ }
93
124
  if (this.options && this.options.hydrate) {
94
125
  if (cb != null) {
95
126
  const originalCb = cb;
@@ -120,16 +151,25 @@ class ChangeStream extends EventEmitter {
120
151
  }
121
152
 
122
153
  addListener(event, handler) {
154
+ if (this.errored) {
155
+ throw new MongooseError('Cannot call addListener() on errored ChangeStream');
156
+ }
123
157
  this._bindEvents();
124
158
  return super.addListener(event, handler);
125
159
  }
126
160
 
127
161
  on(event, handler) {
162
+ if (this.errored) {
163
+ throw new MongooseError('Cannot call on() on errored ChangeStream');
164
+ }
128
165
  this._bindEvents();
129
166
  return super.on(event, handler);
130
167
  }
131
168
 
132
169
  once(event, handler) {
170
+ if (this.errored) {
171
+ throw new MongooseError('Cannot call once() on errored ChangeStream');
172
+ }
133
173
  this._bindEvents();
134
174
  return super.once(event, handler);
135
175
  }
@@ -142,8 +182,12 @@ class ChangeStream extends EventEmitter {
142
182
  this.closed = true;
143
183
  if (this.driverChangeStream) {
144
184
  return this.driverChangeStream.close();
185
+ } else {
186
+ return this.$driverChangeStreamPromise.then(
187
+ () => this.driverChangeStream.close(),
188
+ () => {} // No need to close if opening the change stream failed
189
+ );
145
190
  }
146
- return Promise.resolve();
147
191
  }
148
192
  }
149
193
 
@@ -10,6 +10,7 @@ const eachAsync = require('../helpers/cursor/eachAsync');
10
10
  const helpers = require('../queryHelpers');
11
11
  const kareem = require('kareem');
12
12
  const immediate = require('../helpers/immediate');
13
+ const { once } = require('node:events');
13
14
  const util = require('util');
14
15
 
15
16
  /**
@@ -42,6 +43,7 @@ function QueryCursor(query) {
42
43
  this.cursor = null;
43
44
  this.skipped = false;
44
45
  this.query = query;
46
+ this._closed = false;
45
47
  const model = query.model;
46
48
  this._mongooseOptions = {};
47
49
  this._transforms = [];
@@ -135,6 +137,25 @@ QueryCursor.prototype._read = function() {
135
137
  });
136
138
  };
137
139
 
140
+ /**
141
+ * Returns the underlying cursor from the MongoDB Node driver that this cursor uses.
142
+ *
143
+ * @method getDriverCursor
144
+ * @memberOf QueryCursor
145
+ * @returns {Cursor} MongoDB Node driver cursor instance
146
+ * @instance
147
+ * @api public
148
+ */
149
+
150
+ QueryCursor.prototype.getDriverCursor = async function getDriverCursor() {
151
+ if (this.cursor) {
152
+ return this.cursor;
153
+ }
154
+
155
+ await once(this, 'cursor');
156
+ return this.cursor;
157
+ };
158
+
138
159
  /**
139
160
  * Registers a transform function which subsequently maps documents retrieved
140
161
  * via the streams interface or `.next()`
@@ -209,6 +230,7 @@ QueryCursor.prototype.close = async function close() {
209
230
  }
210
231
  try {
211
232
  await this.cursor.close();
233
+ this._closed = true;
212
234
  this.emit('close');
213
235
  } catch (error) {
214
236
  this.listeners('error').length > 0 && this.emit('error', error);
@@ -246,6 +268,9 @@ QueryCursor.prototype.next = async function next() {
246
268
  if (typeof arguments[0] === 'function') {
247
269
  throw new MongooseError('QueryCursor.prototype.next() no longer accepts a callback');
248
270
  }
271
+ if (this._closed) {
272
+ throw new MongooseError('Cannot call `next()` on a closed cursor');
273
+ }
249
274
  return new Promise((resolve, reject) => {
250
275
  _next(this, function(error, doc) {
251
276
  if (error) {
package/lib/document.js CHANGED
@@ -3856,7 +3856,6 @@ Document.prototype.$toObject = function(options, json) {
3856
3856
  // Parent options should only bubble down for subdocuments, not populated docs
3857
3857
  options._parentOptions = this.$isSubdocument ? options : null;
3858
3858
 
3859
- options._skipSingleNestedGetters = false;
3860
3859
  // remember the root transform function
3861
3860
  // to save it from being overwritten by sub-transform functions
3862
3861
  // const originalTransform = options.transform;
@@ -3870,13 +3869,13 @@ Document.prototype.$toObject = function(options, json) {
3870
3869
  ret = clone(this._doc, options) || {};
3871
3870
  }
3872
3871
 
3873
- options._skipSingleNestedGetters = true;
3874
3872
  const getters = options._calledWithOptions.getters
3875
3873
  ?? options.getters
3876
3874
  ?? defaultOptions.getters
3877
3875
  ?? false;
3876
+
3878
3877
  if (getters) {
3879
- applyGetters(this, ret, options);
3878
+ applyGetters(this, ret);
3880
3879
 
3881
3880
  if (options.minimize) {
3882
3881
  ret = minimize(ret) || {};
@@ -4187,12 +4186,11 @@ function applyVirtuals(self, json, options, toObjectOptions) {
4187
4186
  *
4188
4187
  * @param {Document} self
4189
4188
  * @param {Object} json
4190
- * @param {Object} [options]
4191
4189
  * @return {Object} `json`
4192
4190
  * @api private
4193
4191
  */
4194
4192
 
4195
- function applyGetters(self, json, options) {
4193
+ function applyGetters(self, json) {
4196
4194
  const schema = self.$__schema;
4197
4195
  const paths = Object.keys(schema.paths);
4198
4196
  let i = paths.length;
@@ -4228,8 +4226,10 @@ function applyGetters(self, json, options) {
4228
4226
  if (branch != null && typeof branch !== 'object') {
4229
4227
  break;
4230
4228
  } else if (ii === last) {
4231
- const val = self.$get(path);
4232
- branch[part] = clone(val, options);
4229
+ branch[part] = schema.paths[path].applyGetters(
4230
+ branch[part],
4231
+ self
4232
+ );
4233
4233
  if (Array.isArray(branch[part]) && schema.paths[path].$embeddedSchemaType) {
4234
4234
  for (let i = 0; i < branch[part].length; ++i) {
4235
4235
  branch[part][i] = schema.paths[path].$embeddedSchemaType.applyGetters(
@@ -40,11 +40,6 @@ function clone(obj, options, isArrayChild) {
40
40
 
41
41
  if (isMongooseObject(obj)) {
42
42
  if (options) {
43
- // Single nested subdocs should apply getters later in `applyGetters()`
44
- // when calling `toObject()`. See gh-7442, gh-8295
45
- if (options._skipSingleNestedGetters && obj.$isSingleNested) {
46
- options._calledWithOptions = Object.assign({}, options._calledWithOptions || {}, { getters: false });
47
- }
48
43
  if (options.retainDocuments && obj.$__ != null) {
49
44
  const clonedDoc = obj.$clone();
50
45
  if (obj.__index != null) {
package/lib/model.js CHANGED
@@ -2115,17 +2115,21 @@ Model.countDocuments = function countDocuments(conditions, options) {
2115
2115
  *
2116
2116
  * @param {String} field
2117
2117
  * @param {Object} [conditions] optional
2118
+ * @param {Object} [options] optional
2118
2119
  * @return {Query}
2119
2120
  * @api public
2120
2121
  */
2121
2122
 
2122
- Model.distinct = function distinct(field, conditions) {
2123
+ Model.distinct = function distinct(field, conditions, options) {
2123
2124
  _checkContext(this, 'distinct');
2124
- if (typeof arguments[0] === 'function' || typeof arguments[1] === 'function') {
2125
+ if (typeof arguments[0] === 'function' || typeof arguments[1] === 'function' || typeof arguments[2] === 'function') {
2125
2126
  throw new MongooseError('Model.distinct() no longer accepts a callback');
2126
2127
  }
2127
2128
 
2128
2129
  const mq = new this.Query({}, {}, this, this.$__collection);
2130
+ if (options != null) {
2131
+ mq.setOptions(options);
2132
+ }
2129
2133
 
2130
2134
  return mq.distinct(field, conditions);
2131
2135
  };
@@ -3364,11 +3368,19 @@ Model.bulkWrite = async function bulkWrite(ops, options) {
3364
3368
  };
3365
3369
 
3366
3370
  /**
3367
- * takes an array of documents, gets the changes and inserts/updates documents in the database
3368
- * according to whether or not the document is new, or whether it has changes or not.
3371
+ * Takes an array of documents, gets the changes and inserts/updates documents in the database
3372
+ * according to whether or not the document is new, or whether it has changes or not.
3369
3373
  *
3370
3374
  * `bulkSave` uses `bulkWrite` under the hood, so it's mostly useful when dealing with many documents (10K+)
3371
3375
  *
3376
+ * `bulkSave()` throws errors under the following conditions:
3377
+ *
3378
+ * - one of the provided documents fails validation. In this case, `bulkSave()` does not send a `bulkWrite()`, and throws the first validation error.
3379
+ * - `bulkWrite()` fails (for example, due to being unable to connect to MongoDB or due to duplicate key error)
3380
+ * - `bulkWrite()` did not insert or update **any** documents. In this case, `bulkSave()` will throw a DocumentNotFound error.
3381
+ *
3382
+ * Note that `bulkSave()` will **not** throw an error if only some of the `save()` calls succeeded.
3383
+ *
3372
3384
  * @param {Array<Document>} documents
3373
3385
  * @param {Object} [options] options passed to the underlying `bulkWrite()`
3374
3386
  * @param {Boolean} [options.timestamps] defaults to `null`, when set to false, mongoose will not add/update timestamps to the documents.
@@ -3376,7 +3388,7 @@ Model.bulkWrite = async function bulkWrite(ops, options) {
3376
3388
  * @param {String|number} [options.w=1] The [write concern](https://www.mongodb.com/docs/manual/reference/write-concern/). See [`Query#w()`](https://mongoosejs.com/docs/api/query.html#Query.prototype.w()) for more information.
3377
3389
  * @param {number} [options.wtimeout=null] The [write concern timeout](https://www.mongodb.com/docs/manual/reference/write-concern/#wtimeout).
3378
3390
  * @param {Boolean} [options.j=true] If false, disable [journal acknowledgement](https://www.mongodb.com/docs/manual/reference/write-concern/#j-option)
3379
- *
3391
+ * @return {BulkWriteResult} the return value from `bulkWrite()`
3380
3392
  */
3381
3393
  Model.bulkSave = async function bulkSave(documents, options) {
3382
3394
  options = options || {};
@@ -3404,18 +3416,31 @@ Model.bulkSave = async function bulkSave(documents, options) {
3404
3416
  (err) => ({ bulkWriteResult: null, bulkWriteError: err })
3405
3417
  );
3406
3418
 
3407
- await Promise.all(
3408
- documents.map(async(document) => {
3409
- const documentError = bulkWriteError && bulkWriteError.writeErrors.find(writeError => {
3410
- const writeErrorDocumentId = writeError.err.op._id || writeError.err.op.q._id;
3411
- return writeErrorDocumentId.toString() === document._doc._id.toString();
3412
- });
3419
+ const matchedCount = bulkWriteResult?.matchedCount ?? 0;
3420
+ 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),
3424
+ this.modelName,
3425
+ writeOperations.length,
3426
+ bulkWriteResult
3427
+ );
3428
+ }
3413
3429
 
3414
- if (documentError == null) {
3415
- await handleSuccessfulWrite(document);
3416
- }
3417
- })
3418
- );
3430
+ const successfulDocuments = [];
3431
+ for (let i = 0; i < documents.length; i++) {
3432
+ const document = documents[i];
3433
+ const documentError = bulkWriteError && bulkWriteError.writeErrors.find(writeError => {
3434
+ const writeErrorDocumentId = writeError.err.op._id || writeError.err.op.q._id;
3435
+ return writeErrorDocumentId.toString() === document._doc._id.toString();
3436
+ });
3437
+
3438
+ if (documentError == null) {
3439
+ successfulDocuments.push(document);
3440
+ }
3441
+ }
3442
+
3443
+ await Promise.all(successfulDocuments.map(document => handleSuccessfulWrite(document)));
3419
3444
 
3420
3445
  if (bulkWriteError && bulkWriteError.writeErrors && bulkWriteError.writeErrors.length) {
3421
3446
  throw bulkWriteError;
package/lib/query.js CHANGED
@@ -2777,7 +2777,7 @@ Query.prototype.estimatedDocumentCount = function(options) {
2777
2777
  this.op = 'estimatedDocumentCount';
2778
2778
  this._validateOp();
2779
2779
 
2780
- if (typeof options === 'object' && options != null) {
2780
+ if (options != null) {
2781
2781
  this.setOptions(options);
2782
2782
  }
2783
2783
 
@@ -2836,7 +2836,7 @@ Query.prototype.countDocuments = function(conditions, options) {
2836
2836
  this.merge(conditions);
2837
2837
  }
2838
2838
 
2839
- if (typeof options === 'object' && options != null) {
2839
+ if (options != null) {
2840
2840
  this.setOptions(options);
2841
2841
  }
2842
2842
 
@@ -2874,21 +2874,24 @@ Query.prototype.__distinct = async function __distinct() {
2874
2874
  *
2875
2875
  * #### Example:
2876
2876
  *
2877
+ * distinct(field, conditions, options)
2877
2878
  * distinct(field, conditions)
2878
2879
  * distinct(field)
2879
2880
  * distinct()
2880
2881
  *
2881
2882
  * @param {String} [field]
2882
2883
  * @param {Object|Query} [filter]
2884
+ * @param {Object} [options]
2883
2885
  * @return {Query} this
2884
2886
  * @see distinct https://www.mongodb.com/docs/manual/reference/method/db.collection.distinct/
2885
2887
  * @api public
2886
2888
  */
2887
2889
 
2888
- Query.prototype.distinct = function(field, conditions) {
2890
+ Query.prototype.distinct = function(field, conditions, options) {
2889
2891
  if (typeof field === 'function' ||
2890
2892
  typeof conditions === 'function' ||
2891
- typeof arguments[2] === 'function') {
2893
+ typeof options === 'function' ||
2894
+ typeof arguments[3] === 'function') {
2892
2895
  throw new MongooseError('Query.prototype.distinct() no longer accepts a callback');
2893
2896
  }
2894
2897
 
@@ -2907,6 +2910,10 @@ Query.prototype.distinct = function(field, conditions) {
2907
2910
  this._distinct = field;
2908
2911
  }
2909
2912
 
2913
+ if (options != null) {
2914
+ this.setOptions(options);
2915
+ }
2916
+
2910
2917
  return this;
2911
2918
  };
2912
2919
 
package/package.json CHANGED
@@ -1,7 +1,7 @@
1
1
  {
2
2
  "name": "mongoose",
3
3
  "description": "Mongoose MongoDB ODM",
4
- "version": "8.5.5",
4
+ "version": "8.6.1",
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.7.0",
24
+ "mongodb": "6.8.0",
25
25
  "mpath": "0.9.0",
26
26
  "mquery": "5.0.0",
27
27
  "ms": "2.1.3",
@@ -29,9 +29,9 @@
29
29
  },
30
30
  "devDependencies": {
31
31
  "@babel/core": "7.24.7",
32
- "@babel/preset-env": "7.25.3",
33
- "@typescript-eslint/eslint-plugin": "^6.21.0",
34
- "@typescript-eslint/parser": "^6.21.0",
32
+ "@babel/preset-env": "7.25.4",
33
+ "@typescript-eslint/eslint-plugin": "^8.4.0",
34
+ "@typescript-eslint/parser": "^8.4.0",
35
35
  "acquit": "1.3.0",
36
36
  "acquit-ignore": "0.2.1",
37
37
  "acquit-require": "0.1.1",
@@ -40,7 +40,7 @@
40
40
  "babel-loader": "8.2.5",
41
41
  "broken-link-checker": "^0.7.8",
42
42
  "buffer": "^5.6.0",
43
- "cheerio": "1.0.0-rc.12",
43
+ "cheerio": "1.0.0",
44
44
  "crypto-browserify": "3.12.0",
45
45
  "dotenv": "16.4.5",
46
46
  "dox": "1.0.0",
@@ -53,9 +53,9 @@
53
53
  "lodash.isequal": "4.5.0",
54
54
  "lodash.isequalwith": "4.4.0",
55
55
  "markdownlint-cli2": "^0.13.0",
56
- "marked": "14.0.0",
56
+ "marked": "14.1.0",
57
57
  "mkdirp": "^3.0.1",
58
- "mocha": "10.7.0",
58
+ "mocha": "10.7.3",
59
59
  "moment": "2.30.1",
60
60
  "mongodb-memory-server": "10.0.0",
61
61
  "ncp": "^2.0.0",
@@ -67,7 +67,7 @@
67
67
  "tsd": "0.31.1",
68
68
  "typescript": "5.5.4",
69
69
  "uuid": "10.0.0",
70
- "webpack": "5.93.0"
70
+ "webpack": "5.94.0"
71
71
  },
72
72
  "directories": {
73
73
  "lib": "./lib/mongoose"
@@ -22,7 +22,7 @@ declare module 'mongoose' {
22
22
  constructor(doc?: any);
23
23
 
24
24
  /** This documents _id. */
25
- _id?: T;
25
+ _id: T;
26
26
 
27
27
  /** This documents __v. */
28
28
  __v?: any;
@@ -259,11 +259,14 @@ declare module 'mongoose' {
259
259
  set(value: string | Record<string, any>): this;
260
260
 
261
261
  /** The return value of this method is used in calls to JSON.stringify(doc). */
262
+ toJSON(options?: ToObjectOptions & { flattenMaps?: true }): FlattenMaps<Require_id<DocType>>;
263
+ toJSON(options: ToObjectOptions & { flattenMaps: false }): Require_id<DocType>;
262
264
  toJSON<T = Require_id<DocType>>(options?: ToObjectOptions & { flattenMaps?: true }): FlattenMaps<T>;
263
265
  toJSON<T = Require_id<DocType>>(options: ToObjectOptions & { flattenMaps: false }): T;
264
266
 
265
267
  /** Converts this document into a plain-old JavaScript object ([POJO](https://masteringjs.io/tutorials/fundamentals/pojo)). */
266
- toObject<T = Require_id<DocType>>(options?: ToObjectOptions): Require_id<T>;
268
+ toObject(options?: ToObjectOptions): Require_id<DocType>;
269
+ toObject<T>(options?: ToObjectOptions): Require_id<T>;
267
270
 
268
271
  /** Clears the modified state on the specified path. */
269
272
  unmarkModified<T extends keyof DocType>(path: T): void;
@@ -1,4 +1,5 @@
1
1
  import {
2
+ IsPathRequired,
2
3
  IsSchemaTypeFromBuiltinClass,
3
4
  RequiredPaths,
4
5
  OptionalPaths,
@@ -14,7 +15,9 @@ declare module 'mongoose' {
14
15
  [
15
16
  K in keyof (RequiredPaths<DocDefinition, TSchemaOptions['typeKey']> &
16
17
  OptionalPaths<DocDefinition, TSchemaOptions['typeKey']>)
17
- ]: ObtainRawDocumentPathType<DocDefinition[K], TSchemaOptions['typeKey']>;
18
+ ]: IsPathRequired<DocDefinition[K], TSchemaOptions['typeKey']> extends true
19
+ ? ObtainRawDocumentPathType<DocDefinition[K], TSchemaOptions['typeKey']>
20
+ : ObtainRawDocumentPathType<DocDefinition[K], TSchemaOptions['typeKey']> | null;
18
21
  }, TSchemaOptions>;
19
22
 
20
23
  /**