Skip to content

Commit

Permalink
Merge branch '6.x' into 7.x
Browse files Browse the repository at this point in the history
  • Loading branch information
vkarpov15 committed Jan 3, 2024
2 parents 3e60145 + 2d68983 commit 7248bdf
Show file tree
Hide file tree
Showing 11 changed files with 213 additions and 9 deletions.
13 changes: 13 additions & 0 deletions CHANGELOG.md
Original file line number Diff line number Diff line change
@@ -1,3 +1,16 @@
6.12.5 / 2024-01-03
===================
* perf(schema): remove unnecessary lookahead in numeric subpath check
* fix(document): allow setting nested path to null #14226
* fix(document): avoid flattening dotted paths in mixed path underneath nested path #14198 #14178
* fix: add ignoreAtomics option to isModified() for better backwards compatibility with Mongoose 5 #14213

6.12.4 / 2023-12-27
===================
* fix: upgrade mongodb driver -> 4.17.2
* fix(document): avoid treating nested projection as inclusive when applying defaults #14173 #14115
* fix: account for null values when assigning isNew property #14172 #13883

7.6.7 / 2023-12-06
==================
* fix: avoid minimizing single nested subdocs if they are required #14151 #14058
Expand Down
20 changes: 17 additions & 3 deletions lib/document.js
Original file line number Diff line number Diff line change
Expand Up @@ -1121,6 +1121,8 @@ Document.prototype.$set = function $set(path, val, type, options) {
} else {
throw new StrictModeError(key);
}
} else if (pathtype === 'nested' && valForKey == null) {
this.$set(pathName, valForKey, constructing, options);
}
} else if (valForKey !== void 0) {
this.$set(pathName, valForKey, constructing, options);
Expand Down Expand Up @@ -2228,12 +2230,15 @@ Document.prototype[documentModifiedPaths] = Document.prototype.modifiedPaths;
* doc.isDirectModified('documents') // false
*
* @param {String} [path] optional
* @param {Object} [options]
* @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()`
* @return {Boolean}
* @api public
*/

Document.prototype.isModified = function(paths, modifiedPaths) {
Document.prototype.isModified = function(paths, options, modifiedPaths) {
if (paths) {
const ignoreAtomics = options && options.ignoreAtomics;
const directModifiedPathsObj = this.$__.activePaths.states.modify;
if (directModifiedPathsObj == null) {
return false;
Expand All @@ -2254,7 +2259,16 @@ Document.prototype.isModified = function(paths, modifiedPaths) {
return !!~modified.indexOf(path);
});

const directModifiedPaths = Object.keys(directModifiedPathsObj);
let directModifiedPaths = Object.keys(directModifiedPathsObj);
if (ignoreAtomics) {
directModifiedPaths = directModifiedPaths.filter(path => {
const value = this.$__getValue(path);
if (value != null && value[arrayAtomicsSymbol] != null && value[arrayAtomicsSymbol].$set === undefined) {
return false;
}
return true;
});
}
return isModifiedChild || paths.some(function(path) {
return directModifiedPaths.some(function(mod) {
return mod === path || path.startsWith(mod + '.');
Expand Down Expand Up @@ -2676,7 +2690,7 @@ function _getPathsToValidate(doc, pathsToValidate, pathsToSkip) {
paths.delete(fullPathToSubdoc + '.' + modifiedPath);
}

if (doc.$isModified(fullPathToSubdoc, modifiedPaths) &&
if (doc.$isModified(fullPathToSubdoc, null, modifiedPaths) &&
!doc.isDirectModified(fullPathToSubdoc) &&
!doc.$isDefault(fullPathToSubdoc)) {
paths.add(fullPathToSubdoc);
Expand Down
4 changes: 3 additions & 1 deletion lib/helpers/document/applyDefaults.js
Original file line number Diff line number Diff line change
@@ -1,5 +1,7 @@
'use strict';

const isNestedProjection = require('../projection/isNestedProjection');

module.exports = function applyDefaults(doc, fields, exclude, hasIncludedChildren, isBeforeSetters, pathsToSkip) {
const paths = Object.keys(doc.$__schema.paths);
const plen = paths.length;
Expand Down Expand Up @@ -32,7 +34,7 @@ module.exports = function applyDefaults(doc, fields, exclude, hasIncludedChildre
}
} else if (exclude === false && fields && !included) {
const hasSubpaths = type.$isSingleNested || type.$isMongooseDocumentArray;
if (curPath in fields || (j === len - 1 && hasSubpaths && hasIncludedChildren != null && hasIncludedChildren[curPath])) {
if ((curPath in fields && !isNestedProjection(fields[curPath])) || (j === len - 1 && hasSubpaths && hasIncludedChildren != null && hasIncludedChildren[curPath])) {
included = true;
} else if (hasIncludedChildren != null && !hasIncludedChildren[curPath]) {
break;
Expand Down
1 change: 1 addition & 0 deletions lib/helpers/projection/hasIncludedChildren.js
Original file line number Diff line number Diff line change
Expand Up @@ -21,6 +21,7 @@ module.exports = function hasIncludedChildren(fields) {
const keys = Object.keys(fields);

for (const key of keys) {

if (key.indexOf('.') === -1) {
hasIncludedChildren[key] = 1;
continue;
Expand Down
8 changes: 8 additions & 0 deletions lib/helpers/projection/isNestedProjection.js
Original file line number Diff line number Diff line change
@@ -0,0 +1,8 @@
'use strict';

module.exports = function isNestedProjection(val) {
if (val == null || typeof val !== 'object') {
return false;
}
return val.$slice == null && val.$elemMatch == null && val.$meta == null && val.$ == null;
};
6 changes: 3 additions & 3 deletions lib/types/subdocument.js
Original file line number Diff line number Diff line change
Expand Up @@ -182,7 +182,7 @@ Subdocument.prototype.markModified = function(path) {
* ignore
*/

Subdocument.prototype.isModified = function(paths, modifiedPaths) {
Subdocument.prototype.isModified = function(paths, options, modifiedPaths) {
const parent = this.$parent();
if (parent != null) {
if (Array.isArray(paths) || typeof paths === 'string') {
Expand All @@ -192,10 +192,10 @@ Subdocument.prototype.isModified = function(paths, modifiedPaths) {
paths = this.$__pathRelativeToParent();
}

return parent.$isModified(paths, modifiedPaths);
return parent.$isModified(paths, options, modifiedPaths);
}

return Document.prototype.isModified.call(this, paths, modifiedPaths);
return Document.prototype.isModified.call(this, paths, options, modifiedPaths);
};

/**
Expand Down
37 changes: 37 additions & 0 deletions test/document.modified.test.js
Original file line number Diff line number Diff line change
Expand Up @@ -197,6 +197,43 @@ describe('document modified', function() {
assert.equal(post.isModified('comments.0.title'), true);
assert.equal(post.isDirectModified('comments.0.title'), true);
});
it('with push (gh-14024)', async function() {
const post = new BlogPost();
post.init({
title: 'Test',
slug: 'test',
comments: [{ title: 'Test', date: new Date(), body: 'Test' }]
});

post.comments.push({ title: 'new comment', body: 'test' });

assert.equal(post.isModified('comments.0.title', { ignoreAtomics: true }), false);
assert.equal(post.isModified('comments.0.body', { ignoreAtomics: true }), false);
assert.equal(post.get('comments')[0].isModified('body', { ignoreAtomics: true }), false);
});
it('with push and set (gh-14024)', async function() {
const post = new BlogPost();
post.init({
title: 'Test',
slug: 'test',
comments: [{ title: 'Test', date: new Date(), body: 'Test' }]
});

post.comments.push({ title: 'new comment', body: 'test' });
post.get('comments')[0].set('title', 'Woot');

assert.equal(post.isModified('comments', { ignoreAtomics: true }), true);
assert.equal(post.isModified('comments.0.title', { ignoreAtomics: true }), true);
assert.equal(post.isDirectModified('comments.0.title'), true);
assert.equal(post.isDirectModified('comments.0.body'), false);
assert.equal(post.isModified('comments.0.body', { ignoreAtomics: true }), false);

assert.equal(post.isModified('comments', { ignoreAtomics: true }), true);
assert.equal(post.isModified('comments.0.title', { ignoreAtomics: true }), true);
assert.equal(post.isDirectModified('comments.0.title'), true);
assert.equal(post.isDirectModified('comments.0.body'), false);
assert.equal(post.isModified('comments.0.body', { ignoreAtomics: true }), false);
});
it('with accessors', function() {
const post = new BlogPost();
post.init({
Expand Down
92 changes: 92 additions & 0 deletions test/document.test.js
Original file line number Diff line number Diff line change
Expand Up @@ -12820,6 +12820,98 @@ describe('document', function() {
['__stateBeforeSuspension', '__stateBeforeSuspension.jsonField']
);
});

it('should allow null values in list in self assignment (gh-14172) (gh-13859)', async function() {
const objSchema = new Schema({
date: Date,
value: Number
});

const testSchema = new Schema({
intArray: [Number],
strArray: [String],
objArray: [objSchema]
});
const Test = db.model('Test', testSchema);

const doc = new Test({
intArray: [1, 2, 3, null],
strArray: ['b', null, 'c'],
objArray: [
{ date: new Date(1000), value: 1 },
null,
{ date: new Date(3000), value: 3 }
]
});
await doc.save();
doc.intArray = doc.intArray;
doc.strArray = doc.strArray;
doc.objArray = doc.objArray; // this is the trigger for the error
assert.ok(doc);
await doc.save();
assert.ok(doc);
});

it('avoids overwriting dotted paths in mixed path underneath nested path (gh-14178)', async function() {
const testSchema = new Schema({
__stateBeforeSuspension: {
field1: String,
field3: { type: Schema.Types.Mixed }
}
});
const Test = db.model('Test', testSchema);
const eventObj = new Test({
__stateBeforeSuspension: { field1: 'test' }
});
await eventObj.save();
const newO = eventObj.toObject();
newO.__stateBeforeSuspension.field3 = { '.ippo': 5 };
eventObj.set(newO);
await eventObj.save();

assert.strictEqual(eventObj.__stateBeforeSuspension.field3['.ippo'], 5);

const fromDb = await Test.findById(eventObj._id).lean().orFail();
assert.strictEqual(fromDb.__stateBeforeSuspension.field3['.ippo'], 5);
});

it('handles setting nested path to null (gh-14205)', function() {
const schema = new mongoose.Schema({
nested: {
key1: String,
key2: String
}
});

const Model = db.model('Test', schema);

const doc = new Model();
doc.init({
nested: { key1: 'foo', key2: 'bar' }
});

doc.set({ nested: null });
assert.strictEqual(doc.toObject().nested, null);
});

it('handles setting nested path to undefined (gh-14205)', function() {
const schema = new mongoose.Schema({
nested: {
key1: String,
key2: String
}
});

const Model = db.model('Test', schema);

const doc = new Model();
doc.init({
nested: { key1: 'foo', key2: 'bar' }
});

doc.set({ nested: void 0 });
assert.strictEqual(doc.toObject().nested, void 0);
});
});

describe('Check if instance function that is supplied in schema option is availabe', function() {
Expand Down
35 changes: 35 additions & 0 deletions test/query.test.js
Original file line number Diff line number Diff line change
Expand Up @@ -4095,6 +4095,7 @@ describe('Query', function() {
await Error.find().sort('-');
}, { message: 'Invalid field "" passed to sort()' });
});

it('allows executing a find() with a subdocument with defaults disabled (gh-13512)', async function() {
const schema = mongoose.Schema({
title: String,
Expand Down Expand Up @@ -4179,4 +4180,38 @@ describe('Query', function() {
assert.strictEqual(doc.name, 'test1');
assert.strictEqual(doc.__t, undefined);
});

it('does not apply sibling path defaults if using nested projection (gh-14115)', async function() {
const version = await start.mongodVersion();
if (version[0] < 5) {
return this.skip();
}

const userSchema = new mongoose.Schema({
name: String,
account: {
amount: Number,
owner: { type: String, default: () => 'OWNER' },
taxIds: [Number]
}
});
const User = db.model('User', userSchema);

const { _id } = await User.create({
name: 'test',
account: {
amount: 25,
owner: 'test',
taxIds: [42]
}
});

const doc = await User
.findOne({ _id }, { name: 1, account: { amount: 1 } })
.orFail();
assert.strictEqual(doc.name, 'test');
assert.strictEqual(doc.account.amount, 25);
assert.strictEqual(doc.account.owner, undefined);
assert.strictEqual(doc.account.taxIds, undefined);
});
});
4 changes: 2 additions & 2 deletions types/document.d.ts
Original file line number Diff line number Diff line change
Expand Up @@ -178,8 +178,8 @@ declare module 'mongoose' {
* Returns true if any of the given paths are modified, else false. If no arguments, returns `true` if any path
* in this document is modified.
*/
isModified<T extends keyof DocType>(path?: T | Array<T>): boolean;
isModified(path?: string | Array<string>): boolean;
isModified<T extends keyof DocType>(path?: T | Array<T>, options?: { ignoreAtomics?: boolean } | null): boolean;
isModified(path?: string | Array<string>, options?: { ignoreAtomics?: boolean } | null): boolean;

/** Boolean flag specifying if the document is new. */
isNew: boolean;
Expand Down
2 changes: 2 additions & 0 deletions types/models.d.ts
Original file line number Diff line number Diff line change
Expand Up @@ -26,6 +26,8 @@ declare module 'mongoose' {
interface MongooseBulkWriteOptions {
skipValidation?: boolean;
throwOnValidationError?: boolean;
strict?: boolean;
timestamps?: boolean | 'throw';
}

interface InsertManyOptions extends
Expand Down

0 comments on commit 7248bdf

Please sign in to comment.