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 0a2a767..2d95944 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,60 @@ 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 5ead77c..165e7bb 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,93 @@ 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; +}