diff --git a/CHANGELOG.md b/CHANGELOG.md index bfdeaa2d5f4..254ef6d2699 100644 --- a/CHANGELOG.md +++ b/CHANGELOG.md @@ -1,3 +1,10 @@ +7.6.9 / 2024-02-26 +================== + * fix(document): handle embedded recursive discriminators on nested path defined using Schema.prototype.discriminator #14256 #14245 + * types(model): correct return type for findByIdAndDelete() #14233 #14190 + * docs(connections): add note about using asPromise() with createConnection() for error handling #14364 #14266 + * docs(model+query+findoneandupdate): add more details about overwriteDiscriminatorKey option to docs #14264 #14246 + 8.2.0 / 2024-02-22 ================== * feat(model): add recompileSchema() function to models to allow applying schema changes after compiling #14306 #14296 diff --git a/docs/connections.md b/docs/connections.md index cd95feb3ebb..d9fe9b62e30 100644 --- a/docs/connections.md +++ b/docs/connections.md @@ -426,16 +426,24 @@ The `mongoose.createConnection()` function takes the same arguments as const conn = mongoose.createConnection('mongodb://[username:password@]host1[:port1][,host2[:port2],...[,hostN[:portN]]][/[database][?options]]', options); ``` -This [connection](api/connection.html#connection_Connection) object is then used to -create and retrieve [models](api/model.html#model_Model). Models are -**always** scoped to a single connection. +This [connection](api/connection.html#connection_Connection) object is then used to create and retrieve [models](api/model.html#model_Model). +Models are **always** scoped to a single connection. ```javascript const UserModel = conn.model('User', userSchema); ``` -If you use multiple connections, you should make sure you export schemas, -**not** models. Exporting a model from a file is called the *export model pattern*. +The `createConnection()` function returns a connection instance, not a promise. +If you want to use `await` to make sure Mongoose successfully connects to MongoDB, use the [`asPromise()` function](api/connection.html#Connection.prototype.asPromise()): + +```javascript +// `asPromise()` returns a promise that resolves to the connection +// once the connection succeeds, or rejects if connection failed. +const conn = await mongoose.createConnection(connectionString).asPromise(); +``` + +If you use multiple connections, you should make sure you export schemas, **not** models. +Exporting a model from a file is called the *export model pattern*. The export model pattern is limited because you can only use one connection. ```javascript diff --git a/docs/tutorials/findoneandupdate.md b/docs/tutorials/findoneandupdate.md index 80f99fff4da..79fe5a2945e 100644 --- a/docs/tutorials/findoneandupdate.md +++ b/docs/tutorials/findoneandupdate.md @@ -7,10 +7,18 @@ However, there are some cases where you need to use [`findOneAndUpdate()`](https * [Atomic Updates](#atomic-updates) * [Upsert](#upsert) * [The `includeResultMetadata` Option](#includeresultmetadata) +* [Updating Discriminator Keys](#updating-discriminator-keys) ## Getting Started -As the name implies, `findOneAndUpdate()` finds the first document that matches a given `filter`, applies an `update`, and returns the document. By default, `findOneAndUpdate()` returns the document as it was **before** `update` was applied. +As the name implies, `findOneAndUpdate()` finds the first document that matches a given `filter`, applies an `update`, and returns the document. +The `findOneAndUpdate()` function has the following signature: + +```javascript +function findOneAndUpdate(filter, update, options) {} +``` + +By default, `findOneAndUpdate()` returns the document as it was **before** `update` was applied. ```acquit [require:Tutorial.*findOneAndUpdate.*basic case] @@ -78,3 +86,31 @@ Here's what the `res` object from the above example looks like: age: 29 }, ok: 1 } ``` + +## Updating Discriminator Keys + +Mongoose prevents updating the [discriminator key](../discriminators.html#discriminator-keys) using `findOneAndUpdate()` by default. +For example, suppose you have the following discriminator models. + +```javascript +const eventSchema = new mongoose.Schema({ time: Date }); +const Event = db.model('Event', eventSchema); + +const ClickedLinkEvent = Event.discriminator( + 'ClickedLink', + new mongoose.Schema({ url: String }) +); + +const SignedUpEvent = Event.discriminator( + 'SignedUp', + new mongoose.Schema({ username: String }) +); +``` + +Mongoose will remove `__t` (the default discriminator key) from the `update` parameter, if `__t` is set. +This is to prevent unintentional updates to the discriminator key; for example, if you're passing untrusted user input to the `update` parameter. +However, you can tell Mongoose to allow updating the discriminator key by setting the `overwriteDiscriminatorKey` option to `true` as shown below. + +```acquit +[require:use overwriteDiscriminatorKey to change discriminator key] +``` diff --git a/lib/helpers/discriminator/applyEmbeddedDiscriminators.js b/lib/helpers/discriminator/applyEmbeddedDiscriminators.js index 840a4dc628e..9a04ecb072f 100644 --- a/lib/helpers/discriminator/applyEmbeddedDiscriminators.js +++ b/lib/helpers/discriminator/applyEmbeddedDiscriminators.js @@ -19,11 +19,10 @@ function applyEmbeddedDiscriminators(schema, seen = new WeakSet()) { if (schemaType._appliedDiscriminators) { continue; } - for (const disc of schemaType.schema._applyDiscriminators.keys()) { - schemaType.discriminator( - disc, - schemaType.schema._applyDiscriminators.get(disc) - ); + for (const discriminatorKey of schemaType.schema._applyDiscriminators.keys()) { + const discriminatorSchema = schemaType.schema._applyDiscriminators.get(discriminatorKey); + applyEmbeddedDiscriminators(discriminatorSchema, seen); + schemaType.discriminator(discriminatorKey, discriminatorSchema); } schemaType._appliedDiscriminators = true; } diff --git a/lib/model.js b/lib/model.js index 1542b5e2035..56d391510a0 100644 --- a/lib/model.js +++ b/lib/model.js @@ -2532,6 +2532,7 @@ Model.$where = function $where() { * @param {Boolean} [options.setDefaultsOnInsert=true] If `setDefaultsOnInsert` and `upsert` are true, mongoose will apply the [defaults](https://mongoosejs.com/docs/defaults.html) specified in the model's schema if a new document is created * @param {Boolean} [options.includeResultMetadata] if true, returns the [raw result from the MongoDB driver](https://mongodb.github.io/node-mongodb-native/4.9/interfaces/ModifyResult.html) * @param {Boolean} [options.translateAliases=null] If set to `true`, translates any schema-defined aliases in `filter`, `projection`, `update`, and `distinct`. Throws an error if there are any conflicts where both alias and raw property are defined on the same object. + * @param {Boolean} [options.overwriteDiscriminatorKey=false] Mongoose removes discriminator key updates from `update` by default, set `overwriteDiscriminatorKey` to `true` to allow updating the discriminator key * @return {Query} * @see Tutorial https://mongoosejs.com/docs/tutorials/findoneandupdate.html * @see mongodb https://www.mongodb.com/docs/manual/reference/command/findAndModify/ @@ -2624,6 +2625,7 @@ Model.findOneAndUpdate = function(conditions, update, options) { * @param {Boolean} [options.new=false] if true, return the modified document rather than the original * @param {Object|String} [options.select] sets the document fields to return. * @param {Boolean} [options.translateAliases=null] If set to `true`, translates any schema-defined aliases in `filter`, `projection`, `update`, and `distinct`. Throws an error if there are any conflicts where both alias and raw property are defined on the same object. + * @param {Boolean} [options.overwriteDiscriminatorKey=false] Mongoose removes discriminator key updates from `update` by default, set `overwriteDiscriminatorKey` to `true` to allow updating the discriminator key * @return {Query} * @see Model.findOneAndUpdate https://mongoosejs.com/docs/api/model.html#Model.findOneAndUpdate() * @see mongodb https://www.mongodb.com/docs/manual/reference/command/findAndModify/ @@ -3944,6 +3946,7 @@ Model.hydrate = function(obj, projection, options) { * @param {Object} [options.writeConcern=null] sets the [write concern](https://www.mongodb.com/docs/manual/reference/write-concern/) for replica sets. Overrides the [schema-level write concern](https://mongoosejs.com/docs/guide.html#writeConcern) * @param {Boolean} [options.timestamps=null] If set to `false` and [schema-level timestamps](https://mongoosejs.com/docs/guide.html#timestamps) are enabled, skip timestamps for this update. Does nothing if schema-level timestamps are not set. * @param {Boolean} [options.translateAliases=null] If set to `true`, translates any schema-defined aliases in `filter`, `projection`, `update`, and `distinct`. Throws an error if there are any conflicts where both alias and raw property are defined on the same object. + * @param {Boolean} [options.overwriteDiscriminatorKey=false] Mongoose removes discriminator key updates from `update` by default, set `overwriteDiscriminatorKey` to `true` to allow updating the discriminator key * @return {Query} * @see Query docs https://mongoosejs.com/docs/queries.html * @see MongoDB docs https://www.mongodb.com/docs/manual/reference/command/update/#update-command-output @@ -3983,6 +3986,7 @@ Model.updateMany = function updateMany(conditions, doc, options) { * @param {Object} [options.writeConcern=null] sets the [write concern](https://www.mongodb.com/docs/manual/reference/write-concern/) for replica sets. Overrides the [schema-level write concern](https://mongoosejs.com/docs/guide.html#writeConcern) * @param {Boolean} [options.timestamps=null] If set to `false` and [schema-level timestamps](https://mongoosejs.com/docs/guide.html#timestamps) are enabled, skip timestamps for this update. Note that this allows you to overwrite timestamps. Does nothing if schema-level timestamps are not set. * @param {Boolean} [options.translateAliases=null] If set to `true`, translates any schema-defined aliases in `filter`, `projection`, `update`, and `distinct`. Throws an error if there are any conflicts where both alias and raw property are defined on the same object. + * @param {Boolean} [options.overwriteDiscriminatorKey=false] Mongoose removes discriminator key updates from `update` by default, set `overwriteDiscriminatorKey` to `true` to allow updating the discriminator key * @return {Query} * @see Query docs https://mongoosejs.com/docs/queries.html * @see MongoDB docs https://www.mongodb.com/docs/manual/reference/command/update/#update-command-output diff --git a/lib/query.js b/lib/query.js index 8731f003204..39b5f604d8f 100644 --- a/lib/query.js +++ b/lib/query.js @@ -3191,6 +3191,7 @@ function prepareDiscriminatorCriteria(query) { * @param {Boolean} [options.timestamps=null] If set to `false` and [schema-level timestamps](https://mongoosejs.com/docs/guide.html#timestamps) are enabled, skip timestamps for this update. Note that this allows you to overwrite timestamps. Does nothing if schema-level timestamps are not set. * @param {Boolean} [options.returnOriginal=null] An alias for the `new` option. `returnOriginal: false` is equivalent to `new: true`. * @param {Boolean} [options.translateAliases=null] If set to `true`, translates any schema-defined aliases in `filter`, `projection`, `update`, and `distinct`. Throws an error if there are any conflicts where both alias and raw property are defined on the same object. + * @param {Boolean} [options.overwriteDiscriminatorKey=false] Mongoose removes discriminator key updates from `update` by default, set `overwriteDiscriminatorKey` to `true` to allow updating the discriminator key * @see Tutorial https://mongoosejs.com/docs/tutorials/findoneandupdate.html * @see findAndModify command https://www.mongodb.com/docs/manual/reference/command/findAndModify/ * @see ModifyResult https://mongodb.github.io/node-mongodb-native/4.9/interfaces/ModifyResult.html @@ -3887,6 +3888,7 @@ Query.prototype._replaceOne = async function _replaceOne() { * @param {Object} [options.writeConcern=null] sets the [write concern](https://www.mongodb.com/docs/manual/reference/write-concern/) for replica sets. Overrides the [schema-level write concern](https://mongoosejs.com/docs/guide.html#writeConcern) * @param {Boolean} [options.timestamps=null] If set to `false` and [schema-level timestamps](https://mongoosejs.com/docs/guide.html#timestamps) are enabled, skip timestamps for this update. Does nothing if schema-level timestamps are not set. * @param {Boolean} [options.translateAliases=null] If set to `true`, translates any schema-defined aliases in `filter`, `projection`, `update`, and `distinct`. Throws an error if there are any conflicts where both alias and raw property are defined on the same object. + * @param {Boolean} [options.overwriteDiscriminatorKey=false] Mongoose removes discriminator key updates from `update` by default, set `overwriteDiscriminatorKey` to `true` to allow updating the discriminator key * @param {Function} [callback] params are (error, writeOpResult) * @return {Query} this * @see Model.update https://mongoosejs.com/docs/api/model.html#Model.update() @@ -3955,7 +3957,8 @@ Query.prototype.updateMany = function(conditions, doc, options, callback) { * @param {Boolean} [options.upsert=false] if true, and no documents found, insert a new document * @param {Object} [options.writeConcern=null] sets the [write concern](https://www.mongodb.com/docs/manual/reference/write-concern/) for replica sets. Overrides the [schema-level write concern](https://mongoosejs.com/docs/guide.html#writeConcern) * @param {Boolean} [options.timestamps=null] If set to `false` and [schema-level timestamps](https://mongoosejs.com/docs/guide.html#timestamps) are enabled, skip timestamps for this update. Note that this allows you to overwrite timestamps. Does nothing if schema-level timestamps are not set. - @param {Boolean} [options.translateAliases=null] If set to `true`, translates any schema-defined aliases in `filter`, `projection`, `update`, and `distinct`. Throws an error if there are any conflicts where both alias and raw property are defined on the same object. + * @param {Boolean} [options.translateAliases=null] If set to `true`, translates any schema-defined aliases in `filter`, `projection`, `update`, and `distinct`. Throws an error if there are any conflicts where both alias and raw property are defined on the same object. + * @param {Boolean} [options.overwriteDiscriminatorKey=false] Mongoose removes discriminator key updates from `update` by default, set `overwriteDiscriminatorKey` to `true` to allow updating the discriminator key * @param {Function} [callback] params are (error, writeOpResult) * @return {Query} this * @see Model.update https://mongoosejs.com/docs/api/model.html#Model.update() diff --git a/test/document.test.js b/test/document.test.js index 8f67cf1d74e..9999d18564b 100644 --- a/test/document.test.js +++ b/test/document.test.js @@ -12791,6 +12791,54 @@ describe('document', function() { await doc2.save(); }); + it('handles embedded recursive discriminators on nested path defined using Schema.prototype.discriminator (gh-14245)', async function() { + const baseSchema = new Schema({ + type: { type: Number, required: true } + }, { discriminatorKey: 'type' }); + + class Base { + whoAmI() { return 'I am Base'; } + } + + baseSchema.loadClass(Base); + + class NumberTyped extends Base { + whoAmI() { return 'I am NumberTyped'; } + } + + class StringTyped extends Base { + whoAmI() { return 'I am StringTyped'; } + } + + const selfRefSchema = new Schema({ + self: { type: [baseSchema], required: true } + }); + + class SelfReferenceTyped extends Base { + whoAmI() { return 'I am SelfReferenceTyped'; } + } + + selfRefSchema.loadClass(SelfReferenceTyped); + baseSchema.discriminator(5, selfRefSchema); + + const numberTypedSchema = new Schema({}).loadClass(NumberTyped); + const stringTypedSchema = new Schema({}).loadClass(StringTyped); + baseSchema.discriminator(1, numberTypedSchema); + baseSchema.discriminator(3, stringTypedSchema); + const containerSchema = new Schema({ items: [baseSchema] }); + const containerModel = db.model('Test', containerSchema); + + const instance = await containerModel.create({ + items: [{ type: 5, self: [{ type: 1 }, { type: 3 }] }] + }); + + assert.equal(instance.items[0].whoAmI(), 'I am SelfReferenceTyped'); + assert.deepStrictEqual(instance.items[0].self.map(item => item.whoAmI()), [ + 'I am NumberTyped', + 'I am StringTyped' + ]); + }); + it('can use `collection` as schema name (gh-13956)', async function() { const schema = new mongoose.Schema({ name: String, collection: String }); const Test = db.model('Test', schema); diff --git a/types/models.d.ts b/types/models.d.ts index b232620e1f9..5ac31d24d2a 100644 --- a/types/models.d.ts +++ b/types/models.d.ts @@ -228,7 +228,7 @@ declare module 'mongoose' { /** Creates a `countDocuments` query: counts the number of documents that match `filter`. */ countDocuments( filter?: FilterQuery, - options?: (mongodb.CountOptions & ExcludeKeys, 'lean' | 'timestamps'>) | null + options?: (mongodb.CountOptions & MongooseBaseQueryOptions) | null ): QueryWithHelpers< number, THydratedDocumentType, @@ -266,7 +266,7 @@ declare module 'mongoose' { */ deleteMany( filter?: FilterQuery, - options?: (mongodb.DeleteOptions & ExcludeKeys, 'lean' | 'timestamps'>) | null + options?: (mongodb.DeleteOptions & MongooseBaseQueryOptions) | null ): QueryWithHelpers< mongodb.DeleteResult, THydratedDocumentType, @@ -743,14 +743,14 @@ declare module 'mongoose' { updateMany( filter?: FilterQuery, update?: UpdateQuery | UpdateWithAggregationPipeline, - options?: (mongodb.UpdateOptions & ExcludeKeys, 'lean'>) | null + options?: (mongodb.UpdateOptions & MongooseUpdateQueryOptions) | null ): QueryWithHelpers; /** Creates a `updateOne` query: updates the first document that matches `filter` with `update`. */ updateOne( filter?: FilterQuery, update?: UpdateQuery | UpdateWithAggregationPipeline, - options?: (mongodb.UpdateOptions & ExcludeKeys, 'lean'>) | null + options?: (mongodb.UpdateOptions & MongooseUpdateQueryOptions) | null ): QueryWithHelpers; /** Creates a Query, applies the passed conditions, and returns the Query. */ diff --git a/types/query.d.ts b/types/query.d.ts index 0bdda904350..6d53f4f473e 100644 --- a/types/query.d.ts +++ b/types/query.d.ts @@ -17,25 +17,33 @@ declare module 'mongoose' { */ type FilterQuery = _FilterQuery; - type MongooseQueryOptions = Pick< - QueryOptions, - 'context' | - 'lean' | - 'multipleCastError' | - 'overwriteDiscriminatorKey' | - 'populate' | - 'runValidators' | - 'sanitizeProjection' | - 'sanitizeFilter' | - 'setDefaultsOnInsert' | - 'strict' | - 'strictQuery' | - 'timestamps' | - 'translateAliases' - > & { + type MongooseBaseQueryOptionKeys = + | 'context' + | 'multipleCastError' + | 'overwriteDiscriminatorKey' + | 'populate' + | 'runValidators' + | 'sanitizeProjection' + | 'sanitizeFilter' + | 'setDefaultsOnInsert' + | 'strict' + | 'strictQuery' + | 'translateAliases'; + + type MongooseQueryOptions< + DocType = unknown, + Keys extends keyof QueryOptions = MongooseBaseQueryOptionKeys | 'timestamps' | 'lean' + > = Pick, Keys> & { [other: string]: any; }; + type MongooseBaseQueryOptions = MongooseQueryOptions; + + type MongooseUpdateQueryOptions = MongooseQueryOptions< + DocType, + MongooseBaseQueryOptionKeys | 'timestamps' + >; + type ProjectionFields = { [Key in keyof DocType]?: any } & Record; type QueryWithHelpers = Query & THelpers;