Skip to content

Commit 5ad2c33

Browse files
authored
Merge pull request Automattic#15083 from Automattic/8.9
8.9
2 parents 91272fb + 61b670f commit 5ad2c33

35 files changed

+2383
-248
lines changed

docs/guide.md

Lines changed: 2 additions & 0 deletions
Original file line numberDiff line numberDiff line change
@@ -80,6 +80,8 @@ The permitted SchemaTypes are:
8080
* [Decimal128](api/mongoose.html#mongoose_Mongoose-Decimal128)
8181
* [Map](schematypes.html#maps)
8282
* [UUID](schematypes.html#uuid)
83+
* [Double](schematypes.html#double)
84+
* [Int32](schematypes.html#int32)
8385

8486
Read more about [SchemaTypes here](schematypes.html).
8587

docs/schematypes.md

Lines changed: 73 additions & 0 deletions
Original file line numberDiff line numberDiff line change
@@ -55,6 +55,8 @@ Check out [Mongoose's plugins search](http://plugins.mongoosejs.io) to find plug
5555
* [Schema](#schemas)
5656
* [UUID](#uuid)
5757
* [BigInt](#bigint)
58+
* [Double](#double)
59+
* [Int32](#int32)
5860

5961
### Example
6062

@@ -68,6 +70,8 @@ const schema = new Schema({
6870
mixed: Schema.Types.Mixed,
6971
_someId: Schema.Types.ObjectId,
7072
decimal: Schema.Types.Decimal128,
73+
double: Schema.Types.Double,
74+
int32bit: Schema.Types.Int32,
7175
array: [],
7276
ofString: [String],
7377
ofNumber: [Number],
@@ -647,6 +651,75 @@ const question = new Question({ answer: 42n });
647651
typeof question.answer; // 'bigint'
648652
```
649653

654+
### Double {#double}
655+
656+
Mongoose supports [64-bit IEEE 754-2008 floating point numbers](https://en.wikipedia.org/wiki/IEEE_754-2008_revision) as a SchemaType.
657+
Int32s are stored as [BSON type "double" in MongoDB](https://www.mongodb.com/docs/manual/reference/bson-types/).
658+
659+
```javascript
660+
const studentsSchema = new Schema({
661+
id: Int32
662+
});
663+
const Student = mongoose.model('Student', schema);
664+
665+
const student = new Temperature({ celsius: 1339 });
666+
typeof student.id; // 'number'
667+
```
668+
669+
There are several types of values that will be successfully cast to a Double.
670+
671+
```javascript
672+
new Temperature({ celsius: '1.2e12' }).celsius; // 15 as a Double
673+
new Temperature({ celsius: true }).celsius; // 1 as a Double
674+
new Temperature({ celsius: false }).celsius; // 0 as a Double
675+
new Temperature({ celsius: { valueOf: () => 83.0033 } }).celsius; // 83 as a Double
676+
new Temperature({ celsius: '' }).celsius; // null as a Double
677+
```
678+
679+
The following inputs will result will all result in a [CastError](validation.html#cast-errors) once validated, meaning that it will not throw on initialization, only when validated:
680+
681+
* strings that do not represent a numeric string, a NaN or a null-ish value
682+
* objects that don't have a `valueOf()` function
683+
* an input that represents a value outside the bounds of a IEEE 754-2008 floating point
684+
685+
### Int32 {#int32}
686+
687+
Mongoose supports 32-bit integers as a SchemaType.
688+
Int32s are stored as [32-bit integers in MongoDB (BSON type "int")](https://www.mongodb.com/docs/manual/reference/bson-types/).
689+
690+
```javascript
691+
const studentsSchema = new Schema({
692+
id: Int32
693+
});
694+
const Student = mongoose.model('Student', schema);
695+
696+
const student = new Temperature({ celsius: 1339 });
697+
typeof student.id; // 'number'
698+
```
699+
700+
There are several types of values that will be successfully cast to a Number.
701+
702+
```javascript
703+
new Student({ id: '15' }).id; // 15 as a Int32
704+
new Student({ id: true }).id; // 1 as a Int32
705+
new Student({ id: false }).id; // 0 as a Int32
706+
new Student({ id: { valueOf: () => 83 } }).id; // 83 as a Int32
707+
new Student({ id: '' }).id; // null as a Int32
708+
```
709+
710+
If you pass an object with a `valueOf()` function that returns a Number, Mongoose will
711+
call it and assign the returned value to the path.
712+
713+
The values `null` and `undefined` are not cast.
714+
715+
The following inputs will result will all result in a [CastError](validation.html#cast-errors) once validated, meaning that it will not throw on initialization, only when validated:
716+
717+
* NaN
718+
* strings that cast to NaN
719+
* objects that don't have a `valueOf()` function
720+
* a decimal that must be rounded to be an integer
721+
* an input that represents a value outside the bounds of an 32-bit integer
722+
650723
## Getters {#getters}
651724

652725
Getters are like virtuals for paths defined in your schema. For example,

index.js

Lines changed: 1 addition & 0 deletions
Original file line numberDiff line numberDiff line change
@@ -46,6 +46,7 @@ module.exports.Decimal128 = mongoose.Decimal128;
4646
module.exports.Mixed = mongoose.Mixed;
4747
module.exports.Date = mongoose.Date;
4848
module.exports.Number = mongoose.Number;
49+
module.exports.Double = mongoose.Double;
4950
module.exports.Error = mongoose.Error;
5051
module.exports.MongooseError = mongoose.MongooseError;
5152
module.exports.now = mongoose.now;

lib/cast/double.js

Lines changed: 50 additions & 0 deletions
Original file line numberDiff line numberDiff line change
@@ -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+
};

lib/cast/int32.js

Lines changed: 36 additions & 0 deletions
Original file line numberDiff line numberDiff line change
@@ -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+
};

lib/connection.js

Lines changed: 177 additions & 0 deletions
Original file line numberDiff line numberDiff line change
@@ -8,16 +8,21 @@ const ChangeStream = require('./cursor/changeStream');
88
const EventEmitter = require('events').EventEmitter;
99
const Schema = require('./schema');
1010
const STATES = require('./connectionState');
11+
const MongooseBulkWriteError = require('./error/bulkWriteError');
1112
const MongooseError = require('./error/index');
1213
const ServerSelectionError = require('./error/serverSelection');
1314
const SyncIndexesError = require('./error/syncIndexes');
1415
const applyPlugins = require('./helpers/schema/applyPlugins');
1516
const clone = require('./helpers/clone');
1617
const driver = require('./driver');
1718
const get = require('./helpers/get');
19+
const getDefaultBulkwriteResult = require('./helpers/getDefaultBulkwriteResult');
1820
const immediate = require('./helpers/immediate');
1921
const utils = require('./utils');
2022
const CreateCollectionsError = require('./error/createCollectionsError');
23+
const castBulkWrite = require('./helpers/model/castBulkWrite');
24+
const { modelSymbol } = require('./helpers/symbols');
25+
const isPromise = require('./helpers/isPromise');
2126

2227
const arrayAtomicsSymbol = require('./helpers/symbols').arrayAtomicsSymbol;
2328
const sessionNewDocuments = require('./helpers/symbols').sessionNewDocuments;
@@ -419,6 +424,178 @@ Connection.prototype.createCollection = async function createCollection(collecti
419424
return this.db.createCollection(collection, options);
420425
};
421426

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+
422599
/**
423600
* Calls `createCollection()` on a models in a series.
424601
*

0 commit comments

Comments
 (0)