diff --git a/.circleci/config.yml b/.circleci/config.yml index f5abda5..3cb6011 100644 --- a/.circleci/config.yml +++ b/.circleci/config.yml @@ -75,7 +75,7 @@ workflows: only: - develop - feat/ai-workflows - - pm-1792 + - pm-1955 - 'build-prod': diff --git a/src/api/ai-workflow/ai-workflow.controller.ts b/src/api/ai-workflow/ai-workflow.controller.ts index 29b4b08..0a2a767 100644 --- a/src/api/ai-workflow/ai-workflow.controller.ts +++ b/src/api/ai-workflow/ai-workflow.controller.ts @@ -24,6 +24,7 @@ import { UpdateAiWorkflowDto, CreateAiWorkflowRunItemsDto, UpdateAiWorkflowRunDto, + CreateRunItemCommentDto, UpdateAiWorkflowRunItemDto, } from '../../dto/aiWorkflow.dto'; import { Scopes } from 'src/shared/decorators/scopes.decorator'; @@ -39,6 +40,44 @@ import { User } from 'src/shared/decorators/user.decorator'; export class AiWorkflowController { constructor(private readonly aiWorkflowService: AiWorkflowService) {} + @Post('/:workflowId/runs/:runId/items/:itemId/comments') + @Roles( + UserRole.Submitter, + UserRole.Copilot, + UserRole.ProjectManager, + UserRole.Admin, + UserRole.Reviewer, + UserRole.User, + ) + @ApiOperation({ summary: 'Create a comment for a specific run item' }) + @ApiResponse({ + status: 201, + description: 'Comment created successfully.', + }) + @ApiResponse({ status: 400, description: 'Bad Request.' }) + @ApiResponse({ status: 401, description: 'Unauthorized.' }) + @ApiResponse({ status: 403, description: 'Forbidden.' }) + @ApiResponse({ + status: 404, + description: 'Workflow, Run, or Item not found.', + }) + async createRunItemComment( + @Param('workflowId') workflowId: string, + @Param('runId') runId: string, + @Param('itemId') itemId: string, + @Body(new ValidationPipe({ whitelist: true, transform: true })) + body: CreateRunItemCommentDto, + @User() user: JwtUser, + ) { + return this.aiWorkflowService.createRunItemComment( + workflowId, + runId, + itemId, + body, + user, + ); + } + @Post() @Roles(UserRole.Admin) @Scopes(Scope.CreateWorkflow) diff --git a/src/api/ai-workflow/ai-workflow.service.ts b/src/api/ai-workflow/ai-workflow.service.ts index 542e257..42e42e9 100644 --- a/src/api/ai-workflow/ai-workflow.service.ts +++ b/src/api/ai-workflow/ai-workflow.service.ts @@ -10,6 +10,7 @@ import { PrismaService } from '../../shared/modules/global/prisma.service'; import { CreateAiWorkflowDto, CreateAiWorkflowRunDto, + CreateRunItemCommentDto, UpdateAiWorkflowDto, UpdateAiWorkflowRunDto, UpdateAiWorkflowRunItemDto, @@ -37,6 +38,50 @@ export class AiWorkflowService { this.logger = LoggerService.forRoot('AiWorkflowService'); } + async createRunItemComment( + workflowId: string, + runId: string, + itemId: string, + body: CreateRunItemCommentDto, + user: JwtUser, + ) { + 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 createdComment = await this.prisma.aiWorkflowRunItemComment.create({ + data: { + workflowRunItemId: itemId, + content: body.content, + parentId: body.parentId ?? null, + userId: user.userId!, + }, + }); + + return createdComment; + } + async scorecardExists(scorecardId: string): Promise { const count = await this.prisma.scorecard.count({ where: { id: scorecardId, status: ScorecardStatus.ACTIVE }, diff --git a/src/dto/aiWorkflow.dto.ts b/src/dto/aiWorkflow.dto.ts index 67689a8..4b26dcb 100644 --- a/src/dto/aiWorkflow.dto.ts +++ b/src/dto/aiWorkflow.dto.ts @@ -1,4 +1,9 @@ -import { ApiProperty, OmitType, PartialType } from '@nestjs/swagger'; +import { + ApiHideProperty, + ApiProperty, + OmitType, + PartialType, +} from '@nestjs/swagger'; import { IsString, IsNotEmpty, @@ -162,6 +167,19 @@ export class CommentDto { export class UpdateAiWorkflowRunItemDto extends PartialType( CreateAiWorkflowRunItemDto, ) { + @ApiHideProperty() @IsEmpty({ message: 'scorecardQuestionId cannot be updated' }) scorecardQuestionId?: never; } + +export class CreateRunItemCommentDto { + @ApiProperty() + @IsString() + @IsNotEmpty() + content: string; + + @ApiProperty({ required: false }) + @IsString() + @IsOptional() + parentId?: string; +}