From 40561eb979dc45152470fe6545d218a643f0dcde Mon Sep 17 00:00:00 2001 From: Gyunseo Lee Date: Sat, 16 Mar 2024 11:40:30 +0000 Subject: [PATCH 01/11] =?UTF-8?q?feat(be):=20implement=20transaction=20ext?= =?UTF-8?q?ension=20for=20prisma=20client=20https://github.com/prisma/pris?= =?UTF-8?q?ma-client-extensions/pull/47=20=EC=B0=B8=EA=B3=A0=ED=95=98?= =?UTF-8?q?=EC=97=AC=20=EC=9E=91=EC=84=B1=ED=96=88=EC=8A=B5=EB=8B=88?= =?UTF-8?q?=EB=8B=A4?= MIME-Version: 1.0 Content-Type: text/plain; charset=UTF-8 Content-Transfer-Encoding: 8bit --- .../libs/prisma/src/transaction.extension.ts | 71 +++++++++++++++++++ 1 file changed, 71 insertions(+) create mode 100644 apps/backend/libs/prisma/src/transaction.extension.ts diff --git a/apps/backend/libs/prisma/src/transaction.extension.ts b/apps/backend/libs/prisma/src/transaction.extension.ts new file mode 100644 index 0000000000..5dbd210f22 --- /dev/null +++ b/apps/backend/libs/prisma/src/transaction.extension.ts @@ -0,0 +1,71 @@ +import { Prisma } from '@prisma/client' +import { PrismaService } from './prisma.service' + +export type FlatTransactionClient = Prisma.TransactionClient & { + $commit: () => Promise + $rollback: () => Promise +} + +const ROLLBACK = { [Symbol.for('prisma.client.extension.rollback')]: true } + +export const transactionExtension = Prisma.defineExtension({ + client: { + async $begin() { + const prisma = Prisma.getExtensionContext(this) + let setTxClient: (txClient: Prisma.TransactionClient) => void + let commit: () => void + let rollback: () => void + + // a promise for getting the tx inner client + const txClient = new Promise((res) => { + setTxClient = res + }) + + // a promise for controlling the transaction + const txPromise = new Promise((_res, _rej) => { + commit = () => _res(undefined) + rollback = () => _rej(ROLLBACK) + }) + + // opening a transaction to control externally + if ( + '$transaction' in prisma && + typeof prisma.$transaction === 'function' + ) { + const tx = prisma + .$transaction((txClient) => { + setTxClient(txClient as unknown as Prisma.TransactionClient) + return txPromise + }) + .catch((e) => { + if (e === ROLLBACK) { + return + } + throw e + }) + + // return a proxy TransactionClient with `$commit` and `$rollback` methods + return new Proxy(await txClient, { + get(target, prop) { + if (prop === '$commit') { + return () => { + commit() + return tx + } + } + if (prop === '$rollback') { + return () => { + rollback() + return tx + } + } + return target[prop as keyof typeof target] + } + }) as FlatTransactionClient + } + + throw new Error('Transactions are not supported by this client') + }, + getPaginator: PrismaService.prototype.getPaginator + } +}) From 80de15a14dce25e69c07dd0299a90573a18f7f92 Mon Sep 17 00:00:00 2001 From: Gyunseo Lee Date: Sat, 16 Mar 2024 11:41:48 +0000 Subject: [PATCH 02/11] feat(be): add transaction extension on index ts file --- apps/backend/libs/prisma/src/index.ts | 1 + 1 file changed, 1 insertion(+) diff --git a/apps/backend/libs/prisma/src/index.ts b/apps/backend/libs/prisma/src/index.ts index 242be587a0..7f13878b82 100644 --- a/apps/backend/libs/prisma/src/index.ts +++ b/apps/backend/libs/prisma/src/index.ts @@ -1,2 +1,3 @@ export * from './prisma.module' export * from './prisma.service' +export * from './transaction.extension' From 0f8ac409416c97aea8c01ea75d958573ed2571d1 Mon Sep 17 00:00:00 2001 From: Gyunseo Lee Date: Sat, 16 Mar 2024 11:42:30 +0000 Subject: [PATCH 03/11] feat(be): implement transaction rollback for group service unit test --- .../client/src/group/group.service.spec.ts | 102 +++++++++--------- 1 file changed, 51 insertions(+), 51 deletions(-) diff --git a/apps/backend/apps/client/src/group/group.service.spec.ts b/apps/backend/apps/client/src/group/group.service.spec.ts index f174fae7ef..0ffd8f7d6c 100644 --- a/apps/backend/apps/client/src/group/group.service.spec.ts +++ b/apps/backend/apps/client/src/group/group.service.spec.ts @@ -1,32 +1,57 @@ import { CACHE_MANAGER } from '@nestjs/cache-manager' import { ConfigService } from '@nestjs/config' import { Test, type TestingModule } from '@nestjs/testing' -import { Prisma } from '@prisma/client' +import { Prisma, PrismaClient } from '@prisma/client' import type { Cache } from 'cache-manager' import { expect } from 'chai' -import * as chai from 'chai' -import chaiExclude from 'chai-exclude' import { stub } from 'sinon' import { JOIN_GROUP_REQUEST_EXPIRE_TIME } from '@libs/constants' import { ConflictFoundException, EntityNotExistException } from '@libs/exception' -import { PrismaService } from '@libs/prisma' +import { PrismaService, type FlatTransactionClient } from '@libs/prisma' +import { transactionExtension } from '@libs/prisma' import { GroupService } from './group.service' import type { UserGroupData } from './interface/user-group-data.interface' -chai.use(chaiExclude) - -describe('GroupService', () => { +describe('GroupService', async () => { let service: GroupService let cache: Cache - let prisma: PrismaService + + const prisma = new PrismaClient().$extends(transactionExtension) + const overridePrismaService = async (transaction: FlatTransactionClient) => { + const module: TestingModule = await Test.createTestingModule({ + providers: [ + GroupService, + { + provide: PrismaService, + useValue: transaction + }, + ConfigService, + { + provide: CACHE_MANAGER, + useFactory: () => ({ + set: () => [], + get: () => [] + }) + } + ] + }).compile() + service = module.get(GroupService) + cache = module.get(CACHE_MANAGER) + } + beforeEach(async () => { + const tx = await prisma.$begin() const module: TestingModule = await Test.createTestingModule({ providers: [ GroupService, - PrismaService, + { + provide: PrismaService, + useValue: tx + }, + ConfigService, { provide: CACHE_MANAGER, @@ -39,7 +64,6 @@ describe('GroupService', () => { }).compile() service = module.get(GroupService) cache = module.get(CACHE_MANAGER) - prisma = module.get(PrismaService) }) it('should be defined', () => { @@ -163,12 +187,15 @@ describe('GroupService', () => { }) }) - describe('joinGroupById', () => { + describe('joinGroupById', async () => { let groupId: number const userId = 4 - + let tx beforeEach(async () => { - const group = await prisma.group.create({ + // override the useValue of PrismaService + tx = await prisma.$begin() + overridePrismaService(tx) + const group = await tx.group.create({ data: { groupName: 'test', description: 'test', @@ -182,26 +209,7 @@ describe('GroupService', () => { }) afterEach(async () => { - try { - await prisma.userGroup.delete({ - where: { - // eslint-disable-next-line @typescript-eslint/naming-convention - userId_groupId: { userId, groupId } - } - }) - } catch { - /* 삭제할 내용이 없는 경우 예외 무시 */ - } - - try { - await prisma.group.delete({ - where: { - id: groupId - } - }) - } catch { - /* 삭제할 내용 없을 경우 예외 무시 */ - } + await tx.$rollback() }) it('should return {isJoined: true} when group not set as requireApprovalBeforeJoin', async () => { @@ -225,7 +233,7 @@ describe('GroupService', () => { }) it('should return {isJoined: false} when group set as requireApprovalBeforeJoin', async () => { - await prisma.group.update({ + await tx.group.update({ where: { id: groupId }, @@ -250,7 +258,7 @@ describe('GroupService', () => { }) it('should throw ConflictFoundException when user is already group memeber', async () => { - await prisma.userGroup.create({ + await tx.userGroup.create({ data: { userId, groupId, @@ -270,7 +278,7 @@ describe('GroupService', () => { { userId, expiresAt: Date.now() + JOIN_GROUP_REQUEST_EXPIRE_TIME } ]) - await prisma.group.update({ + await tx.group.update({ where: { id: groupId }, @@ -288,12 +296,15 @@ describe('GroupService', () => { }) }) - describe('leaveGroup', () => { + describe('leaveGroup', async () => { const groupId = 3 const userId = 4 - + let tx beforeEach(async () => { - await prisma.userGroup.createMany({ + // override the useValue of PrismaService + tx = await prisma.$begin() + overridePrismaService(tx) + await tx.userGroup.createMany({ data: [ { userId, @@ -310,18 +321,7 @@ describe('GroupService', () => { }) afterEach(async () => { - try { - await prisma.userGroup.deleteMany({ - where: { - OR: [ - { AND: [{ userId }, { groupId }] }, - { AND: [{ userId: 5 }, { groupId }] } - ] - } - }) - } catch { - return - } + await tx.$rollback() }) it('should return deleted userGroup when valid userId and groupId passed', async () => { From aa378f5599b46ffdecdc7c7445b382cd707affd9 Mon Sep 17 00:00:00 2001 From: Gyunseo Lee Date: Sat, 16 Mar 2024 11:50:11 +0000 Subject: [PATCH 04/11] test(be): add comments and type --- apps/backend/apps/client/src/group/group.service.spec.ts | 9 +++++---- 1 file changed, 5 insertions(+), 4 deletions(-) diff --git a/apps/backend/apps/client/src/group/group.service.spec.ts b/apps/backend/apps/client/src/group/group.service.spec.ts index 0ffd8f7d6c..d2ac60b9fa 100644 --- a/apps/backend/apps/client/src/group/group.service.spec.ts +++ b/apps/backend/apps/client/src/group/group.service.spec.ts @@ -18,8 +18,8 @@ import type { UserGroupData } from './interface/user-group-data.interface' describe('GroupService', async () => { let service: GroupService let cache: Cache - const prisma = new PrismaClient().$extends(transactionExtension) + const overridePrismaService = async (transaction: FlatTransactionClient) => { const module: TestingModule = await Test.createTestingModule({ providers: [ @@ -43,7 +43,8 @@ describe('GroupService', async () => { } beforeEach(async () => { - const tx = await prisma.$begin() + // initial transaction client + const tx: FlatTransactionClient = await prisma.$begin() const module: TestingModule = await Test.createTestingModule({ providers: [ GroupService, @@ -190,7 +191,7 @@ describe('GroupService', async () => { describe('joinGroupById', async () => { let groupId: number const userId = 4 - let tx + let tx: FlatTransactionClient beforeEach(async () => { // override the useValue of PrismaService tx = await prisma.$begin() @@ -299,7 +300,7 @@ describe('GroupService', async () => { describe('leaveGroup', async () => { const groupId = 3 const userId = 4 - let tx + let tx: FlatTransactionClient beforeEach(async () => { // override the useValue of PrismaService tx = await prisma.$begin() From 2a135b3f62781e91d3dade165271bce2a6cd82e9 Mon Sep 17 00:00:00 2001 From: Gyunseo Lee Date: Sat, 16 Mar 2024 12:16:21 +0000 Subject: [PATCH 05/11] test(be): add await keyword --- apps/backend/apps/client/src/group/group.service.spec.ts | 4 ++-- 1 file changed, 2 insertions(+), 2 deletions(-) diff --git a/apps/backend/apps/client/src/group/group.service.spec.ts b/apps/backend/apps/client/src/group/group.service.spec.ts index d2ac60b9fa..63aa216005 100644 --- a/apps/backend/apps/client/src/group/group.service.spec.ts +++ b/apps/backend/apps/client/src/group/group.service.spec.ts @@ -195,7 +195,7 @@ describe('GroupService', async () => { beforeEach(async () => { // override the useValue of PrismaService tx = await prisma.$begin() - overridePrismaService(tx) + await overridePrismaService(tx) const group = await tx.group.create({ data: { groupName: 'test', @@ -304,7 +304,7 @@ describe('GroupService', async () => { beforeEach(async () => { // override the useValue of PrismaService tx = await prisma.$begin() - overridePrismaService(tx) + await overridePrismaService(tx) await tx.userGroup.createMany({ data: [ { From 1f529cae8dab10be21d7a7a2e5648b500d7e924d Mon Sep 17 00:00:00 2001 From: Gyunseo Lee Date: Sat, 16 Mar 2024 13:14:13 +0000 Subject: [PATCH 06/11] test(be): add chai exclude --- apps/backend/apps/client/src/group/group.service.spec.ts | 3 +++ 1 file changed, 3 insertions(+) diff --git a/apps/backend/apps/client/src/group/group.service.spec.ts b/apps/backend/apps/client/src/group/group.service.spec.ts index 63aa216005..b6010f972f 100644 --- a/apps/backend/apps/client/src/group/group.service.spec.ts +++ b/apps/backend/apps/client/src/group/group.service.spec.ts @@ -4,6 +4,8 @@ import { Test, type TestingModule } from '@nestjs/testing' import { Prisma, PrismaClient } from '@prisma/client' import type { Cache } from 'cache-manager' import { expect } from 'chai' +import * as chai from 'chai' +import chaiExclude from 'chai-exclude' import { stub } from 'sinon' import { JOIN_GROUP_REQUEST_EXPIRE_TIME } from '@libs/constants' import { @@ -15,6 +17,7 @@ import { transactionExtension } from '@libs/prisma' import { GroupService } from './group.service' import type { UserGroupData } from './interface/user-group-data.interface' +chai.use(chaiExclude) describe('GroupService', async () => { let service: GroupService let cache: Cache From 7e942110f21f4cc35fb488f930cf407f0eb3788e Mon Sep 17 00:00:00 2001 From: Gyunseo Lee Date: Sat, 16 Mar 2024 13:17:52 +0000 Subject: [PATCH 07/11] test(be): delete override prisma service func --- .../client/src/group/group.service.spec.ts | 36 +++---------------- 1 file changed, 4 insertions(+), 32 deletions(-) diff --git a/apps/backend/apps/client/src/group/group.service.spec.ts b/apps/backend/apps/client/src/group/group.service.spec.ts index b6010f972f..a15068957d 100644 --- a/apps/backend/apps/client/src/group/group.service.spec.ts +++ b/apps/backend/apps/client/src/group/group.service.spec.ts @@ -21,33 +21,13 @@ chai.use(chaiExclude) describe('GroupService', async () => { let service: GroupService let cache: Cache - const prisma = new PrismaClient().$extends(transactionExtension) + let tx: FlatTransactionClient - const overridePrismaService = async (transaction: FlatTransactionClient) => { - const module: TestingModule = await Test.createTestingModule({ - providers: [ - GroupService, - { - provide: PrismaService, - useValue: transaction - }, - ConfigService, - { - provide: CACHE_MANAGER, - useFactory: () => ({ - set: () => [], - get: () => [] - }) - } - ] - }).compile() - service = module.get(GroupService) - cache = module.get(CACHE_MANAGER) - } + const prisma = new PrismaClient().$extends(transactionExtension) beforeEach(async () => { - // initial transaction client - const tx: FlatTransactionClient = await prisma.$begin() + //transaction client + tx = await prisma.$begin() const module: TestingModule = await Test.createTestingModule({ providers: [ GroupService, @@ -194,11 +174,7 @@ describe('GroupService', async () => { describe('joinGroupById', async () => { let groupId: number const userId = 4 - let tx: FlatTransactionClient beforeEach(async () => { - // override the useValue of PrismaService - tx = await prisma.$begin() - await overridePrismaService(tx) const group = await tx.group.create({ data: { groupName: 'test', @@ -303,11 +279,7 @@ describe('GroupService', async () => { describe('leaveGroup', async () => { const groupId = 3 const userId = 4 - let tx: FlatTransactionClient beforeEach(async () => { - // override the useValue of PrismaService - tx = await prisma.$begin() - await overridePrismaService(tx) await tx.userGroup.createMany({ data: [ { From f810cef4de1a0adc94e19eeba2ad9bacacea9759 Mon Sep 17 00:00:00 2001 From: Gyunseo Lee Date: Sat, 16 Mar 2024 13:40:38 +0000 Subject: [PATCH 08/11] test(be): increase timeout for before each hook --- apps/backend/apps/client/src/group/group.service.spec.ts | 5 +++-- 1 file changed, 3 insertions(+), 2 deletions(-) diff --git a/apps/backend/apps/client/src/group/group.service.spec.ts b/apps/backend/apps/client/src/group/group.service.spec.ts index a15068957d..55f330c803 100644 --- a/apps/backend/apps/client/src/group/group.service.spec.ts +++ b/apps/backend/apps/client/src/group/group.service.spec.ts @@ -18,14 +18,15 @@ import { GroupService } from './group.service' import type { UserGroupData } from './interface/user-group-data.interface' chai.use(chaiExclude) -describe('GroupService', async () => { +describe('GroupService', () => { let service: GroupService let cache: Cache let tx: FlatTransactionClient const prisma = new PrismaClient().$extends(transactionExtension) - beforeEach(async () => { + beforeEach(async function () { + this.timeout(3000) //transaction client tx = await prisma.$begin() const module: TestingModule = await Test.createTestingModule({ From 74bbab84f327b8b16468268793619a42458ca5bd Mon Sep 17 00:00:00 2001 From: Gyunseo Lee Date: Sat, 16 Mar 2024 13:54:16 +0000 Subject: [PATCH 09/11] test(be): disable timeout for before each hook --- .../apps/client/src/group/group.service.spec.ts | 12 ++++-------- 1 file changed, 4 insertions(+), 8 deletions(-) diff --git a/apps/backend/apps/client/src/group/group.service.spec.ts b/apps/backend/apps/client/src/group/group.service.spec.ts index 55f330c803..f82419ff58 100644 --- a/apps/backend/apps/client/src/group/group.service.spec.ts +++ b/apps/backend/apps/client/src/group/group.service.spec.ts @@ -26,17 +26,13 @@ describe('GroupService', () => { const prisma = new PrismaClient().$extends(transactionExtension) beforeEach(async function () { - this.timeout(3000) + this.timeout(0) //transaction client tx = await prisma.$begin() const module: TestingModule = await Test.createTestingModule({ providers: [ GroupService, - { - provide: PrismaService, - useValue: tx - }, - + { provide: PrismaService, useValue: tx }, ConfigService, { provide: CACHE_MANAGER, @@ -172,7 +168,7 @@ describe('GroupService', () => { }) }) - describe('joinGroupById', async () => { + describe('joinGroupById', () => { let groupId: number const userId = 4 beforeEach(async () => { @@ -277,7 +273,7 @@ describe('GroupService', () => { }) }) - describe('leaveGroup', async () => { + describe('leaveGroup', () => { const groupId = 3 const userId = 4 beforeEach(async () => { From b17c3defa1b718ab370e4e5c7ad34f8fb5dece77 Mon Sep 17 00:00:00 2001 From: Gyunseo Lee Date: Sat, 16 Mar 2024 14:04:25 +0000 Subject: [PATCH 10/11] test(be): add comment --- apps/backend/apps/client/src/group/group.service.spec.ts | 3 +++ 1 file changed, 3 insertions(+) diff --git a/apps/backend/apps/client/src/group/group.service.spec.ts b/apps/backend/apps/client/src/group/group.service.spec.ts index f82419ff58..b25069e7c1 100644 --- a/apps/backend/apps/client/src/group/group.service.spec.ts +++ b/apps/backend/apps/client/src/group/group.service.spec.ts @@ -26,6 +26,9 @@ describe('GroupService', () => { const prisma = new PrismaClient().$extends(transactionExtension) beforeEach(async function () { + // TODO: CI 테스트에서 timeout이 걸리는 문제를 우회하기 위해서 timeout을 0으로 설정 (timeout disabled) + // local에서는 timeout을 disable 하지 않아도 테스트가 정상적으로 동작함 (default setting: 2000ms) + // timeout이 큰 문제라면 해결을 해야 하는 부분이지만, 현재로서는 사실 큰 문제는 없어 보임 (로컬에서 테스트가 엄청 느려지는 것이 아님) this.timeout(0) //transaction client tx = await prisma.$begin() From 6efd52ff0b723228437747e3992bf90cb4212af7 Mon Sep 17 00:00:00 2001 From: Gyunseo Lee Date: Sat, 16 Mar 2024 14:06:58 +0000 Subject: [PATCH 11/11] test(be): fix comment --- apps/backend/apps/client/src/group/group.service.spec.ts | 1 - 1 file changed, 1 deletion(-) diff --git a/apps/backend/apps/client/src/group/group.service.spec.ts b/apps/backend/apps/client/src/group/group.service.spec.ts index b25069e7c1..91ccc6a95d 100644 --- a/apps/backend/apps/client/src/group/group.service.spec.ts +++ b/apps/backend/apps/client/src/group/group.service.spec.ts @@ -28,7 +28,6 @@ describe('GroupService', () => { beforeEach(async function () { // TODO: CI 테스트에서 timeout이 걸리는 문제를 우회하기 위해서 timeout을 0으로 설정 (timeout disabled) // local에서는 timeout을 disable 하지 않아도 테스트가 정상적으로 동작함 (default setting: 2000ms) - // timeout이 큰 문제라면 해결을 해야 하는 부분이지만, 현재로서는 사실 큰 문제는 없어 보임 (로컬에서 테스트가 엄청 느려지는 것이 아님) this.timeout(0) //transaction client tx = await prisma.$begin()