From 091c85dc22f6e585473320abbc239842b0d7310d Mon Sep 17 00:00:00 2001 From: Valeri Karpov Date: Mon, 24 Jun 2024 15:10:44 -0400 Subject: [PATCH] fix: handle casting primitive array with $elemMatch in bulkWrite() Fix #14678 --- lib/schema/array.js | 19 +-------------- lib/schema/documentArray.js | 41 ++++++++++++++++++++++++++++++++ test/model.query.casting.test.js | 5 +++- test/model.test.js | 37 ++++++++++++++++++++++++++++ 4 files changed, 83 insertions(+), 19 deletions(-) diff --git a/lib/schema/array.js b/lib/schema/array.js index e73f16d2849..67fe713a99b 100644 --- a/lib/schema/array.js +++ b/lib/schema/array.js @@ -607,24 +607,7 @@ function cast$elemMatch(val, context) { } } - // Is this an embedded discriminator and is the discriminator key set? - // If so, use the discriminator schema. See gh-7449 - const discriminatorKey = this && - this.casterConstructor && - this.casterConstructor.schema && - this.casterConstructor.schema.options && - this.casterConstructor.schema.options.discriminatorKey; - const discriminators = this && - this.casterConstructor && - this.casterConstructor.schema && - this.casterConstructor.schema.discriminators || {}; - if (discriminatorKey != null && - val[discriminatorKey] != null && - discriminators[val[discriminatorKey]] != null) { - return cast(discriminators[val[discriminatorKey]], val, null, this && this.$$context); - } - const schema = this.casterConstructor.schema ?? context.schema; - return cast(schema, val, null, this && this.$$context); + return val; } const handle = SchemaArray.prototype.$conditionalHandlers = {}; diff --git a/lib/schema/documentArray.js b/lib/schema/documentArray.js index d35c5dfbf10..a10d2ec76b1 100644 --- a/lib/schema/documentArray.js +++ b/lib/schema/documentArray.js @@ -11,9 +11,11 @@ const SchemaArray = require('./array'); const SchemaDocumentArrayOptions = require('../options/schemaDocumentArrayOptions'); const SchemaType = require('../schemaType'); +const cast = require('../cast'); const discriminator = require('../helpers/model/discriminator'); const handleIdOption = require('../helpers/schema/handleIdOption'); const handleSpreadDoc = require('../helpers/document/handleSpreadDoc'); +const isOperator = require('../helpers/query/isOperator'); const utils = require('../utils'); const getConstructor = require('../helpers/discriminator/getConstructor'); const InvalidSchemaOptionError = require('../error/invalidSchemaOption'); @@ -114,6 +116,7 @@ SchemaDocumentArray.options = { castNonArrays: true }; SchemaDocumentArray.prototype = Object.create(SchemaArray.prototype); SchemaDocumentArray.prototype.constructor = SchemaDocumentArray; SchemaDocumentArray.prototype.OptionsConstructor = SchemaDocumentArrayOptions; +SchemaDocumentArray.prototype.$conditionalHandlers = { ...SchemaArray.prototype.$conditionalHandlers }; /*! * ignore @@ -609,6 +612,44 @@ SchemaDocumentArray.setters = []; SchemaDocumentArray.get = SchemaType.get; +/*! + * Handle casting $elemMatch operators + */ + +SchemaDocumentArray.prototype.$conditionalHandlers.$elemMatch = cast$elemMatch; + +function cast$elemMatch(val, context) { + const keys = Object.keys(val); + const numKeys = keys.length; + for (let i = 0; i < numKeys; ++i) { + const key = keys[i]; + const value = val[key]; + if (isOperator(key) && value != null) { + val[key] = this.castForQuery(key, value, context); + } + } + + // Is this an embedded discriminator and is the discriminator key set? + // If so, use the discriminator schema. See gh-7449 + const discriminatorKey = this && + this.casterConstructor && + this.casterConstructor.schema && + this.casterConstructor.schema.options && + this.casterConstructor.schema.options.discriminatorKey; + const discriminators = this && + this.casterConstructor && + this.casterConstructor.schema && + this.casterConstructor.schema.discriminators || {}; + if (discriminatorKey != null && + val[discriminatorKey] != null && + discriminators[val[discriminatorKey]] != null) { + return cast(discriminators[val[discriminatorKey]], val, null, this && this.$$context); + } + + const schema = this.casterConstructor.schema ?? context.schema; + return cast(schema, val, null, this && this.$$context); +} + /*! * Module exports. */ diff --git a/test/model.query.casting.test.js b/test/model.query.casting.test.js index c7156959fc5..7f0e863bfe7 100644 --- a/test/model.query.casting.test.js +++ b/test/model.query.casting.test.js @@ -453,7 +453,10 @@ describe('model query casting', function() { const id = post._id.toString(); await post.save(); - const doc = await BlogPostB.findOne({ _id: id, comments: { $not: { $elemMatch: { _id: commentId.toString() } } } }); + const doc = await BlogPostB.findOne({ + _id: id, + comments: { $not: { $elemMatch: { _id: commentId.toString() } } } + }); assert.equal(doc, null); }); diff --git a/test/model.test.js b/test/model.test.js index d5937654a45..3d884fec59b 100644 --- a/test/model.test.js +++ b/test/model.test.js @@ -4470,6 +4470,43 @@ describe('Model', function() { assert.equal(err.validationErrors[0].path, 'age'); assert.equal(err.results[0].path, 'age'); }); + + it('casts $elemMatch filter (gh-14678)', async function() { + const schema = new mongoose.Schema({ + name: String, + ids: [String] + }); + const TestModel = db.model('Test', schema); + + const { _id } = await TestModel.create({ ids: ['1'] }); + await TestModel.bulkWrite([ + { + updateOne: { + filter: { + ids: { + $elemMatch: { + $in: [1] + } + } + }, + update: { + $set: { + name: 'test' + }, + $addToSet: { + ids: { + $each: [1, '2', 3] + } + } + } + } + } + ]); + + const doc = await TestModel.findById(_id).orFail(); + assert.strictEqual(doc.name, 'test'); + assert.deepStrictEqual(doc.ids, ['1', '2', '3']); + }); }); it('deleteOne with cast error (gh-5323)', async function() {