-
Notifications
You must be signed in to change notification settings - Fork 0
Commit
This commit does not belong to any branch on this repository, and may belong to a fork outside of the repository.
- Loading branch information
0 parents
commit ba30312
Showing
20 changed files
with
783 additions
and
0 deletions.
There are no files selected for viewing
This file contains bidirectional Unicode text that may be interpreted or compiled differently than what appears below. To review, open the file in an editor that reveals hidden Unicode characters.
Learn more about bidirectional Unicode characters
Original file line number | Diff line number | Diff line change |
---|---|---|
@@ -0,0 +1,11 @@ | ||
# Dependency directories | ||
node_modules | ||
|
||
dist | ||
|
||
# dotenv environment variable files | ||
.env | ||
|
||
package-lock.json | ||
*.tgz | ||
dev.db |
This file contains bidirectional Unicode text that may be interpreted or compiled differently than what appears below. To review, open the file in an editor that reveals hidden Unicode characters.
Learn more about bidirectional Unicode characters
Original file line number | Diff line number | Diff line change |
---|---|---|
@@ -0,0 +1,7 @@ | ||
{ | ||
"singleQuote": true, | ||
"trailingComma": "all", | ||
"tabWidth": 2, | ||
"arrayBracketSpacing": true, | ||
"printWidth": 100 | ||
} |
This file contains bidirectional Unicode text that may be interpreted or compiled differently than what appears below. To review, open the file in an editor that reveals hidden Unicode characters.
Learn more about bidirectional Unicode characters
Original file line number | Diff line number | Diff line change |
---|---|---|
@@ -0,0 +1,21 @@ | ||
MIT License | ||
|
||
Copyright (c) 2024 Denys Myronov (myfunc) | ||
|
||
Permission is hereby granted, free of charge, to any person obtaining a copy | ||
of this software and associated documentation files (the "Software"), to deal | ||
in the Software without restriction, including without limitation the rights | ||
to use, copy, modify, merge, publish, distribute, sublicense, and/or sell | ||
copies of the Software, and to permit persons to whom the Software is | ||
furnished to do so, subject to the following conditions: | ||
|
||
The above copyright notice and this permission notice shall be included in all | ||
copies or substantial portions of the Software. | ||
|
||
THE SOFTWARE IS PROVIDED "AS IS", WITHOUT WARRANTY OF ANY KIND, EXPRESS OR | ||
IMPLIED, INCLUDING BUT NOT LIMITED TO THE WARRANTIES OF MERCHANTABILITY, | ||
FITNESS FOR A PARTICULAR PURPOSE AND NONINFRINGEMENT. IN NO EVENT SHALL THE | ||
AUTHORS OR COPYRIGHT HOLDERS BE LIABLE FOR ANY CLAIM, DAMAGES OR OTHER | ||
LIABILITY, WHETHER IN AN ACTION OF CONTRACT, TORT OR OTHERWISE, ARISING FROM, | ||
OUT OF OR IN CONNECTION WITH THE SOFTWARE OR THE USE OR OTHER DEALINGS IN THE | ||
SOFTWARE. |
This file contains bidirectional Unicode text that may be interpreted or compiled differently than what appears below. To review, open the file in an editor that reveals hidden Unicode characters.
Learn more about bidirectional Unicode characters
Original file line number | Diff line number | Diff line change |
---|---|---|
@@ -0,0 +1,108 @@ | ||
# Prisma Transactional | ||
|
||
Package contains @PrismaTransactional decorator that wraps all prisma queries 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. | ||
|
||
|
||
### How to setup in NestJS application | ||
|
||
Install a package | ||
```bash | ||
npm i @myfunc/prisma-transactional | ||
``` | ||
|
||
Patch your PrismaClient with `patchPrismaTx(client, config)` | ||
```tsx | ||
import { patchPrismaTx } from '@myfunc/prisma-transactional'; // Import | ||
import { Injectable, Logger, OnModuleInit } from '@nestjs/common'; | ||
import { PrismaClient } from '@prisma/client'; | ||
|
||
@Injectable() | ||
export class PrismaService extends PrismaClient implements OnModuleInit { | ||
constructor() { | ||
super(); | ||
// Patch and return the substituted version. | ||
return patchPrismaTx(this, { | ||
enableLogging: true, | ||
customLogger: Logger, | ||
}); | ||
} | ||
|
||
async onModuleInit() { | ||
await this.$connect(); | ||
} | ||
} | ||
``` | ||
Now you can use `PrismaTransactional`. | ||
|
||
### Run example | ||
In [Example application](./example/index.ts) described all possible decorator's use cases. | ||
For running example app, please add .env file and provide DB connection string. | ||
|
||
```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 | ||
|
||
```tsx | ||
// Now all queries (including nested queries in methods) will be executed in transaction | ||
@PrismaTransactional() | ||
private 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 | ||
}; | ||
} | ||
``` | ||
|
||
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. | ||
|
||
```tsx | ||
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. | ||
|
||
```tsx | ||
PrismaTransactional.execute(async () => { | ||
await this.prisma.users.findMany({}); | ||
await this.prisma.users.deleteMany({}); | ||
}); | ||
``` | ||
or | ||
```tsx | ||
const result = await PrismaTransactional.execute(async () => { | ||
const result = await this.prisma.users.findMany({}); | ||
await this.prisma.users.deleteMany({}); | ||
return result; | ||
}); | ||
``` | ||
|
||
## Plans | ||
- [ ] Get rid of hardcoded values and make them configurable. "TRANSACTION_TIMEOUT" | ||
- [ ] 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. | ||
- [ ] Add tests. | ||
- [ ] Add express.js examples. |
This file contains bidirectional Unicode text that may be interpreted or compiled differently than what appears below. To review, open the file in an editor that reveals hidden Unicode characters.
Learn more about bidirectional Unicode characters
Original file line number | Diff line number | Diff line change |
---|---|---|
@@ -0,0 +1 @@ | ||
DATABASE_URL=postgres://postgres:[email protected]:5432/txtest |
This file contains bidirectional Unicode text that may be interpreted or compiled differently than what appears below. To review, open the file in an editor that reveals hidden Unicode characters.
Learn more about bidirectional Unicode characters
Original file line number | Diff line number | Diff line change |
---|---|---|
@@ -0,0 +1,3 @@ | ||
node_modules | ||
.env | ||
prisma/migrations |
This file contains bidirectional Unicode text that may be interpreted or compiled differently than what appears below. To review, open the file in an editor that reveals hidden Unicode characters.
Learn more about bidirectional Unicode characters
Original file line number | Diff line number | Diff line change |
---|---|---|
@@ -0,0 +1,188 @@ | ||
import { Post, Prisma, PrismaClient } from '@prisma/client'; | ||
import { patchPrismaTx, PrismaTransactional } from '../../src'; | ||
import { clear } from 'console'; | ||
|
||
// Init prisma client | ||
const rawPrisma = new PrismaClient({ | ||
log: [ | ||
{ | ||
emit: 'event', | ||
level: 'query', | ||
}, | ||
{ | ||
emit: 'stdout', | ||
level: 'error', | ||
}, | ||
{ | ||
emit: 'stdout', | ||
level: 'info', | ||
}, | ||
{ | ||
emit: 'stdout', | ||
level: 'warn', | ||
}, | ||
], | ||
}); | ||
rawPrisma.$on('query', (e) => { | ||
console.log(`Query: ${e.query} ${e.duration}ms; Params: ${e.params}`); | ||
}); | ||
|
||
// Apply @PrismaTransactional() | ||
const prisma = patchPrismaTx(rawPrisma, { | ||
enableLogging: true, | ||
}); | ||
|
||
// Test code | ||
// Utils | ||
function randomText(length: number) { | ||
return Math.random().toString(36).slice(-length); | ||
} | ||
|
||
function WaitAsync(ms: number) { | ||
return new Promise((resolve) => setTimeout(resolve, ms)); | ||
} | ||
|
||
// Repo | ||
class PostRepository { | ||
constructor(private prisma: PrismaClient) {} | ||
createPost(data: Prisma.PostCreateInput) { | ||
return this.prisma.post.create({ data }); | ||
} | ||
|
||
readPosts(query: Omit<Prisma.PostFindManyArgs, 'select'>): Promise<Post[]> { | ||
return this.prisma.post.findMany(query); | ||
} | ||
|
||
deletePost(query: Prisma.PostDeleteArgs) { | ||
return this.prisma.post.delete(query); | ||
} | ||
|
||
deleteAll() { | ||
return this.prisma.post.deleteMany(); | ||
} | ||
|
||
async createRandomPosts(count: number) { | ||
const postsPromises = Array.from({ length: count }, () => | ||
this.prisma.post.create({ data: { title: 'Post ' + randomText(5) } }), | ||
); | ||
const posts = await Promise.all(postsPromises); | ||
return posts; | ||
} | ||
editPost(id: number, content: string) { | ||
return this.prisma.post.update({ | ||
where: { id }, | ||
data: { content }, | ||
}); | ||
} | ||
} | ||
|
||
// Service | ||
class PostService { | ||
constructor(private postRepository: PostRepository) {} | ||
|
||
throwError() { | ||
throw new Error('Test error'); | ||
} | ||
|
||
async readAllPostsAndCreateOneMerged() { | ||
const posts = await this.postRepository.readPosts({}); | ||
const summary = posts.reduce((acc, post) => { | ||
return acc + post.title + ';\n'; | ||
}, ''); | ||
const newPost = await this.postRepository.createPost({ | ||
title: 'New readAllPostsAndCreateOneMerged post', | ||
content: summary, | ||
}); | ||
return newPost; | ||
} | ||
|
||
async deleteAllAndCreateOnePost() { | ||
await this.postRepository.deleteAll(); | ||
const newPost = await this.postRepository.createPost({ | ||
title: 'New deleteAllAndCreateOnePost post', | ||
}); | ||
return newPost; | ||
} | ||
|
||
@PrismaTransactional() | ||
async txCreate5RandomWait5SecAndSummarize() { | ||
PrismaTransactional.onSuccess(async () => { | ||
console.log('txCreate5RandomWait5SecAndSummarize PrismaTransactional.onSuccess'); | ||
}); | ||
await this.postRepository.createRandomPosts(5); | ||
await WaitAsync(5000); | ||
await this.readAllPostsAndCreateOneMerged(); | ||
|
||
return await this.postRepository.readPosts({}); | ||
} | ||
|
||
@PrismaTransactional() | ||
async txCreate30PostsAndThrow() { | ||
PrismaTransactional.onSuccess(async () => { | ||
console.log('txCreate30PostsAndThrow PrismaTransactional.onSuccess'); | ||
}); | ||
const posts = await this.postRepository.createRandomPosts(30); | ||
await this.postRepository.editPost(posts[0].id, 'edited'); | ||
await WaitAsync(1000); | ||
|
||
this.throwError(); | ||
} | ||
|
||
async txReadAllPostsAndCreateOneWithCount() { | ||
PrismaTransactional.onSuccess(async () => { | ||
console.log('txReadAllPostsAndCreateOneWithCount PrismaTransactional.onSuccess'); | ||
}); | ||
|
||
const posts = await this.postRepository.readPosts({}); | ||
await WaitAsync(2500); | ||
|
||
return await this.postRepository.createPost({ | ||
title: 'New txReadAllPostsAndCreateOneMerged post', | ||
content: `Count: ${posts.length}`, | ||
}); | ||
} | ||
} | ||
|
||
// Main | ||
async function resetDB() { | ||
await prisma.$queryRaw`TRUNCATE TABLE "Post" RESTART IDENTITY;`; | ||
} | ||
|
||
async function main() { | ||
await resetDB(); | ||
const postRepository = new PostRepository(prisma); | ||
const postService = new PostService(postRepository); | ||
|
||
const testErrorAction = async () => { | ||
try { | ||
await postService.txCreate30PostsAndThrow(); | ||
} catch {} | ||
return postRepository.readPosts({}); | ||
}; | ||
|
||
const test1Promise = postService.txCreate5RandomWait5SecAndSummarize(); | ||
const testErrorPromise = testErrorAction(); | ||
await WaitAsync(200); | ||
const test2 = await PrismaTransactional.execute( | ||
async () => await postService.txReadAllPostsAndCreateOneWithCount(), | ||
); | ||
const test1 = await test1Promise; | ||
const test3 = await postService.deleteAllAndCreateOnePost(); | ||
const throwError = await testErrorPromise; | ||
|
||
console.log({ test1, test2, test3, throwError }); | ||
if (test1.length !== 7) { | ||
console.error('test1.length !== 7'); | ||
} | ||
if (test2.content !== 'Count: 0') { | ||
console.error('test2.content !== 0'); | ||
} | ||
if (test3.title !== 'New deleteAllAndCreateOnePost post') { | ||
console.error('test3.content is not correct'); | ||
} | ||
if (throwError.length !== 0) { | ||
console.error('throwError page.length !== 0'); | ||
} | ||
} | ||
|
||
main(); |
This file contains bidirectional Unicode text that may be interpreted or compiled differently than what appears below. To review, open the file in an editor that reveals hidden Unicode characters.
Learn more about bidirectional Unicode characters
Original file line number | Diff line number | Diff line change |
---|---|---|
@@ -0,0 +1,20 @@ | ||
{ | ||
"name": "example", | ||
"scripts": { | ||
"install": "prisma migrate dev", | ||
"dev": "ts-node --transpile-only index.ts", | ||
"lint": "eslint index.ts" | ||
}, | ||
"dependencies": { | ||
"@prisma/client": "^5.13.0" | ||
}, | ||
"devDependencies": { | ||
"@typescript-eslint/eslint-plugin": "^7.8.0", | ||
"@typescript-eslint/parser": "^7.8.0", | ||
"eslint": "^8.57.0", | ||
"eslint-plugin-local": "^4.2.2", | ||
"prisma": "latest", | ||
"ts-node": "^10.9.2", | ||
"typescript": "^5.0.0" | ||
} | ||
} |
This file contains bidirectional Unicode text that may be interpreted or compiled differently than what appears below. To review, open the file in an editor that reveals hidden Unicode characters.
Learn more about bidirectional Unicode characters
Original file line number | Diff line number | Diff line change |
---|---|---|
@@ -0,0 +1,18 @@ | ||
// This is your Prisma schema file, | ||
// learn more about it in the docs: https://pris.ly/d/prisma-schema | ||
|
||
generator client { | ||
provider = "prisma-client-js" | ||
} | ||
|
||
datasource db { | ||
provider = "postgresql" | ||
url = env("DATABASE_URL") | ||
} | ||
|
||
model Post { | ||
id Int @id @default(autoincrement()) | ||
title String | ||
content String? | ||
author String? | ||
} |
Oops, something went wrong.