Skip to content

Commit eb71d85

Browse files
committed
test(transactional): added test for extra providers in custom transactional adapters
1 parent 467c909 commit eb71d85

File tree

4 files changed

+158
-10
lines changed

4 files changed

+158
-10
lines changed

docs/docs/06_plugins/01_available-plugins/01-transactional/99-creating-custom-adapter.md

Lines changed: 8 additions & 5 deletions
Original file line numberDiff line numberDiff line change
@@ -125,9 +125,11 @@ As seen above, we'll need to define the following types:
125125
While the adapter itself can be any object that implements the `TransactionalAdapter` interface, we'll create a class that implements it.
126126

127127
```ts
128-
export class MyTransactionalAdapterKnex
129-
implements TransactionalAdapter<Knex, Knex, Knex.TransactionConfig>
130-
{
128+
export class MyTransactionalAdapterKnex implements TransactionalAdapter<
129+
Knex,
130+
Knex,
131+
Knex.TransactionConfig
132+
> {
131133
// implement the property for the connection token
132134
connectionToken: any;
133135

@@ -147,6 +149,7 @@ export class MyTransactionalAdapterKnex
147149
) {
148150
this.connectionToken = myKnexInstanceToken;
149151
this.defaultTxOptions = defaultTxOptions;
152+
this.extraProviderTokens = extraProviderTokens;
150153
}
151154

152155
//
@@ -211,8 +214,8 @@ ClsModule.forRoot({
211214
// highlight-start
212215
adapter: new MyTransactionalAdapterKnex({
213216
connectionToken: KNEX_TOKEN,
214-
defaultTxOptions: { isolationLevel: 'serializable' }
215-
extraProviderTokens: [SomeExtraProvider]
217+
defaultTxOptions: { isolationLevel: 'serializable' },
218+
extraProviderTokens: [SomeExtraProvider],
216219
}),
217220
// highlight-end
218221
}),

packages/transactional/src/lib/interfaces.ts

Lines changed: 3 additions & 2 deletions
Original file line numberDiff line numberDiff line change
@@ -33,7 +33,7 @@ export interface MergedTransactionalAdapterOptions<
3333

3434
export type TransactionalOptionsAdapterFactory<TConnection, TTx, TOptions> = (
3535
connection: TConnection,
36-
extraProviders?: any[],
36+
extraProviders: any[],
3737
) => TransactionalAdapterOptions<TTx, TOptions>;
3838

3939
export type OptionalLifecycleHooks = Partial<
@@ -82,7 +82,8 @@ export interface TransactionalAdapter<
8282
/**
8383
* An array of extra tokens to be provided to the adapter.
8484
*
85-
* To inject custom providers to the adapter
85+
* To inject custom providers to a transactional adapter
86+
* which can be used to perform additional logic within the transactions
8687
8788
*/
8889
extraProviderTokens?: any[];

packages/transactional/src/lib/plugin-transactional.ts

Lines changed: 9 additions & 3 deletions
Original file line numberDiff line numberDiff line change
@@ -35,12 +35,18 @@ export class ClsPluginTransactional extends ClsPluginBase {
3535
},
3636
{
3737
provide: TRANSACTIONAL_ADAPTER_OPTIONS,
38-
inject: [TRANSACTION_CONNECTION],
38+
inject: [
39+
TRANSACTION_CONNECTION,
40+
...(options.adapter.extraProviderTokens ?? []),
41+
],
3942
useFactory: (
4043
connection: any,
44+
...extraProviders: any[]
4145
): MergedTransactionalAdapterOptions<any, any> => {
42-
const adapterOptions =
43-
options.adapter.optionsFactory(connection);
46+
const adapterOptions = options.adapter.optionsFactory(
47+
connection,
48+
extraProviders,
49+
);
4450
return {
4551
...adapterOptions,
4652
...this.bindLifecycleHooks(options),
Lines changed: 138 additions & 0 deletions
Original file line numberDiff line numberDiff line change
@@ -0,0 +1,138 @@
1+
import { ClsModule } from 'nestjs-cls';
2+
import {
3+
ClsPluginTransactional,
4+
TransactionalAdapter,
5+
TransactionHost,
6+
} from '../src';
7+
import { Injectable, Module } from '@nestjs/common';
8+
import { Test, TestingModule } from '@nestjs/testing';
9+
10+
const MOCK_TOKEN = 'MOCK_TOKEN';
11+
// --- EXTRA SERVICE AND MODULE ---
12+
@Injectable()
13+
export class MyProviderService {
14+
private value: string = 'initial value';
15+
16+
getValue() {
17+
return this.value;
18+
}
19+
setValue(newValue: string) {
20+
this.value = newValue;
21+
}
22+
}
23+
24+
@Module({
25+
providers: [MyProviderService],
26+
exports: [MyProviderService],
27+
})
28+
export class MyProviderModule {}
29+
30+
// --- CUSTOM TRANSACTIONAL ADAPTER ---
31+
interface CustomTransactionalAdapterOptions {
32+
txToken: any;
33+
defaultTxOptions?: any;
34+
extraProviderTokens?: any[];
35+
}
36+
37+
export class MyCustomTransactionalAdapter implements TransactionalAdapter<
38+
any,
39+
any,
40+
any
41+
> {
42+
connectionToken: any;
43+
defaultTxOptions?: any;
44+
extraProviderTokens?: any[];
45+
46+
constructor(options: CustomTransactionalAdapterOptions) {
47+
this.connectionToken = options.txToken;
48+
this.defaultTxOptions = options.defaultTxOptions;
49+
this.extraProviderTokens = options.extraProviderTokens;
50+
}
51+
52+
optionsFactory(
53+
_connection: any,
54+
[extraProviders]: [MyProviderService] | any[],
55+
) {
56+
return {
57+
wrapWithTransaction: async (
58+
_options: any,
59+
fn: (...args: any[]) => Promise<any>,
60+
_setTx: (tx?: any) => void,
61+
) => {
62+
const result = await fn();
63+
console.log('Accessing Extra Provider instance');
64+
extraProviders.setValue('after test');
65+
return result;
66+
},
67+
getFallbackInstance: () => null,
68+
};
69+
}
70+
}
71+
72+
// --- APPLICATION LOGIC (REPO & SERVICE) ---
73+
@Injectable()
74+
class CallingService {
75+
constructor(
76+
private readonly txHost: TransactionHost<MyCustomTransactionalAdapter>,
77+
) {}
78+
79+
async mockTransaction() {
80+
console.log('Before Transaction');
81+
await this.txHost.withTransaction(async () => {
82+
console.log('In Transaction');
83+
});
84+
}
85+
}
86+
87+
@Module({
88+
providers: [
89+
{
90+
provide: MOCK_TOKEN,
91+
useValue: 'MOCK_TOKEN',
92+
},
93+
],
94+
exports: [MOCK_TOKEN],
95+
})
96+
export class MockModule {}
97+
98+
@Module({
99+
imports: [
100+
MockModule,
101+
MyProviderModule,
102+
ClsModule.forRoot({
103+
plugins: [
104+
new ClsPluginTransactional({
105+
imports: [MockModule, MyProviderModule],
106+
adapter: new MyCustomTransactionalAdapter({
107+
txToken: MOCK_TOKEN,
108+
defaultTxOptions: {},
109+
extraProviderTokens: [MyProviderService],
110+
}),
111+
}),
112+
],
113+
}),
114+
],
115+
providers: [CallingService],
116+
})
117+
class AppModule {}
118+
119+
// --- TEST SUITE ---
120+
describe('Custom Transactional Adapter With Custom Provider', () => {
121+
let module: TestingModule;
122+
let customProviderService: MyProviderService;
123+
let callingService: CallingService;
124+
beforeEach(async () => {
125+
module = await Test.createTestingModule({
126+
imports: [AppModule],
127+
}).compile();
128+
await module.init();
129+
customProviderService = module.get(MyProviderService);
130+
callingService = module.get(CallingService);
131+
customProviderService.setValue('before test');
132+
});
133+
134+
it('should use the injected custom provider instance and return the updated value from the transaction method', async () => {
135+
await callingService.mockTransaction();
136+
expect(customProviderService.getValue()).toBe('after test');
137+
});
138+
});

0 commit comments

Comments
 (0)