Skip to content

Commit 84ac690

Browse files
authored
Merge branch '8.0' into vkarpov15/gh-13578
2 parents f6ed0eb + b630afb commit 84ac690

File tree

12 files changed

+318
-39
lines changed

12 files changed

+318
-39
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

docs/migrating_to_8.md

Lines changed: 168 additions & 9 deletions
Original file line numberDiff line numberDiff line change
@@ -13,11 +13,18 @@ If you're still on Mongoose 6.x or earlier, please read the [Mongoose 6.x to 7.x
1313

1414
* [Removed `rawResult` option for `findOneAndUpdate()`](#removed-rawresult-option-for-findoneandupdate)
1515
* [`Document.prototype.deleteOne()` now returns a query](#document-prototype-deleteone-now-returns-a-query)
16-
* [Changed behavior for `findOneAndUpdate()` with `orFail()` and upsert](#changed-behavior-for-findoneandupdate-with-orfail-and-upsert)
1716
* [MongoDB Node Driver 6.0](#mongodb-node-driver-6)
1817
* [Removed `findOneAndRemove()`](#removed-findoneandremove)
18+
* [Removed `count()`](#removed-count)
1919
* [Removed id Setter](#removed-id-setter)
20+
* [`null` is valid for non-required string enums](#null-is-valid-for-non-required-string-enums)
21+
* [Apply minimize when `save()` updates an existing document](#apply-minimize-when-save-updates-an-existing-document)
22+
* [Apply base schema paths before discriminator paths](#apply-base-schema-paths-before-discriminator-paths)
23+
* [Changed behavior for `findOneAndUpdate()` with `orFail()` and upsert](#changed-behavior-for-findoneandupdate-with-orfail-and-upsert)
24+
* [`create()` waits until all saves are done before throwing any error](#create-waits-until-all-saves-are-done-before-throwing-any-error)
25+
* [`Model.validate()` returns copy of object](#model-validate-returns-copy-of-object)
2026
* [Allow `null` For Optional Fields in TypeScript](#allow-null-for-optional-fields-in-typescript)
27+
* [Infer `distinct()` return types from schema](#infer-distinct-return-types-from-schema)
2128

2229
<h2 id="removed-rawresult-option-for-findoneandupdate"><a href="#removed-rawresult-option-for-findoneandupdate">Removed <code>rawResult</code> option for <code>findOneAndUpdate()</code></a></h2>
2330

@@ -54,14 +61,6 @@ const q = numberOne.deleteOne();
5461
const res = await q;
5562
```
5663

57-
<h2 id="changed-behavior-for-findoneandupdate-with-orfail-and-upsert"><a href="#changed-behavior-for-findoneandupdate-with-orfail-and-upsert">Changed behavior for <code>findOneAndUpdate()</code> with <code>orFail()</code> and upsert</a></h2>
58-
59-
In Mongoose 7, `findOneAndUpdate(filter, update, { upsert: true }).orFail()` would throw a `DocumentNotFoundError` if a new document was upserted.
60-
In other words, `findOneAndUpdate().orFail()` always threw an error if no document was found, even if a new document was upserted.
61-
62-
In Mongoose 8, `findOneAndUpdate(filter, update, { upsert: true }).orFail()` always succeeds.
63-
`findOneAndUpdate().orFail()` now throws a `DocumentNotFoundError` if there's no document returned, rather than if no document was found.
64-
6564
<h2 id="mongodb-node-driver-6"><a href="#mongodb-node-driver-6">MongoDB Node Driver 6</a></h2>
6665

6766
Mongoose 8 uses [v6.x of the MongoDB Node driver](https://github.com/mongodb/node-mongodb-native/blob/main/HISTORY.md#600-2023-08-28).
@@ -75,11 +74,153 @@ In Mongoose 7, `findOneAndRemove()` was an alias for `findOneAndDelete()` that M
7574
Mongoose 8 no longer supports `findOneAndRemove()`.
7675
Use `findOneAndDelete()` instead.
7776

77+
<h2 id="removed-count"><a href="#removed-count">Removed <code>count()</code></a></h2>
78+
79+
`Model.count()` and `Query.prototype.count()` were removed in Mongoose 8. Use `Model.countDocuments()` and `Query.prototype.countDocuments()` instead.
80+
7881
<h2 id="removed-id-setter"><a href="#removed-id-setter">Removed id Setter</a></h2>
7982

8083
In Mongoose 7.4, Mongoose introduced an `id` setter that made `doc.id = '0'.repeat(24)` equivalent to `doc._id = '0'.repeat(24)`.
8184
In Mongoose 8, that setter is now removed.
8285

86+
<h2 id="null-is-valid-for-non-required-string-enums"><a href="#null-is-valid-for-non-required-string-enums"><code>null</code> is valid for non-required string enums</a></h2>
87+
88+
Before Mongoose 8, setting a string path with an `enum` to `null` would lead to a validation error, even if that path wasn't `required`.
89+
In Mongoose 8, it is valid to set a string path to `null` if `required` is not set, even with `enum`.
90+
91+
```javascript
92+
const schema = new Schema({
93+
status: {
94+
type: String,
95+
enum: ['on', 'off']
96+
}
97+
});
98+
const Test = mongoose.model('Test', schema);
99+
100+
// Works fine in Mongoose 8
101+
// Throws a `ValidationError` in Mongoose 7
102+
await Test.create({ status: null });
103+
```
104+
105+
<h2 id="apply-minimize-when-save-updates-an-existing-document"><a href="#apply-minimize-when-save-updates-an-existing-document">Apply minimize when <code>save()</code> updates an existing document</a></h2>
106+
107+
In Mongoose 7, Mongoose would only apply minimize when saving a new document, not when updating an existing document.
108+
109+
```javascript
110+
const schema = new Schema({
111+
nested: {
112+
field1: Number
113+
}
114+
});
115+
const Test = mongoose.model('Test', schema);
116+
117+
// Both Mongoose 7 and Mongoose 8 strip out empty objects when saving
118+
// a new document in MongoDB by default
119+
const { _id } = await Test.create({ nested: {} });
120+
let rawDoc = await Test.findById(_id).lean();
121+
rawDoc.nested; // undefined
122+
123+
// Mongoose 8 will also strip out empty objects when saving an
124+
// existing document in MongoDB
125+
const doc = await Test.findById(_id);
126+
doc.nested = {};
127+
doc.markModified('nested');
128+
await doc.save();
129+
130+
let rawDoc = await Test.findById(_id).lean();
131+
rawDoc.nested; // undefined in Mongoose 8, {} in Mongoose 7
132+
```
133+
134+
<h2 id="apply-base-schema-paths-before-discriminator-paths"><a href="#apply-base-schema-paths-before-discriminator-paths">Apply base schema paths before discriminator paths</a></h2>
135+
136+
This means that, in Mongoose 8, getters and setters on discriminator paths run _after_ getters and setters on base paths.
137+
In Mongoose 7, getters and setters on discriminator paths ran _before_ getters and setters on base paths.
138+
139+
```javascript
140+
141+
const schema = new Schema({
142+
name: {
143+
type: String,
144+
get(v) {
145+
console.log('Base schema getter');
146+
return v;
147+
}
148+
}
149+
});
150+
151+
const Test = mongoose.model('Test', schema);
152+
const D = Test.discriminator('D', new Schema({
153+
otherProp: {
154+
type: String,
155+
get(v) {
156+
console.log('Discriminator schema getter');
157+
return v;
158+
}
159+
}
160+
}));
161+
162+
const doc = new D({ name: 'test', otherProp: 'test' });
163+
// In Mongoose 8, prints "Base schema getter" followed by "Discriminator schema getter"
164+
// In Mongoose 7, prints "Discriminator schema getter" followed by "Base schema getter"
165+
console.log(doc.toObject({ getters: true }));
166+
```
167+
168+
<h2 id="changed-behavior-for-findoneandupdate-with-orfail-and-upsert"><a href="#changed-behavior-for-findoneandupdate-with-orfail-and-upsert">Changed behavior for <code>findOneAndUpdate()</code> with <code>orFail()</code> and upsert</a></h2>
169+
170+
In Mongoose 7, `findOneAndUpdate(filter, update, { upsert: true }).orFail()` would throw a `DocumentNotFoundError` if a new document was upserted.
171+
In other words, `findOneAndUpdate().orFail()` always threw an error if no document was found, even if a new document was upserted.
172+
173+
In Mongoose 8, `findOneAndUpdate(filter, update, { upsert: true }).orFail()` always succeeds.
174+
`findOneAndUpdate().orFail()` now throws a `DocumentNotFoundError` if there's no document returned, rather than if no document was found.
175+
176+
<h2 id="create-waits-until-all-saves-are-done-before-throwing-any-error"><a href="#create-waits-until-all-saves-are-done-before-throwing-any-error"><code>create()</code> waits until all saves are done before throwing any error</a></h2>
177+
178+
In Mongoose 7, `create()` would immediately throw if any `save()` threw an error by default.
179+
Mongoose 8 instead waits for all `save()` calls to finish before throwing the first error that occurred.
180+
So `create()` will throw the same error in both Mongoose 7 and Mongoose 8, Mongoose 8 just may take longer to throw the error.
181+
182+
```javascript
183+
const schema = new Schema({
184+
name: {
185+
type: String,
186+
enum: ['Badger', 'Mushroom']
187+
}
188+
});
189+
schema.pre('save', async function() {
190+
await new Promise(resolve => setTimeout(resolve, 1000));
191+
});
192+
const Test = mongoose.model('Test', schema);
193+
194+
const err = await Test.create([
195+
{ name: 'Badger' },
196+
{ name: 'Mushroom' },
197+
{ name: 'Cow' }
198+
]).then(() => null, err => err);
199+
err; // ValidationError
200+
201+
// In Mongoose 7, there would be 0 documents, because `Test.create()`
202+
// would throw before 'Badger' and 'Mushroom' are inserted
203+
// In Mongoose 8, there will be 2 documents. `Test.create()` waits until
204+
// 'Badger' and 'Mushroom' are inserted before throwing.
205+
await Test.countDocuments();
206+
```
207+
208+
<h2 id="model-validate-returns-copy-of-object"><a href="#model-validate-returns-copy-of-object"><code>Model.validate()</code> returns copy of object</a></h2>
209+
210+
In Mongoose 7, `Model.validate()` would potentially modify the passed in object.
211+
Mongoose 8 instead copies the passed in object first.
212+
213+
```javascript
214+
const schema = new Schema({ answer: Number });
215+
const Test = mongoose.model('Test', schema);
216+
217+
const obj = { answer: '42' };
218+
const res = Test.validate(obj);
219+
220+
typeof obj.answer; // 'string' in Mongoose 8, 'number' in Mongoose 7
221+
typeof res.answer; // 'number' in both Mongoose 7 and Mongoose 8
222+
```
223+
83224
<h2 id="allow-null-for-optional-fields-in-typescript"><a href="#allow-null-for-optional-fields-in-typescript">Allow <code>null</code> For Optional Fields in TypeScript</a></h2>
84225

85226
In Mongoose 8, automatically inferred schema types in TypeScript allow `null` for optional fields.
@@ -95,3 +236,21 @@ const doc = new TestModel();
95236
// In Mongoose 7, this type is `string | undefined`
96237
doc.name;
97238
```
239+
240+
<h2 id="infer-distinct-return-types-from-schema"><a href="#infer-distinct-return-types-from-schema">Infer <code>distinct()</code> return types from schema</a></h2>
241+
242+
```ts
243+
interface User {
244+
name: string;
245+
email: string;
246+
avatar?: string;
247+
}
248+
const schema = new Schema<User>({
249+
name: { type: String, required: true },
250+
email: { type: String, required: true },
251+
avatar: String
252+
});
253+
254+
// Works in Mongoose 8. Compile error in Mongoose 7.
255+
const names: string[] = await MyModel.distinct('name');
256+
```

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
@@ -132,6 +143,12 @@ module.exports = function castBulkWrite(originalModel, op, options) {
132143

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

146+
decorateUpdateWithVersionKey(
147+
op['updateMany']['update'],
148+
op['updateMany'],
149+
model.schema.options.versionKey
150+
);
151+
135152
op['updateMany']['filter'] = cast(model.schema, op['updateMany']['filter'], {
136153
strict: strict,
137154
upsert: op['updateMany'].upsert
@@ -171,6 +188,10 @@ module.exports = function castBulkWrite(originalModel, op, options) {
171188
if (options.session != null) {
172189
doc.$session(options.session);
173190
}
191+
const versionKey = model?.schema?.options?.versionKey;
192+
if (versionKey && doc[versionKey] == null) {
193+
doc[versionKey] = 0;
194+
}
174195
op['replaceOne']['replacement'] = doc;
175196

176197
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
@@ -121,7 +121,8 @@ module.exports = function castUpdate(schema, obj, options, context, filter) {
121121
Object.keys(filter).length > 0) {
122122
// Trick the driver into allowing empty upserts to work around
123123
// https://github.com/mongodb/node-mongodb-native/pull/2490
124-
return { $setOnInsert: filter };
124+
// Shallow clone to avoid passing defaults in re: gh-13962
125+
return { $setOnInsert: { ...filter } };
125126
}
126127
return ret;
127128
};
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 & 22 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');
@@ -2432,33 +2432,14 @@ Model.findOneAndUpdate = function(conditions, update, options) {
24322432
_isNested: true
24332433
});
24342434

2435-
_decorateUpdateWithVersionKey(update, options, this.schema.options.versionKey);
2435+
decorateUpdateWithVersionKey(update, options, this.schema.options.versionKey);
24362436

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

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

2443-
/**
2444-
* Decorate the update with a version key, if necessary
2445-
* @api private
2446-
*/
2447-
2448-
function _decorateUpdateWithVersionKey(update, options, versionKey) {
2449-
if (!versionKey || !(options && options.upsert || false)) {
2450-
return;
2451-
}
2452-
2453-
const updatedPaths = modifiedPaths(update);
2454-
if (!updatedPaths[versionKey]) {
2455-
if (!update.$setOnInsert) {
2456-
update.$setOnInsert = {};
2457-
}
2458-
update.$setOnInsert[versionKey] = 0;
2459-
}
2460-
}
2461-
24622443
/**
24632444
* Issues a mongodb findOneAndUpdate command by a document's _id field.
24642445
* `findByIdAndUpdate(id, ...)` is equivalent to `findOneAndUpdate({ _id: id }, ...)`.
@@ -3912,7 +3893,7 @@ function _update(model, op, conditions, doc, options) {
39123893
model.schema &&
39133894
model.schema.options &&
39143895
model.schema.options.versionKey || null;
3915-
_decorateUpdateWithVersionKey(doc, options, versionKey);
3896+
decorateUpdateWithVersionKey(doc, options, versionKey);
39163897

39173898
return mq[op](conditions, doc, options);
39183899
}

0 commit comments

Comments
 (0)