diff --git a/docs/guide.md b/docs/guide.md index fa653acc4dd..6f432a4d639 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) +* [Int32](schematypes.html#int32) Read more about [SchemaTypes here](schematypes.html). diff --git a/docs/schematypes.md b/docs/schematypes.md index 904bb6a8726..bfb42ae4869 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) +* [Int32](#int32) ### Example @@ -68,6 +69,7 @@ const schema = new Schema({ mixed: Schema.Types.Mixed, _someId: Schema.Types.ObjectId, decimal: Schema.Types.Decimal128, + int32bit: Schema.Types.Int32, array: [], ofString: [String], ofNumber: [Number], @@ -647,6 +649,44 @@ const question = new Question({ answer: 42n }); typeof question.answer; // 'bigint' ``` +### 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 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 outside the bounds of an 32-bit integer + ## Getters {#getters} Getters are like virtuals for paths defined in your schema. For example, diff --git a/lib/cast/int32.js b/lib/cast/int32.js new file mode 100644 index 00000000000..34eeae8565f --- /dev/null +++ b/lib/cast/int32.js @@ -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); +}; diff --git a/lib/mongoose.js b/lib/mongoose.js index 2a03b638209..82bd9f50ae4 100644 --- a/lib/mongoose.js +++ b/lib/mongoose.js @@ -987,6 +987,7 @@ Mongoose.prototype.VirtualType = VirtualType; * - [ObjectId](https://mongoosejs.com/docs/schematypes.html#objectids) * - [Map](https://mongoosejs.com/docs/schematypes.html#maps) * - [Subdocument](https://mongoosejs.com/docs/schematypes.html#schemas) + * - [Int32](https://mongoosejs.com/docs/schematypes.html#int32) * * Using this exposed access to the `ObjectId` type, we can construct ids on demand. * @@ -1138,6 +1139,7 @@ Mongoose.prototype.syncIndexes = function(options) { Mongoose.prototype.Decimal128 = SchemaTypes.Decimal128; + /** * The Mongoose Mixed [SchemaType](https://mongoosejs.com/docs/schematypes.html). Used for * declaring paths in your schema that Mongoose's change tracking, casting, diff --git a/lib/schema.js b/lib/schema.js index a27339a1541..7232c06009d 100644 --- a/lib/schema.js +++ b/lib/schema.js @@ -2892,6 +2892,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) + * - [Int32](https://mongoosejs.com/docs/schematypes.html#int32) * * Using this exposed access to the `Mixed` SchemaType, we can use them in our schema. * diff --git a/lib/schema/bigint.js b/lib/schema/bigint.js index 02912627dd5..6baec1f7efd 100644 --- a/lib/schema/bigint.js +++ b/lib/schema/bigint.js @@ -81,12 +81,12 @@ SchemaBigInt.setters = []; SchemaBigInt.get = SchemaType.get; /** - * Get/set the function used to cast arbitrary values to booleans. + * Get/set the function used to cast arbitrary values to bigints. * * #### 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; diff --git a/lib/schema/index.js b/lib/schema/index.js index 0caf091adf2..a1c5087b807 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.Int32 = require('./int32'); // alias diff --git a/lib/schema/int32.js b/lib/schema/int32.js new file mode 100644 index 00000000000..51594934e91 --- /dev/null +++ b/lib/schema/int32.js @@ -0,0 +1,249 @@ +'use strict'; + +/*! + * Module dependencies. + */ + +const CastError = require('../error/cast'); +const SchemaType = require('../schemaType'); +const castInt32 = require('../cast/int32'); + +/** + * Int32 SchemaType constructor. + * + * @param {String} path + * @param {Object} options + * @inherits SchemaType + * @api public + */ + +function SchemaInt32(path, options) { + SchemaType.call(this, path, options, 'Int32'); +} + +/** + * This schema type's name, to defend against minifiers that mangle + * function names. + * + * @api public + */ +SchemaInt32.schemaName = 'Int32'; + +SchemaInt32.defaultOptions = {}; + +/*! + * Inherits from SchemaType. + */ +SchemaInt32.prototype = Object.create(SchemaType.prototype); +SchemaInt32.prototype.constructor = SchemaInt32; + +/*! + * ignore + */ + +SchemaInt32._cast = castInt32; + +/** + * Sets a default option for all Int32 instances. + * + * #### Example: + * + * // Make all Int32 fields required by default + * mongoose.Schema.Int32.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 + */ + +SchemaInt32.set = SchemaType.set; + +SchemaInt32.setters = []; + +/** + * Attaches a getter for all Int32 instances + * + * #### Example: + * + * // Converts int32 to be a represent milliseconds upon access + * mongoose.Schema.Int32.get(v => v == null ? '0 ms' : v.toString() + ' ms'); + * + * @param {Function} getter + * @return {this} + * @function get + * @static + * @api public + */ + +SchemaInt32.get = SchemaType.get; + +/*! + * ignore + */ + +SchemaInt32._defaultCaster = v => { + const INT32_MAX = 0x7FFFFFFF; + const INT32_MIN = -0x80000000; + + if (v != null) { + if (typeof v !== 'number' || v !== (v | 0) || 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.Types.Int32.cast(); + * mongoose.Schema.Types.Int32.cast(v => { + * if (isNaN(v)) { + * return 0; + * } + * return defaultCast(v); + * }); + * + * // 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 + * @static + * @api public + */ + +SchemaInt32.cast = function cast(caster) { + if (arguments.length === 0) { + return this._cast; + } + if (caster === false) { + caster = this._defaultCaster; + } + + this._cast = caster; + + return this._cast; +}; + + +/*! + * ignore + */ + +SchemaInt32._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 + */ + +SchemaInt32.checkRequired = SchemaType.checkRequired; + +/** + * Check if the given value satisfies a required validator. + * + * @param {Any} value + * @return {Boolean} + * @api public + */ + +SchemaInt32.prototype.checkRequired = function(value) { + return this.constructor._checkRequired(value); +}; + +/** + * Casts to Int32 + * + * @param {Object} value + * @param {Object} model this value is optional + * @api private + */ + +SchemaInt32.prototype.cast = function(value) { + let castInt32; + if (typeof this._castFunction === 'function') { + castInt32 = this._castFunction; + } else if (typeof this.constructor.cast === 'function') { + castInt32 = this.constructor.cast(); + } else { + castInt32 = SchemaInt32.cast(); + } + + try { + return castInt32(value); + } catch (error) { + throw new CastError('Int32', value, this.path, error, this); + } +}; + +/*! + * ignore + */ + +SchemaInt32.$conditionalHandlers = { + ...SchemaType.prototype.$conditionalHandlers, + $gt: handleSingle, + $gte: handleSingle, + $lt: handleSingle, + $lte: handleSingle +}; + +/*! + * ignore + */ + +function handleSingle(val, context) { + return this.castForQuery(null, val, context); +} + +/** + * Casts contents for queries. + * + * @param {String} $conditional + * @param {any} val + * @api private + */ + +SchemaInt32.prototype.castForQuery = function($conditional, val, context) { + let handler; + if ($conditional != null) { + handler = SchemaInt32.$conditionalHandlers[$conditional]; + + if (handler) { + return handler.call(this, val); + } + + return this.applySetters(null, val, context); + } + + try { + return this.applySetters(val, context); + } catch (err) { + if (err instanceof CastError && err.path === this.path && this.$fullPath != null) { + err.path = this.$fullPath; + } + throw err; + } +}; + + +/*! + * Module exports. + */ + +module.exports = SchemaInt32; diff --git a/test/helpers/isBsonType.test.js b/test/helpers/isBsonType.test.js index 448aaf72db4..67e5bc754e5 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 Int32 = require('mongodb').Int32; 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 Int32', () => { + assert.ok(isBsonType(new Int32(), 'Int32')); + }); }); diff --git a/test/int32.test.js b/test/int32.test.js new file mode 100644 index 00000000000..b05fa82305b --- /dev/null +++ b/test/int32.test.js @@ -0,0 +1,537 @@ +'use strict'; + +const assert = require('assert'); +const start = require('./common'); +const BSON = require('bson'); +const sinon = require('sinon'); + +const mongoose = start.mongoose; +const Schema = mongoose.Schema; + +const INT32_MAX = 0x7FFFFFFF; +const INT32_MIN = -0x80000000; + +describe('Int32', function() { + beforeEach(() => mongoose.deleteModel(/Test/)); + + it('is a valid schema type', function() { + const schema = new Schema({ + myInt32: Schema.Types.Int32 + }); + const Test = mongoose.model('Test', schema); + + const doc = new Test({ + myInt32: 13 + }); + assert.strictEqual(doc.myInt32, 13); + assert.equal(typeof doc.myInt32, 'number'); + }); + + describe('supports the required property', function() { + it('when value is null', async function() { + const schema = new Schema({ + int32: { + type: Schema.Types.Int32, + required: true + } + }); + const Test = mongoose.model('Test', schema); + + const doc = new Test({ + int: null + }); + + const err = await doc.validate().then(() => null, err => err); + assert.ok(err); + assert.ok(err.errors['int32']); + assert.equal(err.errors['int32'].name, 'ValidatorError'); + assert.equal( + err.errors['int32'].message, + 'Path `int32` is required.' + ); + }); + it('when value is non-null', async function() { + const schema = new Schema({ + int32: { + type: Schema.Types.Int32, + required: true + } + }); + const Test = mongoose.model('Test', schema); + + const doc = new Test({ + int: 3 + }); + + const err = await doc.validate().then(() => null, err => err); + assert.ok(err); + assert.ok(err.errors['int32']); + assert.equal(err.errors['int32'].name, 'ValidatorError'); + assert.equal( + err.errors['int32'].message, + 'Path `int32` is required.' + ); + }); + }); + + describe('special inputs', function() { + it('supports INT32_MIN as input', function() { + const schema = new Schema({ + myInt: { + type: Schema.Types.Int32 + } + }); + const Test = mongoose.model('Test', schema); + + const doc = new Test({ + myInt: INT32_MIN + }); + assert.strictEqual(doc.myInt, INT32_MIN); + }); + + it('supports INT32_MAX as input', function() { + const schema = new Schema({ + myInt: { + type: Schema.Types.Int32 + } + }); + const Test = mongoose.model('Test', schema); + + const doc = new Test({ + myInt: INT32_MAX + }); + assert.strictEqual(doc.myInt, INT32_MAX); + }); + + it('supports undefined as input', function() { + const schema = new Schema({ + myInt: { + type: Schema.Types.Int32 + } + }); + const Test = mongoose.model('Test', schema); + + const doc = new Test({ + myInt: undefined + }); + assert.strictEqual(doc.myInt, undefined); + }); + + it('supports null as input', function() { + const schema = new Schema({ + myInt: { + type: Schema.Types.Int32 + } + }); + const Test = mongoose.model('Test', schema); + + const doc = new Test({ + myInt: null + }); + assert.strictEqual(doc.myInt, null); + }); + }); + + describe('valid casts', function() { + it('casts from string', function() { + const schema = new Schema({ + myInt: { + type: Schema.Types.Int32 + } + }); + const Test = mongoose.model('Test', schema); + + const doc = new Test({ + myInt: '-42' + }); + assert.strictEqual(doc.myInt, -42); + }); + + it('casts from number', function() { + const schema = new Schema({ + myInt: Schema.Types.Int32 + }); + const Test = mongoose.model('Test', schema); + + const doc = new Test({ + myInt: '-997.0' + }); + assert.strictEqual(doc.myInt, -997); + }); + + it('casts from bigint', function() { + const schema = new Schema({ + myInt: Schema.Types.Int32 + }); + const Test = mongoose.model('Test', schema); + + const doc = new Test({ + myInt: -997n + }); + assert.strictEqual(doc.myInt, -997); + }); + + it('casts from BSON.Int32', function() { + const schema = new Schema({ + myInt: Schema.Types.Int32 + }); + const Test = mongoose.model('Test', schema); + + const doc = new Test({ + myInt: new BSON.Int32(-997) + }); + assert.strictEqual(doc.myInt, -997); + }); + + describe('long', function() { + after(function() { + sinon.restore(); + }); + + it('casts from BSON.Long provided its value is within bounds of Int32', function() { + const schema = new Schema({ + myInt: Schema.Types.Int32 + }); + const Test = mongoose.model('Test', schema); + + const doc = new Test({ + myInt: BSON.Long.fromNumber(-997) + }); + assert.strictEqual(doc.myInt, -997); + }); + + it('calls Long.toNumber when casting long', function() { + // this is a perf optimization, since long.toNumber() is faster than Number(long) + const schema = new Schema({ + myInt: Schema.Types.Int32 + }); + const Test = mongoose.model('Test', schema); + + sinon.stub(BSON.Long.prototype, 'toNumber').callsFake(function() { + return 2; + }); + + const doc = new Test({ + myInt: BSON.Long.fromNumber(-997) + }); + + assert.strictEqual(doc.myInt, 2); + }); + }); + + it('casts from BSON.Double provided its value is an integer', function() { + const schema = new Schema({ + myInt: Schema.Types.Int32 + }); + const Test = mongoose.model('Test', schema); + + const doc = new Test({ + myInt: new BSON.Double(-997) + }); + assert.strictEqual(doc.myInt, -997); + }); + + it('casts boolean true to 1', function() { + const schema = new Schema({ + myInt: Schema.Types.Int32 + }); + const Test = mongoose.model('Test', schema); + + const doc = new Test({ + myInt: true + }); + assert.strictEqual(doc.myInt, 1); + }); + + it('casts boolean false to 0', function() { + const schema = new Schema({ + myInt: Schema.Types.Int32 + }); + const Test = mongoose.model('Test', schema); + + const doc = new Test({ + myInt: false + }); + assert.strictEqual(doc.myInt, 0); + }); + + it('casts empty string to null', function() { + const schema = new Schema({ + myInt: Schema.Types.Int32 + }); + const Test = mongoose.model('Test', schema); + + const doc = new Test({ + myInt: '' + }); + assert.strictEqual(doc.myInt, null); + }); + + it('supports valueOf() function ', function() { + const schema = new Schema({ + myInt: Schema.Types.Int32 + }); + const Test = mongoose.model('Test', schema); + + const doc = new Test({ + myInt: { a: 'random', b: { c: 'whatever' }, valueOf: () => 83 } + }); + assert.strictEqual(doc.myInt, 83); + }); + }); + + describe('cast errors', () => { + let Test; + + beforeEach(function() { + const schema = new Schema({ + myInt: Schema.Types.Int32 + }); + Test = mongoose.model('Test', schema); + }); + + describe('when a non-integer decimal input is provided to an Int32 field', () => { + it('throws a CastError upon validation', async() => { + const doc = new Test({ + myInt: -42.4 + }); + + assert.strictEqual(doc.myInt, undefined); + const err = await doc.validate().catch(e => e); + assert.ok(err); + assert.ok(err.errors['myInt']); + assert.equal(err.errors['myInt'].name, 'CastError'); + assert.equal( + err.errors['myInt'].message, + 'Cast to Int32 failed for value "-42.4" (type number) at path "myInt"' + ); + }); + }); + + describe('when a non-numeric string is provided to an Int32 field', () => { + it('throws a CastError upon validation', async() => { + const doc = new Test({ + myInt: 'helloworld' + }); + + assert.strictEqual(doc.myInt, undefined); + const err = await doc.validate().catch(e => e); + assert.ok(err); + assert.ok(err.errors['myInt']); + assert.equal(err.errors['myInt'].name, 'CastError'); + assert.equal( + err.errors['myInt'].message, + 'Cast to Int32 failed for value "helloworld" (type string) at path "myInt"' + ); + }); + }); + + describe('when a non-integer decimal string is provided to an Int32 field', () => { + it('throws a CastError upon validation', async() => { + const doc = new Test({ + myInt: '1.2' + }); + + assert.strictEqual(doc.myInt, undefined); + const err = await doc.validate().catch(e => e); + assert.ok(err); + assert.ok(err.errors['myInt']); + assert.equal(err.errors['myInt'].name, 'CastError'); + assert.equal( + err.errors['myInt'].message, + 'Cast to Int32 failed for value "1.2" (type string) at path "myInt"' + ); + }); + }); + + describe('when NaN is provided to an Int32 field', () => { + it('throws a CastError upon validation', async() => { + const doc = new Test({ + myInt: NaN + }); + + assert.strictEqual(doc.myInt, undefined); + const err = await doc.validate().catch(e => e); + assert.ok(err); + assert.ok(err.errors['myInt']); + assert.equal(err.errors['myInt'].name, 'CastError'); + assert.equal( + err.errors['myInt'].message, + 'Cast to Int32 failed for value "NaN" (type number) at path "myInt"' + ); + }); + }); + + describe('when value above INT32_MAX is provided to an Int32 field', () => { + it('throws a CastError upon validation', async() => { + const doc = new Test({ + myInt: INT32_MAX + 1 + }); + + assert.strictEqual(doc.myInt, undefined); + const err = await doc.validate().catch(e => e); + assert.ok(err); + assert.ok(err.errors['myInt']); + assert.equal(err.errors['myInt'].name, 'CastError'); + assert.equal( + err.errors['myInt'].message, + 'Cast to Int32 failed for value "2147483648" (type number) at path "myInt"' + ); + }); + }); + + describe('when value below INT32_MIN is provided to an Int32 field', () => { + it('throws a CastError upon validation', async() => { + const doc = new Test({ + myInt: INT32_MIN - 1 + }); + + assert.strictEqual(doc.myInt, undefined); + const err = await doc.validate().catch(e => e); + assert.ok(err); + assert.ok(err.errors['myInt']); + assert.equal(err.errors['myInt'].name, 'CastError'); + assert.equal( + err.errors['myInt'].message, + 'Cast to Int32 failed for value "-2147483649" (type number) at path "myInt"' + ); + }); + }); + }); + + describe('custom casters', () => { + const defaultCast = mongoose.Schema.Types.Int32.cast(); + + afterEach(() => { + mongoose.Schema.Types.Int32.cast(defaultCast); + }); + + it('supports cast disabled', async() => { + mongoose.Schema.Types.Int32.cast(false); + const schema = new Schema({ + myInt1: { + type: Schema.Types.Int32 + }, + myInt2: { + type: Schema.Types.Int32 + } + }); + const Test = mongoose.model('Test', schema); + const doc = new Test({ + myInt1: '52', + myInt2: 52 + }); + assert.strictEqual(doc.myInt1, undefined); + assert.strictEqual(doc.myInt2, 52); + + const err = await doc.validate().catch(e => e); + assert.ok(err); + assert.ok(err.errors['myInt1']); + }); + + it('supports custom cast', () => { + mongoose.Schema.Types.Int32.cast(v => { + if (isNaN(v)) { + return 0; + } + return defaultCast(v); + }); + const schema = new Schema({ + myInt: { + type: Schema.Types.Int32 + } + }); + + const Test = mongoose.model('Test', schema); + const doc = new Test({ + myInt: NaN + }); + assert.strictEqual(doc.myInt, 0); + }); + }); + + describe('mongoDB integration', function() { + let db; + let Test; + + before(async function() { + db = await start(); + + const schema = new Schema({ + myInt: Schema.Types.Int32 + }); + 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({ myInt: '42' }); + const doc = await Test.findOne({ myInt: { $type: 'number' } }); + assert.ok(doc); + assert.strictEqual(doc.myInt, 42); + }); + + it('is queryable as a BSON Int32 in MongoDB', async function() { + await Test.create({ myInt: '42' }); + const doc = await Test.findOne({ myInt: { $type: 'int' } }); + assert.ok(doc); + assert.strictEqual(doc.myInt, 42); + }); + + it('is NOT queryable as a BSON Double in MongoDB', async function() { + await Test.create({ myInt: '42' }); + const doc = await Test.findOne({ myInt: { $type: 'double' } }); + assert.equal(doc, undefined); + }); + }); + + it('can query with comparison operators', async function() { + await Test.create([ + { myInt: 1 }, + { myInt: 2 }, + { myInt: 3 }, + { myInt: 4 } + ]); + + let docs = await Test.find({ myInt: { $gte: 3 } }).sort({ myInt: 1 }); + assert.equal(docs.length, 2); + assert.deepStrictEqual(docs.map(doc => doc.myInt), [3, 4]); + + docs = await Test.find({ myInt: { $lt: 3 } }).sort({ myInt: -1 }); + assert.equal(docs.length, 2); + assert.deepStrictEqual(docs.map(doc => doc.myInt), [2, 1]); + }); + + it('supports populate()', async function() { + const parentSchema = new Schema({ + child: { + type: Schema.Types.Int32, + ref: 'Child' + } + }); + const childSchema = new Schema({ + _id: Schema.Types.Int32, + 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-int32-populate' }); + + const doc = await Parent.findById(_id).populate('child'); + assert.ok(doc); + assert.equal(doc.child.name, 'test-int32-populate'); + assert.equal(doc.child._id, 42); + }); + }); +}); diff --git a/test/types/schemaTypeOptions.test.ts b/test/types/schemaTypeOptions.test.ts index 5fc0e23a21d..71ad5c7b539 100644 --- a/test/types/schemaTypeOptions.test.ts +++ b/test/types/schemaTypeOptions.test.ts @@ -64,6 +64,7 @@ function defaultOptions() { expectType>(new Schema.Types.Buffer('none').defaultOptions); expectType>(new Schema.Types.Date('none').defaultOptions); expectType>(new Schema.Types.Decimal128('none').defaultOptions); + expectType>(new Schema.Types.Int32('none').defaultOptions); expectType>(new Schema.Types.DocumentArray('none').defaultOptions); expectType>(new Schema.Types.Map('none').defaultOptions); expectType>(new Schema.Types.Mixed('none').defaultOptions); diff --git a/types/inferschematype.d.ts b/types/inferschematype.d.ts index d73ad4cb81c..85d61a45fea 100644 --- a/types/inferschematype.d.ts +++ b/types/inferschematype.d.ts @@ -219,27 +219,29 @@ type IsSchemaTypeFromBuiltinClass = T extends (typeof String) ? true : T extends (typeof Schema.Types.Decimal128) ? true - : T extends (typeof Schema.Types.String) + : T extends (typeof Schema.Types.Int32) ? true - : T extends (typeof Schema.Types.Number) - ? true - : T extends (typeof Schema.Types.Date) + : T extends (typeof Schema.Types.String) ? true - : T extends (typeof Schema.Types.Boolean) + : T extends (typeof Schema.Types.Number) ? true - : T extends (typeof Schema.Types.Buffer) + : T extends (typeof Schema.Types.Date) ? true - : T extends Types.ObjectId + : T extends (typeof Schema.Types.Boolean) ? true - : T extends Types.Decimal128 + : T extends (typeof Schema.Types.Buffer) ? true - : T extends Buffer - ? true - : T extends NativeDate + : T extends Types.ObjectId ? true - : T extends (typeof Schema.Types.Mixed) + : T extends Types.Decimal128 ? true - : IfEquals; + : T extends Buffer + ? true + : T extends NativeDate + ? true + : T extends (typeof Schema.Types.Mixed) + ? true + : IfEquals; /** * @summary Resolve path type by returning the corresponding type. @@ -302,18 +304,18 @@ type ResolvePathType extends true ? Types.Decimal128 : IfEquals extends true ? Types.Decimal128 : - IfEquals extends true ? bigint : - IfEquals extends true ? bigint : - PathValueType extends 'bigint' | 'BigInt' | typeof Schema.Types.BigInt | typeof BigInt ? bigint : - PathValueType extends 'uuid' | 'UUID' | typeof Schema.Types.UUID ? Buffer : - IfEquals extends true ? Buffer : - PathValueType extends MapConstructor | 'Map' ? Map> : - IfEquals extends true ? Map> : - PathValueType extends ArrayConstructor ? any[] : - PathValueType extends typeof Schema.Types.Mixed ? any: - IfEquals extends true ? any: - IfEquals extends true ? any: - PathValueType extends typeof SchemaType ? PathValueType['prototype'] : - PathValueType extends Record ? ObtainDocumentType : - unknown, - TypeHint>; + IfEquals extends true ? bigint : + IfEquals extends true ? bigint : + PathValueType extends 'bigint' | 'BigInt' | typeof Schema.Types.BigInt | typeof BigInt ? bigint : + PathValueType extends 'uuid' | 'UUID' | typeof Schema.Types.UUID ? Buffer : + IfEquals extends true ? Buffer : + PathValueType extends MapConstructor | 'Map' ? Map> : + IfEquals extends true ? Map> : + PathValueType extends ArrayConstructor ? any[] : + PathValueType extends typeof Schema.Types.Mixed ? any: + IfEquals extends true ? any: + IfEquals extends true ? any: + PathValueType extends typeof SchemaType ? PathValueType['prototype'] : + PathValueType extends Record ? ObtainDocumentType : + unknown, + TypeHint>; diff --git a/types/schematypes.d.ts b/types/schematypes.d.ts index f10b633eec8..3e87f32096f 100644 --- a/types/schematypes.d.ts +++ b/types/schematypes.d.ts @@ -12,6 +12,16 @@ declare module 'mongoose' { */ type Decimal128 = Schema.Types.Decimal128; + + /** + * The Mongoose Int32 [SchemaType](/docs/schematypes.html). Used for + * declaring paths in your schema that should be + * 32-bit integers + * Do not use this to create a new Int32 instance, use `mongoose.Types.Int32` + * instead. + */ + type Int32 = Schema.Types.Int32; + /** * The Mongoose Mixed [SchemaType](/docs/schematypes.html). Used for * declaring paths in your schema that Mongoose's change tracking, casting, @@ -387,6 +397,14 @@ declare module 'mongoose' { defaultOptions: Record; } + class Int32 extends SchemaType { + /** This schema type's name, to defend against minifiers that mangle function names. */ + static schemaName: 'Int32'; + + /** Default options for this SchemaType */ + defaultOptions: Record; + } + class DocumentArray extends SchemaType implements AcceptsDiscriminator { /** This schema type's name, to defend against minifiers that mangle function names. */ static schemaName: 'DocumentArray';