Skip to content

Commit

Permalink
feat(transactional-adapter-mongodb): add mongodb adapter (#161)
Browse files Browse the repository at this point in the history
* feat(transactional-adapter-mongodb): add `mongodb` adapter (#158)

* docs(transactional-adapter-mongodb): add docs
  • Loading branch information
Papooch authored Jul 1, 2024
1 parent 9542cdb commit 90f2df4
Show file tree
Hide file tree
Showing 11 changed files with 1,039 additions and 5 deletions.
Original file line number Diff line number Diff line change
@@ -0,0 +1,126 @@
import Tabs from '@theme/Tabs';
import TabItem from '@theme/TabItem';

# MongoDB adapter

## Installation

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

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

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

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

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

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

</TabItem>
</Tabs>

## Registration

```ts
ClsModule.forRoot({
plugins: [
new ClsPluginTransactional({
imports: [
// module in which the MongoClient instance is provided
MongoDBModule,
],
adapter: new TransactionalAdapterMongoDB({
// the injection token of the MongoClient
mongoClientToken: MONGO_CLIENT,
}),
}),
],
});
```

## Typing & usage

To work correctly, the adapter needs to inject an instance of [`MongoClient`](https://mongodb.github.io/node-mongodb-native/6.7/classes/MongoClient.html)

Due to how [transactions work in MongoDB](https://www.mongodb.com/docs/drivers/node/current/fundamentals/transactions), the usage of the `MongoDBAdapter` adapter is a bit different from the others.

The `tx` property on the `TransactionHost<TransactionalAdapterMongoDB>` 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 with an active transaction, or `undefined` when no transaction is active.

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

:::important

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

::::

## Example

```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(r1.id);
// highlight-end
assert(foundUser.id === user.id);
}
}
```

```ts title="user.repository.ts"
@Injectable()
class UserRepository {
constructor(
@Inject(MONGO_CLIENT)
private readonly mongoClient: MongoClient, // use a regular mongoClient here
private readonly txHost: TransactionHost<TransactionalAdapterMongoDB>,
) {}

async getUserById(id: ObjectId) {
// txHost.tx is typed as Knex
return this.mongoClient.db('default').collection('user').findOne(
{ _id: id },
// highlight-start
{ session: this.txHost.tx }, // here, the `tx` is passed as the `session`
// highlight-end
);
}

async createUser(name: string) {
const created = await this.mongo
.db('default')
.collection('user')
.insertOne(
{ name: name, email: `${name}@email.com` },
// highlight-start
{ session: this.txHost.tx }, // here, the `tx` is passed as the `session`
// highlight-end
);
const createdId = created.insertedId;
const createdUser = await this.getUserById(createdId);
return createdUser;
}
}
```

:::note

Queries don't have to be run using the "raw" `MongoClient`. You can as well use collection aliases and whatnot. What is important, is that they all reference the same underlying `MongoClient` instance, and that you pass the `tx` as the `session` option to the query.

:::
Original file line number Diff line number Diff line change
Expand Up @@ -45,6 +45,7 @@ Adapters for the following libraries are available:
- Kysely (see [@nestjs-cls/transactional-adapter-knex](./03-kysely-adapter.md))
- Pg-promise (see [@nestjs-cls/transactional-adapter-pg-promise](./04-pg-promise-adapter.md))
- TypeORM (see [@nestjs-cls/transactional-adapter-typeorm](./05-typeorm-adapter.md))
- MongoDB (see [@nestjs-cls/transactional-adapter-mongodb](./06-mongodb-adapter.md))

Adapters _will not_ be implemented for the following libraries (unless there is a serious demand):

Expand Down
Original file line number Diff line number Diff line change
@@ -0,0 +1,5 @@
# @nestjs-cls/transactional-adapter-knex

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

### ➡️ [Go to the documentation website](https://papooch.github.io/nestjs-cls/plugins/available-plugins/transactional/mongodb-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-mongodb",
"version": "1.0.0",
"description": "A mongodb 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",
"mongodb"
],
"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",
"mongodb": "> 6",
"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": "^6.7.0",
"mongodb-memory-server": "^9.4.0",
"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-mongodb';
Original file line number Diff line number Diff line change
@@ -0,0 +1,69 @@
import { TransactionalAdapter } from '@nestjs-cls/transactional';

import {
ClientSession,
ClientSessionOptions,
MongoClient,
TransactionOptions,
} from 'mongodb';

export type MongoDBTransactionOptions = TransactionOptions & {
/**
* Options for the encompassing `session` that hosts the transaction.
*/
sessionOptions?: ClientSessionOptions;
};

export interface MongoDBTransactionalAdapterOptions {
/**
* The injection token for the MongoClient instance.
*/
mongoClientToken: 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<MongoDBTransactionOptions>;
}

export class TransactionalAdapterMongoDB
implements
TransactionalAdapter<
MongoClient,
ClientSession | undefined,
MongoDBTransactionOptions
>
{
connectionToken: any;

defaultTxOptions?: Partial<TransactionOptions>;

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

/** cannot proxy an `undefined` value */
supportsTransactionProxy = false;

optionsFactory(mongoClient: MongoClient) {
return {
wrapWithTransaction: async (
options: MongoDBTransactionOptions,
fn: (...args: any[]) => Promise<any>,
setTx: (tx?: ClientSession) => void,
) => {
return mongoClient.withSession(
options.sessionOptions ?? {},
async (session) =>
session.withTransaction(() => {
setTx(session);
return fn();
}, options),
);
},
getFallbackInstance: () => undefined,
};
}
}
Loading

0 comments on commit 90f2df4

Please sign in to comment.