Skip to content
Closed
Show file tree
Hide file tree
Changes from 1 commit
Commits
File filter

Filter by extension

Filter by extension

Conversations
Failed to load comments.
Loading
Jump to
Jump to file
Failed to load files.
Loading
Diff view
Diff view
23 changes: 23 additions & 0 deletions docs/schematypes.md
Original file line number Diff line number Diff line change
Expand Up @@ -664,6 +664,29 @@ const student = new Student({ id: 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 beyond the bounds of an 32-bit integer


## Getters {#getters}

Getters are like virtuals for paths defined in your schema. For example,
Expand Down
26 changes: 17 additions & 9 deletions lib/cast/int32.js
Original file line number Diff line number Diff line change
@@ -1,6 +1,7 @@
'use strict';

const assert = require('assert');
const BSON = require('bson');

/**
* Given a value, cast it to a Int32, or throw an `Error` if the value
Expand All @@ -20,16 +21,23 @@ module.exports = function castInt32(val) {
return null;
}

if (typeof val === 'string' || typeof val === 'number') {

const INT32_MAX = 0x7FFFFFFF;
const INT32_MIN = -0x80000000;
let coercedVal;
if (val instanceof BSON.Int32 || val instanceof BSON.Double) {

Choose a reason for hiding this comment

The reason will be displayed to describe this comment to others. Learn more.

we check for _bsontype because that lines up with how the serializer interprets our BSON types. instanceof is a different contract than our code expects so I think it shouldn't be introduced here so we don't create two versions of what is a true "type check"

Copy link
Member

Choose a reason for hiding this comment

The reason will be displayed to describe this comment to others. Learn more.

I agree with using _bsontype rather than instanceof. Mongoose has an isBsonType() helper for this purpose.

The reason why we use _bsontype over instanceof is that it is common for Node.js apps to have multiple versions of bson floating around, and relying on instanceof can lead to hard-to-debug behavior when we get an Int32 instance from a different version of bson module

Choose a reason for hiding this comment

The reason will be displayed to describe this comment to others. Learn more.

made the change!

coercedVal = val.value;
} else if (val instanceof BSON.Long) {
coercedVal = val.toNumber();
} else {
coercedVal = Number(val);
}

const _val = Number(val);
const INT32_MAX = 0x7FFFFFFF;
const INT32_MIN = -0x80000000;

if (_val === (_val | 0) && _val > INT32_MIN && _val < INT32_MAX) {
return _val;
}
assert.ok(false);
if (coercedVal === (coercedVal | 0) &&
coercedVal >= INT32_MIN &&
coercedVal <= INT32_MAX
) {
return coercedVal;
}
assert.ok(false);
};
2 changes: 1 addition & 1 deletion lib/schema/bigint.js
Original file line number Diff line number Diff line change
Expand Up @@ -86,7 +86,7 @@ SchemaBigInt.get = SchemaType.get;
* #### Example:
*
* // Make Mongoose cast empty string '' to false.
* const original = mongoose.Schema.BigInt.cast();
* const original = mongoose.Schema.Types.BigInt.cast();
* mongoose.Schema.BigInt.cast(v => {
* if (v === '') {
* return false;
Expand Down
53 changes: 27 additions & 26 deletions lib/schema/int32.js
Original file line number Diff line number Diff line change
Expand Up @@ -7,7 +7,6 @@
const CastError = require('../error/cast');
const SchemaType = require('../schemaType');
const castInt32 = require('../cast/int32');
const isBsonType = require('../helpers/isBsonType');

/**
* Int32 SchemaType constructor.
Expand Down Expand Up @@ -81,23 +80,44 @@ SchemaInt32.setters = [];

SchemaInt32.get = SchemaType.get;

/*!
* ignore
*/

SchemaInt32._defaultCaster = v => {
const INT32_MAX = 0x7FFFFFFF;
const INT32_MIN = -0x80000000;

if (v != null) {
if (typeof v !== 'number') {
Copy link
Author

Choose a reason for hiding this comment

The reason will be displayed to describe this comment to others. Learn more.

do you think we should also confirm that it is an integer here?

Choose a reason for hiding this comment

The reason will be displayed to describe this comment to others. Learn more.

I'm not sure - I wonder if that would make it too similar to the other cast function

Choose a reason for hiding this comment

The reason will be displayed to describe this comment to others. Learn more.

We can leave a comment open for when we open this for Val

Copy link
Member

Choose a reason for hiding this comment

The reason will be displayed to describe this comment to others. Learn more.

Yes, this function should check for integer.

The idea behind _defaultCaster() is that _defaultCaster() is the function Mongoose will use if casting is disabled using cast: false, so it should skip any type coercions (like converting strings or objects to int32).

throw new Error();
}
if (v < INT32_MIN || v > INT32_MAX) {
throw new Error();
}
}

return v;
};

/**
* Get/set the function used to cast arbitrary values to 32-bit integers
*
* #### Example:
*
* // Make Mongoose cast NaN to 0
* const defaultCast = mongoose.Schema.Int32.cast();
* mongoose.Schema.Int32.cast(v => {
* const defaultCast = mongoose.Schema.Types.Int32.cast();
* mongoose.Schema.Types.Int32.cast(v => {
* if (isNaN(v)) {
* return 0;
* }
* return defaultCast(v);
* });
*
* // Or disable casting for Int32s entirely
* // Or disable casting for Int32s entirely (only JS numbers within 32-bit integer bounds and null-ish values are permitted)
* mongoose.Schema.Int32.cast(false);
*
*
* @param {Function} caster
* @return {Function}
* @function get
Expand All @@ -112,16 +132,18 @@ SchemaInt32.cast = function cast(caster) {
if (caster === false) {
caster = this._defaultCaster;
}

this._cast = caster;

return this._cast;
};


/*!
* ignore
*/

SchemaInt32._checkRequired = v => v != null && isBsonType(v, 'Int32');
SchemaInt32._checkRequired = v => v != null;
/**
* Override the function the required validator uses to check whether a value
* passes the `required` check.
Expand Down Expand Up @@ -157,9 +179,6 @@ SchemaInt32.prototype.checkRequired = function(value) {

SchemaInt32.prototype.cast = function(value) {
let castInt32;
if (isBsonType(value, 'Int32')) {
return value;
}
if (typeof this._castFunction === 'function') {
castInt32 = this._castFunction;
} else if (typeof this.constructor.cast === 'function') {
Expand Down Expand Up @@ -225,24 +244,6 @@ SchemaInt32.prototype.castForQuery = function($conditional, val, context) {
}
};

/**
*
* @api private
*/

SchemaInt32.prototype._castNullish = function _castNullish(v) {
if (typeof v === 'undefined') {
return v;
}
const castInt32 = typeof this.constructor.cast === 'function' ?
this.constructor.cast() :
SchemaInt32.cast();
if (castInt32 == null) {
return v;
}
return v;
};

/*!
* Module exports.
*/
Expand Down
4 changes: 0 additions & 4 deletions lib/utils.js
Original file line number Diff line number Diff line change
Expand Up @@ -93,10 +93,6 @@ exports.deepEqual = function deepEqual(a, b) {
return a.toString() === b.toString();
}

if (isBsonType(a, 'Int32') && isBsonType(b, 'In32')) {
return a.value === b.value();
}

if (a instanceof RegExp && b instanceof RegExp) {
return a.source === b.source &&
a.ignoreCase === b.ignoreCase &&
Expand Down
Loading
Loading