|
| 1 | +# Migrating from 7.x to 8.x |
| 2 | + |
| 3 | +<style> |
| 4 | + ul > li { |
| 5 | + padding: 4px 0px; |
| 6 | + } |
| 7 | +</style> |
| 8 | + |
| 9 | +There are several backwards-breaking changes |
| 10 | +you should be aware of when migrating from Mongoose 7.x to Mongoose 8.x. |
| 11 | + |
| 12 | +If you're still on Mongoose 6.x or earlier, please read the [Mongoose 6.x to 7.x migration guide](migrating_to_7.html) and upgrade to Mongoose 7.x first before upgrading to Mongoose 8. |
| 13 | + |
| 14 | +* [Removed `rawResult` option for `findOneAndUpdate()`](#removed-rawresult-option-for-findoneandupdate) |
| 15 | +* [`Document.prototype.deleteOne()` now returns a query](#document-prototype-deleteone-now-returns-a-query) |
| 16 | +* [MongoDB Node Driver 6.0](#mongodb-node-driver-6) |
| 17 | +* [Removed `findOneAndRemove()`](#removed-findoneandremove) |
| 18 | +* [Removed `count()`](#removed-count) |
| 19 | +* [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 | +* [Removed `overwrite` option for `findOneAndUpdate()`](#removed-overwrite-option-for-findoneandupdate) |
| 24 | +* [Changed behavior for `findOneAndUpdate()` with `orFail()` and upsert](#changed-behavior-for-findoneandupdate-with-orfail-and-upsert) |
| 25 | +* [`create()` waits until all saves are done before throwing any error](#create-waits-until-all-saves-are-done-before-throwing-any-error) |
| 26 | +* [`Model.validate()` returns copy of object](#model-validate-returns-copy-of-object) |
| 27 | +* [Allow `null` For Optional Fields in TypeScript](#allow-null-for-optional-fields-in-typescript) |
| 28 | +* [Infer `distinct()` return types from schema](#infer-distinct-return-types-from-schema) |
| 29 | + |
| 30 | +<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> |
| 31 | + |
| 32 | +The `rawResult` option for `findOneAndUpdate()`, `findOneAndReplace()`, and `findOneAndDelete()` has been replaced by the `includeResultMetadata` option. |
| 33 | + |
| 34 | +```javascript |
| 35 | +const filter = { name: 'Will Riker' }; |
| 36 | +const update = { age: 29 }; |
| 37 | + |
| 38 | +const res = await Character.findOneAndUpdate(filter, update, { |
| 39 | + new: true, |
| 40 | + upsert: true, |
| 41 | + // Replace `rawResult: true` with `includeResultMetadata: true` |
| 42 | + includeResultMetadata: true |
| 43 | +}); |
| 44 | +``` |
| 45 | + |
| 46 | +`includeResultMetadata` in Mongoose 8 behaves identically to `rawResult`. |
| 47 | + |
| 48 | +<h2 id="document-prototype-deleteone-now-returns-a-query"><a href="#document-prototype-deleteone-now-returns-a-query"><code>Document.prototype.deleteOne</code> now returns a query</a></h2> |
| 49 | + |
| 50 | +In Mongoose 7, `doc.deleteOne()` returned a promise that resolved to `doc`. |
| 51 | +In Mongoose 8, `doc.deleteOne()` returns a query for easier chaining, as well as consistency with `doc.updateOne()`. |
| 52 | + |
| 53 | +```javascript |
| 54 | +const numberOne = await Character.findOne({ name: 'Will Riker' }); |
| 55 | + |
| 56 | +// In Mongoose 7, q is a Promise that resolves to `numberOne` |
| 57 | +// In Mongoose 8, q is a Query. |
| 58 | +const q = numberOne.deleteOne(); |
| 59 | + |
| 60 | +// In Mongoose 7, `res === numberOne` |
| 61 | +// In Mongoose 8, `res` is a `DeleteResult`. |
| 62 | +const res = await q; |
| 63 | +``` |
| 64 | + |
| 65 | +<h2 id="mongodb-node-driver-6"><a href="#mongodb-node-driver-6">MongoDB Node Driver 6</a></h2> |
| 66 | + |
| 67 | +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). |
| 68 | +There's a few noteable changes in MongoDB Node driver v6 that affect Mongoose: |
| 69 | + |
| 70 | +1. The `ObjectId` constructor no longer accepts strings of length 12. In Mongoose 7, `new mongoose.Types.ObjectId('12charstring')` was perfectly valid. In Mongoose 8, `new mongoose.Types.ObjectId('12charstring')` throws an error. |
| 71 | + |
| 72 | +<h2 id="removed-findoneandremove"><a href="#removed-findoneandremove">Removed <code>findOneAndRemove()</code></a></h2> |
| 73 | + |
| 74 | +In Mongoose 7, `findOneAndRemove()` was an alias for `findOneAndDelete()` that Mongoose supported for backwards compatibility. |
| 75 | +Mongoose 8 no longer supports `findOneAndRemove()`. |
| 76 | +Use `findOneAndDelete()` instead. |
| 77 | + |
| 78 | +<h2 id="removed-count"><a href="#removed-count">Removed <code>count()</code></a></h2> |
| 79 | + |
| 80 | +`Model.count()` and `Query.prototype.count()` were removed in Mongoose 8. Use `Model.countDocuments()` and `Query.prototype.countDocuments()` instead. |
| 81 | + |
| 82 | +<h2 id="removed-id-setter"><a href="#removed-id-setter">Removed id Setter</a></h2> |
| 83 | + |
| 84 | +In Mongoose 7.4, Mongoose introduced an `id` setter that made `doc.id = '0'.repeat(24)` equivalent to `doc._id = '0'.repeat(24)`. |
| 85 | +In Mongoose 8, that setter is now removed. |
| 86 | + |
| 87 | +<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> |
| 88 | + |
| 89 | +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`. |
| 90 | +In Mongoose 8, it is valid to set a string path to `null` if `required` is not set, even with `enum`. |
| 91 | + |
| 92 | +```javascript |
| 93 | +const schema = new Schema({ |
| 94 | + status: { |
| 95 | + type: String, |
| 96 | + enum: ['on', 'off'] |
| 97 | + } |
| 98 | +}); |
| 99 | +const Test = mongoose.model('Test', schema); |
| 100 | + |
| 101 | +// Works fine in Mongoose 8 |
| 102 | +// Throws a `ValidationError` in Mongoose 7 |
| 103 | +await Test.create({ status: null }); |
| 104 | +``` |
| 105 | + |
| 106 | +<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> |
| 107 | + |
| 108 | +In Mongoose 7, Mongoose would only apply minimize when saving a new document, not when updating an existing document. |
| 109 | + |
| 110 | +```javascript |
| 111 | +const schema = new Schema({ |
| 112 | + nested: { |
| 113 | + field1: Number |
| 114 | + } |
| 115 | +}); |
| 116 | +const Test = mongoose.model('Test', schema); |
| 117 | + |
| 118 | +// Both Mongoose 7 and Mongoose 8 strip out empty objects when saving |
| 119 | +// a new document in MongoDB by default |
| 120 | +const { _id } = await Test.create({ nested: {} }); |
| 121 | +let rawDoc = await Test.findById(_id).lean(); |
| 122 | +rawDoc.nested; // undefined |
| 123 | + |
| 124 | +// Mongoose 8 will also strip out empty objects when saving an |
| 125 | +// existing document in MongoDB |
| 126 | +const doc = await Test.findById(_id); |
| 127 | +doc.nested = {}; |
| 128 | +doc.markModified('nested'); |
| 129 | +await doc.save(); |
| 130 | + |
| 131 | +let rawDoc = await Test.findById(_id).lean(); |
| 132 | +rawDoc.nested; // undefined in Mongoose 8, {} in Mongoose 7 |
| 133 | +``` |
| 134 | + |
| 135 | +<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> |
| 136 | + |
| 137 | +This means that, in Mongoose 8, getters and setters on discriminator paths run *after* getters and setters on base paths. |
| 138 | +In Mongoose 7, getters and setters on discriminator paths ran *before* getters and setters on base paths. |
| 139 | + |
| 140 | +```javascript |
| 141 | + |
| 142 | +const schema = new Schema({ |
| 143 | + name: { |
| 144 | + type: String, |
| 145 | + get(v) { |
| 146 | + console.log('Base schema getter'); |
| 147 | + return v; |
| 148 | + } |
| 149 | + } |
| 150 | +}); |
| 151 | + |
| 152 | +const Test = mongoose.model('Test', schema); |
| 153 | +const D = Test.discriminator('D', new Schema({ |
| 154 | + otherProp: { |
| 155 | + type: String, |
| 156 | + get(v) { |
| 157 | + console.log('Discriminator schema getter'); |
| 158 | + return v; |
| 159 | + } |
| 160 | + } |
| 161 | +})); |
| 162 | + |
| 163 | +const doc = new D({ name: 'test', otherProp: 'test' }); |
| 164 | +// In Mongoose 8, prints "Base schema getter" followed by "Discriminator schema getter" |
| 165 | +// In Mongoose 7, prints "Discriminator schema getter" followed by "Base schema getter" |
| 166 | +console.log(doc.toObject({ getters: true })); |
| 167 | +``` |
| 168 | + |
| 169 | +<h2 id="removed-overwrite-option-for-findoneandupdate"><a href="#removed-overwrite-option-for-findoneandupdate">Removed <code>overwrite</code> option for <code>findOneAndUpdate()</code></a></h2> |
| 170 | + |
| 171 | +Mongoose 7 and earlier supported an `overwrite` option for `findOneAndUpdate()`, `updateOne()`, and `update()`. |
| 172 | +Before Mongoose 7, `overwrite` would skip wrapping the `update` parameter in `$set`, which meant that `findOneAndUpdate()` and `update()` would overwrite the matched document. |
| 173 | +In Mongoose 7, setting `overwrite` would convert `findOneAndUpdate()` to `findOneAndReplace()` and `updateOne()` to `replaceOne()` to retain backwards compatibility. |
| 174 | + |
| 175 | +In Mongoose 8, the `overwrite` option is no longer supported. |
| 176 | +If you want to overwrite the entire document, use `findOneAndReplace()` or `replaceOne()`. |
| 177 | + |
| 178 | +<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> |
| 179 | + |
| 180 | +In Mongoose 7, `findOneAndUpdate(filter, update, { upsert: true }).orFail()` would throw a `DocumentNotFoundError` if a new document was upserted. |
| 181 | +In other words, `findOneAndUpdate().orFail()` always threw an error if no document was found, even if a new document was upserted. |
| 182 | + |
| 183 | +In Mongoose 8, `findOneAndUpdate(filter, update, { upsert: true }).orFail()` always succeeds. |
| 184 | +`findOneAndUpdate().orFail()` now throws a `DocumentNotFoundError` if there's no document returned, rather than if no document was found. |
| 185 | + |
| 186 | +<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> |
| 187 | + |
| 188 | +In Mongoose 7, `create()` would immediately throw if any `save()` threw an error by default. |
| 189 | +Mongoose 8 instead waits for all `save()` calls to finish before throwing the first error that occurred. |
| 190 | +So `create()` will throw the same error in both Mongoose 7 and Mongoose 8, Mongoose 8 just may take longer to throw the error. |
| 191 | + |
| 192 | +```javascript |
| 193 | +const schema = new Schema({ |
| 194 | + name: { |
| 195 | + type: String, |
| 196 | + enum: ['Badger', 'Mushroom'] |
| 197 | + } |
| 198 | +}); |
| 199 | +schema.pre('save', async function() { |
| 200 | + await new Promise(resolve => setTimeout(resolve, 1000)); |
| 201 | +}); |
| 202 | +const Test = mongoose.model('Test', schema); |
| 203 | + |
| 204 | +const err = await Test.create([ |
| 205 | + { name: 'Badger' }, |
| 206 | + { name: 'Mushroom' }, |
| 207 | + { name: 'Cow' } |
| 208 | +]).then(() => null, err => err); |
| 209 | +err; // ValidationError |
| 210 | + |
| 211 | +// In Mongoose 7, there would be 0 documents, because `Test.create()` |
| 212 | +// would throw before 'Badger' and 'Mushroom' are inserted |
| 213 | +// In Mongoose 8, there will be 2 documents. `Test.create()` waits until |
| 214 | +// 'Badger' and 'Mushroom' are inserted before throwing. |
| 215 | +await Test.countDocuments(); |
| 216 | +``` |
| 217 | + |
| 218 | +<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> |
| 219 | + |
| 220 | +In Mongoose 7, `Model.validate()` would potentially modify the passed in object. |
| 221 | +Mongoose 8 instead copies the passed in object first. |
| 222 | + |
| 223 | +```javascript |
| 224 | +const schema = new Schema({ answer: Number }); |
| 225 | +const Test = mongoose.model('Test', schema); |
| 226 | + |
| 227 | +const obj = { answer: '42' }; |
| 228 | +const res = Test.validate(obj); |
| 229 | + |
| 230 | +typeof obj.answer; // 'string' in Mongoose 8, 'number' in Mongoose 7 |
| 231 | +typeof res.answer; // 'number' in both Mongoose 7 and Mongoose 8 |
| 232 | +``` |
| 233 | + |
| 234 | +<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> |
| 235 | + |
| 236 | +In Mongoose 8, automatically inferred schema types in TypeScript allow `null` for optional fields. |
| 237 | +In Mongoose 7, optional fields only allowed `undefined`, not `null`. |
| 238 | + |
| 239 | +```typescript |
| 240 | +const schema = new Schema({ name: String }); |
| 241 | +const TestModel = model('Test', schema); |
| 242 | + |
| 243 | +const doc = new TestModel(); |
| 244 | + |
| 245 | +// In Mongoose 8, this type is `string | null | undefined`. |
| 246 | +// In Mongoose 7, this type is `string | undefined` |
| 247 | +doc.name; |
| 248 | +``` |
| 249 | + |
| 250 | +<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> |
| 251 | + |
| 252 | +```ts |
| 253 | +interface User { |
| 254 | + name: string; |
| 255 | + email: string; |
| 256 | + avatar?: string; |
| 257 | +} |
| 258 | +const schema = new Schema<User>({ |
| 259 | + name: { type: String, required: true }, |
| 260 | + email: { type: String, required: true }, |
| 261 | + avatar: String |
| 262 | +}); |
| 263 | + |
| 264 | +// Works in Mongoose 8. Compile error in Mongoose 7. |
| 265 | +const names: string[] = await MyModel.distinct('name'); |
| 266 | +``` |
0 commit comments