Skip to content

Commit a4eb3da

Browse files
authored
feat: add @nestjs-cls/transactional-adapter-kysely (#112)
1 parent a256fa9 commit a4eb3da

File tree

13 files changed

+661
-5
lines changed

13 files changed

+661
-5
lines changed
Original file line numberDiff line numberDiff line change
@@ -0,0 +1,143 @@
1+
import Tabs from '@theme/Tabs';
2+
import TabItem from '@theme/TabItem';
3+
4+
# Kysely adapter
5+
6+
## Installation
7+
8+
<Tabs>
9+
<TabItem value="npm" label="npm" default>
10+
11+
```bash
12+
npm install @nestjs-cls/transactional-adapter-kysely
13+
```
14+
15+
</TabItem>
16+
<TabItem value="yarn" label="yarn">
17+
18+
```bash
19+
yarn add @nestjs-cls/transactional-adapter-kysely
20+
```
21+
22+
</TabItem>
23+
<TabItem value="pnpm" label="pnpm">
24+
25+
```bash
26+
pnpm add @nestjs-cls/transactional-adapter-kysely
27+
```
28+
29+
</TabItem>
30+
</Tabs>
31+
32+
## Registration
33+
34+
```ts
35+
ClsModule.forRoot({
36+
plugins: [
37+
new ClsPluginTransactional({
38+
imports: [
39+
// module in which the Kysely is provided
40+
KyselyModule
41+
],
42+
adapter: new TransactionalAdapterKysely({
43+
// the injection token of the Kysely client
44+
kyselyInstanceToken: KYSELY,
45+
}),
46+
}),
47+
],
48+
}),
49+
```
50+
51+
## Typing & usage
52+
53+
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:
54+
55+
```ts
56+
constructor(
57+
private readonly txHost: TransactionHost<
58+
TransactionalAdapterKysely<Database>
59+
>,
60+
) {}
61+
```
62+
63+
:::tip
64+
65+
This may get a bit too verbose, so you you might want to create a type alias for it:
66+
67+
```ts
68+
type MyKyselyAdapter = TransactionalAdapterKysely<Database>;
69+
```
70+
71+
and then inject it with
72+
73+
```ts
74+
constructor(
75+
private readonly txHost: TransactionHost<MyKyselyAdapter>,
76+
) {}
77+
```
78+
79+
:::
80+
81+
## Example
82+
83+
```ts title="database.type.ts"
84+
interface Database {
85+
user: User;
86+
}
87+
88+
interface User {
89+
id: Generated<number>;
90+
name: string;
91+
email: string;
92+
}
93+
```
94+
95+
```ts title="user.service.ts"
96+
@Injectable()
97+
class UserService {
98+
constructor(private readonly userRepository: UserRepository) {}
99+
100+
@Transactional()
101+
async runTransaction() {
102+
// highlight-start
103+
// both methods are executed in the same transaction
104+
const user = await this.userRepository.createUser('John');
105+
const foundUser = await this.userRepository.getUserById(r1.id);
106+
// highlight-end
107+
assert(foundUser.id === user.id);
108+
}
109+
}
110+
```
111+
112+
```ts title="user.repository.ts"
113+
@Injectable()
114+
class UserRepository {
115+
constructor(
116+
private readonly txHost: TransactionHost<
117+
TransactionalAdapterKysely<Database>
118+
>,
119+
) {}
120+
121+
async getUserById(id: number) {
122+
// highlight-start
123+
// txHost.tx is typed as Kysely<Database>
124+
return this.txHost.tx
125+
.selectFrom('user')
126+
.where('id', '=', id)
127+
.selectAll()
128+
.executeTakeFirst();
129+
// highlight-end
130+
}
131+
132+
async createUser(name: string) {
133+
return this.txHost.tx
134+
.insertInto('user')
135+
.values({
136+
name: name,
137+
email: `${name}@email.com`,
138+
})
139+
.returningAll()
140+
.executeTakeFirstOrThrow();
141+
}
142+
}
143+
```

docs/docs/06_plugins/01_available-plugins/01-transactional/03-pg-promise-adapter.md docs/docs/06_plugins/01_available-plugins/01-transactional/04-pg-promise-adapter.md

+1-1
Original file line numberDiff line numberDiff line change
@@ -1,7 +1,7 @@
11
import Tabs from '@theme/Tabs';
22
import TabItem from '@theme/TabItem';
33

4-
# pg-promise adapter
4+
# Pg-promise adapter
55

66
## Installation
77

docs/docs/06_plugins/01_available-plugins/01-transactional/index.md

+2-1
Original file line numberDiff line numberDiff line change
@@ -42,7 +42,8 @@ Adapters for the following libraries are available:
4242

4343
- Prisma (see [@nestjs-cls/transactional-adapter-prisma](./01-prisma-adapter.md))
4444
- Knex (see [@nestjs-cls/transactional-adapter-knex](./02-knex-adapter.md))
45-
- pg-promise (see [@nestjs-cls/transactional-adapter-pg-promise](./03-pg-promise-adapter.md))
45+
- Kysely (see [@nestjs-cls/transactional-adapter-knex](./03-kysely-adapter.md))
46+
- Pg-promise (see [@nestjs-cls/transactional-adapter-pg-promise](./04-pg-promise-adapter.md))
4647

4748
Adapters _will not_ be implemented for the following libraries:
4849

Original file line numberDiff line numberDiff line change
@@ -0,0 +1 @@
1+
test.db
Original file line numberDiff line numberDiff line change
@@ -0,0 +1,5 @@
1+
# @nestjs-cls/transactional-adapter-kysely
2+
3+
Kysely adapter for the `@nestjs-cls/transactional` plugin.
4+
5+
### ➡️ [Go to the documentation website](https://papooch.github.io/nestjs-cls/plugins/available-plugins/transactional/kysely-adapter) 📖
Original file line numberDiff line numberDiff line change
@@ -0,0 +1,17 @@
1+
module.exports = {
2+
moduleFileExtensions: ['js', 'json', 'ts'],
3+
rootDir: '.',
4+
testRegex: '.*\\.spec\\.ts$',
5+
transform: {
6+
'^.+\\.ts$': 'ts-jest',
7+
},
8+
collectCoverageFrom: ['src/**/*.ts'],
9+
coverageDirectory: '../coverage',
10+
testEnvironment: 'node',
11+
globals: {
12+
'ts-jest': {
13+
isolatedModules: true,
14+
maxWorkers: 1,
15+
},
16+
},
17+
};
Original file line numberDiff line numberDiff line change
@@ -0,0 +1,74 @@
1+
{
2+
"name": "@nestjs-cls/transactional-adapter-kysely",
3+
"version": "1.0.0",
4+
"description": "A Kysely adapter for @nestjs-cls/transactional",
5+
"author": "papooch",
6+
"license": "MIT",
7+
"engines": {
8+
"node": ">=18"
9+
},
10+
"publishConfig": {
11+
"access": "public"
12+
},
13+
"repository": {
14+
"type": "git",
15+
"url": "git+https://github.com/Papooch/nestjs-cls.git"
16+
},
17+
"homepage": "https://papooch.github.io/nestjs-cls/",
18+
"keywords": [
19+
"nest",
20+
"nestjs",
21+
"cls",
22+
"continuation-local-storage",
23+
"als",
24+
"AsyncLocalStorage",
25+
"async_hooks",
26+
"request context",
27+
"async context",
28+
"transaction",
29+
"transactional",
30+
"transactional decorator",
31+
"aop",
32+
"kysely"
33+
],
34+
"main": "dist/src/index.js",
35+
"types": "dist/src/index.d.ts",
36+
"files": [
37+
"dist/src/**/!(*.spec).d.ts",
38+
"dist/src/**/!(*.spec).js"
39+
],
40+
"scripts": {
41+
"prepack": "cp ../../../LICENSE ./LICENSE",
42+
"prebuild": "rimraf dist",
43+
"build": "tsc",
44+
"test": "jest",
45+
"test:watch": "jest --watch",
46+
"test:cov": "jest --coverage"
47+
},
48+
"peerDependencies": {
49+
"@nestjs-cls/transactional": "workspace:^2.0.0",
50+
"kysely": "^0.27",
51+
"nestjs-cls": "workspace:^4.0.1"
52+
},
53+
"devDependencies": {
54+
"@nestjs/cli": "^10.0.2",
55+
"@nestjs/common": "^10.0.0",
56+
"@nestjs/core": "^10.0.0",
57+
"@nestjs/testing": "^10.0.0",
58+
"@types/better-sqlite3": "^7.6.9",
59+
"@types/jest": "^28.1.2",
60+
"@types/node": "^18.0.0",
61+
"@types/pg": "^8",
62+
"jest": "^28.1.1",
63+
"kysely": "^0.27.2",
64+
"pg": "^8.11.3",
65+
"reflect-metadata": "^0.1.13",
66+
"rimraf": "^3.0.2",
67+
"rxjs": "^7.5.5",
68+
"ts-jest": "^28.0.5",
69+
"ts-loader": "^9.3.0",
70+
"ts-node": "^10.8.1",
71+
"tsconfig-paths": "^4.0.0",
72+
"typescript": "~4.8.0"
73+
}
74+
}
Original file line numberDiff line numberDiff line change
@@ -0,0 +1 @@
1+
export * from './lib/transactional-adapter-kysely';
Original file line numberDiff line numberDiff line change
@@ -0,0 +1,43 @@
1+
import { TransactionalAdapter } from '@nestjs-cls/transactional';
2+
import { Kysely, TransactionBuilder } from 'kysely';
3+
4+
export interface KyselyTransactionalAdapterOptions {
5+
/**
6+
* The injection token for the Kysely instance.
7+
*/
8+
kyselyInstanceToken: any;
9+
}
10+
11+
export interface KyselyTransactionOptions {
12+
isolationLevel?: Parameters<
13+
TransactionBuilder<any>['setIsolationLevel']
14+
>[0];
15+
}
16+
17+
export class TransactionalAdapterKysely<DB = any>
18+
implements TransactionalAdapter<Kysely<DB>, Kysely<DB>, any>
19+
{
20+
connectionToken: any;
21+
22+
constructor(options: KyselyTransactionalAdapterOptions) {
23+
this.connectionToken = options.kyselyInstanceToken;
24+
}
25+
26+
optionsFactory = (kyselyDb: Kysely<DB>) => ({
27+
wrapWithTransaction: async (
28+
options: KyselyTransactionOptions,
29+
fn: (...args: any[]) => Promise<any>,
30+
setClient: (client?: Kysely<DB>) => void,
31+
) => {
32+
const transaction = kyselyDb.transaction();
33+
if (options?.isolationLevel) {
34+
transaction.setIsolationLevel(options.isolationLevel);
35+
}
36+
return transaction.execute(async (trx) => {
37+
setClient(trx);
38+
return fn();
39+
});
40+
},
41+
getFallbackInstance: () => kyselyDb,
42+
});
43+
}
Original file line numberDiff line numberDiff line change
@@ -0,0 +1,14 @@
1+
services:
2+
kysely-test-db:
3+
image: postgres:15
4+
ports:
5+
- 5445:5432
6+
environment:
7+
POSTGRES_USER: postgres
8+
POSTGRES_PASSWORD: postgres
9+
POSTGRES_DB: postgres
10+
healthcheck:
11+
test: ['CMD-SHELL', 'pg_isready -U postgres']
12+
interval: 1s
13+
timeout: 1s
14+
retries: 5

0 commit comments

Comments
 (0)