From 88e24bed16dfc8ff1c5be764ffb471cc5389cebc Mon Sep 17 00:00:00 2001
From: =?UTF-8?q?Ond=C5=99ej=20=C5=A0vanda?=
<46406259+Papooch@users.noreply.github.com>
Date: Sun, 25 Feb 2024 17:02:29 +0100
Subject: [PATCH] feat: enable setting custom prisma client type for adapter
(#126)
* enable setting custom prisma client type for adapter
* Add docs on custom prisma client type
* Update prisma adapter test setup
---
.../01-transactional/01-prisma-adapter.md | 40 ++++++-
.../01-transactional/index.md | 6 +-
.../prisma/schema.prisma | 2 +-
.../src/lib/transactional-adapter-prisma.ts | 31 +++--
...ional-adapter-prisma-custom-client.spec.ts | 113 ++++++++++++++++++
.../test/transactional-adapter-prisma.spec.ts | 4 +-
6 files changed, 177 insertions(+), 19 deletions(-)
create mode 100644 packages/transactional-adapters/transactional-adapter-prisma/test/transactional-adapter-prisma-custom-client.spec.ts
diff --git a/docs/docs/06_plugins/01_available-plugins/01-transactional/01-prisma-adapter.md b/docs/docs/06_plugins/01_available-plugins/01-transactional/01-prisma-adapter.md
index 36d8f3e3..b553fc21 100644
--- a/docs/docs/06_plugins/01_available-plugins/01-transactional/01-prisma-adapter.md
+++ b/docs/docs/06_plugins/01_available-plugins/01-transactional/01-prisma-adapter.md
@@ -101,6 +101,42 @@ class UserRepository {
}
```
-## Caveats
+## Custom client type
-Since Prisma generates its own client to `node_modules`, this plugin works with the assumption that the types for the client are available as `@prisma/client`. If you have a different setup, you might need to use `declare module '@prisma/client'` to make typescript happy.
+Since `1.1.0`
+
+By default, the adapter assumes that the Prisma client is available as `@prisma/client`. If you have a different setup, or you use some Prisma client _extensions_, you can provide a custom type for the client as a generic parameter of the adapter.
+
+```ts
+TransactionalAdapterPrisma;
+```
+
+This type will need to be used whenever you inject the `TransactionHost` or `Transaction`
+
+```ts
+private readonly txHost: TransactionHost>
+```
+
+Which becomes pretty verbose, so it's recommended to create a custom type alias for the adapter.
+
+:::important
+
+Please make sure you set up the module with the _custom_ prisma client and not the default one,
+otherwise you would get a runtime error.
+
+```ts
+new ClsPluginTransactional({
+ imports: [
+ // module in which the PrismaClient is provided
+ PrismaModule
+ ],
+ adapter: new TransactionalAdapterPrisma({
+ // the injection token of the PrismaClient
+ // highlight-start
+ prismaInjectionToken: CUSTOM_PRISMA_CLIENT_TOKEN,
+ // highlight-end
+ }),
+}),
+```
+
+:::
diff --git a/docs/docs/06_plugins/01_available-plugins/01-transactional/index.md b/docs/docs/06_plugins/01_available-plugins/01-transactional/index.md
index 89e87527..8af8ee5e 100644
--- a/docs/docs/06_plugins/01_available-plugins/01-transactional/index.md
+++ b/docs/docs/06_plugins/01_available-plugins/01-transactional/index.md
@@ -208,9 +208,11 @@ class AccountService {
}
```
-:::note
+:::important
+
+When a transaction is not active, the `Transaction` instance refers to the default non-transactional instance. However, if the CLS context is _not active_, the `Transaction` instance will be `undefined` instead, which could cause runtime errors.
-This feature must be explicitly enabled with the `enableTransactionProxy: true` option of the `ClsPluginTransactional` constructor.
+Therefore, this feature works reliably only when the CLS context is active _prior to starting the transaction_, which should be the case in most cases, however, for that reason, this is an opt-in feature that must be explicitly enabled with the `enableTransactionProxy: true` option of the `ClsPluginTransactional` constructor.
```ts
new ClsPluginTransactional({
diff --git a/packages/transactional-adapters/transactional-adapter-prisma/prisma/schema.prisma b/packages/transactional-adapters/transactional-adapter-prisma/prisma/schema.prisma
index 1a89d845..c6811fe9 100644
--- a/packages/transactional-adapters/transactional-adapter-prisma/prisma/schema.prisma
+++ b/packages/transactional-adapters/transactional-adapter-prisma/prisma/schema.prisma
@@ -7,7 +7,7 @@ generator client {
datasource db {
provider = "sqlite"
- url = "file:../tmp/test.db"
+ url = env("DATA_SOURCE_URL")
}
model User {
diff --git a/packages/transactional-adapters/transactional-adapter-prisma/src/lib/transactional-adapter-prisma.ts b/packages/transactional-adapters/transactional-adapter-prisma/src/lib/transactional-adapter-prisma.ts
index a9419b5a..7de6ddda 100644
--- a/packages/transactional-adapters/transactional-adapter-prisma/src/lib/transactional-adapter-prisma.ts
+++ b/packages/transactional-adapters/transactional-adapter-prisma/src/lib/transactional-adapter-prisma.ts
@@ -1,13 +1,17 @@
import { TransactionalAdapter } from '@nestjs-cls/transactional';
import { PrismaClient } from '@prisma/client';
-export type PrismaTransactionalClient = Parameters<
- Parameters[0]
->[0];
+interface AnyTransactionClient {
+ $transaction: (fn: (client: any) => Promise, options?: any) => any;
+}
+
+export type PrismaTransactionalClient<
+ TClient extends AnyTransactionClient = PrismaClient,
+> = Parameters[0]>[0];
-export type PrismaTransactionOptions = Parameters<
- PrismaClient['$transaction']
->[1];
+export type PrismaTransactionOptions<
+ TClient extends AnyTransactionClient = PrismaClient,
+> = Parameters[1];
export interface PrismaTransactionalAdapterOptions {
/**
@@ -16,12 +20,13 @@ export interface PrismaTransactionalAdapterOptions {
prismaInjectionToken: any;
}
-export class TransactionalAdapterPrisma
- implements
+export class TransactionalAdapterPrisma<
+ TClient extends AnyTransactionClient = PrismaClient,
+> implements
TransactionalAdapter<
- PrismaClient,
- PrismaTransactionalClient,
- PrismaTransactionOptions
+ TClient,
+ PrismaTransactionalClient,
+ PrismaTransactionOptions
>
{
connectionToken: any;
@@ -30,11 +35,11 @@ export class TransactionalAdapterPrisma
this.connectionToken = options.prismaInjectionToken;
}
- optionsFactory = (prisma: PrismaClient) => ({
+ optionsFactory = (prisma: TClient) => ({
wrapWithTransaction: async (
options: PrismaTransactionOptions,
fn: (...args: any[]) => Promise,
- setClient: (client?: PrismaTransactionalClient) => void,
+ setClient: (client?: PrismaTransactionalClient) => void,
) => {
return await prisma.$transaction(async (p) => {
setClient(p);
diff --git a/packages/transactional-adapters/transactional-adapter-prisma/test/transactional-adapter-prisma-custom-client.spec.ts b/packages/transactional-adapters/transactional-adapter-prisma/test/transactional-adapter-prisma-custom-client.spec.ts
new file mode 100644
index 00000000..c15657ff
--- /dev/null
+++ b/packages/transactional-adapters/transactional-adapter-prisma/test/transactional-adapter-prisma-custom-client.spec.ts
@@ -0,0 +1,113 @@
+import {
+ ClsPluginTransactional,
+ InjectTransaction,
+ Transaction,
+ TransactionHost,
+} from '@nestjs-cls/transactional';
+import { Injectable, Module } from '@nestjs/common';
+import { Test, TestingModule } from '@nestjs/testing';
+import { PrismaClient } from '@prisma/client';
+import { execSync } from 'child_process';
+import { ClsModule } from 'nestjs-cls';
+import { TransactionalAdapterPrisma } from '../src';
+
+process.env.DATA_SOURCE_URL = 'file:../tmp/test-custom.db';
+
+const prisma = new PrismaClient();
+const customPrismaClient = prisma.$extends({
+ model: {
+ user: {
+ async createWithEmail(name: string) {
+ return await prisma.user.create({
+ data: { name: name, email: `${name}@email.com` },
+ });
+ },
+ async getById(id: number) {
+ return await prisma.user.findUnique({ where: { id } });
+ },
+ },
+ },
+});
+type CustomPrismaClient = typeof customPrismaClient;
+const CUSTOM_PRISMA_CLIENT = Symbol('CustomPrismaClient');
+
+@Injectable()
+class UserRepository {
+ constructor(
+ @InjectTransaction()
+ private readonly tx: Transaction<
+ TransactionalAdapterPrisma
+ >,
+ private readonly txHost: TransactionHost<
+ TransactionalAdapterPrisma
+ >,
+ ) {}
+
+ async getUserById(id: number) {
+ return this.txHost.tx.user.getById(id);
+ }
+
+ async createUser(name: string) {
+ return this.tx.user.createWithEmail(name);
+ }
+}
+
+@Module({
+ providers: [
+ { provide: CUSTOM_PRISMA_CLIENT, useValue: customPrismaClient },
+ ],
+ exports: [CUSTOM_PRISMA_CLIENT],
+})
+class PrismaModule {}
+
+@Module({
+ imports: [
+ PrismaModule,
+ ClsModule.forRoot({
+ plugins: [
+ new ClsPluginTransactional({
+ imports: [PrismaModule],
+ adapter: new TransactionalAdapterPrisma({
+ prismaInjectionToken: CUSTOM_PRISMA_CLIENT,
+ }),
+ enableTransactionProxy: true,
+ }),
+ ],
+ }),
+ ],
+ providers: [UserRepository],
+})
+class AppModule {}
+
+describe('Transactional', () => {
+ let module: TestingModule;
+ let repository: UserRepository;
+ let txHost: TransactionHost>;
+
+ beforeAll(async () => {
+ execSync('yarn prisma migrate reset --force', { env: process.env });
+ });
+
+ beforeEach(async () => {
+ module = await Test.createTestingModule({
+ imports: [AppModule],
+ }).compile();
+ await module.init();
+ repository = module.get(UserRepository);
+ txHost = module.get(TransactionHost);
+ });
+
+ describe('TransactionalAdapterPrisma - Custom client', () => {
+ it('should work with custom prisma client', async () => {
+ await txHost.withTransaction(async () => {
+ const { id } = await repository.createUser('Carlos');
+ const user = await repository.getUserById(id);
+ expect(user).toEqual({
+ id,
+ name: 'Carlos',
+ email: 'Carlos@email.com',
+ });
+ });
+ });
+ });
+});
diff --git a/packages/transactional-adapters/transactional-adapter-prisma/test/transactional-adapter-prisma.spec.ts b/packages/transactional-adapters/transactional-adapter-prisma/test/transactional-adapter-prisma.spec.ts
index 72b09eaa..17ce4c28 100644
--- a/packages/transactional-adapters/transactional-adapter-prisma/test/transactional-adapter-prisma.spec.ts
+++ b/packages/transactional-adapters/transactional-adapter-prisma/test/transactional-adapter-prisma.spec.ts
@@ -12,6 +12,8 @@ import { execSync } from 'child_process';
import { ClsModule } from 'nestjs-cls';
import { TransactionalAdapterPrisma } from '../src';
+process.env.DATA_SOURCE_URL = 'file:../tmp/test.db';
+
@Injectable()
class UserRepository {
constructor(
@@ -111,7 +113,7 @@ describe('Transactional', () => {
let prisma: PrismaClient;
beforeAll(async () => {
- execSync('yarn prisma migrate reset --force');
+ execSync('yarn prisma migrate reset --force', { env: process.env });
});
beforeEach(async () => {