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. -

remove()

+

rawResult

-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 } +); -

update()

- -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 } +); ``` -

count()

- -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; ``` +

+ + option: collectionOptions + +

+ +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(); +``` +

With ES6 Classes

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')); ```

References to other documents

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>(doc.myMap); + expectType(doc.myMap!.get('answer')); +} diff --git a/types/augmentations.d.ts b/types/augmentations.d.ts new file mode 100644 index 00000000000..82aca589ff8 --- /dev/null +++ b/types/augmentations.d.ts @@ -0,0 +1,9 @@ +// this import is required so that types get merged instead of completely overwritten +import 'bson'; + +declare module 'bson' { + interface ObjectId { + /** Mongoose automatically adds a conveniency "_id" getter on the base ObjectId class */ + _id: this; + } +} diff --git a/types/index.d.ts b/types/index.d.ts index d3733b82926..56f018af999 100644 --- a/types/index.d.ts +++ b/types/index.d.ts @@ -22,6 +22,7 @@ /// /// /// +/// declare class NativeDate extends global.Date { } @@ -377,11 +378,22 @@ declare module 'mongoose' { pre(method: MongooseQueryOrDocumentMiddleware | MongooseQueryOrDocumentMiddleware[] | RegExp, options: SchemaPreOptions & { document: false, query: false }, fn: PreMiddlewareFunction): this; pre(method: MongooseDistinctQueryMiddleware|MongooseDistinctQueryMiddleware[], options: SchemaPreOptions & { document: boolean, query: false }, fn: PreMiddlewareFunction): this; pre(method: MongooseDistinctDocumentMiddleware | MongooseDistinctDocumentMiddleware[] | RegExp, options: SchemaPreOptions & { document: false, query: boolean }, fn: PreMiddlewareFunction): this; + // this = Union of Document and Query, could be called with any of them + pre>( + method: MongooseQueryAndDocumentMiddleware | MongooseQueryAndDocumentMiddleware[] | RegExp, + options: SchemaPreOptions & { document: true, query: true }, + fn: PreMiddlewareFunction + ): this; // this = Document pre(method: 'save', fn: PreSaveMiddlewareFunction): this; pre(method: 'save', options: SchemaPreOptions, fn: PreSaveMiddlewareFunction): this; pre(method: MongooseDistinctDocumentMiddleware|MongooseDistinctDocumentMiddleware[], fn: PreMiddlewareFunction): this; pre(method: MongooseDistinctDocumentMiddleware|MongooseDistinctDocumentMiddleware[], options: SchemaPreOptions, fn: PreMiddlewareFunction): this; + pre( + method: MongooseQueryAndDocumentMiddleware | MongooseQueryAndDocumentMiddleware[] | RegExp, + options: SchemaPreOptions & { document: true }, + fn: PreMiddlewareFunction + ): this; pre(method: MongooseQueryOrDocumentMiddleware | MongooseQueryOrDocumentMiddleware[] | RegExp, options: SchemaPreOptions & { document: true, query: false }, fn: PreMiddlewareFunction): this; // this = Query pre>(method: MongooseDefaultQueryMiddleware|MongooseDefaultQueryMiddleware[], fn: PreMiddlewareFunction): this; diff --git a/types/inferschematype.d.ts b/types/inferschematype.d.ts index 4379a45e112..56c742978dc 100644 --- a/types/inferschematype.d.ts +++ b/types/inferschematype.d.ts @@ -218,10 +218,11 @@ type ResolvePathType extends true ? Buffer : PathValueType extends MapConstructor ? Map> : - PathValueType extends ArrayConstructor ? any[] : - PathValueType extends typeof Schema.Types.Mixed ? any: - IfEquals extends true ? any: - IfEquals extends true ? any: - PathValueType extends typeof SchemaType ? PathValueType['prototype'] : - PathValueType extends Record ? ObtainDocumentType : - unknown; + IfEquals extends true ? Map> : + PathValueType extends ArrayConstructor ? any[] : + PathValueType extends typeof Schema.Types.Mixed ? any: + IfEquals extends true ? any: + IfEquals extends true ? any: + PathValueType extends typeof SchemaType ? PathValueType['prototype'] : + PathValueType extends Record ? ObtainDocumentType : + unknown; diff --git a/types/models.d.ts b/types/models.d.ts index 774b18eb055..50d3cc220fa 100644 --- a/types/models.d.ts +++ b/types/models.d.ts @@ -123,7 +123,6 @@ declare module 'mongoose' { SessionOption { checkKeys?: boolean; j?: boolean; - ordered?: boolean; safe?: boolean | WriteConcern; timestamps?: boolean | QueryTimestampsConfig; validateBeforeSave?: boolean; @@ -132,6 +131,11 @@ declare module 'mongoose' { wtimeout?: number; } + interface CreateOptions extends SaveOptions { + ordered?: boolean; + aggregateErrors?: boolean; + } + interface RemoveOptions extends SessionOption, Omit {} const Model: Model; @@ -217,7 +221,8 @@ declare module 'mongoose' { >; /** Creates a new document or documents */ - create>(docs: Array, options?: SaveOptions): Promise; + create>(docs: Array, options: CreateOptions & { aggregateErrors: true }): Promise<(THydratedDocumentType | Error)[]>; + create>(docs: Array, options?: CreateOptions): Promise; create>(doc: DocContents | TRawDocType): Promise; create>(...docs: Array): Promise; diff --git a/types/query.d.ts b/types/query.d.ts index ab05d38dd83..0958e30dc9b 100644 --- a/types/query.d.ts +++ b/types/query.d.ts @@ -136,6 +136,10 @@ declare module 'mongoose' { * Another alias for the `new` option. `returnOriginal` is deprecated so this should be used. */ returnDocument?: 'before' | 'after'; + /** + * Set to true to enable `update validators` + * (https://mongoosejs.com/docs/validation.html#update-validators). Defaults to false. + */ runValidators?: boolean; /* Set to `true` to automatically sanitize potentially unsafe user-generated query projections */ sanitizeProjection?: boolean; @@ -691,7 +695,7 @@ declare module 'mongoose' { slice(val: number | Array): this; /** Sets the sort order. If an object is passed, values allowed are `asc`, `desc`, `ascending`, `descending`, `1`, and `-1`. */ - sort(arg?: string | { [key: string]: SortOrder | { $meta: 'textScore' } } | [string, SortOrder][] | undefined | null): this; + sort(arg?: string | { [key: string]: SortOrder | { $meta: any } } | [string, SortOrder][] | undefined | null): this; /** Sets the tailable option (for use with capped collections). */ tailable(bool?: boolean, opts?: { diff --git a/types/schemaoptions.d.ts b/types/schemaoptions.d.ts index 96f2bd31169..28b105639fe 100644 --- a/types/schemaoptions.d.ts +++ b/types/schemaoptions.d.ts @@ -49,6 +49,9 @@ declare module 'mongoose' { /** Sets a default collation for every query and aggregation. */ collation?: mongodb.CollationOptions; + /** Arbitrary options passed to `createCollection()` */ + collectionOptions?: mongodb.CreateCollectionOptions; + /** The timeseries option to use when creating the model's collection. */ timeseries?: mongodb.TimeSeriesCollectionOptions; diff --git a/types/schematypes.d.ts b/types/schematypes.d.ts index cb1635a191a..02b2fddf7a8 100644 --- a/types/schematypes.d.ts +++ b/types/schematypes.d.ts @@ -63,7 +63,11 @@ declare module 'mongoose' { validate?: SchemaValidator | AnyArray>; /** Allows overriding casting logic for this individual path. If a string, the given string overwrites Mongoose's default cast error message. */ - cast?: string; + cast?: string | + boolean | + ((value: any) => T) | + [(value: any) => T, string] | + [((value: any) => T) | null, (value: any, path: string, model: Model, kind: string) => string]; /** * If true, attach a required validator to this path, which ensures this path diff --git a/types/types.d.ts b/types/types.d.ts index 29652243403..55b48138ad3 100644 --- a/types/types.d.ts +++ b/types/types.d.ts @@ -80,7 +80,6 @@ declare module 'mongoose' { } class ObjectId extends mongodb.ObjectId { - _id: this; } class Subdocument extends Document {