Skip to content

Commit

Permalink
Merge branch '8.0' into vkarpov15/gh-13578
Browse files Browse the repository at this point in the history
  • Loading branch information
vkarpov15 committed Oct 20, 2023
2 parents f6ed0eb + b630afb commit 84ac690
Show file tree
Hide file tree
Showing 12 changed files with 318 additions and 39 deletions.
7 changes: 7 additions & 0 deletions CHANGELOG.md
@@ -1,3 +1,10 @@
7.6.3 / 2023-10-17
==================
* fix(populate): handle multiple spaces when specifying paths to populate using space-delimited paths #13984 #13951
* fix(update): avoid applying defaults on query filter when upserting with empty update #13983 #13962
* fix(model): add versionKey to bulkWrite when inserting or upserting #13981 #13944
* docs: fix typo in timestamps docs #13976 [danielcoker](https://github.com/danielcoker)

7.6.2 / 2023-10-13
==================
* perf: avoid storing a separate entry in schema subpaths for every element in an array #13953 #13874
Expand Down
177 changes: 168 additions & 9 deletions docs/migrating_to_8.md
Expand Up @@ -13,11 +13,18 @@ If you're still on Mongoose 6.x or earlier, please read the [Mongoose 6.x to 7.x

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

<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>

Expand Down Expand Up @@ -54,14 +61,6 @@ const q = numberOne.deleteOne();
const res = await q;
```

<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>

In Mongoose 7, `findOneAndUpdate(filter, update, { upsert: true }).orFail()` would throw a `DocumentNotFoundError` if a new document was upserted.
In other words, `findOneAndUpdate().orFail()` always threw an error if no document was found, even if a new document was upserted.

In Mongoose 8, `findOneAndUpdate(filter, update, { upsert: true }).orFail()` always succeeds.
`findOneAndUpdate().orFail()` now throws a `DocumentNotFoundError` if there's no document returned, rather than if no document was found.

<h2 id="mongodb-node-driver-6"><a href="#mongodb-node-driver-6">MongoDB Node Driver 6</a></h2>

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).
Expand All @@ -75,11 +74,153 @@ In Mongoose 7, `findOneAndRemove()` was an alias for `findOneAndDelete()` that M
Mongoose 8 no longer supports `findOneAndRemove()`.
Use `findOneAndDelete()` instead.

<h2 id="removed-count"><a href="#removed-count">Removed <code>count()</code></a></h2>

`Model.count()` and `Query.prototype.count()` were removed in Mongoose 8. Use `Model.countDocuments()` and `Query.prototype.countDocuments()` instead.

<h2 id="removed-id-setter"><a href="#removed-id-setter">Removed id Setter</a></h2>

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

<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>

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`.
In Mongoose 8, it is valid to set a string path to `null` if `required` is not set, even with `enum`.

```javascript
const schema = new Schema({
status: {
type: String,
enum: ['on', 'off']
}
});
const Test = mongoose.model('Test', schema);

// Works fine in Mongoose 8
// Throws a `ValidationError` in Mongoose 7
await Test.create({ status: null });
```

<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>

In Mongoose 7, Mongoose would only apply minimize when saving a new document, not when updating an existing document.

```javascript
const schema = new Schema({
nested: {
field1: Number
}
});
const Test = mongoose.model('Test', schema);

// Both Mongoose 7 and Mongoose 8 strip out empty objects when saving
// a new document in MongoDB by default
const { _id } = await Test.create({ nested: {} });
let rawDoc = await Test.findById(_id).lean();
rawDoc.nested; // undefined

// Mongoose 8 will also strip out empty objects when saving an
// existing document in MongoDB
const doc = await Test.findById(_id);
doc.nested = {};
doc.markModified('nested');
await doc.save();

let rawDoc = await Test.findById(_id).lean();
rawDoc.nested; // undefined in Mongoose 8, {} in Mongoose 7
```

<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>

This means that, in Mongoose 8, getters and setters on discriminator paths run _after_ getters and setters on base paths.
In Mongoose 7, getters and setters on discriminator paths ran _before_ getters and setters on base paths.

```javascript

const schema = new Schema({
name: {
type: String,
get(v) {
console.log('Base schema getter');
return v;
}
}
});

const Test = mongoose.model('Test', schema);
const D = Test.discriminator('D', new Schema({
otherProp: {
type: String,
get(v) {
console.log('Discriminator schema getter');
return v;
}
}
}));

const doc = new D({ name: 'test', otherProp: 'test' });
// In Mongoose 8, prints "Base schema getter" followed by "Discriminator schema getter"
// In Mongoose 7, prints "Discriminator schema getter" followed by "Base schema getter"
console.log(doc.toObject({ getters: true }));
```

<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>

In Mongoose 7, `findOneAndUpdate(filter, update, { upsert: true }).orFail()` would throw a `DocumentNotFoundError` if a new document was upserted.
In other words, `findOneAndUpdate().orFail()` always threw an error if no document was found, even if a new document was upserted.

In Mongoose 8, `findOneAndUpdate(filter, update, { upsert: true }).orFail()` always succeeds.
`findOneAndUpdate().orFail()` now throws a `DocumentNotFoundError` if there's no document returned, rather than if no document was found.

<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>

In Mongoose 7, `create()` would immediately throw if any `save()` threw an error by default.
Mongoose 8 instead waits for all `save()` calls to finish before throwing the first error that occurred.
So `create()` will throw the same error in both Mongoose 7 and Mongoose 8, Mongoose 8 just may take longer to throw the error.

```javascript
const schema = new Schema({
name: {
type: String,
enum: ['Badger', 'Mushroom']
}
});
schema.pre('save', async function() {
await new Promise(resolve => setTimeout(resolve, 1000));
});
const Test = mongoose.model('Test', schema);

const err = await Test.create([
{ name: 'Badger' },
{ name: 'Mushroom' },
{ name: 'Cow' }
]).then(() => null, err => err);
err; // ValidationError

// In Mongoose 7, there would be 0 documents, because `Test.create()`
// would throw before 'Badger' and 'Mushroom' are inserted
// In Mongoose 8, there will be 2 documents. `Test.create()` waits until
// 'Badger' and 'Mushroom' are inserted before throwing.
await Test.countDocuments();
```

<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>

In Mongoose 7, `Model.validate()` would potentially modify the passed in object.
Mongoose 8 instead copies the passed in object first.

```javascript
const schema = new Schema({ answer: Number });
const Test = mongoose.model('Test', schema);

const obj = { answer: '42' };
const res = Test.validate(obj);

typeof obj.answer; // 'string' in Mongoose 8, 'number' in Mongoose 7
typeof res.answer; // 'number' in both Mongoose 7 and Mongoose 8
```

<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>

In Mongoose 8, automatically inferred schema types in TypeScript allow `null` for optional fields.
Expand All @@ -95,3 +236,21 @@ const doc = new TestModel();
// In Mongoose 7, this type is `string | undefined`
doc.name;
```

<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>

```ts
interface User {
name: string;
email: string;
avatar?: string;
}
const schema = new Schema<User>({
name: { type: String, required: true },
email: { type: String, required: true },
avatar: String
});

// Works in Mongoose 8. Compile error in Mongoose 7.
const names: string[] = await MyModel.distinct('name');
```
21 changes: 21 additions & 0 deletions lib/helpers/model/castBulkWrite.js
Expand Up @@ -6,6 +6,7 @@ const applyTimestampsToChildren = require('../update/applyTimestampsToChildren')
const applyTimestampsToUpdate = require('../update/applyTimestampsToUpdate');
const cast = require('../../cast');
const castUpdate = require('../query/castUpdate');
const decorateUpdateWithVersionKey = require('../update/decorateUpdateWithVersionKey');
const { inspect } = require('util');
const setDefaultsOnInsert = require('../setDefaultsOnInsert');

Expand Down Expand Up @@ -33,6 +34,10 @@ module.exports = function castBulkWrite(originalModel, op, options) {
if (options.session != null) {
doc.$session(options.session);
}
const versionKey = model?.schema?.options?.versionKey;
if (versionKey && doc[versionKey] == null) {
doc[versionKey] = 0;
}
op['insertOne']['document'] = doc;

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

decorateUpdateWithVersionKey(
op['updateOne']['update'],
op['updateOne'],
model.schema.options.versionKey
);

op['updateOne']['filter'] = cast(model.schema, op['updateOne']['filter'], {
strict: strict,
upsert: op['updateOne'].upsert
Expand Down Expand Up @@ -132,6 +143,12 @@ module.exports = function castBulkWrite(originalModel, op, options) {

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

decorateUpdateWithVersionKey(
op['updateMany']['update'],
op['updateMany'],
model.schema.options.versionKey
);

op['updateMany']['filter'] = cast(model.schema, op['updateMany']['filter'], {
strict: strict,
upsert: op['updateMany'].upsert
Expand Down Expand Up @@ -171,6 +188,10 @@ module.exports = function castBulkWrite(originalModel, op, options) {
if (options.session != null) {
doc.$session(options.session);
}
const versionKey = model?.schema?.options?.versionKey;
if (versionKey && doc[versionKey] == null) {
doc[versionKey] = 0;
}
op['replaceOne']['replacement'] = doc;

if (options.skipValidation || op['replaceOne'].skipValidation) {
Expand Down
3 changes: 2 additions & 1 deletion lib/helpers/query/castUpdate.js
Expand Up @@ -121,7 +121,8 @@ module.exports = function castUpdate(schema, obj, options, context, filter) {
Object.keys(filter).length > 0) {
// Trick the driver into allowing empty upserts to work around
// https://github.com/mongodb/node-mongodb-native/pull/2490
return { $setOnInsert: filter };
// Shallow clone to avoid passing defaults in re: gh-13962
return { $setOnInsert: { ...filter } };
}
return ret;
};
Expand Down
26 changes: 26 additions & 0 deletions lib/helpers/update/decorateUpdateWithVersionKey.js
@@ -0,0 +1,26 @@
'use strict';

const modifiedPaths = require('./modifiedPaths');

/**
* Decorate the update with a version key, if necessary
* @api private
*/

module.exports = function decorateUpdateWithVersionKey(update, options, versionKey) {
if (!versionKey || !(options && options.upsert || false)) {
return;
}

const updatedPaths = modifiedPaths(update);
if (!updatedPaths[versionKey]) {
if (options.overwrite) {
update[versionKey] = 0;
} else {
if (!update.$setOnInsert) {
update.$setOnInsert = {};
}
update.$setOnInsert[versionKey] = 0;
}
}
};
25 changes: 3 additions & 22 deletions lib/model.js
Expand Up @@ -34,6 +34,7 @@ const assignVals = require('./helpers/populate/assignVals');
const castBulkWrite = require('./helpers/model/castBulkWrite');
const clone = require('./helpers/clone');
const createPopulateQueryFilter = require('./helpers/populate/createPopulateQueryFilter');
const decorateUpdateWithVersionKey = require('./helpers/update/decorateUpdateWithVersionKey');
const getDefaultBulkwriteResult = require('./helpers/getDefaultBulkwriteResult');
const getSchemaDiscriminatorByValue = require('./helpers/discriminator/getSchemaDiscriminatorByValue');
const discriminator = require('./helpers/model/discriminator');
Expand All @@ -55,7 +56,6 @@ const isPathExcluded = require('./helpers/projection/isPathExcluded');
const decorateDiscriminatorIndexOptions = require('./helpers/indexes/decorateDiscriminatorIndexOptions');
const isPathSelectedInclusive = require('./helpers/projection/isPathSelectedInclusive');
const leanPopulateMap = require('./helpers/populate/leanPopulateMap');
const modifiedPaths = require('./helpers/update/modifiedPaths');
const parallelLimit = require('./helpers/parallelLimit');
const parentPaths = require('./helpers/path/parentPaths');
const prepareDiscriminatorPipeline = require('./helpers/aggregate/prepareDiscriminatorPipeline');
Expand Down Expand Up @@ -2432,33 +2432,14 @@ Model.findOneAndUpdate = function(conditions, update, options) {
_isNested: true
});

_decorateUpdateWithVersionKey(update, options, this.schema.options.versionKey);
decorateUpdateWithVersionKey(update, options, this.schema.options.versionKey);

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

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

/**
* Decorate the update with a version key, if necessary
* @api private
*/

function _decorateUpdateWithVersionKey(update, options, versionKey) {
if (!versionKey || !(options && options.upsert || false)) {
return;
}

const updatedPaths = modifiedPaths(update);
if (!updatedPaths[versionKey]) {
if (!update.$setOnInsert) {
update.$setOnInsert = {};
}
update.$setOnInsert[versionKey] = 0;
}
}

/**
* Issues a mongodb findOneAndUpdate command by a document's _id field.
* `findByIdAndUpdate(id, ...)` is equivalent to `findOneAndUpdate({ _id: id }, ...)`.
Expand Down Expand Up @@ -3912,7 +3893,7 @@ function _update(model, op, conditions, doc, options) {
model.schema &&
model.schema.options &&
model.schema.options.versionKey || null;
_decorateUpdateWithVersionKey(doc, options, versionKey);
decorateUpdateWithVersionKey(doc, options, versionKey);

return mq[op](conditions, doc, options);
}
Expand Down

0 comments on commit 84ac690

Please sign in to comment.