From 92fa6ebc448a226b16c264f5e58e06eefb5936ab Mon Sep 17 00:00:00 2001 From: Valeri Karpov Date: Mon, 6 May 2024 07:02:24 -0400 Subject: [PATCH 01/53] types(model): allow passing strict type checking override to create() Fix #14548 --- test/types/create.test.ts | 9 +++++++++ types/models.d.ts | 12 ++++++------ 2 files changed, 15 insertions(+), 6 deletions(-) diff --git a/test/types/create.test.ts b/test/types/create.test.ts index 033010ae9bb..f2de193182b 100644 --- a/test/types/create.test.ts +++ b/test/types/create.test.ts @@ -40,6 +40,15 @@ Test.create([{ name: 'test' }], { validateBeforeSave: true }).then(docs => { expectType(docs[0].name); }); +Test.create({}).then(doc => { + expectType(doc.name); +}); + +Test.create([{}]).then(docs => { + expectType(docs[0].name); +}); + +expectError(Test.create({})); Test.insertMany({ name: 'test' }, {}, (err, docs) => { expectType(err); diff --git a/types/models.d.ts b/types/models.d.ts index 1a02fd8a202..5504ec91a58 100644 --- a/types/models.d.ts +++ b/types/models.d.ts @@ -192,12 +192,12 @@ declare module 'mongoose' { countDocuments(callback?: Callback): QueryWithHelpers, TQueryHelpers, T>; /** Creates a new document or documents */ - create>(docs: Array, options?: SaveOptions): Promise[]>; - create>(docs: Array, options?: SaveOptions, callback?: Callback>>): Promise[]>; - create>(docs: Array, callback: Callback>>): void; - create>(doc: DocContents | T): Promise>; - create>(...docs: Array): Promise[]>; - create>(doc: T | DocContents, callback: Callback>): void; + create>(docs: Array, options?: SaveOptions): Promise[]>; + create>(docs: Array, options?: SaveOptions, callback?: Callback>>): Promise[]>; + create>(docs: Array, callback: Callback>>): void; + create>(doc: DocContents): Promise>; + create>(...docs: Array): Promise[]>; + create>(doc: DocContents, callback: Callback>): void; /** * Create the collection for this model. By default, if no indexes are specified, From ed7934840571db5b27704d5bb0ebf36131743d5f Mon Sep 17 00:00:00 2001 From: Valeri Karpov Date: Wed, 8 May 2024 19:37:07 -0400 Subject: [PATCH 02/53] test: address code review comments --- test/types/create.test.ts | 3 +++ 1 file changed, 3 insertions(+) diff --git a/test/types/create.test.ts b/test/types/create.test.ts index f2de193182b..983aafa0bdc 100644 --- a/test/types/create.test.ts +++ b/test/types/create.test.ts @@ -50,6 +50,9 @@ Test.create([{}]).then(docs => { expectError(Test.create({})); +Test.create({ name: 'test' }); +Test.create({ _id: new Types.ObjectId('0'.repeat(24)), name: 'test' }); + Test.insertMany({ name: 'test' }, {}, (err, docs) => { expectType(err); expectType(docs[0]._id); From a70ecc2df638208586364ce6c42440f6986905e4 Mon Sep 17 00:00:00 2001 From: Valeri Karpov Date: Mon, 13 May 2024 11:57:24 -0400 Subject: [PATCH 03/53] fix(cast): cast $comment to string in query filters Fix #14576 --- lib/cast.js | 4 ++++ lib/schema/operators/text.js | 2 +- test/cast.test.js | 27 +++++++++++++++++++++++++++ 3 files changed, 32 insertions(+), 1 deletion(-) diff --git a/lib/cast.js b/lib/cast.js index 8a5bb696999..e7bf5b45a05 100644 --- a/lib/cast.js +++ b/lib/cast.js @@ -8,6 +8,7 @@ const CastError = require('./error/cast'); const StrictModeError = require('./error/strict'); const Types = require('./schema/index'); const cast$expr = require('./helpers/query/cast$expr'); +const castString = require('./cast/string'); const castTextSearch = require('./schema/operators/text'); const get = require('./helpers/get'); const getConstructorName = require('./helpers/getConstructorName'); @@ -89,6 +90,9 @@ module.exports = function cast(schema, obj, options, context) { val = cast(schema, val, options, context); } else if (path === '$text') { val = castTextSearch(val, path); + } else if (path === '$comment' && !schema.paths.hasOwnProperty('$comment')) { + val = castString(val, path); + obj[path] = val; } else { if (!schema) { // no casting for Mixed types diff --git a/lib/schema/operators/text.js b/lib/schema/operators/text.js index 81e8fa21bff..79be4ff7cb6 100644 --- a/lib/schema/operators/text.js +++ b/lib/schema/operators/text.js @@ -15,7 +15,7 @@ const castString = require('../../cast/string'); * @api private */ -module.exports = function(val, path) { +module.exports = function castTextSearch(val, path) { if (val == null || typeof val !== 'object') { throw new CastError('$text', val, path); } diff --git a/test/cast.test.js b/test/cast.test.js index 51b97518ff3..c21861b130d 100644 --- a/test/cast.test.js +++ b/test/cast.test.js @@ -160,6 +160,33 @@ describe('cast: ', function() { }); }); + it('casts $comment (gh-14576)', function() { + const schema = new Schema({ name: String }); + + let res = cast(schema, { + $comment: 'test' + }); + assert.deepStrictEqual(res, { $comment: 'test' }); + + res = cast(schema, { + $comment: 42 + }); + assert.deepStrictEqual(res, { $comment: '42' }); + + assert.throws( + () => cast(schema, { + $comment: { name: 'taco' } + }), + /\$comment/ + ); + + const schema2 = new Schema({ $comment: Number }); + res = cast(schema2, { + $comment: 42 + }); + assert.deepStrictEqual(res, { $comment: 42 }); + }); + it('avoids setting stripped out nested schema values to undefined (gh-11291)', function() { const nested = new Schema({}, { id: false, From f4cfe1eb8a33486301b86c4469b7f2b0e30b177b Mon Sep 17 00:00:00 2001 From: Valeri Karpov Date: Thu, 16 May 2024 17:28:51 -0400 Subject: [PATCH 04/53] feat(model): add throwOnValidationError option for opting into getting MongooseBulkWriteError if all valid operations succeed in bulkWrite() and insertMany() Backport #13410 Backport #14587 Fix #14572 --- lib/error/bulkWriteError.js | 41 ++++++++++++++ lib/model.js | 48 +++++++++++++++++ test/model.test.js | 104 ++++++++++++++++++++++++++++++++++++ types/models.d.ts | 2 + 4 files changed, 195 insertions(+) create mode 100644 lib/error/bulkWriteError.js diff --git a/lib/error/bulkWriteError.js b/lib/error/bulkWriteError.js new file mode 100644 index 00000000000..1711b03b586 --- /dev/null +++ b/lib/error/bulkWriteError.js @@ -0,0 +1,41 @@ +/*! + * Module dependencies. + */ + +'use strict'; + +const MongooseError = require('./'); + + +/** + * If `bulkWrite()` or `insertMany()` has validation errors, but + * all valid operations succeed, and 'throwOnValidationError' is true, + * Mongoose will throw this error. + * + * @api private + */ + +class MongooseBulkWriteError extends MongooseError { + constructor(validationErrors, results, rawResult, operation) { + let preview = validationErrors.map(e => e.message).join(', '); + if (preview.length > 200) { + preview = preview.slice(0, 200) + '...'; + } + super(`${operation} failed with ${validationErrors.length} Mongoose validation errors: ${preview}`); + + this.validationErrors = validationErrors; + this.results = results; + this.rawResult = rawResult; + this.operation = operation; + } +} + +Object.defineProperty(MongooseBulkWriteError.prototype, 'name', { + value: 'MongooseBulkWriteError' +}); + +/*! + * exports + */ + +module.exports = MongooseBulkWriteError; diff --git a/lib/model.js b/lib/model.js index 313d1f4747f..b758e27c545 100644 --- a/lib/model.js +++ b/lib/model.js @@ -10,6 +10,7 @@ const Document = require('./document'); const DocumentNotFoundError = require('./error/notFound'); const DivergentArrayError = require('./error/divergentArray'); const EventEmitter = require('events').EventEmitter; +const MongooseBulkWriteError = require('./error/bulkWriteError'); const MongooseBuffer = require('./types/buffer'); const MongooseError = require('./error/index'); const OverwriteModelError = require('./error/overwriteModel'); @@ -3375,6 +3376,7 @@ Model.startSession = function() { * @param {Boolean} [options.lean=false] if `true`, skips hydrating the documents. This means Mongoose will **not** cast or validate any of the documents passed to `insertMany()`. This option is useful if you need the extra performance, but comes with data integrity risk. Consider using with [`castObject()`](#model_Model-castObject). * @param {Number} [options.limit=null] this limits the number of documents being processed (validation/casting) by mongoose in parallel, this does **NOT** send the documents in batches to MongoDB. Use this option if you're processing a large number of documents and your app is running out of memory. * @param {String|Object|Array} [options.populate=null] populates the result documents. This option is a no-op if `rawResult` is set. + * @param {Boolean} [options.throwOnValidationError=false] If true and `ordered: false`, throw an error if one of the operations failed validation, but all valid operations completed successfully. * @param {Function} [callback] callback * @return {Promise} resolving to the raw result from the MongoDB driver if `options.rawResult` was `true`, or the documents that passed validation, otherwise * @api public @@ -3419,6 +3421,7 @@ Model.$__insertMany = function(arr, options, callback) { const limit = options.limit || 1000; const rawResult = !!options.rawResult; const ordered = typeof options.ordered === 'boolean' ? options.ordered : true; + const throwOnValidationError = typeof options.throwOnValidationError === 'boolean' ? options.throwOnValidationError : false; const lean = !!options.lean; if (!Array.isArray(arr)) { @@ -3496,6 +3499,14 @@ Model.$__insertMany = function(arr, options, callback) { // Quickly escape while there aren't any valid docAttributes if (docAttributes.length === 0) { + if (throwOnValidationError) { + return callback(new MongooseBulkWriteError( + validationErrors, + results, + null, + 'insertMany' + )); + } if (rawResult) { const res = { acknowledged: true, @@ -3598,6 +3609,20 @@ Model.$__insertMany = function(arr, options, callback) { } } + if (ordered === false && throwOnValidationError && validationErrors.length > 0) { + for (let i = 0; i < results.length; ++i) { + if (results[i] === void 0) { + results[i] = docs[i]; + } + } + return callback(new MongooseBulkWriteError( + validationErrors, + results, + res, + 'insertMany' + )); + } + if (rawResult) { if (ordered === false) { for (let i = 0; i < results.length; ++i) { @@ -3728,6 +3753,7 @@ function _setIsNew(doc, val) { * @param {Boolean} [options.skipValidation=false] Set to true to skip Mongoose schema validation on bulk write operations. Mongoose currently runs validation on `insertOne` and `replaceOne` operations by default. * @param {Boolean} [options.bypassDocumentValidation=false] If true, disable [MongoDB server-side schema validation](https://www.mongodb.com/docs/manual/core/schema-validation/) for all writes in this bulk. * @param {Boolean} [options.strict=null] Overwrites the [`strict` option](/docs/guide.html#strict) on schema. If false, allows filtering and writing fields not defined in the schema for all writes in this bulk. + * @param {Boolean} [options.throwOnValidationError=false] If true and `ordered: false`, throw an error if one of the operations failed validation, but all valid operations completed successfully. * @param {Function} [callback] callback `function(error, bulkWriteOpResult) {}` * @return {Promise} resolves to a [`BulkWriteOpResult`](https://mongodb.github.io/node-mongodb-native/4.9/classes/BulkWriteResult.html) if the operation succeeds * @api public @@ -3777,6 +3803,7 @@ Model.bulkWrite = function(ops, options, callback) { let remaining = validations.length; let validOps = []; let validationErrors = []; + const results = []; if (remaining === 0) { completeUnorderedValidation.call(this); } else { @@ -3786,6 +3813,7 @@ Model.bulkWrite = function(ops, options, callback) { validOps.push(i); } else { validationErrors.push({ index: i, error: err }); + results[i] = err; } if (--remaining <= 0) { completeUnorderedValidation.call(this); @@ -3799,13 +3827,25 @@ Model.bulkWrite = function(ops, options, callback) { map(v => v.error); function completeUnorderedValidation() { + const validOpIndexes = validOps; validOps = validOps.sort().map(index => ops[index]); if (validOps.length === 0) { + if ('throwOnValidationError' in options && options.throwOnValidationError && validationErrors.length > 0) { + return cb(new MongooseBulkWriteError( + validationErrors.map(err => err.error), + results, + getDefaultBulkwriteResult(), + 'bulkWrite' + )); + } return cb(null, getDefaultBulkwriteResult()); } this.$__collection.bulkWrite(validOps, options, (error, res) => { + for (let i = 0; i < validOpIndexes.length; ++i) { + results[validOpIndexes[i]] = null; + } if (error) { if (validationErrors.length > 0) { error.mongoose = error.mongoose || {}; @@ -3816,6 +3856,14 @@ Model.bulkWrite = function(ops, options, callback) { } if (validationErrors.length > 0) { + if ('throwOnValidationError' in options && options.throwOnValidationError) { + return cb(new MongooseBulkWriteError( + validationErrors, + results, + res, + 'bulkWrite' + )); + } res.mongoose = res.mongoose || {}; res.mongoose.validationErrors = validationErrors; } diff --git a/test/model.test.js b/test/model.test.js index 00573da4440..c07debc6ff1 100644 --- a/test/model.test.js +++ b/test/model.test.js @@ -6111,6 +6111,71 @@ describe('Model', function() { const { num } = await Test.findById(_id); assert.equal(num, 99); }); + + it('bulkWrite should throw an error if there were operations that failed validation, ' + + 'but all operations that passed validation succeeded (gh-13256)', async function() { + const userSchema = new Schema({ age: { type: Number } }); + const User = db.model('User', userSchema); + + const createdUser = await User.create({ name: 'Test' }); + + const err = await User.bulkWrite([ + { + updateOne: { + filter: { _id: createdUser._id }, + update: { $set: { age: 'NaN' } }, + upsert: true + } + }, + { + updateOne: { + filter: { _id: createdUser._id }, + update: { $set: { age: 13 } }, + upsert: true + } + }, + { + updateOne: { + filter: { _id: createdUser._id }, + update: { $set: { age: 12 } }, + upsert: true + } + } + ], { ordered: false, throwOnValidationError: true }) + .then(() => null) + .catch(err => err); + + assert.ok(err); + assert.equal(err.name, 'MongooseBulkWriteError'); + assert.equal(err.validationErrors[0].path, 'age'); + assert.equal(err.results[0].path, 'age'); + }); + + it('throwOnValidationError (gh-14572) (gh-13256)', async function() { + const schema = new Schema({ + num: Number + }); + + const M = db.model('Test', schema); + + const ops = [ + { + insertOne: { + document: { + num: 'not a number' + } + } + } + ]; + + const err = await M.bulkWrite( + ops, + { ordered: false, throwOnValidationError: true } + ).then(() => null, err => err); + assert.ok(err); + assert.equal(err.name, 'MongooseBulkWriteError'); + assert.equal(err.validationErrors[0].errors['num'].name, 'CastError'); + }); }); it('insertMany with Decimal (gh-5190)', async function() { @@ -9028,6 +9093,45 @@ describe('Model', function() { assert.equal(TestModel.staticFn(), 'Returned from staticFn'); }); }); + + it('insertMany should throw an error if there were operations that failed validation, ' + + 'but all operations that passed validation succeeded (gh-13256)', async function() { + const userSchema = new Schema({ + age: { type: Number } + }); + + const User = db.model('User', userSchema); + + let err = await User.insertMany([ + new User({ age: 12 }), + new User({ age: 12 }), + new User({ age: 'NaN' }) + ], { ordered: false, throwOnValidationError: true }) + .then(() => null) + .catch(err => err); + + assert.ok(err); + assert.equal(err.name, 'MongooseBulkWriteError'); + assert.equal(err.validationErrors[0].errors['age'].name, 'CastError'); + assert.ok(err.results[2] instanceof Error); + assert.equal(err.results[2].errors['age'].name, 'CastError'); + + let docs = await User.find(); + assert.deepStrictEqual(docs.map(doc => doc.age), [12, 12]); + + err = await User.insertMany([ + new User({ age: 'NaN' }) + ], { ordered: false, throwOnValidationError: true }) + .then(() => null) + .catch(err => err); + + assert.ok(err); + assert.equal(err.name, 'MongooseBulkWriteError'); + assert.equal(err.validationErrors[0].errors['age'].name, 'CastError'); + + docs = await User.find(); + assert.deepStrictEqual(docs.map(doc => doc.age), [12, 12]); + }); }); diff --git a/types/models.d.ts b/types/models.d.ts index 5504ec91a58..f1cf0a57bc1 100644 --- a/types/models.d.ts +++ b/types/models.d.ts @@ -19,6 +19,7 @@ declare module 'mongoose' { skipValidation?: boolean; strict?: boolean; timestamps?: boolean | 'throw'; + throwOnValidationError?: boolean; } interface InsertManyOptions extends @@ -28,6 +29,7 @@ declare module 'mongoose' { rawResult?: boolean; ordered?: boolean; lean?: boolean; + throwOnValidationError?: boolean; } type InsertManyResult = mongodb.InsertManyResult & { From c3b4bdbb86d21c837a958a054951418d476f6d52 Mon Sep 17 00:00:00 2001 From: Valeri Karpov Date: Fri, 24 May 2024 10:22:52 -0400 Subject: [PATCH 05/53] chore: release 6.12.9 --- CHANGELOG.md | 5 +++++ package.json | 2 +- 2 files changed, 6 insertions(+), 1 deletion(-) diff --git a/CHANGELOG.md b/CHANGELOG.md index 4bdcdeec521..9057fe4668d 100644 --- a/CHANGELOG.md +++ b/CHANGELOG.md @@ -1,3 +1,8 @@ +6.12.9 / 2024-05-24 +=================== + * fix(cast): cast $comment to string in query filters #14590 #14576 + * types(model): allow passing strict type checking override to create() #14571 #14548 + 6.12.8 / 2024-04-10 =================== * fix(document): handle virtuals that are stored as objects but getter returns string with toJSON #14468 #14446 diff --git a/package.json b/package.json index 6d887a254e2..ef60676d102 100644 --- a/package.json +++ b/package.json @@ -1,7 +1,7 @@ { "name": "mongoose", "description": "Mongoose MongoDB ODM", - "version": "6.12.8", + "version": "6.12.9", "author": "Guillermo Rauch ", "keywords": [ "mongodb", From 218d50a868577bb744ada87cf9ba7336402b2a2e Mon Sep 17 00:00:00 2001 From: Valeri Karpov Date: Fri, 24 May 2024 12:20:10 -0400 Subject: [PATCH 06/53] fix(query): shallow clone $or and $and array elements to avoid mutating query filter arguments Fix #14610 --- lib/query.js | 12 ++++++++---- lib/utils.js | 2 ++ test/query.test.js | 15 +++++++++++++++ 3 files changed, 25 insertions(+), 4 deletions(-) diff --git a/lib/query.js b/lib/query.js index 5db5c0e7ef7..eb6f581f920 100644 --- a/lib/query.js +++ b/lib/query.js @@ -2416,13 +2416,17 @@ Query.prototype.merge = function(source) { } opts.omit = {}; - if (this._conditions && this._conditions.$and && source.$and) { + if (this._conditions && Array.isArray(source.$and)) { opts.omit['$and'] = true; - this._conditions.$and = this._conditions.$and.concat(source.$and); + this._conditions.$and = (this._conditions.$and || []).concat( + source.$and.map(el => utils.isPOJO(el) ? utils.merge({}, el) : el) + ); } - if (this._conditions && this._conditions.$or && source.$or) { + if (this._conditions && Array.isArray(source.$or)) { opts.omit['$or'] = true; - this._conditions.$or = this._conditions.$or.concat(source.$or); + this._conditions.$or = (this._conditions.$or || []).concat( + source.$or.map(el => utils.isPOJO(el) ? utils.merge({}, el) : el) + ); } // plain object diff --git a/lib/utils.js b/lib/utils.js index 512b7728a3e..0172ac3e433 100644 --- a/lib/utils.js +++ b/lib/utils.js @@ -294,6 +294,8 @@ exports.merge = function merge(to, from, options, path) { to[key] = from[key]; } } + + return to; }; /** diff --git a/test/query.test.js b/test/query.test.js index 42354434d90..61e70674f44 100644 --- a/test/query.test.js +++ b/test/query.test.js @@ -4214,4 +4214,19 @@ describe('Query', function() { assert.strictEqual(doc.account.owner, undefined); assert.strictEqual(doc.account.taxIds, undefined); }); + + it('avoids mutating $or, $and elements when casting (gh-14610)', async function() { + const personSchema = new mongoose.Schema({ + name: String, + age: Number + }); + const Person = db.model('Person', personSchema); + + const filter = [{ name: 'Me', age: '20' }, { name: 'You', age: '50' }]; + await Person.find({ $or: filter }); + assert.deepStrictEqual(filter, [{ name: 'Me', age: '20' }, { name: 'You', age: '50' }]); + + await Person.find({ $and: filter }); + assert.deepStrictEqual(filter, [{ name: 'Me', age: '20' }, { name: 'You', age: '50' }]); + }); }); From d4ad58efc0eecbf3b08e99acb9781d4eb708a633 Mon Sep 17 00:00:00 2001 From: Valeri Karpov Date: Fri, 24 May 2024 12:22:24 -0400 Subject: [PATCH 07/53] bring in some changes from #14580 --- lib/query.js | 10 ++++++++-- 1 file changed, 8 insertions(+), 2 deletions(-) diff --git a/lib/query.js b/lib/query.js index eb6f581f920..c4a413fad1f 100644 --- a/lib/query.js +++ b/lib/query.js @@ -2416,14 +2416,20 @@ Query.prototype.merge = function(source) { } opts.omit = {}; - if (this._conditions && Array.isArray(source.$and)) { + if (Array.isArray(source.$and)) { opts.omit['$and'] = true; + if (!this._conditions) { + this._conditions = {}; + } this._conditions.$and = (this._conditions.$and || []).concat( source.$and.map(el => utils.isPOJO(el) ? utils.merge({}, el) : el) ); } - if (this._conditions && Array.isArray(source.$or)) { + if (Array.isArray(source.$or)) { opts.omit['$or'] = true; + if (!this._conditions) { + this._conditions = {}; + } this._conditions.$or = (this._conditions.$or || []).concat( source.$or.map(el => utils.isPOJO(el) ? utils.merge({}, el) : el) ); From da126f4d6c5f3fb5000c000efeb3d4e30b4ba5a6 Mon Sep 17 00:00:00 2001 From: Valeri Karpov Date: Sun, 2 Jun 2024 11:40:58 -0400 Subject: [PATCH 08/53] docs(migrating_to_7): add id setter to Mongoose 7 migration guide --- docs/migrating_to_7.md | 26 ++++++++++++++++++++++++++ 1 file changed, 26 insertions(+) diff --git a/docs/migrating_to_7.md b/docs/migrating_to_7.md index d9526322f4b..c4df3209089 100644 --- a/docs/migrating_to_7.md +++ b/docs/migrating_to_7.md @@ -16,6 +16,7 @@ If you're still on Mongoose 5.x, please read the [Mongoose 5.x to 6.x migration * [Dropped callback support](#dropped-callback-support) * [Removed `update()`](#removed-update) * [ObjectId requires `new`](#objectid-requires-new) +* [`id` setter](#id-setter) * [Discriminator schemas use base schema options by default](#discriminator-schemas-use-base-schema-options-by-default) * [Removed `castForQueryWrapper()`, updated `castForQuery()` signature](#removed-castforquerywrapper) * [Copy schema options in `Schema.prototype.add()`](#copy-schema-options-in-schema-prototype-add) @@ -196,6 +197,31 @@ In Mongoose 7, `ObjectId` is now a [JavaScript class](https://masteringjs.io/tut const oid = new mongoose.Types.ObjectId('0'.repeat(24)); ``` +

id Setter

+ +Starting in Mongoose 7.4, Mongoose's built-in `id` virtual (which stores the document's `_id` as a string) has a setter which allows modifying the document's `_id` property via `id`. + +```javascript +const doc = await TestModel.findOne(); + +doc.id = '000000000000000000000000'; +doc._id; // ObjectId('000000000000000000000000') +``` + +This can cause surprising behavior if you create a `new TestModel(obj)` where `obj` contains both an `id` and an `_id`, or if you use `doc.set()` + +```javascript +// Because `id` is after `_id`, the `id` will overwrite the `_id` +const doc = new TestModel({ + _id: '000000000000000000000000', + id: '111111111111111111111111' +}); + +doc._id; // ObjectId('111111111111111111111111') +``` + +[The `id` setter was later removed in Mongoose 8](/docs/migrating_to_8.html#removed-id-setter) due to compatibility issues. +

Discriminator schemas use base schema options by default

When you use `Model.discriminator()`, Mongoose will now use the discriminator base schema's options by default. From 40ec8139fc0c3ee06e742b95c2ecc802e9bdc4ac Mon Sep 17 00:00:00 2001 From: Valeri Karpov Date: Fri, 24 May 2024 10:03:43 -0400 Subject: [PATCH 09/53] types: pass DocType down to subdocuments so `HydratedSingleSubdocument` and `HydratedArraySubdocument` `toObject()` returns correct type Fix #14601 --- test/types/subdocuments.test.ts | 43 ++++++++++++++++++++++++++++++++- types/index.d.ts | 4 +-- types/types.d.ts | 2 +- 3 files changed, 45 insertions(+), 4 deletions(-) diff --git a/test/types/subdocuments.test.ts b/test/types/subdocuments.test.ts index 59b58716f5f..14386816fe5 100644 --- a/test/types/subdocuments.test.ts +++ b/test/types/subdocuments.test.ts @@ -1,4 +1,13 @@ -import { Schema, model, Model, Document, Types } from 'mongoose'; +import { + Schema, + model, + Model, + Document, + Types, + HydratedArraySubdocument, + HydratedSingleSubdocument +} from 'mongoose'; +import { expectAssignable } from 'tsd'; const childSchema: Schema = new Schema({ name: String }); @@ -108,3 +117,35 @@ function gh13040(): void { product.ownerDocument(); }); } + +function gh14601() { + interface ISub { + field1: string; + } + interface IMain { + f1: string; + f2: HydratedSingleSubdocument; + f3: HydratedArraySubdocument[]; + } + + const subSchema = new Schema({ field1: String }, { _id: false }); + + const mainSchema = new Schema({ + f1: String, + f2: { type: subSchema }, + f3: { type: [subSchema] } + }); + const MainModel = model('Main', mainSchema); + + const item = new MainModel({ + f1: 'test', + f2: { field1: 'test' }, + f3: [{ field1: 'test' }] + }); + + const obj = item.toObject(); + + const obj2 = item.f2.toObject(); + + expectAssignable<{ _id: Types.ObjectId, field1: string }>(obj2); +} diff --git a/types/index.d.ts b/types/index.d.ts index 05578ccc3ca..0422b776090 100644 --- a/types/index.d.ts +++ b/types/index.d.ts @@ -153,8 +153,8 @@ declare module 'mongoose' { > > >; - export type HydratedSingleSubdocument = Types.Subdocument & Require_id & TOverrides; - export type HydratedArraySubdocument = Types.ArraySubdocument & Require_id & TOverrides; + export type HydratedSingleSubdocument = Types.Subdocument, DocType> & Require_id & TOverrides; + export type HydratedArraySubdocument = Types.ArraySubdocument, DocType> & Require_id & TOverrides; export type HydratedDocumentFromSchema = HydratedDocument< InferSchemaType, diff --git a/types/types.d.ts b/types/types.d.ts index 55b48138ad3..b1707c245f7 100644 --- a/types/types.d.ts +++ b/types/types.d.ts @@ -95,7 +95,7 @@ declare module 'mongoose' { $parent(): Document; } - class ArraySubdocument extends Subdocument { + class ArraySubdocument extends Subdocument { /** Returns this sub-documents parent array. */ parentArray(): Types.DocumentArray; } From 7e6db5f24ead97aebb72060efc55f44de7b452c8 Mon Sep 17 00:00:00 2001 From: Valeri Karpov Date: Wed, 5 Jun 2024 16:08:12 -0400 Subject: [PATCH 10/53] types: cherry-pick #14612 back to 7.x and fix conflicts --- types/types.d.ts | 2 +- 1 file changed, 1 insertion(+), 1 deletion(-) diff --git a/types/types.d.ts b/types/types.d.ts index b1707c245f7..5d99ab38ee5 100644 --- a/types/types.d.ts +++ b/types/types.d.ts @@ -82,7 +82,7 @@ declare module 'mongoose' { class ObjectId extends mongodb.ObjectId { } - class Subdocument extends Document { + class Subdocument extends Document { $isSingleNested: true; /** Returns the top level document of this sub-document. */ From 0f99e994d4c2306a6a33d7c8ddabecc12e3c48dc Mon Sep 17 00:00:00 2001 From: Valeri Karpov Date: Wed, 5 Jun 2024 16:14:04 -0400 Subject: [PATCH 11/53] chore: release 7.6.13 --- CHANGELOG.md | 6 ++++++ package.json | 2 +- 2 files changed, 7 insertions(+), 1 deletion(-) diff --git a/CHANGELOG.md b/CHANGELOG.md index 76916f787df..3f2fe8526f7 100644 --- a/CHANGELOG.md +++ b/CHANGELOG.md @@ -1,3 +1,9 @@ +7.6.13 / 2024-06-05 +=================== + * fix(query): shallow clone $or and $and array elements to avoid mutating query filter arguments #14614 #14610 + * types: pass DocType down to subdocuments so HydratedSingleSubdocument and HydratedArraySubdocument toObject() returns correct type #14612 #14601 + * docs(migrating_to_7): add id setter to Mongoose 7 migration guide #14645 #13672 + 7.6.12 / 2024-05-21 =================== * fix(array): avoid converting to $set when calling pull() on an element in the middle of the array #14531 #14502 diff --git a/package.json b/package.json index 14eac926273..d4a78b6b0da 100644 --- a/package.json +++ b/package.json @@ -1,7 +1,7 @@ { "name": "mongoose", "description": "Mongoose MongoDB ODM", - "version": "7.6.12", + "version": "7.6.13", "author": "Guillermo Rauch ", "keywords": [ "mongodb", From 53d382b0f9df9590fcb1c1468de38dbeb56b7f03 Mon Sep 17 00:00:00 2001 From: Valeri Karpov Date: Thu, 6 Jun 2024 15:31:00 -0400 Subject: [PATCH 12/53] chore: release 6.13.0 --- CHANGELOG.md | 4 ++++ package.json | 2 +- 2 files changed, 5 insertions(+), 1 deletion(-) diff --git a/CHANGELOG.md b/CHANGELOG.md index 9057fe4668d..36d481adf2e 100644 --- a/CHANGELOG.md +++ b/CHANGELOG.md @@ -1,3 +1,7 @@ +6.13.0 / 2024-06-06 +=================== + * feat(model): add throwOnValidationError option for opting into getting MongooseBulkWriteError if all valid operations succeed in bulkWrite() and insertMany() #14599 #14587 #14572 #13410 + 6.12.9 / 2024-05-24 =================== * fix(cast): cast $comment to string in query filters #14590 #14576 diff --git a/package.json b/package.json index ef60676d102..bd1ab99dc29 100644 --- a/package.json +++ b/package.json @@ -1,7 +1,7 @@ { "name": "mongoose", "description": "Mongoose MongoDB ODM", - "version": "6.12.9", + "version": "6.13.0", "author": "Guillermo Rauch ", "keywords": [ "mongodb", From 0198236ec2a4eb62e181cd20dec152ecfe829de2 Mon Sep 17 00:00:00 2001 From: Valeri Karpov Date: Thu, 6 Jun 2024 16:23:13 -0400 Subject: [PATCH 13/53] types: remove duplicate key --- types/models.d.ts | 1 - 1 file changed, 1 deletion(-) diff --git a/types/models.d.ts b/types/models.d.ts index aabc64bdd2e..7cb082bc372 100644 --- a/types/models.d.ts +++ b/types/models.d.ts @@ -28,7 +28,6 @@ declare module 'mongoose' { throwOnValidationError?: boolean; strict?: boolean; timestamps?: boolean | 'throw'; - throwOnValidationError?: boolean; } interface InsertManyOptions extends From 061bb828c706512d79c79f5d42774316bf167357 Mon Sep 17 00:00:00 2001 From: Valeri Karpov Date: Tue, 18 Jun 2024 16:09:58 -0400 Subject: [PATCH 14/53] chore: release 7.7.0 --- CHANGELOG.md | 4 ++++ package.json | 2 +- 2 files changed, 5 insertions(+), 1 deletion(-) diff --git a/CHANGELOG.md b/CHANGELOG.md index 9e426df3b9c..be735e4aad7 100644 --- a/CHANGELOG.md +++ b/CHANGELOG.md @@ -1,3 +1,7 @@ +7.7.0 / 2024-06-18 +================== + * feat(model): add throwOnValidationError option for opting into getting MongooseBulkWriteError if all valid operations succeed in bulkWrite() and insertMany() #14599 #14587 #14572 #13410 + 6.13.0 / 2024-06-06 =================== * feat(model): add throwOnValidationError option for opting into getting MongooseBulkWriteError if all valid operations succeed in bulkWrite() and insertMany() #14599 #14587 #14572 #13410 diff --git a/package.json b/package.json index d4a78b6b0da..d2877b4c2b6 100644 --- a/package.json +++ b/package.json @@ -1,7 +1,7 @@ { "name": "mongoose", "description": "Mongoose MongoDB ODM", - "version": "7.6.13", + "version": "7.7.0", "author": "Guillermo Rauch ", "keywords": [ "mongodb", From 1ced0150feca358586e16ee1265c089ba54f28e8 Mon Sep 17 00:00:00 2001 From: hasezoey Date: Wed, 10 Jul 2024 13:14:03 +0200 Subject: [PATCH 15/53] types(query): fix usage of "RawDocType" where "DocType" should be passed --- types/query.d.ts | 4 ++-- 1 file changed, 2 insertions(+), 2 deletions(-) diff --git a/types/query.d.ts b/types/query.d.ts index 39ff69de9df..87fabb10a7c 100644 --- a/types/query.d.ts +++ b/types/query.d.ts @@ -374,7 +374,7 @@ declare module 'mongoose' { ): QueryWithHelpers, DocType, THelpers, RawDocType, 'find'>; find( filter: FilterQuery - ): QueryWithHelpers, DocType, THelpers, RawDocType, 'find'>; + ): QueryWithHelpers, DocType, THelpers, RawDocType, 'find'>; find(): QueryWithHelpers, DocType, THelpers, RawDocType, 'find'>; /** Declares the query a findOne operation. When executed, returns the first found document. */ @@ -389,7 +389,7 @@ declare module 'mongoose' { ): QueryWithHelpers; findOne( filter?: FilterQuery - ): QueryWithHelpers; + ): QueryWithHelpers; /** Creates a `findOneAndDelete` query: atomically finds the given document, deletes it, and returns the document as it was before deletion. */ findOneAndDelete( From ec619000a0db61a8d8ea638938176e0b704e4acb Mon Sep 17 00:00:00 2001 From: Valeri Karpov Date: Thu, 11 Jul 2024 12:15:58 -0400 Subject: [PATCH 16/53] feat: add transactionAsyncLocalStorage option to opt in to automatically setting session on all transactions Backport #14583 to 7.x Re: #13889 --- docs/transactions.md | 31 ++++++++++++++++++--- lib/aggregate.js | 5 ++++ lib/connection.js | 13 ++++++--- lib/index.js | 16 +++++++++-- lib/model.js | 7 +++++ lib/query.js | 5 ++++ lib/validoptions.js | 1 + test/docs/transactions.test.js | 49 ++++++++++++++++++++++++++++++++++ types/mongooseoptions.d.ts | 7 +++++ 9 files changed, 126 insertions(+), 8 deletions(-) diff --git a/docs/transactions.md b/docs/transactions.md index 901282dac44..4251cd5d017 100644 --- a/docs/transactions.md +++ b/docs/transactions.md @@ -1,8 +1,6 @@ # Transactions in Mongoose -[Transactions](https://www.mongodb.com/transactions) are new in MongoDB -4.0 and Mongoose 5.2.0. Transactions let you execute multiple operations -in isolation and potentially undo all the operations if one of them fails. +[Transactions](https://www.mongodb.com/transactions) let you execute multiple operations in isolation and potentially undo all the operations if one of them fails. This guide will get you started using transactions with Mongoose.

Getting Started with Transactions

@@ -86,6 +84,33 @@ Below is an example of executing an aggregation within a transaction. [require:transactions.*aggregate] ``` +

Using AsyncLocalStorage

+ +One major pain point with transactions in Mongoose is that you need to remember to set the `session` option on every operation. +If you don't, your operation will execute outside of the transaction. +Mongoose 8.4 is able to set the `session` operation on all operations within a `Connection.prototype.transaction()` executor function using Node's [AsyncLocalStorage API](https://nodejs.org/api/async_context.html#class-asynclocalstorage). +Set the `transactionAsyncLocalStorage` option using `mongoose.set('transactionAsyncLocalStorage', true)` to enable this feature. + +```javascript +mongoose.set('transactionAsyncLocalStorage', true); + +const Test = mongoose.model('Test', mongoose.Schema({ name: String })); + +const doc = new Test({ name: 'test' }); + +// Save a new doc in a transaction that aborts +await connection.transaction(async() => { + await doc.save(); // Notice no session here + throw new Error('Oops'); +}).catch(() => {}); + +// false, `save()` was rolled back +await Test.exists({ _id: doc._id }); +``` + +With `transactionAsyncLocalStorage`, you no longer need to pass sessions to every operation. +Mongoose will add the session by default under the hood. +

Advanced Usage

Advanced users who want more fine-grained control over when they commit or abort transactions diff --git a/lib/aggregate.js b/lib/aggregate.js index 450f9e34d12..0d3ec1bf927 100644 --- a/lib/aggregate.js +++ b/lib/aggregate.js @@ -1022,6 +1022,11 @@ Aggregate.prototype.exec = async function exec() { applyGlobalMaxTimeMS(this.options, model); applyGlobalDiskUse(this.options, model); + const asyncLocalStorage = this.model()?.db?.base.transactionAsyncLocalStorage?.getStore(); + if (!this.options.hasOwnProperty('session') && asyncLocalStorage?.session != null) { + this.options.session = asyncLocalStorage.session; + } + if (this.options && this.options.cursor) { return new AggregationCursor(this); } diff --git a/lib/connection.js b/lib/connection.js index c116a3cde32..bec66623d0b 100644 --- a/lib/connection.js +++ b/lib/connection.js @@ -517,7 +517,7 @@ Connection.prototype.startSession = async function startSession(options) { Connection.prototype.transaction = function transaction(fn, options) { return this.startSession().then(session => { session[sessionNewDocuments] = new Map(); - return session.withTransaction(() => _wrapUserTransaction(fn, session), options). + return session.withTransaction(() => _wrapUserTransaction(fn, session, this.base), options). then(res => { delete session[sessionNewDocuments]; return res; @@ -536,9 +536,16 @@ Connection.prototype.transaction = function transaction(fn, options) { * Reset document state in between transaction retries re: gh-13698 */ -async function _wrapUserTransaction(fn, session) { +async function _wrapUserTransaction(fn, session, mongoose) { try { - const res = await fn(session); + const res = mongoose.transactionAsyncLocalStorage == null + ? await fn(session) + : await new Promise(resolve => { + mongoose.transactionAsyncLocalStorage.run( + { session }, + () => resolve(fn(session)) + ); + }); return res; } catch (err) { _resetSessionDocuments(session); diff --git a/lib/index.js b/lib/index.js index 38197ec50ae..5b716293a4c 100644 --- a/lib/index.js +++ b/lib/index.js @@ -40,6 +40,8 @@ require('./helpers/printJestWarning'); const objectIdHexRegexp = /^[0-9A-Fa-f]{24}$/; +const { AsyncLocalStorage } = require('node:async_hooks'); + /** * Mongoose constructor. * @@ -102,6 +104,10 @@ function Mongoose(options) { } this.Schema.prototype.base = this; + if (options?.transactionAsyncLocalStorage) { + this.transactionAsyncLocalStorage = new AsyncLocalStorage(); + } + Object.defineProperty(this, 'plugins', { configurable: false, enumerable: true, @@ -258,7 +264,7 @@ Mongoose.prototype.set = function(key, value) { if (optionKey === 'objectIdGetter') { if (optionValue) { - Object.defineProperty(mongoose.Types.ObjectId.prototype, '_id', { + Object.defineProperty(_mongoose.Types.ObjectId.prototype, '_id', { enumerable: false, configurable: true, get: function() { @@ -266,7 +272,13 @@ Mongoose.prototype.set = function(key, value) { } }); } else { - delete mongoose.Types.ObjectId.prototype._id; + delete _mongoose.Types.ObjectId.prototype._id; + } + } else if (optionKey === 'transactionAsyncLocalStorage') { + if (optionValue && !_mongoose.transactionAsyncLocalStorage) { + _mongoose.transactionAsyncLocalStorage = new AsyncLocalStorage(); + } else if (!optionValue && _mongoose.transactionAsyncLocalStorage) { + delete _mongoose.transactionAsyncLocalStorage; } } } diff --git a/lib/model.js b/lib/model.js index 3aba07e3563..96b0c78398a 100644 --- a/lib/model.js +++ b/lib/model.js @@ -288,8 +288,11 @@ Model.prototype.$__handleSave = function(options, callback) { } const session = this.$session(); + const asyncLocalStorage = this[modelDbSymbol].base.transactionAsyncLocalStorage?.getStore(); if (!saveOptions.hasOwnProperty('session') && session != null) { saveOptions.session = session; + } else if (asyncLocalStorage?.session != null) { + saveOptions.session = asyncLocalStorage.session; } if (this.$isNew) { @@ -3463,6 +3466,10 @@ Model.bulkWrite = async function bulkWrite(ops, options) { const ordered = options.ordered == null ? true : options.ordered; const validations = ops.map(op => castBulkWrite(this, op, options)); + const asyncLocalStorage = this.db.base.transactionAsyncLocalStorage?.getStore(); + if ((!options || !options.hasOwnProperty('session')) && asyncLocalStorage?.session != null) { + options = { ...options, session: asyncLocalStorage.session }; + } return new Promise((resolve, reject) => { if (ordered) { diff --git a/lib/query.js b/lib/query.js index c4a413fad1f..3f6fc76fb89 100644 --- a/lib/query.js +++ b/lib/query.js @@ -1980,6 +1980,11 @@ Query.prototype._optionsForExec = function(model) { // Apply schema-level `writeConcern` option applyWriteConcern(model.schema, options); + const asyncLocalStorage = this.model?.db?.base.transactionAsyncLocalStorage?.getStore(); + if (!this.options.hasOwnProperty('session') && asyncLocalStorage?.session != null) { + options.session = asyncLocalStorage.session; + } + const readPreference = model && model.schema && model.schema.options && diff --git a/lib/validoptions.js b/lib/validoptions.js index af4e116deec..ee087337bbf 100644 --- a/lib/validoptions.js +++ b/lib/validoptions.js @@ -31,6 +31,7 @@ const VALID_OPTIONS = Object.freeze([ 'strictQuery', 'toJSON', 'toObject', + 'transactionAsyncLocalStorage', 'translateAliases' ]); diff --git a/test/docs/transactions.test.js b/test/docs/transactions.test.js index d196fab7180..bde95f4b176 100644 --- a/test/docs/transactions.test.js +++ b/test/docs/transactions.test.js @@ -421,4 +421,53 @@ describe('transactions', function() { assert.equal(i, 3); }); + + describe('transactionAsyncLocalStorage option', function() { + let m; + before(async function() { + m = new mongoose.Mongoose(); + m.set('transactionAsyncLocalStorage', true); + + await m.connect(start.uri); + }); + + after(async function() { + await m.disconnect(); + }); + + it('transaction() sets `session` by default if transactionAsyncLocalStorage option is set', async function() { + const Test = m.model('Test', m.Schema({ name: String })); + + await Test.createCollection(); + await Test.deleteMany({}); + + const doc = new Test({ name: 'test_transactionAsyncLocalStorage' }); + await assert.rejects( + () => m.connection.transaction(async() => { + await doc.save(); + + await Test.updateOne({ name: 'foo' }, { name: 'foo' }, { upsert: true }); + + let docs = await Test.aggregate([{ $match: { _id: doc._id } }]); + assert.equal(docs.length, 1); + + docs = await Test.find({ _id: doc._id }); + assert.equal(docs.length, 1); + + docs = await async function test() { + return await Test.findOne({ _id: doc._id }); + }(); + assert.equal(doc.name, 'test_transactionAsyncLocalStorage'); + + throw new Error('Oops!'); + }), + /Oops!/ + ); + let exists = await Test.exists({ _id: doc._id }); + assert.ok(!exists); + + exists = await Test.exists({ name: 'foo' }); + assert.ok(!exists); + }); + }); }); diff --git a/types/mongooseoptions.d.ts b/types/mongooseoptions.d.ts index 7fec10b208f..9c35ab8222b 100644 --- a/types/mongooseoptions.d.ts +++ b/types/mongooseoptions.d.ts @@ -203,6 +203,13 @@ declare module 'mongoose' { */ toObject?: ToObjectOptions; + /** + * Set to true to make Mongoose use Node.js' built-in AsyncLocalStorage (Node >= 16.0.0) + * to set `session` option on all operations within a `connection.transaction(fn)` call + * by default. Defaults to false. + */ + transactionAsyncLocalStorage?: boolean; + /** * If `true`, convert any aliases in filter, projection, update, and distinct * to their database property names. Defaults to false. From b9deadb9ce76cecd43db325cc6b064104915f8e6 Mon Sep 17 00:00:00 2001 From: Valeri Karpov Date: Fri, 12 Jul 2024 10:39:29 -0400 Subject: [PATCH 17/53] Update docs/transactions.md Co-authored-by: hasezoey --- docs/transactions.md | 2 +- 1 file changed, 1 insertion(+), 1 deletion(-) diff --git a/docs/transactions.md b/docs/transactions.md index 4251cd5d017..2fb0a6f395d 100644 --- a/docs/transactions.md +++ b/docs/transactions.md @@ -88,7 +88,7 @@ Below is an example of executing an aggregation within a transaction. One major pain point with transactions in Mongoose is that you need to remember to set the `session` option on every operation. If you don't, your operation will execute outside of the transaction. -Mongoose 8.4 is able to set the `session` operation on all operations within a `Connection.prototype.transaction()` executor function using Node's [AsyncLocalStorage API](https://nodejs.org/api/async_context.html#class-asynclocalstorage). +Mongoose 7.8 is able to set the `session` operation on all operations within a `Connection.prototype.transaction()` executor function using Node's [AsyncLocalStorage API](https://nodejs.org/api/async_context.html#class-asynclocalstorage). Set the `transactionAsyncLocalStorage` option using `mongoose.set('transactionAsyncLocalStorage', true)` to enable this feature. ```javascript From 378d1155487d80d33f2a30816fc0db396fb0185b Mon Sep 17 00:00:00 2001 From: Valeri Karpov Date: Fri, 12 Jul 2024 10:39:46 -0400 Subject: [PATCH 18/53] Update types/mongooseoptions.d.ts Co-authored-by: hasezoey --- types/mongooseoptions.d.ts | 2 +- 1 file changed, 1 insertion(+), 1 deletion(-) diff --git a/types/mongooseoptions.d.ts b/types/mongooseoptions.d.ts index 9c35ab8222b..9aee6c9e206 100644 --- a/types/mongooseoptions.d.ts +++ b/types/mongooseoptions.d.ts @@ -204,7 +204,7 @@ declare module 'mongoose' { toObject?: ToObjectOptions; /** - * Set to true to make Mongoose use Node.js' built-in AsyncLocalStorage (Node >= 16.0.0) + * Set to true to make Mongoose use Node.js' built-in AsyncLocalStorage (Added in: v13.10.0, v12.17.0; Stable since 16.4.0) * to set `session` option on all operations within a `connection.transaction(fn)` call * by default. Defaults to false. */ From 3f21bfaedf235440c8040a3d58cdf2a9faf9a8a8 Mon Sep 17 00:00:00 2001 From: Valeri Karpov Date: Fri, 12 Jul 2024 11:08:14 -0400 Subject: [PATCH 19/53] fix: backport #14743 --- lib/model.js | 5 +++++ test/docs/transactions.test.js | 5 +++++ 2 files changed, 10 insertions(+) diff --git a/lib/model.js b/lib/model.js index 96b0c78398a..0e38daa8942 100644 --- a/lib/model.js +++ b/lib/model.js @@ -3113,6 +3113,11 @@ Model.$__insertMany = function(arr, options, callback) { const throwOnValidationError = typeof options.throwOnValidationError === 'boolean' ? options.throwOnValidationError : false; const lean = !!options.lean; + const asyncLocalStorage = this.db.base.transactionAsyncLocalStorage?.getStore(); + if ((!options || !options.hasOwnProperty('session')) && asyncLocalStorage?.session != null) { + options = { ...options, session: asyncLocalStorage.session }; + } + if (!Array.isArray(arr)) { arr = [arr]; } diff --git a/test/docs/transactions.test.js b/test/docs/transactions.test.js index bde95f4b176..d93ea32e60d 100644 --- a/test/docs/transactions.test.js +++ b/test/docs/transactions.test.js @@ -459,6 +459,8 @@ describe('transactions', function() { }(); assert.equal(doc.name, 'test_transactionAsyncLocalStorage'); + await Test.insertMany([{ name: 'bar' }]); + throw new Error('Oops!'); }), /Oops!/ @@ -468,6 +470,9 @@ describe('transactions', function() { exists = await Test.exists({ name: 'foo' }); assert.ok(!exists); + + exists = await Test.exists({ name: 'bar' }); + assert.ok(!exists); }); }); }); From e909e0b940f946dcbf8b4436de99e015c99061a7 Mon Sep 17 00:00:00 2001 From: Valeri Karpov Date: Mon, 15 Jul 2024 09:35:47 -0400 Subject: [PATCH 20/53] fix: support session: null option for save() to opt out of automatic session option with transactionAsyncLocalStorage; backport #14744 --- lib/model.js | 5 +++-- test/docs/transactions.test.js | 13 ++++++++++++- 2 files changed, 15 insertions(+), 3 deletions(-) diff --git a/lib/model.js b/lib/model.js index 0e38daa8942..f649ce33651 100644 --- a/lib/model.js +++ b/lib/model.js @@ -289,9 +289,10 @@ Model.prototype.$__handleSave = function(options, callback) { const session = this.$session(); const asyncLocalStorage = this[modelDbSymbol].base.transactionAsyncLocalStorage?.getStore(); - if (!saveOptions.hasOwnProperty('session') && session != null) { + if (session != null) { saveOptions.session = session; - } else if (asyncLocalStorage?.session != null) { + } else if (!options.hasOwnProperty('session') && asyncLocalStorage?.session != null) { + // Only set session from asyncLocalStorage if `session` option wasn't originally passed in options saveOptions.session = asyncLocalStorage.session; } diff --git a/test/docs/transactions.test.js b/test/docs/transactions.test.js index d93ea32e60d..a6f2a501250 100644 --- a/test/docs/transactions.test.js +++ b/test/docs/transactions.test.js @@ -441,7 +441,7 @@ describe('transactions', function() { await Test.createCollection(); await Test.deleteMany({}); - const doc = new Test({ name: 'test_transactionAsyncLocalStorage' }); + let doc = new Test({ name: 'test_transactionAsyncLocalStorage' }); await assert.rejects( () => m.connection.transaction(async() => { await doc.save(); @@ -473,6 +473,17 @@ describe('transactions', function() { exists = await Test.exists({ name: 'bar' }); assert.ok(!exists); + + doc = new Test({ name: 'test_transactionAsyncLocalStorage' }); + await assert.rejects( + () => m.connection.transaction(async() => { + await doc.save({ session: null }); + throw new Error('Oops!'); + }), + /Oops!/ + ); + exists = await Test.exists({ _id: doc._id }); + assert.ok(exists); }); }); }); From 0c11d129cefb2bc966d5597a23dd8aa27253c082 Mon Sep 17 00:00:00 2001 From: Valeri Karpov Date: Tue, 23 Jul 2024 12:50:54 -0400 Subject: [PATCH 21/53] fix(query): handle casting $switch in $expr Fix #14751 Backport #14755 to 7.x --- lib/helpers/query/cast$expr.js | 8 ++++++-- test/helpers/query.cast$expr.test.js | 29 ++++++++++++++++++++++++++++ 2 files changed, 35 insertions(+), 2 deletions(-) diff --git a/lib/helpers/query/cast$expr.js b/lib/helpers/query/cast$expr.js index a13190b1c41..efa2cace344 100644 --- a/lib/helpers/query/cast$expr.js +++ b/lib/helpers/query/cast$expr.js @@ -92,8 +92,12 @@ function _castExpression(val, schema, strictQuery) { } else if (val.$ifNull != null) { val.$ifNull.map(v => _castExpression(v, schema, strictQuery)); } else if (val.$switch != null) { - val.branches.map(v => _castExpression(v, schema, strictQuery)); - val.default = _castExpression(val.default, schema, strictQuery); + if (Array.isArray(val.$switch.branches)) { + val.$switch.branches = val.$switch.branches.map(v => _castExpression(v, schema, strictQuery)); + } + if ('default' in val.$switch) { + val.$switch.default = _castExpression(val.$switch.default, schema, strictQuery); + } } const keys = Object.keys(val); diff --git a/test/helpers/query.cast$expr.test.js b/test/helpers/query.cast$expr.test.js index 416db9af5c6..fd3f7cd5bfe 100644 --- a/test/helpers/query.cast$expr.test.js +++ b/test/helpers/query.cast$expr.test.js @@ -118,4 +118,33 @@ describe('castexpr', function() { res = cast$expr({ $eq: [{ $round: ['$value'] }, 2] }, testSchema); assert.deepStrictEqual(res, { $eq: [{ $round: ['$value'] }, 2] }); }); + + it('casts $switch (gh-14751)', function() { + const testSchema = new Schema({ + name: String, + scores: [Number] + }); + const res = cast$expr({ + $eq: [ + { + $switch: { + branches: [{ case: { $eq: ['$$NOW', '$$NOW'] }, then: true }], + default: false + } + }, + true + ] + }, testSchema); + assert.deepStrictEqual(res, { + $eq: [ + { + $switch: { + branches: [{ case: { $eq: ['$$NOW', '$$NOW'] }, then: true }], + default: false + } + }, + true + ] + }); + }); }); From e8b0933538655bd8e624e2e8860a39c75eef8f4e Mon Sep 17 00:00:00 2001 From: Valeri Karpov Date: Tue, 23 Jul 2024 17:04:40 -0400 Subject: [PATCH 22/53] chore: release 7.8.0 --- CHANGELOG.md | 5 +++++ package.json | 2 +- 2 files changed, 6 insertions(+), 1 deletion(-) diff --git a/CHANGELOG.md b/CHANGELOG.md index be735e4aad7..03043834ea9 100644 --- a/CHANGELOG.md +++ b/CHANGELOG.md @@ -1,3 +1,8 @@ +7.8.0 / 2024-07-23 +================== + * feat: add transactionAsyncLocalStorage option to opt in to automatically setting session on all transactions #14744 #14742 #14583 #13889 + * types(query): fix usage of "RawDocType" where "DocType" should be passed #14737 [hasezoey](https://github.com/hasezoey) + 7.7.0 / 2024-06-18 ================== * feat(model): add throwOnValidationError option for opting into getting MongooseBulkWriteError if all valid operations succeed in bulkWrite() and insertMany() #14599 #14587 #14572 #13410 diff --git a/package.json b/package.json index d2877b4c2b6..cbd4adc15af 100644 --- a/package.json +++ b/package.json @@ -1,7 +1,7 @@ { "name": "mongoose", "description": "Mongoose MongoDB ODM", - "version": "7.7.0", + "version": "7.8.0", "author": "Guillermo Rauch ", "keywords": [ "mongodb", From 87fb3824eeaa612af3cb904fb5d80e53f69a6853 Mon Sep 17 00:00:00 2001 From: Valeri Karpov Date: Mon, 12 Aug 2024 14:02:01 -0400 Subject: [PATCH 23/53] add id setter to changelog re: #13517 --- CHANGELOG.md | 1 + 1 file changed, 1 insertion(+) diff --git a/CHANGELOG.md b/CHANGELOG.md index 03043834ea9..3b00baf559e 100644 --- a/CHANGELOG.md +++ b/CHANGELOG.md @@ -292,6 +292,7 @@ ================== * perf: speed up mapOfSubdocs benchmark by 4x by avoiding unnecessary O(n^2) loop in getPathsToValidate() #13614 * feat: upgrade to MongoDB Node.js driver 5.7.0 #13591 + * feat: add `id` setter which allows modifying `_id` by setting `id` (Note this change was reverted in Mongoose 8) #13517 * feat: support generating custom cast error message with a function #13608 #3162 * feat(query): support MongoDB driver's includeResultMetadata option for findOneAndUpdate #13584 #13539 * feat(connection): add Connection.prototype.removeDb() for removing a related connection #13580 #11821 From 7af3dbda79f302bbceb22da244ce6421efa56934 Mon Sep 17 00:00:00 2001 From: Valeri Karpov Date: Thu, 15 Aug 2024 19:08:45 -0400 Subject: [PATCH 24/53] docs: upgrade marked -> 14.x, use new {#id-here} syntax for declaring custom header ids Fix #13211 --- docs/async-await.md | 2 +- docs/browser.md | 2 +- docs/connections.md | 32 +++++------ docs/deprecations.md | 4 +- docs/documents.md | 12 ++-- docs/geojson.md | 8 +-- docs/guide.md | 114 +++++++++++++++---------------------- docs/jest.md | 8 +-- docs/middleware.md | 26 ++++----- docs/migrating_to_5.md | 126 +++++++++++------------------------------ docs/migrating_to_8.md | 32 +++++------ package.json | 2 +- scripts/website.js | 66 ++++++++++++++------- 13 files changed, 190 insertions(+), 244 deletions(-) diff --git a/docs/async-await.md b/docs/async-await.md index 241d938a783..eb2d98ea405 100644 --- a/docs/async-await.md +++ b/docs/async-await.md @@ -82,7 +82,7 @@ async function doStuffWithUser() { } ``` -

Async/Await with Mongoose Queries

+## Async/Await with Mongoose Queries {#queries} Under the hood, [async/await is syntactic sugar](https://developer.mozilla.org/en-US/docs/Learn/JavaScript/Asynchronous/Async_await) over the Promise API. Due to the surprisingly simple way promises are implemented in JavaScript, the keyword `await` will try to unwrap any object with a property whose key is the string β€˜then’ and whose value is a function. diff --git a/docs/browser.md b/docs/browser.md index a32172bbdb2..75aa21a9ddd 100644 --- a/docs/browser.md +++ b/docs/browser.md @@ -19,7 +19,7 @@ const mongoose = require('mongoose/browser'); import mongoose from 'mongoose/browser'; ``` -

Using the Browser Library

+Using the Browser Library {#usage} Mongoose's browser library is very limited. The only use case it supports is validating documents as shown below. diff --git a/docs/connections.md b/docs/connections.md index e823e3e3f40..24164c01b8b 100644 --- a/docs/connections.md +++ b/docs/connections.md @@ -35,7 +35,7 @@ See the [mongodb connection string spec](http://www.mongodb.com/docs/manual/refe
  • Multi Tenant Connections
  • -

    Operation Buffering

    +## Operation Buffering {#buffering} Mongoose lets you start using your models immediately, without waiting for mongoose to establish a connection to MongoDB. @@ -95,7 +95,7 @@ const Model = mongoose.model('Test', schema); await Model.createCollection(); ``` -

    Error Handling

    +## Error Handling {#error-handling} There are two classes of errors that can occur with a Mongoose connection. @@ -129,7 +129,7 @@ mongoose.connection.on('error', err => { Note that Mongoose does not necessarily emit an 'error' event if it loses connectivity to MongoDB. You should listen to the `disconnected` event to report when Mongoose is disconnected from MongoDB. -

    Options

    +## Options {#options} The `connect` method also accepts an `options` object which will be passed on to the underlying MongoDB driver. @@ -158,7 +158,7 @@ Below are some of the options that are important for tuning Mongoose. * `serverSelectionTimeoutMS` - The MongoDB driver will try to find a server to send any given operation to, and keep retrying for `serverSelectionTimeoutMS` milliseconds. If not set, the MongoDB driver defaults to using `30000` (30 seconds). * `heartbeatFrequencyMS` - The MongoDB driver sends a heartbeat every `heartbeatFrequencyMS` to check on the status of the connection. A heartbeat is subject to `serverSelectionTimeoutMS`, so the MongoDB driver will retry failed heartbeats for up to 30 seconds by default. Mongoose only emits a `'disconnected'` event after a heartbeat has failed, so you may want to decrease this setting to reduce the time between when your server goes down and when Mongoose emits `'disconnected'`. We recommend you do **not** set this setting below 1000, too many heartbeats can lead to performance degradation. -

    serverSelectionTimeoutMS

    +## serverSelectionTimeoutMS {#serverselectiontimeoutms} The `serverSelectionTimeoutMS` option is extremely important: it controls how long the MongoDB Node.js driver will attempt to retry any operation before erroring out. This includes initial connection, like `await mongoose.connect()`, as well as any operations that make requests to MongoDB, like `save()` or `find()`. @@ -208,7 +208,7 @@ for (let i = 0; i < 3; ++i) { } ``` -

    Callback

    +## Callback {#callback} The `connect()` function also accepts a callback parameter and returns a [promise](promises.html). @@ -225,7 +225,7 @@ mongoose.connect(uri, options).then( ); ``` -

    Connection String Options

    +## Connection String Options {#connection-string-options} You can also specify driver options in your connection string as [parameters in the query string](https://en.wikipedia.org/wiki/Query_string) @@ -258,7 +258,7 @@ are closely associated with the hostname and authentication information. * `authSource` - The database to use when authenticating with `user` and `pass`. In MongoDB, [users are scoped to a database](https://www.mongodb.com/docs/manual/tutorial/manage-users-and-roles/). If you are getting an unexpected login failure, you may need to set this option. * `family` - Whether to connect using IPv4 or IPv6. This option passed to [Node.js' `dns.lookup()`](https://nodejs.org/api/dns.html#dns_dns_lookup_hostname_options_callback) function. If you don't specify this option, the MongoDB driver will try IPv6 first and then IPv4 if IPv6 fails. If your `mongoose.connect(uri)` call takes a long time, try `mongoose.connect(uri, { family: 4 })` -

    Connection Events

    +## Connection Events {#connection-events} Connections inherit from [Node.js' `EventEmitter` class](https://nodejs.org/api/events.html#events_class_eventemitter), and emit events when something happens to the connection, like losing @@ -304,13 +304,13 @@ conn.on('disconnecting', () => console.log('disconnecting')); conn.on('close', () => console.log('close')); ``` -

    A note about keepAlive

    +## A note about keepAlive {#keepAlive} Before Mongoose 5.2.0, you needed to enable the `keepAlive` option to initiate [TCP keepalive](https://tldp.org/HOWTO/TCP-Keepalive-HOWTO/overview.html) to prevent `"connection closed"` errors. However, `keepAlive` has been `true` by default since Mongoose 5.2.0, and the `keepAlive` is deprecated as of Mongoose 7.2.0. Please remove `keepAlive` and `keepAliveInitialDelay` options from your Mongoose connections. -

    Replica Set Connections

    +## Replica Set Connections {#replicaset_connections} To connect to a replica set you pass a comma delimited list of hosts to connect to rather than a single host. @@ -331,7 +331,7 @@ To connect to a single node replica set, specify the `replicaSet` option. mongoose.connect('mongodb://host1:port1/?replicaSet=rsName'); ``` -

    Server Selection

    +## Server Selection {#server-selection} The underlying MongoDB driver uses a process known as [server selection](https://github.com/mongodb/specifications/blob/master/source/server-selection/server-selection.rst) to connect to MongoDB and send operations to MongoDB. If the MongoDB driver can't find a server to send an operation to after `serverSelectionTimeoutMS`, @@ -366,7 +366,7 @@ mongoose.connect(uri, { }).catch(err => console.log(err.reason)); ``` -

    Replica Set Host Names

    +## Replica Set Host Names {#replicaset-hostnames} MongoDB replica sets rely on being able to reliably figure out the domain name for each member. On Linux and OSX, the MongoDB server uses the output of the [`hostname` command](https://linux.die.net/man/1/hostname) to figure out the domain name to report to the replica set. @@ -399,7 +399,7 @@ if (err.name === 'MongooseServerSelectionError') { } ``` -

    Multi-mongos support

    +## Multi-mongos support {#mongos_connections} You can also connect to multiple [mongos](https://www.mongodb.com/docs/manual/reference/program/mongos/) instances for high availability in a sharded cluster. You do @@ -410,7 +410,7 @@ for high availability in a sharded cluster. You do mongoose.connect('mongodb://mongosA:27501,mongosB:27501', cb); ``` -

    Multiple connections

    +## Multiple connections {#multiple_connections} So far we've seen how to connect to MongoDB using Mongoose's default connection. Mongoose creates a *default connection* when you call `mongoose.connect()`. @@ -493,7 +493,7 @@ module.exports = conn; You can create separate files for each connection, like `connections/web.js` and `connections/mobile.js` if you want to create separate connections for your web API backend and your mobile API backend. Your business logic can then `require()` or `import` the connection it needs. -

    Connection Pools

    +## Connection Pools {#connection_pools} Each `connection`, whether created with `mongoose.connect` or `mongoose.createConnection` are all backed by an internal configurable @@ -512,7 +512,7 @@ mongoose.createConnection(uri); The connection pool size is important because [MongoDB currently can only process one operation per socket](https://thecodebarbarian.com/slow-trains-in-mongodb-and-nodejs). So `maxPoolSize` functions as a cap on the number of concurrent operations. -

    Multi Tenant Connections

    +## Multi Tenant Connections {#multi-tenant-connections} In the context of Mongoose, a multi-tenant architecture typically means a case where multiple different clients talk to MongoDB through a single Mongoose application. This typically means each client makes queries and executes updates through a single Mongoose application, but has a distinct MongoDB database within the same MongoDB cluster. @@ -592,6 +592,6 @@ app.get('/users/:tenantId', function(req, res) { app.listen(3000); ``` -

    Next Up

    +## Next Up {#next} Now that we've covered connections, let's take a look at [models](models.html). diff --git a/docs/deprecations.md b/docs/deprecations.md index bb194674e19..d7a7936e1fe 100644 --- a/docs/deprecations.md +++ b/docs/deprecations.md @@ -5,7 +5,7 @@ that Mongoose users should be aware of. Mongoose provides options to work around these deprecation warnings, but you need to test whether these options cause any problems for your application. Please [report any issues on GitHub](https://github.com/Automattic/mongoose/issues/new). -

    Summary

    +## Summary {#summary} To fix all deprecation warnings, follow the below steps: @@ -13,7 +13,7 @@ To fix all deprecation warnings, follow the below steps: Read below for more a more detailed description of each deprecation warning. -

    rawResult

    +## `rawResult` {#rawresult} As of Mongoose 7.4.0, the `rawResult` option to `findOneAndUpdate()` is deprecated. You should instead use the `includeResultMetadata` option, which the MongoDB Node.js driver's new option that replaces `rawResult`. diff --git a/docs/documents.md b/docs/documents.md index 93cb1b9e9fe..20764c6dbef 100644 --- a/docs/documents.md +++ b/docs/documents.md @@ -13,7 +13,7 @@ to documents as stored in MongoDB. Each document is an instance of its
  • Overwriting
  • -

    Documents vs Models

    +## Documents vs Models {#documents-vs-models} [Document](api/document.html#Document) and [Model](api/model.html#Model) are distinct classes in Mongoose. The Model class is a subclass of the Document class. @@ -33,7 +33,7 @@ In Mongoose, a "document" generally means an instance of a model. You should not have to create an instance of the Document class without going through a model. -

    Retrieving

    +## Retrieving {#retrieving} When you load documents from MongoDB using model functions like [`findOne()`](api/model.html#model_Model-findOne), you get a Mongoose document back. @@ -46,7 +46,7 @@ doc instanceof mongoose.Model; // true doc instanceof mongoose.Document; // true ``` -

    Updating Using save()

    +## Updating Using `save()` {#updating-using-save} Mongoose documents track changes. You can modify a document using vanilla JavaScript assignments and Mongoose will convert it into [MongoDB update operators](https://www.mongodb.com/docs/manual/reference/operator/update/). @@ -81,7 +81,7 @@ doc.name = 'foo'; await doc.save(); // Throws DocumentNotFoundError ``` -

    Updating Using Queries

    +## Updating Using Queries {#updating-using-queries} The [`save()`](api/model.html#model_Model-save) function is generally the right way to update a document with Mongoose. With `save()`, you get full @@ -100,7 +100,7 @@ await MyModel.updateMany({}, { $set: { name: 'foo' } }); execute `save()` middleware. If you need save middleware and full validation, first query for the document and then `save()` it.* -

    Validating

    +## Validating {#validating} Documents are casted and validated before they are saved. Mongoose first casts values to the specified type and then validates them. Internally, Mongoose @@ -136,7 +136,7 @@ await Person.updateOne({}, { age: -1 }, { runValidators: true }); Read the [validation](validation.html) guide for more details. -

    Overwriting

    +## Overwriting {#overwriting} There are 2 different ways to overwrite a document (replacing all keys in the document). One way is to use the diff --git a/docs/geojson.md b/docs/geojson.md index c094894b249..b42461056f3 100644 --- a/docs/geojson.md +++ b/docs/geojson.md @@ -5,7 +5,7 @@ polygons. [MongoDB has excellent support for geospatial queries](http://thecodeb on GeoJSON objects. Let's take a look at how you can use Mongoose to store and query GeoJSON objects. -

    Point Schema

    +## Point Schema {#points} The most simple structure in GeoJSON is a point. Below is an example point representing the approximate location of [San Francisco](https://www.google.com/maps/@37.7,-122.5,9z). @@ -64,7 +64,7 @@ const citySchema = new mongoose.Schema({ }); ``` -

    Polygon Schema

    +## Polygon Schema {#polygons} GeoJSON polygons let you define an arbitrary shape on a map. For example, the below polygon is a GeoJSON rectangle that approximates the border @@ -106,7 +106,7 @@ const citySchema = new mongoose.Schema({ }); ``` -

    Geospatial Queries with Mongoose

    +## Geospatial Queries with Mongoose {#querying} Mongoose queries support the same [geospatial query operators](http://thecodebarbarian.com/80-20-guide-to-mongodb-geospatial-queries) that the MongoDB driver does. For example, the below script saves a @@ -128,7 +128,7 @@ that's a shorthand for `$geoWithin`. [require:geojson.*within helper] ``` -

    Geospatial Indexes

    +## Geospatial Indexes {#geospatial-indexes} MongoDB supports [2dsphere indexes](https://www.mongodb.com/docs/manual/core/2dsphere/) for speeding up geospatial queries. Here's how you can define diff --git a/docs/guide.md b/docs/guide.md index 9c2766c311b..19d94665e7f 100644 --- a/docs/guide.md +++ b/docs/guide.md @@ -19,7 +19,7 @@ If you are migrating from 7.x to 8.x please take a moment to read the [migration
  • Further Reading
  • -

    Defining your schema

    +## Defining your schema {#definition} Everything in Mongoose starts with a Schema. Each schema maps to a MongoDB collection and defines the shape of the documents within that collection. @@ -88,7 +88,7 @@ properties, they also define document [instance methods](#methods), [static Model methods](#statics), [compound indexes](#indexes), and document lifecycle hooks called [middleware](middleware.html). -

    Creating a model

    +## Creating a model {#models} To use our schema definition, we need to convert our `blogSchema` into a [Model](models.html) we can work with. @@ -99,7 +99,7 @@ const Blog = mongoose.model('Blog', blogSchema); // ready to go! ``` -

    Ids

    +## Ids {#_id} By default, Mongoose adds an `_id` property to your schemas. @@ -165,7 +165,7 @@ const nestedSchema = new Schema({ }); ``` -

    Instance methods

    +## Instance methods {#methods} Instances of `Models` are [documents](documents.html). Documents have many of their own [built-in instance methods](api/document.html). @@ -206,7 +206,7 @@ dog.findSimilarTypes((err, dogs) => { * The example above uses the `Schema.methods` object directly to save an instance method. You can also use the `Schema.method()` helper as described [here](api/schema.html#schema_Schema-method). * Do **not** declare methods using ES6 arrow functions (`=>`). Arrow functions [explicitly prevent binding `this`](https://developer.mozilla.org/en-US/docs/Web/JavaScript/Reference/Functions/Arrow_functions#No_binding_of_this), so your method will **not** have access to the document and the above examples will not work. -

    Statics

    +## Statics {#statics} You can also add static functions to your model. There are three equivalent ways to add a static: @@ -243,7 +243,7 @@ animals = animals.concat(await Animal.findByBreed('Poodle')); Do **not** declare statics using ES6 arrow functions (`=>`). Arrow functions [explicitly prevent binding `this`](https://developer.mozilla.org/en-US/docs/Web/JavaScript/Reference/Functions/Arrow_functions#No_binding_of_this), so the above examples will not work because of the value of `this`. -

    Query Helpers

    +## Query Helpers {#query-helpers} You can also add query helper functions, which are like instance methods but for mongoose queries. Query helper methods let you extend mongoose's @@ -279,7 +279,7 @@ Animal.findOne().byName('fido').exec((err, animal) => { }); ``` -

    Indexes

    +## Indexes {#indexes} MongoDB supports [secondary indexes](http://www.mongodb.com/docs/manual/indexes/). With mongoose, we define these indexes within our `Schema` [at](api/schematype.html#schematype_SchemaType-index) [the](api/schematype.html#schematype_SchemaType-unique) [path](api/schematype.html#schematype_SchemaType-sparse) [level](api/schemadateoptions.html#schemadateoptions_SchemaDateOptions-expires) or the `schema` level. @@ -332,7 +332,7 @@ Animal.on('index', error => { See also the [Model#ensureIndexes](api/model.html#model_Model-ensureIndexes) method. -

    Virtuals

    +## Virtuals {#virtuals} [Virtuals](api/schema.html#schema_Schema-virtual) are document properties that you can get and set but that do not get persisted to MongoDB. The getters @@ -475,7 +475,7 @@ Since virtuals are not stored in MongoDB, you can't query with them. You can [learn more about virtuals here](https://masteringjs.io/tutorials/mongoose/virtuals). -

    Aliases

    +## Aliases {#aliases} Aliases are a particular type of virtual where the getter and setter seamlessly get and set another property. This is handy for saving network @@ -510,7 +510,7 @@ nested path aliases inline as long as you use the full nested path [require:gh-6671] ``` -

    Options

    +## Options {#options} Schemas have a few configurable options which can be passed to the constructor or to the `set` method: @@ -561,7 +561,7 @@ Valid options: * [autoSearchIndex](#autoSearchIndex) * [readConcern](#readConcern) -

    option: autoIndex

    +## option: autoIndex {#autoIndex} By default, Mongoose's [`init()` function](api/model.html#model_Model-init) creates all the indexes defined in your model's schema by calling @@ -580,7 +580,7 @@ Clock.ensureIndexes(callback); The `autoIndex` option is set to `true` by default. You can change this default by setting [`mongoose.set('autoIndex', false);`](api/mongoose.html#mongoose_Mongoose-set) -

    option: autoCreate

    +## option: autoCreate {#autoCreate} Before Mongoose builds indexes, it calls `Model.createCollection()` to create the underlying collection in MongoDB by default. Calling `createCollection()` sets the [collection's default collation](https://thecodebarbarian.com/a-nodejs-perspective-on-mongodb-34-collations) based on the [collation option](#collation) and establishes the collection as @@ -605,7 +605,7 @@ const Test = mongoose.model('Test', schema); await Test.createCollection(); ``` -

    option: bufferCommands

    +## option: bufferCommands {#bufferCommands} By default, mongoose buffers commands when the connection goes down until the driver manages to reconnect. To disable buffering, set `bufferCommands` @@ -623,7 +623,7 @@ mongoose.set('bufferCommands', true); const schema = new Schema({ /* ... */ }, { bufferCommands: false }); ``` -

    option: bufferTimeoutMS

    +## option: bufferTimeoutMS {#bufferTimeoutMS} If `bufferCommands` is on, this option sets the maximum amount of time Mongoose buffering will wait before throwing an error. If not specified, Mongoose will use 10000 (10 seconds). @@ -633,7 +633,7 @@ throwing an error. If not specified, Mongoose will use 10000 (10 seconds). const schema = new Schema({ /* ... */ }, { bufferTimeoutMS: 1000 }); ``` -

    option: capped

    +## option: capped {#capped} Mongoose supports MongoDBs [capped](https://www.mongodb.com/docs/manual/core/capped-collections/) collections. To specify the underlying MongoDB collection be `capped`, set @@ -652,7 +652,7 @@ In this case you must explicitly pass the `size` option, which is required. new Schema({ /* ... */ }, { capped: { size: 1024, max: 1000, autoIndexId: true } }); ``` -

    option: collection

    +## option: collection {#collection} Mongoose by default produces a collection name by passing the model name to the `utils.toCollectionName` method. @@ -663,7 +663,7 @@ for your collection. const dataSchema = new Schema({ /* ... */ }, { collection: 'data' }); ``` -

    option: discriminatorKey

    +## option: discriminatorKey {#discriminatorKey} When you define a [discriminator](discriminators.html), Mongoose adds a path to your schema that stores which discriminator a document is an instance of. By default, Mongoose @@ -682,7 +682,7 @@ const doc = new PersonModel({ name: 'James T. Kirk' }); doc.type; // 'Person' ``` -

    option: excludeIndexes

    +## option: excludeIndexes {#excludeIndexes} When `excludeIndexes` is `true`, Mongoose will not create indexes from the given subdocument schema. This option only works when the schema is used in a subdocument path or document array path, Mongoose ignores this option if set on the top-level schema for a model. @@ -706,7 +706,7 @@ const User = new Schema({ }); ``` -

    option: id

    +## option: id {#id} Mongoose assigns each of your schemas an `id` virtual getter by default which returns the document's `_id` field cast to a string, or in the case of @@ -727,7 +727,7 @@ const p = new Page({ name: 'mongodb.org' }); console.log(p.id); // undefined ``` -

    option: _id

    +## option: _id {#_id} Mongoose assigns each of your schemas an `_id` field by default if one is not passed into the [Schema](api/schema.html#schema_Schema) constructor. @@ -757,7 +757,7 @@ Model.create({ children: [{ name: 'Luke' }] }, (error, doc) => { }); ``` -

    option: minimize

    +## option: minimize {#minimize} Mongoose will, by default, "minimize" schemas by removing empty objects. @@ -802,7 +802,7 @@ sam.inventory.barrowBlade = 1; sam.$isEmpty('inventory'); // false ``` -

    option: read

    +## option: read {#read} Allows setting [query#read](api/query.html#query_Query-read) options at the schema level, providing us a way to apply default @@ -838,7 +838,7 @@ const schema = new Schema({ /* ... */ }, { read: ['nearest', { disk: 'ssd' }] }) mongoose.model('JellyBean', schema); ``` -

    option: writeConcern

    +## option: writeConcern {#writeConcern} Allows setting [write concern](https://www.mongodb.com/docs/manual/reference/write-concern/) at the schema level. @@ -853,7 +853,7 @@ const schema = new Schema({ name: String }, { }); ``` -

    option: shardKey

    +## option: shardKey {#shardKey} The `shardKey` option is used when we have a [sharded MongoDB architecture](https://www.mongodb.com/docs/manual/sharding/). Each sharded collection is given a shard key which must be present in all @@ -867,7 +867,7 @@ new Schema({ /* ... */ }, { shardKey: { tag: 1, name: 1 } }); *Note that Mongoose does not send the `shardcollection` command for you. You must configure your shards yourself.* -

    option: strict

    +## option: strict {#strict} The strict option, (enabled by default), ensures that values passed to our model constructor that were not specified in our schema do not get saved to @@ -918,7 +918,7 @@ thing.iAmNotInTheSchema = true; thing.save(); // iAmNotInTheSchema is never saved to the db ``` -

    option: strictQuery

    +## option: strictQuery {#strictQuery} Mongoose supports a separate `strictQuery` option to avoid strict mode for query filters. This is because empty query filters cause Mongoose to return all documents in the model, which can cause issues. @@ -970,7 +970,7 @@ However, you can override this behavior globally: mongoose.set('strictQuery', true); ``` -

    option: toJSON

    +## option: toJSON {#toJSON} Exactly the same as the [toObject](#toObject) option but only applies when the document's [`toJSON` method](https://thecodebarbarian.com/what-is-the-tojson-function-in-javascript.html) is called. @@ -991,7 +991,7 @@ console.log(JSON.stringify(m)); // { "_id": "504e0cd7dd992d9be2f20b6f", "name": To see all available `toJSON/toObject` options, read [this](api/document.html#document_Document-toObject). -

    option: toObject

    +## option: toObject {#toObject} Documents have a [toObject](api/document.html#document_Document-toObject) method which converts the mongoose document into a plain JavaScript object. This @@ -1015,7 +1015,7 @@ console.log(m); // { _id: 504e0cd7dd992d9be2f20b6f, name: 'Max Headroom is my na To see all available `toObject` options, read [this](api/document.html#document_Document-toObject). -

    option: typeKey

    +## option: typeKey {#typeKey} By default, if you have an object with key 'type' in your schema, mongoose will interpret it as a type declaration. @@ -1038,7 +1038,7 @@ const schema = new Schema({ }, { typeKey: '$type' }); // A '$type' key means this object is a type declaration ``` -

    option: validateBeforeSave

    +## option: validateBeforeSave {#validateBeforeSave} By default, documents are automatically validated before they are saved to the database. This is to prevent saving an invalid document. If you want to @@ -1059,7 +1059,7 @@ m.validate(function(err) { m.save(); // Succeeds despite being invalid ``` -

    option: versionKey

    +## option: versionKey {#versionKey} The `versionKey` is a property set on each document when first created by Mongoose. This keys value contains the internal @@ -1137,7 +1137,7 @@ schema.pre('findOneAndUpdate', function() { }); ``` -

    option: optimisticConcurrency

    +## option: optimisticConcurrency {#optimisticConcurrency} [Optimistic concurrency](https://en.wikipedia.org/wiki/Optimistic_concurrency_control) is a strategy to ensure the document you're updating didn't change between when you loaded it using `find()` or `findOne()`, and when @@ -1201,7 +1201,7 @@ house.status = 'APPROVED'; await house.save(); ``` -

    option: collation

    +## option: collation {#collation} Sets a default [collation](https://www.mongodb.com/docs/manual/reference/collation/) for every query and aggregation. [Here's a beginner-friendly overview of collations](http://thecodebarbarian.com/a-nodejs-perspective-on-mongodb-34-collations). @@ -1223,7 +1223,7 @@ MyModel.create([{ name: 'val' }, { name: 'Val' }]). }); ``` -

    option: timeseries

    +## option: timeseries {#timeseries} If you set the `timeseries` option on a schema, Mongoose will create a [timeseries collection](https://www.mongodb.com/docs/manual/core/timeseries-collections/) for any model that you create from that schema. @@ -1242,7 +1242,7 @@ const schema = Schema({ name: String, timestamp: Date, metadata: Object }, { const Test = db.model('Test', schema); ``` -

    option: skipVersioning

    +## option: skipVersioning {#skipVersioning} `skipVersioning` allows excluding paths from versioning (i.e., the internal revision will not be incremented even if these paths are updated). DO NOT @@ -1255,7 +1255,7 @@ thing.dontVersionMe.push('hey'); thing.save(); // version is not incremented ``` -

    option: timestamps

    +## option: timestamps {#timestamps} The `timestamps` option tells Mongoose to assign `createdAt` and `updatedAt` fields to your schema. The type assigned is [Date](schematypes.html#dates). @@ -1325,7 +1325,7 @@ const schema = Schema({ }); ``` -

    option: pluginTags

    +## option: pluginTags {#pluginTags} Mongoose supports defining global plugins, plugins that apply to all schemas. @@ -1358,11 +1358,7 @@ mongoose.plugin(function myPlugin(schema) { }, { tags: ['useMetaPlugin'] }); ``` -

    - - option: selectPopulatedPaths - -

    +## option: selectPopulatedPaths {#selectPopulatedPaths} By default, Mongoose will automatically `select()` any populated paths for you, unless you explicitly exclude them. @@ -1396,11 +1392,7 @@ const Book = mongoose.model('Book', bookSchema); const doc = await Book.findOne().select('title').populate('author'); ``` -

    - - option: storeSubdocValidationError - -

    +## option: storeSubdocValidationError {#storeSubdocValidationError} For legacy reasons, when there is a validation error in subpath of a single nested schema, Mongoose will record that there was a validation error @@ -1431,11 +1423,7 @@ const Parent = mongoose.model('Parent', parentSchema); new Parent({ child: {} }).validateSync().errors; ``` -

    - - option: collectionOptions - -

    +## option: collectionOptions {#collectionOptions} Options like [`collation`](#collation) and [`capped`](#capped) affect the options Mongoose passes to MongoDB when creating a new collection. Mongoose schemas support most [MongoDB `createCollection()` options](https://www.mongodb.com/docs/manual/reference/method/db.createCollection/), but not all. @@ -1455,11 +1443,7 @@ const Test = mongoose.model('Test', schema); await Test.createCollection(); ``` -

    - - option: autoSearchIndex - -

    +## option: autoSearchIndex {#autoSearchIndex} Similar to [`autoIndex`](#autoIndex), except for automatically creates any [Atlas search indexes](https://www.mongodb.com/docs/atlas/atlas-search/create-index/) defined in your schema. Unlike `autoIndex`, this option defaults to false. @@ -1474,11 +1458,7 @@ schema.searchIndex({ const Test = mongoose.model('Test', schema); ``` -

    - - option: readConcern - -

    +## option: readConcern {#readConcern} [Read concerns](https://www.mongodb.com/docs/manual/reference/read-concern/) are similar to [`writeConcern`](#writeConcern), but for read operations like `find()` and `findOne()`. To set a default `readConcern`, pass the `readConcern` option to the schema constructor as follows. @@ -1492,7 +1472,7 @@ const eventSchema = new mongoose.Schema( ); ``` -

    With ES6 Classes

    +## With ES6 Classes {#es6-classes} Schemas have a [`loadClass()` method](api/schema.html#schema_Schema-loadClass) that you can use to create a Mongoose schema from an [ES6 class](https://thecodebarbarian.com/an-overview-of-es6-classes): @@ -1518,12 +1498,12 @@ console.log(schema.statics); // { myStatic: [Function: myStatic] } console.log(schema.virtuals); // { myVirtual: VirtualType { ... } } ``` -

    Pluggable

    +## Pluggable {#plugins} Schemas are also [pluggable](plugins.html) which allows us to package up reusable features into plugins that can be shared with the community or just between your projects. -

    Further Reading

    +## Further Reading {#further-reading} Here's an [alternative introduction to Mongoose schemas](https://masteringjs.io/tutorials/mongoose/schema). @@ -1540,6 +1520,6 @@ by Christian Kvalheim, the original author of the [MongoDB Node.js driver](http: This book shows you how to implement performant schemas for a laundry list of use cases, including e-commerce, wikis, and appointment bookings. -

    Next Up

    +## Next Up {#next} -Now that we've covered `Schemas`, let's take a look at [SchemaTypes](schematypes.html). +Now that we've covered `Schemas`, let's take a look at [SchemaTypes](schematypes.html). \ No newline at end of file diff --git a/docs/jest.md b/docs/jest.md index 9b492f5b76c..5c1e4e47b10 100644 --- a/docs/jest.md +++ b/docs/jest.md @@ -12,7 +12,7 @@ env SUPPRESS_JEST_WARNINGS=1 npm test If you choose to delve into dangerous waters and test Mongoose apps with Jest, here's what you need to know: - +## Recommended `testEnvironment` {#recommended-testenvironment} If you are using Jest `<=26`, do **not** use Jest's default [`jsdom` test environment](https://jestjs.io/docs/en/configuration.html#testenvironment-string) when testing Mongoose apps, *unless* you are explicitly testing an application that only uses [Mongoose's browser library](browser.html). In Jest `>=27`, ["node" is Jest's default `testEnvironment`](https://jestjs.io/ro/blog/2021/05/25/jest-27#flipping-defaults), so this is no longer an issue. @@ -32,7 +32,7 @@ module.exports = { }; ``` -

    Timer Mocks

    +## Timer Mocks {#timer-mocks} Absolutely do **not** use [timer mocks](https://jestjs.io/docs/en/timer-mocks.html) when testing Mongoose apps. This is especially important if you're using Jest `>=25`, which stubs out `process.nextTick()`. @@ -71,7 +71,7 @@ const sinon = require('sinon'); sinon.stub(time, 'setTimeout'); ``` -

    globalSetup and globalTeardown

    +## globalSetup and globalTeardown {#globalsetup-and-globalteardown} Do **not** use `globalSetup` to call `mongoose.connect()` or `mongoose.createConnection()`. Jest runs `globalSetup` in @@ -87,4 +87,4 @@ course on Pluralsight has a great section on testing Mongoose apps with [Mocha]( RESTful Web Services with Node.js and Express - + \ No newline at end of file diff --git a/docs/middleware.md b/docs/middleware.md index 03a40983a7a..e8d67a8a2a6 100644 --- a/docs/middleware.md +++ b/docs/middleware.md @@ -129,7 +129,7 @@ childSchema.pre('findOneAndUpdate', function() { }); ``` -

    Pre

    +## Pre {#pre} Pre middleware functions are executed one after another, when each middleware calls `next`. @@ -228,7 +228,7 @@ myDoc.save(function(err) { Calling `next()` multiple times is a no-op. If you call `next()` with an error `err1` and then throw an error `err2`, mongoose will report `err1`. -

    Post middleware

    +## Post middleware {#post} [post](api.html#schema_Schema-post) middleware are executed *after* the hooked method and all of its `pre` middleware have completed. @@ -248,7 +248,7 @@ schema.post('deleteOne', function(doc) { }); ``` -

    Asynchronous Post Hooks

    +## Asynchronous Post Hooks {#post-async} If your post hook function takes at least 2 parameters, mongoose will assume the second parameter is a `next()` function that you will call to trigger the next middleware in the sequence. @@ -288,7 +288,7 @@ schema.post('save', async function(doc, next) { }); ``` -

    Define Middleware Before Compiling Models

    +## Define Middleware Before Compiling Models {#defining} Calling `pre()` or `post()` after [compiling a model](models.html#compiling) does **not** work in Mongoose in general. For example, the below `pre('save')` @@ -338,7 +338,7 @@ const schema = new mongoose.Schema({ name: String }); module.exports = mongoose.model('User', schema); ``` -

    Save/Validate Hooks

    +## Save/Validate Hooks {#order} The `save()` function triggers `validate()` hooks, because mongoose has a built-in `pre('save')` hook that calls `validate()`. This means @@ -360,7 +360,7 @@ schema.post('save', function() { }); ``` -

    Accessing Parameters in Middleware

    +## Accessing Parameters in Middleware {#accessing-parameters-in-middleware} Mongoose provides 2 ways to get information about the function call that triggered the middleware. For query middleware, we recommend using `this`, which will be a [Mongoose Query instance](api/query.html). @@ -393,7 +393,7 @@ const doc = new User({ name: 'John', age: 30 }); await doc.save({ validateModifiedOnly: true }); ``` -

    Naming Conflicts

    +## Naming Conflicts {#naming} Mongoose has both query and document hooks for `deleteOne()`. @@ -447,7 +447,7 @@ await doc.validate(); await Test.find().validate(); ``` -

    Notes on findAndUpdate() and Query Middleware

    +## Notes on findAndUpdate() and Query Middleware {#notes} Pre and post `save()` hooks are **not** executed on `update()`, `findOneAndUpdate()`, etc. You can see a more detailed discussion why in @@ -514,7 +514,7 @@ await doc.updateOne({ $set: { name: 'test' } }); // Prints "Updating" await Model.updateOne({}, { $set: { name: 'test' } }); ``` -

    Error Handling Middleware

    +## Error Handling Middleware {#error-handling-middleware} Middleware execution normally stops the first time a piece of middleware calls `next()` with an error. However, there is a special kind of post @@ -577,7 +577,7 @@ Error handling middleware can transform an error, but it can't remove the error. Even if you call `next()` with no error as shown above, the function call will still error out. -

    Aggregation Hooks

    +## Aggregation Hooks {#aggregate} You can also define hooks for the [`Model.aggregate()` function](api/model.html#model_Model-aggregate). In aggregation middleware functions, `this` refers to the [Mongoose `Aggregate` object](api/aggregate.html#Aggregate). @@ -599,7 +599,7 @@ lets you access the MongoDB aggregation pipeline that Mongoose will send to the MongoDB server. It is useful for adding stages to the beginning of the pipeline from middleware. -

    Synchronous Hooks

    +## Synchronous Hooks {#synchronous} Certain Mongoose hooks are synchronous, which means they do **not** support functions that return promises or receive a `next()` callback. Currently, @@ -618,7 +618,7 @@ rejections. [require:post init hooks.*error] ``` -

    Next Up

    +## Next Up {#next} Now that we've covered middleware, let's take a look at Mongoose's approach -to faking JOINs with its query [population](populate.html) helper. +to faking JOINs with its query [population](populate.html) helper. \ No newline at end of file diff --git a/docs/migrating_to_5.md b/docs/migrating_to_5.md index 2f892993ff8..44ff6f56014 100644 --- a/docs/migrating_to_5.md +++ b/docs/migrating_to_5.md @@ -41,13 +41,13 @@ If you're still on Mongoose 3.x, please read the [Mongoose 3.x to 4.x migration * [`bulkWrite()` results](#bulkwrite-results) * [Strict SSL validation](#strict-ssl-validation) -

    Version Requirements

    +## Version Requirements {#version-requirements} Mongoose now requires Node.js >= 4.0.0 and MongoDB >= 3.0.0. [MongoDB 2.6](https://www.mongodb.com/blog/post/mongodb-2-6-end-of-life) and [Node.js < 4](https://github.com/nodejs/Release) where both EOL-ed in 2016. -

    Query Middleware

    +## Query Middleware {#query-middleware} Query middleware is now compiled when you call `mongoose.model()` or `db.model()`. If you add query middleware after calling `mongoose.model()`, that middleware will **not** get called. @@ -63,9 +63,7 @@ MyModel.find().exec(function() { }); ``` -

    - Promises and Callbacks for mongoose.connect() -

    +## Promises and Callbacks for `mongoose.connect()` {#promises-and-callbacks} `mongoose.connect()` and `mongoose.disconnect()` now return a promise if no callback specified, or `null` otherwise. It does **not** return the mongoose singleton. @@ -80,9 +78,7 @@ mongoose.connect('mongodb://127.0.0.1:27017/test'); mongoose.model('Test', new Schema({})); ``` -

    - Connection Logic and useMongoClient -

    +## Connection Logic and `useMongoClient` {#connection-logic} The [`useMongoClient` option](/docs/4.x/docs/connections.html#use-mongo-client) was removed in Mongoose 5, it is now always `true`. As a consequence, Mongoose 5 @@ -97,9 +93,7 @@ examples of `mongoose.connect()` calls that do **not** work in Mongoose 5.x. In Mongoose 5.x, the first parameter to `mongoose.connect()` and `mongoose.createConnection()`, if specified, **must** be a [MongoDB connection string](https://www.mongodb.com/docs/manual/reference/connection-string/). The connection string and options are then passed down to [the MongoDB Node.js driver's `MongoClient.connect()` function](http://mongodb.github.io/node-mongodb-native/3.0/api/MongoClient.html#.connect). Mongoose does not modify the connection string, although `mongoose.connect()` and `mongoose.createConnection()` support a [few additional options in addition to the ones the MongoDB driver supports](http://mongoosejs.com/docs/connections.html#options). -

    - Setter Order -

    +## Setter Order {#setter-order} Setters run in reverse order in 4.x: @@ -119,9 +113,7 @@ schema.path('name'). set(() => console.log('This will print 2nd')); ``` -

    - Checking if a path is populated -

    +## Checking if a path is populated {#id-getter} Mongoose 5.1.0 introduced an `_id` getter to ObjectIds that lets you get an ObjectId regardless of whether a path is populated. @@ -151,9 +143,7 @@ As a consequence, checking whether `blogPost.author._id` is [no longer viable as Note that you can call `mongoose.set('objectIdGetter', false)` to change this behavior. -

    - Return Values for remove() and deleteX() -

    +## Return Values for `remove()` and `deleteX()` {#return-value-for-delete} `deleteOne()`, `deleteMany()`, and `remove()` now resolve to the result object rather than the full [driver `WriteOpResult` object](http://mongodb.github.io/node-mongodb-native/2.2/api/Collection.html#~writeOpCallback). @@ -165,9 +155,7 @@ MyModel.deleteMany().then(res => console.log(res.result.n)); MyModel.deleteMany().then(res => res.n); ``` -

    - Aggregation Cursors -

    +## Aggregation Cursors {#aggregation-cursors} The `useMongooseAggCursor` option from 4.x is now always on. This is the new syntax for aggregation cursors in mongoose 5: @@ -185,23 +173,17 @@ const cursorWithOptions = MyModel. exec(); ``` -

    - geoNear -

    +## geoNear {#geonear} `Model.geoNear()` has been removed because the [MongoDB driver no longer supports it](https://github.com/mongodb/node-mongodb-native/blob/4bac63ce7b9e9fff87c31c5a27d78bcdaca12669/etc/notes/CHANGES_3.0.0.md#geonear-command-helper) -

    - Required URI encoding of connection strings -

    +## Required URI encoding of connection strings {#uri-encoding} Due to changes in the MongoDB driver, connection strings must be URI encoded. If they are not, connections may fail with an illegal character message. -

    - Passwords which contain certain characters -

    +## Passwords which contain certain characters {#password-characters} See a [full list of affected characters](https://developer.mozilla.org/en-US/docs/Glossary/percent-encoding). @@ -230,9 +212,7 @@ mongoose.connect(encodeMongoURI(mongodbConnectString)); The function above is safe to use whether the existing string is already encoded or not. -

    - Domain sockets -

    +## Domain sockets {#domain-sockets} Domain sockets must be URI encoded. For example: @@ -247,9 +227,7 @@ const host = encodeURIComponent('/tmp/mongodb-27017.sock'); mongoose.createConnection(`mongodb://aaron:psw@${host}/fake`); ``` -

    - toObject() Options -

    +## `toObject()` Options {#toobject-options} The `options` parameter to `toObject()` and `toJSON()` merge defaults rather than overwriting them. @@ -267,9 +245,7 @@ const doc = new MyModel({ name: 'test' }); console.log(doc.toJSON({ minimize: false }).answer); ``` -

    - Aggregate Parameters -

    +## Aggregate Parameters {#aggregate-parameters} `aggregate()` no longer accepts a spread, you **must** pass your aggregation pipeline as an array. The below code worked in 4.x: @@ -283,9 +259,7 @@ The above code does **not** work in 5.x, you **must** wrap the `$match` and `$sk MyModel.aggregate([{ $match: { isDeleted: false } }, { $skip: 10 }]).exec(cb); ``` -

    - Boolean Casting -

    +## Boolean Casting {#boolean-casting} By default, mongoose 4 would coerce any value to a boolean without error. @@ -316,9 +290,7 @@ And the following values to `false`: All other values will cause a `CastError` -

    - Query Casting -

    +## Query Casting {#query-casting} Casting for `update()`, `updateOne()`, `updateMany()`, `replaceOne()`, `remove()`, `deleteOne()`, and `deleteMany()` doesn't happen until `exec()`. @@ -334,9 +306,7 @@ query helpers have ran. It also makes it possible to set the `overwrite` option User.where({ name: 'Bar' }).update({ name: 'Baz' }).setOptions({ overwrite: true }); ``` -

    - Post Save Hooks Get Flow Control -

    +## Post Save Hooks Get Flow Control {#post-save-flow-control} Post hooks now get flow control, which means async post save hooks and child document post save hooks execute **before** your `save()` callback. @@ -363,21 +333,15 @@ m.save(function() { }); ``` -

    - The $pushAll Operator -

    +## The `$pushAll` Operator {#pushall} `$pushAll` is no longer supported and no longer used internally for `save()`, since it has been [deprecated since MongoDB 2.4](https://www.mongodb.com/docs/manual/reference/operator/update/pushAll/). Use `$push` with `$each` instead. -

    - Always Use Forward Key Order -

    +## Always Use Forward Key Order {#retain-key-order} The `retainKeyOrder` option was removed, mongoose will now always retain the same key position when cloning objects. If you have queries or indexes that rely on reverse key order, you will have to change them. -

    - Run setters on queries -

    +## Run setters on queries {#run-setters-on-queries} Setters now run on queries by default, and the old `runSettersOnQuery` option has been removed. @@ -390,69 +354,49 @@ const Model = mongoose.model('Test', schema); Model.find({ email: 'FOO@BAR.BAZ' }); // Converted to `find({ email: 'foo@bar.baz' })` ``` -

    - Pre-compiled Browser Bundle -

    +## Pre-compiled Browser Bundle {#browser-bundle} We no longer have a pre-compiled version of mongoose for the browser. If you want to use mongoose schemas in the browser, you need to build your own bundle with browserify/webpack. -

    - Save Errors -

    +## Save Errors {#save-errors} The `saveErrorIfNotFound` option was removed, mongoose will now always error out from `save()` if the underlying document was not found -

    - Init hook signatures -

    +## Init hook signatures {#init-hooks} `init` hooks are now fully synchronous and do not receive `next()` as a parameter. `Document.prototype.init()` no longer takes a callback as a parameter. It was always synchronous, just had a callback for legacy reasons. -

    - numAffected and save() -

    +## `numAffected` and `save()` {#save-num-affected} `doc.save()` no longer passes `numAffected` as a 3rd param to its callback. -

    - remove() and debouncing -

    +## `remove()` and debouncing {#remove-debounce} `doc.remove()` no longer debounces -

    - getPromiseConstructor() -

    +## `getPromiseConstructor()` {#get-promise-constructor} `getPromiseConstructor()` is gone, just use `mongoose.Promise`. -

    - Passing Parameters from Pre Hooks -

    +## Passing Parameters from Pre Hooks {#pre-hook-params} You cannot pass parameters to the next pre middleware in the chain using `next()` in mongoose 5.x. In mongoose 4, `next('Test')` in pre middleware would call the next middleware with 'Test' as a parameter. Mongoose 5.x has removed support for this. -

    - required validator for arrays -

    +## `required` validator for arrays {#array-required} In mongoose 5 the `required` validator only verifies if the value is an array. That is, it will **not** fail for *empty* arrays as it would in mongoose 4. -

    - debug output defaults to stdout instead of stderr -

    +## debug output defaults to stdout instead of stderr {#debug-output} In mongoose 5 the default debug function uses `console.info()` to display messages instead of `console.error()`. -

    - Overwriting filter properties -

    +## Overwriting filter properties {#overwrite-filter} In Mongoose 4.x, overwriting a filter property that's a primitive with one that is an object would silently fail. For example, the below code would ignore the `where()` and be equivalent to `Sport.find({ name: 'baseball' })` @@ -462,9 +406,7 @@ Sport.find({ name: 'baseball' }).where({ name: { $ne: 'softball' } }); In Mongoose 5.x, the above code will correctly overwrite `'baseball'` with `{ $ne: 'softball' }` -

    - bulkWrite() results -

    +## `bulkWrite()` results {#bulkwrite-results} Mongoose 5.x uses version 3.x of the [MongoDB Node.js driver](http://npmjs.com/package/mongodb). MongoDB driver 3.x changed the format of the result of [`bulkWrite()` calls](api/model.html#model_Model-bulkWrite) so there is no longer a top-level `nInserted`, `nModified`, etc. property. The new result object structure is [described here](http://mongodb.github.io/node-mongodb-native/3.1/api/Collection.html#~BulkWriteOpResult). @@ -536,9 +478,7 @@ BulkWriteResult { n: 1 } ``` -

    - Strict SSL Validation -

    +## Strict SSL Validation {#strict-ssl-validation} The most recent versions of the [MongoDB Node.js driver use strict SSL validation by default](http://mongodb.github.io/node-mongodb-native/3.5/tutorials/connect/tls/), which may lead to errors if you're using [self-signed certificates](https://github.com/Automattic/mongoose/issues/9147). @@ -547,4 +487,4 @@ If this is blocking you from upgrading, you can set the `tlsInsecure` option to ```javascript mongoose.connect(uri, { tlsInsecure: false }); // Opt out of additional SSL validation -``` +``` \ No newline at end of file diff --git a/docs/migrating_to_8.md b/docs/migrating_to_8.md index 55b9d12a4ee..e57eacdbc55 100644 --- a/docs/migrating_to_8.md +++ b/docs/migrating_to_8.md @@ -29,7 +29,7 @@ We also recommend reviewing the [MongoDB Node.js driver's release notes for v6.0 * [Model constructor properties are all optional in TypeScript](#model-constructor-properties-are-all-optional-in-typescript) * [Infer `distinct()` return types from schema](#infer-distinct-return-types-from-schema) -

    Removed rawResult option for findOneAndUpdate()

    +## Removed `rawResult` option for `findOneAndUpdate()` {#removed-rawresult-option-for-findoneandupdate} The `rawResult` option for `findOneAndUpdate()`, `findOneAndReplace()`, and `findOneAndDelete()` has been replaced by the `includeResultMetadata` option. @@ -47,7 +47,7 @@ const res = await Character.findOneAndUpdate(filter, update, { `includeResultMetadata` in Mongoose 8 behaves identically to `rawResult`. -

    Document.prototype.deleteOne now returns a query

    +## Document.prototype.deleteOne now returns a query {#document-prototype-deleteone-now-returns-a-query} In Mongoose 7, `doc.deleteOne()` returned a promise that resolved to `doc`. In Mongoose 8, `doc.deleteOne()` returns a query for easier chaining, as well as consistency with `doc.updateOne()`. @@ -64,7 +64,7 @@ const q = numberOne.deleteOne(); const res = await q; ``` -

    MongoDB Node Driver 6

    +## MongoDB Node Driver 6 {#mongodb-node-driver-6} Mongoose 8 uses [v6.x of the MongoDB Node driver](https://github.com/mongodb/node-mongodb-native/releases/tag/v6.0.0). There's a few noteable changes in MongoDB Node driver v6 that affect Mongoose: @@ -81,22 +81,22 @@ There's a few noteable changes in MongoDB Node driver v6 that affect Mongoose: * `sslValidate` -> `tlsAllowInvalidCertificates` * `tlsCertificateFile` -> `tlsCertificateKeyFile` -

    Removed findOneAndRemove()

    +## Removed `findOneAndRemove()` {#removed-findoneandremove} In Mongoose 7, `findOneAndRemove()` was an alias for `findOneAndDelete()` that Mongoose supported for backwards compatibility. Mongoose 8 no longer supports `findOneAndRemove()`. Use `findOneAndDelete()` instead. -

    Removed count()

    +## Removed `count()` {#removed-count} `Model.count()` and `Query.prototype.count()` were removed in Mongoose 8. Use `Model.countDocuments()` and `Query.prototype.countDocuments()` instead. -

    Removed id Setter

    +## Removed id Setter {#removed-id-setter} 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. -

    null is valid for non-required string enums

    +## null is valid for non-required string enums {#null-is-valid-for-non-required-string-enums} 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`. @@ -115,7 +115,7 @@ const Test = mongoose.model('Test', schema); await Test.create({ status: null }); ``` -

    Apply minimize when save() updates an existing document

    +## Apply minimize when `save()` updates an existing document {#apply-minimize-when-save-updates-an-existing-document} In Mongoose 7, Mongoose would only apply minimize when saving a new document, not when updating an existing document. @@ -144,7 +144,7 @@ let rawDoc = await Test.findById(_id).lean(); rawDoc.nested; // undefined in Mongoose 8, {} in Mongoose 7 ``` -

    Apply base schema paths before discriminator paths

    +## Apply base schema paths before discriminator paths {#apply-base-schema-paths-before-discriminator-paths} 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. @@ -178,7 +178,7 @@ const doc = new D({ name: 'test', otherProp: 'test' }); console.log(doc.toObject({ getters: true })); ``` -

    Removed overwrite option for findOneAndUpdate()

    +## Removed `overwrite` option for `findOneAndUpdate()` {#removed-overwrite-option-for-findoneandupdate} Mongoose 7 and earlier supported an `overwrite` option for `findOneAndUpdate()`, `updateOne()`, and `update()`. Before Mongoose 7, `overwrite` would skip wrapping the `update` parameter in `$set`, which meant that `findOneAndUpdate()` and `update()` would overwrite the matched document. @@ -187,7 +187,7 @@ In Mongoose 7, setting `overwrite` would convert `findOneAndUpdate()` to `findOn In Mongoose 8, the `overwrite` option is no longer supported. If you want to overwrite the entire document, use `findOneAndReplace()` or `replaceOne()`. -

    Changed behavior for findOneAndUpdate() with orFail() and upsert

    +## Changed behavior for `findOneAndUpdate()` with `orFail()` and upsert {#changed-behavior-for-findoneandupdate-with-orfail-and-upsert} 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. @@ -195,7 +195,7 @@ In other words, `findOneAndUpdate().orFail()` always threw an error if no docume 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. -

    create() waits until all saves are done before throwing any error

    +## Create waits until all saves are done before throwing any error {#create-waits-until-all-saves-are-done-before-throwing-any-error} 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. @@ -227,7 +227,7 @@ err; // ValidationError await Test.countDocuments(); ``` -

    Model.validate() returns copy of object

    +## Model.validate() returns copy of object {#model-validate-returns-copy-of-object} In Mongoose 7, `Model.validate()` would potentially modify the passed in object. Mongoose 8 instead copies the passed in object first. @@ -243,7 +243,7 @@ typeof obj.answer; // 'string' in Mongoose 8, 'number' in Mongoose 7 typeof res.answer; // 'number' in both Mongoose 7 and Mongoose 8 ``` -

    Allow null For Optional Fields in TypeScript

    +## Allow `null` For Optional Fields in TypeScript {#allow-null-for-optional-fields-in-typescript} In Mongoose 8, automatically inferred schema types in TypeScript allow `null` for optional fields. In Mongoose 7, optional fields only allowed `undefined`, not `null`. @@ -259,7 +259,7 @@ const doc = new TestModel(); doc.name; ``` -

    Model constructor properties are all optional in TypeScript

    +## Model constructor properties are all optional in TypeScript {#model-constructor-properties-are-all-optional-in-typescript} In Mongoose 8, no properties are required on model constructors by default. @@ -292,7 +292,7 @@ const newDoc2 = new TestModel({ }); ``` -

    Infer distinct() return types from schema

    +## Infer `distinct()` return types from schema {#infer-distinct-return-types-from-schema} ```ts interface User { diff --git a/package.json b/package.json index 013c7325c21..f684e3a0e1f 100644 --- a/package.json +++ b/package.json @@ -53,7 +53,7 @@ "lodash.isequal": "4.5.0", "lodash.isequalwith": "4.4.0", "markdownlint-cli2": "^0.13.0", - "marked": "4.3.0", + "marked": "14.0.0", "mkdirp": "^3.0.1", "mocha": "10.7.0", "moment": "2.30.1", diff --git a/scripts/website.js b/scripts/website.js index 63779a7e7be..53ac80d2673 100644 --- a/scripts/website.js +++ b/scripts/website.js @@ -10,11 +10,16 @@ const pug = require('pug'); const pkg = require('../package.json'); const transform = require('acquit-require'); const childProcess = require("child_process"); -require('./generateSearch'); + // using "__dirname" and ".." to have a consistent CWD, this script should not be runnable, even when not being in the root of the project // also a consistent root path so that it is easy to change later when the script should be moved const cwd = path.resolve(__dirname, '..'); +// support github-like custom header ids +// Example: +// # Some Header {#custom-id} +const CustomIdRefex = /{#([a-z][\w-]*)}(?: *)$/; + const isMain = require.main === module; let jobs = []; @@ -29,28 +34,39 @@ try { require('acquit-ignore')(); -const { marked: markdown } = require('marked'); +const markdown = require('marked'); const highlight = require('highlight.js'); -const { promisify } = require("util"); +const { promisify } = require('util'); + markdown.use({ - heading: function(text, level, raw, slugger) { - const slug = slugger.slug(raw); - return ` - - ${text} - - \n`; - } -}); -markdown.setOptions({ - highlight: function(code, language) { - if (!language) { - language = 'javascript'; - } - if (language === 'no-highlight') { - return code; + renderer: { + heading: function({ tokens, depth }) { + let raw = this.parser.parseInline(tokens); + let slug; + const idMatch = CustomIdRefex.exec(raw); + + // use github-like custom header if available, otherwise fallback to default slugger + if (idMatch) { + slug = idMatch[1]; + raw = raw.replace(CustomIdRefex, ''); + } else { + slug = createSlug(raw.trim()); + } + return ` + + ${raw} + + \n`; + }, + code: function({ text, lang }) { + if (!lang || lang === 'acquit') { + lang = 'javascript'; + } + if (lang === 'no-highlight') { + return text; + } + return `
    ${highlight.highlight(text, { language: lang }).value}
    `; } - return highlight.highlight(code, { language }).value; } }); @@ -590,6 +606,7 @@ if (isMain) { (async function main() { console.log(`Processing ~${files.length} files`); + require('./generateSearch'); await deleteAllHtmlFiles(); await pugifyAllFiles(); await copyAllRequiredFiles(); @@ -600,3 +617,12 @@ if (isMain) { console.log('Done Processing'); })(); } + +// Modified from github-slugger +function createSlug(value) { + if (typeof value !== 'string') { + return ''; + } + value = value.toLowerCase(); + return value.replace(/[^a-z0-9-_\s]/, '').replace(/ /g, '-'); +} \ No newline at end of file From 92cf1785fd7bff54bb8c4beca78b35ae7eda4ab1 Mon Sep 17 00:00:00 2001 From: Valeri Karpov Date: Thu, 15 Aug 2024 19:24:13 -0400 Subject: [PATCH 25/53] markdown lint fix --- docs/guide.md | 12 +++++++++++- docs/jest.md | 2 +- docs/middleware.md | 2 +- docs/migrating_to_5.md | 2 +- 4 files changed, 14 insertions(+), 4 deletions(-) diff --git a/docs/guide.md b/docs/guide.md index 19d94665e7f..4912b23282f 100644 --- a/docs/guide.md +++ b/docs/guide.md @@ -526,6 +526,7 @@ schema.set(option, value); Valid options: + * [autoIndex](#autoIndex) * [autoCreate](#autoCreate) * [bufferCommands](#bufferCommands) @@ -560,6 +561,7 @@ Valid options: * [query](#query-helpers) * [autoSearchIndex](#autoSearchIndex) * [readConcern](#readConcern) + ## option: autoIndex {#autoIndex} @@ -972,8 +974,10 @@ mongoose.set('strictQuery', true); ## option: toJSON {#toJSON} + Exactly the same as the [toObject](#toObject) option but only applies when the document's [`toJSON` method](https://thecodebarbarian.com/what-is-the-tojson-function-in-javascript.html) is called. + ```javascript const schema = new Schema({ name: String }); @@ -1100,7 +1104,9 @@ doc2.set('comments.1.body', 'new comment'); await doc2.save(); ``` + If you need optimistic concurrency support for `save()`, you can set the [`optimisticConcurrency` option](#optimisticConcurrency) + Document versioning can also be disabled by setting the `versionKey` to `false`. @@ -1445,8 +1451,10 @@ await Test.createCollection(); ## option: autoSearchIndex {#autoSearchIndex} + Similar to [`autoIndex`](#autoIndex), except for automatically creates any [Atlas search indexes](https://www.mongodb.com/docs/atlas/atlas-search/create-index/) defined in your schema. Unlike `autoIndex`, this option defaults to false. + ```javascript const schema = new Schema({ name: String }, { autoSearchIndex: true }); @@ -1460,8 +1468,10 @@ const Test = mongoose.model('Test', schema); ## option: readConcern {#readConcern} + [Read concerns](https://www.mongodb.com/docs/manual/reference/read-concern/) are similar to [`writeConcern`](#writeConcern), but for read operations like `find()` and `findOne()`. To set a default `readConcern`, pass the `readConcern` option to the schema constructor as follows. + ```javascript const eventSchema = new mongoose.Schema( @@ -1522,4 +1532,4 @@ of use cases, including e-commerce, wikis, and appointment bookings. ## Next Up {#next} -Now that we've covered `Schemas`, let's take a look at [SchemaTypes](schematypes.html). \ No newline at end of file +Now that we've covered `Schemas`, let's take a look at [SchemaTypes](schematypes.html). diff --git a/docs/jest.md b/docs/jest.md index 5c1e4e47b10..2c9073c1b52 100644 --- a/docs/jest.md +++ b/docs/jest.md @@ -87,4 +87,4 @@ course on Pluralsight has a great section on testing Mongoose apps with [Mocha]( RESTful Web Services with Node.js and Express - \ No newline at end of file + diff --git a/docs/middleware.md b/docs/middleware.md index e8d67a8a2a6..bcf1fbf4c7d 100644 --- a/docs/middleware.md +++ b/docs/middleware.md @@ -621,4 +621,4 @@ rejections. ## Next Up {#next} Now that we've covered middleware, let's take a look at Mongoose's approach -to faking JOINs with its query [population](populate.html) helper. \ No newline at end of file +to faking JOINs with its query [population](populate.html) helper. diff --git a/docs/migrating_to_5.md b/docs/migrating_to_5.md index 44ff6f56014..6a512c6386d 100644 --- a/docs/migrating_to_5.md +++ b/docs/migrating_to_5.md @@ -487,4 +487,4 @@ If this is blocking you from upgrading, you can set the `tlsInsecure` option to ```javascript mongoose.connect(uri, { tlsInsecure: false }); // Opt out of additional SSL validation -``` \ No newline at end of file +``` From 31c2ae4c0ab5e18cd177ae0dcab87cd4e8c80c53 Mon Sep 17 00:00:00 2001 From: Valeri Karpov Date: Fri, 16 Aug 2024 16:42:57 -0400 Subject: [PATCH 26/53] docs(model): add 'throw' as valid strict value for bulkWrite() and add some more clarification on `throwOnValidationError` Fix #14804 --- lib/model.js | 4 ++-- 1 file changed, 2 insertions(+), 2 deletions(-) diff --git a/lib/model.js b/lib/model.js index da841ae28f7..a255003b6a4 100644 --- a/lib/model.js +++ b/lib/model.js @@ -3202,8 +3202,8 @@ function _setIsNew(doc, val) { * @param {Boolean} [options.j=true] If false, disable [journal acknowledgement](https://www.mongodb.com/docs/manual/reference/write-concern/#j-option) * @param {Boolean} [options.skipValidation=false] Set to true to skip Mongoose schema validation on bulk write operations. Mongoose currently runs validation on `insertOne` and `replaceOne` operations by default. * @param {Boolean} [options.bypassDocumentValidation=false] If true, disable [MongoDB server-side schema validation](https://www.mongodb.com/docs/manual/core/schema-validation/) for all writes in this bulk. - * @param {Boolean} [options.throwOnValidationError=false] If true and `ordered: false`, throw an error if one of the operations failed validation, but all valid operations completed successfully. - * @param {Boolean} [options.strict=null] Overwrites the [`strict` option](https://mongoosejs.com/docs/guide.html#strict) on schema. If false, allows filtering and writing fields not defined in the schema for all writes in this bulk. + * @param {Boolean} [options.throwOnValidationError=false] If true and `ordered: false`, throw an error if one of the operations failed validation, but all valid operations completed successfully. Note that Mongoose will still send all valid operations to the MongoDB server. + * @param {Boolean|"throw"} [options.strict=null] Overwrites the [`strict` option](https://mongoosejs.com/docs/guide.html#strict) on schema. If false, allows filtering and writing fields not defined in the schema for all writes in this bulk. * @return {Promise} resolves to a [`BulkWriteOpResult`](https://mongodb.github.io/node-mongodb-native/4.9/classes/BulkWriteResult.html) if the operation succeeds * @api public */ From b446a2e5f7c0c3f9fd877b9630b583146218f2fe Mon Sep 17 00:00:00 2001 From: Valeri Karpov Date: Fri, 16 Aug 2024 17:17:46 -0400 Subject: [PATCH 27/53] docs(mongoose): remove out-of-date callback-based example for `mongoose.connect()` Fix #14810 --- lib/index.js | 9 ++++----- 1 file changed, 4 insertions(+), 5 deletions(-) diff --git a/lib/index.js b/lib/index.js index 5b716293a4c..367006ff282 100644 --- a/lib/index.js +++ b/lib/index.js @@ -330,7 +330,7 @@ Mongoose.prototype.get = Mongoose.prototype.set; * * // initialize now, connect later * db = mongoose.createConnection(); - * db.openUri('127.0.0.1', 'database', port, [opts]); + * await db.openUri('mongodb://127.0.0.1:27017/database'); * * @param {String} uri mongodb URI to connect to * @param {Object} [options] passed down to the [MongoDB driver's `connect()` function](https://mongodb.github.io/node-mongodb-native/4.9/interfaces/MongoClientOptions.html), except for 4 mongoose-specific options explained below. @@ -378,11 +378,10 @@ Mongoose.prototype.createConnection = function(uri, options) { * // with options * mongoose.connect(uri, options); * - * // optional callback that gets fired when initial connection completed + * // Using `await` throws "MongooseServerSelectionError: Server selection timed out after 30000 ms" + * // if Mongoose can't connect. * const uri = 'mongodb://nonexistent.domain:27000'; - * mongoose.connect(uri, function(error) { - * // if error is truthy, the initial connection failed. - * }) + * await mongoose.connect(uri); * * @param {String} uri mongodb URI to connect to * @param {Object} [options] passed down to the [MongoDB driver's `connect()` function](https://mongodb.github.io/node-mongodb-native/4.9/interfaces/MongoClientOptions.html), except for 4 mongoose-specific options explained below. From e31d5980307c9f5f2cd05c6a5792f988ea4ea767 Mon Sep 17 00:00:00 2001 From: Valeri Karpov Date: Mon, 19 Aug 2024 10:58:51 -0400 Subject: [PATCH 28/53] Update scripts/website.js Co-authored-by: hasezoey --- scripts/website.js | 3 ++- 1 file changed, 2 insertions(+), 1 deletion(-) diff --git a/scripts/website.js b/scripts/website.js index 53ac80d2673..e6b1077df1c 100644 --- a/scripts/website.js +++ b/scripts/website.js @@ -15,7 +15,8 @@ const childProcess = require("child_process"); // also a consistent root path so that it is easy to change later when the script should be moved const cwd = path.resolve(__dirname, '..'); -// support github-like custom header ids +// support custom heading ids +// see https://www.markdownguide.org/extended-syntax/#heading-ids // Example: // # Some Header {#custom-id} const CustomIdRefex = /{#([a-z][\w-]*)}(?: *)$/; From 7fb97166e905b8949196db01c62ec54df83fb6d5 Mon Sep 17 00:00:00 2001 From: Valeri Karpov Date: Mon, 19 Aug 2024 10:58:58 -0400 Subject: [PATCH 29/53] Update scripts/website.js Co-authored-by: hasezoey --- scripts/website.js | 2 +- 1 file changed, 1 insertion(+), 1 deletion(-) diff --git a/scripts/website.js b/scripts/website.js index e6b1077df1c..2f4da54448d 100644 --- a/scripts/website.js +++ b/scripts/website.js @@ -46,7 +46,7 @@ markdown.use({ let slug; const idMatch = CustomIdRefex.exec(raw); - // use github-like custom header if available, otherwise fallback to default slugger + // use custom header id if available, otherwise fallback to default slugger if (idMatch) { slug = idMatch[1]; raw = raw.replace(CustomIdRefex, ''); From 5fa88210014ce40870ab4252bad6e6223d46c191 Mon Sep 17 00:00:00 2001 From: Valeri Karpov Date: Mon, 19 Aug 2024 10:59:13 -0400 Subject: [PATCH 30/53] Update docs/browser.md Co-authored-by: hasezoey --- docs/browser.md | 2 +- 1 file changed, 1 insertion(+), 1 deletion(-) diff --git a/docs/browser.md b/docs/browser.md index 75aa21a9ddd..43bc487384a 100644 --- a/docs/browser.md +++ b/docs/browser.md @@ -19,7 +19,7 @@ const mongoose = require('mongoose/browser'); import mongoose from 'mongoose/browser'; ``` -Using the Browser Library {#usage} +## Using the Browser Library {#usage} Mongoose's browser library is very limited. The only use case it supports is validating documents as shown below. From a7b04daca77545518d6d4016b4b832823535be43 Mon Sep 17 00:00:00 2001 From: Valeri Karpov Date: Mon, 19 Aug 2024 10:59:21 -0400 Subject: [PATCH 31/53] Update docs/jest.md Co-authored-by: hasezoey --- docs/jest.md | 2 +- 1 file changed, 1 insertion(+), 1 deletion(-) diff --git a/docs/jest.md b/docs/jest.md index 2c9073c1b52..aa1669ef329 100644 --- a/docs/jest.md +++ b/docs/jest.md @@ -71,7 +71,7 @@ const sinon = require('sinon'); sinon.stub(time, 'setTimeout'); ``` -## globalSetup and globalTeardown {#globalsetup-and-globalteardown} +## `globalSetup` and `globalTeardown` {#globalsetup-and-globalteardown} Do **not** use `globalSetup` to call `mongoose.connect()` or `mongoose.createConnection()`. Jest runs `globalSetup` in From 251a05d85e7603622db6af631d9a4e663bc258b6 Mon Sep 17 00:00:00 2001 From: Valeri Karpov Date: Mon, 19 Aug 2024 10:59:29 -0400 Subject: [PATCH 32/53] Update docs/middleware.md Co-authored-by: hasezoey --- docs/middleware.md | 2 +- 1 file changed, 1 insertion(+), 1 deletion(-) diff --git a/docs/middleware.md b/docs/middleware.md index bcf1fbf4c7d..ca5e2f1f4ed 100644 --- a/docs/middleware.md +++ b/docs/middleware.md @@ -447,7 +447,7 @@ await doc.validate(); await Test.find().validate(); ``` -## Notes on findAndUpdate() and Query Middleware {#notes} +## Notes on `findAndUpdate()` and Query Middleware {#notes} Pre and post `save()` hooks are **not** executed on `update()`, `findOneAndUpdate()`, etc. You can see a more detailed discussion why in From 5aa9cd49c24fe5e5ad7d20952b941b7a0bf1a988 Mon Sep 17 00:00:00 2001 From: Valeri Karpov Date: Mon, 19 Aug 2024 10:59:36 -0400 Subject: [PATCH 33/53] Update docs/migrating_to_5.md Co-authored-by: hasezoey --- docs/migrating_to_5.md | 2 +- 1 file changed, 1 insertion(+), 1 deletion(-) diff --git a/docs/migrating_to_5.md b/docs/migrating_to_5.md index 6a512c6386d..06d60e81fb3 100644 --- a/docs/migrating_to_5.md +++ b/docs/migrating_to_5.md @@ -173,7 +173,7 @@ const cursorWithOptions = MyModel. exec(); ``` -## geoNear {#geonear} +## `geoNear` {#geonear} `Model.geoNear()` has been removed because the [MongoDB driver no longer supports it](https://github.com/mongodb/node-mongodb-native/blob/4bac63ce7b9e9fff87c31c5a27d78bcdaca12669/etc/notes/CHANGES_3.0.0.md#geonear-command-helper) From 472269dd9806417b31c16f225fa543b2c6413b4b Mon Sep 17 00:00:00 2001 From: Valeri Karpov Date: Mon, 19 Aug 2024 10:59:45 -0400 Subject: [PATCH 34/53] Update docs/migrating_to_8.md Co-authored-by: hasezoey --- docs/migrating_to_8.md | 2 +- 1 file changed, 1 insertion(+), 1 deletion(-) diff --git a/docs/migrating_to_8.md b/docs/migrating_to_8.md index e57eacdbc55..e440dfa5222 100644 --- a/docs/migrating_to_8.md +++ b/docs/migrating_to_8.md @@ -47,7 +47,7 @@ const res = await Character.findOneAndUpdate(filter, update, { `includeResultMetadata` in Mongoose 8 behaves identically to `rawResult`. -## Document.prototype.deleteOne now returns a query {#document-prototype-deleteone-now-returns-a-query} +## `Document.prototype.deleteOne` now returns a query {#document-prototype-deleteone-now-returns-a-query} In Mongoose 7, `doc.deleteOne()` returned a promise that resolved to `doc`. In Mongoose 8, `doc.deleteOne()` returns a query for easier chaining, as well as consistency with `doc.updateOne()`. From ce7024fc98ff8cbebdfabbf7e94ff3a12f9b3258 Mon Sep 17 00:00:00 2001 From: Valeri Karpov Date: Mon, 19 Aug 2024 10:59:54 -0400 Subject: [PATCH 35/53] Update docs/migrating_to_8.md Co-authored-by: hasezoey --- docs/migrating_to_8.md | 2 +- 1 file changed, 1 insertion(+), 1 deletion(-) diff --git a/docs/migrating_to_8.md b/docs/migrating_to_8.md index e440dfa5222..0b1d2fd352d 100644 --- a/docs/migrating_to_8.md +++ b/docs/migrating_to_8.md @@ -96,7 +96,7 @@ Use `findOneAndDelete()` instead. 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. -## null is valid for non-required string enums {#null-is-valid-for-non-required-string-enums} +## `null` is valid for non-required string enums {#null-is-valid-for-non-required-string-enums} 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`. From a4603d63daec32e3eff8d3c9aa609b0aa0b08120 Mon Sep 17 00:00:00 2001 From: Valeri Karpov Date: Mon, 19 Aug 2024 10:59:59 -0400 Subject: [PATCH 36/53] Update docs/migrating_to_8.md Co-authored-by: hasezoey --- docs/migrating_to_8.md | 2 +- 1 file changed, 1 insertion(+), 1 deletion(-) diff --git a/docs/migrating_to_8.md b/docs/migrating_to_8.md index 0b1d2fd352d..5914318003e 100644 --- a/docs/migrating_to_8.md +++ b/docs/migrating_to_8.md @@ -227,7 +227,7 @@ err; // ValidationError await Test.countDocuments(); ``` -## Model.validate() returns copy of object {#model-validate-returns-copy-of-object} +## `Model.validate()` returns copy of object {#model-validate-returns-copy-of-object} In Mongoose 7, `Model.validate()` would potentially modify the passed in object. Mongoose 8 instead copies the passed in object first. From eb225075cedd330f2f80fb5bfd3958321bfae8a0 Mon Sep 17 00:00:00 2001 From: Valeri Karpov Date: Mon, 19 Aug 2024 11:00:21 -0400 Subject: [PATCH 37/53] Update scripts/website.js Co-authored-by: hasezoey --- scripts/website.js | 2 +- 1 file changed, 1 insertion(+), 1 deletion(-) diff --git a/scripts/website.js b/scripts/website.js index 2f4da54448d..c5bc2251764 100644 --- a/scripts/website.js +++ b/scripts/website.js @@ -19,7 +19,7 @@ const cwd = path.resolve(__dirname, '..'); // see https://www.markdownguide.org/extended-syntax/#heading-ids // Example: // # Some Header {#custom-id} -const CustomIdRefex = /{#([a-z][\w-]*)}(?: *)$/; +const CustomIdRefex = /{#([a-zA-Z0-9_-]+)}(?: *)$/; const isMain = require.main === module; From f588f223601f6965395585bb224015be9dfad147 Mon Sep 17 00:00:00 2001 From: Valeri Karpov Date: Mon, 19 Aug 2024 12:34:19 -0400 Subject: [PATCH 38/53] migrate remaining docs to new markdown header and link syntax --- docs/migrating_to_6.md | 76 +++++++++++++++++++++--------------------- docs/migrating_to_7.md | 28 ++++++++-------- docs/migration.md | 6 ++-- docs/models.md | 2 +- docs/plugins.md | 8 ++--- docs/populate.md | 46 ++++++++++++------------- docs/queries.md | 16 ++++----- docs/schematypes.md | 54 +++++++++++++++--------------- docs/subdocs.md | 2 +- docs/transactions.md | 12 +++---- 10 files changed, 123 insertions(+), 127 deletions(-) diff --git a/docs/migrating_to_6.md b/docs/migrating_to_6.md index f0b91a5f9b8..6bb5e83bcdb 100644 --- a/docs/migrating_to_6.md +++ b/docs/migrating_to_6.md @@ -54,11 +54,11 @@ If you're still on Mongoose 4.x, please read the [Mongoose 4.x to 5.x migration * [Removed `reconnectTries` and `reconnectInterval` options](#removed-reconnecttries-and-reconnectinterval-options) * [MongoDB Driver's New URL Parser Incompatible with Some npm Packages](#mongodb-drivers-new-url-parser-incompatible-with-some-npm-packages) -

    Version Requirements

    +## Version Requirements {#version-requirements} Mongoose now requires Node.js >= 12.0.0. Mongoose still supports MongoDB server versions back to 3.0.0. -

    MongoDB Driver 4.0

    +## MongoDB Driver 4.0 {#mongodb-driver-40} Mongoose now uses v4.x of the [MongoDB Node driver](https://www.npmjs.com/package/mongodb). See [the MongoDB Node drivers' migration guide](https://github.com/mongodb/node-mongodb-native/blob/4.0/docs/CHANGES_4.0.0.md) for detailed info. @@ -87,7 +87,7 @@ res; res.deletedCount; // Number of documents that were deleted. Replaces `res.n` ``` -

    No More Deprecation Warning Options

    +## No More Deprecation Warning Options {#no-more-deprecation-warning-options} `useNewUrlParser`, `useUnifiedTopology`, `useFindAndModify`, and `useCreateIndex` are no longer supported options. Mongoose 6 always behaves as if `useNewUrlParser`, `useUnifiedTopology`, and `useCreateIndex` are `true`, and `useFindAndModify` is `false`. Please remove these options from your code. @@ -101,7 +101,7 @@ await mongoose.connect('mongodb://127.0.0.1:27017/test', { }); ``` -

    The asPromise() Method for Connections

    +## The `asPromise()` Method for Connections {#the-aspromise-method-for-connections} Mongoose connections are no longer [thenable](https://masteringjs.io/tutorials/fundamentals/thenable). This means that `await mongoose.createConnection(uri)` **no longer waits for Mongoose to connect**. Use `mongoose.createConnection(uri).asPromise()` instead. See [#8810](https://github.com/Automattic/mongoose/issues/8810). @@ -113,11 +113,11 @@ await mongoose.createConnection(uri); await mongoose.createConnection(uri).asPromise(); ``` -

    mongoose.connect() Returns a Promise

    +## `mongoose.connect()` Returns a Promise {#mongoose-connect-returns-a-promise} The `mongoose.connect()` function now always returns a promise, **not** a Mongoose instance. -

    Duplicate Query Execution

    +## Duplicate Query Execution {#duplicate-query-execution} Mongoose no longer allows executing the same query object twice. If you do, you'll get a `Query was already executed` error. Executing the same query instance twice is typically indicative of mixing callbacks and promises, but if you need to execute the same query twice, you can call `Query#clone()` to clone the query and re-execute it. See [gh-7398](https://github.com/Automattic/mongoose/issues/7398) @@ -130,7 +130,7 @@ await q; await q.clone(); // Can `clone()` the query to allow executing the query again ``` -

    Model.exists(...) now returns a lean document instead of boolean

    +## Model.exists(...) now returns a lean document instead of boolean {#model-exists-returns-a-lean-document-instead-of-boolean} ```js // in Mongoose 5.x, `existingUser` used to be a boolean @@ -141,7 +141,7 @@ if (existingUser) { } ``` -

    strictQuery is now equal to strict by default

    +## `strictQuery` is now equal to `strict` by default {#strictquery-is-removed-and-replaced-by-strict} ~Mongoose no longer supports a `strictQuery` option. You must now use `strict`.~ As of Mongoose 6.0.10, we brought back the `strictQuery` option. @@ -167,11 +167,11 @@ You can also disable `strictQuery` globally to override: mongoose.set('strictQuery', false); ``` -

    MongoError is now MongoServerError

    +## MongoError is now MongoServerError {#mongoerror-is-now-mongoservererror} In MongoDB Node.js Driver v4.x, 'MongoError' is now 'MongoServerError'. Please change any code that depends on the hardcoded string 'MongoError'. -

    Clone Discriminator Schemas By Default

    +## Clone Discriminator Schemas By Default {#clone-discriminator-schemas-by-default} Mongoose now clones discriminator schemas by default. This means you need to pass `{ clone: false }` to `discriminator()` if you're using recursive embedded discriminators. @@ -184,7 +184,7 @@ User.discriminator('author', authorSchema.clone()); User.discriminator('author', authorSchema, { clone: false }); ``` -

    Simplified isValidObjectId() and separate isObjectIdOrHexString()

    +## Simplified `isValidObjectId()` and separate `isObjectIdOrHexString()` {#simplified-isvalidobjectid-and-separate-isobjectidorhexstring} In Mongoose 5, `mongoose.isValidObjectId()` returned `false` for values like numbers, which was inconsistent with the MongoDB driver's `ObjectId.isValid()` function. Technically, any JavaScript number can be converted to a MongoDB ObjectId. @@ -209,7 +209,7 @@ mongoose.isObjectIdOrHexString('0123456789ab'); // false mongoose.isObjectIdOrHexString(6); // false ``` -

    Schema Defined Document Key Order

    +## Schema Defined Document Key Order {#schema-defined-document-key-order} Mongoose now saves objects with keys in the order the keys are specified in the schema, not in the user-defined object. So whether `Object.keys(new User({ name: String, email: String }).toObject()` is `['name', 'email']` or `['email', 'name']` depends on the order `name` and `email` are defined in your schema. @@ -233,7 +233,7 @@ const doc = new Test({ assert.deepEqual(Object.keys(doc.toObject().profile.name), ['first', 'last']); ``` -

    sanitizeFilter and trusted()

    +## `sanitizeFilter` and `trusted()` {#sanitizefilter-and-trusted} Mongoose 6 introduces a new `sanitizeFilter` option to globals and queries that defends against [query selector injection attacks](https://thecodebarbarian.com/2014/09/04/defending-against-query-selector-injection-attacks.html). If you enable `sanitizeFilter`, Mongoose will wrap any object in the query filter in a `$eq`: @@ -250,7 +250,7 @@ To explicitly allow a query selector, use `mongoose.trusted()`: await Test.find({ username: 'val', pwd: mongoose.trusted({ $ne: null }) }).setOptions({ sanitizeFilter: true }); ``` -

    Removed omitUndefined: Mongoose now removes undefined keys in updates instead of setting them to null

    +## Removed `omitUndefined`: Mongoose now removes `undefined` keys in updates instead of setting them to `null` {#removed-omitundefined} In Mongoose 5.x, setting a key to `undefined` in an update operation was equivalent to setting it to `null`. @@ -279,7 +279,7 @@ The only workaround is to explicitly set properties to `null` in your updates: const res = await Test.findOneAndUpdate({}, { $set: { name: null } }, { new: true }); ``` -

    Document Parameter to Default Functions

    +## Document Parameter to Default Functions {#document-parameter-to-default-functions} Mongoose now passes the document as the first parameter to `default` functions, which is helpful for using [arrow functions](https://masteringjs.io/tutorials/fundamentals/arrow) with defaults. @@ -297,7 +297,7 @@ const schema = new Schema({ }); ``` -

    Arrays are Proxies

    +## Arrays are Proxies {#arrays-are-proxies} Mongoose arrays are now ES6 proxies. You no longer need to `markModified()` after setting an array index directly. @@ -308,7 +308,7 @@ post.tags[0] = 'javascript'; await post.save(); // Works, no need for `markModified()`! ``` -

    typePojoToMixed

    +## `typePojoToMixed` {#typepojotomixed} Schema paths declared with `type: { name: String }` become single nested subdocs in Mongoose 6, as opposed to Mixed in Mongoose 5. This removes the need for the `typePojoToMixed` option. See [gh-7181](https://github.com/Automattic/mongoose/issues/7181). @@ -320,11 +320,11 @@ const schema = new Schema({ }); ``` -

    strictPopulate()

    +## `strictPopulate()` {#strictpopulate} Mongoose now throws an error if you `populate()` a path that isn't defined in your schema. This is only for cases when we can infer the local schema, like when you use `Query#populate()`, **not** when you call `Model.populate()` on a POJO. See [gh-5124](https://github.com/Automattic/mongoose/issues/5124). -

    Subdocument ref Function Context

    +## Subdocument `ref` Function Context {#subdocument-ref-function-context} When populating a subdocument with a function `ref` or `refPath`, `this` is now the subdocument being populated, not the top-level document. See [#8469](https://github.com/Automattic/mongoose/issues/8469). @@ -344,37 +344,37 @@ const schema = new Schema({ }); ``` -

    Schema Reserved Names Warning

    +## Schema Reserved Names Warning {#schema-reserved-names-warning} Using `save`, `isNew`, and other Mongoose reserved names as schema path names now triggers a warning, not an error. You can suppress the warning by setting the `suppressReservedKeysWarning` in your schema options: `new Schema({ save: String }, { suppressReservedKeysWarning: true })`. Keep in mind that this may break plugins that rely on these reserved names. -

    Subdocument Paths

    +## Subdocument Paths {#subdocument-paths} Single nested subdocs have been renamed to "subdocument paths". So `SchemaSingleNestedOptions` is now `SchemaSubdocumentOptions` and `mongoose.Schema.Types.Embedded` is now `mongoose.Schema.Types.Subdocument`. See [gh-10419](https://github.com/Automattic/mongoose/issues/10419) -

    Creating Aggregation Cursors

    +## Creating Aggregation Cursors {#creating-aggregation-cursors} `Aggregate#cursor()` now returns an AggregationCursor instance to be consistent with `Query#cursor()`. You no longer need to do `Model.aggregate(pipeline).cursor().exec()` to get an aggregation cursor, just `Model.aggregate(pipeline).cursor()`. -

    autoCreate Defaults to true

    +## `autoCreate` Defaults to `true` {#autocreate-defaults-to-true} `autoCreate` is `true` by default **unless** readPreference is secondary or secondaryPreferred, which means Mongoose will attempt to create every model's underlying collection before creating indexes. If readPreference is secondary or secondaryPreferred, Mongoose will default to `false` for both `autoCreate` and `autoIndex` because both `createCollection()` and `createIndex()` will fail when connected to a secondary. -

    No More context: 'query'

    +## No More `context: 'query'` {#no-more-context-query} The `context` option for queries has been removed. Now Mongoose always uses `context = 'query'`. -

    Custom Validators with Populated Paths

    +## Custom Validators with Populated Paths {#custom-validators-with-populated-paths} Mongoose 6 always calls validators with depopulated paths (that is, with the id rather than the document itself). In Mongoose 5, Mongoose would call validators with the populated doc if the path was populated. See [#8042](https://github.com/Automattic/mongoose/issues/8042) -

    Disconnected Event with Replica Sets

    +## Disconnected Event with Replica Sets {#disconnected-event-with-replica-sets} When connected to a replica set, connections now emit 'disconnected' when connection to the primary is lost. In Mongoose 5, connections only emitted 'disconnected' when losing connection to all members of the replica set. However, Mongoose 6 does **not** buffer commands while a connection is disconnected. So you can still successfully execute commands like queries with `readPreference = 'secondary'`, even if the Mongoose connection is in the disconnected state. -

    Removed execPopulate()

    +## Removed `execPopulate()` {#removed-execpopulate} `Document#populate()` now returns a promise and is now no longer chainable. @@ -385,15 +385,15 @@ However, Mongoose 6 does **not** buffer commands while a connection is disconnec await doc.populate([{path: 'path1', select: 'select1'}, {path: 'path2', select: 'select2'}]); ``` -

    create() with Empty Array

    +## `create()` with Empty Array {#create-with-empty-array} `await Model.create([])` in v6.0 returns an empty array when provided an empty array, in v5.0 it used to return `undefined`. If any of your code is checking whether the output is `undefined` or not, you need to modify it with the assumption that `await Model.create(...)` will always return an array if provided an array. -

    Removed Nested Path Merging

    +## Removed Nested Path Merging {#removed-nested-path-merging} `doc.set({ child: { age: 21 } })` now works the same whether `child` is a nested path or a subdocument: Mongoose will overwrite the value of `child`. In Mongoose 5, this operation would merge `child` if `child` was a nested path. -

    ObjectId valueOf()

    +## ObjectId `valueOf()` {#objectid-valueof} Mongoose now adds a `valueOf()` function to ObjectIds. This means you can now use `==` to compare an ObjectId against a string. @@ -403,19 +403,19 @@ const a = ObjectId('6143b55ac9a762738b15d4f0'); a == '6143b55ac9a762738b15d4f0'; // true ``` -

    Immutable createdAt

    +## Immutable `createdAt` {#immutable-createdat} If you set `timestamps: true`, Mongoose will now make the `createdAt` property `immutable`. See [gh-10139](https://github.com/Automattic/mongoose/issues/10139) -

    Removed Validator isAsync

    +## Removed Validator `isAsync` {#removed-validator-isasync} `isAsync` is no longer an option for `validate`. Use an `async function` instead. -

    Removed safe

    +## Removed `safe` {#removed-safe} `safe` is no longer an option for schemas, queries, or `save()`. Use `writeConcern` instead. -

    SchemaType set parameters

    +## SchemaType `set` parameters {#schematype-set-parameters} Mongoose now calls setter functions with `priorValue` as the 2nd parameter, rather than `schemaType` in Mongoose 5. @@ -442,7 +442,7 @@ const user = new User({ name: 'Robert Martin' }); console.log(user.name); // 'robert martin' ``` -

    toObject() and toJSON() Use Nested Schema minimize

    +## `toObject()` and `toJSON()` Use Nested Schema `minimize` {#toobject-and-tojson-use-nested-schema-minimize} This change was technically released with 5.10.5, but [caused issues for users migrating from 5.9.x to 6.x](https://github.com/Automattic/mongoose/issues/10827). In Mongoose `< 5.10.5`, `toObject()` and `toJSON()` would use the top-level schema's `minimize` option by default. @@ -473,7 +473,7 @@ const parent = new Schema({ }, { minimize: false }); ``` -

    No default model for Query.prototype.populate()

    +## No default model for `Query.prototype.populate()` {#no-default-model-for-query-prototype-populate} In Mongoose 5, calling `populate()` on a mixed type or other path with no `ref` would fall back to using the query's model. @@ -498,7 +498,7 @@ In Mongoose 6, populating a path with no `ref`, `refPath`, or `model` is a no-op await Test.findOne().populate('parents'); ``` -

    MongoDB Driver's New URL Parser Incompatible with Some npm Packages

    +## MongoDB Driver's New URL Parser Incompatible with Some npm Packages {#mongodb-drivers-new-url-parser-incompatible-with-some-npm-packages} The MongoDB Node driver version that Mongoose 6 uses relies on a [URL parser module](https://npmjs.com/package/whatwg-url) that has several known compatibility issues with other npm packages. This can lead to errors like `Invalid URL: mongodb+srv://username:password@development.xyz.mongodb.net/abc` if you use one of the incompatible packages. @@ -542,7 +542,7 @@ schema.virtual('myVirtual').get(function() { }); ``` -

    Removed reconnectTries and reconnectInterval options

    +## Removed `reconnectTries` and `reconnectInterval` options {#removed-reconnecttries-and-reconnectinterval-options} The `reconnectTries` and `reconnectInterval` options have been removed since they are no longer necessary. diff --git a/docs/migrating_to_7.md b/docs/migrating_to_7.md index c0500576c28..569684b6ed7 100644 --- a/docs/migrating_to_7.md +++ b/docs/migrating_to_7.md @@ -26,7 +26,7 @@ If you're still on Mongoose 5.x, please read the [Mongoose 5.x to 6.x migration * [Removed `LeanDocument` and support for `extends Document`](#removed-leandocument-and-support-for-extends-document) * [New parameters for `HydratedDocument`](#new-parameters-for-hydrateddocument) -

    strictQuery

    +## `strictQuery` {#strictquery} `strictQuery` is now false by default. @@ -40,7 +40,7 @@ const docs = await MyModel.find({ notInSchema: 1 }); docs; ``` -

    Removed remove()

    +## Removed `remove()` {#removed-remove} The `remove()` method on documents and models has been removed. Use `deleteOne()` or `deleteMany()` instead. @@ -79,7 +79,7 @@ schema.pre('deleteOne', { document: true, query: false }, function() { }); ``` -

    Dropped callback support

    +## Dropped callback support {#dropped-callback-support} The following functions no longer accept callbacks. They always return promises. @@ -167,7 +167,7 @@ const [err, session] = await conn.startSession().then( ); ``` -

    Removed update()

    +## Removed `update()` {#removed-update} `Model.update()`, `Query.prototype.update()`, and `Document.prototype.update()` have been removed. Use `updateOne()` instead. @@ -182,7 +182,7 @@ await Model.updateOne(filter, update); await doc.updateOne(update); ``` -

    ObjectId requires new

    +## ObjectId requires `new` {#objectid-requires-new} In Mongoose 6 and older, you could define a new ObjectId without using the `new` keyword: @@ -199,7 +199,7 @@ In Mongoose 7, `ObjectId` is now a [JavaScript class](https://masteringjs.io/tut const oid = new mongoose.Types.ObjectId('0'.repeat(24)); ``` -

    Discriminator schemas use base schema options by default

    +## Discriminator schemas use base schema options by default {#discriminator-schemas-use-base-schema-options-by-default} When you use `Model.discriminator()`, Mongoose will now use the discriminator base schema's options by default. This means you don't need to explicitly set child schema options to match the base schema's. @@ -217,7 +217,7 @@ const Test = Base.discriminator('Child', childSchema); Test.schema.options.typeKey; // '$type' ``` -

    Removed castForQueryWrapper, updated castForQuery() signature

    +## Removed `castForQueryWrapper`, updated `castForQuery()` signature {#removed-castforquerywrapper} Mongoose now always calls SchemaType `castForQuery()` method with 3 arguments: `$conditional`, `value`, and `context`. If you've implemented a custom schema type that defines its own `castForQuery()` method, you need to update the method as follows. @@ -243,7 +243,7 @@ MySchemaType.prototype.castForQuery = function($conditional, value, context) { }; ``` -

    Copy Schema options in Schema.prototype.add()

    +## Copy Schema options in `Schema.prototype.add()` {#copy-schema-options-in-schema-prototype-add} Mongoose now copies user defined schema options when adding one schema to another. For example, `childSchema` below will get `baseSchema`'s `id` and `toJSON` options. @@ -263,7 +263,7 @@ childSchema.add(new Schema({}, { toObject: { virtuals: true } })); childSchema.options.toObject; // { virtuals: true } in Mongoose 7. undefined in Mongoose 6. ``` -

    ObjectId bsontype now has lowercase d

    +## ObjectId bsontype now has lowercase d {#objectid-bsontype-now-has-lowercase-d} The internal `_bsontype` property on ObjectIds is equal to `'ObjectId'` in Mongoose 7, as opposed to `'ObjectID'` in Mongoose 6. @@ -276,7 +276,7 @@ oid._bsontype; // 'ObjectId' in Mongoose 7, 'ObjectID' in older versions of Mong Please update any places where you use `_bsontype` to check if an object is an ObjectId. This may also affect libraries that use Mongoose. -

    Removed mapReduce

    +## Removed mapReduce {#removed-mapreduce} MongoDB no longer supports `mapReduce`, so Mongoose 7 no longer has a `Model.mapReduce()` function. Use the aggregation framework as a replacement for `mapReduce()`. @@ -295,7 +295,7 @@ const o = { await MR.mapReduce(o); ``` -

    Removed Support for custom promise libraries

    +## Removed Support for custom promise libraries {#removed-support-for-custom-promise-libraries} Mongoose 7 no longer supports plugging in custom promise libraries. So the following no longer makes Mongoose return Bluebird promises in Mongoose 7. @@ -312,9 +312,9 @@ If you want to use Bluebird for all promises globally, you can do the following: global.Promise = require('bluebird'); ``` -

    TypeScript-specific Changes

    +## TypeScript-specific Changes {#typescript-specific-changes} -

    Removed LeanDocument and support for extends Document

    +### Removed `LeanDocument` and support for `extends Document` {#removed-leandocument-and-support-for-extends-document} Mongoose 7 no longer exports a `LeanDocument` type, and no longer supports passing a document type that `extends Document` into `Model<>`. @@ -335,7 +335,7 @@ const Test = model('Test', schema); type TestDocument = ReturnType<(typeof Test)['hydrate']>; ``` -

    New Parameters for HydratedDocument

    +### New Parameters for `HydratedDocument` {#new-parameters-for-hydrateddocument} Mongoose's `HydratedDocument` type transforms a raw document interface into the type of the hydrated Mongoose document, including virtuals, methods, etc. In Mongoose 7, the generic parameters to `HydratedDocument` have changed. diff --git a/docs/migration.md b/docs/migration.md index 1e6b7aae5ad..dc9b112ca8b 100644 --- a/docs/migration.md +++ b/docs/migration.md @@ -2,7 +2,7 @@ There are several [backwards-breaking changes](https://github.com/Automattic/mongoose/wiki/4.0-Release-Notes) to be aware of when migrating from Mongoose 3 to Mongoose 4. -

    `findOneAndUpdate()` new field is now `false` by default

    +## `findOneAndUpdate()` new field is now `false` by default {#findandmodify-new} Mongoose's `findOneAndUpdate()`, `findOneAndRemove()`, `findByIdAndUpdate()`, and `findByIdAndRemove()` functions are just @@ -25,7 +25,7 @@ MyModel.findOneAndUpdate({}, { $set: { test: 1 } }, { new: true }, callback); In Mongoose 3, CastError and ValidationError had a `type` field. For instance, user defined validation errors would have a `type` property that contained the string 'user defined'. In Mongoose 4, this property has been renamed to `kind` due to [the V8 JavaScript engine using the Error.type property internally](https://code.google.com/p/v8/issues/detail?id=2397). -

    Query now has a `.then()` function

    +## Query now has a `.then()` function {#promises} In mongoose 3, you needed to call `.exec()` on a query chain to get a promise back, like `MyModel.find().exec().then();`. Mongoose 4 queries are @@ -34,7 +34,7 @@ you're using functions like [q's `Q.ninvoke()`](https://github.com/kriskowal/q#adapting-node) or otherwise returning a mongoose query from a promise. -

    More Info

    +## More Info {#moreinfo} Related blog posts: diff --git a/docs/models.md b/docs/models.md index 84c53cca0d1..a5c7b3a39dc 100644 --- a/docs/models.md +++ b/docs/models.md @@ -13,7 +13,7 @@ reading documents from the underlying MongoDB database. * [Change Streams](#change-streams) * [Views](#views) -

    Compiling your first model

    +## Compiling your first model {#compiling} When you call `mongoose.model()` on a schema, Mongoose *compiles* a model for you. diff --git a/docs/plugins.md b/docs/plugins.md index 978a11cfcad..1cbfedcd3ba 100644 --- a/docs/plugins.md +++ b/docs/plugins.md @@ -9,7 +9,7 @@ Schemas are pluggable, that is, they allow for applying pre-packaged capabilitie
  • Officially Supported Plugins
  • -

    Example

    +## Example {#example} Plugins are a tool for reusing logic in multiple schemas. Suppose you have several models in your database and want to add a `loadedAt` property @@ -46,7 +46,7 @@ playerSchema.plugin(loadedAtPlugin); We just added loaded-time behavior to both our `Game` and `Player` schemas and declared an index on the `loadedAt` path of our Games to boot. Not bad for a few lines of code. -

    Global Plugins

    +## Global Plugins {#global} Want to register a plugin for all schemas? The mongoose singleton has a `.plugin()` function that registers a plugin for every schema. For @@ -63,7 +63,7 @@ const Game = mongoose.model('Game', gameSchema); const Player = mongoose.model('Player', playerSchema); ``` -

    Apply Plugins Before Compiling Models

    +## Apply Plugins Before Compiling Models {#apply-plugins-before-compiling-models} Because many plugins rely on [middleware](middleware.html), you should make sure to apply plugins **before** you call `mongoose.model()` or `conn.model()`. Otherwise, [any middleware the plugin registers won't get applied](middleware.html#defining). @@ -96,7 +96,7 @@ const Game = mongoose.model('Game', gameSchema); gameSchema.plugin(loadedAtPlugin); ``` -

    Officially Supported Plugins

    +## Officially Supported Plugins {#official} The Mongoose team maintains several plugins that add cool new features to Mongoose. Here's a couple: diff --git a/docs/populate.md b/docs/populate.md index 9bc16f65962..0a24878d68d 100644 --- a/docs/populate.md +++ b/docs/populate.md @@ -57,7 +57,7 @@ the `Story` model.
  • Transform populated documents
  • -

    Saving refs

    +## Saving refs {#saving-refs} Saving refs to other documents works the same way you normally save properties, just assign the `_id` value: @@ -85,7 +85,7 @@ You can set the `ref` option on `ObjectId`, `Number`, `String`, and `Buffer` pat However, we recommend using ObjectIds as `_id` properties (and thus ObjectIds for `ref` properties) unless you have a good reason not to. That is because MongoDB will set `_id` to an ObjectId if you create a new document without an `_id` property, so if you make your `_id` property a Number, you need to be extra careful not to insert a document without a numeric `_id`. -

    Population

    +## Population {#population} So far we haven't done anything much different. We've merely created a `Person` and a `Story`. Now let's take a look at populating our story's @@ -108,7 +108,7 @@ Arrays of refs work the same way. Just call the [populate](api/query.html#query_Query-populate) method on the query and an array of documents will be returned *in place* of the original `_id`s. -

    Setting Populated Fields

    +## Setting Populated Fields {#setting-populated-fields} You can manually populate a property by setting it to a document. The document must be an instance of the model your `ref` property refers to. @@ -119,7 +119,7 @@ story.author = author; console.log(story.author.name); // prints "Ian Fleming" ``` -

    Checking Whether a Field is Populated

    +## Checking Whether a Field is Populated {#checking-populated} You can call the `populated()` function to check whether a field is populated. If `populated()` returns a [truthy value](https://masteringjs.io/tutorials/fundamentals/truthy), @@ -147,7 +147,7 @@ story.author instanceof ObjectId; // true story.author._id; // ObjectId, because Mongoose adds a special getter ``` -

    What If There's No Foreign Document?

    +## What If There's No Foreign Document? {#doc-not-found} Mongoose populate doesn't behave like conventional [SQL joins](https://www.w3schools.com/sql/sql_join.asp). When there's no @@ -176,7 +176,7 @@ const story = await Story.findOne({ title: 'Casino Royale' }).populate('authors' story.authors; // `[]` ``` -

    Field Selection

    +## Field Selection {#field-selection} What if we only want a few specific fields returned for the populated documents? This can be accomplished by passing the usual @@ -194,7 +194,7 @@ console.log('The author is %s', story.author.name); console.log('The authors age is %s', story.author.age); ``` -

    Populating Multiple Paths

    +## Populating Multiple Paths {#populating-multiple-paths} What if we wanted to populate multiple paths at the same time? @@ -220,7 +220,7 @@ await Story. await Story.find().populate({ path: 'fans', select: 'email' }); ``` -

    Query conditions and other options

    +## Query conditions and other options {#query-conditions} What if we wanted to populate our fans array based on their age and select just their names? @@ -264,7 +264,7 @@ story; // null If you want to filter stories by their author's name, you should use [denormalization](https://www.mongodb.com/blog/post/6-rules-of-thumb-for-mongodb-schema-design-part-3). -

    limit vs. perDocumentLimit

    +## `limit` vs. `perDocumentLimit` {#limit-vs-perDocumentLimit} Populate does support a `limit` option, however, it currently does **not** limit on a per-document basis for backwards compatibility. For example, @@ -316,7 +316,7 @@ stories[1].name; // 'Live and Let Die' stories[1].fans.length; // 2 ``` -

    Refs to children

    +## Refs to children {#refs-to-children} We may find however, if we use the `author` object, we are unable to get a list of the stories. This is because no `story` objects were ever 'pushed' @@ -364,7 +364,7 @@ them with [sub docs](subdocs.html). Take caution when calling its remove method because you'll be removing it from the database, not just the array. -

    Populating an existing document

    +## Populating an existing document {#populate_an_existing_mongoose_document} If you have an existing mongoose document and want to populate some of its paths, you can use the @@ -390,7 +390,7 @@ await person.populate(['stories', 'fans']); person.populated('fans'); // Array of ObjectIds ``` -

    Populating multiple existing documents

    +## Populating multiple existing documents {#populate_multiple_documents} If we have one or many mongoose documents or even plain objects (*like [mapReduce](api/model.html#model_Model-mapReduce) output*), we may @@ -398,7 +398,7 @@ populate them using the [Model.populate()](api/model.html#model_Model-populate) method. This is what `Document#populate()` and `Query#populate()` use to populate documents. -

    Populating across multiple levels

    +## Populating across multiple levels {#deep-populate} Say you have a user schema which keeps track of the user's friends. @@ -423,7 +423,7 @@ await User. }); ``` -

    Cross Database Populate

    +## Cross Database Populate {#cross-db-populate} Let's say you have a schema representing events, and a schema representing conversations. Each event has a corresponding conversation thread. @@ -470,7 +470,7 @@ const events = await Event. populate({ path: 'conversation', model: Conversation }); ``` -

    Dynamic References via refPath

    +## Dynamic References via `refPath` {#dynamic-refpath} Mongoose can also populate from multiple collections based on the value of a property in the document. Let's say you're building a schema for @@ -591,7 +591,7 @@ const commentSchema = new Schema({ }); ``` -

    Dynamic References via ref

    +## Dynamic References via `ref` {#dynamic-ref} Just like `refPath`, `ref` can also be assigned a function. @@ -609,7 +609,7 @@ const commentSchema = new Schema({ }); ``` -

    Populate Virtuals

    +## Populate Virtuals {#populate-virtuals} So far you've only populated based on the `_id` field. However, that's sometimes not the right choice. @@ -706,7 +706,7 @@ authors = await Author. exec(); ``` -

    Populate Virtuals: The Count Option

    +## Populate Virtuals: The Count Option {#count} Populate virtuals also support counting the number of documents with matching `foreignField` as opposed to the documents themselves. Set the @@ -734,7 +734,7 @@ const doc = await Band.findOne({ name: 'Motley Crue' }). doc.numMembers; // 2 ``` -

    Populate Virtuals: The Match Option

    +## Populate Virtuals: The Match Option {#match} Another option for Populate virtuals is `match`. This option adds an extra filter condition to the query Mongoose uses to `populate()`: @@ -796,7 +796,7 @@ await Author.findOne().populate({ }); ``` -

    Populating Maps

    +## Populating Maps {#populating-maps} [Maps](schematypes.html#maps) are a type that represents an object with arbitrary string keys. For example, in the below schema, `members` is a map from strings to ObjectIds. @@ -866,7 +866,7 @@ You can `populate()` every book's author by populating `books.$*.author`: const libraries = await Library.find().populate('books.$*.author'); ``` -

    Populate in Middleware

    +## Populate in Middleware {#populate-middleware} You can populate in either pre or post [hooks](http://mongoosejs.com/docs/middleware.html). If you want to always populate a certain field, check out the [mongoose-autopopulate plugin](http://npmjs.com/package/mongoose-autopopulate). @@ -900,7 +900,7 @@ MySchema.post('save', function(doc, next) { }); ``` -

    Populating Multiple Paths in Middleware

    +## Populating Multiple Paths in Middleware {#populating-multiple-paths-middleware} Populating multiple paths in middleware can be helpful when you always want to populate some fields. But, the implementation is just a tiny bit trickier than what you may think. Here's how you may expect it to work: @@ -936,7 +936,7 @@ userSchema.pre('find', function(next) { Alternatively, you can check out the [mongoose-autopopulate plugin](http://npmjs.com/package/mongoose-autopopulate). -

    Transform populated documents

    +## Transform populated documents {#transform-populated-documents} You can manipulate populated documents using the `transform` option. If you specify a `transform` function, Mongoose will call this function on every populated document in the result with two arguments: the populated document, and the original id used to populate the document. diff --git a/docs/queries.md b/docs/queries.md index 0b00e752cf1..df552c5043f 100644 --- a/docs/queries.md +++ b/docs/queries.md @@ -97,11 +97,7 @@ await Person. A full list of [Query helper functions can be found in the API docs](api/query.html). -

    - - Queries are Not Promises - -

    +## Queries are Not Promises Mongoose queries are **not** promises. Queries are [thenables](https://masteringjs.io/tutorials/fundamentals/thenable), meaning they have a `.then()` method for [async/await](http://thecodebarbarian.com/common-async-await-design-patterns-in-node.js.html) as a convenience. @@ -115,14 +111,14 @@ await q.then(() => console.log('Update 2')); await q.then(() => console.log('Update 3')); ``` -

    References to other documents

    +## References to other documents {#refs} There are no joins in MongoDB but sometimes we still want references to documents in other collections. This is where [population](populate.html) comes in. Read more about how to include documents from other collections in your query results [here](api/query.html#query_Query-populate). -

    Streaming

    +## Streaming {#streaming} You can [stream](http://nodejs.org/api/stream.html) query results from MongoDB. You need to call the @@ -159,7 +155,7 @@ However, cursors can still time out because of [session idle timeouts](https://w So even a cursor with `noCursorTimeout` set will still time out after 30 minutes of inactivity. You can read more about working around session idle timeouts in the [MongoDB documentation](https://www.mongodb.com/docs/manual/reference/method/cursor.noCursorTimeout/#session-idle-timeout-overrides-nocursortimeout). -

    Versus Aggregation

    +## Versus Aggregation {#versus-aggregation} [Aggregation](api/aggregate.html#aggregate_Aggregate) can do many of the same things that queries can. For example, below is @@ -201,7 +197,7 @@ const queryRes = await Person.findOne({ _id: idString }); const aggRes = await Person.aggregate([{ $match: { _id: idString } }]); ``` -

    Sorting

    +## Sorting {#sorting} [Sorting](/docs/api.html#query_Query-sort) is how you can ensure your query results come back in the desired order. @@ -284,6 +280,6 @@ As you can see, age is sorted from 0 to 2 but when age is equal, sorts by weight ]; ``` -

    Next Up

    +## Next Up {#next} Now that we've covered `Queries`, let's take a look at [Validation](validation.html). diff --git a/docs/schematypes.md b/docs/schematypes.md index d4f90a6a014..904bb6a8726 100644 --- a/docs/schematypes.md +++ b/docs/schematypes.md @@ -18,7 +18,7 @@ and other general characteristics for Mongoose document properties. * [The `schema.path()` Function](#path) * [Further Reading](#further-reading) -

    What is a SchemaType?

    +## What is a SchemaType? {#what-is-a-schematype} You can think of a Mongoose schema as the configuration object for a Mongoose model. A SchemaType is then a configuration object for an individual @@ -56,7 +56,7 @@ Check out [Mongoose's plugins search](http://plugins.mongoosejs.io) to find plug * [UUID](#uuid) * [BigInt](#bigint) -

    Example

    +### Example ```javascript const schema = new Schema({ @@ -112,7 +112,7 @@ m.map = new Map([['key', 'value']]); m.save(callback); ``` -

    The type Key

    +## The `type` Key {#type-key} `type` is a special property in Mongoose schemas. When Mongoose finds a nested property named `type` in your schema, Mongoose assumes that @@ -163,7 +163,7 @@ const holdingSchema = new Schema({ }); ``` -

    SchemaType Options

    +## SchemaType Options {#schematype-options} You can declare a schema type using the type directly, or an object with a `type` property. @@ -201,7 +201,7 @@ The `lowercase` option only works for strings. There are certain options which apply for all schema types, and some that apply for specific schema types. -

    All Schema Types

    +### All Schema Types * `required`: boolean or function, if true adds a [required validator](validation.html#built-in-validators) for this property * `default`: Any or function, sets a default value for the path. If the value is a function, the return value of the function is used as the default. @@ -234,7 +234,7 @@ doc.integerOnly; // 3 doc.i; // 3 ``` -

    Indexes

    +### Indexes You can also define [MongoDB indexes](https://www.mongodb.com/docs/manual/indexes/) using schema type options. @@ -254,7 +254,7 @@ const schema2 = new Schema({ }); ``` -

    String

    +### String {#string-validators} * `lowercase`: boolean, whether to always call `.toLowerCase()` on the value * `uppercase`: boolean, whether to always call `.toUpperCase()` on the value @@ -265,26 +265,26 @@ const schema2 = new Schema({ * `maxLength`: Number, creates a [validator](validation.html) that checks if the value length is not greater than the given number * `populate`: Object, sets default [populate options](populate.html#query-conditions) -

    Number

    +### Number {#number-validators} * `min`: Number, creates a [validator](validation.html) that checks if the value is greater than or equal to the given minimum. * `max`: Number, creates a [validator](validation.html) that checks if the value is less than or equal to the given maximum. * `enum`: Array, creates a [validator](validation.html) that checks if the value is strictly equal to one of the values in the given array. * `populate`: Object, sets default [populate options](populate.html#query-conditions) -

    Date

    +### Date * `min`: Date, creates a [validator](validation.html) that checks if the value is greater than or equal to the given minimum. * `max`: Date, creates a [validator](validation.html) that checks if the value is less than or equal to the given maximum. * `expires`: Number or String, creates a TTL index with the value expressed in seconds. -

    ObjectId

    +### ObjectId * `populate`: Object, sets default [populate options](populate.html#query-conditions) -

    Usage Notes

    +## Usage Notes {#usage-notes} -

    String

    +### String {#strings} To declare a path as a string, you may use either the `String` global constructor or the string `'String'`. @@ -308,7 +308,7 @@ new Person({ name: { toString: () => 42 } }).name; // "42" as a string new Person({ name: { foo: 42 } }).name; ``` -

    Number

    +### Number {#numbers} To declare a path as a number, you may use either the `Number` global constructor or the string `'Number'`. @@ -337,7 +337,7 @@ The values `null` and `undefined` are not cast. NaN, strings that cast to NaN, arrays, and objects that don't have a `valueOf()` function will all result in a [CastError](validation.html#cast-errors) once validated, meaning that it will not throw on initialization, only when validated. -

    Dates

    +### Dates {#dates} [Built-in `Date` methods](https://developer.mozilla.org/en-US/docs/Web/JavaScript/Reference/Global_Objects/Date) are [**not** hooked into](https://github.com/Automattic/mongoose/issues/1598) the mongoose change tracking logic which in English means that if you use a `Date` in your document and modify it with a method like `setMonth()`, mongoose will be unaware of this change and `doc.save()` will not persist this modification. If you must modify `Date` types using built-in methods, tell mongoose about the change with `doc.markModified('pathToYourDate')` before saving. @@ -351,7 +351,7 @@ doc.markModified('dueDate'); await doc.save(); // works ``` -

    Buffer

    +### Buffer {#buffers} To declare a path as a Buffer, you may use either the `Buffer` global constructor or the string `'Buffer'`. @@ -371,7 +371,7 @@ const file2 = new Data({ binData: 72987 }); // {"type":"Buffer","data":[27]} const file4 = new Data({ binData: { type: 'Buffer', data: [1, 2, 3]}}); // {"type":"Buffer","data":[1,2,3]} ``` -

    Mixed

    +### Mixed {#mixed} An "anything goes" SchemaType. Mongoose will not do any casting on mixed paths. You can define a mixed path using `Schema.Types.Mixed` or by passing an empty @@ -398,7 +398,7 @@ person.markModified('anything'); person.save(); // Mongoose will save changes to `anything`. ``` -

    ObjectIds

    +### ObjectIds {#objectids} An [ObjectId](https://www.mongodb.com/docs/manual/reference/method/ObjectId/) is a special type typically used for unique identifiers. Here's how @@ -425,7 +425,7 @@ car.driver instanceof mongoose.Types.ObjectId; // true car.driver.toString(); // Something like "5e1a0651741b255ddda996c4" ``` -

    Boolean

    +### Boolean {#booleans} Booleans in Mongoose are [plain JavaScript booleans](https://www.w3schools.com/js/js_booleans.asp). By default, Mongoose casts the below values to `true`: @@ -459,7 +459,7 @@ mongoose.Schema.Types.Boolean.convertToFalse.add('nay'); console.log(new M({ b: 'nay' }).b); // false ``` -

    Arrays

    +### Arrays {#arrays} Mongoose supports arrays of [SchemaTypes](api/schema.html#schema_Schema-Types) and arrays of [subdocuments](subdocs.html). Arrays of SchemaTypes are @@ -505,7 +505,7 @@ const Empty3 = new Schema({ any: [Schema.Types.Mixed] }); const Empty4 = new Schema({ any: [{}] }); ``` -

    Maps

    +### Maps {#maps} A `MongooseMap` is a subclass of [JavaScript's `Map` class](http://thecodebarbarian.com/the-80-20-guide-to-maps-in-javascript.html). In these docs, we'll use the terms 'map' and `MongooseMap` interchangeably. @@ -591,7 +591,7 @@ on `socialMediaHandles.$*.oauth`: const user = await User.findOne().populate('socialMediaHandles.$*.oauth'); ``` -

    UUID

    +### UUID {#uuid} Mongoose also supports a UUID type that stores UUID instances as [Node.js buffers](https://thecodebarbarian.com/an-overview-of-buffers-in-node-js.html). We recommend using [ObjectIds](#objectids) rather than UUIDs for unique document ids in Mongoose, but you may use UUIDs if you need to. @@ -632,7 +632,7 @@ const schema = new mongoose.Schema({ }); ``` -

    BigInt

    +### BigInt {#bigint} Mongoose supports [JavaScript BigInts](https://thecodebarbarian.com/an-overview-of-bigint-in-node-js.html) as a SchemaType. BigInts are stored as [64-bit integers in MongoDB (BSON type "long")](https://www.mongodb.com/docs/manual/reference/bson-types/). @@ -647,7 +647,7 @@ const question = new Question({ answer: 42n }); typeof question.answer; // 'bigint' ``` -

    Getters

    +## Getters {#getters} Getters are like virtuals for paths defined in your schema. For example, let's say you wanted to store user profile pictures as relative paths and @@ -709,7 +709,7 @@ const root = 'https://s3.amazonaws.com/mybucket'; schema.path('arr.0.url').get(v => `${root}${v}`); ``` -

    Schemas

    +## Schemas {#schemas} To declare a path as another [schema](guide.html#definition), set `type` to the sub-schema's instance. @@ -731,7 +731,7 @@ const schema = new mongoose.Schema({ }); ``` -

    Creating Custom Types

    +## Creating Custom Types {#customtypes} Mongoose can also be extended with [custom SchemaTypes](customschematypes.html). Search the [plugins](http://plugins.mongoosejs.io) @@ -743,7 +743,7 @@ and Read more about creating custom SchemaTypes [here](customschematypes.html). -

    The `schema.path()` Function

    +## The `schema.path()` Function {#path} The `schema.path()` function returns the instantiated schema type for a given path. @@ -765,7 +765,7 @@ console.log(sampleSchema.path('name')); You can use this function to inspect the schema type for a given path, including what validators it has and what the type is. -

    Further Reading

    +## Further Reading {#further-reading}
    • An Introduction to Mongoose SchemaTypes
    • diff --git a/docs/subdocs.md b/docs/subdocs.md index 2c64ba96d98..eeb4c2973b0 100644 --- a/docs/subdocs.md +++ b/docs/subdocs.md @@ -272,7 +272,7 @@ await parent.save(); console.log('the subdocs were removed'); ``` -

      Parents of Subdocs

      +## Parents of Subdocs {#subdoc-parents} Sometimes, you need to get the parent of a subdoc. You can access the parent using the `parent()` function. diff --git a/docs/transactions.md b/docs/transactions.md index bafd16a0164..d69ae7b8efc 100644 --- a/docs/transactions.md +++ b/docs/transactions.md @@ -3,7 +3,7 @@ [Transactions](https://www.mongodb.com/transactions) let you execute multiple operations in isolation and potentially undo all the operations if one of them fails. This guide will get you started using transactions with Mongoose. -

      Getting Started with Transactions

      +## Getting Started with Transactions {#getting-started-with-transactions} If you haven't already, import mongoose: @@ -61,12 +61,12 @@ await db.transaction(async function setRank(session) { doc.isNew; ``` -

      Note About Parallelism in Transactions

      +## Note About Parallelism in Transactions {#note-about-parallelism-in-transactions} Running operations in parallel is **not supported** during a transaction. The use of `Promise.all`, `Promise.allSettled`, `Promise.race`, etc. to parallelize operations inside a transaction is undefined behaviour and should be avoided. -

      With Mongoose Documents and save()

      +## With Mongoose Documents and `save()` {#with-mongoose-documents-and-save} If you get a [Mongoose document](documents.html) from [`findOne()`](api/model.html#model_Model-findOne) or [`find()`](api/model.html#model_Model-find) using a session, the document will @@ -78,7 +78,7 @@ To get/set the session associated with a given document, use [`doc.$session()`]( [require:transactions.*save] ``` -

      With the Aggregation Framework

      +## With the Aggregation Framework {#with-the-aggregation-framework} The `Model.aggregate()` function also supports transactions. Mongoose aggregations have a [`session()` helper](api/aggregate.html#aggregate_Aggregate-session) @@ -89,7 +89,7 @@ Below is an example of executing an aggregation within a transaction. [require:transactions.*aggregate] ``` -

      Using AsyncLocalStorage

      +## Using AsyncLocalStorage {#asynclocalstorage} One major pain point with transactions in Mongoose is that you need to remember to set the `session` option on every operation. If you don't, your operation will execute outside of the transaction. @@ -116,7 +116,7 @@ await Test.exists({ _id: doc._id }); With `transactionAsyncLocalStorage`, you no longer need to pass sessions to every operation. Mongoose will add the session by default under the hood. -

      Advanced Usage

      +## Advanced Usage {#advanced-usage} Advanced users who want more fine-grained control over when they commit or abort transactions can use `session.startTransaction()` to start a transaction: From 271f90d392b4764f533ec8fc807602895a3db787 Mon Sep 17 00:00:00 2001 From: Valeri Karpov Date: Mon, 19 Aug 2024 12:34:56 -0400 Subject: [PATCH 39/53] style: fix variable name typo --- scripts/website.js | 6 +++--- 1 file changed, 3 insertions(+), 3 deletions(-) diff --git a/scripts/website.js b/scripts/website.js index c5bc2251764..75317225791 100644 --- a/scripts/website.js +++ b/scripts/website.js @@ -19,7 +19,7 @@ const cwd = path.resolve(__dirname, '..'); // see https://www.markdownguide.org/extended-syntax/#heading-ids // Example: // # Some Header {#custom-id} -const CustomIdRefex = /{#([a-zA-Z0-9_-]+)}(?: *)$/; +const CustomIdRegex = /{#([a-zA-Z0-9_-]+)}(?: *)$/; const isMain = require.main === module; @@ -44,12 +44,12 @@ markdown.use({ heading: function({ tokens, depth }) { let raw = this.parser.parseInline(tokens); let slug; - const idMatch = CustomIdRefex.exec(raw); + const idMatch = CustomIdRegex.exec(raw); // use custom header id if available, otherwise fallback to default slugger if (idMatch) { slug = idMatch[1]; - raw = raw.replace(CustomIdRefex, ''); + raw = raw.replace(CustomIdRegex, ''); } else { slug = createSlug(raw.trim()); } From 7b988446f9024b3611fe8554395fc5c431fb8b60 Mon Sep 17 00:00:00 2001 From: Valeri Karpov Date: Mon, 19 Aug 2024 12:43:28 -0400 Subject: [PATCH 40/53] chore: use TypeScript@5.5 in benchmarks and bump max instantiations until we can figure out why TS 5.5 causes so many more instantiations --- benchmarks/typescript/simple/package.json | 2 +- scripts/tsc-diagnostics-check.js | 2 +- 2 files changed, 2 insertions(+), 2 deletions(-) diff --git a/benchmarks/typescript/simple/package.json b/benchmarks/typescript/simple/package.json index ee74cdf4d70..6571d866d06 100644 --- a/benchmarks/typescript/simple/package.json +++ b/benchmarks/typescript/simple/package.json @@ -1,7 +1,7 @@ { "dependencies": { "mongoose": "file:../../../mongoose.tgz", - "typescript": "4.9.x" + "typescript": "5.5.x" }, "scripts": { "benchmark": "tsc --extendedDiagnostics" diff --git a/scripts/tsc-diagnostics-check.js b/scripts/tsc-diagnostics-check.js index 3e1f6c66282..fd585f9901c 100644 --- a/scripts/tsc-diagnostics-check.js +++ b/scripts/tsc-diagnostics-check.js @@ -3,7 +3,7 @@ const fs = require('fs'); const stdin = fs.readFileSync(0).toString('utf8'); -const maxInstantiations = isNaN(process.argv[2]) ? 127500 : parseInt(process.argv[2], 10); +const maxInstantiations = isNaN(process.argv[2]) ? 230000 : parseInt(process.argv[2], 10); console.log(stdin); From bfb9cde49d8029ee0d96462ee0beb1f4b74b3f48 Mon Sep 17 00:00:00 2001 From: Valeri Karpov Date: Mon, 19 Aug 2024 12:46:57 -0400 Subject: [PATCH 41/53] bump max typescript instantiations again --- scripts/tsc-diagnostics-check.js | 2 +- 1 file changed, 1 insertion(+), 1 deletion(-) diff --git a/scripts/tsc-diagnostics-check.js b/scripts/tsc-diagnostics-check.js index fd585f9901c..a1c4408cb79 100644 --- a/scripts/tsc-diagnostics-check.js +++ b/scripts/tsc-diagnostics-check.js @@ -3,7 +3,7 @@ const fs = require('fs'); const stdin = fs.readFileSync(0).toString('utf8'); -const maxInstantiations = isNaN(process.argv[2]) ? 230000 : parseInt(process.argv[2], 10); +const maxInstantiations = isNaN(process.argv[2]) ? 250000 : parseInt(process.argv[2], 10); console.log(stdin); From 9dcd8aa32ab328ec5445de9f79db634ffa644434 Mon Sep 17 00:00:00 2001 From: Valeri Karpov Date: Mon, 19 Aug 2024 13:11:29 -0400 Subject: [PATCH 42/53] chore: release 7.8.1 --- CHANGELOG.md | 5 +++++ package.json | 2 +- 2 files changed, 6 insertions(+), 1 deletion(-) diff --git a/CHANGELOG.md b/CHANGELOG.md index 3b00baf559e..978abc41b4f 100644 --- a/CHANGELOG.md +++ b/CHANGELOG.md @@ -1,3 +1,8 @@ +7.8.1 / 2024-08-19 +================== + * fix(query): handle casting $switch in $expr #14761 + * docs(mongoose): remove out-of-date callback-based example for mongoose.connect() #14811 #14810 + 7.8.0 / 2024-07-23 ================== * feat: add transactionAsyncLocalStorage option to opt in to automatically setting session on all transactions #14744 #14742 #14583 #13889 diff --git a/package.json b/package.json index cbd4adc15af..92169d360ca 100644 --- a/package.json +++ b/package.json @@ -1,7 +1,7 @@ { "name": "mongoose", "description": "Mongoose MongoDB ODM", - "version": "7.8.0", + "version": "7.8.1", "author": "Guillermo Rauch ", "keywords": [ "mongodb", From dc4ffab0fe08350f8e8213252839267e1baa6cd3 Mon Sep 17 00:00:00 2001 From: Valeri Karpov Date: Tue, 20 Aug 2024 10:08:56 -0400 Subject: [PATCH 43/53] Update docs/migrating_to_7.md Co-authored-by: hasezoey --- docs/migrating_to_7.md | 2 +- 1 file changed, 1 insertion(+), 1 deletion(-) diff --git a/docs/migrating_to_7.md b/docs/migrating_to_7.md index 569684b6ed7..866c6d017e0 100644 --- a/docs/migrating_to_7.md +++ b/docs/migrating_to_7.md @@ -276,7 +276,7 @@ oid._bsontype; // 'ObjectId' in Mongoose 7, 'ObjectID' in older versions of Mong Please update any places where you use `_bsontype` to check if an object is an ObjectId. This may also affect libraries that use Mongoose. -## Removed mapReduce {#removed-mapreduce} +## Removed `mapReduce` {#removed-mapreduce} MongoDB no longer supports `mapReduce`, so Mongoose 7 no longer has a `Model.mapReduce()` function. Use the aggregation framework as a replacement for `mapReduce()`. From 20cb1461a7e10cb33737efb7c975d163274d8295 Mon Sep 17 00:00:00 2001 From: Valeri Karpov Date: Tue, 20 Aug 2024 10:09:03 -0400 Subject: [PATCH 44/53] Update docs/migrating_to_6.md Co-authored-by: hasezoey --- docs/migrating_to_6.md | 2 +- 1 file changed, 1 insertion(+), 1 deletion(-) diff --git a/docs/migrating_to_6.md b/docs/migrating_to_6.md index 6bb5e83bcdb..9455c83577c 100644 --- a/docs/migrating_to_6.md +++ b/docs/migrating_to_6.md @@ -130,7 +130,7 @@ await q; await q.clone(); // Can `clone()` the query to allow executing the query again ``` -## Model.exists(...) now returns a lean document instead of boolean {#model-exists-returns-a-lean-document-instead-of-boolean} +## `Model.exists(...)` now returns a lean document instead of boolean {#model-exists-returns-a-lean-document-instead-of-boolean} ```js // in Mongoose 5.x, `existingUser` used to be a boolean From edd94fc78ea9edd506a931f39489c571050cb91f Mon Sep 17 00:00:00 2001 From: Valeri Karpov Date: Tue, 20 Aug 2024 10:12:34 -0400 Subject: [PATCH 45/53] docs: fix duplicate header id --- docs/guide.md | 4 ++-- 1 file changed, 2 insertions(+), 2 deletions(-) diff --git a/docs/guide.md b/docs/guide.md index 4912b23282f..78b935ca633 100644 --- a/docs/guide.md +++ b/docs/guide.md @@ -6,7 +6,7 @@ If you are migrating from 7.x to 8.x please take a moment to read the [migration
      • Defining your schema
      • Creating a model
      • -
      • Ids
      • +
      • Ids
      • Instance methods
      • Statics
      • Query Helpers
      • @@ -99,7 +99,7 @@ const Blog = mongoose.model('Blog', blogSchema); // ready to go! ``` -## Ids {#_id} +## Ids By default, Mongoose adds an `_id` property to your schemas. From a563adc8a5bc5c43d8836953cde93a8ef182abc9 Mon Sep 17 00:00:00 2001 From: Valeri Karpov Date: Tue, 20 Aug 2024 10:17:38 -0400 Subject: [PATCH 46/53] fix a couple of more legacy style header definitions --- docs/middleware.md | 4 ++-- docs/subdocs.md | 2 +- docs/tutorials/lean.md | 6 +++--- 3 files changed, 6 insertions(+), 6 deletions(-) diff --git a/docs/middleware.md b/docs/middleware.md index ca5e2f1f4ed..b74452affbd 100644 --- a/docs/middleware.md +++ b/docs/middleware.md @@ -175,7 +175,7 @@ schema.pre('save', function(next) { }); ``` -

        Use Cases

        +### Use Cases Middleware are useful for atomizing model logic. Here are some other ideas: @@ -184,7 +184,7 @@ Middleware are useful for atomizing model logic. Here are some other ideas: * asynchronous defaults * asynchronous tasks that a certain action triggers -

        Errors in Pre Hooks

        +### Errors in Pre Hooks {#error-handling} If any pre hook errors out, mongoose will not execute subsequent middleware or the hooked function. Mongoose will instead pass an error to the callback diff --git a/docs/subdocs.md b/docs/subdocs.md index eeb4c2973b0..5bd37433435 100644 --- a/docs/subdocs.md +++ b/docs/subdocs.md @@ -313,7 +313,7 @@ doc.level1.level2.parent() === doc.level1; // true doc.level1.level2.ownerDocument() === doc; // true ``` -

        Alternate declaration syntax for arrays

        +### Alternate declaration syntax for arrays {#altsyntaxarrays} If you create a schema with an array of objects, Mongoose will automatically convert the object to a schema for you: diff --git a/docs/tutorials/lean.md b/docs/tutorials/lean.md index 404d352de4f..e11641449d1 100644 --- a/docs/tutorials/lean.md +++ b/docs/tutorials/lean.md @@ -12,7 +12,7 @@ In this tutorial, you'll learn more about the tradeoffs of using `lean()`. * [Plugins](#plugins) * [BigInts](#bigints) -

        Using Lean

        +## Using Lean By default, Mongoose queries return an instance of the [Mongoose `Document` class](../api/document.html#Document). Documents are much @@ -53,7 +53,7 @@ and virtuals don't run if you enable `lean`. [require:Lean Tutorial.*getters and virtuals] ``` -

        Lean and Populate

        +## Lean and Populate [Populate](../populate.html) works with `lean()`. If you use both `populate()` and `lean()`, the `lean` option propagates to the @@ -70,7 +70,7 @@ populated documents as well. In the below example, both the top-level [require:Lean Tutorial.*virtual populate] ``` -

        When to Use Lean

        +## When to Use Lean If you're executing a query and sending the results without modification to, say, an [Express response](http://expressjs.com/en/4x/api.html#res), you should From 3169d92ed63df939241a513140fe227c0e9d0b8e Mon Sep 17 00:00:00 2001 From: Valeri Karpov Date: Fri, 23 Aug 2024 13:42:44 -0400 Subject: [PATCH 47/53] chore: release 8.5.4 --- CHANGELOG.md | 5 +++++ package.json | 2 +- 2 files changed, 6 insertions(+), 1 deletion(-) diff --git a/CHANGELOG.md b/CHANGELOG.md index 3d0ff9b9b1d..ffdc11063b7 100644 --- a/CHANGELOG.md +++ b/CHANGELOG.md @@ -1,3 +1,8 @@ +8.5.4 / 2024-08-23 +================== + * fix: add empty string check for collection name passed #14806 [Shubham2552](https://github.com/Shubham2552) + * docs(model): add 'throw' as valid strict value for bulkWrite() and add some more clarification on throwOnValidationError #14809 + 7.8.1 / 2024-08-19 ================== * fix(query): handle casting $switch in $expr #14761 diff --git a/package.json b/package.json index f684e3a0e1f..335b61efb5f 100644 --- a/package.json +++ b/package.json @@ -1,7 +1,7 @@ { "name": "mongoose", "description": "Mongoose MongoDB ODM", - "version": "8.5.3", + "version": "8.5.4", "author": "Guillermo Rauch ", "keywords": [ "mongodb", From 270b9f526bf153c50af67e85fc8f5efafe2223d8 Mon Sep 17 00:00:00 2001 From: Danila Stryzhonak Date: Sun, 25 Aug 2024 20:14:29 +0300 Subject: [PATCH 48/53] fix: fixing schema type based on timestamps schema options value --- test/types/schema.test.ts | 27 +++++++++++++++++++++++++++ types/inferschematype.d.ts | 4 ++-- 2 files changed, 29 insertions(+), 2 deletions(-) diff --git a/test/types/schema.test.ts b/test/types/schema.test.ts index 2d946adc93c..8c9d1a8f3d5 100644 --- a/test/types/schema.test.ts +++ b/test/types/schema.test.ts @@ -1604,3 +1604,30 @@ function gh13215() { type SchemaType = InferSchemaType; expectType({} as SchemaType); } + +function gh14825() { + const schemaDefinition = { + userName: { type: String, required: true } + } as const; + const schemaOptions = { + typeKey: 'type' as const, + timestamps: { + createdAt: 'date', + updatedAt: false + } + }; + + type RawDocType = InferRawDocType< + typeof schemaDefinition, + typeof schemaOptions + >; + type User = { + userName: string; + }; + + expectAssignable({} as RawDocType); + + const schema = new Schema(schemaDefinition, schemaOptions); + type SchemaType = InferSchemaType; + expectAssignable({} as SchemaType); +} diff --git a/types/inferschematype.d.ts b/types/inferschematype.d.ts index 2bb6abf8bbe..dbcb3ad851e 100644 --- a/types/inferschematype.d.ts +++ b/types/inferschematype.d.ts @@ -87,8 +87,8 @@ declare module 'mongoose' { 'createdAt' | 'updatedAt' > as TimestampOptions[K] extends true ? K - : TimestampOptions[K] extends string - ? TimestampOptions[K] + : TimestampOptions[K] extends `${infer TimestampValue}` + ? TimestampValue : never]: NativeDate; } & T : T From 032a4f7166b74ee94c06d73dd95d962ea21ade5f Mon Sep 17 00:00:00 2001 From: shallow-beach <96891913+shallow-beach@users.noreply.github.com> Date: Mon, 26 Aug 2024 15:48:51 -0700 Subject: [PATCH 49/53] Update docs/guide.md add period --- docs/guide.md | 2 +- 1 file changed, 1 insertion(+), 1 deletion(-) diff --git a/docs/guide.md b/docs/guide.md index 78b935ca633..fa653acc4dd 100644 --- a/docs/guide.md +++ b/docs/guide.md @@ -1105,7 +1105,7 @@ await doc2.save(); ``` -If you need optimistic concurrency support for `save()`, you can set the [`optimisticConcurrency` option](#optimisticConcurrency) +If you need optimistic concurrency support for `save()`, you can set the [`optimisticConcurrency` option](#optimisticConcurrency). Document versioning can also be disabled by setting the `versionKey` to From 62c6a202332a511e5d8c7e1e1a2b93757ddda167 Mon Sep 17 00:00:00 2001 From: Valeri Karpov Date: Tue, 27 Aug 2024 08:45:34 -0400 Subject: [PATCH 50/53] fix(populate): fix a couple of other places where we get the document's _id with getters Fix #14827 Re: #14759 --- lib/document.js | 6 ++-- lib/error/parallelSave.js | 2 +- lib/error/parallelValidate.js | 2 +- lib/error/version.js | 2 +- .../populate/getModelsMapForPopulate.js | 4 +-- .../populate/markArraySubdocsPopulated.js | 4 +-- lib/model.js | 6 ++-- lib/query.js | 6 ++-- test/model.populate.test.js | 35 +++++++++++++++++++ 9 files changed, 51 insertions(+), 16 deletions(-) diff --git a/lib/document.js b/lib/document.js index c6214d477f2..f6a6016c72e 100644 --- a/lib/document.js +++ b/lib/document.js @@ -842,7 +842,7 @@ function init(self, obj, doc, opts, prefix) { */ Document.prototype.updateOne = function updateOne(doc, options, callback) { - const query = this.constructor.updateOne({ _id: this._id }, doc, options); + const query = this.constructor.updateOne({ _id: this._doc._id }, doc, options); const self = this; query.pre(function queryPreUpdateOne(cb) { self.constructor._middleware.execPre('updateOne', self, [self], cb); @@ -883,7 +883,7 @@ Document.prototype.updateOne = function updateOne(doc, options, callback) { Document.prototype.replaceOne = function replaceOne() { const args = [...arguments]; - args.unshift({ _id: this._id }); + args.unshift({ _id: this._doc._id }); return this.constructor.replaceOne.apply(this.constructor, args); }; @@ -3050,7 +3050,7 @@ Document.prototype.$__validate = function(pathsToValidate, options, callback) { } else if (val != null && val.$__ != null && val.$__.wasPopulated) { // Array paths, like `somearray.1`, do not show up as populated with `$populated()`, // so in that case pull out the document's id - val = val._id; + val = val._doc._id; } const scope = _this.$__.pathsToScopes != null && path in _this.$__.pathsToScopes ? _this.$__.pathsToScopes[path] : diff --git a/lib/error/parallelSave.js b/lib/error/parallelSave.js index 57ac458238d..25e12481d49 100644 --- a/lib/error/parallelSave.js +++ b/lib/error/parallelSave.js @@ -15,7 +15,7 @@ class ParallelSaveError extends MongooseError { */ constructor(doc) { const msg = 'Can\'t save() the same doc multiple times in parallel. Document: '; - super(msg + doc._id); + super(msg + doc._doc._id); } } diff --git a/lib/error/parallelValidate.js b/lib/error/parallelValidate.js index 477954dc870..84b7940d6df 100644 --- a/lib/error/parallelValidate.js +++ b/lib/error/parallelValidate.js @@ -16,7 +16,7 @@ class ParallelValidateError extends MongooseError { */ constructor(doc) { const msg = 'Can\'t validate() the same doc multiple times in parallel. Document: '; - super(msg + doc._id); + super(msg + doc._doc._id); } } diff --git a/lib/error/version.js b/lib/error/version.js index 85f2921a517..6bc2b5d3af5 100644 --- a/lib/error/version.js +++ b/lib/error/version.js @@ -17,7 +17,7 @@ class VersionError extends MongooseError { */ constructor(doc, currentVersion, modifiedPaths) { const modifiedPathsStr = modifiedPaths.join(', '); - super('No matching document found for id "' + doc._id + + super('No matching document found for id "' + doc._doc._id + '" version ' + currentVersion + ' modifiedPaths "' + modifiedPathsStr + '"'); this.version = currentVersion; this.modifiedPaths = modifiedPaths; diff --git a/lib/helpers/populate/getModelsMapForPopulate.js b/lib/helpers/populate/getModelsMapForPopulate.js index 276698217d0..2b2a2b40312 100644 --- a/lib/helpers/populate/getModelsMapForPopulate.js +++ b/lib/helpers/populate/getModelsMapForPopulate.js @@ -626,7 +626,7 @@ function _getLocalFieldValues(doc, localField, model, options, virtual, schema) function convertTo_id(val, schema) { if (val != null && val.$__ != null) { - return val._id; + return val._doc._id; } if (val != null && val._id != null && (schema == null || !schema.$isSchemaMap)) { return val._id; @@ -636,7 +636,7 @@ function convertTo_id(val, schema) { const rawVal = val.__array != null ? val.__array : val; for (let i = 0; i < rawVal.length; ++i) { if (rawVal[i] != null && rawVal[i].$__ != null) { - rawVal[i] = rawVal[i]._id; + rawVal[i] = rawVal[i]._doc._id; } } if (utils.isMongooseArray(val) && val.$schema()) { diff --git a/lib/helpers/populate/markArraySubdocsPopulated.js b/lib/helpers/populate/markArraySubdocsPopulated.js index f9cc83cb6bf..c0ea5ef5207 100644 --- a/lib/helpers/populate/markArraySubdocsPopulated.js +++ b/lib/helpers/populate/markArraySubdocsPopulated.js @@ -17,11 +17,11 @@ const utils = require('../../utils'); */ module.exports = function markArraySubdocsPopulated(doc, populated) { - if (doc._id == null || populated == null || populated.length === 0) { + if (doc._doc._id == null || populated == null || populated.length === 0) { return; } - const id = String(doc._id); + const id = String(doc._doc._id); for (const item of populated) { if (item.isVirtual) { continue; diff --git a/lib/model.js b/lib/model.js index 05d89db8d76..e0270872155 100644 --- a/lib/model.js +++ b/lib/model.js @@ -2345,7 +2345,7 @@ Model.findByIdAndUpdate = function(id, update, options) { // if a model is passed in instead of an id if (id instanceof Document) { - id = id._id; + id = id._doc._id; } return this.findOneAndUpdate.call(this, { _id: id }, update, options); @@ -3408,7 +3408,7 @@ Model.bulkSave = async function bulkSave(documents, options) { documents.map(async(document) => { const documentError = bulkWriteError && bulkWriteError.writeErrors.find(writeError => { const writeErrorDocumentId = writeError.err.op._id || writeError.err.op.q._id; - return writeErrorDocumentId.toString() === document._id.toString(); + return writeErrorDocumentId.toString() === document._doc._id.toString(); }); if (documentError == null) { @@ -4436,7 +4436,7 @@ function _assign(model, vals, mod, assignmentOpts) { for (let __val of _val) { if (__val instanceof Document) { - __val = __val._id; + __val = __val._doc._id; } key = String(__val); if (rawDocs[key]) { diff --git a/lib/query.js b/lib/query.js index 55e5d3f02af..f928ca75375 100644 --- a/lib/query.js +++ b/lib/query.js @@ -3380,9 +3380,9 @@ Query.prototype._findOneAndUpdate = async function _findOneAndUpdate() { if (!this._update || Object.keys(this._update).length === 0) { if (options.upsert) { // still need to do the upsert to empty doc - const doc = clone(this._update); - delete doc._id; - this._update = { $set: doc }; + const $set = clone(this._update); + delete $set._id; + this._update = { $set }; } else { this._executionStack = null; const res = await this._findOne(); diff --git a/test/model.populate.test.js b/test/model.populate.test.js index bbdfadfe281..11ccd58ae35 100644 --- a/test/model.populate.test.js +++ b/test/model.populate.test.js @@ -11049,6 +11049,41 @@ describe('model: populate:', function() { assert.equal(pet2.owner.name, 'Alice'); }); + it('avoids populating manually populated doc as getter value (gh-14827)', async function() { + const ownerSchema = new mongoose.Schema({ + _id: { + type: 'ObjectId', + get(value) { + return value == null ? value : value.toString(); + } + }, + name: 'String' + }); + const petSchema = new mongoose.Schema({ + name: 'String', + owner: { type: 'ObjectId', ref: 'Owner' } + }); + + const Owner = db.model('Owner', ownerSchema); + const Pet = db.model('Pet', petSchema); + + const _id = new mongoose.Types.ObjectId(); + const owner = new Owner({ _id, name: 'Alice' }); + const pet = new Pet({ name: 'Kitty', owner: owner }); + + await owner.save(); + + assert.equal(typeof pet._doc.owner.$__.wasPopulated.value, 'object'); // object πŸ˜€ + await pet.populate('owner'); // This breaks it πŸ’₯πŸ’₯πŸ’₯πŸ’₯πŸ’₯πŸ’₯πŸ’₯πŸ’₯πŸ’₯πŸ’₯ + assert.equal(typeof pet._doc.owner.$__.wasPopulated.value, 'object'); // string πŸ™ + + await pet.save(); + + + const fromDb = await Pet.findOne({ owner: _id }).lean().orFail(); + assert.ok(fromDb.owner instanceof mongoose.Types.ObjectId); + }); + it('makes sure that populate works correctly with duplicate foreignField with lean(); (gh-14794)', async function() { const authorSchema = new mongoose.Schema({ group: String, From 16df3da848a7ae5f1c9ef7e94d12da58514db9d9 Mon Sep 17 00:00:00 2001 From: Valeri Karpov Date: Tue, 27 Aug 2024 08:48:28 -0400 Subject: [PATCH 51/53] chore: remove extraneous headers --- test/model.populate.test.js | 6 +++--- 1 file changed, 3 insertions(+), 3 deletions(-) diff --git a/test/model.populate.test.js b/test/model.populate.test.js index 11ccd58ae35..7f0fe844eb8 100644 --- a/test/model.populate.test.js +++ b/test/model.populate.test.js @@ -11073,9 +11073,9 @@ describe('model: populate:', function() { await owner.save(); - assert.equal(typeof pet._doc.owner.$__.wasPopulated.value, 'object'); // object πŸ˜€ - await pet.populate('owner'); // This breaks it πŸ’₯πŸ’₯πŸ’₯πŸ’₯πŸ’₯πŸ’₯πŸ’₯πŸ’₯πŸ’₯πŸ’₯ - assert.equal(typeof pet._doc.owner.$__.wasPopulated.value, 'object'); // string πŸ™ + assert.equal(typeof pet._doc.owner.$__.wasPopulated.value, 'object'); + await pet.populate('owner'); + assert.equal(typeof pet._doc.owner.$__.wasPopulated.value, 'object'); await pet.save(); From 9526e66de133c4ca1289021f9b8e9b9a9fb2c940 Mon Sep 17 00:00:00 2001 From: Valeri Karpov Date: Wed, 28 Aug 2024 10:01:58 -0400 Subject: [PATCH 52/53] fix(discriminator): shallow clone `Schema.prototype.obj` before merging schemas to avoid modifying original obj Fix #14827 --- lib/helpers/model/discriminator.js | 3 +++ test/model.discriminator.test.js | 17 +++++++++++++++++ 2 files changed, 20 insertions(+) diff --git a/lib/helpers/model/discriminator.js b/lib/helpers/model/discriminator.js index d587c7930bb..7c8633ab5dc 100644 --- a/lib/helpers/model/discriminator.js +++ b/lib/helpers/model/discriminator.js @@ -115,6 +115,9 @@ module.exports = function discriminator(model, name, schema, tiedValue, applyPlu } } + // Shallow clone `obj` so we can add additional properties without modifying original + // schema. `Schema.prototype.clone()` copies `obj` by reference, no cloning. + schema.obj = { ...schema.obj }; mergeDiscriminatorSchema(schema, baseSchema); // Clean up conflicting paths _after_ merging re: gh-6076 diff --git a/test/model.discriminator.test.js b/test/model.discriminator.test.js index 8e8d529e2df..a829211b236 100644 --- a/test/model.discriminator.test.js +++ b/test/model.discriminator.test.js @@ -620,6 +620,23 @@ describe('model', function() { Person.discriminator('Parent2', parentSchema.clone()); }); + it('clone() does not modify original schema `obj` (gh-14821)', function() { + const personSchema = new Schema({ + name: String + }, { discriminatorKey: 'kind' }); + + const parentSchema = new Schema({ + child: String + }); + + const Person = db.model('Person', personSchema); + const Parent = Person.discriminator('Parent', parentSchema.clone()); + + assert.ok(!Person.schema.obj.child); + assert.ok(!personSchema.obj.child); + assert.ok(Parent.schema.obj.child); + }); + it('clone() allows reusing with different models (gh-5721)', async function() { const schema = new mongoose.Schema({ name: String From fb0febb8406e0f450cf3aea758f7857486fe731f Mon Sep 17 00:00:00 2001 From: Valeri Karpov Date: Wed, 28 Aug 2024 12:56:04 -0400 Subject: [PATCH 53/53] chore: release 8.5.5 --- CHANGELOG.md | 6 ++++++ package.json | 2 +- 2 files changed, 7 insertions(+), 1 deletion(-) diff --git a/CHANGELOG.md b/CHANGELOG.md index ffdc11063b7..07464045681 100644 --- a/CHANGELOG.md +++ b/CHANGELOG.md @@ -1,3 +1,9 @@ +8.5.5 / 2024-08-28 +================== + * fix(populate): fix a couple of other places where Mongoose gets the document's _id with getters #14833 #14827 #14759 + * fix(discriminator): shallow clone Schema.prototype.obj before merging schemas to avoid modifying original obj #14821 + * types: fix schema type based on timestamps schema options value #14829 #14825 [ark23CIS](https://github.com/ark23CIS) + 8.5.4 / 2024-08-23 ================== * fix: add empty string check for collection name passed #14806 [Shubham2552](https://github.com/Shubham2552) diff --git a/package.json b/package.json index 335b61efb5f..53551cff5c0 100644 --- a/package.json +++ b/package.json @@ -1,7 +1,7 @@ { "name": "mongoose", "description": "Mongoose MongoDB ODM", - "version": "8.5.4", + "version": "8.5.5", "author": "Guillermo Rauch ", "keywords": [ "mongodb",