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 () => {