Skip to content

Commit 7248bdf

Browse files
committed
Merge branch '6.x' into 7.x
2 parents 3e60145 + 2d68983 commit 7248bdf

File tree

11 files changed

+213
-9
lines changed

11 files changed

+213
-9
lines changed

CHANGELOG.md

Lines changed: 13 additions & 0 deletions
Original file line numberDiff line numberDiff line change
@@ -1,3 +1,16 @@
1+
6.12.5 / 2024-01-03
2+
===================
3+
* perf(schema): remove unnecessary lookahead in numeric subpath check
4+
* fix(document): allow setting nested path to null #14226
5+
* fix(document): avoid flattening dotted paths in mixed path underneath nested path #14198 #14178
6+
* fix: add ignoreAtomics option to isModified() for better backwards compatibility with Mongoose 5 #14213
7+
8+
6.12.4 / 2023-12-27
9+
===================
10+
* fix: upgrade mongodb driver -> 4.17.2
11+
* fix(document): avoid treating nested projection as inclusive when applying defaults #14173 #14115
12+
* fix: account for null values when assigning isNew property #14172 #13883
13+
114
7.6.7 / 2023-12-06
215
==================
316
* fix: avoid minimizing single nested subdocs if they are required #14151 #14058

lib/document.js

Lines changed: 17 additions & 3 deletions
Original file line numberDiff line numberDiff line change
@@ -1121,6 +1121,8 @@ Document.prototype.$set = function $set(path, val, type, options) {
11211121
} else {
11221122
throw new StrictModeError(key);
11231123
}
1124+
} else if (pathtype === 'nested' && valForKey == null) {
1125+
this.$set(pathName, valForKey, constructing, options);
11241126
}
11251127
} else if (valForKey !== void 0) {
11261128
this.$set(pathName, valForKey, constructing, options);
@@ -2228,12 +2230,15 @@ Document.prototype[documentModifiedPaths] = Document.prototype.modifiedPaths;
22282230
* doc.isDirectModified('documents') // false
22292231
*
22302232
* @param {String} [path] optional
2233+
* @param {Object} [options]
2234+
* @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()`
22312235
* @return {Boolean}
22322236
* @api public
22332237
*/
22342238

2235-
Document.prototype.isModified = function(paths, modifiedPaths) {
2239+
Document.prototype.isModified = function(paths, options, modifiedPaths) {
22362240
if (paths) {
2241+
const ignoreAtomics = options && options.ignoreAtomics;
22372242
const directModifiedPathsObj = this.$__.activePaths.states.modify;
22382243
if (directModifiedPathsObj == null) {
22392244
return false;
@@ -2254,7 +2259,16 @@ Document.prototype.isModified = function(paths, modifiedPaths) {
22542259
return !!~modified.indexOf(path);
22552260
});
22562261

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

2679-
if (doc.$isModified(fullPathToSubdoc, modifiedPaths) &&
2693+
if (doc.$isModified(fullPathToSubdoc, null, modifiedPaths) &&
26802694
!doc.isDirectModified(fullPathToSubdoc) &&
26812695
!doc.$isDefault(fullPathToSubdoc)) {
26822696
paths.add(fullPathToSubdoc);

lib/helpers/document/applyDefaults.js

Lines changed: 3 additions & 1 deletion
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

Lines changed: 1 addition & 0 deletions
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;
Lines changed: 8 additions & 0 deletions
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/types/subdocument.js

Lines changed: 3 additions & 3 deletions
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

Lines changed: 37 additions & 0 deletions
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

Lines changed: 92 additions & 0 deletions
Original file line numberDiff line numberDiff line change
@@ -12820,6 +12820,98 @@ describe('document', function() {
1282012820
['__stateBeforeSuspension', '__stateBeforeSuspension.jsonField']
1282112821
);
1282212822
});
12823+
12824+
it('should allow null values in list in self assignment (gh-14172) (gh-13859)', async function() {
12825+
const objSchema = new Schema({
12826+
date: Date,
12827+
value: Number
12828+
});
12829+
12830+
const testSchema = new Schema({
12831+
intArray: [Number],
12832+
strArray: [String],
12833+
objArray: [objSchema]
12834+
});
12835+
const Test = db.model('Test', testSchema);
12836+
12837+
const doc = new Test({
12838+
intArray: [1, 2, 3, null],
12839+
strArray: ['b', null, 'c'],
12840+
objArray: [
12841+
{ date: new Date(1000), value: 1 },
12842+
null,
12843+
{ date: new Date(3000), value: 3 }
12844+
]
12845+
});
12846+
await doc.save();
12847+
doc.intArray = doc.intArray;
12848+
doc.strArray = doc.strArray;
12849+
doc.objArray = doc.objArray; // this is the trigger for the error
12850+
assert.ok(doc);
12851+
await doc.save();
12852+
assert.ok(doc);
12853+
});
12854+
12855+
it('avoids overwriting dotted paths in mixed path underneath nested path (gh-14178)', async function() {
12856+
const testSchema = new Schema({
12857+
__stateBeforeSuspension: {
12858+
field1: String,
12859+
field3: { type: Schema.Types.Mixed }
12860+
}
12861+
});
12862+
const Test = db.model('Test', testSchema);
12863+
const eventObj = new Test({
12864+
__stateBeforeSuspension: { field1: 'test' }
12865+
});
12866+
await eventObj.save();
12867+
const newO = eventObj.toObject();
12868+
newO.__stateBeforeSuspension.field3 = { '.ippo': 5 };
12869+
eventObj.set(newO);
12870+
await eventObj.save();
12871+
12872+
assert.strictEqual(eventObj.__stateBeforeSuspension.field3['.ippo'], 5);
12873+
12874+
const fromDb = await Test.findById(eventObj._id).lean().orFail();
12875+
assert.strictEqual(fromDb.__stateBeforeSuspension.field3['.ippo'], 5);
12876+
});
12877+
12878+
it('handles setting nested path to null (gh-14205)', function() {
12879+
const schema = new mongoose.Schema({
12880+
nested: {
12881+
key1: String,
12882+
key2: String
12883+
}
12884+
});
12885+
12886+
const Model = db.model('Test', schema);
12887+
12888+
const doc = new Model();
12889+
doc.init({
12890+
nested: { key1: 'foo', key2: 'bar' }
12891+
});
12892+
12893+
doc.set({ nested: null });
12894+
assert.strictEqual(doc.toObject().nested, null);
12895+
});
12896+
12897+
it('handles setting nested path to undefined (gh-14205)', function() {
12898+
const schema = new mongoose.Schema({
12899+
nested: {
12900+
key1: String,
12901+
key2: String
12902+
}
12903+
});
12904+
12905+
const Model = db.model('Test', schema);
12906+
12907+
const doc = new Model();
12908+
doc.init({
12909+
nested: { key1: 'foo', key2: 'bar' }
12910+
});
12911+
12912+
doc.set({ nested: void 0 });
12913+
assert.strictEqual(doc.toObject().nested, void 0);
12914+
});
1282312915
});
1282412916

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

test/query.test.js

Lines changed: 35 additions & 0 deletions
Original file line numberDiff line numberDiff line change
@@ -4095,6 +4095,7 @@ describe('Query', function() {
40954095
await Error.find().sort('-');
40964096
}, { message: 'Invalid field "" passed to sort()' });
40974097
});
4098+
40984099
it('allows executing a find() with a subdocument with defaults disabled (gh-13512)', async function() {
40994100
const schema = mongoose.Schema({
41004101
title: String,
@@ -4179,4 +4180,38 @@ describe('Query', function() {
41794180
assert.strictEqual(doc.name, 'test1');
41804181
assert.strictEqual(doc.__t, undefined);
41814182
});
4183+
4184+
it('does not apply sibling path defaults if using nested projection (gh-14115)', async function() {
4185+
const version = await start.mongodVersion();
4186+
if (version[0] < 5) {
4187+
return this.skip();
4188+
}
4189+
4190+
const userSchema = new mongoose.Schema({
4191+
name: String,
4192+
account: {
4193+
amount: Number,
4194+
owner: { type: String, default: () => 'OWNER' },
4195+
taxIds: [Number]
4196+
}
4197+
});
4198+
const User = db.model('User', userSchema);
4199+
4200+
const { _id } = await User.create({
4201+
name: 'test',
4202+
account: {
4203+
amount: 25,
4204+
owner: 'test',
4205+
taxIds: [42]
4206+
}
4207+
});
4208+
4209+
const doc = await User
4210+
.findOne({ _id }, { name: 1, account: { amount: 1 } })
4211+
.orFail();
4212+
assert.strictEqual(doc.name, 'test');
4213+
assert.strictEqual(doc.account.amount, 25);
4214+
assert.strictEqual(doc.account.owner, undefined);
4215+
assert.strictEqual(doc.account.taxIds, undefined);
4216+
});
41824217
});

types/document.d.ts

Lines changed: 2 additions & 2 deletions
Original file line numberDiff line numberDiff line change
@@ -178,8 +178,8 @@ declare module 'mongoose' {
178178
* Returns true if any of the given paths are modified, else false. If no arguments, returns `true` if any path
179179
* in this document is modified.
180180
*/
181-
isModified<T extends keyof DocType>(path?: T | Array<T>): boolean;
182-
isModified(path?: string | Array<string>): boolean;
181+
isModified<T extends keyof DocType>(path?: T | Array<T>, options?: { ignoreAtomics?: boolean } | null): boolean;
182+
isModified(path?: string | Array<string>, options?: { ignoreAtomics?: boolean } | null): boolean;
183183

184184
/** Boolean flag specifying if the document is new. */
185185
isNew: boolean;

types/models.d.ts

Lines changed: 2 additions & 0 deletions
Original file line numberDiff line numberDiff line change
@@ -26,6 +26,8 @@ declare module 'mongoose' {
2626
interface MongooseBulkWriteOptions {
2727
skipValidation?: boolean;
2828
throwOnValidationError?: boolean;
29+
strict?: boolean;
30+
timestamps?: boolean | 'throw';
2931
}
3032

3133
interface InsertManyOptions extends

0 commit comments

Comments
 (0)