Skip to content

Commit

Permalink
feat: add @nestjs-cls/transactional-adapter-kysely (#112)
Browse files Browse the repository at this point in the history
  • Loading branch information
Papooch authored Jan 29, 2024
1 parent a256fa9 commit a4eb3da
Show file tree
Hide file tree
Showing 13 changed files with 661 additions and 5 deletions.
Original file line number Diff line number Diff line change
@@ -0,0 +1,143 @@
import Tabs from '@theme/Tabs';
import TabItem from '@theme/TabItem';

# Kysely adapter

## Installation

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

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

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

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

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

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

</TabItem>
</Tabs>

## Registration

```ts
ClsModule.forRoot({
plugins: [
new ClsPluginTransactional({
imports: [
// module in which the Kysely is provided
KyselyModule
],
adapter: new TransactionalAdapterKysely({
// the injection token of the Kysely client
kyselyInstanceToken: KYSELY,
}),
}),
],
}),
```

## Typing & usage

The `tx` property on the `TransactionHost<TransactionalAdapterKysely>` is typed as `Kysely<any>` by default. To get the full typing, you need to supply your database type as the type parameter for the `TransactionalAdapterKysely` when injecting it:

```ts
constructor(
private readonly txHost: TransactionHost<
TransactionalAdapterKysely<Database>
>,
) {}
```

:::tip

This may get a bit too verbose, so you you might want to create a type alias for it:

```ts
type MyKyselyAdapter = TransactionalAdapterKysely<Database>;
```

and then inject it with

```ts
constructor(
private readonly txHost: TransactionHost<MyKyselyAdapter>,
) {}
```

:::

## Example

```ts title="database.type.ts"
interface Database {
user: User;
}

interface User {
id: Generated<number>;
name: string;
email: string;
}
```

```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(
private readonly txHost: TransactionHost<
TransactionalAdapterKysely<Database>
>,
) {}

async getUserById(id: number) {
// highlight-start
// txHost.tx is typed as Kysely<Database>
return this.txHost.tx
.selectFrom('user')
.where('id', '=', id)
.selectAll()
.executeTakeFirst();
// highlight-end
}

async createUser(name: string) {
return this.txHost.tx
.insertInto('user')
.values({
name: name,
email: `${name}@email.com`,
})
.returningAll()
.executeTakeFirstOrThrow();
}
}
```
Original file line number Diff line number Diff line change
@@ -1,7 +1,7 @@
import Tabs from '@theme/Tabs';
import TabItem from '@theme/TabItem';

# pg-promise adapter
# Pg-promise adapter

## Installation

Expand Down
Original file line number Diff line number Diff line change
Expand Up @@ -42,7 +42,8 @@ Adapters for the following libraries are available:

- Prisma (see [@nestjs-cls/transactional-adapter-prisma](./01-prisma-adapter.md))
- Knex (see [@nestjs-cls/transactional-adapter-knex](./02-knex-adapter.md))
- pg-promise (see [@nestjs-cls/transactional-adapter-pg-promise](./03-pg-promise-adapter.md))
- 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))

Adapters _will not_ be implemented for the following libraries:

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

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

### ➡️ [Go to the documentation website](https://papooch.github.io/nestjs-cls/plugins/available-plugins/transactional/kysely-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',
},
collectCoverageFrom: ['src/**/*.ts'],
coverageDirectory: '../coverage',
testEnvironment: 'node',
globals: {
'ts-jest': {
isolatedModules: true,
maxWorkers: 1,
},
},
};
Original file line number Diff line number Diff line change
@@ -0,0 +1,74 @@
{
"name": "@nestjs-cls/transactional-adapter-kysely",
"version": "1.0.0",
"description": "A Kysely 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",
"kysely"
],
"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.0.0",
"kysely": "^0.27",
"nestjs-cls": "workspace:^4.0.1"
},
"devDependencies": {
"@nestjs/cli": "^10.0.2",
"@nestjs/common": "^10.0.0",
"@nestjs/core": "^10.0.0",
"@nestjs/testing": "^10.0.0",
"@types/better-sqlite3": "^7.6.9",
"@types/jest": "^28.1.2",
"@types/node": "^18.0.0",
"@types/pg": "^8",
"jest": "^28.1.1",
"kysely": "^0.27.2",
"pg": "^8.11.3",
"reflect-metadata": "^0.1.13",
"rimraf": "^3.0.2",
"rxjs": "^7.5.5",
"ts-jest": "^28.0.5",
"ts-loader": "^9.3.0",
"ts-node": "^10.8.1",
"tsconfig-paths": "^4.0.0",
"typescript": "~4.8.0"
}
}
Original file line number Diff line number Diff line change
@@ -0,0 +1 @@
export * from './lib/transactional-adapter-kysely';
Original file line number Diff line number Diff line change
@@ -0,0 +1,43 @@
import { TransactionalAdapter } from '@nestjs-cls/transactional';
import { Kysely, TransactionBuilder } from 'kysely';

export interface KyselyTransactionalAdapterOptions {
/**
* The injection token for the Kysely instance.
*/
kyselyInstanceToken: any;
}

export interface KyselyTransactionOptions {
isolationLevel?: Parameters<
TransactionBuilder<any>['setIsolationLevel']
>[0];
}

export class TransactionalAdapterKysely<DB = any>
implements TransactionalAdapter<Kysely<DB>, Kysely<DB>, any>
{
connectionToken: any;

constructor(options: KyselyTransactionalAdapterOptions) {
this.connectionToken = options.kyselyInstanceToken;
}

optionsFactory = (kyselyDb: Kysely<DB>) => ({
wrapWithTransaction: async (
options: KyselyTransactionOptions,
fn: (...args: any[]) => Promise<any>,
setClient: (client?: Kysely<DB>) => void,
) => {
const transaction = kyselyDb.transaction();
if (options?.isolationLevel) {
transaction.setIsolationLevel(options.isolationLevel);
}
return transaction.execute(async (trx) => {
setClient(trx);
return fn();
});
},
getFallbackInstance: () => kyselyDb,
});
}
Original file line number Diff line number Diff line change
@@ -0,0 +1,14 @@
services:
kysely-test-db:
image: postgres:15
ports:
- 5445:5432
environment:
POSTGRES_USER: postgres
POSTGRES_PASSWORD: postgres
POSTGRES_DB: postgres
healthcheck:
test: ['CMD-SHELL', 'pg_isready -U postgres']
interval: 1s
timeout: 1s
retries: 5
Loading

0 comments on commit a4eb3da

Please sign in to comment.