Skip to content
New issue

Have a question about this project? Sign up for a free GitHub account to open an issue and contact its maintainers and the community.

By clicking “Sign up for GitHub”, you agree to our terms of service and privacy statement. We’ll occasionally send you account related emails.

Already on GitHub? Sign in to your account

feat(transactional-adapter-mongoose): add mongoose adapter #159

Merged
merged 5 commits into from
Jul 1, 2024
Merged
Show file tree
Hide file tree
Changes from all commits
Commits
File filter

Filter by extension

Filter by extension


Conversations
Failed to load comments.
Loading
Jump to
Jump to file
Failed to load files.
Loading
Diff view
Diff view
Original file line number Diff line number Diff line change
@@ -0,0 +1,132 @@
import Tabs from '@theme/Tabs';
import TabItem from '@theme/TabItem';

# Mongoose adapter

## Installation

<Tabs>
<TabItem value="npm" label="npm" default>

```bash
npm install @nestjs-cls/transactional-adapter-mongoose
```

</TabItem>
<TabItem value="yarn" label="yarn">

```bash
yarn add @nestjs-cls/transactional-adapter-mongoose
```

</TabItem>
<TabItem value="pnpm" label="pnpm">

```bash
pnpm add @nestjs-cls/transactional-adapter-mongoose
```

</TabItem>
</Tabs>

## Registration

```ts
ClsModule.forRoot({
plugins: [
new ClsPluginTransactional({
imports: [
// module in which the Connection instance is provided
MongooseModule,
],
adapter: new TransactionalAdapterMongoose({
// the injection token of the mongoose Connection
mongooseConnectionToken: Connection,
}),
}),
],
});
```

## Typing & usage

To work correctly, the adapter needs to inject an instance of mongoose [`Connection`](<https://mongoosejs.com/docs/api/connection.html#Connection()>).

Due to how transactions work in MongoDB, [and in turn in Mongoose](https://mongoosejs.com/docs/transactions.html), the usage of the Mongoose adapter is a bit different from the others.

The `tx` property on the `TransactionHost<TransactionalAdapterMongoose>` does _not_ refer to any _transactional_ instance, but rather to a [`ClientSession`](https://mongodb.github.io/node-mongodb-native/6.7/classes/ClientSession.html) instance of `mongodb`, with an active transaction, or `null` when no transaction is active.

Queries are not executed using the `ClientSession` instance, but instead the `ClientSession` instance or `null` is passed to the query as the `session` option.

:::important

The `TransactionalAdapterMongoose` _does not support_ the ["Transaction Proxy"](./index.md#using-the-injecttransaction-decorator) feature, because proxying a `null` value is not supported by the JavaScript Proxy.

::::

## Example

```ts title="database.schemas.ts"
const userSchema = new Schema({
name: String,
email: String,
});

const User = mongoose.model('user', userSchema);
```

```ts title="user.service.ts"
@Injectable()
class UserService {
constructor(private readonly userRepository: UserRepository) {}

@Transactional()
async runTransaction() {
// highlight-start
// both methods are executed in the same transaction
const user = await this.userRepository.createUser('John');
const foundUser = await this.userRepository.getUserById(user._id);
// highlight-end
assert(foundUser._id === user._id);
}
}
```

```ts title="user.repository.ts"
@Injectable()
class UserRepository {
constructor(
private readonly txHost: TransactionHost<TransactionalAdapterMongoose>,
) {}

async getUserById(id: ObjectId) {
// txHost.tx is typed as ClientSession
return await User.findById(id)
// highlight-start
.session(this.txHost.tx);
// highlight-end
}

async createUser(name: string) {
const user = new User({ name: name, email: `${name}@email.com` });
await user
// highlight-start
.save({ session: this.txHost.tx });
// highlight-end
return user;
}
}
```

## Considerations

### Using with built-in Mongoose AsyncLocalStorage support

Mongoose > 8.4 has a built-in support for [propagating the `session` via `AsyncLocalStorage`](https://mongoosejs.com/docs/transactions.html#asynclocalstorage).

The feature is compatible with `@nestjs-cls/transactional` and when enabled, one _does not_ have to pass `TransactionHost#tx` to queries and still enjoy the simplicity of the `@Transactional` decorator, which starts and ends the underlying transaction automatically.

**However**, because `@nestjs-cls/transactional` has no control over the propagation of the `session` instance via Mongoose's `AsyncLocalStorage`,
there is **no implicit support for opting out of an ongoing transaction** via `TransactionHost#withoutTransaction` (or analogously the [`Propagation.NotSupported`](./index.md#transaction-propagation) mode).

To opt out of an ongoing transaction, you have to explicitly pass `null` to the `session` option when calling the query. Alternatively, you can explicitly pass in the value of `TransactionHost#tx` if the query should support both transactional and non-transactional mode and you want to control it using `Propagation`.
Original file line number Diff line number Diff line change
@@ -0,0 +1,5 @@
# @nestjs-cls/transactional-adapter-knex

Mongoose adapter for the `@nestjs-cls/transactional` plugin.

### ➡️ [Go to the documentation website](https://papooch.github.io/nestjs-cls/plugins/available-plugins/transactional/mongoose-adapter) 📖
Original file line number Diff line number Diff line change
@@ -0,0 +1,17 @@
module.exports = {
moduleFileExtensions: ['js', 'json', 'ts'],
rootDir: '.',
testRegex: '.*\\.spec\\.ts$',
transform: {
'^.+\\.ts$': [
'ts-jest',
{
isolatedModules: true,
maxWorkers: 1,
},
],
},
collectCoverageFrom: ['src/**/*.ts'],
coverageDirectory: '../coverage',
testEnvironment: 'node',
};
Original file line number Diff line number Diff line change
@@ -0,0 +1,73 @@
{
"name": "@nestjs-cls/transactional-adapter-mongoose",
"version": "1.0.0",
"description": "A mongoose adapter for @nestjs-cls/transactional",
"author": "papooch",
"license": "MIT",
"engines": {
"node": ">=18"
},
"publishConfig": {
"access": "public"
},
"repository": {
"type": "git",
"url": "git+https://github.com/Papooch/nestjs-cls.git"
},
"homepage": "https://papooch.github.io/nestjs-cls/",
"keywords": [
"nest",
"nestjs",
"cls",
"continuation-local-storage",
"als",
"AsyncLocalStorage",
"async_hooks",
"request context",
"async context",
"transaction",
"transactional",
"transactional decorator",
"aop",
"mongoose"
],
"main": "dist/src/index.js",
"types": "dist/src/index.d.ts",
"files": [
"dist/src/**/!(*.spec).d.ts",
"dist/src/**/!(*.spec).js"
],
"scripts": {
"prepack": "cp ../../../LICENSE ./LICENSE",
"prebuild": "rimraf dist",
"build": "tsc",
"test": "jest",
"test:watch": "jest --watch",
"test:cov": "jest --coverage"
},
"peerDependencies": {
"@nestjs-cls/transactional": "workspace:^2.2.2",
"mongoose": "> 8",
"nestjs-cls": "workspace:^4.3.0"
},
"devDependencies": {
"@nestjs/cli": "^10.0.2",
"@nestjs/common": "^10.3.7",
"@nestjs/core": "^10.3.7",
"@nestjs/testing": "^10.3.7",
"@types/jest": "^28.1.2",
"@types/node": "^18.0.0",
"jest": "^29.7.0",
"mongodb-memory-server": "^9.4.0",
"mongoose": "^8.4.4",
"reflect-metadata": "^0.1.13",
"rimraf": "^3.0.2",
"rxjs": "^7.5.5",
"sqlite3": "^5.1.7",
"ts-jest": "^29.1.2",
"ts-loader": "^9.3.0",
"ts-node": "^10.8.1",
"tsconfig-paths": "^4.0.0",
"typescript": "5.0"
}
}
Original file line number Diff line number Diff line change
@@ -0,0 +1 @@
export * from './lib/transactional-adapter-mongoose';
Original file line number Diff line number Diff line change
@@ -0,0 +1,53 @@
import { TransactionalAdapter } from '@nestjs-cls/transactional';
import { ClientSession, Connection } from 'mongoose';

type MongooseTransactionOptions = Parameters<Connection['transaction']>[1];

export interface MongoDBTransactionalAdapterOptions {
/**
* The injection token for the mongoose Connection instance.
*/
mongooseConnectionToken: any;

/**
* Default options for the transaction. These will be merged with any transaction-specific options
* passed to the `@Transactional` decorator or the `TransactionHost#withTransaction` method.
*/
defaultTxOptions?: Partial<MongooseTransactionOptions>;
}

export class TransactionalAdapterMongoose
implements
TransactionalAdapter<
Connection,
ClientSession | null,
MongooseTransactionOptions
>
{
connectionToken: any;

defaultTxOptions?: Partial<MongooseTransactionOptions>;

constructor(options: MongoDBTransactionalAdapterOptions) {
this.connectionToken = options.mongooseConnectionToken;
this.defaultTxOptions = options.defaultTxOptions;
}

supportsTransactionProxy = false;

optionsFactory(connection: Connection) {
return {
wrapWithTransaction: async (
options: MongooseTransactionOptions,
fn: (...args: any[]) => Promise<any>,
setTx: (tx?: ClientSession) => void,
) => {
return connection.transaction((session) => {
setTx(session);
return fn();
}, options);
},
getFallbackInstance: () => null,
};
}
}
Loading
Loading