From 5feaff339310db49b228fb01cee10731247dfc73 Mon Sep 17 00:00:00 2001 From: Hentry Martin Date: Fri, 19 Sep 2025 22:02:53 +0200 Subject: [PATCH 1/3] feat: update comment endpoint --- src/api/ai-workflow/ai-workflow.controller.ts | 51 +++++++++++ src/api/ai-workflow/ai-workflow.service.ts | 86 +++++++++++++++++++ src/dto/aiWorkflow.dto.ts | 29 +++++++ 3 files changed, 166 insertions(+) diff --git a/src/api/ai-workflow/ai-workflow.controller.ts b/src/api/ai-workflow/ai-workflow.controller.ts index 0a2a767..a300c59 100644 --- a/src/api/ai-workflow/ai-workflow.controller.ts +++ b/src/api/ai-workflow/ai-workflow.controller.ts @@ -26,6 +26,7 @@ import { UpdateAiWorkflowRunDto, CreateRunItemCommentDto, UpdateAiWorkflowRunItemDto, + UpdateRunItemCommentDto, } from '../../dto/aiWorkflow.dto'; import { Scopes } from 'src/shared/decorators/scopes.decorator'; import { UserRole } from 'src/shared/enums/userRole.enum'; @@ -40,6 +41,56 @@ import { User } from 'src/shared/decorators/user.decorator'; export class AiWorkflowController { constructor(private readonly aiWorkflowService: AiWorkflowService) {} + @Patch('/:workflowId/runs/:runId/items/:itemId/comments/:commentId') + @Roles( + UserRole.Submitter, + UserRole.Copilot, + UserRole.ProjectManager, + UserRole.Admin, + UserRole.Reviewer, + UserRole.User, + ) + @ApiOperation({ summary: 'Update a comment by id' }) + @ApiParam({ name: 'workflowId', description: 'Workflow ID' }) + @ApiParam({ name: 'runId', description: 'Run ID' }) + @ApiParam({ name: 'itemId', description: 'Item ID' }) + @ApiParam({ name: 'commentId', description: 'Comment ID' }) + @ApiBody({ + description: 'Partial comment data to update', + schema: { + type: 'object', + properties: { + content: { type: 'string' }, + upVotes: { type: 'number' }, + downVotes: { type: 'number' }, + }, + additionalProperties: false, + }, + }) + @ApiResponse({ + status: 200, + description: 'Comment updated successfully.', + }) + @ApiResponse({ status: 403, description: 'Forbidden. User not comment creator.' }) + @ApiResponse({ status: 404, description: 'Comment not found.' }) + async updateRunItemComment( + @Param('workflowId') workflowId: string, + @Param('runId') runId: string, + @Param('itemId') itemId: string, + @Param('commentId') commentId: string, + @Body(new ValidationPipe({ whitelist: true, transform: true })) body: UpdateRunItemCommentDto, + @User() user: JwtUser, + ) { + return this.aiWorkflowService.updateCommentById( + user, + workflowId, + runId, + itemId, + commentId, + body, + ); + } + @Post('/:workflowId/runs/:runId/items/:itemId/comments') @Roles( UserRole.Submitter, diff --git a/src/api/ai-workflow/ai-workflow.service.ts b/src/api/ai-workflow/ai-workflow.service.ts index 42e42e9..4274ffa 100644 --- a/src/api/ai-workflow/ai-workflow.service.ts +++ b/src/api/ai-workflow/ai-workflow.service.ts @@ -14,6 +14,7 @@ import { UpdateAiWorkflowDto, UpdateAiWorkflowRunDto, UpdateAiWorkflowRunItemDto, + UpdateRunItemCommentDto, } from '../../dto/aiWorkflow.dto'; import { ScorecardStatus } from 'src/dto/scorecard.dto'; import { JwtUser } from 'src/shared/modules/global/jwt.service'; @@ -38,6 +39,91 @@ export class AiWorkflowService { this.logger = LoggerService.forRoot('AiWorkflowService'); } + async updateCommentById( + user: JwtUser, + workflowId: string, + runId: string, + itemId: string, + commentId: string, + patchData: UpdateRunItemCommentDto, + ) { + this.logger.log( + `Updating comment ${commentId} for workflow ${workflowId}, run ${runId}, item ${itemId}`, + ); + + try { + const workflow = await this.prisma.aiWorkflow.findUnique({ + where: { id: workflowId }, + }); + if (!workflow) { + throw new NotFoundException(`Workflow with id ${workflowId} not found.`); + } + + const run = await this.prisma.aiWorkflowRun.findUnique({ + where: { id: runId }, + }); + if (!run || run.workflowId !== workflowId) { + throw new NotFoundException( + `Run with id ${runId} not found or does not belong to workflow ${workflowId}.`, + ); + } + + const item = await this.prisma.aiWorkflowRunItem.findUnique({ + where: { id: itemId }, + }); + if (!item || item.workflowRunId !== runId) { + throw new NotFoundException( + `Item with id ${itemId} not found or does not belong to run ${runId}.`, + ); + } + + const comment = await this.prisma.aiWorkflowRunItemComment.findUnique({ + where: { id: commentId }, + }); + if (!comment || comment.workflowRunItemId !== itemId) { + throw new NotFoundException( + `Comment with id ${commentId} not found or does not belong to item ${itemId}.`, + ); + } + + if (String(comment.userId) !== String(user.userId)) { + throw new ForbiddenException( + 'User is not the creator of this comment and cannot update it.', + ); + } + + const allowedFields = ['content', 'upVotes', 'downVotes']; + const updateData: any = {}; + for (const key of allowedFields) { + if (key in patchData) { + updateData[key] = patchData[key]; + } + } + + if (Object.keys(updateData).length === 0) { + throw new BadRequestException('No valid fields provided for update.'); + } + + const updatedComment = await this.prisma.aiWorkflowRunItemComment.update({ + where: { id: commentId }, + data: updateData, + }); + + return updatedComment; + } catch (error) { + if ( + error instanceof NotFoundException || + error instanceof ForbiddenException || + error instanceof BadRequestException + ) { + throw error; + } + + this.logger.error(`Failed to update comment ${commentId}`, error); + throw new InternalServerErrorException('Failed to update comment'); + } + } + async createRunItemComment( workflowId: string, runId: string, diff --git a/src/dto/aiWorkflow.dto.ts b/src/dto/aiWorkflow.dto.ts index 4b26dcb..98ae8e1 100644 --- a/src/dto/aiWorkflow.dto.ts +++ b/src/dto/aiWorkflow.dto.ts @@ -183,3 +183,32 @@ export class CreateRunItemCommentDto { @IsOptional() parentId?: string; } + +export class UpdateRunItemCommentDto { + @ApiProperty({ required: false }) + @IsString() + @IsOptional() + content?: string; + + @ApiProperty({ required: false }) + @IsInt() + @IsOptional() + upVotes?: number; + + @ApiProperty({ required: false }) + @IsInt() + @IsOptional() + downVotes?: number; + + @ApiHideProperty() + @IsEmpty({ message: 'parentId cannot be updated' }) + parentId?: never; + + @ApiHideProperty() + @IsEmpty({ message: 'userId cannot be updated' }) + userId?: never; + + @ApiHideProperty() + @IsEmpty({ message: 'workflowRunItemId cannot be updated' }) + workflowRunItemId?: never; +} From a32f344e1d4817eda5c1e5492cf5f85773a5d3f1 Mon Sep 17 00:00:00 2001 From: Hentry Martin Date: Fri, 19 Sep 2025 23:12:48 +0200 Subject: [PATCH 2/3] fix: lint --- .circleci/config.yml | 2 +- src/api/ai-workflow/ai-workflow.controller.ts | 8 ++++++-- src/api/ai-workflow/ai-workflow.service.ts | 4 +++- 3 files changed, 10 insertions(+), 4 deletions(-) diff --git a/.circleci/config.yml b/.circleci/config.yml index 3cb6011..4956c7f 100644 --- a/.circleci/config.yml +++ b/.circleci/config.yml @@ -75,7 +75,7 @@ workflows: only: - develop - feat/ai-workflows - - pm-1955 + - pm-1957 - 'build-prod': diff --git a/src/api/ai-workflow/ai-workflow.controller.ts b/src/api/ai-workflow/ai-workflow.controller.ts index a300c59..2d95944 100644 --- a/src/api/ai-workflow/ai-workflow.controller.ts +++ b/src/api/ai-workflow/ai-workflow.controller.ts @@ -71,14 +71,18 @@ export class AiWorkflowController { status: 200, description: 'Comment updated successfully.', }) - @ApiResponse({ status: 403, description: 'Forbidden. User not comment creator.' }) + @ApiResponse({ + status: 403, + description: 'Forbidden. User not comment creator.', + }) @ApiResponse({ status: 404, description: 'Comment not found.' }) async updateRunItemComment( @Param('workflowId') workflowId: string, @Param('runId') runId: string, @Param('itemId') itemId: string, @Param('commentId') commentId: string, - @Body(new ValidationPipe({ whitelist: true, transform: true })) body: UpdateRunItemCommentDto, + @Body(new ValidationPipe({ whitelist: true, transform: true })) + body: UpdateRunItemCommentDto, @User() user: JwtUser, ) { return this.aiWorkflowService.updateCommentById( diff --git a/src/api/ai-workflow/ai-workflow.service.ts b/src/api/ai-workflow/ai-workflow.service.ts index 4274ffa..c315dbf 100644 --- a/src/api/ai-workflow/ai-workflow.service.ts +++ b/src/api/ai-workflow/ai-workflow.service.ts @@ -56,7 +56,9 @@ export class AiWorkflowService { where: { id: workflowId }, }); if (!workflow) { - throw new NotFoundException(`Workflow with id ${workflowId} not found.`); + throw new NotFoundException( + `Workflow with id ${workflowId} not found.`, + ); } const run = await this.prisma.aiWorkflowRun.findUnique({ From a27e365b20995d8d2d399613d9d468ef74e58676 Mon Sep 17 00:00:00 2001 From: Hentry Martin Date: Fri, 19 Sep 2025 23:42:10 +0200 Subject: [PATCH 3/3] fix: creating comment --- src/api/ai-workflow/ai-workflow.service.ts | 6 +++++- 1 file changed, 5 insertions(+), 1 deletion(-) diff --git a/src/api/ai-workflow/ai-workflow.service.ts b/src/api/ai-workflow/ai-workflow.service.ts index c315dbf..67eae04 100644 --- a/src/api/ai-workflow/ai-workflow.service.ts +++ b/src/api/ai-workflow/ai-workflow.service.ts @@ -158,12 +158,16 @@ export class AiWorkflowService { ); } + if (!user.userId) { + throw new BadRequestException(`User id not available`); + } + const createdComment = await this.prisma.aiWorkflowRunItemComment.create({ data: { workflowRunItemId: itemId, content: body.content, parentId: body.parentId ?? null, - userId: user.userId!, + userId: user.userId.toString(), }, });