Skip to content

Commit

Permalink
Merge pull request #15083 from Automattic/8.9
Browse files Browse the repository at this point in the history
8.9
  • Loading branch information
vkarpov15 authored Dec 13, 2024
2 parents 91272fb + 61b670f commit 5ad2c33
Show file tree
Hide file tree
Showing 35 changed files with 2,383 additions and 248 deletions.
2 changes: 2 additions & 0 deletions docs/guide.md
Original file line number Diff line number Diff line change
Expand Up @@ -80,6 +80,8 @@ The permitted SchemaTypes are:
* [Decimal128](api/mongoose.html#mongoose_Mongoose-Decimal128)
* [Map](schematypes.html#maps)
* [UUID](schematypes.html#uuid)
* [Double](schematypes.html#double)
* [Int32](schematypes.html#int32)

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

Expand Down
73 changes: 73 additions & 0 deletions docs/schematypes.md
Original file line number Diff line number Diff line change
Expand Up @@ -55,6 +55,8 @@ Check out [Mongoose's plugins search](http://plugins.mongoosejs.io) to find plug
* [Schema](#schemas)
* [UUID](#uuid)
* [BigInt](#bigint)
* [Double](#double)
* [Int32](#int32)

### Example

Expand All @@ -68,6 +70,8 @@ const schema = new Schema({
mixed: Schema.Types.Mixed,
_someId: Schema.Types.ObjectId,
decimal: Schema.Types.Decimal128,
double: Schema.Types.Double,
int32bit: Schema.Types.Int32,
array: [],
ofString: [String],
ofNumber: [Number],
Expand Down Expand Up @@ -647,6 +651,75 @@ const question = new Question({ answer: 42n });
typeof question.answer; // 'bigint'
```

### Double {#double}

Mongoose supports [64-bit IEEE 754-2008 floating point numbers](https://en.wikipedia.org/wiki/IEEE_754-2008_revision) as a SchemaType.
Int32s are stored as [BSON type "double" in MongoDB](https://www.mongodb.com/docs/manual/reference/bson-types/).

```javascript
const studentsSchema = new Schema({
id: Int32
});
const Student = mongoose.model('Student', schema);

const student = new Temperature({ celsius: 1339 });
typeof student.id; // 'number'
```

There are several types of values that will be successfully cast to a Double.

```javascript
new Temperature({ celsius: '1.2e12' }).celsius; // 15 as a Double
new Temperature({ celsius: true }).celsius; // 1 as a Double
new Temperature({ celsius: false }).celsius; // 0 as a Double
new Temperature({ celsius: { valueOf: () => 83.0033 } }).celsius; // 83 as a Double
new Temperature({ celsius: '' }).celsius; // null as a Double
```

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:

* strings that do not represent a numeric string, a NaN or a null-ish value
* objects that don't have a `valueOf()` function
* an input that represents a value outside the bounds of a IEEE 754-2008 floating point

### Int32 {#int32}

Mongoose supports 32-bit integers as a SchemaType.
Int32s are stored as [32-bit integers in MongoDB (BSON type "int")](https://www.mongodb.com/docs/manual/reference/bson-types/).

```javascript
const studentsSchema = new Schema({
id: Int32
});
const Student = mongoose.model('Student', schema);

const student = new Temperature({ celsius: 1339 });
typeof student.id; // 'number'
```

There are several types of values that will be successfully cast to a Number.

```javascript
new Student({ id: '15' }).id; // 15 as a Int32
new Student({ id: true }).id; // 1 as a Int32
new Student({ id: false }).id; // 0 as a Int32
new Student({ id: { valueOf: () => 83 } }).id; // 83 as a Int32
new Student({ id: '' }).id; // null as a Int32
```

If you pass an object with a `valueOf()` function that returns a Number, Mongoose will
call it and assign the returned value to the path.

The values `null` and `undefined` are not cast.

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:

* NaN
* strings that cast to NaN
* objects that don't have a `valueOf()` function
* a decimal that must be rounded to be an integer
* an input that represents a value outside the bounds of an 32-bit integer

## Getters {#getters}

Getters are like virtuals for paths defined in your schema. For example,
Expand Down
1 change: 1 addition & 0 deletions index.js
Original file line number Diff line number Diff line change
Expand Up @@ -46,6 +46,7 @@ module.exports.Decimal128 = mongoose.Decimal128;
module.exports.Mixed = mongoose.Mixed;
module.exports.Date = mongoose.Date;
module.exports.Number = mongoose.Number;
module.exports.Double = mongoose.Double;
module.exports.Error = mongoose.Error;
module.exports.MongooseError = mongoose.MongooseError;
module.exports.now = mongoose.now;
Expand Down
50 changes: 50 additions & 0 deletions lib/cast/double.js
Original file line number Diff line number Diff line change
@@ -0,0 +1,50 @@
'use strict';

const assert = require('assert');
const BSON = require('bson');
const isBsonType = require('../helpers/isBsonType');

/**
* Given a value, cast it to a IEEE 754-2008 floating point, or throw an `Error` if the value
* cannot be casted. `null`, `undefined`, and `NaN` are considered valid inputs.
*
* @param {Any} value
* @return {Number}
* @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)
* @api private
*/

module.exports = function castDouble(val) {
if (val == null || val === '') {
return null;
}

let coercedVal;
if (isBsonType(val, 'Long')) {
coercedVal = val.toNumber();
} else if (typeof val === 'string') {
try {
coercedVal = BSON.Double.fromString(val);
return coercedVal;
} catch {
assert.ok(false);
}
} else if (typeof val === 'object') {
const tempVal = val.valueOf() ?? val.toString();
// ex: { a: 'im an object, valueOf: () => 'helloworld' } // throw an error
if (typeof tempVal === 'string') {
try {
coercedVal = BSON.Double.fromString(val);
return coercedVal;
} catch {
assert.ok(false);
}
} else {
coercedVal = Number(tempVal);
}
} else {
coercedVal = Number(val);
}

return new BSON.Double(coercedVal);
};
36 changes: 36 additions & 0 deletions lib/cast/int32.js
Original file line number Diff line number Diff line change
@@ -0,0 +1,36 @@
'use strict';

const isBsonType = require('../helpers/isBsonType');
const assert = require('assert');

/**
* Given a value, cast it to a Int32, or throw an `Error` if the value
* cannot be casted. `null` and `undefined` are considered valid.
*
* @param {Any} value
* @return {Number}
* @throws {Error} if `value` does not represent an integer, or is outside the bounds of an 32-bit integer.
* @api private
*/

module.exports = function castInt32(val) {
if (val == null) {
return val;
}
if (val === '') {
return null;
}

const coercedVal = isBsonType(val, 'Long') ? val.toNumber() : Number(val);

const INT32_MAX = 0x7FFFFFFF;
const INT32_MIN = -0x80000000;

if (coercedVal === (coercedVal | 0) &&
coercedVal >= INT32_MIN &&
coercedVal <= INT32_MAX
) {
return coercedVal;
}
assert.ok(false);
};
177 changes: 177 additions & 0 deletions lib/connection.js
Original file line number Diff line number Diff line change
Expand Up @@ -8,16 +8,21 @@ const ChangeStream = require('./cursor/changeStream');
const EventEmitter = require('events').EventEmitter;
const Schema = require('./schema');
const STATES = require('./connectionState');
const MongooseBulkWriteError = require('./error/bulkWriteError');
const MongooseError = require('./error/index');
const ServerSelectionError = require('./error/serverSelection');
const SyncIndexesError = require('./error/syncIndexes');
const applyPlugins = require('./helpers/schema/applyPlugins');
const clone = require('./helpers/clone');
const driver = require('./driver');
const get = require('./helpers/get');
const getDefaultBulkwriteResult = require('./helpers/getDefaultBulkwriteResult');
const immediate = require('./helpers/immediate');
const utils = require('./utils');
const CreateCollectionsError = require('./error/createCollectionsError');
const castBulkWrite = require('./helpers/model/castBulkWrite');
const { modelSymbol } = require('./helpers/symbols');
const isPromise = require('./helpers/isPromise');

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

/**
* _Requires MongoDB Server 8.0 or greater_. Executes bulk write operations across multiple models in a single operation.
* You must specify the `model` for each operation: Mongoose will use `model` for casting and validation, as well as
* determining which collection to apply the operation to.
*
* #### Example:
* const Test = mongoose.model('Test', new Schema({ name: String }));
*
* await db.bulkWrite([
* { model: Test, name: 'insertOne', document: { name: 'test1' } }, // Can specify model as a Model class...
* { model: 'Test', name: 'insertOne', document: { name: 'test2' } } // or as a model name
* ], { ordered: false });
*
* @method bulkWrite
* @param {Array} ops
* @param {Object} [options]
* @param {Boolean} [options.ordered] If false, perform unordered operations. If true, perform ordered operations.
* @param {Session} [options.session] The session to use for the operation.
* @return {Promise}
* @see MongoDB https://www.mongodb.com/docs/manual/reference/command/bulkWrite/#mongodb-dbcommand-dbcmd.bulkWrite
* @api public
*/


Connection.prototype.bulkWrite = async function bulkWrite(ops, options) {
await this._waitForConnect();
options = options || {};

const ordered = options.ordered == null ? true : options.ordered;
const asyncLocalStorage = this.base.transactionAsyncLocalStorage?.getStore();
if ((!options || !options.hasOwnProperty('session')) && asyncLocalStorage?.session != null) {
options = { ...options, session: asyncLocalStorage.session };
}

const now = this.base.now();

let res = null;
if (ordered) {
const opsToSend = [];
for (const op of ops) {
if (typeof op.model !== 'string' && !op.model?.[modelSymbol]) {
throw new MongooseError('Must specify model in Connection.prototype.bulkWrite() operations');
}
const Model = op.model[modelSymbol] ? op.model : this.model(op.model);

if (op.name == null) {
throw new MongooseError('Must specify operation name in Connection.prototype.bulkWrite()');
}
if (!castBulkWrite.cast.hasOwnProperty(op.name)) {
throw new MongooseError(`Unrecognized bulkWrite() operation name ${op.name}`);
}

await castBulkWrite.cast[op.name](Model, op, options, now);
opsToSend.push({ ...op, namespace: Model.namespace() });
}

res = await this.client.bulkWrite(opsToSend, options);
} else {
const validOps = [];
const validOpIndexes = [];
let validationErrors = [];
const asyncValidations = [];
const results = [];
for (let i = 0; i < ops.length; ++i) {
const op = ops[i];
if (typeof op.model !== 'string' && !op.model?.[modelSymbol]) {
const error = new MongooseError('Must specify model in Connection.prototype.bulkWrite() operations');
validationErrors.push({ index: i, error: error });
results[i] = error;
continue;
}
let Model;
try {
Model = op.model[modelSymbol] ? op.model : this.model(op.model);
} catch (error) {
validationErrors.push({ index: i, error: error });
continue;
}
if (op.name == null) {
const error = new MongooseError('Must specify operation name in Connection.prototype.bulkWrite()');
validationErrors.push({ index: i, error: error });
results[i] = error;
continue;
}
if (!castBulkWrite.cast.hasOwnProperty(op.name)) {
const error = new MongooseError(`Unrecognized bulkWrite() operation name ${op.name}`);
validationErrors.push({ index: i, error: error });
results[i] = error;
continue;
}

let maybePromise = null;
try {
maybePromise = castBulkWrite.cast[op.name](Model, op, options, now);
} catch (error) {
validationErrors.push({ index: i, error: error });
results[i] = error;
continue;
}
if (isPromise(maybePromise)) {
asyncValidations.push(
maybePromise.then(
() => {
validOps.push({ ...op, namespace: Model.namespace() });
validOpIndexes.push(i);
},
error => {
validationErrors.push({ index: i, error: error });
results[i] = error;
}
)
);
} else {
validOps.push({ ...op, namespace: Model.namespace() });
validOpIndexes.push(i);
}
}

if (asyncValidations.length > 0) {
await Promise.all(asyncValidations);
}

validationErrors = validationErrors.
sort((v1, v2) => v1.index - v2.index).
map(v => v.error);

if (validOps.length === 0) {
if (options.throwOnValidationError && validationErrors.length) {
throw new MongooseBulkWriteError(
validationErrors,
results,
res,
'bulkWrite'
);
}
return getDefaultBulkwriteResult();
}

let error;
[res, error] = await this.client.bulkWrite(validOps, options).
then(res => ([res, null])).
catch(err => ([null, err]));

if (error) {
if (validationErrors.length > 0) {
error.mongoose = error.mongoose || {};
error.mongoose.validationErrors = validationErrors;
}
}

for (let i = 0; i < validOpIndexes.length; ++i) {
results[validOpIndexes[i]] = null;
}
if (validationErrors.length > 0) {
if (options.throwOnValidationError) {
throw new MongooseBulkWriteError(
validationErrors,
results,
res,
'bulkWrite'
);
} else {
res.mongoose = res.mongoose || {};
res.mongoose.validationErrors = validationErrors;
res.mongoose.results = results;
}
}
}

return res;
};

/**
* Calls `createCollection()` on a models in a series.
*
Expand Down
Loading

0 comments on commit 5ad2c33

Please sign in to comment.