From 0e5fd097f8c74e584963e0dbf141dbfc30507110 Mon Sep 17 00:00:00 2001 From: Aditi Khare Date: Tue, 12 Nov 2024 17:13:31 -0500 Subject: [PATCH 1/8] initial commit --- docs/guide.md | 1 + docs/schematypes.md | 1 + lib/schema/double.js | 0 3 files changed, 2 insertions(+) create mode 100644 lib/schema/double.js diff --git a/docs/guide.md b/docs/guide.md index fa653acc4dd..0164db46b9a 100644 --- a/docs/guide.md +++ b/docs/guide.md @@ -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). diff --git a/docs/schematypes.md b/docs/schematypes.md index 904bb6a8726..009da571388 100644 --- a/docs/schematypes.md +++ b/docs/schematypes.md @@ -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 diff --git a/lib/schema/double.js b/lib/schema/double.js new file mode 100644 index 00000000000..e69de29bb2d From 727ca14f1aa0df411374184e2b59238855185ab2 Mon Sep 17 00:00:00 2001 From: Aditi Khare Date: Tue, 12 Nov 2024 17:38:28 -0500 Subject: [PATCH 2/8] temp --- docs/schematypes.md | 39 +++++++++++++++++++++++++++++++++++++++ 1 file changed, 39 insertions(+) diff --git a/docs/schematypes.md b/docs/schematypes.md index 009da571388..e9e1281a16e 100644 --- a/docs/schematypes.md +++ b/docs/schematypes.md @@ -69,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], @@ -648,6 +649,44 @@ 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 +* numbers in non-decimal or exponential format +* 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, From c88c52ebed6c9dda3b514ca6b6d65168de51042b Mon Sep 17 00:00:00 2001 From: Aditi Khare Date: Tue, 12 Nov 2024 18:39:58 -0500 Subject: [PATCH 3/8] casted --- docs/schematypes.md | 1 - lib/cast/double.js | 52 +++++++++++++++++++++++++++++++++++++++++++++ 2 files changed, 52 insertions(+), 1 deletion(-) create mode 100644 lib/cast/double.js diff --git a/docs/schematypes.md b/docs/schematypes.md index e9e1281a16e..c6cdd386f3c 100644 --- a/docs/schematypes.md +++ b/docs/schematypes.md @@ -682,7 +682,6 @@ 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 -* numbers in non-decimal or exponential format * objects that don't have a `valueOf()` function * an input that represents a value outside the bounds of a IEEE 754-2008 floating point diff --git a/lib/cast/double.js b/lib/cast/double.js new file mode 100644 index 00000000000..9d4315c3503 --- /dev/null +++ b/lib/cast/double.js @@ -0,0 +1,52 @@ +'use strict'; + +const assert = require('assert'); +const BSON = require('bson'); + +/** + * 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 + * @api private + */ + +module.exports = function castDouble(val) { + if (val == null) { + return val; + } + if (val === '') { + return null; + } + + let coercedVal; + if (val instanceof BSON.Int32 || val instanceof BSON.Double) { + coercedVal = val.value; + } else if (val instanceof BSON.Long) { + coercedVal = val.toNumber(); + } else if (typeof val === 'string') { + try { + coercedVal = BSON.Double.fromString(val); + } 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); + } catch { + assert.ok(false); + } + } else { + coercedVal = Number(tempVal); + } + } { + coercedVal = Number(val); + } + + return coercedVal; +}; From 4fb53d671434375bb508abf29fb5249fbe9f23e8 Mon Sep 17 00:00:00 2001 From: Aditi Khare Date: Tue, 12 Nov 2024 19:29:52 -0500 Subject: [PATCH 4/8] ready for review --- index.js | 1 + lib/mongoose.js | 13 + lib/schema.js | 1 + lib/schema/double.js | 212 +++++++++++++ lib/schema/index.js | 1 + test/double.test.js | 426 +++++++++++++++++++++++++++ test/helpers/isBsonType.test.js | 5 + test/types/schemaTypeOptions.test.ts | 1 + types/inferschematype.d.ts | 14 +- types/schematypes.d.ts | 15 + 10 files changed, 683 insertions(+), 6 deletions(-) create mode 100644 test/double.test.js diff --git a/index.js b/index.js index 6ebbd5fd5d3..f44ddb21f3a 100644 --- a/index.js +++ b/index.js @@ -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; diff --git a/lib/mongoose.js b/lib/mongoose.js index f314e4c399a..4abb3e99210 100644 --- a/lib/mongoose.js +++ b/lib/mongoose.js @@ -1167,6 +1167,19 @@ Mongoose.prototype.Mixed = SchemaTypes.Mixed; Mongoose.prototype.Date = SchemaTypes.Date; +/** + * The Mongoose Double [SchemaType](https://mongoosejs.com/docs/schematypes.html). Used for + * declaring paths in your schema that should be 64-bit IEEE 754-2008 floating points. + * + * #### Example: + * + * const vehicleSchema = new Car({ gasLevel: mongoose.Double }); + * + * @property Double + * @api public + */ +Mongoose.prototype.Double = SchemaTypes.Double; + /** * The Mongoose Number [SchemaType](https://mongoosejs.com/docs/schematypes.html). Used for * declaring paths in your schema that Mongoose should cast to numbers. diff --git a/lib/schema.js b/lib/schema.js index a9d23fd6199..deb0190061a 100644 --- a/lib/schema.js +++ b/lib/schema.js @@ -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. * diff --git a/lib/schema/double.js b/lib/schema/double.js index e69de29bb2d..7b4225b033f 100644 --- a/lib/schema/double.js +++ b/lib/schema/double.js @@ -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 (typeof v !== 'number') { + 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; diff --git a/lib/schema/index.js b/lib/schema/index.js index 0caf091adf2..c31bcf6e046 100644 --- a/lib/schema/index.js +++ b/lib/schema/index.js @@ -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 diff --git a/test/double.test.js b/test/double.test.js new file mode 100644 index 00000000000..eed5f867307 --- /dev/null +++ b/test/double.test.js @@ -0,0 +1,426 @@ +'use strict'; + +const assert = require('assert'); +const start = require('./common'); +const BSON = require('bson'); + +const mongoose = start.mongoose; +const Schema = mongoose.Schema; + + +describe('Double', function() { + beforeEach(() => mongoose.deleteModel(/Test/)); + + it('is a valid schema type', function() { + const schema = new Schema({ + myDouble: Schema.Types.Double + }); + const Test = mongoose.model('Test', schema); + + const doc = new Test({ + myDouble: 13 + }); + assert.strictEqual(doc.myDouble, 13); + assert.equal(typeof doc.myDouble, 'number'); + }); + + describe('supports the required property', function() { + it('when vaglue is null', async function() { + const schema = new Schema({ + Double: { + type: Schema.Types.Double, + required: true + } + }); + const Test = mongoose.model('Test', schema); + + const doc = new Test({ + double: null + }); + + const err = await doc.validate().then(() => null, err => err); + assert.ok(err); + assert.ok(err.errors['Double']); + assert.equal(err.errors['Double'].name, 'ValidatorError'); + assert.equal( + err.errors['Double'].message, + 'Path `Double` is required.' + ); + }); + it('when value is non-null', async function() { + const schema = new Schema({ + Double: { + type: Schema.Types.Double, + required: true + } + }); + const Test = mongoose.model('Test', schema); + + const doc = new Test({ + double: 3 + }); + + const err = await doc.validate().then(() => null, err => err); + assert.ok(err); + assert.ok(err.errors['Double']); + assert.equal(err.errors['Double'].name, 'ValidatorError'); + assert.equal( + err.errors['Double'].message, + 'Path `Double` is required.' + ); + }); + }); + + describe('special inputs', function() { + it('supports undefined as input', function() { + const schema = new Schema({ + myDouble: { + type: Schema.Types.Double + } + }); + const Test = mongoose.model('Test', schema); + + const doc = new Test({ + myDouble: undefined + }); + assert.strictEqual(doc.myDouble, undefined); + }); + + it('supports null as input', function() { + const schema = new Schema({ + myDouble: { + type: Schema.Types.Double + } + }); + const Test = mongoose.model('Test', schema); + + const doc = new Test({ + myDouble: null + }); + assert.strictEqual(doc.myDouble, null); + }); + }); + + describe('valid casts', function() { + it('casts from decimal string', function() { + const schema = new Schema({ + myDouble: { + type: Schema.Types.Double + } + }); + const Test = mongoose.model('Test', schema); + + const doc = new Test({ + myDouble: '-42.008' + }); + assert.strictEqual(doc.myDouble, -42.008); + }); + + it('casts from exponential string', function() { + const schema = new Schema({ + myDouble: { + type: Schema.Types.Double + } + }); + const Test = mongoose.model('Test', schema); + + const doc = new Test({ + myDouble: '1.22008e45' + }); + assert.strictEqual(doc.myDouble, 1.22008e45); + }); + + it('casts from infinite string', function() { + const schema = new Schema({ + myDouble1: { + type: Schema.Types.Double + }, + myDouble2: { + type: Schema.Types.Double + } + }); + const Test = mongoose.model('Test', schema); + + const doc = new Test({ + myDouble1: 'Infinity', + myDouble2: '-Infinity' + }); + assert.strictEqual(doc.myDouble1, Infinity); + assert.strictEqual(doc.myDouble2, -Infinity); + }); + + it('casts from NaN string', function() { + const schema = new Schema({ + myDouble: { + type: Schema.Types.Double + } + }); + const Test = mongoose.model('Test', schema); + + const doc = new Test({ + myDouble: 'NaN' + }); + assert.strictEqual(doc.myDouble, NaN); + }); + + it('casts from number', function() { + const schema = new Schema({ + myDouble: Schema.Types.Double + }); + const Test = mongoose.model('Test', schema); + + const doc = new Test({ + myDouble: 988 + }); + assert.strictEqual(doc.myDouble, 988); + }); + + it('casts from bigint', function() { + const schema = new Schema({ + myDouble: Schema.Types.Double + }); + const Test = mongoose.model('Test', schema); + + const doc = new Test({ + myDouble: -997n + }); + assert.strictEqual(doc.myDouble, -997); + }); + + it('casts from BSON.Long', function() { + const schema = new Schema({ + myDouble: Schema.Types.Double + }); + const Test = mongoose.model('Test', schema); + + const doc = new Test({ + myDouble: BSON.Long.fromNumber(-997987) + }); + assert.strictEqual(doc.myDouble, -997987); + }); + + it('casts from BSON.Double', function() { + const schema = new Schema({ + myDouble: Schema.Types.Double + }); + const Test = mongoose.model('Test', schema); + + const doc = new Test({ + myDouble: new BSON.Double(-997983.33) + }); + assert.strictEqual(doc.myDouble, -997983.33); + }); + + it('casts boolean true to 1', function() { + const schema = new Schema({ + myDouble: Schema.Types.Double + }); + const Test = mongoose.model('Test', schema); + + const doc = new Test({ + myDouble: true + }); + assert.strictEqual(doc.myDouble, 1); + }); + + it('casts boolean false to 0', function() { + const schema = new Schema({ + myDouble: Schema.Types.Double + }); + const Test = mongoose.model('Test', schema); + + const doc = new Test({ + myDouble: false + }); + assert.strictEqual(doc.myDouble, 0); + }); + + it('casts empty string to null', function() { + const schema = new Schema({ + myDouble: Schema.Types.Double + }); + const Test = mongoose.model('Test', schema); + + const doc = new Test({ + myDouble: '' + }); + assert.strictEqual(doc.myDouble, null); + }); + + it('supports valueOf() function ', function() { + const schema = new Schema({ + myDouble: Schema.Types.Double + }); + const Test = mongoose.model('Test', schema); + + const doc = new Test({ + myDouble: { a: 'random', b: { c: 'whatever' }, valueOf: () => 83.008 } + }); + assert.strictEqual(doc.myDouble, 83.008); + }); + }); + + describe('cast errors', () => { + let Test; + + beforeEach(function() { + const schema = new Schema({ + myDouble: Schema.Types.Double + }); + Test = mongoose.model('Test', schema); + }); + + describe('when a non-numeric string is provided to an Double field', () => { + it('throws a CastError upon validation', async() => { + const doc = new Test({ + myDouble: 'helloworld' + }); + + assert.strictEqual(doc.myDouble, undefined); + const err = await doc.validate().catch(e => e); + assert.ok(err); + assert.ok(err.errors['myDouble']); + assert.equal(err.errors['myDouble'].name, 'CastError'); + assert.equal( + err.errors['myDouble'].message, + 'Cast to Double failed for value "helloworld" (type string) at path "myDouble"' + ); + }); + }); + }); + + describe('custom casters', () => { + const defaultCast = mongoose.Schema.Types.Double.cast(); + + afterEach(() => { + mongoose.Schema.Types.Double.cast(defaultCast); + }); + + it('supports cast disabled', async() => { + mongoose.Schema.Types.Double.cast(false); + const schema = new Schema({ + myDouble1: { + type: Schema.Types.Double + }, + myDouble2: { + type: Schema.Types.Double + } + }); + const Test = mongoose.model('Test', schema); + const doc = new Test({ + myDouble1: '52', + myDouble2: 52 + }); + assert.strictEqual(doc.myDouble1, undefined); + assert.strictEqual(doc.myDouble2, 52); + + const err = await doc.validate().catch(e => e); + assert.ok(err); + assert.ok(err.errors['myDouble1']); + }); + + it('supports custom cast', () => { + mongoose.Schema.Types.Double.cast(v => { + if (isNaN(v)) { + return 0; + } + return defaultCast(v); + }); + const schema = new Schema({ + myDouble: { + type: Schema.Types.Double + } + }); + + const Test = mongoose.model('Test', schema); + const doc = new Test({ + myDouble: NaN + }); + assert.strictEqual(doc.myDouble, 0); + }); + }); + + describe('mongoDB integration', function() { + let db; + let Test; + + before(async function() { + db = await start(); + + const schema = new Schema({ + myDouble: Schema.Types.Double + }); + db.deleteModel(/Test/); + Test = db.model('Test', schema); + }); + + after(async function() { + await db.close(); + }); + + beforeEach(async() => { + await Test.deleteMany({}); + }); + + describe('$type compatibility', function() { + it('is queryable as a JS number in MongoDB', async function() { + await Test.create({ myDouble: '42.04' }); + const doc = await Test.findOne({ myDouble: { $type: 'number' } }); + assert.ok(doc); + assert.strictEqual(doc.myDouble, 42.04); + }); + + it('is NOT queryable as a BSON Integer in MongoDB if the value is NOT integer', async function() { + await Test.create({ myDouble: '42.04' }); + const doc = await Test.findOne({ myDouble: { $type: 'int' } }); + assert.strictEqual(doc, null); + }); + + it('is queryable as a BSON Double in MongoDB', async function() { + await Test.create({ myDouble: '42.04' }); + const doc = await Test.findOne({ myDouble: { $type: 'double' } }); + assert.equal(doc.myDouble, 42.04); + }); + }); + + it('can query with comparison operators', async function() { + await Test.create([ + { myDouble: 1.2 }, + { myDouble: 1.709 }, + { myDouble: 1.710 }, + { myDouble: 1.8 } + ]); + + let docs = await Test.find({ myDouble: { $gte: 1.710 } }).sort({ myDouble: 1 }); + assert.equal(docs.length, 2); + assert.deepStrictEqual(docs.map(doc => doc.myDouble), [1.710, 1.8]); + + docs = await Test.find({ myDouble: { $lt: 1.710 } }).sort({ myDouble: -1 }); + assert.equal(docs.length, 2); + assert.deepStrictEqual(docs.map(doc => doc.myDouble), [1.709, 1.2]); + }); + + it('supports populate()', async function() { + const parentSchema = new Schema({ + child: { + type: Schema.Types.Double, + ref: 'Child' + } + }); + const childSchema = new Schema({ + _id: Schema.Types.Double, + name: String + }); + const Parent = db.model('Parent', parentSchema); + const Child = db.model('Child', childSchema); + + const { _id } = await Parent.create({ child: 42 }); + await Child.create({ _id: 42, name: 'test-Double-populate' }); + + const doc = await Parent.findById(_id).populate('child'); + assert.ok(doc); + assert.equal(doc.child.name, 'test-Double-populate'); + assert.equal(doc.child._id, 42); + }); + }); +}); diff --git a/test/helpers/isBsonType.test.js b/test/helpers/isBsonType.test.js index 448aaf72db4..c43d6edc4e3 100644 --- a/test/helpers/isBsonType.test.js +++ b/test/helpers/isBsonType.test.js @@ -5,6 +5,7 @@ const isBsonType = require('../../lib/helpers/isBsonType'); const Decimal128 = require('mongodb').Decimal128; const ObjectId = require('mongodb').ObjectId; +const Double = require('mongodb').Double; describe('isBsonType', () => { it('true for any object with _bsontype property equal typename', () => { @@ -30,4 +31,8 @@ describe('isBsonType', () => { it('true for ObjectId', () => { assert.ok(isBsonType(new ObjectId(), 'ObjectId')); }); + + it('true for Double', () => { + assert.ok(isBsonType(new Double(), 'Double')); + }); }); diff --git a/test/types/schemaTypeOptions.test.ts b/test/types/schemaTypeOptions.test.ts index 5fc0e23a21d..2c865565386 100644 --- a/test/types/schemaTypeOptions.test.ts +++ b/test/types/schemaTypeOptions.test.ts @@ -69,6 +69,7 @@ function defaultOptions() { expectType>(new Schema.Types.Mixed('none').defaultOptions); expectType>(new Schema.Types.Number('none').defaultOptions); expectType>(new Schema.Types.ObjectId('none').defaultOptions); + expectType>(new Schema.Types.Double('none').defaultOptions); expectType>(new Schema.Types.Subdocument('none').defaultOptions); expectType>(new Schema.Types.UUID('none').defaultOptions); } diff --git a/types/inferschematype.d.ts b/types/inferschematype.d.ts index d73ad4cb81c..00df47b7bf8 100644 --- a/types/inferschematype.d.ts +++ b/types/inferschematype.d.ts @@ -229,17 +229,19 @@ type IsSchemaTypeFromBuiltinClass = T extends (typeof String) ? true : T extends (typeof Schema.Types.Buffer) ? true - : T extends Types.ObjectId + : T extends (typeof Schema.Types.Double) ? true - : T extends Types.Decimal128 + : T extends Types.ObjectId ? true - : T extends Buffer + : T extends Types.Decimal128 ? true - : T extends NativeDate + : T extends Buffer ? true - : T extends (typeof Schema.Types.Mixed) + : T extends NativeDate ? true - : IfEquals; + : T extends (typeof Schema.Types.Mixed) + ? true + : IfEquals; /** * @summary Resolve path type by returning the corresponding type. diff --git a/types/schematypes.d.ts b/types/schematypes.d.ts index aff686e1ec9..df82f6f6677 100644 --- a/types/schematypes.d.ts +++ b/types/schematypes.d.ts @@ -25,6 +25,13 @@ declare module 'mongoose' { */ type Number = Schema.Types.Number; + + /** + * The Mongoose Double [SchemaType](/docs/schematypes.html). Used for + * declaring paths in your schema that Mongoose should cast to doubles (IEEE 754-2008)/ + */ + type Double = Schema.Types.Double; + /** * The Mongoose ObjectId [SchemaType](/docs/schematypes.html). Used for * declaring paths in your schema that should be @@ -439,6 +446,14 @@ declare module 'mongoose' { defaultOptions: Record; } + class Double extends SchemaType { + /** This schema type's name, to defend against minifiers that mangle function names. */ + static schemaName: 'Double'; + + /** Default options for this SchemaType */ + defaultOptions: Record; + } + class ObjectId extends SchemaType { /** This schema type's name, to defend against minifiers that mangle function names. */ static schemaName: 'ObjectId'; From 6ec99a9f61ddc8e376705b7ff9e04dc8bc818f2e Mon Sep 17 00:00:00 2001 From: Aditi Khare Date: Tue, 12 Nov 2024 19:35:31 -0500 Subject: [PATCH 5/8] lint fix --- docs/schematypes.md | 1 - 1 file changed, 1 deletion(-) diff --git a/docs/schematypes.md b/docs/schematypes.md index c6cdd386f3c..f3d0a72261e 100644 --- a/docs/schematypes.md +++ b/docs/schematypes.md @@ -685,7 +685,6 @@ The following inputs will result will all result in a [CastError](validation.htm * 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, From dbb68dc439263084a63b9a5701ce8d3e1da61cdb Mon Sep 17 00:00:00 2001 From: Aditi Khare Date: Tue, 12 Nov 2024 19:38:23 -0500 Subject: [PATCH 6/8] add link --- lib/cast/double.js | 2 +- 1 file changed, 1 insertion(+), 1 deletion(-) diff --git a/lib/cast/double.js b/lib/cast/double.js index 9d4315c3503..66f3e3a03b7 100644 --- a/lib/cast/double.js +++ b/lib/cast/double.js @@ -9,7 +9,7 @@ const BSON = require('bson'); * * @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 + * @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 */ From b3c8955f3b282d35eb9c3f8572b4cd94b734a61c Mon Sep 17 00:00:00 2001 From: Aditi Khare Date: Tue, 19 Nov 2024 19:13:41 -0500 Subject: [PATCH 7/8] requested changes --- lib/cast/double.js | 15 ++++------ lib/helpers/clone.js | 5 ++++ lib/mongoose.js | 13 --------- lib/schema/double.js | 2 +- test/double.test.js | 66 ++++++++++++++++++++++++-------------------- 5 files changed, 48 insertions(+), 53 deletions(-) diff --git a/lib/cast/double.js b/lib/cast/double.js index 66f3e3a03b7..e0f2b196ca1 100644 --- a/lib/cast/double.js +++ b/lib/cast/double.js @@ -14,21 +14,17 @@ const BSON = require('bson'); */ module.exports = function castDouble(val) { - if (val == null) { - return val; - } - if (val === '') { + if (val == null || val === '') { return null; } let coercedVal; - if (val instanceof BSON.Int32 || val instanceof BSON.Double) { - coercedVal = val.value; - } else if (val instanceof BSON.Long) { + if (val instanceof BSON.Long) { coercedVal = val.toNumber(); } else if (typeof val === 'string') { try { coercedVal = BSON.Double.fromString(val); + return coercedVal; } catch { assert.ok(false); } @@ -38,15 +34,16 @@ module.exports = function castDouble(val) { 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 coercedVal; + return new BSON.Double(coercedVal); }; diff --git a/lib/helpers/clone.js b/lib/helpers/clone.js index 09204b8c8a4..f3afc3167ca 100644 --- a/lib/helpers/clone.js +++ b/lib/helpers/clone.js @@ -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. @@ -30,6 +31,10 @@ function clone(obj, options, isArrayChild) { if (obj == null) { return obj; } + + if (obj._bsontype === 'Double') { + return new BSON.Double(obj.value); + } if (typeof obj === 'number' || typeof obj === 'string' || typeof obj === 'boolean' || typeof obj === 'bigint') { return obj; } diff --git a/lib/mongoose.js b/lib/mongoose.js index 4abb3e99210..f314e4c399a 100644 --- a/lib/mongoose.js +++ b/lib/mongoose.js @@ -1167,19 +1167,6 @@ Mongoose.prototype.Mixed = SchemaTypes.Mixed; Mongoose.prototype.Date = SchemaTypes.Date; -/** - * The Mongoose Double [SchemaType](https://mongoosejs.com/docs/schematypes.html). Used for - * declaring paths in your schema that should be 64-bit IEEE 754-2008 floating points. - * - * #### Example: - * - * const vehicleSchema = new Car({ gasLevel: mongoose.Double }); - * - * @property Double - * @api public - */ -Mongoose.prototype.Double = SchemaTypes.Double; - /** * The Mongoose Number [SchemaType](https://mongoosejs.com/docs/schematypes.html). Used for * declaring paths in your schema that Mongoose should cast to numbers. diff --git a/lib/schema/double.js b/lib/schema/double.js index 7b4225b033f..79c94752184 100644 --- a/lib/schema/double.js +++ b/lib/schema/double.js @@ -86,7 +86,7 @@ SchemaDouble.get = SchemaType.get; SchemaDouble._defaultCaster = v => { if (v != null) { - if (typeof v !== 'number') { + if (v._bsontype !== 'Double') { throw new Error(); } } diff --git a/test/double.test.js b/test/double.test.js index eed5f867307..6bf7e6c59e7 100644 --- a/test/double.test.js +++ b/test/double.test.js @@ -20,12 +20,12 @@ describe('Double', function() { const doc = new Test({ myDouble: 13 }); - assert.strictEqual(doc.myDouble, 13); - assert.equal(typeof doc.myDouble, 'number'); + assert.deepStrictEqual(doc.myDouble, new BSON.Double(13)); + assert.equal(typeof doc.myDouble, 'object'); }); describe('supports the required property', function() { - it('when vaglue is null', async function() { + it('when value is null', async function() { const schema = new Schema({ Double: { type: Schema.Types.Double, @@ -83,7 +83,7 @@ describe('Double', function() { const doc = new Test({ myDouble: undefined }); - assert.strictEqual(doc.myDouble, undefined); + assert.deepStrictEqual(doc.myDouble, undefined); }); it('supports null as input', function() { @@ -97,7 +97,7 @@ describe('Double', function() { const doc = new Test({ myDouble: null }); - assert.strictEqual(doc.myDouble, null); + assert.deepStrictEqual(doc.myDouble, null); }); }); @@ -113,7 +113,7 @@ describe('Double', function() { const doc = new Test({ myDouble: '-42.008' }); - assert.strictEqual(doc.myDouble, -42.008); + assert.deepStrictEqual(doc.myDouble, new BSON.Double(-42.008)); }); it('casts from exponential string', function() { @@ -127,7 +127,7 @@ describe('Double', function() { const doc = new Test({ myDouble: '1.22008e45' }); - assert.strictEqual(doc.myDouble, 1.22008e45); + assert.deepStrictEqual(doc.myDouble, new BSON.Double(1.22008e45)); }); it('casts from infinite string', function() { @@ -145,8 +145,8 @@ describe('Double', function() { myDouble1: 'Infinity', myDouble2: '-Infinity' }); - assert.strictEqual(doc.myDouble1, Infinity); - assert.strictEqual(doc.myDouble2, -Infinity); + assert.deepStrictEqual(doc.myDouble1, new BSON.Double(Infinity)); + assert.deepStrictEqual(doc.myDouble2, new BSON.Double(-Infinity)); }); it('casts from NaN string', function() { @@ -160,7 +160,7 @@ describe('Double', function() { const doc = new Test({ myDouble: 'NaN' }); - assert.strictEqual(doc.myDouble, NaN); + assert.deepStrictEqual(doc.myDouble, new BSON.Double('NaN')); }); it('casts from number', function() { @@ -172,7 +172,7 @@ describe('Double', function() { const doc = new Test({ myDouble: 988 }); - assert.strictEqual(doc.myDouble, 988); + assert.deepStrictEqual(doc.myDouble, new BSON.Double(988)); }); it('casts from bigint', function() { @@ -184,7 +184,7 @@ describe('Double', function() { const doc = new Test({ myDouble: -997n }); - assert.strictEqual(doc.myDouble, -997); + assert.deepStrictEqual(doc.myDouble, new BSON.Double(-997)); }); it('casts from BSON.Long', function() { @@ -196,7 +196,7 @@ describe('Double', function() { const doc = new Test({ myDouble: BSON.Long.fromNumber(-997987) }); - assert.strictEqual(doc.myDouble, -997987); + assert.deepStrictEqual(doc.myDouble, new BSON.Double(-997987)); }); it('casts from BSON.Double', function() { @@ -208,7 +208,7 @@ describe('Double', function() { const doc = new Test({ myDouble: new BSON.Double(-997983.33) }); - assert.strictEqual(doc.myDouble, -997983.33); + assert.deepStrictEqual(doc.myDouble, new BSON.Double(-997983.33)); }); it('casts boolean true to 1', function() { @@ -220,7 +220,7 @@ describe('Double', function() { const doc = new Test({ myDouble: true }); - assert.strictEqual(doc.myDouble, 1); + assert.deepStrictEqual(doc.myDouble, new BSON.Double(1)); }); it('casts boolean false to 0', function() { @@ -232,7 +232,7 @@ describe('Double', function() { const doc = new Test({ myDouble: false }); - assert.strictEqual(doc.myDouble, 0); + assert.deepStrictEqual(doc.myDouble, new BSON.Double(0)); }); it('casts empty string to null', function() { @@ -244,7 +244,7 @@ describe('Double', function() { const doc = new Test({ myDouble: '' }); - assert.strictEqual(doc.myDouble, null); + assert.deepStrictEqual(doc.myDouble, null); }); it('supports valueOf() function ', function() { @@ -256,7 +256,7 @@ describe('Double', function() { const doc = new Test({ myDouble: { a: 'random', b: { c: 'whatever' }, valueOf: () => 83.008 } }); - assert.strictEqual(doc.myDouble, 83.008); + assert.deepStrictEqual(doc.myDouble, new BSON.Double(83.008)); }); }); @@ -276,7 +276,7 @@ describe('Double', function() { myDouble: 'helloworld' }); - assert.strictEqual(doc.myDouble, undefined); + assert.deepStrictEqual(doc.myDouble, undefined); const err = await doc.validate().catch(e => e); assert.ok(err); assert.ok(err.errors['myDouble']); @@ -309,10 +309,10 @@ describe('Double', function() { const Test = mongoose.model('Test', schema); const doc = new Test({ myDouble1: '52', - myDouble2: 52 + myDouble2: new BSON.Double(52) }); - assert.strictEqual(doc.myDouble1, undefined); - assert.strictEqual(doc.myDouble2, 52); + assert.deepStrictEqual(doc.myDouble1, undefined); + assert.deepStrictEqual(doc.myDouble2, new BSON.Double(52)); const err = await doc.validate().catch(e => e); assert.ok(err); @@ -322,7 +322,7 @@ describe('Double', function() { it('supports custom cast', () => { mongoose.Schema.Types.Double.cast(v => { if (isNaN(v)) { - return 0; + return new BSON.Double(2); } return defaultCast(v); }); @@ -336,7 +336,7 @@ describe('Double', function() { const doc = new Test({ myDouble: NaN }); - assert.strictEqual(doc.myDouble, 0); + assert.deepStrictEqual(doc.myDouble, new BSON.Double(2)); }); }); @@ -367,19 +367,25 @@ describe('Double', function() { await Test.create({ myDouble: '42.04' }); const doc = await Test.findOne({ myDouble: { $type: 'number' } }); assert.ok(doc); - assert.strictEqual(doc.myDouble, 42.04); + assert.deepStrictEqual(doc.myDouble, new BSON.Double(42.04)); }); it('is NOT queryable as a BSON Integer in MongoDB if the value is NOT integer', async function() { await Test.create({ myDouble: '42.04' }); const doc = await Test.findOne({ myDouble: { $type: 'int' } }); - assert.strictEqual(doc, null); + assert.deepStrictEqual(doc, null); }); - it('is queryable as a BSON Double in MongoDB', async function() { + it('is queryable as a BSON Double in MongoDB when a non-integer is provided', async function() { await Test.create({ myDouble: '42.04' }); const doc = await Test.findOne({ myDouble: { $type: 'double' } }); - assert.equal(doc.myDouble, 42.04); + assert.deepStrictEqual(doc.myDouble, new BSON.Double(42.04)); + }); + + it('is queryable as a BSON Double in MongoDB when an integer is provided', async function() { + await Test.create({ myDouble: '42' }); + const doc = await Test.findOne({ myDouble: { $type: 'double' } }); + assert.deepStrictEqual(doc.myDouble, new BSON.Double(42)); }); }); @@ -393,11 +399,11 @@ describe('Double', function() { let docs = await Test.find({ myDouble: { $gte: 1.710 } }).sort({ myDouble: 1 }); assert.equal(docs.length, 2); - assert.deepStrictEqual(docs.map(doc => doc.myDouble), [1.710, 1.8]); + assert.deepStrictEqual(docs.map(doc => doc.myDouble), [new BSON.Double(1.710), new BSON.Double(1.8)]); docs = await Test.find({ myDouble: { $lt: 1.710 } }).sort({ myDouble: -1 }); assert.equal(docs.length, 2); - assert.deepStrictEqual(docs.map(doc => doc.myDouble), [1.709, 1.2]); + assert.deepStrictEqual(docs.map(doc => doc.myDouble), [new BSON.Double(1.709), new BSON.Double(1.2)]); }); it('supports populate()', async function() { From b467e696e758d1b80ef0345f92d5b99300d27ac9 Mon Sep 17 00:00:00 2001 From: Aditi Khare Date: Fri, 22 Nov 2024 14:40:21 -0500 Subject: [PATCH 8/8] requested change 2 --- lib/cast/double.js | 3 ++- lib/helpers/clone.js | 2 +- 2 files changed, 3 insertions(+), 2 deletions(-) diff --git a/lib/cast/double.js b/lib/cast/double.js index e0f2b196ca1..5dfc6c1a797 100644 --- a/lib/cast/double.js +++ b/lib/cast/double.js @@ -2,6 +2,7 @@ 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 @@ -19,7 +20,7 @@ module.exports = function castDouble(val) { } let coercedVal; - if (val instanceof BSON.Long) { + if (isBsonType(val, 'Long')) { coercedVal = val.toNumber(); } else if (typeof val === 'string') { try { diff --git a/lib/helpers/clone.js b/lib/helpers/clone.js index f3afc3167ca..a8dd587dbf9 100644 --- a/lib/helpers/clone.js +++ b/lib/helpers/clone.js @@ -32,7 +32,7 @@ function clone(obj, options, isArrayChild) { return obj; } - if (obj._bsontype === 'Double') { + if (isBsonType(obj, 'Double')) { return new BSON.Double(obj.value); } if (typeof obj === 'number' || typeof obj === 'string' || typeof obj === 'boolean' || typeof obj === 'bigint') {