diff --git a/.eslintrc.js b/.eslintrc.js
index 5377c64c6c2..213f2d57c1b 100644
--- a/.eslintrc.js
+++ b/.eslintrc.js
@@ -116,7 +116,8 @@ module.exports = {
},
env: {
node: true,
- es6: true
+ es6: true,
+ es2020: true
},
rules: {
'comma-style': 'error',
diff --git a/docs/schematypes.md b/docs/schematypes.md
index 1c3fb9a34f7..37525c969b6 100644
--- a/docs/schematypes.md
+++ b/docs/schematypes.md
@@ -54,6 +54,7 @@ Check out [Mongoose's plugins search](http://plugins.mongoosejs.io) to find plug
- [Map](#maps)
- [Schema](#schemas)
- [UUID](#uuid)
+- [BigInt](#bigint)
Example
@@ -632,6 +633,21 @@ const schema = new mongoose.Schema({
});
```
+BigInt
+
+Mongoose supports [JavaScript BigInts](https://thecodebarbarian.com/an-overview-of-bigint-in-node-js.html) as a SchemaType.
+BigInts are stored as [64-bit integers in MongoDB (BSON type "long")](https://www.mongodb.com/docs/manual/reference/bson-types/).
+
+```javascript
+const questionSchema = new Schema({
+ answer: BigInt
+});
+const Question = mongoose.model('Question', questionSchema);
+
+const question = new Question({ answer: 42n });
+typeof question.answer; // 'bigint'
+```
+
Getters are like virtuals for paths defined in your schema. For example,
diff --git a/docs/tutorials/lean.md b/docs/tutorials/lean.md
index 8edd3438ba7..baa830f8fe5 100644
--- a/docs/tutorials/lean.md
+++ b/docs/tutorials/lean.md
@@ -10,6 +10,7 @@ In this tutorial, you'll learn more about the tradeoffs of using `lean()`.
* [Lean and Populate](#lean-and-populate)
* [When to Use Lean](#when-to-use-lean)
* [Plugins](#plugins)
+* [BigInts](#bigints)
@@ -140,3 +141,12 @@ schema.virtual('lowercase', function() {
this.get('name'); // Crashes because `this` is not a Mongoose document.
});
```
+
+## BigInts
+
+By default, the MongoDB Node driver converts longs stored in MongoDB into JavaScript numbers, **not** [BigInts](https://thecodebarbarian.com/an-overview-of-bigint-in-node-js.html).
+Set the `useBigInt64` option on your `lean()` queries to inflate longs into BigInts.
+
+```acquit
+[require:Lean Tutorial.*bigint]
+```
\ No newline at end of file
diff --git a/lib/cast/bigint.js b/lib/cast/bigint.js
new file mode 100644
index 00000000000..7191311d647
--- /dev/null
+++ b/lib/cast/bigint.js
@@ -0,0 +1,31 @@
+'use strict';
+
+const assert = require('assert');
+
+/**
+ * Given a value, cast it to a BigInt, 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` is not one of the allowed values
+ * @api private
+ */
+
+module.exports = function castBigInt(val) {
+ if (val == null) {
+ return val;
+ }
+ if (val === '') {
+ return null;
+ }
+ if (typeof val === 'bigint') {
+ return val;
+ }
+
+ if (typeof val === 'string' || typeof val === 'number') {
+ return BigInt(val);
+ }
+
+ assert.ok(false);
+};
diff --git a/lib/query.js b/lib/query.js
index 2e93d237503..84ccc5068cb 100644
--- a/lib/query.js
+++ b/lib/query.js
@@ -1525,12 +1525,13 @@ Query.prototype.getOptions = function() {
* - [timestamps](https://mongoosejs.com/docs/guide.html#timestamps): If `timestamps` is set in the schema, set this option to `false` to skip timestamps for that particular update. Has no effect if `timestamps` is not enabled in the schema options.
* - overwriteDiscriminatorKey: allow setting the discriminator key in the update. Will use the correct discriminator schema if the update changes the discriminator key.
*
- * The following options are only for `find()`, `findOne()`, `findById()`, `findOneAndUpdate()`, and `findByIdAndUpdate()`:
+ * The following options are only for `find()`, `findOne()`, `findById()`, `findOneAndUpdate()`, `findOneAndReplace()`, `findOneAndDelete()`, and `findByIdAndUpdate()`:
*
* - [lean](https://mongoosejs.com/docs/api/query.html#Query.prototype.lean())
* - [populate](https://mongoosejs.com/docs/populate.html)
* - [projection](https://mongoosejs.com/docs/api/query.html#Query.prototype.projection())
* - sanitizeProjection
+ * - useBigInt64
*
* The following options are only for all operations **except** `updateOne()`, `updateMany()`, `deleteOne()`, and `deleteMany()`:
*
diff --git a/lib/schema.js b/lib/schema.js
index 7b5ef0ece89..a9014caadcc 100644
--- a/lib/schema.js
+++ b/lib/schema.js
@@ -2665,6 +2665,8 @@ module.exports = exports = Schema;
* - [Date](https://mongoosejs.com/docs/schematypes.html#dates)
* - [ObjectId](https://mongoosejs.com/docs/schematypes.html#objectids) | Oid
* - [Mixed](https://mongoosejs.com/docs/schematypes.html#mixed)
+ * - [UUID](https://mongoosejs.com/docs/schematypes.html#uuid)
+ * - [BigInt](https://mongoosejs.com/docs/schematypes.html#bigint)
*
* 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
new file mode 100644
index 00000000000..24cff4ea9ed
--- /dev/null
+++ b/lib/schema/bigint.js
@@ -0,0 +1,221 @@
+'use strict';
+
+/*!
+ * Module dependencies.
+ */
+
+const CastError = require('../error/cast');
+const SchemaType = require('../schematype');
+const castBigInt = require('../cast/bigint');
+const utils = require('../utils');
+
+/**
+ * BigInt SchemaType constructor.
+ *
+ * @param {String} path
+ * @param {Object} options
+ * @inherits SchemaType
+ * @api public
+ */
+
+function SchemaBigInt(path, options) {
+ SchemaType.call(this, path, options, 'BigInt');
+}
+
+/**
+ * This schema type's name, to defend against minifiers that mangle
+ * function names.
+ *
+ * @api public
+ */
+SchemaBigInt.schemaName = 'BigInt';
+
+SchemaBigInt.defaultOptions = {};
+
+/*!
+ * Inherits from SchemaType.
+ */
+SchemaBigInt.prototype = Object.create(SchemaType.prototype);
+SchemaBigInt.prototype.constructor = SchemaBigInt;
+
+/*!
+ * ignore
+ */
+
+SchemaBigInt._cast = castBigInt;
+
+/**
+ * Sets a default option for all BigInt instances.
+ *
+ * #### Example:
+ *
+ * // Make all bigints required by default
+ * mongoose.Schema.BigInt.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
+ */
+
+SchemaBigInt.set = SchemaType.set;
+
+/**
+ * Get/set the function used to cast arbitrary values to booleans.
+ *
+ * #### Example:
+ *
+ * // Make Mongoose cast empty string '' to false.
+ * const original = mongoose.Schema.BigInt.cast();
+ * mongoose.Schema.BigInt.cast(v => {
+ * if (v === '') {
+ * return false;
+ * }
+ * return original(v);
+ * });
+ *
+ * // Or disable casting entirely
+ * mongoose.Schema.BigInt.cast(false);
+ *
+ * @param {Function} caster
+ * @return {Function}
+ * @function get
+ * @static
+ * @api public
+ */
+
+SchemaBigInt.cast = function cast(caster) {
+ if (arguments.length === 0) {
+ return this._cast;
+ }
+ if (caster === false) {
+ caster = this._defaultCaster;
+ }
+ this._cast = caster;
+
+ return this._cast;
+};
+
+/*!
+ * ignore
+ */
+
+SchemaBigInt._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
+ */
+
+SchemaBigInt.checkRequired = SchemaType.checkRequired;
+
+/**
+ * Check if the given value satisfies a required validator.
+ *
+ * @param {Any} value
+ * @return {Boolean}
+ * @api public
+ */
+
+SchemaBigInt.prototype.checkRequired = function(value) {
+ return this.constructor._checkRequired(value);
+};
+
+/**
+ * Casts to bigint
+ *
+ * @param {Object} value
+ * @param {Object} model this value is optional
+ * @api private
+ */
+
+SchemaBigInt.prototype.cast = function(value) {
+ let castBigInt;
+ if (typeof this._castFunction === 'function') {
+ castBigInt = this._castFunction;
+ } else if (typeof this.constructor.cast === 'function') {
+ castBigInt = this.constructor.cast();
+ } else {
+ castBigInt = SchemaBigInt.cast();
+ }
+
+ try {
+ return castBigInt(value);
+ } catch (error) {
+ throw new CastError('BigInt', value, this.path, error, this);
+ }
+};
+
+/*!
+ * ignore
+ */
+
+SchemaBigInt.$conditionalHandlers = utils.options(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
+ */
+
+SchemaBigInt.prototype.castForQuery = function($conditional, val, context) {
+ let handler;
+ if ($conditional != null) {
+ handler = SchemaBigInt.$conditionalHandlers[$conditional];
+
+ if (handler) {
+ return handler.call(this, val);
+ }
+
+ return this.applySetters(null, val, context);
+ }
+
+ return this.applySetters(val, context);
+};
+
+/**
+ *
+ * @api private
+ */
+
+SchemaBigInt.prototype._castNullish = function _castNullish(v) {
+ if (typeof v === 'undefined') {
+ return v;
+ }
+ const castBigInt = typeof this.constructor.cast === 'function' ?
+ this.constructor.cast() :
+ SchemaBigInt.cast();
+ if (castBigInt == null) {
+ return v;
+ }
+ return v;
+};
+
+/*!
+ * Module exports.
+ */
+
+module.exports = SchemaBigInt;
diff --git a/lib/schema/index.js b/lib/schema/index.js
index f3eb9851ea6..4bd4d8d5934 100644
--- a/lib/schema/index.js
+++ b/lib/schema/index.js
@@ -5,30 +5,19 @@
'use strict';
-exports.String = require('./string');
-
-exports.Number = require('./number');
-
-exports.Boolean = require('./boolean');
-
-exports.DocumentArray = require('./documentarray');
-
-exports.Subdocument = require('./SubdocumentPath');
-
exports.Array = require('./array');
-
+exports.Boolean = require('./boolean');
+exports.BigInt = require('./bigint');
exports.Buffer = require('./buffer');
-
exports.Date = require('./date');
-
-exports.ObjectId = require('./objectid');
-
-exports.Mixed = require('./mixed');
-
exports.Decimal128 = exports.Decimal = require('./decimal128');
-
+exports.DocumentArray = require('./documentarray');
exports.Map = require('./map');
-
+exports.Mixed = require('./mixed');
+exports.Number = require('./number');
+exports.ObjectId = require('./objectid');
+exports.String = require('./string');
+exports.Subdocument = require('./SubdocumentPath');
exports.UUID = require('./uuid');
// alias
diff --git a/lib/types/index.js b/lib/types/index.js
index 1f67a5f9b8b..8babdf4b4d1 100644
--- a/lib/types/index.js
+++ b/lib/types/index.js
@@ -19,4 +19,4 @@ exports.Map = require('./map');
exports.Subdocument = require('./subdocument');
-exports.UUID = require('mongodb').BSON.UUID;
+exports.UUID = require('./uuid');
diff --git a/lib/types/uuid.js b/lib/types/uuid.js
new file mode 100644
index 00000000000..fc9db855f7d
--- /dev/null
+++ b/lib/types/uuid.js
@@ -0,0 +1,13 @@
+/**
+ * UUID type constructor
+ *
+ * #### Example:
+ *
+ * const id = new mongoose.Types.UUID();
+ *
+ * @constructor UUID
+ */
+
+'use strict';
+
+module.exports = require('bson').UUID;
diff --git a/package.json b/package.json
index ee75c8d0026..f20351d273e 100644
--- a/package.json
+++ b/package.json
@@ -19,9 +19,9 @@
],
"license": "MIT",
"dependencies": {
- "bson": "^5.0.1",
+ "bson": "^5.2.0",
"kareem": "2.5.1",
- "mongodb": "5.1.0",
+ "mongodb": "5.3.0",
"mpath": "0.9.0",
"mquery": "5.0.0",
"ms": "2.1.3",
diff --git a/test/bigint.test.js b/test/bigint.test.js
new file mode 100644
index 00000000000..e3d00418e2c
--- /dev/null
+++ b/test/bigint.test.js
@@ -0,0 +1,167 @@
+'use strict';
+
+const assert = require('assert');
+const start = require('./common');
+
+const mongoose = start.mongoose;
+const Schema = mongoose.Schema;
+
+describe('BigInt', function() {
+ beforeEach(() => mongoose.deleteModel(/Test/));
+
+ it('is a valid schema type', function() {
+ const schema = new Schema({
+ myBigInt: BigInt
+ });
+ const Test = mongoose.model('Test', schema);
+
+ const doc = new Test({
+ myBigInt: 42n
+ });
+ assert.strictEqual(doc.myBigInt, 42n);
+ assert.equal(typeof doc.myBigInt, 'bigint');
+ });
+
+ it('casting from strings and numbers', function() {
+ const schema = new Schema({
+ bigint1: {
+ type: BigInt
+ },
+ bigint2: 'BigInt'
+ });
+ const Test = mongoose.model('Test', schema);
+
+ const doc = new Test({
+ bigint1: 42,
+ bigint2: '997'
+ });
+ assert.strictEqual(doc.bigint1, 42n);
+ assert.strictEqual(doc.bigint2, 997n);
+ });
+
+ it('handles cast errors', async function() {
+ const schema = new Schema({
+ bigint: 'BigInt'
+ });
+ const Test = mongoose.model('Test', schema);
+
+ const doc = new Test({
+ bigint: 'foo bar'
+ });
+ assert.strictEqual(doc.bigint, undefined);
+
+ const err = await doc.validate().then(() => null, err => err);
+ assert.ok(err);
+ assert.ok(err.errors['bigint']);
+ assert.equal(err.errors['bigint'].name, 'CastError');
+ assert.equal(
+ err.errors['bigint'].message,
+ 'Cast to BigInt failed for value "foo bar" (type string) at path "bigint" because of "SyntaxError"'
+ );
+ });
+
+ it('supports required', async function() {
+ const schema = new Schema({
+ bigint: {
+ type: BigInt,
+ required: true
+ }
+ });
+ const Test = mongoose.model('Test', schema);
+
+ const doc = new Test({
+ bigint: null
+ });
+
+ const err = await doc.validate().then(() => null, err => err);
+ assert.ok(err);
+ assert.ok(err.errors['bigint']);
+ assert.equal(err.errors['bigint'].name, 'ValidatorError');
+ assert.equal(
+ err.errors['bigint'].message,
+ 'Path `bigint` is required.'
+ );
+ });
+
+ describe('MongoDB integration', function() {
+ let db;
+ let Test;
+
+ before(async function() {
+ db = await start();
+
+ const schema = new Schema({
+ myBigInt: BigInt
+ });
+ db.deleteModel(/Test/);
+ Test = db.model('Test', schema);
+ });
+
+ after(async function() {
+ await db.close();
+ });
+
+ beforeEach(async() => {
+ await Test.deleteMany({});
+ });
+
+ it('is stored as a long in MongoDB', async function() {
+ await Test.create({ myBigInt: 42n });
+
+ const doc = await Test.findOne({ myBigInt: { $type: 'long' } });
+ assert.ok(doc);
+ assert.strictEqual(doc.myBigInt, 42n);
+ });
+
+ it('becomes a bigint with lean using useBigInt64', async function() {
+ await Test.create({ myBigInt: 7n });
+
+ const doc = await Test.
+ findOne({ myBigInt: 7n }).
+ setOptions({ useBigInt64: true }).
+ lean();
+ assert.ok(doc);
+ assert.strictEqual(doc.myBigInt, 7n);
+ });
+
+ it('can query with comparison operators', async function() {
+ await Test.create([
+ { myBigInt: 1n },
+ { myBigInt: 2n },
+ { myBigInt: 3n },
+ { myBigInt: 4n }
+ ]);
+
+ let docs = await Test.find({ myBigInt: { $gte: 3n } }).sort({ myBigInt: 1 });
+ assert.equal(docs.length, 2);
+ assert.deepStrictEqual(docs.map(doc => doc.myBigInt), [3n, 4n]);
+
+ docs = await Test.find({ myBigInt: { $lt: 3n } }).sort({ myBigInt: -1 });
+ assert.equal(docs.length, 2);
+ assert.deepStrictEqual(docs.map(doc => doc.myBigInt), [2n, 1n]);
+ });
+
+ it('supports populate()', async function() {
+ const parentSchema = new Schema({
+ child: {
+ type: BigInt,
+ ref: 'Child'
+ }
+ });
+ const childSchema = new Schema({
+ _id: BigInt,
+ name: String
+ });
+ const Parent = db.model('Parent', parentSchema);
+ const Child = db.model('Child', childSchema);
+
+ const { _id } = await Parent.create({ child: 42n });
+ await Child.create({ _id: 42n, name: 'test-bigint-populate' });
+
+ const doc = await Parent.findById(_id).populate('child');
+ assert.ok(doc);
+ assert.equal(doc.child.name, 'test-bigint-populate');
+ assert.equal(doc.child._id, 42n);
+ });
+ });
+});
diff --git a/test/docs/lean.test.js b/test/docs/lean.test.js
index 51ca5458cff..4512c7ced7f 100644
--- a/test/docs/lean.test.js
+++ b/test/docs/lean.test.js
@@ -203,4 +203,35 @@ describe('Lean Tutorial', function() {
assert.equal(group.members[1].name, 'Kira Nerys');
// acquit:ignore:end
});
+
+ it('bigint', async function() {
+ const Person = mongoose.model('Person', new mongoose.Schema({
+ name: String,
+ age: BigInt
+ }));
+ // acquit:ignore:start
+ await Person.deleteMany({});
+ // acquit:ignore:end
+ // Mongoose will convert `age` to a BigInt
+ const { age } = await Person.create({ name: 'Benjamin Sisko', age: 37 });
+ typeof age; // 'bigint'
+
+ // By default, if you store a document with a BigInt property in MongoDB and you
+ // load the document with `lean()`, the BigInt property will be a number
+ let person = await Person.findOne({ name: 'Benjamin Sisko' }).lean();
+ typeof person.age; // 'number'
+ // acquit:ignore:start
+ assert.equal(typeof person.age, 'number');
+ assert.equal(person.age, 37);
+ // acquit:ignore:end
+
+ // Set the `useBigInt64` option to opt in to converting MongoDB longs to BigInts.
+ person = await Person.findOne({ name: 'Benjamin Sisko' }).
+ setOptions({ useBigInt64: true }).
+ lean();
+ typeof person.age; // 'bigint'
+ // acquit:ignore:start
+ assert.equal(typeof person.age, 'bigint');
+ // acquit:ignore:end
+ });
});
diff --git a/test/schema.uuid.test.js b/test/schema.uuid.test.js
index 864c276ebb0..02728a44296 100644
--- a/test/schema.uuid.test.js
+++ b/test/schema.uuid.test.js
@@ -149,6 +149,28 @@ describe('SchemaUUID', function() {
await pop.save();
});
+
+ it('handles built-in UUID type (gh-13103)', async function() {
+ const schema = new Schema({
+ _id: {
+ type: Schema.Types.UUID
+ }
+ }, { _id: false });
+
+ db.deleteModel(/Test/);
+ const Test = db.model('Test', schema);
+
+ const uuid = new mongoose.Types.UUID();
+ let { _id } = await Test.create({ _id: uuid });
+ assert.ok(_id);
+ assert.equal(typeof _id, 'string');
+ assert.equal(_id, uuid.toString());
+
+ ({ _id } = await Test.findById(uuid));
+ assert.ok(_id);
+ assert.equal(typeof _id, 'string');
+ assert.equal(_id, uuid.toString());
+ });
// the following are TODOs based on SchemaUUID.prototype.$conditionalHandlers which are not tested yet
it('should work with $bits* operators');
diff --git a/test/types/lean.test.ts b/test/types/lean.test.ts
index c9b922b541b..5a0d6aaaabd 100644
--- a/test/types/lean.test.ts
+++ b/test/types/lean.test.ts
@@ -125,3 +125,21 @@ async function _11767() {
// expectError(examFound2Obj.questions[0].populated);
expectType(examFound2Obj.questions[0].answers);
}
+
+async function gh13010() {
+ const schema = new Schema({
+ name: { required: true, type: Map, of: String }
+ });
+
+ const CountryModel = model('Country', schema);
+
+ await CountryModel.create({
+ name: {
+ en: 'Croatia',
+ ru: 'Хорватия'
+ }
+ });
+
+ const country = await CountryModel.findOne().lean().orFail().exec();
+ expectType>(country.name);
+}
diff --git a/types/index.d.ts b/types/index.d.ts
index 39fb9eadfa8..377fe916d09 100644
--- a/types/index.d.ts
+++ b/types/index.d.ts
@@ -560,13 +560,13 @@ declare module 'mongoose' {
export type UpdateQuery = _UpdateQuery & AnyObject;
export type FlattenMaps = {
- [K in keyof T]: T[K] extends Map
- ? AnyObject : T[K] extends TreatAsPrimitives
+ [K in keyof T]: T[K] extends Map
+ ? Record : T[K] extends TreatAsPrimitives
? T[K] : FlattenMaps;
};
export type actualPrimitives = string | boolean | number | bigint | symbol | null | undefined;
- export type TreatAsPrimitives = actualPrimitives | NativeDate | RegExp | symbol | Error | BigInt | Types.ObjectId;
+ export type TreatAsPrimitives = actualPrimitives | NativeDate | RegExp | symbol | Error | BigInt | Types.ObjectId | Buffer | Function;
export type SchemaDefinitionType = T extends Document ? Omit> : T;
diff --git a/types/query.d.ts b/types/query.d.ts
index 15b973bbb4f..d08a1e345cc 100644
--- a/types/query.d.ts
+++ b/types/query.d.ts
@@ -162,6 +162,7 @@ declare module 'mongoose' {
*/
timestamps?: boolean | QueryTimestampsConfig;
upsert?: boolean;
+ useBigInt64?: boolean;
writeConcern?: mongodb.WriteConcern;
[other: string]: any;
@@ -430,7 +431,7 @@ declare module 'mongoose' {
j(val: boolean | null): this;
/** Sets the lean option. */
- lean[] : Require_id>(val?: boolean | any): QueryWithHelpers;
+ lean>[] : Require_id>>(val?: boolean | any): QueryWithHelpers;
/** Specifies the maximum number of documents the query will return. */
limit(val: number): this;