diff --git a/CHANGELOG.md b/CHANGELOG.md
index 76354c8664c..5666e039dd1 100644
--- a/CHANGELOG.md
+++ b/CHANGELOG.md
@@ -1,3 +1,30 @@
+7.4.1 / 2023-07-24
+==================
+ * fix(document): correctly clean up nested subdocs modified state on save() #13644 #13609
+ * fix(schema): avoid propagating toObject.transform and toJSON.transform option to implicitly created schemas #13634 #13599
+ * fix: prevent schema options overwriting user defined writeConcern #13612 #13592
+ * types: correctly handle pre('deleteOne', { document: true }) #13632
+ * types(schema): handle type: Schema.Types.Map in TypeScript #13628
+ * types: Add inline comment to to tell the default value of the runValidator flag in the queryOptions types #13636 [omran95](https://github.com/omran95)
+ * docs: rework several code examples that still use callbacks #13635 #13616
+ * docs: remove callbacks from validation description #13638 #13501
+
+7.4.0 / 2023-07-18
+==================
+ * perf: speed up mapOfSubdocs benchmark by 4x by avoiding unnecessary O(n^2) loop in getPathsToValidate() #13614
+ * feat: upgrade to MongoDB Node.js driver 5.7.0 #13591
+ * feat: support generating custom cast error message with a function #13608 #3162
+ * feat(query): support MongoDB driver's includeResultMetadata option for findOneAndUpdate #13584 #13539
+ * feat(connection): add Connection.prototype.removeDb() for removing a related connection #13580 #11821
+ * feat(query): delay converting documents into POJOs until query execution, allow querying subdocuments with defaults disabled #13522
+ * feat(model): add option "aggregateErrors" for create() #13544 [hasezoey](https://github.com/hasezoey)
+ * feat(schema): add collectionOptions option to schemas #13513
+ * fix: move all MongoDB-specific connection logic into driver layer, add createClient() method to handle creating MongoClient #13542
+ * fix(document): allow setting keys with dots in mixed paths underneath nested paths #13536
+ * types: augment bson.ObjectId instead of adding on own type #13515 #12537 [hasezoey](https://github.com/hasezoey)
+ * docs(guide): fix md lint #13593 [hasezoey](https://github.com/hasezoey)
+ * docs: changed the code from 'await author.save()' to 'await story1.save()' #13596 [SomSingh23](https://github.com/SomSingh23)
+
6.11.4 / 2023-07-17
===================
* perf: speed up mapOfSubdocs benchmark by 4x by avoiding unnecessary O(n^2) loop in getPathsToValidate() #13614
diff --git a/docs/async-await.md b/docs/async-await.md
index 1752ebbef27..241d938a783 100644
--- a/docs/async-await.md
+++ b/docs/async-await.md
@@ -11,27 +11,7 @@ This is especially helpful for avoiding callback hell when executing multiple as
Each of the three functions below retrieves a record from the database, updates it, and prints the updated record to the console.
```javascript
-// Works.
-function callbackUpdate() {
- MyModel.findOne({ firstName: 'franklin', lastName: 'roosevelt' }, function(err, doc) {
- if (err) {
- handleError(err);
- }
-
- doc.middleName = 'delano';
-
- doc.save(function(err, updatedDoc) {
- if (err) {
- handleError(err);
- }
-
- // Final logic is 2 callbacks deep
- console.log(updatedDoc);
- });
- });
-}
-
-// Better.
+// Using promise chaining
function thenUpdate() {
MyModel.findOne({ firstName: 'franklin', lastName: 'roosevelt' })
.then(function(doc) {
@@ -44,7 +24,7 @@ function thenUpdate() {
});
}
-// Best?
+// Using async/await
async function awaitUpdate() {
try {
const doc = await MyModel.findOne({
diff --git a/docs/deprecations.md b/docs/deprecations.md
index f96a7791b89..08003401a0b 100644
--- a/docs/deprecations.md
+++ b/docs/deprecations.md
@@ -9,108 +9,30 @@ cause any problems for your application. Please [report any issues on GitHub](ht
To fix all deprecation warnings, follow the below steps:
-* Replace `update()` with `updateOne()`, `updateMany()`, or `replaceOne()`
-* Replace `remove()` with `deleteOne()` or `deleteMany()`.
-* Replace `count()` with `countDocuments()`, unless you want to count how many documents are in the whole collection (no filter). In the latter case, use `estimatedDocumentCount()`.
+* Replace `rawResult: true` with `includeResultMetadata: false` in `findOneAndUpdate()`, `findOneAndReplace()`, `findOneAndDelete()` calls.
Read below for more a more detailed description of each deprecation warning.
-
+
-The MongoDB driver's [`remove()` function](http://mongodb.github.io/node-mongodb-native/3.1/api/Collection.html#remove) is deprecated in favor of `deleteOne()` and `deleteMany()`. This is to comply with
-the [MongoDB CRUD specification](https://github.com/mongodb/specifications/blob/master/source/crud/crud.rst),
-which aims to provide a consistent API for CRUD operations across all MongoDB
-drivers.
-
-```txt
-DeprecationWarning: collection.remove is deprecated. Use deleteOne,
-deleteMany, or bulkWrite instead.
-```
-
-To remove this deprecation warning, replace any usage of `remove()` with
-`deleteMany()`, *unless* you specify the [`single` option to `remove()`](api/model.html#model_Model-remove). The `single`
-option limited `remove()` to deleting at most one document, so you should
-replace `remove(filter, { single: true })` with `deleteOne(filter)`.
+As of Mongoose 7.4.0, the `rawResult` option to `findOneAndUpdate()` is deprecated.
+You should instead use the `includeResultMetadata` option, which the MongoDB Node.js driver's new option that replaces `rawResult`.
```javascript
// Replace this:
-MyModel.remove({ foo: 'bar' });
-// With this:
-MyModel.deleteMany({ foo: 'bar' });
-
-// Replace this:
-MyModel.remove({ answer: 42 }, { single: true });
-// With this:
-MyModel.deleteOne({ answer: 42 });
-```
+const doc = await Test.findOneAndUpdate(
+ { name: 'Test' },
+ { name: 'Test Testerson' },
+ { rawResult: true }
+);
-
-
-Like `remove()`, the [`update()` function](api/model.html#model_Model-update) is deprecated in favor
-of the more explicit [`updateOne()`](api/model.html#model_Model-updateOne), [`updateMany()`](api/model.html#model_Model-updateMany), and [`replaceOne()`](api/model.html#model_Model-replaceOne) functions. You should replace
-`update()` with `updateOne()`, unless you use the [`multi` or `overwrite` options](api/model.html#model_Model-update).
-
-```txt
-collection.update is deprecated. Use updateOne, updateMany, or bulkWrite
-instead.
-```
-
-```javascript
-// Replace this:
-MyModel.update({ foo: 'bar' }, { answer: 42 });
// With this:
-MyModel.updateOne({ foo: 'bar' }, { answer: 42 });
-
-// If you use `overwrite: true`, you should use `replaceOne()` instead:
-MyModel.update(filter, update, { overwrite: true });
-// Replace with this:
-MyModel.replaceOne(filter, update);
-
-// If you use `multi: true`, you should use `updateMany()` instead:
-MyModel.update(filter, update, { multi: true });
-// Replace with this:
-MyModel.updateMany(filter, update);
+const doc = await Test.findOneAndUpdate(
+ { name: 'Test' },
+ { name: 'Test Testerson' },
+ { includeResultMetadata: false }
+);
```
-
-
-The MongoDB server has deprecated the `count()` function in favor of two
-separate functions, [`countDocuments()`](api/query.html#Query.prototype.countDocuments()) and
-[`estimatedDocumentCount()`](api/query.html#Query.prototype.estimatedDocumentCount()).
-
-```txt
-DeprecationWarning: collection.count is deprecated, and will be removed in a future version. Use collection.countDocuments or collection.estimatedDocumentCount instead
-```
-
-The difference between the two is `countDocuments()` can accept a filter
-parameter like [`find()`](api/query.html#Query.prototype.find()). The `estimatedDocumentCount()`
-function is faster, but can only tell you the total number of documents in
-a collection. You cannot pass a `filter` to `estimatedDocumentCount()`.
-
-To migrate, replace `count()` with `countDocuments()` *unless* you do not
-pass any arguments to `count()`. If you use `count()` to count all documents
-in a collection as opposed to counting documents that match a query, use
-`estimatedDocumentCount()` instead of `countDocuments()`.
-
-```javascript
-// Replace this:
-MyModel.count({ answer: 42 });
-// With this:
-MyModel.countDocuments({ answer: 42 });
-
-// If you're counting all documents in the collection, use
-// `estimatedDocumentCount()` instead.
-MyModel.count();
-// Replace with:
-MyModel.estimatedDocumentCount();
-
-// Replace this:
-MyModel.find({ answer: 42 }).count().exec();
-// With this:
-MyModel.find({ answer: 42 }).countDocuments().exec();
-
-// Replace this:
-MyModel.find().count().exec();
-// With this, since there's no filter
-MyModel.find().estimatedDocumentCount().exec();
-```
+The `rawResult` option only affects Mongoose; the MongoDB Node.js driver still returns the full result metadata, Mongoose just parses out the raw document.
+The `includeResultMetadata` option also tells the MongoDB Node.js driver to only return the document, not the full `ModifyResult` object.
diff --git a/docs/faq.md b/docs/faq.md
index fe6d4146e71..8215347b6a7 100644
--- a/docs/faq.md
+++ b/docs/faq.md
@@ -79,9 +79,8 @@ const schema = new mongoose.Schema({
});
const Model = db.model('Test', schema);
-Model.create([{ name: 'Val' }, { name: 'Val' }], function(err) {
- console.log(err); // No error, unless index was already built
-});
+// No error, unless index was already built
+await Model.create([{ name: 'Val' }, { name: 'Val' }]);
```
However, if you wait for the index to build using the `Model.on('index')` event, attempts to save duplicates will correctly error.
@@ -92,21 +91,12 @@ const schema = new mongoose.Schema({
});
const Model = db.model('Test', schema);
-Model.on('index', function(err) { // <-- Wait for model's indexes to finish
- assert.ifError(err);
- Model.create([{ name: 'Val' }, { name: 'Val' }], function(err) {
- console.log(err);
- });
-});
-
-// Promise based alternative. `init()` returns a promise that resolves
-// when the indexes have finished building successfully. The `init()`
+// Wait for model's indexes to finish. The `init()`
// function is idempotent, so don't worry about triggering an index rebuild.
-Model.init().then(function() {
- Model.create([{ name: 'Val' }, { name: 'Val' }], function(err) {
- console.log(err);
- });
-});
+await Model.init();
+
+// Throws a duplicate key error
+await Model.create([{ name: 'Val' }, { name: 'Val' }]);
```
MongoDB persists indexes, so you only need to rebuild indexes if you're starting
diff --git a/docs/guide.md b/docs/guide.md
index 3d4dfbf9f19..7de820affa0 100644
--- a/docs/guide.md
+++ b/docs/guide.md
@@ -526,6 +526,7 @@ Valid options:
* [skipVersioning](#skipVersioning)
* [timestamps](#timestamps)
* [storeSubdocValidationError](#storeSubdocValidationError)
+* [collectionOptions](#collectionOptions)
* [methods](#methods)
* [query](#query-helpers)
@@ -1399,6 +1400,30 @@ const Parent = mongoose.model('Parent', parentSchema);
new Parent({ child: {} }).validateSync().errors;
```
+
+
+Options like [`collation`](#collation) and [`capped`](#capped) affect the options Mongoose passes to MongoDB when creating a new collection.
+Mongoose schemas support most [MongoDB `createCollection()` options](https://www.mongodb.com/docs/manual/reference/method/db.createCollection/), but not all.
+You can use the `collectionOptions` option to set any `createCollection()` options; Mongoose will use `collectionOptions` as the default values when calling `createCollection()` for your schema.
+
+```javascript
+const schema = new Schema({ name: String }, {
+ autoCreate: false,
+ collectionOptions: {
+ capped: true,
+ max: 1000
+ }
+});
+const Test = mongoose.model('Test', schema);
+
+// Equivalent to `createCollection({ capped: true, max: 1000 })`
+await Test.createCollection();
+```
+
Schemas have a [`loadClass()` method](api/schema.html#schema_Schema-loadClass)
diff --git a/docs/middleware.md b/docs/middleware.md
index 1c11bae093f..8c214cfc1e6 100644
--- a/docs/middleware.md
+++ b/docs/middleware.md
@@ -532,11 +532,10 @@ schema.post('update', function(error, res, next) {
});
const people = [{ name: 'Axl Rose' }, { name: 'Slash' }];
-Person.create(people, function(error) {
- Person.update({ name: 'Slash' }, { $set: { name: 'Axl Rose' } }, function(error) {
- // `error.message` will be "There was a duplicate key error"
- });
-});
+await Person.create(people);
+
+// Throws "There was a duplicate key error"
+await Person.update({ name: 'Slash' }, { $set: { name: 'Axl Rose' } });
```
Error handling middleware can transform an error, but it can't remove the
diff --git a/docs/queries.md b/docs/queries.md
index 1a9706d3c86..8ca6cf1345a 100644
--- a/docs/queries.md
+++ b/docs/queries.md
@@ -104,44 +104,16 @@ A full list of [Query helper functions can be found in the API docs](api/query.h
-Mongoose queries are **not** promises. They have a `.then()`
-function for [co](https://www.npmjs.com/package/co) and
-[async/await](http://thecodebarbarian.com/common-async-await-design-patterns-in-node.js.html)
-as a convenience. However, unlike promises, calling a query's `.then()`
-can execute the query multiple times.
-
-For example, the below code will execute 3 `updateMany()` calls, one
-because of the callback, and two because `.then()` is called twice.
+Mongoose queries are **not** promises.
+Queries are [thenables](https://masteringjs.io/tutorials/fundamentals/thenable), meaning they have a `.then()` method for [async/await](http://thecodebarbarian.com/common-async-await-design-patterns-in-node.js.html) as a convenience.
+However, unlike promises, calling a query's `.then()` executes the query, so calling `then()` multiple times will throw an error.
```javascript
-const q = MyModel.updateMany({}, { isDeleted: true }, function() {
- console.log('Update 1');
-});
-
-q.then(() => console.log('Update 2'));
-q.then(() => console.log('Update 3'));
-```
+const q = MyModel.updateMany({}, { isDeleted: true });
-Don't mix using callbacks and promises with queries, or you may end up
-with duplicate operations. That's because passing a callback to a query function
-immediately executes the query, and calling [`then()`](https://masteringjs.io/tutorials/fundamentals/then)
-executes the query again.
-
-Mixing promises and callbacks can lead to duplicate entries in arrays.
-For example, the below code inserts 2 entries into the `tags` array, **not** just 1.
-
-```javascript
-const BlogPost = mongoose.model('BlogPost', new Schema({
- title: String,
- tags: [String]
-}));
-
-// Because there's both `await` **and** a callback, this `updateOne()` executes twice
-// and thus pushes the same string into `tags` twice.
-const update = { $push: { tags: ['javascript'] } };
-await BlogPost.updateOne({ title: 'Introduction to Promises' }, update, (err, res) => {
- console.log(res);
-});
+await q.then(() => console.log('Update 2'));
+// Throws "Query was already executed: Test.updateMany({}, { isDeleted: true })"
+await q.then(() => console.log('Update 3'));
```
diff --git a/docs/validation.md b/docs/validation.md
index c0fc6d4aed1..43ae5ceaef6 100644
--- a/docs/validation.md
+++ b/docs/validation.md
@@ -109,19 +109,35 @@ thrown.
## Cast Errors
-Before running validators, Mongoose attempts to coerce values to the
-correct type. This process is called *casting* the document. If
-casting fails for a given path, the `error.errors` object will contain
-a `CastError` object.
+Before running validators, Mongoose attempts to coerce values to the correct type. This process is called *casting* the document.
+If casting fails for a given path, the `error.errors` object will contain a `CastError` object.
-Casting runs before validation, and validation does not run if casting
-fails. That means your custom validators may assume `v` is `null`,
-`undefined`, or an instance of the type specified in your schema.
+Casting runs before validation, and validation does not run if casting fails.
+That means your custom validators may assume `v` is `null`, `undefined`, or an instance of the type specified in your schema.
```acquit
[require:Cast Errors]
```
+By default, Mongoose cast error messages look like `Cast to Number failed for value "pie" at path "numWheels"`.
+You can overwrite Mongoose's default cast error message by the `cast` option on your SchemaType to a string as follows.
+
+```acquit
+[require:Cast Error Message Overwrite]
+```
+
+Mongoose's cast error message templating supports the following parameters:
+
+* `{PATH}`: the path that failed to cast
+* `{VALUE}`: a string representation of the value that failed to cast
+* `{KIND}`: the type that Mongoose attempted to cast to, like `'String'` or `'Number'`
+
+You can also define a function that Mongoose will call to get the cast error message as follows.
+
+```acquit
+[require:Cast Error Message Function Overwrite]
+```
+
## Global SchemaType Validation
In addition to defining custom validators on individual schema paths, you can also configure a custom validator to run on every instance of a given `SchemaType`.
diff --git a/lib/cast.js b/lib/cast.js
index 278cc1f00cf..61b1f8caf13 100644
--- a/lib/cast.js
+++ b/lib/cast.js
@@ -10,12 +10,12 @@ const Types = require('./schema/index');
const cast$expr = require('./helpers/query/cast$expr');
const castTextSearch = require('./schema/operators/text');
const get = require('./helpers/get');
-const getConstructorName = require('./helpers/getConstructorName');
const getSchemaDiscriminatorByValue = require('./helpers/discriminator/getSchemaDiscriminatorByValue');
const isOperator = require('./helpers/query/isOperator');
const util = require('util');
const isObject = require('./helpers/isObject');
const isMongooseObject = require('./helpers/isMongooseObject');
+const utils = require('./utils');
const ALLOWED_GEOWITHIN_GEOJSON_TYPES = ['Polygon', 'MultiPolygon'];
@@ -291,7 +291,7 @@ module.exports = function cast(schema, obj, options, context) {
}
} else if (val == null) {
continue;
- } else if (getConstructorName(val) === 'Object') {
+ } else if (utils.isPOJO(val)) {
any$conditionals = Object.keys(val).some(isOperator);
if (!any$conditionals) {
diff --git a/lib/connection.js b/lib/connection.js
index 28e56989c16..e77e0b9b757 100644
--- a/lib/connection.js
+++ b/lib/connection.js
@@ -16,10 +16,7 @@ const clone = require('./helpers/clone');
const driver = require('./driver');
const get = require('./helpers/get');
const immediate = require('./helpers/immediate');
-const mongodb = require('mongodb');
-const pkg = require('../package.json');
const utils = require('./utils');
-const processConnectionOptions = require('./helpers/processConnectionOptions');
const CreateCollectionsError = require('./error/createCollectionsError');
const arrayAtomicsSymbol = require('./helpers/symbols').arrayAtomicsSymbol;
@@ -738,7 +735,7 @@ Connection.prototype.openUri = async function openUri(uri, options) {
throw err;
}
- this.$initialConnection = _createMongoClient(this, uri, options).
+ this.$initialConnection = this.createClient(uri, options).
then(() => this).
catch(err => {
this.readyState = STATES.disconnected;
@@ -795,184 +792,6 @@ function _handleConnectionErrors(err) {
return err;
}
-/*!
- * ignore
- */
-
-async function _createMongoClient(conn, uri, options) {
- if (typeof uri !== 'string') {
- throw new MongooseError('The `uri` parameter to `openUri()` must be a ' +
- `string, got "${typeof uri}". Make sure the first parameter to ` +
- '`mongoose.connect()` or `mongoose.createConnection()` is a string.');
- }
-
- if (conn._destroyCalled) {
- throw new MongooseError(
- 'Connection has been closed and destroyed, and cannot be used for re-opening the connection. ' +
- 'Please create a new connection with `mongoose.createConnection()` or `mongoose.connect()`.'
- );
- }
-
- if (conn.readyState === STATES.connecting || conn.readyState === STATES.connected) {
- if (conn._connectionString !== uri) {
- throw new MongooseError('Can\'t call `openUri()` on an active connection with ' +
- 'different connection strings. Make sure you aren\'t calling `mongoose.connect()` ' +
- 'multiple times. See: https://mongoosejs.com/docs/connections.html#multiple_connections');
- }
- }
-
- options = processConnectionOptions(uri, options);
-
- if (options) {
-
- const autoIndex = options.config && options.config.autoIndex != null ?
- options.config.autoIndex :
- options.autoIndex;
- if (autoIndex != null) {
- conn.config.autoIndex = autoIndex !== false;
- delete options.config;
- delete options.autoIndex;
- }
-
- if ('autoCreate' in options) {
- conn.config.autoCreate = !!options.autoCreate;
- delete options.autoCreate;
- }
-
- if ('sanitizeFilter' in options) {
- conn.config.sanitizeFilter = options.sanitizeFilter;
- delete options.sanitizeFilter;
- }
-
- // Backwards compat
- if (options.user || options.pass) {
- options.auth = options.auth || {};
- options.auth.username = options.user;
- options.auth.password = options.pass;
-
- conn.user = options.user;
- conn.pass = options.pass;
- }
- delete options.user;
- delete options.pass;
-
- if (options.bufferCommands != null) {
- conn.config.bufferCommands = options.bufferCommands;
- delete options.bufferCommands;
- }
- } else {
- options = {};
- }
-
- conn._connectionOptions = options;
- const dbName = options.dbName;
- if (dbName != null) {
- conn.$dbName = dbName;
- }
- delete options.dbName;
-
- if (!utils.hasUserDefinedProperty(options, 'driverInfo')) {
- options.driverInfo = {
- name: 'Mongoose',
- version: pkg.version
- };
- }
-
- conn.readyState = STATES.connecting;
- conn._connectionString = uri;
-
- let client;
- try {
- client = new mongodb.MongoClient(uri, options);
- } catch (error) {
- conn.readyState = STATES.disconnected;
- throw error;
- }
- conn.client = client;
-
- client.setMaxListeners(0);
- await client.connect();
-
- _setClient(conn, client, options, dbName);
-
- for (const db of conn.otherDbs) {
- _setClient(db, client, {}, db.name);
- }
- return conn;
-}
-
-/*!
- * ignore
- */
-
-function _setClient(conn, client, options, dbName) {
- const db = dbName != null ? client.db(dbName) : client.db();
- conn.db = db;
- conn.client = client;
- conn.host = client &&
- client.s &&
- client.s.options &&
- client.s.options.hosts &&
- client.s.options.hosts[0] &&
- client.s.options.hosts[0].host || void 0;
- conn.port = client &&
- client.s &&
- client.s.options &&
- client.s.options.hosts &&
- client.s.options.hosts[0] &&
- client.s.options.hosts[0].port || void 0;
- conn.name = dbName != null ? dbName : client && client.s && client.s.options && client.s.options.dbName || void 0;
- conn._closeCalled = client._closeCalled;
-
- const _handleReconnect = () => {
- // If we aren't disconnected, we assume this reconnect is due to a
- // socket timeout. If there's no activity on a socket for
- // `socketTimeoutMS`, the driver will attempt to reconnect and emit
- // this event.
- if (conn.readyState !== STATES.connected) {
- conn.readyState = STATES.connected;
- conn.emit('reconnect');
- conn.emit('reconnected');
- conn.onOpen();
- }
- };
-
- const type = client &&
- client.topology &&
- client.topology.description &&
- client.topology.description.type || '';
-
- if (type === 'Single') {
- client.on('serverDescriptionChanged', ev => {
- const newDescription = ev.newDescription;
- if (newDescription.type === 'Unknown') {
- conn.readyState = STATES.disconnected;
- } else {
- _handleReconnect();
- }
- });
- } else if (type.startsWith('ReplicaSet')) {
- client.on('topologyDescriptionChanged', ev => {
- // Emit disconnected if we've lost connectivity to the primary
- const description = ev.newDescription;
- if (conn.readyState === STATES.connected && description.type !== 'ReplicaSetWithPrimary') {
- // Implicitly emits 'disconnected'
- conn.readyState = STATES.disconnected;
- } else if (conn.readyState === STATES.disconnected && description.type === 'ReplicaSetWithPrimary') {
- _handleReconnect();
- }
- });
- }
-
- conn.onOpen();
-
- for (const i in conn.collections) {
- if (utils.object.hasOwnProperty(conn.collections, i)) {
- conn.collections[i].onOpen();
- }
- }
-}
-
/**
* Destroy the connection. Similar to [`.close`](https://mongoosejs.com/docs/api/connection.html#Connection.prototype.close()),
* but also removes the connection from Mongoose's `connections` list and prevents the
@@ -1525,26 +1344,16 @@ Connection.prototype.getClient = function getClient() {
* @return {Connection} this
*/
-Connection.prototype.setClient = function setClient(client) {
- if (!(client instanceof mongodb.MongoClient)) {
- throw new MongooseError('Must call `setClient()` with an instance of MongoClient');
- }
- if (this.readyState !== STATES.disconnected) {
- throw new MongooseError('Cannot call `setClient()` on a connection that is already connected.');
- }
- if (client.topology == null) {
- throw new MongooseError('Cannot call `setClient()` with a MongoClient that you have not called `connect()` on yet.');
- }
-
- this._connectionString = client.s.url;
- _setClient(this, client, {}, client.s.options.dbName);
+Connection.prototype.setClient = function setClient() {
+ throw new MongooseError('Connection#setClient not implemented by driver');
+};
- for (const model of Object.values(this.models)) {
- // Errors handled internally, so safe to ignore error
- model.init().catch(function $modelInitNoop() {});
- }
+/*!
+ * Called internally by `openUri()` to create a MongoClient instance.
+ */
- return this;
+Connection.prototype.createClient = function createClient() {
+ throw new MongooseError('Connection#createClient not implemented by driver');
};
/**
@@ -1609,6 +1418,29 @@ Connection.prototype.syncIndexes = async function syncIndexes(options = {}) {
* @api public
*/
+/**
+ * Removes the database connection with the given name created with with `useDb()`.
+ *
+ * Throws an error if the database connection was not found.
+ *
+ * #### Example:
+ *
+ * // Connect to `initialdb` first
+ * const conn = await mongoose.createConnection('mongodb://127.0.0.1:27017/initialdb').asPromise();
+ *
+ * // Creates an un-cached connection to `mydb`
+ * const db = conn.useDb('mydb');
+ *
+ * // Closes `db`, and removes `db` from `conn.relatedDbs` and `conn.otherDbs`
+ * await conn.removeDb('mydb');
+ *
+ * @method removeDb
+ * @memberOf Connection
+ * @param {String} name The database name
+ * @return {Connection} this
+ * @api public
+ */
+
/*!
* Module exports.
*/
diff --git a/lib/document.js b/lib/document.js
index de03bf46d9b..ad38e4a597c 100644
--- a/lib/document.js
+++ b/lib/document.js
@@ -22,7 +22,6 @@ const clone = require('./helpers/clone');
const compile = require('./helpers/document/compile').compile;
const defineKey = require('./helpers/document/compile').defineKey;
const flatten = require('./helpers/common').flatten;
-const flattenObjectWithDottedPaths = require('./helpers/path/flattenObjectWithDottedPaths');
const get = require('./helpers/get');
const getEmbeddedDiscriminatorPath = require('./helpers/document/getEmbeddedDiscriminatorPath');
const getKeysInSchemaOrder = require('./helpers/schema/getKeysInSchemaOrder');
@@ -473,8 +472,6 @@ function $applyDefaultsToNested(val, path, doc) {
return;
}
- flattenObjectWithDottedPaths(val);
-
const paths = Object.keys(doc.$__schema.paths);
const plen = paths.length;
@@ -2524,21 +2521,17 @@ Document.prototype.isDirectSelected = function isDirectSelected(path) {
*
* #### Note:
*
- * This method is called `pre` save and if a validation rule is violated, [save](https://mongoosejs.com/docs/api/model.html#Model.prototype.save()) is aborted and the error is returned to your `callback`.
+ * This method is called `pre` save and if a validation rule is violated, [save](https://mongoosejs.com/docs/api/model.html#Model.prototype.save()) is aborted and the error is thrown.
*
* #### Example:
*
- * doc.validate(function (err) {
- * if (err) handleError(err);
- * else // validation passed
- * });
+ * await doc.validate({ validateModifiedOnly: false, pathsToSkip: ['name', 'email']});
*
* @param {Array|String} [pathsToValidate] list of paths to validate. If set, Mongoose will validate only the modified paths that are in the given list.
* @param {Object} [options] internal options
* @param {Boolean} [options.validateModifiedOnly=false] if `true` mongoose validates only modified paths.
* @param {Array|string} [options.pathsToSkip] list of paths to skip. If set, Mongoose will validate every modified path that is not in this list.
- * @param {Function} [callback] optional callback called after validation completes, passing an error if one occurred
- * @return {Promise} Returns a Promise if no `callback` is given.
+ * @return {Promise} Returns a Promise.
* @api public
*/
@@ -3328,8 +3321,8 @@ Document.prototype.$__reset = function reset() {
const resetArrays = new Set();
for (const subdoc of subdocs) {
const fullPathWithIndexes = subdoc.$__fullPathWithIndexes();
+ subdoc.$__reset();
if (this.isModified(fullPathWithIndexes) || isParentInit(fullPathWithIndexes)) {
- subdoc.$__reset();
if (subdoc.$isDocumentArrayElement) {
resetArrays.add(subdoc.parentArray());
} else {
diff --git a/lib/drivers/node-mongodb-native/connection.js b/lib/drivers/node-mongodb-native/connection.js
index aaa3acd6160..0e6a2c29836 100644
--- a/lib/drivers/node-mongodb-native/connection.js
+++ b/lib/drivers/node-mongodb-native/connection.js
@@ -5,8 +5,13 @@
'use strict';
const MongooseConnection = require('../../connection');
+const MongooseError = require('../../error/index');
const STATES = require('../../connectionstate');
+const mongodb = require('mongodb');
+const pkg = require('../../../package.json');
+const processConnectionOptions = require('../../helpers/processConnectionOptions');
const setTimeout = require('../../helpers/timers').setTimeout;
+const utils = require('../../utils');
/**
* A [node-mongodb-native](https://github.com/mongodb/node-mongodb-native) connection implementation.
@@ -121,6 +126,44 @@ NativeConnection.prototype.useDb = function(name, options) {
return newConn;
};
+/**
+ * Removes the database connection with the given name created with `useDb()`.
+ *
+ * Throws an error if the database connection was not found.
+ *
+ * #### Example:
+ *
+ * // Connect to `initialdb` first
+ * const conn = await mongoose.createConnection('mongodb://127.0.0.1:27017/initialdb').asPromise();
+ *
+ * // Creates an un-cached connection to `mydb`
+ * const db = conn.useDb('mydb');
+ *
+ * // Closes `db`, and removes `db` from `conn.relatedDbs` and `conn.otherDbs`
+ * await conn.removeDb('mydb');
+ *
+ * @method removeDb
+ * @memberOf Connection
+ * @param {String} name The database name
+ * @return {Connection} this
+ */
+
+NativeConnection.prototype.removeDb = function removeDb(name) {
+ const dbs = this.otherDbs.filter(db => db.name === name);
+ if (!dbs.length) {
+ throw new MongooseError(`No connections to database "${name}" found`);
+ }
+
+ for (const db of dbs) {
+ db._closeCalled = true;
+ db._destroyCalled = true;
+ db._readyState = STATES.disconnected;
+ db.$wasForceClosed = true;
+ }
+ delete this.relatedDbs[name];
+ this.otherDbs = this.otherDbs.filter(db => db.name !== name);
+};
+
/**
* Closes the connection
*
@@ -154,6 +197,210 @@ NativeConnection.prototype.doClose = async function doClose(force) {
return this;
};
+/*!
+ * ignore
+ */
+
+NativeConnection.prototype.createClient = async function createClient(uri, options) {
+ if (typeof uri !== 'string') {
+ throw new MongooseError('The `uri` parameter to `openUri()` must be a ' +
+ `string, got "${typeof uri}". Make sure the first parameter to ` +
+ '`mongoose.connect()` or `mongoose.createConnection()` is a string.');
+ }
+
+ if (this._destroyCalled) {
+ throw new MongooseError(
+ 'Connection has been closed and destroyed, and cannot be used for re-opening the connection. ' +
+ 'Please create a new connection with `mongoose.createConnection()` or `mongoose.connect()`.'
+ );
+ }
+
+ if (this.readyState === STATES.connecting || this.readyState === STATES.connected) {
+ if (this._connectionString !== uri) {
+ throw new MongooseError('Can\'t call `openUri()` on an active connection with ' +
+ 'different connection strings. Make sure you aren\'t calling `mongoose.connect()` ' +
+ 'multiple times. See: https://mongoosejs.com/docs/connections.html#multiple_connections');
+ }
+ }
+
+ options = processConnectionOptions(uri, options);
+
+ if (options) {
+
+ const autoIndex = options.config && options.config.autoIndex != null ?
+ options.config.autoIndex :
+ options.autoIndex;
+ if (autoIndex != null) {
+ this.config.autoIndex = autoIndex !== false;
+ delete options.config;
+ delete options.autoIndex;
+ }
+
+ if ('autoCreate' in options) {
+ this.config.autoCreate = !!options.autoCreate;
+ delete options.autoCreate;
+ }
+
+ if ('sanitizeFilter' in options) {
+ this.config.sanitizeFilter = options.sanitizeFilter;
+ delete options.sanitizeFilter;
+ }
+
+ // Backwards compat
+ if (options.user || options.pass) {
+ options.auth = options.auth || {};
+ options.auth.username = options.user;
+ options.auth.password = options.pass;
+
+ this.user = options.user;
+ this.pass = options.pass;
+ }
+ delete options.user;
+ delete options.pass;
+
+ if (options.bufferCommands != null) {
+ this.config.bufferCommands = options.bufferCommands;
+ delete options.bufferCommands;
+ }
+ } else {
+ options = {};
+ }
+
+ this._connectionOptions = options;
+ const dbName = options.dbName;
+ if (dbName != null) {
+ this.$dbName = dbName;
+ }
+ delete options.dbName;
+
+ if (!utils.hasUserDefinedProperty(options, 'driverInfo')) {
+ options.driverInfo = {
+ name: 'Mongoose',
+ version: pkg.version
+ };
+ }
+
+ this.readyState = STATES.connecting;
+ this._connectionString = uri;
+
+ let client;
+ try {
+ client = new mongodb.MongoClient(uri, options);
+ } catch (error) {
+ this.readyState = STATES.disconnected;
+ throw error;
+ }
+ this.client = client;
+
+ client.setMaxListeners(0);
+ await client.connect();
+
+ _setClient(this, client, options, dbName);
+
+ for (const db of this.otherDbs) {
+ _setClient(db, client, {}, db.name);
+ }
+ return this;
+};
+
+/*!
+ * ignore
+ */
+
+NativeConnection.prototype.setClient = function setClient(client) {
+ if (!(client instanceof mongodb.MongoClient)) {
+ throw new MongooseError('Must call `setClient()` with an instance of MongoClient');
+ }
+ if (this.readyState !== STATES.disconnected) {
+ throw new MongooseError('Cannot call `setClient()` on a connection that is already connected.');
+ }
+ if (client.topology == null) {
+ throw new MongooseError('Cannot call `setClient()` with a MongoClient that you have not called `connect()` on yet.');
+ }
+
+ this._connectionString = client.s.url;
+ _setClient(this, client, {}, client.s.options.dbName);
+
+ for (const model of Object.values(this.models)) {
+ // Errors handled internally, so safe to ignore error
+ model.init().catch(function $modelInitNoop() {});
+ }
+
+ return this;
+};
+
+/*!
+ * ignore
+ */
+
+function _setClient(conn, client, options, dbName) {
+ const db = dbName != null ? client.db(dbName) : client.db();
+ conn.db = db;
+ conn.client = client;
+ conn.host = client &&
+ client.s &&
+ client.s.options &&
+ client.s.options.hosts &&
+ client.s.options.hosts[0] &&
+ client.s.options.hosts[0].host || void 0;
+ conn.port = client &&
+ client.s &&
+ client.s.options &&
+ client.s.options.hosts &&
+ client.s.options.hosts[0] &&
+ client.s.options.hosts[0].port || void 0;
+ conn.name = dbName != null ? dbName : client && client.s && client.s.options && client.s.options.dbName || void 0;
+ conn._closeCalled = client._closeCalled;
+
+ const _handleReconnect = () => {
+ // If we aren't disconnected, we assume this reconnect is due to a
+ // socket timeout. If there's no activity on a socket for
+ // `socketTimeoutMS`, the driver will attempt to reconnect and emit
+ // this event.
+ if (conn.readyState !== STATES.connected) {
+ conn.readyState = STATES.connected;
+ conn.emit('reconnect');
+ conn.emit('reconnected');
+ conn.onOpen();
+ }
+ };
+
+ const type = client &&
+ client.topology &&
+ client.topology.description &&
+ client.topology.description.type || '';
+
+ if (type === 'Single') {
+ client.on('serverDescriptionChanged', ev => {
+ const newDescription = ev.newDescription;
+ if (newDescription.type === 'Unknown') {
+ conn.readyState = STATES.disconnected;
+ } else {
+ _handleReconnect();
+ }
+ });
+ } else if (type.startsWith('ReplicaSet')) {
+ client.on('topologyDescriptionChanged', ev => {
+ // Emit disconnected if we've lost connectivity to the primary
+ const description = ev.newDescription;
+ if (conn.readyState === STATES.connected && description.type !== 'ReplicaSetWithPrimary') {
+ // Implicitly emits 'disconnected'
+ conn.readyState = STATES.disconnected;
+ } else if (conn.readyState === STATES.disconnected && description.type === 'ReplicaSetWithPrimary') {
+ _handleReconnect();
+ }
+ });
+ }
+
+ conn.onOpen();
+
+ for (const i in conn.collections) {
+ if (utils.object.hasOwnProperty(conn.collections, i)) {
+ conn.collections[i].onOpen();
+ }
+ }
+}
+
/*!
* Module exports.
diff --git a/lib/error/cast.js b/lib/error/cast.js
index c42e8216691..f7df49b8c7e 100644
--- a/lib/error/cast.js
+++ b/lib/error/cast.js
@@ -20,10 +20,9 @@ class CastError extends MongooseError {
constructor(type, value, path, reason, schemaType) {
// If no args, assume we'll `init()` later.
if (arguments.length > 0) {
- const stringValue = getStringValue(value);
const valueType = getValueType(value);
const messageFormat = getMessageFormat(schemaType);
- const msg = formatMessage(null, type, stringValue, path, messageFormat, valueType, reason);
+ const msg = formatMessage(null, type, value, path, messageFormat, valueType, reason);
super(msg);
this.init(type, value, path, reason, schemaType);
} else {
@@ -77,7 +76,7 @@ class CastError extends MongooseError {
*/
setModel(model) {
this.model = model;
- this.message = formatMessage(model, this.kind, this.stringValue, this.path,
+ this.message = formatMessage(model, this.kind, this.value, this.path,
this.messageFormat, this.valueType);
}
}
@@ -111,10 +110,8 @@ function getValueType(value) {
}
function getMessageFormat(schemaType) {
- const messageFormat = schemaType &&
- schemaType.options &&
- schemaType.options.cast || null;
- if (typeof messageFormat === 'string') {
+ const messageFormat = schemaType && schemaType._castErrorMessage || null;
+ if (typeof messageFormat === 'string' || typeof messageFormat === 'function') {
return messageFormat;
}
}
@@ -123,8 +120,9 @@ function getMessageFormat(schemaType) {
* ignore
*/
-function formatMessage(model, kind, stringValue, path, messageFormat, valueType, reason) {
- if (messageFormat != null) {
+function formatMessage(model, kind, value, path, messageFormat, valueType, reason) {
+ if (typeof messageFormat === 'string') {
+ const stringValue = getStringValue(value);
let ret = messageFormat.
replace('{KIND}', kind).
replace('{VALUE}', stringValue).
@@ -134,7 +132,10 @@ function formatMessage(model, kind, stringValue, path, messageFormat, valueType,
}
return ret;
+ } else if (typeof messageFormat === 'function') {
+ return messageFormat(value, path, model, kind);
} else {
+ const stringValue = getStringValue(value);
const valueTypeMsg = valueType ? ' (type ' + valueType + ')' : '';
let ret = 'Cast to ' + kind + ' failed for value ' +
stringValue + valueTypeMsg + ' at path "' + path + '"';
diff --git a/lib/error/messages.js b/lib/error/messages.js
index b750d5d5ec2..a2db50a6fce 100644
--- a/lib/error/messages.js
+++ b/lib/error/messages.js
@@ -6,7 +6,7 @@
* const mongoose = require('mongoose');
* mongoose.Error.messages.String.enum = "Your custom message for {PATH}.";
*
- * As you might have noticed, error messages support basic templating
+ * Error messages support basic templating. Mongoose will replace the following strings with the corresponding value.
*
* - `{PATH}` is replaced with the invalid document path
* - `{VALUE}` is replaced with the invalid value
diff --git a/lib/helpers/model/castBulkWrite.js b/lib/helpers/model/castBulkWrite.js
index 4ae1f47efc8..94a38806029 100644
--- a/lib/helpers/model/castBulkWrite.js
+++ b/lib/helpers/model/castBulkWrite.js
@@ -20,7 +20,6 @@ const setDefaultsOnInsert = require('../setDefaultsOnInsert');
module.exports = function castBulkWrite(originalModel, op, options) {
const now = originalModel.base.now();
-
if (op['insertOne']) {
return (callback) => {
const model = decideModelByObject(originalModel, op['insertOne']['document']);
@@ -66,7 +65,9 @@ module.exports = function castBulkWrite(originalModel, op, options) {
applyTimestampsToUpdate(now, createdAt, updatedAt, op['updateOne']['update'], {});
}
- applyTimestampsToChildren(now, op['updateOne']['update'], model.schema);
+ if (op['updateOne'].timestamps !== false) {
+ applyTimestampsToChildren(now, op['updateOne']['update'], model.schema);
+ }
if (op['updateOne'].setDefaultsOnInsert !== false) {
setDefaultsOnInsert(op['updateOne']['filter'], model.schema, op['updateOne']['update'], {
@@ -117,8 +118,9 @@ module.exports = function castBulkWrite(originalModel, op, options) {
const updatedAt = model.schema.$timestamps.updatedAt;
applyTimestampsToUpdate(now, createdAt, updatedAt, op['updateMany']['update'], {});
}
-
- applyTimestampsToChildren(now, op['updateMany']['update'], model.schema);
+ if (op['updateMany'].timestamps !== false) {
+ applyTimestampsToChildren(now, op['updateMany']['update'], model.schema);
+ }
_addDiscriminatorToObject(schema, op['updateMany']['filter']);
diff --git a/lib/helpers/path/flattenObjectWithDottedPaths.js b/lib/helpers/path/flattenObjectWithDottedPaths.js
deleted file mode 100644
index 2771796d082..00000000000
--- a/lib/helpers/path/flattenObjectWithDottedPaths.js
+++ /dev/null
@@ -1,39 +0,0 @@
-'use strict';
-
-const MongooseError = require('../../error/mongooseError');
-const isMongooseObject = require('../isMongooseObject');
-const setDottedPath = require('../path/setDottedPath');
-const util = require('util');
-
-/**
- * Given an object that may contain dotted paths, flatten the paths out.
- * For example: `flattenObjectWithDottedPaths({ a: { 'b.c': 42 } })` => `{ a: { b: { c: 42 } } }`
- */
-
-module.exports = function flattenObjectWithDottedPaths(obj) {
- if (obj == null || typeof obj !== 'object' || Array.isArray(obj)) {
- return;
- }
- // Avoid Mongoose docs, like docs and maps, because these may cause infinite recursion
- if (isMongooseObject(obj)) {
- return;
- }
- const keys = Object.keys(obj);
- for (const key of keys) {
- const val = obj[key];
- if (key.indexOf('.') !== -1) {
- try {
- delete obj[key];
- setDottedPath(obj, key, val);
- } catch (err) {
- if (!(err instanceof TypeError)) {
- throw err;
- }
- throw new MongooseError(`Conflicting dotted paths when setting document path, key: "${key}", value: ${util.inspect(val)}`);
- }
- continue;
- }
-
- flattenObjectWithDottedPaths(obj[key]);
- }
-};
diff --git a/lib/helpers/populate/assignRawDocsToIdStructure.js b/lib/helpers/populate/assignRawDocsToIdStructure.js
index c08671e5918..6a0cc06653c 100644
--- a/lib/helpers/populate/assignRawDocsToIdStructure.js
+++ b/lib/helpers/populate/assignRawDocsToIdStructure.js
@@ -7,7 +7,7 @@ const utils = require('../../utils');
module.exports = assignRawDocsToIdStructure;
-const kHasArray = Symbol('assignRawDocsToIdStructure.hasArray');
+const kHasArray = Symbol('mongoose#assignRawDocsToIdStructure#hasArray');
/**
* Assign `vals` returned by mongo query to the `rawIds`
diff --git a/lib/helpers/promiseOrCallback.js b/lib/helpers/promiseOrCallback.js
index 585a6486f2e..952eecf4bf8 100644
--- a/lib/helpers/promiseOrCallback.js
+++ b/lib/helpers/promiseOrCallback.js
@@ -2,7 +2,7 @@
const immediate = require('./immediate');
-const emittedSymbol = Symbol('mongoose:emitted');
+const emittedSymbol = Symbol('mongoose#emitted');
module.exports = function promiseOrCallback(callback, fn, ee, Promise) {
if (typeof callback === 'function') {
diff --git a/lib/helpers/query/validOps.js b/lib/helpers/query/validOps.js
index f89c84264a2..cb17549dbbd 100644
--- a/lib/helpers/query/validOps.js
+++ b/lib/helpers/query/validOps.js
@@ -2,7 +2,6 @@
module.exports = Object.freeze([
// Read
- 'count',
'countDocuments',
'distinct',
'estimatedDocumentCount',
diff --git a/lib/helpers/schema/applyWriteConcern.js b/lib/helpers/schema/applyWriteConcern.js
index 4095bd94bc7..fcce08f8410 100644
--- a/lib/helpers/schema/applyWriteConcern.js
+++ b/lib/helpers/schema/applyWriteConcern.js
@@ -3,6 +3,9 @@
const get = require('../get');
module.exports = function applyWriteConcern(schema, options) {
+ if (options.writeConcern != null) {
+ return;
+ }
const writeConcern = get(schema, 'options.writeConcern', {});
if (Object.keys(writeConcern).length != 0) {
options.writeConcern = {};
diff --git a/lib/helpers/schema/idGetter.js b/lib/helpers/schema/idGetter.js
index 31ea2ec8659..6df8a8cc04f 100644
--- a/lib/helpers/schema/idGetter.js
+++ b/lib/helpers/schema/idGetter.js
@@ -12,8 +12,11 @@ module.exports = function addIdGetter(schema) {
if (!autoIdGetter) {
return schema;
}
-
+ if (schema.aliases && schema.aliases.id) {
+ return schema;
+ }
schema.virtual('id').get(idGetter);
+ schema.virtual('id').set(idSetter);
return schema;
};
@@ -30,3 +33,14 @@ function idGetter() {
return null;
}
+
+/**
+ *
+ * @param {String} v the id to set
+ * @api private
+ */
+
+function idSetter(v) {
+ this._id = v;
+ return v;
+}
diff --git a/lib/helpers/symbols.js b/lib/helpers/symbols.js
index f12db3d8272..a9af890a5d6 100644
--- a/lib/helpers/symbols.js
+++ b/lib/helpers/symbols.js
@@ -5,7 +5,7 @@ exports.arrayAtomicsSymbol = Symbol('mongoose#Array#_atomics');
exports.arrayParentSymbol = Symbol('mongoose#Array#_parent');
exports.arrayPathSymbol = Symbol('mongoose#Array#_path');
exports.arraySchemaSymbol = Symbol('mongoose#Array#_schema');
-exports.documentArrayParent = Symbol('mongoose:documentArrayParent');
+exports.documentArrayParent = Symbol('mongoose#documentArrayParent');
exports.documentIsSelected = Symbol('mongoose#Document#isSelected');
exports.documentIsModified = Symbol('mongoose#Document#isModified');
exports.documentModifiedPaths = Symbol('mongoose#Document#modifiedPaths');
@@ -13,8 +13,8 @@ exports.documentSchemaSymbol = Symbol('mongoose#Document#schema');
exports.getSymbol = Symbol('mongoose#Document#get');
exports.modelSymbol = Symbol('mongoose#Model');
exports.objectIdSymbol = Symbol('mongoose#ObjectId');
-exports.populateModelSymbol = Symbol('mongoose.PopulateOptions#Model');
+exports.populateModelSymbol = Symbol('mongoose#PopulateOptions#Model');
exports.schemaTypeSymbol = Symbol('mongoose#schemaType');
-exports.sessionNewDocuments = Symbol('mongoose:ClientSession#newDocuments');
+exports.sessionNewDocuments = Symbol('mongoose#ClientSession#newDocuments');
exports.scopeSymbol = Symbol('mongoose#Document#scope');
-exports.validatorErrorSymbol = Symbol('mongoose:validatorError');
+exports.validatorErrorSymbol = Symbol('mongoose#validatorError');
diff --git a/lib/model.js b/lib/model.js
index ceb5a4b2f01..c275bbb3007 100644
--- a/lib/model.js
+++ b/lib/model.js
@@ -77,7 +77,8 @@ const modelSymbol = require('./helpers/symbols').modelSymbol;
const subclassedSymbol = Symbol('mongoose#Model#subclassed');
const saveToObjectOptions = Object.assign({}, internalToObjectOptions, {
- bson: true
+ bson: true,
+ flattenObjectIds: false
});
/**
@@ -320,7 +321,6 @@ Model.prototype.$__handleSave = function(options, callback) {
_setIsNew(this, false);
// Make it possible to retry the insert
this.$__.inserting = true;
-
return;
}
@@ -479,7 +479,6 @@ function generateVersionError(doc, modifiedPaths) {
* newProduct === product; // true
*
* @param {Object} [options] options optional options
- * @param {Boolean} [options.ordered] saves the docs in series rather than parallel.
* @param {Session} [options.session=null] the [session](https://www.mongodb.com/docs/manual/reference/server-sessions/) associated with this save operation. If not specified, defaults to the [document's associated session](https://mongoosejs.com/docs/api/document.html#Document.prototype.session()).
* @param {Object} [options.safe] (DEPRECATED) overrides [schema's safe option](https://mongoosejs.com/docs/guide.html#safe). Use the `w` option instead.
* @param {Boolean} [options.validateBeforeSave] set to false to save without validating.
@@ -1342,6 +1341,14 @@ Model.createCollection = async function createCollection(options) {
throw new MongooseError('Model.createCollection() no longer accepts a callback');
}
+ const collectionOptions = this &&
+ this.schema &&
+ this.schema.options &&
+ this.schema.options.collectionOptions;
+ if (collectionOptions != null) {
+ options = Object.assign({}, collectionOptions, options);
+ }
+
const schemaCollation = this &&
this.schema &&
this.schema.options &&
@@ -2205,34 +2212,6 @@ Model.countDocuments = function countDocuments(conditions, options) {
return mq.countDocuments(conditions);
};
-/**
- * Counts number of documents that match `filter` in a database collection.
- *
- * This method is deprecated. If you want to count the number of documents in
- * a collection, e.g. `count({})`, use the [`estimatedDocumentCount()` function](https://mongoosejs.com/docs/api/model.html#Model.estimatedDocumentCount())
- * instead. Otherwise, use the [`countDocuments()`](https://mongoosejs.com/docs/api/model.html#Model.countDocuments()) function instead.
- *
- * #### Example:
- *
- * const count = await Adventure.count({ type: 'jungle' });
- * console.log('there are %d jungle adventures', count);
- *
- * @deprecated
- * @param {Object} [filter]
- * @return {Query}
- * @api public
- */
-
-Model.count = function count(conditions) {
- _checkContext(this, 'count');
- if (typeof arguments[0] === 'function' || typeof arguments[1] === 'function') {
- throw new MongooseError('Model.count() no longer accepts a callback');
- }
-
- const mq = new this.Query({}, {}, this, this.$__collection);
-
- return mq.count(conditions);
-};
/**
* Creates a Query for a `distinct` operation.
@@ -2772,6 +2751,8 @@ Model.findByIdAndRemove = function(id, options) {
*
* @param {Array|Object} docs Documents to insert, as a spread or array
* @param {Object} [options] Options passed down to `save()`. To specify `options`, `docs` **must** be an array, not a spread. See [Model.save](https://mongoosejs.com/docs/api/model.html#Model.prototype.save()) for available options.
+ * @param {Boolean} [options.ordered] saves the docs in series rather than parallel.
+ * @param {Boolean} [options.aggregateErrors] Aggregate Errors instead of throwing the first one that occurs. Default: false
* @return {Promise}
* @api public
*/
@@ -2828,44 +2809,86 @@ Model.create = async function create(doc, options) {
return Array.isArray(doc) ? [] : null;
}
let res = [];
+ const immediateError = typeof options.aggregateErrors === 'boolean' ? !options.aggregateErrors : true;
+
+ delete options.aggregateErrors; // dont pass on the option to "$save"
+
if (options.ordered) {
for (let i = 0; i < args.length; i++) {
- const doc = args[i];
+ try {
+ const doc = args[i];
+ const Model = this.discriminators && doc[discriminatorKey] != null ?
+ this.discriminators[doc[discriminatorKey]] || getDiscriminatorByValue(this.discriminators, doc[discriminatorKey]) :
+ this;
+ if (Model == null) {
+ throw new MongooseError(`Discriminator "${doc[discriminatorKey]}" not ` +
+ `found for model "${this.modelName}"`);
+ }
+ let toSave = doc;
+ if (!(toSave instanceof Model)) {
+ toSave = new Model(toSave);
+ }
+
+ await toSave.$save(options);
+ res.push(toSave);
+ } catch (err) {
+ if (!immediateError) {
+ res.push(err);
+ } else {
+ throw err;
+ }
+ }
+ }
+ return res;
+ } else if (!immediateError) {
+ res = await Promise.allSettled(args.map(async doc => {
const Model = this.discriminators && doc[discriminatorKey] != null ?
this.discriminators[doc[discriminatorKey]] || getDiscriminatorByValue(this.discriminators, doc[discriminatorKey]) :
this;
if (Model == null) {
throw new MongooseError(`Discriminator "${doc[discriminatorKey]}" not ` +
- `found for model "${this.modelName}"`);
+ `found for model "${this.modelName}"`);
}
let toSave = doc;
+
if (!(toSave instanceof Model)) {
toSave = new Model(toSave);
}
await toSave.$save(options);
- res.push(toSave);
- }
- return res;
+
+ return toSave;
+ }));
+ res = res.map(result => result.status === 'fulfilled' ? result.value : result.reason);
} else {
+ let firstError = null;
res = await Promise.all(args.map(async doc => {
const Model = this.discriminators && doc[discriminatorKey] != null ?
this.discriminators[doc[discriminatorKey]] || getDiscriminatorByValue(this.discriminators, doc[discriminatorKey]) :
this;
if (Model == null) {
throw new MongooseError(`Discriminator "${doc[discriminatorKey]}" not ` +
- `found for model "${this.modelName}"`);
+ `found for model "${this.modelName}"`);
}
- let toSave = doc;
+ try {
+ let toSave = doc;
- if (!(toSave instanceof Model)) {
- toSave = new Model(toSave);
- }
+ if (!(toSave instanceof Model)) {
+ toSave = new Model(toSave);
+ }
- await toSave.$save(options);
+ await toSave.$save(options);
- return toSave;
+ return toSave;
+ } catch (err) {
+ if (!firstError) {
+ firstError = err;
+ }
+ }
}));
+ if (firstError) {
+ throw firstError;
+ }
}
@@ -2986,8 +3009,10 @@ Model.startSession = function() {
*
* #### Example:
*
- * const arr = [{ name: 'Star Wars' }, { name: 'The Empire Strikes Back' }];
- * Movies.insertMany(arr, function(error, docs) {});
+ * await Movies.insertMany([
+ * { name: 'Star Wars' },
+ * { name: 'The Empire Strikes Back' }
+ * ]);
*
* @param {Array|Object|*} doc(s)
* @param {Object} [options] see the [mongodb driver options](https://mongodb.github.io/node-mongodb-native/4.9/classes/Collection.html#insertMany)
diff --git a/lib/query.js b/lib/query.js
index a1ae882de64..756b19992f2 100644
--- a/lib/query.js
+++ b/lib/query.js
@@ -24,6 +24,7 @@ const getDiscriminatorByValue = require('./helpers/discriminator/getDiscriminato
const hasDollarKeys = require('./helpers/query/hasDollarKeys');
const helpers = require('./queryhelpers');
const immediate = require('./helpers/immediate');
+const internalToObjectOptions = require('./options').internalToObjectOptions;
const isExclusive = require('./helpers/projection/isExclusive');
const isInclusive = require('./helpers/projection/isInclusive');
const isPathSelectedInclusive = require('./helpers/projection/isPathSelectedInclusive');
@@ -1635,6 +1636,10 @@ Query.prototype.setOptions = function(options, overwrite) {
delete options.translateAliases;
}
+ if ('rawResult' in options) {
+ printRawResultDeprecationWarning();
+ }
+
if (options.lean == null && this.schema && 'lean' in this.schema.options) {
this._mongooseOptions.lean = this.schema.options.lean;
}
@@ -1669,6 +1674,15 @@ Query.prototype.setOptions = function(options, overwrite) {
return this;
};
+/*!
+ * ignore
+ */
+
+const printRawResultDeprecationWarning = util.deprecate(
+ function printRawResultDeprecationWarning() {},
+ 'The `rawResult` option for Mongoose queries is deprecated. Use `includeResultMetadata: false` as a replacement for `rawResult: true`.'
+);
+
/**
* Sets the [`explain` option](https://www.mongodb.com/docs/manual/reference/method/cursor.explain/),
* which makes this query return detailed execution stats instead of the actual
@@ -1954,7 +1968,6 @@ Query.prototype._optionsForExec = function(model) {
if (!model) {
return options;
}
-
// Apply schema-level `writeConcern` option
applyWriteConcern(model.schema, options);
@@ -2319,8 +2332,6 @@ Query.prototype.find = function(conditions) {
this.op = 'find';
- conditions = utils.toObject(conditions);
-
if (mquery.canMerge(conditions)) {
this.merge(conditions);
@@ -2392,6 +2403,8 @@ Query.prototype.merge = function(source) {
utils.merge(this._conditions, { _id: source }, opts);
return this;
+ } else if (source && source.$__) {
+ source = source.toObject(internalToObjectOptions);
}
opts.omit = {};
@@ -2434,7 +2447,7 @@ Query.prototype.collation = function(value) {
*/
Query.prototype._completeOne = function(doc, res, callback) {
- if (!doc && !this.options.rawResult) {
+ if (!doc && !this.options.rawResult && !this.options.includeResultMetadata) {
return callback(null, null);
}
@@ -2560,9 +2573,6 @@ Query.prototype.findOne = function(conditions, projection, options) {
this.op = 'findOne';
this._validateOp();
- // make sure we don't send in the whole Document to merge()
- conditions = utils.toObject(conditions);
-
if (options) {
this.setOptions(options);
}
@@ -2584,35 +2594,6 @@ Query.prototype.findOne = function(conditions, projection, options) {
return this;
};
-/**
- * Execute a count query
- *
- * @see count https://www.mongodb.com/docs/manual/reference/method/db.collection.count/
- * @api private
- */
-
-Query.prototype._count = async function _count() {
- try {
- this.cast(this.model);
- } catch (err) {
- this.error(err);
- }
-
- if (this.error()) {
- throw this.error();
- }
-
- applyGlobalMaxTimeMS(this.options, this.model);
- applyGlobalDiskUse(this.options, this.model);
-
- const options = this._optionsForExec();
-
- this._applyTranslateAliases(options);
-
- const conds = this._conditions;
-
- return this._collection.collection.count(conds, options);
-};
/**
* Execute a countDocuments query
@@ -2689,52 +2670,6 @@ Query.prototype._estimatedDocumentCount = async function _estimatedDocumentCount
return this._collection.collection.estimatedDocumentCount(options);
};
-/**
- * Specifies this query as a `count` query.
- *
- * This method is deprecated. If you want to count the number of documents in
- * a collection, e.g. `count({})`, use the [`estimatedDocumentCount()` function](https://mongoosejs.com/docs/api/query.html#Query.prototype.estimatedDocumentCount())
- * instead. Otherwise, use the [`countDocuments()`](https://mongoosejs.com/docs/api/query.html#Query.prototype.countDocuments()) function instead.
- *
- * This function triggers the following middleware.
- *
- * - `count()`
- *
- * #### Example:
- *
- * const countQuery = model.where({ 'color': 'black' }).count();
- *
- * query.count({ color: 'black' }).count().exec();
- *
- * await query.count({ color: 'black' });
- *
- * query.where('color', 'black').count();
- *
- * @deprecated
- * @param {Object} [filter] count documents that match this object
- * @return {Query} this
- * @see count https://www.mongodb.com/docs/manual/reference/method/db.collection.count/
- * @api public
- */
-
-Query.prototype.count = function(filter) {
- if (typeof filter === 'function' ||
- typeof arguments[1] === 'function') {
- throw new MongooseError('Query.prototype.count() no longer accepts a callback');
- }
-
- this.op = 'count';
- this._validateOp();
-
- filter = utils.toObject(filter);
-
- if (mquery.canMerge(filter)) {
- this.merge(filter);
- }
-
- return this;
-};
-
/**
* Specifies this query as a `estimatedDocumentCount()` query. Faster than
* using `countDocuments()` for large collections because
@@ -2822,8 +2757,6 @@ Query.prototype.countDocuments = function(conditions, options) {
this.op = 'countDocuments';
this._validateOp();
- conditions = utils.toObject(conditions);
-
if (mquery.canMerge(conditions)) {
this.merge(conditions);
}
@@ -2886,7 +2819,6 @@ Query.prototype.distinct = function(field, conditions) {
this.op = 'distinct';
this._validateOp();
- conditions = utils.toObject(conditions);
if (mquery.canMerge(conditions)) {
this.merge(conditions);
@@ -2981,8 +2913,6 @@ Query.prototype.deleteOne = function deleteOne(filter, options) {
this.op = 'deleteOne';
this.setOptions(options);
- filter = utils.toObject(filter);
-
if (mquery.canMerge(filter)) {
this.merge(filter);
@@ -3058,8 +2988,6 @@ Query.prototype.deleteMany = function(filter, options) {
this.setOptions(options);
this.op = 'deleteMany';
- filter = utils.toObject(filter);
-
if (mquery.canMerge(filter)) {
this.merge(filter);
@@ -3110,7 +3038,7 @@ Query.prototype._deleteMany = async function _deleteMany() {
*/
function completeOne(model, doc, res, options, fields, userProvidedFields, pop, callback) {
- if (options.rawResult && doc == null) {
+ if ((options.rawResult || options.includeResultMetadata) && doc == null) {
_init(null);
return null;
}
@@ -3123,7 +3051,7 @@ function completeOne(model, doc, res, options, fields, userProvidedFields, pop,
}
- if (options.rawResult) {
+ if (options.rawResult || options.includeResultMetadata) {
if (doc && casted) {
if (options.session != null) {
casted.$session(options.session);
@@ -3298,6 +3226,10 @@ Query.prototype._findOneAndUpdate = async function _findOneAndUpdate() {
applyGlobalMaxTimeMS(this.options, this.model);
applyGlobalDiskUse(this.options, this.model);
+ if (this.options.rawResult && this.options.includeResultMetadata === false) {
+ throw new MongooseError('Cannot set `rawResult` option when `includeResultMetadata` is false');
+ }
+
if ('strict' in this.options) {
this._mongooseOptions.strict = this.options.strict;
}
@@ -3348,7 +3280,7 @@ Query.prototype._findOneAndUpdate = async function _findOneAndUpdate() {
for (const fn of this._transforms) {
res = fn(res);
}
- const doc = res.value;
+ const doc = options.includeResultMetadata === false ? res : res.value;
return new Promise((resolve, reject) => {
this._completeOne(doc, res, _wrapThunkCallback(this, (err, res) => {
@@ -3489,6 +3421,11 @@ Query.prototype._findOneAndDelete = async function _findOneAndDelete() {
throw this.error();
}
+ const includeResultMetadata = this.options.includeResultMetadata;
+ if (this.options.rawResult && includeResultMetadata === false) {
+ throw new MongooseError('Cannot set `rawResult` option when `includeResultMetadata` is false');
+ }
+
const filter = this._conditions;
const options = this._optionsForExec(this.model);
this._applyTranslateAliases(options);
@@ -3497,7 +3434,7 @@ Query.prototype._findOneAndDelete = async function _findOneAndDelete() {
for (const fn of this._transforms) {
res = fn(res);
}
- const doc = res.value;
+ const doc = includeResultMetadata === false ? res : res.value;
return new Promise((resolve, reject) => {
this._completeOne(doc, res, _wrapThunkCallback(this, (err, res) => {
@@ -3624,6 +3561,11 @@ Query.prototype._findOneAndReplace = async function _findOneAndReplace() {
this._applyTranslateAliases(options);
convertNewToReturnDocument(options);
+ const includeResultMetadata = this.options.includeResultMetadata;
+ if (this.options.rawResult && includeResultMetadata === false) {
+ throw new MongooseError('Cannot set `rawResult` option when `includeResultMetadata` is false');
+ }
+
const modelOpts = { skipId: true };
if ('strict' in this._mongooseOptions) {
modelOpts.strict = this._mongooseOptions.strict;
@@ -3654,7 +3596,7 @@ Query.prototype._findOneAndReplace = async function _findOneAndReplace() {
res = fn(res);
}
- const doc = res.value;
+ const doc = includeResultMetadata === false ? res : res.value;
return new Promise((resolve, reject) => {
this._completeOne(doc, res, _wrapThunkCallback(this, (err, res) => {
if (err) {
@@ -4168,7 +4110,6 @@ function _update(query, op, filter, doc, options, callback) {
// make sure we don't send in the whole Document to merge()
query.op = op;
query._validateOp();
- filter = utils.toObject(filter);
doc = doc || {};
// strict is an option used in the update checking, make sure it gets set
diff --git a/lib/schema.js b/lib/schema.js
index 0dd5e8d6424..852851b2265 100644
--- a/lib/schema.js
+++ b/lib/schema.js
@@ -80,6 +80,7 @@ let id = 0;
* - [timestamps](https://mongoosejs.com/docs/guide.html#timestamps): object or boolean - defaults to `false`. If true, Mongoose adds `createdAt` and `updatedAt` properties to your schema and manages those properties for you.
* - [pluginTags](https://mongoosejs.com/docs/guide.html#pluginTags): array of strings - defaults to `undefined`. If set and plugin called with `tags` option, will only apply that plugin to schemas with a matching tag.
* - [virtuals](https://mongoosejs.com/docs/tutorials/virtuals.html#virtuals-via-schema-options): object - virtuals to define, alias for [`.virtual`](https://mongoosejs.com/docs/api/schema.html#Schema.prototype.virtual())
+ * - [collectionOptions]: object with options passed to [`createCollection()`](https://www.mongodb.com/docs/manual/reference/method/db.createCollection/) when calling `Model.createCollection()` or `autoCreate` set to true.
*
* #### Options for Nested Schemas:
*
@@ -747,10 +748,10 @@ Schema.prototype.add = function add(obj, prefix) {
childSchemaOptions.strict = this._userProvidedOptions.strict;
}
if (this._userProvidedOptions.toObject != null) {
- childSchemaOptions.toObject = this._userProvidedOptions.toObject;
+ childSchemaOptions.toObject = utils.omit(this._userProvidedOptions.toObject, ['transform']);
}
if (this._userProvidedOptions.toJSON != null) {
- childSchemaOptions.toJSON = this._userProvidedOptions.toJSON;
+ childSchemaOptions.toJSON = utils.omit(this._userProvidedOptions.toJSON, ['transform']);
}
const _schema = new Schema(_typeDef, childSchemaOptions);
@@ -1345,10 +1346,10 @@ Schema.prototype.interpretAsType = function(path, obj, options) {
childSchemaOptions.strictQuery = options.strictQuery;
}
if (options.hasOwnProperty('toObject')) {
- childSchemaOptions.toObject = options.toObject;
+ childSchemaOptions.toObject = utils.omit(options.toObject, ['transform']);
}
if (options.hasOwnProperty('toJSON')) {
- childSchemaOptions.toJSON = options.toJSON;
+ childSchemaOptions.toJSON = utils.omit(options.toJSON, ['transform']);
}
if (this._userProvidedOptions.hasOwnProperty('_id')) {
diff --git a/lib/schema/SubdocumentPath.js b/lib/schema/SubdocumentPath.js
index 7d6ea1a8d8f..6d166bd15bc 100644
--- a/lib/schema/SubdocumentPath.js
+++ b/lib/schema/SubdocumentPath.js
@@ -212,11 +212,15 @@ SubdocumentPath.prototype.castForQuery = function($conditional, val, context, op
return val;
}
+ const Constructor = getConstructor(this.caster, val);
+ if (val instanceof Constructor) {
+ return val;
+ }
+
if (this.options.runSetters) {
val = this._applySetters(val, context);
}
- const Constructor = getConstructor(this.caster, val);
const overrideStrict = options != null && options.strict != null ?
options.strict :
void 0;
@@ -347,6 +351,8 @@ SubdocumentPath.defaultOptions = {};
SubdocumentPath.set = SchemaType.set;
+SubdocumentPath.setters = [];
+
/**
* Attaches a getter for all SubdocumentPath instances
*
diff --git a/lib/schema/array.js b/lib/schema/array.js
index 9b292fbc3d7..4e10468a769 100644
--- a/lib/schema/array.js
+++ b/lib/schema/array.js
@@ -170,6 +170,8 @@ SchemaArray.defaultOptions = {};
*/
SchemaArray.set = SchemaType.set;
+SchemaArray.setters = [];
+
/**
* Attaches a getter for all Array instances
*
diff --git a/lib/schema/bigint.js b/lib/schema/bigint.js
index bea5abc61df..4c7dcb77039 100644
--- a/lib/schema/bigint.js
+++ b/lib/schema/bigint.js
@@ -62,6 +62,8 @@ SchemaBigInt._cast = castBigInt;
SchemaBigInt.set = SchemaType.set;
+SchemaBigInt.setters = [];
+
/**
* Attaches a getter for all BigInt instances
*
diff --git a/lib/schema/boolean.js b/lib/schema/boolean.js
index 49e4b4e67b0..316c825df45 100644
--- a/lib/schema/boolean.js
+++ b/lib/schema/boolean.js
@@ -65,6 +65,8 @@ SchemaBoolean._cast = castBoolean;
SchemaBoolean.set = SchemaType.set;
+SchemaBoolean.setters = [];
+
/**
* Attaches a getter for all Boolean instances
*
diff --git a/lib/schema/buffer.js b/lib/schema/buffer.js
index badb1bafabe..5bfaabcd2f6 100644
--- a/lib/schema/buffer.js
+++ b/lib/schema/buffer.js
@@ -70,6 +70,8 @@ SchemaBuffer._checkRequired = v => !!(v && v.length);
SchemaBuffer.set = SchemaType.set;
+SchemaBuffer.setters = [];
+
/**
* Attaches a getter for all Buffer instances
*
diff --git a/lib/schema/date.js b/lib/schema/date.js
index 41625703492..61928bb457d 100644
--- a/lib/schema/date.js
+++ b/lib/schema/date.js
@@ -70,6 +70,8 @@ SchemaDate._cast = castDate;
SchemaDate.set = SchemaType.set;
+SchemaDate.setters = [];
+
/**
* Attaches a getter for all Date instances
*
diff --git a/lib/schema/decimal128.js b/lib/schema/decimal128.js
index c0c79d5834c..650d78c2571 100644
--- a/lib/schema/decimal128.js
+++ b/lib/schema/decimal128.js
@@ -66,6 +66,8 @@ Decimal128._cast = castDecimal128;
Decimal128.set = SchemaType.set;
+Decimal128.setters = [];
+
/**
* Attaches a getter for all Decimal128 instances
*
diff --git a/lib/schema/documentarray.js b/lib/schema/documentarray.js
index 7de35055ce7..7f8a39a04d6 100644
--- a/lib/schema/documentarray.js
+++ b/lib/schema/documentarray.js
@@ -605,6 +605,8 @@ DocumentArrayPath.defaultOptions = {};
DocumentArrayPath.set = SchemaType.set;
+DocumentArrayPath.setters = [];
+
/**
* Attaches a getter for all DocumentArrayPath instances
*
diff --git a/lib/schema/mixed.js b/lib/schema/mixed.js
index 5d6e40cd926..bd38a286213 100644
--- a/lib/schema/mixed.js
+++ b/lib/schema/mixed.js
@@ -94,6 +94,8 @@ Mixed.get = SchemaType.get;
Mixed.set = SchemaType.set;
+Mixed.setters = [];
+
/**
* Casts `val` for Mixed.
*
diff --git a/lib/schema/number.js b/lib/schema/number.js
index 48d3ff7ffda..b2695cd94a6 100644
--- a/lib/schema/number.js
+++ b/lib/schema/number.js
@@ -67,6 +67,8 @@ SchemaNumber.get = SchemaType.get;
SchemaNumber.set = SchemaType.set;
+SchemaNumber.setters = [];
+
/*!
* ignore
*/
diff --git a/lib/schema/objectid.js b/lib/schema/objectid.js
index 15e41eaefcd..f0a0b6be747 100644
--- a/lib/schema/objectid.js
+++ b/lib/schema/objectid.js
@@ -94,6 +94,8 @@ ObjectId.get = SchemaType.get;
ObjectId.set = SchemaType.set;
+ObjectId.setters = [];
+
/**
* Adds an auto-generated ObjectId default if turnOn is true.
* @param {Boolean} turnOn auto generated ObjectId defaults
diff --git a/lib/schema/string.js b/lib/schema/string.js
index bf1b6d85749..da3ef8a181a 100644
--- a/lib/schema/string.js
+++ b/lib/schema/string.js
@@ -143,6 +143,8 @@ SchemaString.get = SchemaType.get;
SchemaString.set = SchemaType.set;
+SchemaString.setters = [];
+
/*!
* ignore
*/
@@ -242,7 +244,7 @@ SchemaString.prototype.enum = function() {
const vals = this.enumValues;
this.enumValidator = function(v) {
- return undefined === v || ~vals.indexOf(v);
+ return null == v || ~vals.indexOf(v);
};
this.validators.push({
validator: this.enumValidator,
diff --git a/lib/schema/uuid.js b/lib/schema/uuid.js
index 7656ceccec6..9de95f6cd4d 100644
--- a/lib/schema/uuid.js
+++ b/lib/schema/uuid.js
@@ -194,6 +194,8 @@ SchemaUUID.get = SchemaType.get;
SchemaUUID.set = SchemaType.set;
+SchemaUUID.setters = [];
+
/**
* Get/set the function used to cast arbitrary values to UUIDs.
*
diff --git a/lib/schematype.js b/lib/schematype.js
index 76664b77529..ef22979203b 100644
--- a/lib/schematype.js
+++ b/lib/schematype.js
@@ -48,7 +48,9 @@ function SchemaType(path, options, instance) {
this.getters = this.constructor.hasOwnProperty('getters') ?
this.constructor.getters.slice() :
[];
- this.setters = [];
+ this.setters = this.constructor.hasOwnProperty('setters') ?
+ this.constructor.setters.slice() :
+ [];
this.splitPath();
@@ -80,7 +82,11 @@ function SchemaType(path, options, instance) {
const keys = Object.keys(this.options);
for (const prop of keys) {
if (prop === 'cast') {
- this.castFunction(this.options[prop]);
+ if (Array.isArray(this.options[prop])) {
+ this.castFunction.apply(this, this.options[prop]);
+ } else {
+ this.castFunction(this.options[prop]);
+ }
continue;
}
if (utils.hasUserDefinedProperty(this.options, prop) && typeof this[prop] === 'function') {
@@ -253,14 +259,24 @@ SchemaType.cast = function cast(caster) {
* @api public
*/
-SchemaType.prototype.castFunction = function castFunction(caster) {
+SchemaType.prototype.castFunction = function castFunction(caster, message) {
if (arguments.length === 0) {
return this._castFunction;
}
+
if (caster === false) {
caster = this.constructor._defaultCaster || (v => v);
}
- this._castFunction = caster;
+ if (typeof caster === 'string') {
+ this._castErrorMessage = caster;
+ return this._castFunction;
+ }
+ if (caster != null) {
+ this._castFunction = caster;
+ }
+ if (message != null) {
+ this._castErrorMessage = message;
+ }
return this._castFunction;
};
diff --git a/lib/types/ArraySubdocument.js b/lib/types/ArraySubdocument.js
index 55889caa839..94ce5bcc5b4 100644
--- a/lib/types/ArraySubdocument.js
+++ b/lib/types/ArraySubdocument.js
@@ -33,7 +33,15 @@ function ArraySubdocument(obj, parentArr, skipId, fields, index) {
this.$setIndex(index);
this.$__parent = this[documentArrayParent];
- Subdocument.call(this, obj, fields, this[documentArrayParent], skipId, { isNew: true });
+ let options;
+ if (typeof skipId === 'object' && skipId != null) {
+ options = { isNew: true, ...skipId };
+ skipId = undefined;
+ } else {
+ options = { isNew: true };
+ }
+
+ Subdocument.call(this, obj, fields, this[documentArrayParent], skipId, options);
}
/*!
diff --git a/lib/types/DocumentArray/methods/index.js b/lib/types/DocumentArray/methods/index.js
index d71c5e06d1f..109f3450a0f 100644
--- a/lib/types/DocumentArray/methods/index.js
+++ b/lib/types/DocumentArray/methods/index.js
@@ -38,7 +38,7 @@ const methods = {
* @memberOf MongooseDocumentArray
*/
- _cast(value, index) {
+ _cast(value, index, options) {
if (this[arraySchemaSymbol] == null) {
return value;
}
@@ -89,7 +89,7 @@ const methods = {
if (Constructor.$isMongooseDocumentArray) {
return Constructor.cast(value, this, undefined, undefined, index);
}
- const ret = new Constructor(value, this, undefined, undefined, index);
+ const ret = new Constructor(value, this, options, undefined, index);
ret.isNew = true;
return ret;
},
diff --git a/lib/types/array/methods/index.js b/lib/types/array/methods/index.js
index 61e18c958cd..deb4e801f29 100644
--- a/lib/types/array/methods/index.js
+++ b/lib/types/array/methods/index.js
@@ -595,7 +595,7 @@ const methods = {
*/
pull() {
- const values = [].map.call(arguments, this._cast, this);
+ const values = [].map.call(arguments, (v, i) => this._cast(v, i, { defaults: false }), this);
const cur = this[arrayParentSymbol].get(this[arrayPathSymbol]);
let i = cur.length;
let mem;
diff --git a/lib/types/subdocument.js b/lib/types/subdocument.js
index b0f0789bbf6..690457f69bd 100644
--- a/lib/types/subdocument.js
+++ b/lib/types/subdocument.js
@@ -16,6 +16,10 @@ module.exports = Subdocument;
*/
function Subdocument(value, fields, parent, skipId, options) {
+ if (typeof skipId === 'object' && skipId != null && options == null) {
+ options = skipId;
+ skipId = undefined;
+ }
if (parent != null) {
// If setting a nested path, should copy isNew from parent re: gh-7048
const parentOptions = { isNew: parent.isNew };
diff --git a/package.json b/package.json
index 9d29e912292..bc42e1ce8f4 100644
--- a/package.json
+++ b/package.json
@@ -1,7 +1,7 @@
{
"name": "mongoose",
"description": "Mongoose MongoDB ODM",
- "version": "7.3.4",
+ "version": "7.4.1",
"author": "Guillermo Rauch ",
"keywords": [
"mongodb",
@@ -19,9 +19,9 @@
],
"license": "MIT",
"dependencies": {
- "bson": "^5.3.0",
+ "bson": "^5.4.0",
"kareem": "2.5.1",
- "mongodb": "5.6.0",
+ "mongodb": "5.7.0",
"mpath": "0.9.0",
"mquery": "5.0.0",
"ms": "2.1.3",
diff --git a/test/connection.test.js b/test/connection.test.js
index 114cfbb2a2b..30b5f640f81 100644
--- a/test/connection.test.js
+++ b/test/connection.test.js
@@ -6,6 +6,7 @@
const start = require('./common');
+const STATES = require('../lib/connectionstate');
const Q = require('q');
const assert = require('assert');
const mongodb = require('mongodb');
@@ -746,6 +747,35 @@ describe('connections:', function() {
assert.strictEqual(db2, db3);
return db.close();
});
+
+ it('supports removing db (gh-11821)', async function() {
+ const db = await mongoose.createConnection(start.uri).asPromise();
+
+ const schema = mongoose.Schema({ name: String }, { autoCreate: false, autoIndex: false });
+ const Test = db.model('Test', schema);
+ await Test.deleteMany({});
+ await Test.create({ name: 'gh-11821' });
+
+ const db2 = db.useDb(start.databases[1]);
+ const Test2 = db2.model('Test', schema);
+
+ await Test2.deleteMany({});
+ let doc = await Test2.findOne();
+ assert.equal(doc, null);
+
+ db.removeDb(start.databases[1]);
+ assert.equal(db2.readyState, STATES.disconnected);
+ assert.equal(db.readyState, STATES.connected);
+ await assert.rejects(
+ () => Test2.findOne(),
+ /Connection was force closed/
+ );
+
+ doc = await Test.findOne();
+ assert.equal(doc.name, 'gh-11821');
+
+ await db.close();
+ });
});
describe('shouldAuthenticate()', function() {
diff --git a/test/docs/validation.test.js b/test/docs/validation.test.js
index fd4607b4925..20e654a4f34 100644
--- a/test/docs/validation.test.js
+++ b/test/docs/validation.test.js
@@ -14,6 +14,8 @@ describe('validation docs', function() {
});
});
+ beforeEach(() => db.deleteModel(/Vehicle/));
+
after(async function() {
await db.close();
});
@@ -384,6 +386,53 @@ describe('validation docs', function() {
// acquit:ignore:end
});
+ it('Cast Error Message Overwrite', function() {
+ const vehicleSchema = new mongoose.Schema({
+ numWheels: {
+ type: Number,
+ cast: '{VALUE} is not a number'
+ }
+ });
+ const Vehicle = db.model('Vehicle', vehicleSchema);
+
+ const doc = new Vehicle({ numWheels: 'pie' });
+ const err = doc.validateSync();
+
+ err.errors['numWheels'].name; // 'CastError'
+ // "pie" is not a number
+ err.errors['numWheels'].message;
+ // acquit:ignore:start
+ assert.equal(err.errors['numWheels'].name, 'CastError');
+ assert.equal(err.errors['numWheels'].message,
+ '"pie" is not a number');
+ db.deleteModel(/Vehicle/);
+ // acquit:ignore:end
+ });
+
+ /* eslint-disable no-unused-vars */
+ it('Cast Error Message Function Overwrite', function() {
+ const vehicleSchema = new mongoose.Schema({
+ numWheels: {
+ type: Number,
+ cast: [null, (value, path, model, kind) => `"${value}" is not a number`]
+ }
+ });
+ const Vehicle = db.model('Vehicle', vehicleSchema);
+
+ const doc = new Vehicle({ numWheels: 'pie' });
+ const err = doc.validateSync();
+
+ err.errors['numWheels'].name; // 'CastError'
+ // "pie" is not a number
+ err.errors['numWheels'].message;
+ // acquit:ignore:start
+ assert.equal(err.errors['numWheels'].name, 'CastError');
+ assert.equal(err.errors['numWheels'].message,
+ '"pie" is not a number');
+ db.deleteModel(/Vehicle/);
+ // acquit:ignore:end
+ });
+
it('Global SchemaType Validation', async function() {
// Add a custom validator to all strings
mongoose.Schema.Types.String.set('validate', v => v == null || v > 0);
diff --git a/test/document.test.js b/test/document.test.js
index 578726d8f3f..4207ebec68a 100644
--- a/test/document.test.js
+++ b/test/document.test.js
@@ -793,14 +793,21 @@ describe('document', function() {
assert.strictEqual(myModel.toObject().foo, void 0);
});
- it('should propogate toObject to implicitly created schemas gh-13325', async function() {
+ it('should propogate toObject to implicitly created schemas (gh-13599) (gh-13325)', async function() {
+ const transformCalls = [];
const userSchema = Schema({
firstName: String,
company: {
type: { companyId: { type: Schema.Types.ObjectId }, companyName: String }
}
}, {
- toObject: { virtuals: true }
+ toObject: {
+ virtuals: true,
+ transform(doc, ret) {
+ transformCalls.push(doc);
+ return ret;
+ }
+ }
});
userSchema.virtual('company.details').get(() => 42);
@@ -809,6 +816,8 @@ describe('document', function() {
const user = new User({ firstName: 'test', company: { companyName: 'foo' } });
const obj = user.toObject();
assert.strictEqual(obj.company.details, 42);
+ assert.equal(transformCalls.length, 1);
+ assert.strictEqual(transformCalls[0], user);
});
});
@@ -996,7 +1005,8 @@ describe('document', function() {
assert.equal(foundAlicJson.friends, undefined);
assert.equal(foundAlicJson.name, 'Alic');
});
- it('should propogate toJSON to implicitly created schemas gh-13325', async function() {
+ it('should propogate toJSON to implicitly created schemas (gh-13599) (gh-13325)', async function() {
+ const transformCalls = [];
const userSchema = Schema({
firstName: String,
company: {
@@ -1004,7 +1014,13 @@ describe('document', function() {
}
}, {
id: false,
- toJSON: { virtuals: true }
+ toJSON: {
+ virtuals: true,
+ transform(doc, ret) {
+ transformCalls.push(doc);
+ return ret;
+ }
+ }
});
userSchema.virtual('company.details').get(() => 'foo');
@@ -1016,6 +1032,8 @@ describe('document', function() {
});
const obj = doc.toJSON();
assert.strictEqual(obj.company.details, 'foo');
+ assert.equal(transformCalls.length, 1);
+ assert.strictEqual(transformCalls[0], doc);
});
});
@@ -7617,7 +7635,11 @@ describe('document', function() {
schema.path('createdAt').immutable(true);
assert.ok(schema.path('createdAt').$immutable);
- assert.equal(schema.path('createdAt').setters.length, 1);
+ assert.equal(
+ schema.path('createdAt').setters.length,
+ 1,
+ schema.path('createdAt').setters.map(setter => setter.toString())
+ );
schema.path('createdAt').immutable(false);
assert.ok(!schema.path('createdAt').$immutable);
@@ -12215,6 +12237,40 @@ describe('document', function() {
assert.equal(fromDb.c.x.y, 1);
});
+ it('can change the value of the id property on documents gh-10096', async function() {
+ const testSchema = new Schema({
+ name: String
+ });
+ const Test = db.model('Test', testSchema);
+ const doc = new Test({ name: 'Test Testerson ' });
+ const oldVal = doc.id;
+ doc.id = '648b8aa6a97549b03835c0b3';
+ await doc.save();
+ assert.notEqual(oldVal, doc.id);
+ assert.equal(doc.id, '648b8aa6a97549b03835c0b3');
+ });
+
+ it('should allow storing keys with dots in name in mixed under nested (gh-13530)', async function() {
+ const TestModelSchema = new mongoose.Schema({
+ metadata:
+ {
+ labels: mongoose.Schema.Types.Mixed
+ }
+ });
+ const TestModel = db.model('Test', TestModelSchema);
+ const { _id } = await TestModel.create({
+ metadata: {
+ labels: { 'my.label.com': 'true' }
+ }
+ });
+ const doc = await TestModel.findById(_id).lean();
+ assert.deepStrictEqual(doc.metadata, {
+ labels: {
+ 'my.label.com': 'true'
+ }
+ });
+ });
+
it('cleans up all array subdocs modified state on save (gh-13582)', async function() {
const ElementSchema = new mongoose.Schema({
elementName: String
@@ -12234,6 +12290,29 @@ describe('document', function() {
assert.deepStrictEqual(doc.elements[1].modifiedPaths(), []);
});
+ it('cleans up all nested subdocs modified state on save (gh-13609)', async function() {
+ const TwoElementSchema = new mongoose.Schema({
+ elementName: String
+ });
+
+ const TwoNestedSchema = new mongoose.Schema({
+ nestedName: String,
+ elements: [TwoElementSchema]
+ });
+
+ const TwoDocDefaultSchema = new mongoose.Schema({
+ docName: String,
+ nested: { type: TwoNestedSchema, default: {} }
+ });
+
+ const Test = db.model('Test', TwoDocDefaultSchema);
+ const doc = new Test({ docName: 'MyDocName' });
+ doc.nested.nestedName = 'qdwqwd';
+ doc.nested.elements.push({ elementName: 'ElementName1' });
+ await doc.save();
+ assert.deepStrictEqual(doc.nested.modifiedPaths(), []);
+ });
+
it('avoids prototype pollution on init', async function() {
const Example = db.model('Example', new Schema({ hello: String }));
diff --git a/test/helpers/applyWriteConcern.test.js b/test/helpers/applyWriteConcern.test.js
new file mode 100644
index 00000000000..ce9e5783465
--- /dev/null
+++ b/test/helpers/applyWriteConcern.test.js
@@ -0,0 +1,22 @@
+'use strict';
+
+const assert = require('assert');
+const applyWriteConcern = require('../../lib/helpers/schema/applyWriteConcern');
+const start = require('../common');
+const mongoose = start.mongoose;
+
+describe('applyWriteConcern', function() {
+ let db;
+ before(function() {
+ db = start();
+ });
+ after(async function() {
+ await db.close();
+ });
+ it('should not overwrite user specified writeConcern options (gh-13592)', async function() {
+ const options = { writeConcern: { w: 'majority' } };
+ const testSchema = new mongoose.Schema({ name: String }, { writeConcern: { w: 0 } });
+ applyWriteConcern(testSchema, options);
+ assert.deepStrictEqual({ writeConcern: { w: 'majority' } }, options);
+ });
+});
diff --git a/test/model.create.test.js b/test/model.create.test.js
index 20036fb5397..d587e70ae16 100644
--- a/test/model.create.test.js
+++ b/test/model.create.test.js
@@ -197,6 +197,66 @@ describe('model', function() {
const docs = await Test.find();
assert.equal(docs.length, 5);
});
+ it('should throw an error only after all the documents have finished saving gh-4628', async function() {
+ const testSchema = new Schema({ name: { type: String, unique: true } });
+
+
+ const Test = db.model('gh4628Test', testSchema);
+ await Test.init();
+ const data = [];
+ for (let i = 0; i < 11; i++) {
+ data.push({ name: 'Test' + Math.abs(i - 4) });
+ }
+ await Test.create(data, { ordered: false }).catch(err => err);
+ const docs = await Test.find();
+ assert.equal(docs.length, 7); // docs 1,2,3,4 should not go through 11-4 == 7
+ });
+
+ it('should return the first error immediately if "aggregateErrors" is not explicitly set (ordered)', async function() {
+ const testSchema = new Schema({ name: { type: String, required: true } });
+
+ const TestModel = db.model('gh1731-1', testSchema);
+
+ const res = await TestModel.create([{ name: 'test' }, {}, { name: 'another' }], { ordered: true }).then(null).catch(err => err);
+
+ assert.ok(res instanceof mongoose.Error.ValidationError);
+ });
+
+ it('should not return errors immediately if "aggregateErrors" is "true" (ordered)', async function() {
+ const testSchema = new Schema({ name: { type: String, required: true } });
+
+ const TestModel = db.model('gh1731-2', testSchema);
+
+ const res = await TestModel.create([{ name: 'test' }, {}, { name: 'another' }], { ordered: true, aggregateErrors: true });
+
+ assert.equal(res.length, 3);
+ assert.ok(res[0] instanceof mongoose.Document);
+ assert.ok(res[1] instanceof mongoose.Error.ValidationError);
+ assert.ok(res[2] instanceof mongoose.Document);
+ });
+ });
+
+ it('should return the first error immediately if "aggregateErrors" is not explicitly set', async function() {
+ const testSchema = new Schema({ name: { type: String, required: true } });
+
+ const TestModel = db.model('gh1731-3', testSchema);
+
+ const res = await TestModel.create([{ name: 'test' }, {}, { name: 'another' }], {}).then(null).catch(err => err);
+
+ assert.ok(res instanceof mongoose.Error.ValidationError);
+ });
+
+ it('should not return errors immediately if "aggregateErrors" is "true"', async function() {
+ const testSchema = new Schema({ name: { type: String, required: true } });
+
+ const TestModel = db.model('gh1731-4', testSchema);
+
+ const res = await TestModel.create([{ name: 'test' }, {}, { name: 'another' }], { aggregateErrors: true });
+
+ assert.equal(res.length, 3);
+ assert.ok(res[0] instanceof mongoose.Document);
+ assert.ok(res[1] instanceof mongoose.Error.ValidationError);
+ assert.ok(res[2] instanceof mongoose.Document);
});
});
});
diff --git a/test/model.findOneAndDelete.test.js b/test/model.findOneAndDelete.test.js
index 1653c3e8a3d..38986a9ca33 100644
--- a/test/model.findOneAndDelete.test.js
+++ b/test/model.findOneAndDelete.test.js
@@ -335,4 +335,42 @@ describe('model: findOneAndDelete:', function() {
assert.equal(postCount, 1);
});
});
+
+ it('supports the `includeResultMetadata` option (gh-13539)', async function() {
+ const testSchema = new mongoose.Schema({
+ name: String
+ });
+ const Test = db.model('Test', testSchema);
+ await Test.create({ name: 'Test' });
+ const doc = await Test.findOneAndDelete(
+ { name: 'Test' },
+ { includeResultMetadata: false }
+ );
+ assert.equal(doc.ok, undefined);
+ assert.equal(doc.name, 'Test');
+
+ await Test.create({ name: 'Test' });
+ let data = await Test.findOneAndDelete(
+ { name: 'Test' },
+ { includeResultMetadata: true }
+ );
+ assert(data.ok);
+ assert.equal(data.value.name, 'Test');
+
+ await Test.create({ name: 'Test' });
+ data = await Test.findOneAndDelete(
+ { name: 'Test' },
+ { includeResultMetadata: true, rawResult: true }
+ );
+ assert(data.ok);
+ assert.equal(data.value.name, 'Test');
+
+ await assert.rejects(
+ () => Test.findOneAndDelete(
+ { name: 'Test' },
+ { includeResultMetadata: false, rawResult: true }
+ ),
+ /Cannot set `rawResult` option when `includeResultMetadata` is false/
+ );
+ });
});
diff --git a/test/model.findOneAndReplace.test.js b/test/model.findOneAndReplace.test.js
index d463a0b5924..5550f198915 100644
--- a/test/model.findOneAndReplace.test.js
+++ b/test/model.findOneAndReplace.test.js
@@ -451,4 +451,46 @@ describe('model: findOneAndReplace:', function() {
assert.ok(!Object.keys(opts).includes('overwrite'));
assert.ok(!Object.keys(opts).includes('timestamps'));
});
+
+ it('supports the `includeResultMetadata` option (gh-13539)', async function() {
+ const testSchema = new mongoose.Schema({
+ name: String
+ });
+ const Test = db.model('Test', testSchema);
+ await Test.create({
+ name: 'Test'
+ });
+ const doc = await Test.findOneAndReplace(
+ { name: 'Test' },
+ { name: 'Test Testerson' },
+ { new: true, upsert: true, includeResultMetadata: false }
+ );
+ assert.equal(doc.ok, undefined);
+ assert.equal(doc.name, 'Test Testerson');
+
+ let data = await Test.findOneAndReplace(
+ { name: 'Test Testerson' },
+ { name: 'Test' },
+ { new: true, upsert: true, includeResultMetadata: true }
+ );
+ assert(data.ok);
+ assert.equal(data.value.name, 'Test');
+
+ data = await Test.findOneAndReplace(
+ { name: 'Test Testerson' },
+ { name: 'Test' },
+ { new: true, upsert: true, includeResultMetadata: true, rawResult: true }
+ );
+ assert(data.ok);
+ assert.equal(data.value.name, 'Test');
+
+ await assert.rejects(
+ () => Test.findOneAndReplace(
+ { name: 'Test Testerson' },
+ { name: 'Test' },
+ { new: true, upsert: true, includeResultMetadata: false, rawResult: true }
+ ),
+ /Cannot set `rawResult` option when `includeResultMetadata` is false/
+ );
+ });
});
diff --git a/test/model.findOneAndUpdate.test.js b/test/model.findOneAndUpdate.test.js
index 8ab0c67244b..18f9e5e8692 100644
--- a/test/model.findOneAndUpdate.test.js
+++ b/test/model.findOneAndUpdate.test.js
@@ -2143,4 +2143,45 @@ describe('model: findOneAndUpdate:', function() {
assert.equal(doc.info['second.name'], 'Quiz');
assert.equal(doc.info2['second.name'], 'Quiz');
});
+ it('supports the `includeResultMetadata` option (gh-13539)', async function() {
+ const testSchema = new mongoose.Schema({
+ name: String
+ });
+ const Test = db.model('Test', testSchema);
+ await Test.create({
+ name: 'Test'
+ });
+ const doc = await Test.findOneAndUpdate(
+ { name: 'Test' },
+ { name: 'Test Testerson' },
+ { new: true, upsert: true, includeResultMetadata: false }
+ );
+ assert.equal(doc.ok, undefined);
+ assert.equal(doc.name, 'Test Testerson');
+
+ let data = await Test.findOneAndUpdate(
+ { name: 'Test Testerson' },
+ { name: 'Test' },
+ { new: true, upsert: true, includeResultMetadata: true }
+ );
+ assert(data.ok);
+ assert.equal(data.value.name, 'Test');
+
+ data = await Test.findOneAndUpdate(
+ { name: 'Test Testerson' },
+ { name: 'Test' },
+ { new: true, upsert: true, includeResultMetadata: true, rawResult: true }
+ );
+ assert(data.ok);
+ assert.equal(data.value.name, 'Test');
+
+ await assert.rejects(
+ () => Test.findOneAndUpdate(
+ { name: 'Test Testerson' },
+ { name: 'Test' },
+ { new: true, upsert: true, includeResultMetadata: false, rawResult: true }
+ ),
+ /Cannot set `rawResult` option when `includeResultMetadata` is false/
+ );
+ });
});
diff --git a/test/model.insertMany.test.js b/test/model.insertMany.test.js
index 0efa1baa04b..db8c96b535c 100644
--- a/test/model.insertMany.test.js
+++ b/test/model.insertMany.test.js
@@ -26,8 +26,7 @@ describe('insertMany()', function() {
const Movie = db.model('Movie', schema);
const start = Date.now();
- const arr = [{ name: 'Star Wars' }, { name: 'The Empire Strikes Back' }];
- return Movie.insertMany(arr).
+ return Movie.insertMany([{ name: 'Star Wars' }, { name: 'The Empire Strikes Back' }]).
then(docs => {
assert.equal(docs.length, 2);
assert.ok(!docs[0].isNew);
@@ -93,8 +92,7 @@ describe('insertMany()', function() {
}, { timestamps: true });
const Movie = db.model('Movie', schema);
- const arr = [{ name: 'Star Wars' }, { name: 'The Empire Strikes Back' }];
- let docs = await Movie.insertMany(arr);
+ let docs = await Movie.insertMany([{ name: 'Star Wars' }, { name: 'The Empire Strikes Back' }]);
assert.equal(docs.length, 2);
assert.ok(!docs[0].isNew);
assert.ok(!docs[1].isNew);
@@ -299,8 +297,7 @@ describe('insertMany()', function() {
});
const Movie = db.model('Movie', schema);
- const arr = [{ name: 'Star Wars' }, { name: 'The Empire Strikes Back' }];
- let docs = await Movie.insertMany(arr);
+ let docs = await Movie.insertMany([{ name: 'Star Wars' }, { name: 'The Empire Strikes Back' }]);
assert.equal(docs.length, 2);
assert.equal(calledPre, 2);
assert.equal(calledPost, 1);
diff --git a/test/model.middleware.preposttypes.test.js b/test/model.middleware.preposttypes.test.js
index 2a2e3323cb4..3d8f6fdd77e 100644
--- a/test/model.middleware.preposttypes.test.js
+++ b/test/model.middleware.preposttypes.test.js
@@ -185,7 +185,7 @@ describe('pre/post hooks, type of this', function() {
const MongooseDocumentMiddleware = [...MongooseDistinctDocumentMiddleware, ...MongooseQueryAndDocumentMiddleware];
const MongooseDistinctQueryMiddleware = [
- 'count', 'estimatedDocumentCount', 'countDocuments',
+ 'estimatedDocumentCount', 'countDocuments',
'deleteMany', 'distinct',
'find', 'findOne', 'findOneAndDelete', 'findOneAndRemove', 'findOneAndReplace', 'findOneAndUpdate',
'replaceOne', 'updateMany'];
@@ -278,7 +278,6 @@ describe('pre/post hooks, type of this', function() {
await doc.save(); // triggers save and validate hooks
// MongooseDistinctQueryMiddleware
- await Doc.count().exec();
await Doc.estimatedDocumentCount().exec();
await Doc.countDocuments().exec();
await Doc.deleteMany().exec(); await Doc.create({ data: 'value' });
diff --git a/test/model.test.js b/test/model.test.js
index 2042b5e8b2b..7d362a6797c 100644
--- a/test/model.test.js
+++ b/test/model.test.js
@@ -208,8 +208,8 @@ describe('Model', function() {
describe('defaults', function() {
it('to a non-empty array', function() {
const DefaultArraySchema = new Schema({
- arr: { type: Array, cast: String, default: ['a', 'b', 'c'] },
- single: { type: Array, cast: String, default: ['a'] }
+ arr: { type: Array, default: ['a', 'b', 'c'] },
+ single: { type: Array, default: ['a'] }
});
const DefaultArray = db.model('Test', DefaultArraySchema);
const arr = new DefaultArray();
@@ -223,7 +223,7 @@ describe('Model', function() {
it('empty', function() {
const DefaultZeroCardArraySchema = new Schema({
- arr: { type: Array, cast: String, default: [] },
+ arr: { type: Array, default: [] },
auto: [Number]
});
const DefaultZeroCardArray = db.model('Test', DefaultZeroCardArraySchema);
@@ -2319,7 +2319,7 @@ describe('Model', function() {
const title = 'interop ad-hoc as promise';
const created = await BlogPost.create({ title: title });
- const query = BlogPost.count({ title: title });
+ const query = BlogPost.countDocuments({ title: title });
const found = await query.exec('findOne');
assert.equal(found.id, created.id);
});
@@ -5304,7 +5304,7 @@ describe('Model', function() {
}
]);
- const beforeExpirationCount = await Test.count({});
+ const beforeExpirationCount = await Test.countDocuments({});
assert.ok(beforeExpirationCount === 12);
let intervalid;
@@ -5318,7 +5318,7 @@ describe('Model', function() {
// in case it happens faster, to reduce test time
new Promise(resolve => {
intervalid = setInterval(async() => {
- const count = await Test.count({});
+ const count = await Test.countDocuments({});
if (count === 0) {
resolve();
}
@@ -5328,7 +5328,7 @@ describe('Model', function() {
clearInterval(intervalid);
- const afterExpirationCount = await Test.count({});
+ const afterExpirationCount = await Test.countDocuments({});
assert.equal(afterExpirationCount, 0);
});
@@ -5732,6 +5732,52 @@ describe('Model', function() {
});
+ it('bulkwrite should not change updatedAt on subdocs when timestamps set to false (gh-13611)', async function() {
+
+ const postSchema = new Schema({
+ title: String,
+ category: String,
+ isDeleted: Boolean
+ }, { timestamps: true });
+
+ const userSchema = new Schema({
+ name: String,
+ isDeleted: Boolean,
+ posts: { type: [postSchema] }
+ }, { timestamps: true });
+
+ const User = db.model('gh13611User', userSchema);
+
+ const entry = await User.create({
+ name: 'Test Testerson',
+ posts: [{ title: 'title a', category: 'a', isDeleted: false }, { title: 'title b', category: 'b', isDeleted: false }],
+ isDeleted: false
+ });
+ const initialTime = entry.posts[0].updatedAt;
+ await delay(10);
+
+ await User.bulkWrite([{
+ updateMany: {
+ filter: {
+ isDeleted: false
+ },
+ update: {
+ 'posts.$[post].isDeleted': true
+ },
+ arrayFilters: [
+ {
+ 'post.category': { $eq: 'a' }
+ }
+ ],
+ upsert: false,
+ timestamps: false
+ }
+ }]);
+ const res = await User.findOne({ _id: entry._id });
+ const currentTime = res.posts[0].updatedAt;
+ assert.equal(initialTime.getTime(), currentTime.getTime());
+ });
+
it('bulkWrite can overwrite schema `strict` option for filters and updates (gh-8778)', async function() {
// Arrange
const userSchema = new Schema({
@@ -7028,6 +7074,19 @@ describe('Model', function() {
assert(bypass);
});
});
+
+ it('respects schema-level `collectionOptions` for setting options to createCollection()', async function() {
+ const testSchema = new Schema({
+ name: String
+ }, { collectionOptions: { capped: true, size: 1024 } });
+ const TestModel = db.model('Test', testSchema);
+ await TestModel.init();
+ await TestModel.collection.drop();
+ await TestModel.createCollection();
+
+ const isCapped = await TestModel.collection.isCapped();
+ assert.ok(isCapped);
+ });
});
diff --git a/test/query.test.js b/test/query.test.js
index d37a917d111..9d7ee1da6ac 100644
--- a/test/query.test.js
+++ b/test/query.test.js
@@ -1916,7 +1916,6 @@ describe('Query', function() {
const TestSchema = new Schema({ name: String });
const ops = [
- 'count',
'find',
'findOne',
'findOneAndRemove',
@@ -4105,7 +4104,36 @@ describe('Query', function() {
await Error.find().sort('-');
}, { message: 'Invalid field "" passed to sort()' });
});
+ it('allows executing a find() with a subdocument with defaults disabled (gh-13512)', async function() {
+ const schema = mongoose.Schema({
+ title: String,
+ bookHolder: mongoose.Schema({
+ isReading: Boolean,
+ tags: [String]
+ })
+ });
+ const Test = db.model('Test', schema);
+
+ const BookHolder = schema.path('bookHolder').caster;
+
+ await Test.collection.insertOne({
+ title: 'test-defaults-disabled',
+ bookHolder: { isReading: true }
+ });
+ // Create a new BookHolder subdocument, skip applying defaults
+ // Otherwise, default `[]` for `tags` would cause this query to
+ // return no results.
+ const bookHolder = new BookHolder(
+ { isReading: true },
+ null,
+ null,
+ { defaults: false }
+ );
+ const doc = await Test.findOne({ bookHolder });
+ assert.ok(doc);
+ assert.equal(doc.title, 'test-defaults-disabled');
+ });
it('throws a readable error when executing Query instance without a model (gh-13570)', async function() {
const schema = new Schema({ name: String });
const M = db.model('Test', schema, 'Test');
diff --git a/test/query.toconstructor.test.js b/test/query.toconstructor.test.js
index 39d24838436..1de9f78f152 100644
--- a/test/query.toconstructor.test.js
+++ b/test/query.toconstructor.test.js
@@ -162,6 +162,7 @@ describe('Query:', function() {
});
const Test = db.model('Test', schema);
+ await Test.init();
await Test.deleteMany({});
const test = new Test({ name: 'Romero' });
const Q = Test.findOne({}).toConstructor();
diff --git a/test/schema.alias.test.js b/test/schema.alias.test.js
index 0f37f595c1a..0e9a96e6a5b 100644
--- a/test/schema.alias.test.js
+++ b/test/schema.alias.test.js
@@ -183,4 +183,21 @@ describe('schema alias option', function() {
assert.ok(schema.virtuals['name1']);
assert.ok(schema.virtuals['name2']);
});
+ it('should disable the id virtual entirely if there\'s a field with alias `id` gh-13650', async function() {
+
+ const testSchema = new Schema({
+ _id: {
+ type: String,
+ required: true,
+ set: (val) => val.replace(/\s+/g, ' '),
+ alias: 'id'
+ }
+ });
+ const Test = db.model('gh13650', testSchema);
+ const doc = new Test({
+ id: 'H-1'
+ });
+ await doc.save();
+ assert.ok(doc);
+ });
});
diff --git a/test/schema.test.js b/test/schema.test.js
index 8752374b866..da6055067bf 100644
--- a/test/schema.test.js
+++ b/test/schema.test.js
@@ -2396,6 +2396,25 @@ describe('schema', function() {
assert.ok(threw);
});
+ it('with function cast error format', function() {
+ const schema = Schema({
+ num: {
+ type: Number,
+ cast: [null, value => `${value} isn't a number`]
+ }
+ });
+
+ let threw = false;
+ try {
+ schema.path('num').cast('horseradish');
+ } catch (err) {
+ threw = true;
+ assert.equal(err.name, 'CastError');
+ assert.equal(err.message, 'horseradish isn\'t a number');
+ }
+ assert.ok(threw);
+ });
+
it('with objectids', function() {
const schema = Schema({
userId: {
@@ -3078,4 +3097,14 @@ describe('schema', function() {
assert.ok(res[0].tags.createdAt);
assert.ok(res[0].tags.updatedAt);
});
+ it('should not save objectids as strings when using the `flattenObjectIds` option (gh-13648)', async function() {
+ const testSchema = new Schema({
+ name: String
+ }, { toObject: { flattenObjectIds: true } });
+ const Test = db.model('gh13648', testSchema);
+
+ const doc = await Test.create({ name: 'Test Testerson' });
+ const res = await Test.findOne({ _id: { $eq: doc._id, $type: 'objectId' } });
+ assert.equal(res.name, 'Test Testerson');
+ });
});
diff --git a/test/schema.validation.test.js b/test/schema.validation.test.js
index 22fab457bd1..e467f6ea088 100644
--- a/test/schema.validation.test.js
+++ b/test/schema.validation.test.js
@@ -827,6 +827,19 @@ describe('schema', function() {
}
});
+ it('should allow null values for enum gh-3044', async function() {
+ const testSchema = new Schema({
+ name: {
+ type: String,
+ enum: ['test']
+ }
+ });
+ const Test = mongoose.model('allow-null' + random(), testSchema);
+ const a = new Test({ name: null });
+ const err = await a.validate().then(() => null, err => err);
+ assert.equal(err, null);
+ });
+
it('should allow an array of subdocuments with enums (gh-3521)', async function() {
const coolSchema = new Schema({
votes: [{
diff --git a/test/schematype.test.js b/test/schematype.test.js
index 7cb806cada8..13eb8c5dce4 100644
--- a/test/schematype.test.js
+++ b/test/schematype.test.js
@@ -4,7 +4,9 @@
* Module dependencies.
*/
-const mongoose = require('./common').mongoose;
+const start = require('./common');
+
+const mongoose = start.mongoose;
const assert = require('assert');
@@ -211,6 +213,25 @@ describe('schematype', function() {
it('SchemaType.set, is a function', () => {
assert.equal(typeof mongoose.SchemaType.set, 'function');
});
+ it('should allow setting values to a given property gh-13510', async function() {
+ const m = new mongoose.Mongoose();
+ await m.connect(start.uri);
+ m.SchemaTypes.Date.setters.push(v => typeof v === 'string' && /^\d{8}$/.test(v) ? new Date(v.slice(0, 4), +v.slice(4, 6) - 1, v.slice(6, 8)) : v);
+ const testSchema = new m.Schema({
+ myDate: Date
+ });
+ const Test = m.model('Test', testSchema);
+ await Test.deleteMany({});
+ const doc = new Test();
+ doc.myDate = '20220601';
+ await doc.save();
+ await m.connections[0].close();
+ assert(doc.myDate instanceof Date);
+ });
+
+ after(() => {
+ mongoose.SchemaTypes.Date.setters = [];
+ });
});
const typesToTest = Object.values(mongoose.SchemaTypes).
diff --git a/test/timestamps.test.js b/test/timestamps.test.js
index 027d7601f2c..49ab3e82762 100644
--- a/test/timestamps.test.js
+++ b/test/timestamps.test.js
@@ -608,12 +608,13 @@ describe('timestamps', function() {
});
it('should change updatedAt when findOneAndUpdate', async function() {
+ await Cat.deleteMany({});
await Cat.create({ name: 'test123' });
let doc = await Cat.findOne({ name: 'test123' });
const old = doc.updatedAt;
+ await new Promise(resolve => setTimeout(resolve, 10));
doc = await Cat.findOneAndUpdate({ name: 'test123' }, { $set: { hobby: 'fish' } }, { new: true });
- assert.ok(doc.updatedAt.getTime() > old.getTime());
-
+ assert.ok(doc.updatedAt.getTime() > old.getTime(), `Expected ${doc.updatedAt} > ${old}`);
});
it('insertMany with createdAt off (gh-6381)', async function() {
diff --git a/test/types/create.test.ts b/test/types/create.test.ts
index 3686bf077f8..c0a7b0e5630 100644
--- a/test/types/create.test.ts
+++ b/test/types/create.test.ts
@@ -1,4 +1,4 @@
-import { Schema, model, Types, CallbackError } from 'mongoose';
+import { Schema, model, Types, HydratedDocument } from 'mongoose';
import { expectError, expectType } from 'tsd';
const schema = new Schema({ name: { type: 'String' } });
@@ -118,3 +118,10 @@ Test.insertMany({ _id: new Types.ObjectId('000000000000000000000000'), name: 'te
(await Test.create([{ name: 'test' }]))[0];
(await Test.create({ name: 'test' }))._id;
})();
+
+async function createWithAggregateErrors() {
+ expectType<(HydratedDocument)[]>(await Test.create([{}]));
+ expectType<(HydratedDocument | Error)[]>(await Test.create([{}], { aggregateErrors: true }));
+}
+
+createWithAggregateErrors();
diff --git a/test/types/middleware.test.ts b/test/types/middleware.test.ts
index 053fd9a9438..a0f3e53caa1 100644
--- a/test/types/middleware.test.ts
+++ b/test/types/middleware.test.ts
@@ -1,5 +1,5 @@
import { Schema, model, Model, Document, SaveOptions, Query, Aggregate, HydratedDocument, PreSaveMiddlewareFunction } from 'mongoose';
-import { expectError, expectType, expectNotType } from 'tsd';
+import { expectError, expectType, expectNotType, expectAssignable } from 'tsd';
const preMiddlewareFn: PreSaveMiddlewareFunction = function(next, opts) {
this.$markValid('name');
@@ -173,3 +173,13 @@ function gh11257() {
this.find();
});
}
+
+function gh13601() {
+ const testSchema = new Schema({
+ name: String
+ });
+
+ testSchema.pre('deleteOne', { document: true }, function() {
+ expectAssignable(this);
+ });
+}
diff --git a/test/types/mongo.test.ts b/test/types/mongo.test.ts
index 320ae24501b..f622ca6598c 100644
--- a/test/types/mongo.test.ts
+++ b/test/types/mongo.test.ts
@@ -1,4 +1,19 @@
import * as mongoose from 'mongoose';
import { expectType } from 'tsd';
+import * as bson from 'bson';
import GridFSBucket = mongoose.mongo.GridFSBucket;
+
+function gh12537() {
+ const schema = new mongoose.Schema({ test: String });
+ const model = mongoose.model('Test', schema);
+
+ const doc = new model({});
+
+ const v = new bson.ObjectId('somehex');
+ expectType(v._id.toHexString());
+
+ doc._id = new bson.ObjectId('somehex');
+}
+
+gh12537();
diff --git a/test/types/schema.test.ts b/test/types/schema.test.ts
index 7b926c26aca..04fd53a64b2 100644
--- a/test/types/schema.test.ts
+++ b/test/types/schema.test.ts
@@ -1123,3 +1123,14 @@ function gh13534() {
const doc = new Test({ myId: '0'.repeat(24) });
expectType(doc.myId);
}
+
+function maps() {
+ const schema = new Schema({
+ myMap: { type: Schema.Types.Map, of: Number, required: true }
+ });
+ const Test = model('Test', schema);
+
+ const doc = new Test({ myMap: { answer: 42 } });
+ expectType