Can't disconnect from DB while testing and using transactional plugin
Closed this issue · 4 comments
Hello, I'm trying to integrate nestjs-cls
into my project for transactions and faced issue with disconnecting from DB while testing repositories.
There code of tests:
describe(`POSITIVE: UserRepository`, () => {
let repository: IUserRepository;
let prismaService: PrismaService;
beforeEach(async () => {
const module: TestingModule = await Test.createTestingModule({
imports: [
PrismaModule.forRoot({
isGlobal: true,
}),
ClsModule.forRoot({
plugins: [
new ClsPluginTransactional({
imports: [
PrismaModule
],
adapter: new TransactionalAdapterPrisma({
prismaInjectionToken: PrismaService,
}),
}),
],
global: true,
}),
],
providers: [
{
provide: IUserRepository,
useClass: UserRepository,
},
UserMapper,
],
}).compile();
repository = module.get<IUserRepository>(IUserRepository);
prismaService = module.get<PrismaService>(PrismaService);
});
afterEach(async () => {
await prismaService.$disconnect();
});
it(`Should be defined`, () => {
expect(repository).toBeDefined();
});
it(`Should create user`, async () => {
const user = UserEntity.create();
const createdUser = await repository.create(user);
expect(createdUser).toBeDefined();
expect(createdUser.id).toBeDefined();
expect(createdUser.createdAt).toEqual(user.createdAt);
expect(createdUser.updatedAt).toEqual(user.updatedAt);
await prismaService.user.delete({
where: {
id: createdUser.id
}
});
});
// More test cases
});
In repository I've provided both private prisma: PrismaService
and private readonly txHost: TransactionHost<TransactionalAdapterPrisma>
since I need transactions in few methods like creating, updating, but not for searching.
So after tests are done, I'm dropping test DB and there occurs that error Error dropping database: error: database "..." is being accessed by other users
and I can't finish testing properly.
Hi, thanks for the report.
There should be nothing preventing the use of txHost
along with the original PrismaService
(although you can also use txHost.withoutTransaction
if you need to run a piece of code outside of the current transaction).
The issue sounds more akin to forgetting an await
somewhere.
Would you be able to provide a minimal reproduction of the issue that I can run on my side? The test that you shared doesn't really tell the whole story and I don't know why it shouldn't work.
Sure, need time for creating reproduction so I'll do it in next few days
Hello, there reproduction goes and in Readme I've provided details how to prepare for reproduction
Hi, thank you for the repository, I was able to reproduce it locally, and find the issue.
Investigation
Initially, I thought the issue was caused by the 3rd party library nestjs-prisma
, because when I removed it and replaced with setting up Prisma directly according to the NestJS docs, the issue was gone.
Then, I reverted back and thought that maybe the PrismaService
was being instantiated twice, so I put a simple console.log
into the constructor of the nestjs-prisma
library's PrismaService
(by editing the compiled javasript file in node_modules
) and sure enough, two logs appeared in the output.
The problem
That led me to finally notice the issue with your setup, where you actually register PrismaModule
twice.
imports: [
PrismaModule.forRoot({ // registration #1 - dynamic version
isGlobal: true,
}),
ClsModule.forRoot({
plugins: [
new ClsPluginTransactional({
imports: [
PrismaModule // registration #2 - static version
],
adapter: new TransactionalAdapterPrisma({
prismaInjectionToken: PrismaService,
}),
}),
],
global: true,
}),
],
The problem is that the first dynamic registration creates a new PrismaModule
instance, but so does the second static registration - and because those are two different instances of the module, each provides their own version of PrismaModule
- but the static one is only available within the context of ClsPluginTransactional
, so you never call $disconnect
on it in your test suite.
Another issue you would have noticed is that if you provided some more configuration to the forRoot
registration, it would have not been respected within the transactional plugin.
There's a simple fix to this on your side (actually multiple ones):
Potential Fix 1: Don't import PrismaModule
in ClsPluginTransactional
.
Since you already marked PrismaModule.forRoot
as global
, there's no need to additionally import it - the PrismaService is available globally
new ClsPluginTransactional({
- imports: [
- PrismaModule
- ],
adapter: new TransactionalAdapterPrisma({
prismaInjectionToken: PrismaService,
}),
}),
Potential Fix 2: Save the dynamic instance to a variable and reuse that
To refer to an instance of a dynamic module, one has to save it into a variable first. If you don't want to make the dynamic instance global, you can do this instead:
+ const prismaModuleInstance = PrismaModule.forRoot({ /* config */ });
const module: TestingModule = await Test.createTestingModule({
imports: [
- PrismaModule.forRoot({
- isGlobal: true,
- }),
+ prismaModuleInstance
ClsModule.forRoot({
plugins: [
new ClsPluginTransactional({
imports: [
- PrismaModule
+ prismaModuleInstance
],
adapter: new TransactionalAdapterPrisma({
prismaInjectionToken: PrismaService,
}),
}),
],
global: true,
}),
],
providers: [
{
provide: IUserRepository,
useClass: UserRepository,
},
UserMapper,
],
}).compile();
Potetial fix 3: Use a wrapper module
Dynamic modules that need to be configured and re-used can be wrapped in a static module and re-exported via Module re-exporting feature (bottom of the page)
+ @Module({
+ imports: [PrismaModule.forRoot({/* config */})],
+ exports: [PrismaModule] // module re-exporting
+ })
+ class ConifiguredPrismaModule {}
const module: TestingModule = await Test.createTestingModule({
imports: [
- PrismaModule.forRoot({
- isGlobal: true,
- }),
+ ConifiguredPrismaModule
ClsModule.forRoot({
plugins: [
new ClsPluginTransactional({
imports: [
- PrismaModule
+ ConifiguredPrismaModule
],
adapter: new TransactionalAdapterPrisma({
prismaInjectionToken: PrismaService,
}),
}),
],
global: true,
}),
],
providers: [
{
provide: IUserRepository,
useClass: UserRepository,
},
UserMapper,
],
}).compile();
Conclusion
The issue is not caused by @nestjs-cls/transactional
, but by an incorrect use of a 3rd party nestjs-prisma
module.
If you have any further questions, feel free to continue this thread, but I'm closing it as completed.