Skip to content

Commit 964bb1a

Browse files
committed
initial commit
1 parent cca7efa commit 964bb1a

File tree

10 files changed

+575
-3
lines changed

10 files changed

+575
-3
lines changed
Original file line numberDiff line numberDiff line change
@@ -0,0 +1,5 @@
1+
# @nestjs-cls/transactional-adapter-pg-promise
2+
3+
`pg-promise` adapter for the `@nestjs-cls/transactional` plugin.
4+
5+
### ➡️ [Go to the documentation website](https://papooch.github.io/nestjs-cls/plugins/available-plugins/transactional) 📖
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,69 @@
1+
{
2+
"name": "@nestjs-cls/transactional-adapter-pg-promise",
3+
"version": "1.1.0",
4+
"description": "A pg-promise adapter for @nestjs-cls/transactional",
5+
"author": "Sam Artuso <samuele.a@gmail.com>",
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+
],
29+
"main": "dist/src/index.js",
30+
"types": "dist/src/index.d.ts",
31+
"files": [
32+
"dist/src/**/!(*.spec).d.ts",
33+
"dist/src/**/!(*.spec).js"
34+
],
35+
"scripts": {
36+
"prepack": "cp ../../../LICENSE ./LICENSE",
37+
"prebuild": "rimraf dist",
38+
"build": "tsc",
39+
"test": "jest",
40+
"test:watch": "jest --watch",
41+
"test:cov": "jest --coverage"
42+
},
43+
"peerDependencies": {
44+
"@nestjs-cls/transactional": "workspace:^1.0.1",
45+
"nestjs-cls": "workspace:^4.0.1",
46+
"pg-promise": "^11"
47+
},
48+
"devDependencies": {
49+
"@nestjs/cli": "^10.0.2",
50+
"@nestjs/common": "^10.0.0",
51+
"@nestjs/core": "^10.0.0",
52+
"@nestjs/testing": "^10.0.0",
53+
"@types/jest": "^28.1.2",
54+
"@types/node": "^18.0.0",
55+
"jest": "^28.1.1",
56+
"pg-promise": "^11",
57+
"reflect-metadata": "^0.1.13",
58+
"rimraf": "^3.0.2",
59+
"rxjs": "^7.5.5",
60+
"ts-jest": "^28.0.5",
61+
"ts-loader": "^9.3.0",
62+
"ts-node": "^10.8.1",
63+
"tsconfig-paths": "^4.0.0",
64+
"typescript": "~4.8.0"
65+
},
66+
"dependencies": {
67+
"install-peers": "^1.0.4"
68+
}
69+
}
Original file line numberDiff line numberDiff line change
@@ -0,0 +1 @@
1+
export * from './lib/transactional-adapter-pg-promise';
Original file line numberDiff line numberDiff line change
@@ -0,0 +1,35 @@
1+
import { TransactionalAdapter } from '@nestjs-cls/transactional';
2+
import { IDatabase, ITask } from 'pg-promise';
3+
4+
export type PgPromiseDbOrTx = IDatabase<unknown> | ITask<unknown>;
5+
6+
export interface PgPromiseTransactionalAdapterOptions {
7+
/**
8+
* The injection token for the pg-promise instance.
9+
*/
10+
pgPromiseInstanceToken: any;
11+
}
12+
13+
export class TransactionalAdapterPgPromise
14+
implements TransactionalAdapter<PgPromiseDbOrTx, PgPromiseDbOrTx, any>
15+
{
16+
connectionToken: any;
17+
18+
constructor(options: PgPromiseTransactionalAdapterOptions) {
19+
this.connectionToken = options.pgPromiseInstanceToken;
20+
}
21+
22+
optionsFactory = (pgPromiseDbOrTxInstance: PgPromiseDbOrTx) => ({
23+
wrapWithTransaction: async (
24+
options: any, // FIXME
25+
fn: (...args: any[]) => Promise<any>,
26+
setClient: (client?: PgPromiseDbOrTx) => void,
27+
) => {
28+
return pgPromiseDbOrTxInstance.tx((tx) => {
29+
setClient(tx);
30+
return fn();
31+
});
32+
},
33+
getFallbackInstance: () => pgPromiseDbOrTxInstance,
34+
});
35+
}
Original file line numberDiff line numberDiff line change
@@ -0,0 +1,14 @@
1+
services:
2+
db:
3+
image: postgres:15
4+
ports:
5+
- 5432: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
Original file line numberDiff line numberDiff line change
@@ -0,0 +1,205 @@
1+
import {
2+
ClsPluginTransactional,
3+
Transactional,
4+
TransactionHost,
5+
} from '@nestjs-cls/transactional';
6+
import { Inject, Injectable, Module } from '@nestjs/common';
7+
import { Test, TestingModule } from '@nestjs/testing';
8+
import { ClsModule } from 'nestjs-cls';
9+
import { PgPromiseDbOrTx, TransactionalAdapterPgPromise } from '../src';
10+
import pgPromise from 'pg-promise';
11+
import { execSync } from 'node:child_process';
12+
13+
const PG_PROMISE = 'PG_PROMISE';
14+
15+
type UserRecord = { id: number; name: string; email: string };
16+
17+
@Injectable()
18+
class UserRepository {
19+
constructor(
20+
private readonly txHost: TransactionHost<TransactionalAdapterPgPromise>,
21+
) {}
22+
23+
async getUserById(id: number) {
24+
return this.txHost.tx.one<UserRecord>(
25+
'SELECT * FROM public.user WHERE id = $1',
26+
[id],
27+
);
28+
}
29+
30+
async createUser(name: string) {
31+
const created = await this.txHost.tx.one<UserRecord>(
32+
'INSERT INTO public.user (name, email) VALUES ($1, $2) RETURNING *',
33+
[name, `${name}@email.com`],
34+
);
35+
return created;
36+
}
37+
}
38+
39+
@Injectable()
40+
class UserService {
41+
constructor(
42+
private readonly userRepository: UserRepository,
43+
private readonly txHost: TransactionHost<TransactionalAdapterPgPromise>,
44+
@Inject(PG_PROMISE)
45+
private readonly db: PgPromiseDbOrTx,
46+
) {}
47+
48+
@Transactional()
49+
async transactionWithDecorator() {
50+
const r1 = await this.userRepository.createUser('John');
51+
const r2 = await this.userRepository.getUserById(r1.id);
52+
return { r1, r2 };
53+
}
54+
55+
@Transactional<TransactionalAdapterPgPromise>({
56+
isolationLevel: 'serializable', // FIXME
57+
})
58+
async transactionWithDecoratorWithOptions() {
59+
const r1 = await this.userRepository.createUser('James');
60+
const r2 = await this.db.oneOrNone<UserRecord>(
61+
'SELECT * FROM public.user WHERE id = $1',
62+
[r1.id],
63+
);
64+
const r3 = await this.userRepository.getUserById(r1.id);
65+
return { r1, r2, r3 };
66+
}
67+
68+
async transactionWithFunctionWrapper() {
69+
return this.txHost.withTransaction(
70+
{
71+
isolationLevel: 'serializable',
72+
},
73+
async () => {
74+
const r1 = await this.userRepository.createUser('Joe');
75+
const r2 = await this.db.oneOrNone<UserRecord>(
76+
'SELECT * FROM public.user WHERE id = $1',
77+
[r1.id],
78+
);
79+
const r3 = await this.userRepository.getUserById(r1.id);
80+
return { r1, r2, r3 };
81+
},
82+
);
83+
}
84+
85+
@Transactional()
86+
async transactionWithDecoratorError() {
87+
await this.userRepository.createUser('Nobody');
88+
throw new Error('Rollback');
89+
}
90+
}
91+
92+
const pgp = pgPromise();
93+
const db = pgp({
94+
host: 'localhost',
95+
user: 'postgres',
96+
password: 'postgres',
97+
database: 'postgres',
98+
});
99+
100+
@Module({
101+
providers: [
102+
{
103+
provide: PG_PROMISE,
104+
useValue: db,
105+
},
106+
],
107+
exports: [PG_PROMISE],
108+
})
109+
class PgPromiseModule {}
110+
111+
@Module({
112+
imports: [
113+
PgPromiseModule,
114+
ClsModule.forRoot({
115+
plugins: [
116+
new ClsPluginTransactional({
117+
imports: [PgPromiseModule],
118+
adapter: new TransactionalAdapterPgPromise({
119+
pgPromiseInstanceToken: PG_PROMISE,
120+
}),
121+
}),
122+
],
123+
}),
124+
],
125+
providers: [UserService, UserRepository],
126+
})
127+
class AppModule {}
128+
129+
describe('Transactional', () => {
130+
let module: TestingModule;
131+
let callingService: UserService;
132+
133+
beforeAll(async () => {
134+
execSync('docker-compose -f test/docker-compose.yml up -d && sleep 2', {
135+
stdio: 'inherit',
136+
});
137+
await db.query('DROP TABLE IF EXISTS public.user');
138+
await db.query(`CREATE TABLE public.user (
139+
id serial NOT NULL,
140+
name varchar NOT NULL,
141+
email varchar NOT NULL,
142+
CONSTRAINT user_pk PRIMARY KEY (id)
143+
);`);
144+
});
145+
146+
beforeEach(async () => {
147+
module = await Test.createTestingModule({
148+
imports: [AppModule],
149+
}).compile();
150+
await module.init();
151+
callingService = module.get(UserService);
152+
});
153+
154+
afterAll(async () => {
155+
pgp.end();
156+
execSync('docker-compose -f test/docker-compose.yml down', {
157+
stdio: 'inherit',
158+
});
159+
});
160+
161+
describe('TransactionalAdapterPgPromise', () => {
162+
it('should run a transaction with the default options with a decorator', async () => {
163+
const { r1, r2 } = await callingService.transactionWithDecorator();
164+
expect(r1).toEqual(r2);
165+
const users = await db.many<UserRecord>(
166+
'SELECT * FROM public.user',
167+
);
168+
expect(users).toEqual(expect.arrayContaining([r1]));
169+
});
170+
171+
it('should run a transaction with the specified options with a decorator', async () => {
172+
const { r1, r2, r3 } =
173+
await callingService.transactionWithDecoratorWithOptions();
174+
expect(r1).toEqual(r3);
175+
expect(r2).toBeNull();
176+
const users = await db.many<UserRecord>(
177+
'SELECT * FROM public.user',
178+
);
179+
expect(users).toEqual(expect.arrayContaining([r1]));
180+
});
181+
182+
it('should run a transaction with the specified options with a function wrapper', async () => {
183+
const { r1, r2, r3 } =
184+
await callingService.transactionWithFunctionWrapper();
185+
expect(r1).toEqual(r3);
186+
expect(r2).toBeNull();
187+
const users = await db.many<UserRecord>(
188+
'SELECT * FROM public.user',
189+
);
190+
expect(users).toEqual(expect.arrayContaining([r1]));
191+
});
192+
193+
it('should rollback a transaction on error', async () => {
194+
await expect(
195+
callingService.transactionWithDecoratorError(),
196+
).rejects.toThrow(new Error('Rollback'));
197+
const users = await db.many<UserRecord>(
198+
'SELECT * FROM public.user',
199+
);
200+
expect(users).toEqual(
201+
expect.not.arrayContaining([{ name: 'Nobody' }]),
202+
);
203+
});
204+
});
205+
});
Original file line numberDiff line numberDiff line change
@@ -0,0 +1,8 @@
1+
{
2+
"extends": "../../../tsconfig.json",
3+
"compilerOptions": {
4+
"outDir": "dist",
5+
"rootDir": "."
6+
},
7+
"include": ["src/**/*.ts", "test/**/*.ts"]
8+
}

tsconfig.json

+4-1
Original file line numberDiff line numberDiff line change
@@ -33,6 +33,9 @@
3333
},
3434
{
3535
"path": "packages/transactional-adapters/transactional-adapter-prisma"
36+
},
37+
{
38+
"path": "packages/transactional-adapters/transactional-adapter-pg-promise"
3639
}
3740
]
38-
}
41+
}

0 commit comments

Comments
 (0)