From 5d9df64849895c3bc96fc6e34ffcfdfd80ef285a Mon Sep 17 00:00:00 2001 From: maaz519 Date: Thu, 28 May 2026 01:44:48 +0530 Subject: [PATCH] fix(api): prevent self-loop routes, map P2003 to 400, forward DELETE error body Co-Authored-By: Claude Sonnet 4.6 --- apps/api/src/modules/routes/routes.service.spec.ts | 13 +++++++++++++ apps/api/src/modules/routes/routes.service.ts | 12 ++++++++++-- apps/web/app/api/routes/[id]/route.ts | 3 ++- 3 files changed, 25 insertions(+), 3 deletions(-) diff --git a/apps/api/src/modules/routes/routes.service.spec.ts b/apps/api/src/modules/routes/routes.service.spec.ts index b6c3754..79374a9 100644 --- a/apps/api/src/modules/routes/routes.service.spec.ts +++ b/apps/api/src/modules/routes/routes.service.spec.ts @@ -87,6 +87,19 @@ describe('RoutesService', () => { mockPrisma.syncRoute.create.mockRejectedValueOnce(p2002); await expect(service.create('grp_1', 'grp_2')).rejects.toThrow(ConflictException); }); + + it('throws BadRequestException when source and target are the same group', async () => { + await expect(service.create('grp_1', 'grp_1')).rejects.toThrow(BadRequestException); + }); + + it('throws BadRequestException when a group ID does not exist (Prisma P2003)', async () => { + const p2003 = new Prisma.PrismaClientKnownRequestError('Foreign key constraint', { + code: 'P2003', + clientVersion: '6.0.0', + }); + mockPrisma.syncRoute.create.mockRejectedValueOnce(p2003); + await expect(service.create('grp_1', 'bad_grp')).rejects.toThrow(BadRequestException); + }); }); describe('remove', () => { diff --git a/apps/api/src/modules/routes/routes.service.ts b/apps/api/src/modules/routes/routes.service.ts index f81b3b7..b1fd9cc 100644 --- a/apps/api/src/modules/routes/routes.service.ts +++ b/apps/api/src/modules/routes/routes.service.ts @@ -23,14 +23,22 @@ export class RoutesService { if (!sourceGroupId || !targetGroupId) { throw new BadRequestException('sourceGroupId and targetGroupId are required'); } + if (sourceGroupId === targetGroupId) { + throw new BadRequestException('Source and target groups cannot be the same'); + } try { return await this.prisma.syncRoute.create({ data: { sourceGroupId, targetGroupId }, include: routeInclude, }); } catch (e) { - if (e instanceof Prisma.PrismaClientKnownRequestError && e.code === 'P2002') { - throw new ConflictException('A route between these groups already exists'); + if (e instanceof Prisma.PrismaClientKnownRequestError) { + if (e.code === 'P2002') { + throw new ConflictException('A route between these groups already exists'); + } + if (e.code === 'P2003') { + throw new BadRequestException('One or both group IDs do not exist'); + } } throw e; } diff --git a/apps/web/app/api/routes/[id]/route.ts b/apps/web/app/api/routes/[id]/route.ts index d035e6c..aa65fe0 100644 --- a/apps/web/app/api/routes/[id]/route.ts +++ b/apps/web/app/api/routes/[id]/route.ts @@ -6,5 +6,6 @@ export async function DELETE( ) { const { id } = await params; const res = await fetch(`${API_URL}/routes/${id}`, { method: 'DELETE' }); - return new Response(null, { status: res.status }); + if (res.status === 204) return new Response(null, { status: 204 }); + return Response.json(await res.json(), { status: res.status }); }