mongoose 8.8.3 → 8.9.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/index.js CHANGED
@@ -46,6 +46,7 @@ module.exports.Decimal128 = mongoose.Decimal128;
46
46
  module.exports.Mixed = mongoose.Mixed;
47
47
  module.exports.Date = mongoose.Date;
48
48
  module.exports.Number = mongoose.Number;
49
+ module.exports.Double = mongoose.Double;
49
50
  module.exports.Error = mongoose.Error;
50
51
  module.exports.MongooseError = mongoose.MongooseError;
51
52
  module.exports.now = mongoose.now;
@@ -0,0 +1,50 @@
1
+ 'use strict';
2
+
3
+ const assert = require('assert');
4
+ const BSON = require('bson');
5
+ const isBsonType = require('../helpers/isBsonType');
6
+
7
+ /**
8
+ * Given a value, cast it to a IEEE 754-2008 floating point, or throw an `Error` if the value
9
+ * cannot be casted. `null`, `undefined`, and `NaN` are considered valid inputs.
10
+ *
11
+ * @param {Any} value
12
+ * @return {Number}
13
+ * @throws {Error} if `value` does not represent a IEEE 754-2008 floating point. If casting from a string, see [BSON Double.fromString API documentation](https://mongodb.github.io/node-mongodb-native/Next/classes/BSON.Double.html#fromString)
14
+ * @api private
15
+ */
16
+
17
+ module.exports = function castDouble(val) {
18
+ if (val == null || val === '') {
19
+ return null;
20
+ }
21
+
22
+ let coercedVal;
23
+ if (isBsonType(val, 'Long')) {
24
+ coercedVal = val.toNumber();
25
+ } else if (typeof val === 'string') {
26
+ try {
27
+ coercedVal = BSON.Double.fromString(val);
28
+ return coercedVal;
29
+ } catch {
30
+ assert.ok(false);
31
+ }
32
+ } else if (typeof val === 'object') {
33
+ const tempVal = val.valueOf() ?? val.toString();
34
+ // ex: { a: 'im an object, valueOf: () => 'helloworld' } // throw an error
35
+ if (typeof tempVal === 'string') {
36
+ try {
37
+ coercedVal = BSON.Double.fromString(val);
38
+ return coercedVal;
39
+ } catch {
40
+ assert.ok(false);
41
+ }
42
+ } else {
43
+ coercedVal = Number(tempVal);
44
+ }
45
+ } else {
46
+ coercedVal = Number(val);
47
+ }
48
+
49
+ return new BSON.Double(coercedVal);
50
+ };
@@ -0,0 +1,36 @@
1
+ 'use strict';
2
+
3
+ const isBsonType = require('../helpers/isBsonType');
4
+ const assert = require('assert');
5
+
6
+ /**
7
+ * Given a value, cast it to a Int32, or throw an `Error` if the value
8
+ * cannot be casted. `null` and `undefined` are considered valid.
9
+ *
10
+ * @param {Any} value
11
+ * @return {Number}
12
+ * @throws {Error} if `value` does not represent an integer, or is outside the bounds of an 32-bit integer.
13
+ * @api private
14
+ */
15
+
16
+ module.exports = function castInt32(val) {
17
+ if (val == null) {
18
+ return val;
19
+ }
20
+ if (val === '') {
21
+ return null;
22
+ }
23
+
24
+ const coercedVal = isBsonType(val, 'Long') ? val.toNumber() : Number(val);
25
+
26
+ const INT32_MAX = 0x7FFFFFFF;
27
+ const INT32_MIN = -0x80000000;
28
+
29
+ if (coercedVal === (coercedVal | 0) &&
30
+ coercedVal >= INT32_MIN &&
31
+ coercedVal <= INT32_MAX
32
+ ) {
33
+ return coercedVal;
34
+ }
35
+ assert.ok(false);
36
+ };
package/lib/connection.js CHANGED
@@ -8,6 +8,7 @@ const ChangeStream = require('./cursor/changeStream');
8
8
  const EventEmitter = require('events').EventEmitter;
9
9
  const Schema = require('./schema');
10
10
  const STATES = require('./connectionState');
11
+ const MongooseBulkWriteError = require('./error/bulkWriteError');
11
12
  const MongooseError = require('./error/index');
12
13
  const ServerSelectionError = require('./error/serverSelection');
13
14
  const SyncIndexesError = require('./error/syncIndexes');
@@ -15,9 +16,13 @@ const applyPlugins = require('./helpers/schema/applyPlugins');
15
16
  const clone = require('./helpers/clone');
16
17
  const driver = require('./driver');
17
18
  const get = require('./helpers/get');
19
+ const getDefaultBulkwriteResult = require('./helpers/getDefaultBulkwriteResult');
18
20
  const immediate = require('./helpers/immediate');
19
21
  const utils = require('./utils');
20
22
  const CreateCollectionsError = require('./error/createCollectionsError');
23
+ const castBulkWrite = require('./helpers/model/castBulkWrite');
24
+ const { modelSymbol } = require('./helpers/symbols');
25
+ const isPromise = require('./helpers/isPromise');
21
26
 
22
27
  const arrayAtomicsSymbol = require('./helpers/symbols').arrayAtomicsSymbol;
23
28
  const sessionNewDocuments = require('./helpers/symbols').sessionNewDocuments;
@@ -111,6 +116,9 @@ Object.defineProperty(Connection.prototype, 'readyState', {
111
116
  if (
112
117
  this._readyState === STATES.connected &&
113
118
  this._lastHeartbeatAt != null &&
119
+ // LoadBalanced topology (behind haproxy, including Atlas serverless instances) don't use heartbeats,
120
+ // so we can't use this check in that case.
121
+ this.client?.topology?.s?.description?.type !== 'LoadBalanced' &&
114
122
  typeof this.client?.topology?.s?.description?.heartbeatFrequencyMS === 'number' &&
115
123
  Date.now() - this._lastHeartbeatAt >= this.client.topology.s.description.heartbeatFrequencyMS * 2) {
116
124
  return STATES.disconnected;
@@ -416,6 +424,178 @@ Connection.prototype.createCollection = async function createCollection(collecti
416
424
  return this.db.createCollection(collection, options);
417
425
  };
418
426
 
427
+ /**
428
+ * _Requires MongoDB Server 8.0 or greater_. Executes bulk write operations across multiple models in a single operation.
429
+ * You must specify the `model` for each operation: Mongoose will use `model` for casting and validation, as well as
430
+ * determining which collection to apply the operation to.
431
+ *
432
+ * #### Example:
433
+ * const Test = mongoose.model('Test', new Schema({ name: String }));
434
+ *
435
+ * await db.bulkWrite([
436
+ * { model: Test, name: 'insertOne', document: { name: 'test1' } }, // Can specify model as a Model class...
437
+ * { model: 'Test', name: 'insertOne', document: { name: 'test2' } } // or as a model name
438
+ * ], { ordered: false });
439
+ *
440
+ * @method bulkWrite
441
+ * @param {Array} ops
442
+ * @param {Object} [options]
443
+ * @param {Boolean} [options.ordered] If false, perform unordered operations. If true, perform ordered operations.
444
+ * @param {Session} [options.session] The session to use for the operation.
445
+ * @return {Promise}
446
+ * @see MongoDB https://www.mongodb.com/docs/manual/reference/command/bulkWrite/#mongodb-dbcommand-dbcmd.bulkWrite
447
+ * @api public
448
+ */
449
+
450
+
451
+ Connection.prototype.bulkWrite = async function bulkWrite(ops, options) {
452
+ await this._waitForConnect();
453
+ options = options || {};
454
+
455
+ const ordered = options.ordered == null ? true : options.ordered;
456
+ const asyncLocalStorage = this.base.transactionAsyncLocalStorage?.getStore();
457
+ if ((!options || !options.hasOwnProperty('session')) && asyncLocalStorage?.session != null) {
458
+ options = { ...options, session: asyncLocalStorage.session };
459
+ }
460
+
461
+ const now = this.base.now();
462
+
463
+ let res = null;
464
+ if (ordered) {
465
+ const opsToSend = [];
466
+ for (const op of ops) {
467
+ if (typeof op.model !== 'string' && !op.model?.[modelSymbol]) {
468
+ throw new MongooseError('Must specify model in Connection.prototype.bulkWrite() operations');
469
+ }
470
+ const Model = op.model[modelSymbol] ? op.model : this.model(op.model);
471
+
472
+ if (op.name == null) {
473
+ throw new MongooseError('Must specify operation name in Connection.prototype.bulkWrite()');
474
+ }
475
+ if (!castBulkWrite.cast.hasOwnProperty(op.name)) {
476
+ throw new MongooseError(`Unrecognized bulkWrite() operation name ${op.name}`);
477
+ }
478
+
479
+ await castBulkWrite.cast[op.name](Model, op, options, now);
480
+ opsToSend.push({ ...op, namespace: Model.namespace() });
481
+ }
482
+
483
+ res = await this.client.bulkWrite(opsToSend, options);
484
+ } else {
485
+ const validOps = [];
486
+ const validOpIndexes = [];
487
+ let validationErrors = [];
488
+ const asyncValidations = [];
489
+ const results = [];
490
+ for (let i = 0; i < ops.length; ++i) {
491
+ const op = ops[i];
492
+ if (typeof op.model !== 'string' && !op.model?.[modelSymbol]) {
493
+ const error = new MongooseError('Must specify model in Connection.prototype.bulkWrite() operations');
494
+ validationErrors.push({ index: i, error: error });
495
+ results[i] = error;
496
+ continue;
497
+ }
498
+ let Model;
499
+ try {
500
+ Model = op.model[modelSymbol] ? op.model : this.model(op.model);
501
+ } catch (error) {
502
+ validationErrors.push({ index: i, error: error });
503
+ continue;
504
+ }
505
+ if (op.name == null) {
506
+ const error = new MongooseError('Must specify operation name in Connection.prototype.bulkWrite()');
507
+ validationErrors.push({ index: i, error: error });
508
+ results[i] = error;
509
+ continue;
510
+ }
511
+ if (!castBulkWrite.cast.hasOwnProperty(op.name)) {
512
+ const error = new MongooseError(`Unrecognized bulkWrite() operation name ${op.name}`);
513
+ validationErrors.push({ index: i, error: error });
514
+ results[i] = error;
515
+ continue;
516
+ }
517
+
518
+ let maybePromise = null;
519
+ try {
520
+ maybePromise = castBulkWrite.cast[op.name](Model, op, options, now);
521
+ } catch (error) {
522
+ validationErrors.push({ index: i, error: error });
523
+ results[i] = error;
524
+ continue;
525
+ }
526
+ if (isPromise(maybePromise)) {
527
+ asyncValidations.push(
528
+ maybePromise.then(
529
+ () => {
530
+ validOps.push({ ...op, namespace: Model.namespace() });
531
+ validOpIndexes.push(i);
532
+ },
533
+ error => {
534
+ validationErrors.push({ index: i, error: error });
535
+ results[i] = error;
536
+ }
537
+ )
538
+ );
539
+ } else {
540
+ validOps.push({ ...op, namespace: Model.namespace() });
541
+ validOpIndexes.push(i);
542
+ }
543
+ }
544
+
545
+ if (asyncValidations.length > 0) {
546
+ await Promise.all(asyncValidations);
547
+ }
548
+
549
+ validationErrors = validationErrors.
550
+ sort((v1, v2) => v1.index - v2.index).
551
+ map(v => v.error);
552
+
553
+ if (validOps.length === 0) {
554
+ if (options.throwOnValidationError && validationErrors.length) {
555
+ throw new MongooseBulkWriteError(
556
+ validationErrors,
557
+ results,
558
+ res,
559
+ 'bulkWrite'
560
+ );
561
+ }
562
+ return getDefaultBulkwriteResult();
563
+ }
564
+
565
+ let error;
566
+ [res, error] = await this.client.bulkWrite(validOps, options).
567
+ then(res => ([res, null])).
568
+ catch(err => ([null, err]));
569
+
570
+ if (error) {
571
+ if (validationErrors.length > 0) {
572
+ error.mongoose = error.mongoose || {};
573
+ error.mongoose.validationErrors = validationErrors;
574
+ }
575
+ }
576
+
577
+ for (let i = 0; i < validOpIndexes.length; ++i) {
578
+ results[validOpIndexes[i]] = null;
579
+ }
580
+ if (validationErrors.length > 0) {
581
+ if (options.throwOnValidationError) {
582
+ throw new MongooseBulkWriteError(
583
+ validationErrors,
584
+ results,
585
+ res,
586
+ 'bulkWrite'
587
+ );
588
+ } else {
589
+ res.mongoose = res.mongoose || {};
590
+ res.mongoose.validationErrors = validationErrors;
591
+ res.mongoose.results = results;
592
+ }
593
+ }
594
+ }
595
+
596
+ return res;
597
+ };
598
+
419
599
  /**
420
600
  * Calls `createCollection()` on a models in a series.
421
601
  *
package/lib/document.js CHANGED
@@ -624,16 +624,17 @@ Document.prototype.toBSON = function() {
624
624
  };
625
625
 
626
626
  /**
627
- * Initializes the document without setters or marking anything modified.
627
+ * Hydrates this document with the data in `doc`. Does not run setters or mark any paths modified.
628
628
  *
629
- * Called internally after a document is returned from mongodb. Normally,
629
+ * Called internally after a document is returned from MongoDB. Normally,
630
630
  * you do **not** need to call this function on your own.
631
631
  *
632
632
  * This function triggers `init` [middleware](https://mongoosejs.com/docs/middleware.html).
633
633
  * Note that `init` hooks are [synchronous](https://mongoosejs.com/docs/middleware.html#synchronous).
634
634
  *
635
- * @param {Object} doc document returned by mongo
635
+ * @param {Object} doc raw document returned by mongo
636
636
  * @param {Object} [opts]
637
+ * @param {Boolean} [opts.hydratedPopulatedDocs=false] If true, hydrate and mark as populated any paths that are populated in the raw document
637
638
  * @param {Function} [fn]
638
639
  * @api public
639
640
  * @memberOf Document
@@ -805,6 +806,14 @@ function init(self, obj, doc, opts, prefix) {
805
806
  reason: e
806
807
  }));
807
808
  }
809
+ } else if (opts.hydratedPopulatedDocs) {
810
+ doc[i] = schemaType.cast(value, self, true);
811
+
812
+ if (doc[i] && doc[i].$__ && doc[i].$__.wasPopulated) {
813
+ self.$populated(path, doc[i].$__.wasPopulated.value, doc[i].$__.wasPopulated.options);
814
+ } else if (Array.isArray(doc[i]) && doc[i].length && doc[i][0]?.$__?.wasPopulated) {
815
+ self.$populated(path, doc[i].map(populatedDoc => populatedDoc?.$__?.wasPopulated?.value).filter(val => val != null), doc[i][0].$__.wasPopulated.options);
816
+ }
808
817
  } else {
809
818
  doc[i] = value;
810
819
  }
@@ -426,9 +426,6 @@ function _setClient(conn, client, options, dbName) {
426
426
  }
427
427
 
428
428
  conn.onOpen();
429
- if (client.topology?.s?.state === 'connected') {
430
- conn._lastHeartbeatAt = Date.now();
431
- }
432
429
 
433
430
  for (const i in conn.collections) {
434
431
  if (utils.object.hasOwnProperty(conn.collections, i)) {
@@ -437,7 +434,6 @@ function _setClient(conn, client, options, dbName) {
437
434
  }
438
435
  }
439
436
 
440
-
441
437
  /*!
442
438
  * Module exports.
443
439
  */
@@ -11,6 +11,7 @@ const isObject = require('./isObject');
11
11
  const isPOJO = require('./isPOJO');
12
12
  const symbols = require('./symbols');
13
13
  const trustedSymbol = require('./query/trusted').trustedSymbol;
14
+ const BSON = require('bson');
14
15
 
15
16
  /**
16
17
  * Object clone with Mongoose natives support.
@@ -30,6 +31,10 @@ function clone(obj, options, isArrayChild) {
30
31
  if (obj == null) {
31
32
  return obj;
32
33
  }
34
+
35
+ if (isBsonType(obj, 'Double')) {
36
+ return new BSON.Double(obj.value);
37
+ }
33
38
  if (typeof obj === 'number' || typeof obj === 'string' || typeof obj === 'boolean' || typeof obj === 'bigint') {
34
39
  return obj;
35
40
  }