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