@myfunc/prisma-transactional
Version:
Decorator that wraps all prisma queries along the whole call stack to a single transaction.
160 lines (129 loc) • 4.69 kB
Markdown
# @myfunc/prisma-transactional
Package contains @PrismaTransactional decorator that wraps all prisma queries along **the whole call stack** to a single transaction. In case of overlapping several transactions they will be merged.
**Use in production at your own risk.**
A decorator is being actively used on production environment with no issues, but I strictly recommend to wait for a stable release.
### Installation
```bash
npm i @myfunc/prisma-transactional
```
### Universal setup in node.js application
```typescript
import { PrismaClient } from '@prisma/client';
import { patchPrismaTx } from '@myfunc/prisma-transactional';
const prisma = patchPrismaTx(new PrismaClient());
```
### How to setup in NestJS application
Patch your PrismaClient with `patchPrismaTx(client, config)`
```typescript
import { patchPrismaTx } from '@myfunc/prisma-transactional';
import { Injectable, OnModuleInit } from '@nestjs/common';
import { PrismaClient } from '@prisma/client';
@Injectable()
export class PrismaService extends PrismaClient implements OnModuleInit {
constructor() {
super();
return patchPrismaTx(this);
}
async onModuleInit() {
await this.$connect();
}
}
```
Now you can use `PrismaTransactional`.
### Run example
In [Example application](./examples/express/index.ts) described all possible decorator's use cases.
For running example app, please edit DB connection string in the .env file.
```bash
npm i
npm run dev
```
### How to use the decorator
You can add decorator to any class-method. All queries inside will be wrapped in a single transaction.
On any unhandled error all changed will be rolled back.
**BE CAREFUL when using it, all queries inside transaction will be isolated and can lead to deadlock.**
Example
```typescript
export class BalanceService {
constructor(private prisma: PrismaService) {}
// Now all queries (including nested queries in methods) will be executed in transaction
@PrismaTransactional()
async addPoints(userId: string, amount: number) {
const { balance } = await this.getBalance(userId);
const newBalance = await this.prisma.user.update({
select: {
balance: true,
},
where: { id: userId },
data: { balance: roundBalance(balance + amount) },
});
return {
newBalance
};
}
async getBalance(userId: string) {
// Query will be wrapped in transaction if called by addPoints() method.
const { balance } = await this.prisma.user.findUnique({
where: {
id: userId
},
select: {
balance: true
}
});
return balance;
}
}
```
To handle success commit you can put the following code anywhere in the code. If there is no transaction, a callback will be executed immediately.
```typescript
PrismaTransactional.onSuccess(() => {
this.notifyBalanceUpdated(balance!, args._notificationDelay);
});
```
Also, you can add many callbacks. All callbacks are stored in a stack under the hood.
You can execute all in transaction with no decorator.
```typescript
await PrismaTransactional.execute(async () => {
await this.prisma.users.findMany({});
await this.prisma.users.deleteMany({});
});
```
Or return a result from a success execution.
```typescript
const result = await PrismaTransactional.execute(async () => {
const result = await this.prisma.users.findMany({});
await this.prisma.users.deleteMany({});
return result;
});
```
Execute a query out of current transaction context.
```typescript
@PrismaTransactional()
async addPoints(userId: string, amount: number) {
const { balance } = await this.getBalance(userId);
// userLog item will be created even if current transaction will be rolled back.
await PrismaTransactional.prismaRoot.userLog.create(
{
note: `Attempt to add balance for user ${userId} with balance ${balance}`
}
);
const newBalance = await this.prisma.user.update({
select: {
balance: true,
},
where: { id: userId },
data: { balance: roundBalance(balance + amount) },
});
return {
newBalance
};
}
```
## Plans
- [x] Add `PrismaTransactional.prismaRoot` method for running queries out of transaction context.
- [ ] Add tests.
- [x] Add express.js example.
- [ ] Add nestjs example.
- [ ] Get rid of hardcoded values and make them configurable. "TRANSACTION_TIMEOUT"
- [ ] Safety improvements. As an idea - implement ESLint rule for nested prisma queries that might be unintentionally executed in transaction. That means a developer will be aknowledged about possible transaction wrapping and force him to add an eslint-ignore comment.
- [ ] Clean code.