Skip to content

Commit eefe935

Browse files
committed
Merge branch 'master' into 8.0
2 parents f6e5a50 + 8831f03 commit eefe935

File tree

11 files changed

+150
-34
lines changed

11 files changed

+150
-34
lines changed

CHANGELOG.md

Lines changed: 7 additions & 0 deletions
Original file line numberDiff line numberDiff line change
@@ -1,3 +1,10 @@
1+
7.6.3 / 2023-10-17
2+
==================
3+
* fix(populate): handle multiple spaces when specifying paths to populate using space-delimited paths #13984 #13951
4+
* fix(update): avoid applying defaults on query filter when upserting with empty update #13983 #13962
5+
* fix(model): add versionKey to bulkWrite when inserting or upserting #13981 #13944
6+
* docs: fix typo in timestamps docs #13976 [danielcoker](https://github.com/danielcoker)
7+
18
7.6.2 / 2023-10-13
29
==================
310
* perf: avoid storing a separate entry in schema subpaths for every element in an array #13953 #13874

lib/helpers/model/castBulkWrite.js

Lines changed: 21 additions & 0 deletions
Original file line numberDiff line numberDiff line change
@@ -6,6 +6,7 @@ const applyTimestampsToChildren = require('../update/applyTimestampsToChildren')
66
const applyTimestampsToUpdate = require('../update/applyTimestampsToUpdate');
77
const cast = require('../../cast');
88
const castUpdate = require('../query/castUpdate');
9+
const decorateUpdateWithVersionKey = require('../update/decorateUpdateWithVersionKey');
910
const { inspect } = require('util');
1011
const setDefaultsOnInsert = require('../setDefaultsOnInsert');
1112

@@ -33,6 +34,10 @@ module.exports = function castBulkWrite(originalModel, op, options) {
3334
if (options.session != null) {
3435
doc.$session(options.session);
3536
}
37+
const versionKey = model?.schema?.options?.versionKey;
38+
if (versionKey && doc[versionKey] == null) {
39+
doc[versionKey] = 0;
40+
}
3641
op['insertOne']['document'] = doc;
3742

3843
if (options.skipValidation || op['insertOne'].skipValidation) {
@@ -81,6 +86,12 @@ module.exports = function castBulkWrite(originalModel, op, options) {
8186
});
8287
}
8388

89+
decorateUpdateWithVersionKey(
90+
op['updateOne']['update'],
91+
op['updateOne'],
92+
model.schema.options.versionKey
93+
);
94+
8495
op['updateOne']['filter'] = cast(model.schema, op['updateOne']['filter'], {
8596
strict: strict,
8697
upsert: op['updateOne'].upsert
@@ -133,6 +144,12 @@ module.exports = function castBulkWrite(originalModel, op, options) {
133144

134145
_addDiscriminatorToObject(schema, op['updateMany']['filter']);
135146

147+
decorateUpdateWithVersionKey(
148+
op['updateMany']['update'],
149+
op['updateMany'],
150+
model.schema.options.versionKey
151+
);
152+
136153
op['updateMany']['filter'] = cast(model.schema, op['updateMany']['filter'], {
137154
strict: strict,
138155
upsert: op['updateMany'].upsert
@@ -173,6 +190,10 @@ module.exports = function castBulkWrite(originalModel, op, options) {
173190
if (options.session != null) {
174191
doc.$session(options.session);
175192
}
193+
const versionKey = model?.schema?.options?.versionKey;
194+
if (versionKey && doc[versionKey] == null) {
195+
doc[versionKey] = 0;
196+
}
176197
op['replaceOne']['replacement'] = doc;
177198

178199
if (options.skipValidation || op['replaceOne'].skipValidation) {

lib/helpers/query/castUpdate.js

Lines changed: 2 additions & 1 deletion
Original file line numberDiff line numberDiff line change
@@ -126,7 +126,8 @@ module.exports = function castUpdate(schema, obj, options, context, filter) {
126126
Object.keys(filter).length > 0) {
127127
// Trick the driver into allowing empty upserts to work around
128128
// https://github.com/mongodb/node-mongodb-native/pull/2490
129-
return { $setOnInsert: filter };
129+
// Shallow clone to avoid passing defaults in re: gh-13962
130+
return { $setOnInsert: { ...filter } };
130131
}
131132
return ret;
132133
};
Lines changed: 26 additions & 0 deletions
Original file line numberDiff line numberDiff line change
@@ -0,0 +1,26 @@
1+
'use strict';
2+
3+
const modifiedPaths = require('./modifiedPaths');
4+
5+
/**
6+
* Decorate the update with a version key, if necessary
7+
* @api private
8+
*/
9+
10+
module.exports = function decorateUpdateWithVersionKey(update, options, versionKey) {
11+
if (!versionKey || !(options && options.upsert || false)) {
12+
return;
13+
}
14+
15+
const updatedPaths = modifiedPaths(update);
16+
if (!updatedPaths[versionKey]) {
17+
if (options.overwrite) {
18+
update[versionKey] = 0;
19+
} else {
20+
if (!update.$setOnInsert) {
21+
update.$setOnInsert = {};
22+
}
23+
update.$setOnInsert[versionKey] = 0;
24+
}
25+
}
26+
};

lib/model.js

Lines changed: 3 additions & 26 deletions
Original file line numberDiff line numberDiff line change
@@ -34,6 +34,7 @@ const assignVals = require('./helpers/populate/assignVals');
3434
const castBulkWrite = require('./helpers/model/castBulkWrite');
3535
const clone = require('./helpers/clone');
3636
const createPopulateQueryFilter = require('./helpers/populate/createPopulateQueryFilter');
37+
const decorateUpdateWithVersionKey = require('./helpers/update/decorateUpdateWithVersionKey');
3738
const getDefaultBulkwriteResult = require('./helpers/getDefaultBulkwriteResult');
3839
const getSchemaDiscriminatorByValue = require('./helpers/discriminator/getSchemaDiscriminatorByValue');
3940
const discriminator = require('./helpers/model/discriminator');
@@ -55,7 +56,6 @@ const isPathExcluded = require('./helpers/projection/isPathExcluded');
5556
const decorateDiscriminatorIndexOptions = require('./helpers/indexes/decorateDiscriminatorIndexOptions');
5657
const isPathSelectedInclusive = require('./helpers/projection/isPathSelectedInclusive');
5758
const leanPopulateMap = require('./helpers/populate/leanPopulateMap');
58-
const modifiedPaths = require('./helpers/update/modifiedPaths');
5959
const parallelLimit = require('./helpers/parallelLimit');
6060
const parentPaths = require('./helpers/path/parentPaths');
6161
const prepareDiscriminatorPipeline = require('./helpers/aggregate/prepareDiscriminatorPipeline');
@@ -2433,37 +2433,14 @@ Model.findOneAndUpdate = function(conditions, update, options) {
24332433
_isNested: true
24342434
});
24352435

2436-
_decorateUpdateWithVersionKey(update, options, this.schema.options.versionKey);
2436+
decorateUpdateWithVersionKey(update, options, this.schema.options.versionKey);
24372437

24382438
const mq = new this.Query({}, {}, this, this.$__collection);
24392439
mq.select(fields);
24402440

24412441
return mq.findOneAndUpdate(conditions, update, options);
24422442
};
24432443

2444-
/**
2445-
* Decorate the update with a version key, if necessary
2446-
* @api private
2447-
*/
2448-
2449-
function _decorateUpdateWithVersionKey(update, options, versionKey) {
2450-
if (!versionKey || !(options && options.upsert || false)) {
2451-
return;
2452-
}
2453-
2454-
const updatedPaths = modifiedPaths(update);
2455-
if (!updatedPaths[versionKey]) {
2456-
if (options.overwrite) {
2457-
update[versionKey] = 0;
2458-
} else {
2459-
if (!update.$setOnInsert) {
2460-
update.$setOnInsert = {};
2461-
}
2462-
update.$setOnInsert[versionKey] = 0;
2463-
}
2464-
}
2465-
}
2466-
24672444
/**
24682445
* Issues a mongodb findOneAndUpdate command by a document's _id field.
24692446
* `findByIdAndUpdate(id, ...)` is equivalent to `findOneAndUpdate({ _id: id }, ...)`.
@@ -3921,7 +3898,7 @@ function _update(model, op, conditions, doc, options) {
39213898
model.schema &&
39223899
model.schema.options &&
39233900
model.schema.options.versionKey || null;
3924-
_decorateUpdateWithVersionKey(doc, options, versionKey);
3901+
decorateUpdateWithVersionKey(doc, options, versionKey);
39253902

39263903
return mq[op](conditions, doc, options);
39273904
}

lib/utils.js

Lines changed: 8 additions & 5 deletions
Original file line numberDiff line numberDiff line change
@@ -31,6 +31,9 @@ exports.isMongooseDocumentArray = isMongooseDocumentArray.isMongooseDocumentArra
3131
exports.registerMongooseArray = isMongooseArray.registerMongooseArray;
3232
exports.registerMongooseDocumentArray = isMongooseDocumentArray.registerMongooseDocumentArray;
3333

34+
const oneSpaceRE = /\s/;
35+
const manySpaceRE = /\s+/;
36+
3437
/**
3538
* Produces a collection name from model `name`. By default, just returns
3639
* the model name
@@ -572,8 +575,8 @@ exports.populate = function populate(path, select, model, match, options, subPop
572575
function makeSingles(arr) {
573576
const ret = [];
574577
arr.forEach(function(obj) {
575-
if (/[\s]/.test(obj.path)) {
576-
const paths = obj.path.split(' ');
578+
if (oneSpaceRE.test(obj.path)) {
579+
const paths = obj.path.split(manySpaceRE);
577580
paths.forEach(function(p) {
578581
const copy = Object.assign({}, obj);
579582
copy.path = p;
@@ -592,9 +595,9 @@ function _populateObj(obj) {
592595
if (Array.isArray(obj.populate)) {
593596
const ret = [];
594597
obj.populate.forEach(function(obj) {
595-
if (/[\s]/.test(obj.path)) {
598+
if (oneSpaceRE.test(obj.path)) {
596599
const copy = Object.assign({}, obj);
597-
const paths = copy.path.split(' ');
600+
const paths = copy.path.split(manySpaceRE);
598601
paths.forEach(function(p) {
599602
copy.path = p;
600603
ret.push(exports.populate(copy)[0]);
@@ -609,7 +612,7 @@ function _populateObj(obj) {
609612
}
610613

611614
const ret = [];
612-
const paths = obj.path.split(' ');
615+
const paths = oneSpaceRE.test(obj.path) ? obj.path.split(manySpaceRE) : [obj.path];
613616
if (obj.options != null) {
614617
obj.options = clone(obj.options);
615618
}

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": "7.6.2",
4+
"version": "7.6.3",
55
"author": "Guillermo Rauch <guillermo@learnboost.com>",
66
"keywords": [
77
"mongodb",

test/model.findOneAndUpdate.test.js

Lines changed: 17 additions & 0 deletions
Original file line numberDiff line numberDiff line change
@@ -2183,4 +2183,21 @@ describe('model: findOneAndUpdate:', function() {
21832183
assert.ok(document);
21842184
assert.equal(document.name, 'test');
21852185
});
2186+
2187+
it('skips adding defaults to filter when passing empty update (gh-13962)', async function() {
2188+
const schema = new Schema({
2189+
myField: Number,
2190+
defaultField: { type: String, default: 'default' }
2191+
}, { versionKey: false });
2192+
const Test = db.model('Test', schema);
2193+
2194+
await Test.create({ myField: 1, defaultField: 'some non-default value' });
2195+
2196+
const updated = await Test.findOneAndUpdate(
2197+
{ myField: 1 },
2198+
{},
2199+
{ upsert: true, returnDocument: 'after' }
2200+
);
2201+
assert.equal(updated.defaultField, 'some non-default value');
2202+
});
21862203
});

test/model.populate.test.js

Lines changed: 21 additions & 0 deletions
Original file line numberDiff line numberDiff line change
@@ -2819,7 +2819,28 @@ describe('model: populate:', function() {
28192819

28202820
assert.equal(blogposts[0].user.name, 'Fan 1');
28212821
assert.equal(blogposts[0].title, 'Test 1');
2822+
});
2823+
2824+
it('handles multiple spaces in between paths to populate (gh-13951)', async function() {
2825+
const BlogPost = db.model('BlogPost', new Schema({
2826+
title: String,
2827+
user: { type: ObjectId, ref: 'User' },
2828+
fans: [{ type: ObjectId, ref: 'User' }]
2829+
}));
2830+
const User = db.model('User', new Schema({ name: String }));
2831+
2832+
const fans = await User.create([{ name: 'Fan 1' }]);
2833+
const posts = [
2834+
{ title: 'Test 1', user: fans[0]._id, fans: [fans[0]._id] }
2835+
];
2836+
await BlogPost.create(posts);
2837+
const blogPost = await BlogPost.
2838+
findOne({ title: 'Test 1' }).
2839+
populate('user \t fans');
28222840

2841+
assert.equal(blogPost.user.name, 'Fan 1');
2842+
assert.equal(blogPost.fans[0].name, 'Fan 1');
2843+
assert.equal(blogPost.title, 'Test 1');
28232844
});
28242845

28252846
it('maps results back to correct document (gh-1444)', async function() {

test/model.test.js

Lines changed: 43 additions & 0 deletions
Original file line numberDiff line numberDiff line change
@@ -4047,6 +4047,49 @@ describe('Model', function() {
40474047

40484048
});
40494049

4050+
it('sets version key (gh-13944)', async function() {
4051+
const userSchema = new Schema({
4052+
firstName: { type: String, required: true },
4053+
lastName: { type: String }
4054+
});
4055+
const User = db.model('User', userSchema);
4056+
4057+
await User.bulkWrite([
4058+
{
4059+
updateOne: {
4060+
filter: { lastName: 'Gibbons' },
4061+
update: { firstName: 'Peter' },
4062+
upsert: true
4063+
}
4064+
},
4065+
{
4066+
insertOne: {
4067+
document: {
4068+
firstName: 'Michael',
4069+
lastName: 'Bolton'
4070+
}
4071+
}
4072+
},
4073+
{
4074+
replaceOne: {
4075+
filter: { lastName: 'Lumbergh' },
4076+
replacement: { firstName: 'Bill', lastName: 'Lumbergh' },
4077+
upsert: true
4078+
}
4079+
}
4080+
], { ordered: false });
4081+
4082+
const users = await User.find();
4083+
assert.deepStrictEqual(
4084+
users.map(user => user.firstName).sort(),
4085+
['Bill', 'Michael', 'Peter']
4086+
);
4087+
assert.deepStrictEqual(
4088+
users.map(user => user.__v),
4089+
[0, 0, 0]
4090+
);
4091+
});
4092+
40504093
it('with single nested and setOnInsert (gh-7534)', function() {
40514094
const nested = new Schema({ name: String });
40524095
const schema = new Schema({ nested: nested });

0 commit comments

Comments
 (0)