Skip to content

Commit 3783ed8

Browse files
authored
Merge pull request #14594 from Automattic/8.4
8.4
2 parents cdedde6 + 26375d6 commit 3783ed8

Some content is hidden

Large Commits have some content hidden by default. Use the searchbox below for content that may be hidden.

48 files changed

+535
-66
lines changed

docs/guide.md

+19
Original file line numberDiff line numberDiff line change
@@ -559,6 +559,7 @@ Valid options:
559559
* [methods](#methods)
560560
* [query](#query-helpers)
561561
* [autoSearchIndex](#autoSearchIndex)
562+
* [readConcern](#readConcern)
562563

563564
<h2 id="autoIndex"><a href="#autoIndex">option: autoIndex</a></h2>
564565

@@ -1473,6 +1474,24 @@ schema.searchIndex({
14731474
const Test = mongoose.model('Test', schema);
14741475
```
14751476

1477+
<h2 id="readConcern">
1478+
<a href="#readConcern">
1479+
option: readConcern
1480+
</a>
1481+
</h2>
1482+
1483+
[Read concerns](https://www.mongodb.com/docs/manual/reference/read-concern/) are similar to [`writeConcern`](#writeConcern), but for read operations like `find()` and `findOne()`.
1484+
To set a default `readConcern`, pass the `readConcern` option to the schema constructor as follows.
1485+
1486+
```javascript
1487+
const eventSchema = new mongoose.Schema(
1488+
{ name: String },
1489+
{
1490+
readConcern: { level: 'available' } // <-- set default readConcern for all queries
1491+
}
1492+
);
1493+
```
1494+
14761495
<h2 id="es6-classes"><a href="#es6-classes">With ES6 Classes</a></h2>
14771496

14781497
Schemas have a [`loadClass()` method](api/schema.html#schema_Schema-loadClass)

docs/transactions.md

+28-3
Original file line numberDiff line numberDiff line change
@@ -1,8 +1,6 @@
11
# Transactions in Mongoose
22

3-
[Transactions](https://www.mongodb.com/transactions) are new in MongoDB
4-
4.0 and Mongoose 5.2.0. Transactions let you execute multiple operations
5-
in isolation and potentially undo all the operations if one of them fails.
3+
[Transactions](https://www.mongodb.com/transactions) let you execute multiple operations in isolation and potentially undo all the operations if one of them fails.
64
This guide will get you started using transactions with Mongoose.
75

86
<h2 id="getting-started-with-transactions"><a href="#getting-started-with-transactions">Getting Started with Transactions</a></h2>
@@ -86,6 +84,33 @@ Below is an example of executing an aggregation within a transaction.
8684
[require:transactions.*aggregate]
8785
```
8886

87+
<h2 id="asynclocalstorage"><a href="#asynclocalstorage">Using AsyncLocalStorage</a></h2>
88+
89+
One major pain point with transactions in Mongoose is that you need to remember to set the `session` option on every operation.
90+
If you don't, your operation will execute outside of the transaction.
91+
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).
92+
Set the `transactionAsyncLocalStorage` option using `mongoose.set('transactionAsyncLocalStorage', true)` to enable this feature.
93+
94+
```javascript
95+
mongoose.set('transactionAsyncLocalStorage', true);
96+
97+
const Test = mongoose.model('Test', mongoose.Schema({ name: String }));
98+
99+
const doc = new Test({ name: 'test' });
100+
101+
// Save a new doc in a transaction that aborts
102+
await connection.transaction(async() => {
103+
await doc.save(); // Notice no session here
104+
throw new Error('Oops');
105+
}).catch(() => {});
106+
107+
// false, `save()` was rolled back
108+
await Test.exists({ _id: doc._id });
109+
```
110+
111+
With `transactionAsyncLocalStorage`, you no longer need to pass sessions to every operation.
112+
Mongoose will add the session by default under the hood.
113+
89114
<h2 id="advanced-usage"><a href="#advanced-usage">Advanced Usage</a></h2>
90115

91116
Advanced users who want more fine-grained control over when they commit or abort transactions

docs/typescript/schemas.md

+26-1
Original file line numberDiff line numberDiff line change
@@ -9,7 +9,7 @@ Mongoose can automatically infer the document type from your schema definition a
99
We recommend relying on automatic type inference when defining schemas and models.
1010

1111
```typescript
12-
import { Schema } from 'mongoose';
12+
import { Schema, model } from 'mongoose';
1313
// Schema
1414
const schema = new Schema({
1515
name: { type: String, required: true },
@@ -32,6 +32,31 @@ There are a few caveats for using automatic type inference:
3232
2. You need to define your schema in the `new Schema()` call. Don't assign your schema definition to a temporary variable. Doing something like `const schemaDefinition = { name: String }; const schema = new Schema(schemaDefinition);` will not work.
3333
3. Mongoose adds `createdAt` and `updatedAt` to your schema if you specify the `timestamps` option in your schema, *except* if you also specify `methods`, `virtuals`, or `statics`. There is a [known issue](https://github.com/Automattic/mongoose/issues/12807) with type inference with timestamps and methods/virtuals/statics options. If you use methods, virtuals, and statics, you're responsible for adding `createdAt` and `updatedAt` to your schema definition.
3434

35+
If you need to explicitly get the raw document type (the value returned from `doc.toObject()`, `await Model.findOne().lean()`, etc.) from your schema definition, you can use Mongoose's `inferRawDocType` helper as follows:
36+
37+
```ts
38+
import { Schema, InferRawDocType, model } from 'mongoose';
39+
40+
const schemaDefinition = {
41+
name: { type: String, required: true },
42+
email: { type: String, required: true },
43+
avatar: String
44+
} as const;
45+
const schema = new Schema(schemaDefinition);
46+
47+
const UserModel = model('User', schema);
48+
const doc = new UserModel({ name: 'test', email: 'test' });
49+
50+
type RawUserDocument = InferRawDocType<typeof schemaDefinition>;
51+
52+
useRawDoc(doc.toObject());
53+
54+
function useRawDoc(doc: RawUserDocument) {
55+
// ...
56+
}
57+
58+
```
59+
3560
If automatic type inference doesn't work for you, you can always fall back to document interface definitions.
3661

3762
## Separate document interface definition

lib/aggregate.js

+5
Original file line numberDiff line numberDiff line change
@@ -1022,6 +1022,11 @@ Aggregate.prototype.exec = async function exec() {
10221022
applyGlobalMaxTimeMS(this.options, model.db.options, model.base.options);
10231023
applyGlobalDiskUse(this.options, model.db.options, model.base.options);
10241024

1025+
const asyncLocalStorage = this.model()?.db?.base.transactionAsyncLocalStorage?.getStore();
1026+
if (!this.options.hasOwnProperty('session') && asyncLocalStorage?.session != null) {
1027+
this.options.session = asyncLocalStorage.session;
1028+
}
1029+
10251030
if (this.options && this.options.cursor) {
10261031
return new AggregationCursor(this);
10271032
}

lib/connection.js

+46-25
Original file line numberDiff line numberDiff line change
@@ -398,11 +398,7 @@ Connection.prototype.createCollection = async function createCollection(collecti
398398
throw new MongooseError('Connection.prototype.createCollection() no longer accepts a callback');
399399
}
400400

401-
if ((this.readyState === STATES.connecting || this.readyState === STATES.disconnected) && this._shouldBufferCommands()) {
402-
await new Promise(resolve => {
403-
this._queue.push({ fn: resolve });
404-
});
405-
}
401+
await this._waitForConnect();
406402

407403
return this.db.createCollection(collection, options);
408404
};
@@ -494,11 +490,7 @@ Connection.prototype.startSession = async function startSession(options) {
494490
throw new MongooseError('Connection.prototype.startSession() no longer accepts a callback');
495491
}
496492

497-
if ((this.readyState === STATES.connecting || this.readyState === STATES.disconnected) && this._shouldBufferCommands()) {
498-
await new Promise(resolve => {
499-
this._queue.push({ fn: resolve });
500-
});
501-
}
493+
await this._waitForConnect();
502494

503495
const session = this.client.startSession(options);
504496
return session;
@@ -539,7 +531,7 @@ Connection.prototype.startSession = async function startSession(options) {
539531
Connection.prototype.transaction = function transaction(fn, options) {
540532
return this.startSession().then(session => {
541533
session[sessionNewDocuments] = new Map();
542-
return session.withTransaction(() => _wrapUserTransaction(fn, session), options).
534+
return session.withTransaction(() => _wrapUserTransaction(fn, session, this.base), options).
543535
then(res => {
544536
delete session[sessionNewDocuments];
545537
return res;
@@ -558,9 +550,16 @@ Connection.prototype.transaction = function transaction(fn, options) {
558550
* Reset document state in between transaction retries re: gh-13698
559551
*/
560552

561-
async function _wrapUserTransaction(fn, session) {
553+
async function _wrapUserTransaction(fn, session, mongoose) {
562554
try {
563-
const res = await fn(session);
555+
const res = mongoose.transactionAsyncLocalStorage == null
556+
? await fn(session)
557+
: await new Promise(resolve => {
558+
mongoose.transactionAsyncLocalStorage.run(
559+
{ session },
560+
() => resolve(fn(session))
561+
);
562+
});
564563
return res;
565564
} catch (err) {
566565
_resetSessionDocuments(session);
@@ -618,13 +617,24 @@ Connection.prototype.dropCollection = async function dropCollection(collection)
618617
throw new MongooseError('Connection.prototype.dropCollection() no longer accepts a callback');
619618
}
620619

620+
await this._waitForConnect();
621+
622+
return this.db.dropCollection(collection);
623+
};
624+
625+
/**
626+
* Waits for connection to be established, so the connection has a `client`
627+
*
628+
* @return Promise
629+
* @api private
630+
*/
631+
632+
Connection.prototype._waitForConnect = async function _waitForConnect() {
621633
if ((this.readyState === STATES.connecting || this.readyState === STATES.disconnected) && this._shouldBufferCommands()) {
622634
await new Promise(resolve => {
623635
this._queue.push({ fn: resolve });
624636
});
625637
}
626-
627-
return this.db.dropCollection(collection);
628638
};
629639

630640
/**
@@ -637,16 +647,31 @@ Connection.prototype.dropCollection = async function dropCollection(collection)
637647
*/
638648

639649
Connection.prototype.listCollections = async function listCollections() {
640-
if ((this.readyState === STATES.connecting || this.readyState === STATES.disconnected) && this._shouldBufferCommands()) {
641-
await new Promise(resolve => {
642-
this._queue.push({ fn: resolve });
643-
});
644-
}
650+
await this._waitForConnect();
645651

646652
const cursor = this.db.listCollections();
647653
return await cursor.toArray();
648654
};
649655

656+
/**
657+
* Helper for MongoDB Node driver's `listDatabases()`.
658+
* Returns an object with a `databases` property that contains an
659+
* array of database objects.
660+
*
661+
* #### Example:
662+
* const { databases } = await mongoose.connection.listDatabases();
663+
* databases; // [{ name: 'mongoose_test', sizeOnDisk: 0, empty: false }]
664+
*
665+
* @method listCollections
666+
* @return {Promise<{ databases: Array<{ name: string }> }>}
667+
* @api public
668+
*/
669+
670+
Connection.prototype.listDatabases = async function listDatabases() {
671+
// Implemented in `lib/drivers/node-mongodb-native/connection.js`
672+
throw new MongooseError('listDatabases() not implemented by driver');
673+
};
674+
650675
/**
651676
* Helper for `dropDatabase()`. Deletes the given database, including all
652677
* collections, documents, and indexes.
@@ -667,11 +692,7 @@ Connection.prototype.dropDatabase = async function dropDatabase() {
667692
throw new MongooseError('Connection.prototype.dropDatabase() no longer accepts a callback');
668693
}
669694

670-
if ((this.readyState === STATES.connecting || this.readyState === STATES.disconnected) && this._shouldBufferCommands()) {
671-
await new Promise(resolve => {
672-
this._queue.push({ fn: resolve });
673-
});
674-
}
695+
await this._waitForConnect();
675696

676697
// If `dropDatabase()` is called, this model's collection will not be
677698
// init-ed. It is sufficiently common to call `dropDatabase()` after

lib/drivers/node-mongodb-native/connection.js

+13
Original file line numberDiff line numberDiff line change
@@ -197,6 +197,19 @@ NativeConnection.prototype.doClose = async function doClose(force) {
197197
return this;
198198
};
199199

200+
/**
201+
* Implementation of `listDatabases()` for MongoDB driver
202+
*
203+
* @return Promise
204+
* @api public
205+
*/
206+
207+
NativeConnection.prototype.listDatabases = async function listDatabases() {
208+
await this._waitForConnect();
209+
210+
return await this.db.admin().listDatabases();
211+
};
212+
200213
/*!
201214
* ignore
202215
*/

lib/error/browserMissingSchema.js

+1-1
Original file line numberDiff line numberDiff line change
@@ -4,7 +4,7 @@
44

55
'use strict';
66

7-
const MongooseError = require('./');
7+
const MongooseError = require('./mongooseError');
88

99

1010
class MissingSchemaError extends MongooseError {

lib/error/cast.js

-1
Original file line numberDiff line numberDiff line change
@@ -75,7 +75,6 @@ class CastError extends MongooseError {
7575
* ignore
7676
*/
7777
setModel(model) {
78-
this.model = model;
7978
this.message = formatMessage(model, this.kind, this.value, this.path,
8079
this.messageFormat, this.valueType);
8180
}

lib/error/divergentArray.js

+1-1
Original file line numberDiff line numberDiff line change
@@ -5,7 +5,7 @@
55

66
'use strict';
77

8-
const MongooseError = require('./');
8+
const MongooseError = require('./mongooseError');
99

1010
class DivergentArrayError extends MongooseError {
1111
/**

lib/error/eachAsyncMultiError.js

+1-1
Original file line numberDiff line numberDiff line change
@@ -4,7 +4,7 @@
44

55
'use strict';
66

7-
const MongooseError = require('./');
7+
const MongooseError = require('./mongooseError');
88

99

1010
/**

lib/error/invalidSchemaOption.js

+1-1
Original file line numberDiff line numberDiff line change
@@ -5,7 +5,7 @@
55

66
'use strict';
77

8-
const MongooseError = require('./');
8+
const MongooseError = require('./mongooseError');
99

1010
class InvalidSchemaOptionError extends MongooseError {
1111
/**

lib/error/missingSchema.js

+1-1
Original file line numberDiff line numberDiff line change
@@ -5,7 +5,7 @@
55

66
'use strict';
77

8-
const MongooseError = require('./');
8+
const MongooseError = require('./mongooseError');
99

1010
class MissingSchemaError extends MongooseError {
1111
/**

lib/error/notFound.js

+1-1
Original file line numberDiff line numberDiff line change
@@ -4,7 +4,7 @@
44
* Module dependencies.
55
*/
66

7-
const MongooseError = require('./');
7+
const MongooseError = require('./mongooseError');
88
const util = require('util');
99

1010
class DocumentNotFoundError extends MongooseError {

lib/error/objectExpected.js

+1-1
Original file line numberDiff line numberDiff line change
@@ -4,7 +4,7 @@
44

55
'use strict';
66

7-
const MongooseError = require('./');
7+
const MongooseError = require('./mongooseError');
88

99

1010
class ObjectExpectedError extends MongooseError {

lib/error/objectParameter.js

+1-1
Original file line numberDiff line numberDiff line change
@@ -4,7 +4,7 @@
44

55
'use strict';
66

7-
const MongooseError = require('./');
7+
const MongooseError = require('./mongooseError');
88

99
class ObjectParameterError extends MongooseError {
1010
/**

lib/error/overwriteModel.js

+1-1
Original file line numberDiff line numberDiff line change
@@ -5,7 +5,7 @@
55

66
'use strict';
77

8-
const MongooseError = require('./');
8+
const MongooseError = require('./mongooseError');
99

1010

1111
class OverwriteModelError extends MongooseError {

lib/error/parallelSave.js

+1-1
Original file line numberDiff line numberDiff line change
@@ -4,7 +4,7 @@
44
* Module dependencies.
55
*/
66

7-
const MongooseError = require('./');
7+
const MongooseError = require('./mongooseError');
88

99
class ParallelSaveError extends MongooseError {
1010
/**

lib/error/strict.js

+1-1
Original file line numberDiff line numberDiff line change
@@ -4,7 +4,7 @@
44

55
'use strict';
66

7-
const MongooseError = require('./');
7+
const MongooseError = require('./mongooseError');
88

99

1010
class StrictModeError extends MongooseError {

lib/error/strictPopulate.js

+1-1
Original file line numberDiff line numberDiff line change
@@ -4,7 +4,7 @@
44

55
'use strict';
66

7-
const MongooseError = require('./');
7+
const MongooseError = require('./mongooseError');
88

99
class StrictPopulateError extends MongooseError {
1010
/**

0 commit comments

Comments
 (0)