Skip to content

Commit eb7deb0

Browse files
committed
Merge branch '8.5' into vkarpov15/gh-14333
2 parents c5b7b71 + 1a0cda7 commit eb7deb0

13 files changed

Lines changed: 206 additions & 22 deletions

CHANGELOG.md

Lines changed: 11 additions & 0 deletions
Original file line numberDiff line numberDiff line change
@@ -1,3 +1,14 @@
1+
8.4.4 / 2024-06-25
2+
==================
3+
* perf: avoid unnecesary get() call and use faster approach for converting to string #14673 #14394
4+
* fix(projection): handle projections on arrays in Model.hydrate() projection option #14686 #14680
5+
* fix(document): avoid passing validateModifiedOnly to subdocs so subdocs get fully validating if they're directly modified #14685 #14677
6+
* fix: handle casting primitive array with $elemMatch in bulkWrite() #14687 #14678
7+
* fix(query): cast $pull using embedded discriminator schema when discriminator key is set in filter #14676 #14675
8+
* types(connection): fix return type of withSession() #14690 [tt-public](https://github.com/tt-public)
9+
* types: add $documents pipeline stage and fix $unionWith type #14666 [nick-statsig](https://github.com/nick-statsig)
10+
* docs(findoneandupdate): improve example that shows findOneAndUpdate() returning doc before updates were applied #14671 #14670
11+
112
8.4.3 / 2024-06-17
213
==================
314
* fix: remove 0x flamegraph files from release

lib/document.js

Lines changed: 0 additions & 1 deletion
Original file line numberDiff line numberDiff line change
@@ -3052,7 +3052,6 @@ Document.prototype.$__validate = function(pathsToValidate, options, callback) {
30523052
const doValidateOptions = {
30533053
...doValidateOptionsByPath[path],
30543054
path: path,
3055-
validateModifiedOnly: shouldValidateModifiedOnly,
30563055
validateAllPaths
30573056
};
30583057

lib/helpers/projection/applyProjection.js

Lines changed: 6 additions & 0 deletions
Original file line numberDiff line numberDiff line change
@@ -35,6 +35,9 @@ function applyExclusiveProjection(doc, projection, hasIncludedChildren, projecti
3535
if (doc == null || typeof doc !== 'object') {
3636
return doc;
3737
}
38+
if (Array.isArray(doc)) {
39+
return doc.map(el => applyExclusiveProjection(el, projection, hasIncludedChildren, projectionLimb, prefix));
40+
}
3841
const ret = { ...doc };
3942
projectionLimb = prefix ? (projectionLimb || {}) : projection;
4043

@@ -57,6 +60,9 @@ function applyInclusiveProjection(doc, projection, hasIncludedChildren, projecti
5760
if (doc == null || typeof doc !== 'object') {
5861
return doc;
5962
}
63+
if (Array.isArray(doc)) {
64+
return doc.map(el => applyInclusiveProjection(el, projection, hasIncludedChildren, projectionLimb, prefix));
65+
}
6066
const ret = { ...doc };
6167
projectionLimb = prefix ? (projectionLimb || {}) : projection;
6268

lib/query.js

Lines changed: 6 additions & 0 deletions
Original file line numberDiff line numberDiff line change
@@ -153,6 +153,12 @@ function Query(conditions, options, model, collection) {
153153

154154
Query.prototype = new mquery();
155155
Query.prototype.constructor = Query;
156+
157+
// Remove some legacy methods that we removed in Mongoose 8, but
158+
// are still in mquery 5.
159+
Query.prototype.count = undefined;
160+
Query.prototype.findOneAndRemove = undefined;
161+
156162
Query.base = mquery.prototype;
157163

158164
/*!

lib/schema/array.js

Lines changed: 1 addition & 18 deletions
Original file line numberDiff line numberDiff line change
@@ -607,24 +607,7 @@ function cast$elemMatch(val, context) {
607607
}
608608
}
609609

610-
// Is this an embedded discriminator and is the discriminator key set?
611-
// If so, use the discriminator schema. See gh-7449
612-
const discriminatorKey = this &&
613-
this.casterConstructor &&
614-
this.casterConstructor.schema &&
615-
this.casterConstructor.schema.options &&
616-
this.casterConstructor.schema.options.discriminatorKey;
617-
const discriminators = this &&
618-
this.casterConstructor &&
619-
this.casterConstructor.schema &&
620-
this.casterConstructor.schema.discriminators || {};
621-
if (discriminatorKey != null &&
622-
val[discriminatorKey] != null &&
623-
discriminators[val[discriminatorKey]] != null) {
624-
return cast(discriminators[val[discriminatorKey]], val, null, this && this.$$context);
625-
}
626-
const schema = this.casterConstructor.schema ?? context.schema;
627-
return cast(schema, val, null, this && this.$$context);
610+
return val;
628611
}
629612

630613
const handle = SchemaArray.prototype.$conditionalHandlers = {};

lib/schema/documentArray.js

Lines changed: 41 additions & 0 deletions
Original file line numberDiff line numberDiff line change
@@ -11,9 +11,11 @@ const SchemaArray = require('./array');
1111
const SchemaDocumentArrayOptions =
1212
require('../options/schemaDocumentArrayOptions');
1313
const SchemaType = require('../schemaType');
14+
const cast = require('../cast');
1415
const discriminator = require('../helpers/model/discriminator');
1516
const handleIdOption = require('../helpers/schema/handleIdOption');
1617
const handleSpreadDoc = require('../helpers/document/handleSpreadDoc');
18+
const isOperator = require('../helpers/query/isOperator');
1719
const utils = require('../utils');
1820
const getConstructor = require('../helpers/discriminator/getConstructor');
1921
const InvalidSchemaOptionError = require('../error/invalidSchemaOption');
@@ -114,6 +116,7 @@ SchemaDocumentArray.options = { castNonArrays: true };
114116
SchemaDocumentArray.prototype = Object.create(SchemaArray.prototype);
115117
SchemaDocumentArray.prototype.constructor = SchemaDocumentArray;
116118
SchemaDocumentArray.prototype.OptionsConstructor = SchemaDocumentArrayOptions;
119+
SchemaDocumentArray.prototype.$conditionalHandlers = { ...SchemaArray.prototype.$conditionalHandlers };
117120

118121
/*!
119122
* ignore
@@ -609,6 +612,44 @@ SchemaDocumentArray.setters = [];
609612

610613
SchemaDocumentArray.get = SchemaType.get;
611614

615+
/*!
616+
* Handle casting $elemMatch operators
617+
*/
618+
619+
SchemaDocumentArray.prototype.$conditionalHandlers.$elemMatch = cast$elemMatch;
620+
621+
function cast$elemMatch(val, context) {
622+
const keys = Object.keys(val);
623+
const numKeys = keys.length;
624+
for (let i = 0; i < numKeys; ++i) {
625+
const key = keys[i];
626+
const value = val[key];
627+
if (isOperator(key) && value != null) {
628+
val[key] = this.castForQuery(key, value, context);
629+
}
630+
}
631+
632+
// Is this an embedded discriminator and is the discriminator key set?
633+
// If so, use the discriminator schema. See gh-7449
634+
const discriminatorKey = this &&
635+
this.casterConstructor &&
636+
this.casterConstructor.schema &&
637+
this.casterConstructor.schema.options &&
638+
this.casterConstructor.schema.options.discriminatorKey;
639+
const discriminators = this &&
640+
this.casterConstructor &&
641+
this.casterConstructor.schema &&
642+
this.casterConstructor.schema.discriminators || {};
643+
if (discriminatorKey != null &&
644+
val[discriminatorKey] != null &&
645+
discriminators[val[discriminatorKey]] != null) {
646+
return cast(discriminators[val[discriminatorKey]], val, null, this && this.$$context);
647+
}
648+
649+
const schema = this.casterConstructor.schema ?? context.schema;
650+
return cast(schema, val, null, this && this.$$context);
651+
}
652+
612653
/*!
613654
* Module exports.
614655
*/

package.json

Lines changed: 1 addition & 1 deletion
Original file line numberDiff line numberDiff line change
@@ -1,7 +1,7 @@
11
{
22
"name": "mongoose",
33
"description": "Mongoose MongoDB ODM",
4-
"version": "8.4.3",
4+
"version": "8.4.4",
55
"author": "Guillermo Rauch <guillermo@learnboost.com>",
66
"keywords": [
77
"mongodb",

test/document.test.js

Lines changed: 82 additions & 0 deletions
Original file line numberDiff line numberDiff line change
@@ -2555,6 +2555,88 @@ describe('document', function() {
25552555
// Does not throw
25562556
await Model.create({ name: 'test' });
25572557
});
2558+
2559+
it('fully validates modified subdocs (gh-14677)', async function() {
2560+
const embedSchema = new mongoose.Schema({
2561+
field1: {
2562+
type: String,
2563+
required: true
2564+
},
2565+
field2: String
2566+
});
2567+
const testSchema = new mongoose.Schema({
2568+
testField: {
2569+
type: String,
2570+
required: true
2571+
},
2572+
testArray: [embedSchema]
2573+
});
2574+
const TestModel = db.model('Test', testSchema);
2575+
2576+
let doc = new TestModel({ testArray: [{ field2: 'test' }] });
2577+
let err = await doc.validate({ validateModifiedOnly: true }).then(() => null, err => err);
2578+
assert.ok(err);
2579+
assert.ok(err.errors['testArray.0.field1']);
2580+
assert.equal(err.errors['testArray.0.field1'].kind, 'required');
2581+
2582+
await TestModel.collection.insertOne(doc.toObject());
2583+
doc = await TestModel.findById(doc._id).orFail();
2584+
doc.testArray[0].field2 = 'test modified';
2585+
err = await doc.validate({ validateModifiedOnly: true }).then(() => null, err => err);
2586+
assert.ifError(err);
2587+
2588+
err = await doc.validate().then(() => null, err => err);
2589+
assert.ok(err);
2590+
assert.ok(err.errors['testArray.0.field1']);
2591+
assert.equal(err.errors['testArray.0.field1'].kind, 'required');
2592+
2593+
doc.testArray[0] = { field2: 'test modified 3' };
2594+
err = await doc.validate({ validateModifiedOnly: true }).then(() => null, err => err);
2595+
assert.ok(err);
2596+
assert.ok(err.errors['testArray.0.field1']);
2597+
assert.equal(err.errors['testArray.0.field1'].kind, 'required');
2598+
});
2599+
2600+
it('fully validates modified single nested subdocs (gh-14677)', async function() {
2601+
const embedSchema = new mongoose.Schema({
2602+
field1: {
2603+
type: String,
2604+
required: true
2605+
},
2606+
field2: String
2607+
});
2608+
const testSchema = new mongoose.Schema({
2609+
testField: {
2610+
type: String,
2611+
required: true
2612+
},
2613+
subdoc: embedSchema
2614+
});
2615+
const TestModel = db.model('Test', testSchema);
2616+
2617+
let doc = new TestModel({ subdoc: { field2: 'test' } });
2618+
let err = await doc.validate({ validateModifiedOnly: true }).then(() => null, err => err);
2619+
assert.ok(err);
2620+
assert.ok(err.errors['subdoc.field1']);
2621+
assert.equal(err.errors['subdoc.field1'].kind, 'required');
2622+
2623+
await TestModel.collection.insertOne(doc.toObject());
2624+
doc = await TestModel.findById(doc._id).orFail();
2625+
doc.subdoc.field2 = 'test modified';
2626+
err = await doc.validate({ validateModifiedOnly: true }).then(() => null, err => err);
2627+
assert.ifError(err);
2628+
2629+
err = await doc.validate().then(() => null, err => err);
2630+
assert.ok(err);
2631+
assert.ok(err.errors['subdoc.field1']);
2632+
assert.equal(err.errors['subdoc.field1'].kind, 'required');
2633+
2634+
doc.subdoc = { field2: 'test modified 3' };
2635+
err = await doc.validate({ validateModifiedOnly: true }).then(() => null, err => err);
2636+
assert.ok(err);
2637+
assert.ok(err.errors['subdoc.field1']);
2638+
assert.equal(err.errors['subdoc.field1'].kind, 'required');
2639+
});
25582640
});
25592641

25602642
describe('bug fixes', function() {

test/helpers/projection.applyProjection.test.js

Lines changed: 11 additions & 0 deletions
Original file line numberDiff line numberDiff line change
@@ -21,4 +21,15 @@ describe('applyProjection', function() {
2121
assert.deepEqual(applyProjection(obj, { 'nested.str2': 0 }), { str: 'test', nested: { num3: 42 } });
2222
assert.deepEqual(applyProjection(obj, { nested: { num3: 0 } }), { str: 'test', nested: { str2: 'test2' } });
2323
});
24+
25+
it('handles projections underneath arrays (gh-14680)', function() {
26+
const obj = {
27+
_id: 12,
28+
testField: 'foo',
29+
testArray: [{ _id: 42, field1: 'bar' }]
30+
};
31+
32+
assert.deepEqual(applyProjection(obj, { 'testArray.field1': 1 }), { testArray: [{ field1: 'bar' }] });
33+
assert.deepEqual(applyProjection(obj, { 'testArray.field1': 0, _id: 0 }), { testField: 'foo', testArray: [{ _id: 42 }] });
34+
});
2435
});

test/model.query.casting.test.js

Lines changed: 4 additions & 1 deletion
Original file line numberDiff line numberDiff line change
@@ -453,7 +453,10 @@ describe('model query casting', function() {
453453
const id = post._id.toString();
454454

455455
await post.save();
456-
const doc = await BlogPostB.findOne({ _id: id, comments: { $not: { $elemMatch: { _id: commentId.toString() } } } });
456+
const doc = await BlogPostB.findOne({
457+
_id: id,
458+
comments: { $not: { $elemMatch: { _id: commentId.toString() } } }
459+
});
457460
assert.equal(doc, null);
458461
});
459462

0 commit comments

Comments
 (0)