diff --git a/.circleci/config.yml b/.circleci/config.yml index 3737228..f5abda5 100644 --- a/.circleci/config.yml +++ b/.circleci/config.yml @@ -75,7 +75,8 @@ workflows: only: - develop - feat/ai-workflows - - pm-1782 + - pm-1792 + - 'build-prod': context: org-global diff --git a/src/api/ai-workflow/ai-workflow.controller.ts b/src/api/ai-workflow/ai-workflow.controller.ts index 57188f5..29b4b08 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, + UpdateAiWorkflowRunItemDto, } from '../../dto/aiWorkflow.dto'; import { Scopes } from 'src/shared/decorators/scopes.decorator'; import { UserRole } from 'src/shared/enums/userRole.enum'; @@ -227,6 +228,52 @@ export class AiWorkflowController { ); } + @Patch('/:workflowId/runs/:runId/items/:itemId') + @Scopes(Scope.UpdateWorkflowRun) + @Roles( + UserRole.Admin, + UserRole.Copilot, + UserRole.ProjectManager, + UserRole.Reviewer, + UserRole.Submitter, + UserRole.User, + ) + @ApiOperation({ summary: 'Update an AIWorkflowRunItem by id' }) + @ApiParam({ name: 'workflowId', description: 'The ID of the AI workflow' }) + @ApiParam({ name: 'runId', description: 'The ID of the AI workflow run' }) + @ApiParam({ + name: 'itemId', + description: 'The ID of the AI workflow run item', + }) + @ApiBody({ + description: 'AIWorkflowRunItem update data', + type: UpdateAiWorkflowRunItemDto, + }) + @ApiResponse({ + status: 200, + description: 'AIWorkflowRunItem updated 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 updateRunItem( + @Param('workflowId') workflowId: string, + @Param('runId') runId: string, + @Param('itemId') itemId: string, + @Body(new ValidationPipe({ whitelist: true, transform: true })) + patchData: UpdateAiWorkflowRunItemDto, + @User() user: JwtUser, + ) { + return this.aiWorkflowService.updateRunItem( + workflowId, + runId, + itemId, + patchData, + user, + ); + } + @Get('/:workflowId/runs/:runId/items') @Roles( UserRole.Admin, diff --git a/src/api/ai-workflow/ai-workflow.service.ts b/src/api/ai-workflow/ai-workflow.service.ts index e75d38a..542e257 100644 --- a/src/api/ai-workflow/ai-workflow.service.ts +++ b/src/api/ai-workflow/ai-workflow.service.ts @@ -12,6 +12,7 @@ import { CreateAiWorkflowRunDto, UpdateAiWorkflowDto, UpdateAiWorkflowRunDto, + UpdateAiWorkflowRunItemDto, } from '../../dto/aiWorkflow.dto'; import { ScorecardStatus } from 'src/dto/scorecard.dto'; import { JwtUser } from 'src/shared/modules/global/jwt.service'; @@ -444,12 +445,12 @@ export class AiWorkflowService { UserRole.Submitter, ].map((r) => r.toLowerCase()); - const memberRoles = ( - await this.resourceApiService.getMemberResourcesRoles( - challengeId, - user.userId, - ) - ).filter((resource) => + const userRoles = await this.resourceApiService.getMemberResourcesRoles( + challengeId, + user.userId, + ); + + const memberRoles = userRoles.filter((resource) => requiredRoles.some( (role) => resource.roleName!.toLowerCase().indexOf(role.toLowerCase()) >= 0, @@ -486,4 +487,82 @@ export class AiWorkflowService { return items; } + + async updateRunItem( + workflowId: string, + runId: string, + itemId: string, + patchData: UpdateAiWorkflowRunItemDto, + user: JwtUser, + ) { + const workflow = await this.prisma.aiWorkflow.findUnique({ + where: { id: workflowId }, + }); + if (!workflow) { + this.logger.error(`Workflow with id ${workflowId} not found.`); + throw new NotFoundException(`Workflow with id ${workflowId} not found.`); + } + + const run = await this.prisma.aiWorkflowRun.findUnique({ + where: { id: runId }, + }); + if (!run || run.workflowId !== workflowId) { + this.logger.error( + `Run with id ${runId} not found or does not belong to workflow ${workflowId}.`, + ); + throw new NotFoundException( + `Run with id ${runId} not found or does not belong to workflow ${workflowId}.`, + ); + } + + const runItem = await this.prisma.aiWorkflowRunItem.findUnique({ + where: { id: itemId }, + }); + if (!runItem || runItem.workflowRunId !== runId) { + this.logger.error( + `Run item with id ${itemId} not found or does not belong to run ${runId}.`, + ); + throw new NotFoundException( + `Run item with id ${itemId} not found or does not belong to run ${runId}.`, + ); + } + + const updateData: any = {}; + + if (patchData.upVotes !== undefined) { + updateData.upVotes = patchData.upVotes; + } + if (patchData.downVotes !== undefined) { + updateData.downVotes = patchData.downVotes; + } + + if (!user.isMachine) { + const keys = Object.keys(patchData); + const prohibitedKeys = ['content', 'questionScore']; + if (keys.some((key) => prohibitedKeys.includes(key))) { + throw new BadRequestException( + `Users cannot update one of these properties - ${prohibitedKeys.join(',')}`, + ); + } + } + + // Update properties which can be updated only via m2m + if (user.isMachine) { + if (patchData.content) { + updateData.content = patchData.content; + } + + if (patchData.questionScore) { + updateData.questionScore = patchData.questionScore; + } + } + + return this.prisma.aiWorkflowRunItem.update({ + where: { id: itemId }, + include: { + comments: true, + }, + data: updateData, + }); + } } diff --git a/src/dto/aiWorkflow.dto.ts b/src/dto/aiWorkflow.dto.ts index b5332b3..67689a8 100644 --- a/src/dto/aiWorkflow.dto.ts +++ b/src/dto/aiWorkflow.dto.ts @@ -9,7 +9,9 @@ import { IsInt, IsDate, Min, + IsUUID, Max, + IsEmpty, } from 'class-validator'; import { Type, Transform } from 'class-transformer'; @@ -138,3 +140,28 @@ export class CreateAiWorkflowRunItemsDto { @Type(() => CreateAiWorkflowRunItemDto) items: CreateAiWorkflowRunItemDto[]; } + +export class CommentDto { + @ApiProperty({ required: false }) + @IsOptional() + @IsUUID() + id?: string; + + @ApiProperty() + @IsString() + @Transform(trimTransformer) + @IsNotEmpty() + content: string; + + @ApiProperty({ required: false }) + @IsOptional() + @IsUUID() + parentId?: string; +} + +export class UpdateAiWorkflowRunItemDto extends PartialType( + CreateAiWorkflowRunItemDto, +) { + @IsEmpty({ message: 'scorecardQuestionId cannot be updated' }) + scorecardQuestionId?: never; +}