Skip to content

Commit 3bc4482

Browse files
committed
Merge branch '7.x'
2 parents 2c2377d + ac9af5b commit 3bc4482

14 files changed

+251
-23
lines changed

CHANGELOG.md

+19
Original file line numberDiff line numberDiff line change
@@ -1,3 +1,22 @@
1+
7.6.8 / 2024-01-08
2+
==================
3+
* perf(schema): remove unnecessary lookahead in numeric subpath check
4+
* fix(discriminator): handle reusing schema with embedded discriminators defined using Schema.prototype.discriminator #14202 #14162
5+
* fix(ChangeStream): avoid suppressing errors in closed change stream #14206 #14177
6+
7+
6.12.5 / 2024-01-03
8+
===================
9+
* perf(schema): remove unnecessary lookahead in numeric subpath check
10+
* fix(document): allow setting nested path to null #14226
11+
* fix(document): avoid flattening dotted paths in mixed path underneath nested path #14198 #14178
12+
* fix: add ignoreAtomics option to isModified() for better backwards compatibility with Mongoose 5 #14213
13+
14+
6.12.4 / 2023-12-27
15+
===================
16+
* fix: upgrade mongodb driver -> 4.17.2
17+
* fix(document): avoid treating nested projection as inclusive when applying defaults #14173 #14115
18+
* fix: account for null values when assigning isNew property #14172 #13883
19+
120
8.0.3 / 2023-12-07
221
==================
322
* fix(schema): avoid creating unnecessary clone of schematype in nested array so nested document arrays use correct constructor #14128 #14101

lib/cursor/changeStream.js

-12
Original file line numberDiff line numberDiff line change
@@ -60,12 +60,6 @@ class ChangeStream extends EventEmitter {
6060

6161
driverChangeStreamEvents.forEach(ev => {
6262
this.driverChangeStream.on(ev, data => {
63-
// Sometimes Node driver still polls after close, so
64-
// avoid any uncaught exceptions due to closed change streams
65-
// See tests for gh-7022
66-
if (ev === 'error' && this.closed) {
67-
return;
68-
}
6963
if (data != null && data.fullDocument != null && this.options && this.options.hydrate) {
7064
data.fullDocument = this.options.model.hydrate(data.fullDocument);
7165
}
@@ -83,12 +77,6 @@ class ChangeStream extends EventEmitter {
8377

8478
driverChangeStreamEvents.forEach(ev => {
8579
this.driverChangeStream.on(ev, data => {
86-
// Sometimes Node driver still polls after close, so
87-
// avoid any uncaught exceptions due to closed change streams
88-
// See tests for gh-7022
89-
if (ev === 'error' && this.closed) {
90-
return;
91-
}
9280
if (data != null && data.fullDocument != null && this.options && this.options.hydrate) {
9381
data.fullDocument = this.options.model.hydrate(data.fullDocument);
9482
}

lib/document.js

+17-3
Original file line numberDiff line numberDiff line change
@@ -1122,6 +1122,8 @@ Document.prototype.$set = function $set(path, val, type, options) {
11221122
} else {
11231123
throw new StrictModeError(key);
11241124
}
1125+
} else if (pathtype === 'nested' && valForKey == null) {
1126+
this.$set(pathName, valForKey, constructing, options);
11251127
}
11261128
} else if (valForKey !== void 0) {
11271129
this.$set(pathName, valForKey, constructing, options);
@@ -2229,12 +2231,15 @@ Document.prototype[documentModifiedPaths] = Document.prototype.modifiedPaths;
22292231
* doc.isDirectModified('documents') // false
22302232
*
22312233
* @param {String} [path] optional
2234+
* @param {Object} [options]
2235+
* @param {Boolean} [options.ignoreAtomics=false] If true, doesn't return true if path is underneath an array that was modified with atomic operations like `push()`
22322236
* @return {Boolean}
22332237
* @api public
22342238
*/
22352239

2236-
Document.prototype.isModified = function(paths, modifiedPaths) {
2240+
Document.prototype.isModified = function(paths, options, modifiedPaths) {
22372241
if (paths) {
2242+
const ignoreAtomics = options && options.ignoreAtomics;
22382243
const directModifiedPathsObj = this.$__.activePaths.states.modify;
22392244
if (directModifiedPathsObj == null) {
22402245
return false;
@@ -2255,7 +2260,16 @@ Document.prototype.isModified = function(paths, modifiedPaths) {
22552260
return !!~modified.indexOf(path);
22562261
});
22572262

2258-
const directModifiedPaths = Object.keys(directModifiedPathsObj);
2263+
let directModifiedPaths = Object.keys(directModifiedPathsObj);
2264+
if (ignoreAtomics) {
2265+
directModifiedPaths = directModifiedPaths.filter(path => {
2266+
const value = this.$__getValue(path);
2267+
if (value != null && value[arrayAtomicsSymbol] != null && value[arrayAtomicsSymbol].$set === undefined) {
2268+
return false;
2269+
}
2270+
return true;
2271+
});
2272+
}
22592273
return isModifiedChild || paths.some(function(path) {
22602274
return directModifiedPaths.some(function(mod) {
22612275
return mod === path || path.startsWith(mod + '.');
@@ -2677,7 +2691,7 @@ function _getPathsToValidate(doc, pathsToValidate, pathsToSkip) {
26772691
paths.delete(fullPathToSubdoc + '.' + modifiedPath);
26782692
}
26792693

2680-
if (doc.$isModified(fullPathToSubdoc, modifiedPaths) &&
2694+
if (doc.$isModified(fullPathToSubdoc, null, modifiedPaths) &&
26812695
!doc.isDirectModified(fullPathToSubdoc) &&
26822696
!doc.$isDefault(fullPathToSubdoc)) {
26832697
paths.add(fullPathToSubdoc);

lib/helpers/discriminator/applyEmbeddedDiscriminators.js

+8-1
Original file line numberDiff line numberDiff line change
@@ -16,8 +16,15 @@ function applyEmbeddedDiscriminators(schema, seen = new WeakSet()) {
1616
if (!schemaType.schema._applyDiscriminators) {
1717
continue;
1818
}
19+
if (schemaType._appliedDiscriminators) {
20+
continue;
21+
}
1922
for (const disc of schemaType.schema._applyDiscriminators.keys()) {
20-
schemaType.discriminator(disc, schemaType.schema._applyDiscriminators.get(disc));
23+
schemaType.discriminator(
24+
disc,
25+
schemaType.schema._applyDiscriminators.get(disc)
26+
);
2127
}
28+
schemaType._appliedDiscriminators = true;
2229
}
2330
}

lib/helpers/document/applyDefaults.js

+3-1
Original file line numberDiff line numberDiff line change
@@ -1,5 +1,7 @@
11
'use strict';
22

3+
const isNestedProjection = require('../projection/isNestedProjection');
4+
35
module.exports = function applyDefaults(doc, fields, exclude, hasIncludedChildren, isBeforeSetters, pathsToSkip) {
46
const paths = Object.keys(doc.$__schema.paths);
57
const plen = paths.length;
@@ -32,7 +34,7 @@ module.exports = function applyDefaults(doc, fields, exclude, hasIncludedChildre
3234
}
3335
} else if (exclude === false && fields && !included) {
3436
const hasSubpaths = type.$isSingleNested || type.$isMongooseDocumentArray;
35-
if (curPath in fields || (j === len - 1 && hasSubpaths && hasIncludedChildren != null && hasIncludedChildren[curPath])) {
37+
if ((curPath in fields && !isNestedProjection(fields[curPath])) || (j === len - 1 && hasSubpaths && hasIncludedChildren != null && hasIncludedChildren[curPath])) {
3638
included = true;
3739
} else if (hasIncludedChildren != null && !hasIncludedChildren[curPath]) {
3840
break;

lib/helpers/projection/hasIncludedChildren.js

+1
Original file line numberDiff line numberDiff line change
@@ -21,6 +21,7 @@ module.exports = function hasIncludedChildren(fields) {
2121
const keys = Object.keys(fields);
2222

2323
for (const key of keys) {
24+
2425
if (key.indexOf('.') === -1) {
2526
hasIncludedChildren[key] = 1;
2627
continue;
Original file line numberDiff line numberDiff line change
@@ -0,0 +1,8 @@
1+
'use strict';
2+
3+
module.exports = function isNestedProjection(val) {
4+
if (val == null || typeof val !== 'object') {
5+
return false;
6+
}
7+
return val.$slice == null && val.$elemMatch == null && val.$meta == null && val.$ == null;
8+
};

lib/schema.js

+3-1
Original file line numberDiff line numberDiff line change
@@ -25,6 +25,8 @@ const utils = require('./utils');
2525
const validateRef = require('./helpers/populate/validateRef');
2626
const util = require('util');
2727

28+
const hasNumericSubpathRegex = /\.\d+(\.|$)/;
29+
2830
let MongooseTypes;
2931

3032
const queryHooks = require('./helpers/query/applyQueryMiddleware').
@@ -1008,7 +1010,7 @@ Schema.prototype.path = function(path, obj) {
10081010
}
10091011

10101012
// subpaths?
1011-
return /\.\d+\.?.*$/.test(path)
1013+
return hasNumericSubpathRegex.test(path)
10121014
? getPositionalPath(this, path, cleanPath)
10131015
: undefined;
10141016
}

lib/types/subdocument.js

+3-3
Original file line numberDiff line numberDiff line change
@@ -182,7 +182,7 @@ Subdocument.prototype.markModified = function(path) {
182182
* ignore
183183
*/
184184

185-
Subdocument.prototype.isModified = function(paths, modifiedPaths) {
185+
Subdocument.prototype.isModified = function(paths, options, modifiedPaths) {
186186
const parent = this.$parent();
187187
if (parent != null) {
188188
if (Array.isArray(paths) || typeof paths === 'string') {
@@ -192,10 +192,10 @@ Subdocument.prototype.isModified = function(paths, modifiedPaths) {
192192
paths = this.$__pathRelativeToParent();
193193
}
194194

195-
return parent.$isModified(paths, modifiedPaths);
195+
return parent.$isModified(paths, options, modifiedPaths);
196196
}
197197

198-
return Document.prototype.isModified.call(this, paths, modifiedPaths);
198+
return Document.prototype.isModified.call(this, paths, options, modifiedPaths);
199199
};
200200

201201
/**

test/document.modified.test.js

+37
Original file line numberDiff line numberDiff line change
@@ -197,6 +197,43 @@ describe('document modified', function() {
197197
assert.equal(post.isModified('comments.0.title'), true);
198198
assert.equal(post.isDirectModified('comments.0.title'), true);
199199
});
200+
it('with push (gh-14024)', async function() {
201+
const post = new BlogPost();
202+
post.init({
203+
title: 'Test',
204+
slug: 'test',
205+
comments: [{ title: 'Test', date: new Date(), body: 'Test' }]
206+
});
207+
208+
post.comments.push({ title: 'new comment', body: 'test' });
209+
210+
assert.equal(post.isModified('comments.0.title', { ignoreAtomics: true }), false);
211+
assert.equal(post.isModified('comments.0.body', { ignoreAtomics: true }), false);
212+
assert.equal(post.get('comments')[0].isModified('body', { ignoreAtomics: true }), false);
213+
});
214+
it('with push and set (gh-14024)', async function() {
215+
const post = new BlogPost();
216+
post.init({
217+
title: 'Test',
218+
slug: 'test',
219+
comments: [{ title: 'Test', date: new Date(), body: 'Test' }]
220+
});
221+
222+
post.comments.push({ title: 'new comment', body: 'test' });
223+
post.get('comments')[0].set('title', 'Woot');
224+
225+
assert.equal(post.isModified('comments', { ignoreAtomics: true }), true);
226+
assert.equal(post.isModified('comments.0.title', { ignoreAtomics: true }), true);
227+
assert.equal(post.isDirectModified('comments.0.title'), true);
228+
assert.equal(post.isDirectModified('comments.0.body'), false);
229+
assert.equal(post.isModified('comments.0.body', { ignoreAtomics: true }), false);
230+
231+
assert.equal(post.isModified('comments', { ignoreAtomics: true }), true);
232+
assert.equal(post.isModified('comments.0.title', { ignoreAtomics: true }), true);
233+
assert.equal(post.isDirectModified('comments.0.title'), true);
234+
assert.equal(post.isDirectModified('comments.0.body'), false);
235+
assert.equal(post.isModified('comments.0.body', { ignoreAtomics: true }), false);
236+
});
200237
it('with accessors', function() {
201238
const post = new BlogPost();
202239
post.init({

test/document.test.js

+114
Original file line numberDiff line numberDiff line change
@@ -12769,6 +12769,28 @@ describe('document', function() {
1276912769
);
1277012770
});
1277112771

12772+
it('handles reusing schema with embedded discriminators defined using Schema.prototype.discriminator (gh-14162)', async function() {
12773+
const discriminated = new Schema({
12774+
type: { type: Number, required: true }
12775+
}, { discriminatorKey: 'type' });
12776+
12777+
discriminated.discriminator(1, new Schema({ prop1: String }));
12778+
discriminated.discriminator(3, new Schema({ prop2: String }));
12779+
12780+
const containerSchema = new Schema({ items: [discriminated] });
12781+
const containerModel = db.model('Test', containerSchema);
12782+
const containerModel2 = db.model('Test1', containerSchema);
12783+
const doc1 = new containerModel({ items: [{ type: 1, prop1: 'foo' }, { type: 3, prop2: 'bar' }] });
12784+
const doc2 = new containerModel2({ items: [{ type: 1, prop1: 'baz' }, { type: 3, prop2: 'qux' }] });
12785+
await doc1.save();
12786+
await doc2.save();
12787+
12788+
doc1.items.push({ type: 3, prop2: 'test1' });
12789+
doc2.items.push({ type: 3, prop2: 'test1' });
12790+
await doc1.save();
12791+
await doc2.save();
12792+
});
12793+
1277212794
it('can use `collection` as schema name (gh-13956)', async function() {
1277312795
const schema = new mongoose.Schema({ name: String, collection: String });
1277412796
const Test = db.model('Test', schema);
@@ -12802,6 +12824,98 @@ describe('document', function() {
1280212824
['__stateBeforeSuspension', '__stateBeforeSuspension.jsonField']
1280312825
);
1280412826
});
12827+
12828+
it('should allow null values in list in self assignment (gh-14172) (gh-13859)', async function() {
12829+
const objSchema = new Schema({
12830+
date: Date,
12831+
value: Number
12832+
});
12833+
12834+
const testSchema = new Schema({
12835+
intArray: [Number],
12836+
strArray: [String],
12837+
objArray: [objSchema]
12838+
});
12839+
const Test = db.model('Test', testSchema);
12840+
12841+
const doc = new Test({
12842+
intArray: [1, 2, 3, null],
12843+
strArray: ['b', null, 'c'],
12844+
objArray: [
12845+
{ date: new Date(1000), value: 1 },
12846+
null,
12847+
{ date: new Date(3000), value: 3 }
12848+
]
12849+
});
12850+
await doc.save();
12851+
doc.intArray = doc.intArray;
12852+
doc.strArray = doc.strArray;
12853+
doc.objArray = doc.objArray; // this is the trigger for the error
12854+
assert.ok(doc);
12855+
await doc.save();
12856+
assert.ok(doc);
12857+
});
12858+
12859+
it('avoids overwriting dotted paths in mixed path underneath nested path (gh-14178)', async function() {
12860+
const testSchema = new Schema({
12861+
__stateBeforeSuspension: {
12862+
field1: String,
12863+
field3: { type: Schema.Types.Mixed }
12864+
}
12865+
});
12866+
const Test = db.model('Test', testSchema);
12867+
const eventObj = new Test({
12868+
__stateBeforeSuspension: { field1: 'test' }
12869+
});
12870+
await eventObj.save();
12871+
const newO = eventObj.toObject();
12872+
newO.__stateBeforeSuspension.field3 = { '.ippo': 5 };
12873+
eventObj.set(newO);
12874+
await eventObj.save();
12875+
12876+
assert.strictEqual(eventObj.__stateBeforeSuspension.field3['.ippo'], 5);
12877+
12878+
const fromDb = await Test.findById(eventObj._id).lean().orFail();
12879+
assert.strictEqual(fromDb.__stateBeforeSuspension.field3['.ippo'], 5);
12880+
});
12881+
12882+
it('handles setting nested path to null (gh-14205)', function() {
12883+
const schema = new mongoose.Schema({
12884+
nested: {
12885+
key1: String,
12886+
key2: String
12887+
}
12888+
});
12889+
12890+
const Model = db.model('Test', schema);
12891+
12892+
const doc = new Model();
12893+
doc.init({
12894+
nested: { key1: 'foo', key2: 'bar' }
12895+
});
12896+
12897+
doc.set({ nested: null });
12898+
assert.strictEqual(doc.toObject().nested, null);
12899+
});
12900+
12901+
it('handles setting nested path to undefined (gh-14205)', function() {
12902+
const schema = new mongoose.Schema({
12903+
nested: {
12904+
key1: String,
12905+
key2: String
12906+
}
12907+
});
12908+
12909+
const Model = db.model('Test', schema);
12910+
12911+
const doc = new Model();
12912+
doc.init({
12913+
nested: { key1: 'foo', key2: 'bar' }
12914+
});
12915+
12916+
doc.set({ nested: void 0 });
12917+
assert.strictEqual(doc.toObject().nested, void 0);
12918+
});
1280512919
});
1280612920

1280712921
describe('Check if instance function that is supplied in schema option is availabe', function() {

test/model.test.js

+1
Original file line numberDiff line numberDiff line change
@@ -3550,6 +3550,7 @@ describe('Model', function() {
35503550
assert.equal(changeData.operationType, 'insert');
35513551
assert.equal(changeData.fullDocument.name, 'Ned Stark');
35523552

3553+
await changeStream.close();
35533554
await db.close();
35543555
});
35553556

0 commit comments

Comments
 (0)