Skip to content

Commit

Permalink
feat: enable setting custom prisma client type for adapter (#126)
Browse files Browse the repository at this point in the history
* enable setting custom prisma client type for adapter

* Add docs on custom prisma client type

* Update prisma adapter test setup
  • Loading branch information
Papooch authored Feb 25, 2024
1 parent bebefb6 commit 88e24be
Show file tree
Hide file tree
Showing 6 changed files with 177 additions and 19 deletions.
Original file line number Diff line number Diff line change
Expand Up @@ -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.
<small>Since `1.1.0`</small>

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<CustomPrismaClient>;
```

This type will need to be used whenever you inject the `TransactionHost` or `Transaction`

```ts
private readonly txHost: TransactionHost<TransactionalAdapterPrisma<CustomPrismaClient>>
```

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
}),
}),
```

:::
Original file line number Diff line number Diff line change
Expand Up @@ -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({
Expand Down
Original file line number Diff line number Diff line change
Expand Up @@ -7,7 +7,7 @@ generator client {

datasource db {
provider = "sqlite"
url = "file:../tmp/test.db"
url = env("DATA_SOURCE_URL")
}

model User {
Expand Down
Original file line number Diff line number Diff line change
@@ -1,13 +1,17 @@
import { TransactionalAdapter } from '@nestjs-cls/transactional';
import { PrismaClient } from '@prisma/client';

export type PrismaTransactionalClient = Parameters<
Parameters<PrismaClient['$transaction']>[0]
>[0];
interface AnyTransactionClient {
$transaction: (fn: (client: any) => Promise<any>, options?: any) => any;
}

export type PrismaTransactionalClient<
TClient extends AnyTransactionClient = PrismaClient,
> = Parameters<Parameters<TClient['$transaction']>[0]>[0];

export type PrismaTransactionOptions = Parameters<
PrismaClient['$transaction']
>[1];
export type PrismaTransactionOptions<
TClient extends AnyTransactionClient = PrismaClient,
> = Parameters<TClient['$transaction']>[1];

export interface PrismaTransactionalAdapterOptions {
/**
Expand All @@ -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<TClient>,
PrismaTransactionOptions<TClient>
>
{
connectionToken: any;
Expand All @@ -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<any>,
setClient: (client?: PrismaTransactionalClient) => void,
setClient: (client?: PrismaTransactionalClient<TClient>) => void,
) => {
return await prisma.$transaction(async (p) => {
setClient(p);
Expand Down
Original file line number Diff line number Diff line change
@@ -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<CustomPrismaClient>
>,
private readonly txHost: TransactionHost<
TransactionalAdapterPrisma<CustomPrismaClient>
>,
) {}

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<TransactionalAdapterPrisma<CustomPrismaClient>>;

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',
});
});
});
});
});
Original file line number Diff line number Diff line change
Expand Up @@ -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(
Expand Down Expand Up @@ -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 () => {
Expand Down

0 comments on commit 88e24be

Please sign in to comment.