Skip to content
New issue

Have a question about this project? Sign up for a free GitHub account to open an issue and contact its maintainers and the community.

By clicking “Sign up for GitHub”, you agree to our terms of service and privacy statement. We’ll occasionally send you account related emails.

Already on GitHub? Sign in to your account

feat(NODE-6504): Add Double as a SchemaType #3

Closed
wants to merge 8 commits into from
Closed
Show file tree
Hide file tree
Changes from all commits
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
1 change: 1 addition & 0 deletions docs/guide.md
Original file line number Diff line number Diff line change
Expand Up @@ -80,6 +80,7 @@ The permitted SchemaTypes are:
* [Decimal128](api/mongoose.html#mongoose_Mongoose-Decimal128)
* [Map](schematypes.html#maps)
* [UUID](schematypes.html#uuid)
* [Double](schematypes.html#double)

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

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

### Example

Expand All @@ -68,6 +69,7 @@ const schema = new Schema({
mixed: Schema.Types.Mixed,
_someId: Schema.Types.ObjectId,
decimal: Schema.Types.Decimal128,
double: Schema.Types.Double,
array: [],
ofString: [String],
ofNumber: [Number],
Expand Down Expand Up @@ -647,6 +649,42 @@ 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
```

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:

* 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

## 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) {
baileympearson marked this conversation as resolved.
Show resolved Hide resolved
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);
};
5 changes: 5 additions & 0 deletions lib/helpers/clone.js
Original file line number Diff line number Diff line change
Expand Up @@ -11,6 +11,7 @@ const isObject = require('./isObject');
const isPOJO = require('./isPOJO');
const symbols = require('./symbols');
const trustedSymbol = require('./query/trusted').trustedSymbol;
const BSON = require('bson');

/**
* Object clone with Mongoose natives support.
Expand All @@ -30,6 +31,10 @@ function clone(obj, options, isArrayChild) {
if (obj == null) {
return obj;
}

if (isBsonType(obj, 'Double')) {
return new BSON.Double(obj.value);
}
if (typeof obj === 'number' || typeof obj === 'string' || typeof obj === 'boolean' || typeof obj === 'bigint') {
return obj;
}
Expand Down
1 change: 1 addition & 0 deletions lib/schema.js
Original file line number Diff line number Diff line change
Expand Up @@ -2848,6 +2848,7 @@ module.exports = exports = Schema;
* - [Mixed](https://mongoosejs.com/docs/schematypes.html#mixed)
* - [UUID](https://mongoosejs.com/docs/schematypes.html#uuid)
* - [BigInt](https://mongoosejs.com/docs/schematypes.html#bigint)
* - [Double] (https://mongoosejs.com/docs/schematypes.html#double)
*
* Using this exposed access to the `Mixed` SchemaType, we can use them in our schema.
*
Expand Down
212 changes: 212 additions & 0 deletions lib/schema/double.js
Original file line number Diff line number Diff line change
@@ -0,0 +1,212 @@
'use strict';

/*!
* Module dependencies.
*/

const CastError = require('../error/cast');
const SchemaType = require('../schemaType');
const castDouble = require('../cast/double');

/**
* Double SchemaType constructor.
*
* @param {String} path
* @param {Object} options
* @inherits SchemaType
* @api public
*/

function SchemaDouble(path, options) {
SchemaType.call(this, path, options, 'Double');
}

/**
* This schema type's name, to defend against minifiers that mangle
* function names.
*
* @api public
*/
SchemaDouble.schemaName = 'Double';

SchemaDouble.defaultOptions = {};

/*!
* Inherits from SchemaType.
*/
SchemaDouble.prototype = Object.create(SchemaType.prototype);
SchemaDouble.prototype.constructor = SchemaDouble;

/*!
* ignore
*/

SchemaDouble._cast = castDouble;

/**
* Sets a default option for all Double instances.
*
* #### Example:
*
* // Make all Double fields required by default
* mongoose.Schema.Double.set('required', true);
*
* @param {String} option The option you'd like to set the value for
* @param {Any} value value for option
* @return {undefined}
* @function set
* @static
* @api public
*/

SchemaDouble.set = SchemaType.set;

SchemaDouble.setters = [];

/**
* Attaches a getter for all Double instances
*
* #### Example:
*
* // Converts Double to be a represent milliseconds upon access
* mongoose.Schema.Double.get(v => v == null ? '0.000 ms' : v.toString() + ' ms');
*
* @param {Function} getter
* @return {this}
* @function get
* @static
* @api public
*/

SchemaDouble.get = SchemaType.get;

/*!
* ignore
*/

SchemaDouble._defaultCaster = v => {
if (v != null) {
if (v._bsontype !== 'Double') {
baileympearson marked this conversation as resolved.
Show resolved Hide resolved
throw new Error();
}
}

return v;
};

/**
* Get/set the function used to cast arbitrary values to IEEE 754-2008 floating points
*
* #### Example:
*
* // Make Mongoose cast any NaNs to 0
* const defaultCast = mongoose.Schema.Types.Double.cast();
* mongoose.Schema.Types.Double.cast(v => {
* if (isNaN(v)) {
* return 0;
* }
* return defaultCast(v);
* });
*
* // Or disable casting for Doubles entirely (only JS numbers are permitted)
* mongoose.Schema.Double.cast(false);
*
*
* @param {Function} caster
* @return {Function}
* @function get
* @static
* @api public
*/

SchemaDouble.cast = function cast(caster) {
if (arguments.length === 0) {
return this._cast;
}
if (caster === false) {
caster = this._defaultCaster;
}

this._cast = caster;

return this._cast;
};


/*!
* ignore
*/

SchemaDouble._checkRequired = v => v != null;
/**
* Override the function the required validator uses to check whether a value
* passes the `required` check.
*
* @param {Function} fn
* @return {Function}
* @function checkRequired
* @static
* @api public
*/

SchemaDouble.checkRequired = SchemaType.checkRequired;

/**
* Check if the given value satisfies a required validator.
*
* @param {Any} value
* @return {Boolean}
* @api public
*/

SchemaDouble.prototype.checkRequired = function(value) {
return this.constructor._checkRequired(value);
};

/**
* Casts to Double
*
* @param {Object} value
* @param {Object} model this value is optional
* @api private
*/

SchemaDouble.prototype.cast = function(value) {
let castDouble;
if (typeof this._castFunction === 'function') {
castDouble = this._castFunction;
} else if (typeof this.constructor.cast === 'function') {
castDouble = this.constructor.cast();
} else {
castDouble = SchemaDouble.cast();
}

try {
return castDouble(value);
} catch (error) {
throw new CastError('Double', value, this.path, error, this);
}
};

/*!
* ignore
*/

function handleSingle(val) {
return this.cast(val);
}

SchemaDouble.prototype.$conditionalHandlers = {
...SchemaType.prototype.$conditionalHandlers,
$gt: handleSingle,
$gte: handleSingle,
$lt: handleSingle,
$lte: handleSingle
};


/*!
* Module exports.
*/

module.exports = SchemaDouble;
1 change: 1 addition & 0 deletions lib/schema/index.js
Original file line number Diff line number Diff line change
Expand Up @@ -19,6 +19,7 @@ exports.ObjectId = require('./objectId');
exports.String = require('./string');
exports.Subdocument = require('./subdocument');
exports.UUID = require('./uuid');
exports.Double = require('./double');

// alias

Expand Down
Loading