Skip to content

Commit

Permalink
Merge pull request #14594 from Automattic/8.4
Browse files Browse the repository at this point in the history
8.4
  • Loading branch information
vkarpov15 committed May 17, 2024
2 parents cdedde6 + 26375d6 commit 3783ed8
Show file tree
Hide file tree
Showing 48 changed files with 535 additions and 66 deletions.
19 changes: 19 additions & 0 deletions docs/guide.md
Original file line number Diff line number Diff line change
Expand Up @@ -559,6 +559,7 @@ Valid options:
* [methods](#methods)
* [query](#query-helpers)
* [autoSearchIndex](#autoSearchIndex)
* [readConcern](#readConcern)

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

Expand Down Expand Up @@ -1473,6 +1474,24 @@ schema.searchIndex({
const Test = mongoose.model('Test', schema);
```

<h2 id="readConcern">
<a href="#readConcern">
option: readConcern
</a>
</h2>

[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(
{ name: String },
{
readConcern: { level: 'available' } // <-- set default readConcern for all queries
}
);
```

<h2 id="es6-classes"><a href="#es6-classes">With ES6 Classes</a></h2>

Schemas have a [`loadClass()` method](api/schema.html#schema_Schema-loadClass)
Expand Down
31 changes: 28 additions & 3 deletions docs/transactions.md
Original file line number Diff line number Diff line change
@@ -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.

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

<h2 id="asynclocalstorage"><a href="#asynclocalstorage">Using AsyncLocalStorage</a></h2>

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.

<h2 id="advanced-usage"><a href="#advanced-usage">Advanced Usage</a></h2>

Advanced users who want more fine-grained control over when they commit or abort transactions
Expand Down
27 changes: 26 additions & 1 deletion docs/typescript/schemas.md
Original file line number Diff line number Diff line change
Expand Up @@ -9,7 +9,7 @@ Mongoose can automatically infer the document type from your schema definition a
We recommend relying on automatic type inference when defining schemas and models.

```typescript
import { Schema } from 'mongoose';
import { Schema, model } from 'mongoose';
// Schema
const schema = new Schema({
name: { type: String, required: true },
Expand All @@ -32,6 +32,31 @@ There are a few caveats for using automatic type inference:
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.
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.

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:

```ts
import { Schema, InferRawDocType, model } from 'mongoose';

const schemaDefinition = {
name: { type: String, required: true },
email: { type: String, required: true },
avatar: String
} as const;
const schema = new Schema(schemaDefinition);

const UserModel = model('User', schema);
const doc = new UserModel({ name: 'test', email: 'test' });

type RawUserDocument = InferRawDocType<typeof schemaDefinition>;

useRawDoc(doc.toObject());

function useRawDoc(doc: RawUserDocument) {
// ...
}

```

If automatic type inference doesn't work for you, you can always fall back to document interface definitions.

## Separate document interface definition
Expand Down
5 changes: 5 additions & 0 deletions lib/aggregate.js
Original file line number Diff line number Diff line change
Expand Up @@ -1022,6 +1022,11 @@ Aggregate.prototype.exec = async function exec() {
applyGlobalMaxTimeMS(this.options, model.db.options, model.base.options);
applyGlobalDiskUse(this.options, model.db.options, model.base.options);

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);
}
Expand Down
71 changes: 46 additions & 25 deletions lib/connection.js
Original file line number Diff line number Diff line change
Expand Up @@ -398,11 +398,7 @@ Connection.prototype.createCollection = async function createCollection(collecti
throw new MongooseError('Connection.prototype.createCollection() no longer accepts a callback');
}

if ((this.readyState === STATES.connecting || this.readyState === STATES.disconnected) && this._shouldBufferCommands()) {
await new Promise(resolve => {
this._queue.push({ fn: resolve });
});
}
await this._waitForConnect();

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

if ((this.readyState === STATES.connecting || this.readyState === STATES.disconnected) && this._shouldBufferCommands()) {
await new Promise(resolve => {
this._queue.push({ fn: resolve });
});
}
await this._waitForConnect();

const session = this.client.startSession(options);
return session;
Expand Down Expand Up @@ -539,7 +531,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;
Expand All @@ -558,9 +550,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);
Expand Down Expand Up @@ -618,13 +617,24 @@ Connection.prototype.dropCollection = async function dropCollection(collection)
throw new MongooseError('Connection.prototype.dropCollection() no longer accepts a callback');
}

await this._waitForConnect();

return this.db.dropCollection(collection);
};

/**
* Waits for connection to be established, so the connection has a `client`
*
* @return Promise
* @api private
*/

Connection.prototype._waitForConnect = async function _waitForConnect() {
if ((this.readyState === STATES.connecting || this.readyState === STATES.disconnected) && this._shouldBufferCommands()) {
await new Promise(resolve => {
this._queue.push({ fn: resolve });
});
}

return this.db.dropCollection(collection);
};

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

Connection.prototype.listCollections = async function listCollections() {
if ((this.readyState === STATES.connecting || this.readyState === STATES.disconnected) && this._shouldBufferCommands()) {
await new Promise(resolve => {
this._queue.push({ fn: resolve });
});
}
await this._waitForConnect();

const cursor = this.db.listCollections();
return await cursor.toArray();
};

/**
* Helper for MongoDB Node driver's `listDatabases()`.
* Returns an object with a `databases` property that contains an
* array of database objects.
*
* #### Example:
* const { databases } = await mongoose.connection.listDatabases();
* databases; // [{ name: 'mongoose_test', sizeOnDisk: 0, empty: false }]
*
* @method listCollections
* @return {Promise<{ databases: Array<{ name: string }> }>}
* @api public
*/

Connection.prototype.listDatabases = async function listDatabases() {
// Implemented in `lib/drivers/node-mongodb-native/connection.js`
throw new MongooseError('listDatabases() not implemented by driver');
};

/**
* Helper for `dropDatabase()`. Deletes the given database, including all
* collections, documents, and indexes.
Expand All @@ -667,11 +692,7 @@ Connection.prototype.dropDatabase = async function dropDatabase() {
throw new MongooseError('Connection.prototype.dropDatabase() no longer accepts a callback');
}

if ((this.readyState === STATES.connecting || this.readyState === STATES.disconnected) && this._shouldBufferCommands()) {
await new Promise(resolve => {
this._queue.push({ fn: resolve });
});
}
await this._waitForConnect();

// If `dropDatabase()` is called, this model's collection will not be
// init-ed. It is sufficiently common to call `dropDatabase()` after
Expand Down
13 changes: 13 additions & 0 deletions lib/drivers/node-mongodb-native/connection.js
Original file line number Diff line number Diff line change
Expand Up @@ -197,6 +197,19 @@ NativeConnection.prototype.doClose = async function doClose(force) {
return this;
};

/**
* Implementation of `listDatabases()` for MongoDB driver
*
* @return Promise
* @api public
*/

NativeConnection.prototype.listDatabases = async function listDatabases() {
await this._waitForConnect();

return await this.db.admin().listDatabases();
};

/*!
* ignore
*/
Expand Down
2 changes: 1 addition & 1 deletion lib/error/browserMissingSchema.js
Original file line number Diff line number Diff line change
Expand Up @@ -4,7 +4,7 @@

'use strict';

const MongooseError = require('./');
const MongooseError = require('./mongooseError');


class MissingSchemaError extends MongooseError {
Expand Down
1 change: 0 additions & 1 deletion lib/error/cast.js
Original file line number Diff line number Diff line change
Expand Up @@ -75,7 +75,6 @@ class CastError extends MongooseError {
* ignore
*/
setModel(model) {
this.model = model;
this.message = formatMessage(model, this.kind, this.value, this.path,
this.messageFormat, this.valueType);
}
Expand Down
2 changes: 1 addition & 1 deletion lib/error/divergentArray.js
Original file line number Diff line number Diff line change
Expand Up @@ -5,7 +5,7 @@

'use strict';

const MongooseError = require('./');
const MongooseError = require('./mongooseError');

class DivergentArrayError extends MongooseError {
/**
Expand Down
2 changes: 1 addition & 1 deletion lib/error/eachAsyncMultiError.js
Original file line number Diff line number Diff line change
Expand Up @@ -4,7 +4,7 @@

'use strict';

const MongooseError = require('./');
const MongooseError = require('./mongooseError');


/**
Expand Down
2 changes: 1 addition & 1 deletion lib/error/invalidSchemaOption.js
Original file line number Diff line number Diff line change
Expand Up @@ -5,7 +5,7 @@

'use strict';

const MongooseError = require('./');
const MongooseError = require('./mongooseError');

class InvalidSchemaOptionError extends MongooseError {
/**
Expand Down
2 changes: 1 addition & 1 deletion lib/error/missingSchema.js
Original file line number Diff line number Diff line change
Expand Up @@ -5,7 +5,7 @@

'use strict';

const MongooseError = require('./');
const MongooseError = require('./mongooseError');

class MissingSchemaError extends MongooseError {
/**
Expand Down
2 changes: 1 addition & 1 deletion lib/error/notFound.js
Original file line number Diff line number Diff line change
Expand Up @@ -4,7 +4,7 @@
* Module dependencies.
*/

const MongooseError = require('./');
const MongooseError = require('./mongooseError');
const util = require('util');

class DocumentNotFoundError extends MongooseError {
Expand Down
2 changes: 1 addition & 1 deletion lib/error/objectExpected.js
Original file line number Diff line number Diff line change
Expand Up @@ -4,7 +4,7 @@

'use strict';

const MongooseError = require('./');
const MongooseError = require('./mongooseError');


class ObjectExpectedError extends MongooseError {
Expand Down
2 changes: 1 addition & 1 deletion lib/error/objectParameter.js
Original file line number Diff line number Diff line change
Expand Up @@ -4,7 +4,7 @@

'use strict';

const MongooseError = require('./');
const MongooseError = require('./mongooseError');

class ObjectParameterError extends MongooseError {
/**
Expand Down
2 changes: 1 addition & 1 deletion lib/error/overwriteModel.js
Original file line number Diff line number Diff line change
Expand Up @@ -5,7 +5,7 @@

'use strict';

const MongooseError = require('./');
const MongooseError = require('./mongooseError');


class OverwriteModelError extends MongooseError {
Expand Down
2 changes: 1 addition & 1 deletion lib/error/parallelSave.js
Original file line number Diff line number Diff line change
Expand Up @@ -4,7 +4,7 @@
* Module dependencies.
*/

const MongooseError = require('./');
const MongooseError = require('./mongooseError');

class ParallelSaveError extends MongooseError {
/**
Expand Down
2 changes: 1 addition & 1 deletion lib/error/strict.js
Original file line number Diff line number Diff line change
Expand Up @@ -4,7 +4,7 @@

'use strict';

const MongooseError = require('./');
const MongooseError = require('./mongooseError');


class StrictModeError extends MongooseError {
Expand Down
2 changes: 1 addition & 1 deletion lib/error/strictPopulate.js
Original file line number Diff line number Diff line change
Expand Up @@ -4,7 +4,7 @@

'use strict';

const MongooseError = require('./');
const MongooseError = require('./mongooseError');

class StrictPopulateError extends MongooseError {
/**
Expand Down

0 comments on commit 3783ed8

Please sign in to comment.