From dc80999cc477785a3c58dd585aa978767132d645 Mon Sep 17 00:00:00 2001 From: Hentry Martin Date: Mon, 15 Sep 2025 22:59:31 +0200 Subject: [PATCH 01/20] feat: update workflow run item --- .circleci/config.yml | 1 + src/api/ai-workflow/ai-workflow.controller.ts | 24 ++++ src/api/ai-workflow/ai-workflow.service.ts | 106 ++++++++++++++++++ src/dto/aiWorkflow.dto.ts | 43 +++++++ 4 files changed, 174 insertions(+) diff --git a/.circleci/config.yml b/.circleci/config.yml index 4c07744..1ea53e6 100644 --- a/.circleci/config.yml +++ b/.circleci/config.yml @@ -75,6 +75,7 @@ workflows: only: - develop - feat/ai-workflows + - 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 000417e..103ff9c 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'; @@ -210,4 +211,27 @@ export class AiWorkflowController { ) { return this.aiWorkflowService.updateWorkflowRun(workflowId, runId, body); } + + @Patch('/:workflowId/runs/:runId/items/:itemId') + @Scopes(Scope.UpdateWorkflowRun) + @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); + } } diff --git a/src/api/ai-workflow/ai-workflow.service.ts b/src/api/ai-workflow/ai-workflow.service.ts index 3a2ff09..5c82b16 100644 --- a/src/api/ai-workflow/ai-workflow.service.ts +++ b/src/api/ai-workflow/ai-workflow.service.ts @@ -13,6 +13,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'; @@ -364,4 +365,109 @@ export class AiWorkflowService { throw error; } } + + 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.content !== undefined) { + updateData.content = patchData.content; + } + if (patchData.upVotes !== undefined) { + updateData.upVotes = patchData.upVotes; + } + if (patchData.downVotes !== undefined) { + updateData.downVotes = patchData.downVotes; + } + if (patchData.questionScore !== undefined) { + updateData.questionScore = patchData.questionScore; + } + + return this.prisma.$transaction(async (tx) => { + const updatedRunItem = await tx.aiWorkflowRunItem.update({ + where: { id: itemId }, + data: updateData, + }); + + if (patchData.comments) { + for (const comment of patchData.comments) { + if (comment.id) { + const existingComment = await tx.aiWorkflowRunItemComment.findUnique({ + where: { id: comment.id }, + }); + if (!existingComment) { + this.logger.error(`Comment with id ${comment.id} not found.`); + throw new NotFoundException(`Comment with id ${comment.id} not found.`); + } + + if (existingComment.createdBy !== user.userId && !user.roles?.includes(UserRole.Admin)) { + this.logger.error(`User ${user.userId} unauthorized to update comment ${comment.id}.`); + throw new ForbiddenException(`Unauthorized to update comment ${comment.id}.`); + } + + await tx.aiWorkflowRunItemComment.update({ + where: { id: comment.id }, + data: { + content: comment.content, + updatedAt: new Date(), + }, + }); + } else { + await tx.aiWorkflowRunItemComment.create({ + data: { + workflowRunItemId: itemId, + content: comment.content, + parentId: comment.parentId, + createdBy: '', + createdAt: new Date(), + userId: user.userId as string, + updatedAt: new Date(), + updatedBy: '', + }, + }); + } + } + } + + return updatedRunItem; + }); + } } diff --git a/src/dto/aiWorkflow.dto.ts b/src/dto/aiWorkflow.dto.ts index f91dcce..c42f98b 100644 --- a/src/dto/aiWorkflow.dto.ts +++ b/src/dto/aiWorkflow.dto.ts @@ -9,6 +9,8 @@ import { IsInt, IsDate, Min, + IsUUID, + IsBoolean, } from 'class-validator'; import { Type, Transform } from 'class-transformer'; @@ -134,3 +136,44 @@ export class CreateAiWorkflowRunItemsDto { @Type(() => CreateAiWorkflowRunItemDto) items: CreateAiWorkflowRunItemDto[]; } + +// New DTOs for update run item and comments + +export class CommentDto { + @ApiProperty({ required: false }) + @IsOptional() + @IsUUID() + id?: string; + + @ApiProperty() + @IsString() + @IsNotEmpty() + content: string; + + @ApiProperty({ required: false }) + @IsOptional() + @IsUUID() + parentId?: string; + + @ApiProperty() + @IsString() + @IsNotEmpty() + createdBy: string; + + @ApiProperty({ required: false }) + @IsOptional() + @IsDate() + @Transform(({ value }) => (value ? new Date(value) : undefined)) + createdAt?: Date; +} + +export class UpdateAiWorkflowRunItemDto extends PartialType( + OmitType(CreateAiWorkflowRunItemDto, ['scorecardQuestionId'] as const), +) { + @ApiProperty({ required: false }) + @IsOptional() + @IsArray() + @ValidateNested({ each: true }) + @Type(() => CommentDto) + comments?: CommentDto[]; +} From d22440471194d5dd20db8a3b1966969f58e50bf8 Mon Sep 17 00:00:00 2001 From: Hentry Martin Date: Mon, 15 Sep 2025 23:00:52 +0200 Subject: [PATCH 02/20] fix: lint --- src/api/ai-workflow/ai-workflow.controller.ts | 23 ++++++++++++++--- src/api/ai-workflow/ai-workflow.service.ts | 25 +++++++++++++------ src/dto/aiWorkflow.dto.ts | 1 - 3 files changed, 36 insertions(+), 13 deletions(-) diff --git a/src/api/ai-workflow/ai-workflow.controller.ts b/src/api/ai-workflow/ai-workflow.controller.ts index 103ff9c..96e6665 100644 --- a/src/api/ai-workflow/ai-workflow.controller.ts +++ b/src/api/ai-workflow/ai-workflow.controller.ts @@ -217,9 +217,18 @@ export class AiWorkflowController { @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.' }) + @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.' }) @@ -232,6 +241,12 @@ export class AiWorkflowController { patchData: UpdateAiWorkflowRunItemDto, @User() user: JwtUser, ) { - return this.aiWorkflowService.updateRunItem(workflowId, runId, itemId, patchData, user); + return this.aiWorkflowService.updateRunItem( + workflowId, + runId, + itemId, + patchData, + user, + ); } } diff --git a/src/api/ai-workflow/ai-workflow.service.ts b/src/api/ai-workflow/ai-workflow.service.ts index 5c82b16..9259825 100644 --- a/src/api/ai-workflow/ai-workflow.service.ts +++ b/src/api/ai-workflow/ai-workflow.service.ts @@ -373,7 +373,6 @@ export class AiWorkflowService { patchData: UpdateAiWorkflowRunItemDto, user: JwtUser, ) { - const workflow = await this.prisma.aiWorkflow.findUnique({ where: { id: workflowId }, }); @@ -430,17 +429,27 @@ export class AiWorkflowService { if (patchData.comments) { for (const comment of patchData.comments) { if (comment.id) { - const existingComment = await tx.aiWorkflowRunItemComment.findUnique({ - where: { id: comment.id }, - }); + const existingComment = + await tx.aiWorkflowRunItemComment.findUnique({ + where: { id: comment.id }, + }); if (!existingComment) { this.logger.error(`Comment with id ${comment.id} not found.`); - throw new NotFoundException(`Comment with id ${comment.id} not found.`); + throw new NotFoundException( + `Comment with id ${comment.id} not found.`, + ); } - if (existingComment.createdBy !== user.userId && !user.roles?.includes(UserRole.Admin)) { - this.logger.error(`User ${user.userId} unauthorized to update comment ${comment.id}.`); - throw new ForbiddenException(`Unauthorized to update comment ${comment.id}.`); + if ( + existingComment.createdBy !== user.userId && + !user.roles?.includes(UserRole.Admin) + ) { + this.logger.error( + `User ${user.userId} unauthorized to update comment ${comment.id}.`, + ); + throw new ForbiddenException( + `Unauthorized to update comment ${comment.id}.`, + ); } await tx.aiWorkflowRunItemComment.update({ diff --git a/src/dto/aiWorkflow.dto.ts b/src/dto/aiWorkflow.dto.ts index c42f98b..262de18 100644 --- a/src/dto/aiWorkflow.dto.ts +++ b/src/dto/aiWorkflow.dto.ts @@ -10,7 +10,6 @@ import { IsDate, Min, IsUUID, - IsBoolean, } from 'class-validator'; import { Type, Transform } from 'class-transformer'; From 56e99a310c46cc4ec3dd0a5c1f2680e0174b8708 Mon Sep 17 00:00:00 2001 From: Hentry Martin Date: Mon, 15 Sep 2025 23:01:58 +0200 Subject: [PATCH 03/20] fix: lint --- src/dto/aiWorkflow.dto.ts | 2 -- 1 file changed, 2 deletions(-) diff --git a/src/dto/aiWorkflow.dto.ts b/src/dto/aiWorkflow.dto.ts index 262de18..b556798 100644 --- a/src/dto/aiWorkflow.dto.ts +++ b/src/dto/aiWorkflow.dto.ts @@ -136,8 +136,6 @@ export class CreateAiWorkflowRunItemsDto { items: CreateAiWorkflowRunItemDto[]; } -// New DTOs for update run item and comments - export class CommentDto { @ApiProperty({ required: false }) @IsOptional() From 8251d86cc63b4d6673de923c4ebac5ca402e0210 Mon Sep 17 00:00:00 2001 From: Hentry Martin Date: Tue, 16 Sep 2025 22:42:53 +0200 Subject: [PATCH 04/20] fix: removed createdAt and createdBy from comment DTO --- src/dto/aiWorkflow.dto.ts | 11 ----------- 1 file changed, 11 deletions(-) diff --git a/src/dto/aiWorkflow.dto.ts b/src/dto/aiWorkflow.dto.ts index 60e73f4..65f3602 100644 --- a/src/dto/aiWorkflow.dto.ts +++ b/src/dto/aiWorkflow.dto.ts @@ -155,17 +155,6 @@ export class CommentDto { @IsOptional() @IsUUID() parentId?: string; - - @ApiProperty() - @IsString() - @IsNotEmpty() - createdBy: string; - - @ApiProperty({ required: false }) - @IsOptional() - @IsDate() - @Transform(({ value }) => (value ? new Date(value) : undefined)) - createdAt?: Date; } export class UpdateAiWorkflowRunItemDto extends PartialType( From 237d5d678b20cd1e39a2224bb2a894df8a684ac7 Mon Sep 17 00:00:00 2001 From: Hentry Martin Date: Tue, 16 Sep 2025 23:27:51 +0200 Subject: [PATCH 05/20] fix: removed createdAt and createdBy from comment DTO --- src/api/ai-workflow/ai-workflow.service.ts | 12 ++++++++++-- 1 file changed, 10 insertions(+), 2 deletions(-) diff --git a/src/api/ai-workflow/ai-workflow.service.ts b/src/api/ai-workflow/ai-workflow.service.ts index d47126f..7424688 100644 --- a/src/api/ai-workflow/ai-workflow.service.ts +++ b/src/api/ai-workflow/ai-workflow.service.ts @@ -543,8 +543,11 @@ export class AiWorkflowService { } return this.prisma.$transaction(async (tx) => { - const updatedRunItem = await tx.aiWorkflowRunItem.update({ + await tx.aiWorkflowRunItem.update({ where: { id: itemId }, + include: { + comments: true, + }, data: updateData, }); @@ -598,7 +601,12 @@ export class AiWorkflowService { } } - return updatedRunItem; + return tx.aiWorkflowRunItem.findUnique({ + where: { id: itemId }, + include: { + comments: true, + }, + }); }); } } From 997a68af2de0bd7b8617b9e9697bdeddbb328c64 Mon Sep 17 00:00:00 2001 From: Hentry Martin Date: Tue, 16 Sep 2025 23:44:02 +0200 Subject: [PATCH 06/20] fix: removed createdAt and createdBy from comment DTO --- src/dto/aiWorkflow.dto.ts | 1 + 1 file changed, 1 insertion(+) diff --git a/src/dto/aiWorkflow.dto.ts b/src/dto/aiWorkflow.dto.ts index 65f3602..8fe42df 100644 --- a/src/dto/aiWorkflow.dto.ts +++ b/src/dto/aiWorkflow.dto.ts @@ -148,6 +148,7 @@ export class CommentDto { @ApiProperty() @IsString() + @Transform(trimTransformer) @IsNotEmpty() content: string; From abf0b9a5340719b3f8ee1cee817f61eab9b06fca Mon Sep 17 00:00:00 2001 From: Hentry Martin Date: Wed, 17 Sep 2025 00:08:59 +0200 Subject: [PATCH 07/20] fix: removed createdAt and createdBy from comment DTO --- src/api/ai-workflow/ai-workflow.service.ts | 11 +++++++---- 1 file changed, 7 insertions(+), 4 deletions(-) diff --git a/src/api/ai-workflow/ai-workflow.service.ts b/src/api/ai-workflow/ai-workflow.service.ts index 7424688..76c0893 100644 --- a/src/api/ai-workflow/ai-workflow.service.ts +++ b/src/api/ai-workflow/ai-workflow.service.ts @@ -445,11 +445,14 @@ export class AiWorkflowService { UserRole.Submitter, ].map((r) => r.toLowerCase()); + const userRoles = await this.resourceApiService.getMemberResourcesRoles( + challengeId!, + user.userId, + ); + + this.logger.debug(userRoles); const memberRoles = ( - await this.resourceApiService.getMemberResourcesRoles( - challengeId, - user.userId, - ) + userRoles ).filter((resource) => requiredRoles.some( (role) => From ab1a8605f9245e323c3026dcca87e771792046df Mon Sep 17 00:00:00 2001 From: Hentry Martin Date: Wed, 17 Sep 2025 00:12:35 +0200 Subject: [PATCH 08/20] fix: removed createdAt and createdBy from comment DTO --- src/api/ai-workflow/ai-workflow.service.ts | 8 +++----- 1 file changed, 3 insertions(+), 5 deletions(-) diff --git a/src/api/ai-workflow/ai-workflow.service.ts b/src/api/ai-workflow/ai-workflow.service.ts index 76c0893..d737b6d 100644 --- a/src/api/ai-workflow/ai-workflow.service.ts +++ b/src/api/ai-workflow/ai-workflow.service.ts @@ -445,15 +445,13 @@ export class AiWorkflowService { UserRole.Submitter, ].map((r) => r.toLowerCase()); - const userRoles = await this.resourceApiService.getMemberResourcesRoles( - challengeId!, + const userRoles = await this.resourceApiService.getMemberResourcesRoles( + challengeId, user.userId, ); this.logger.debug(userRoles); - const memberRoles = ( - userRoles - ).filter((resource) => + const memberRoles = userRoles.filter((resource) => requiredRoles.some( (role) => resource.roleName!.toLowerCase().indexOf(role.toLowerCase()) >= 0, From 0a02608558a3fdde5fd4778b7d6e7775ed9d5732 Mon Sep 17 00:00:00 2001 From: Hentry Martin Date: Wed, 17 Sep 2025 00:38:55 +0200 Subject: [PATCH 09/20] fix: removed createdAt and createdBy from comment DTO --- src/api/ai-workflow/ai-workflow.service.ts | 178 +++++++++++---------- 1 file changed, 93 insertions(+), 85 deletions(-) diff --git a/src/api/ai-workflow/ai-workflow.service.ts b/src/api/ai-workflow/ai-workflow.service.ts index d737b6d..0a39068 100644 --- a/src/api/ai-workflow/ai-workflow.service.ts +++ b/src/api/ai-workflow/ai-workflow.service.ts @@ -390,103 +390,111 @@ export class AiWorkflowService { } async getRunItems(workflowId: string, runId: string, 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 }, - include: { workflow: true }, - }); - 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}.`, - ); - } + try { + 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 submission = run.submissionId - ? await this.prisma.submission.findUnique({ - where: { id: run.submissionId }, - }) - : null; - const challengeId = submission?.challengeId; + const run = await this.prisma.aiWorkflowRun.findUnique({ + where: { id: runId }, + include: { workflow: true }, + }); + 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}.`, + ); + } - if (!challengeId) { - this.logger.error( - `Challenge ID not found for submission ${run.submissionId}`, - ); - throw new InternalServerErrorException( - `Challenge ID not found for submission ${run.submissionId}`, - ); - } + const submission = run.submissionId + ? await this.prisma.submission.findUnique({ + where: { id: run.submissionId }, + }) + : null; + const challengeId = submission?.challengeId; - const challenge: ChallengeData = - await this.challengeApiService.getChallengeDetail(challengeId); + if (!challengeId) { + this.logger.error( + `Challenge ID not found for submission ${run.submissionId}`, + ); + throw new InternalServerErrorException( + `Challenge ID not found for submission ${run.submissionId}`, + ); + } - if (!challenge) { - throw new InternalServerErrorException( - `Challenge with id ${challengeId} was not found!`, - ); - } + const challenge: ChallengeData = + await this.challengeApiService.getChallengeDetail(challengeId); - const isM2mOrAdmin = user.isMachine || user.roles?.includes(UserRole.Admin); - if (!isM2mOrAdmin) { - const requiredRoles = [ - UserRole.Reviewer, - UserRole.ProjectManager, - UserRole.Copilot, - UserRole.Submitter, - ].map((r) => r.toLowerCase()); + if (!challenge) { + throw new InternalServerErrorException( + `Challenge with id ${challengeId} was not found!`, + ); + } - const userRoles = await this.resourceApiService.getMemberResourcesRoles( - challengeId, - user.userId, - ); + const isM2mOrAdmin = + user.isMachine || user.roles?.includes(UserRole.Admin); + if (!isM2mOrAdmin) { + const requiredRoles = [ + UserRole.Reviewer, + UserRole.ProjectManager, + UserRole.Copilot, + UserRole.Submitter, + ].map((r) => r.toLowerCase()); + + const userRoles = await this.resourceApiService.getMemberResourcesRoles( + challengeId, + user.userId, + ); - this.logger.debug(userRoles); - const memberRoles = userRoles.filter((resource) => - requiredRoles.some( - (role) => - resource.roleName!.toLowerCase().indexOf(role.toLowerCase()) >= 0, - ), - ); + this.logger.debug(userRoles); + const memberRoles = userRoles.filter((resource) => + requiredRoles.some( + (role) => + resource.roleName!.toLowerCase().indexOf(role.toLowerCase()) >= 0, + ), + ); - if (!memberRoles.length) { - throw new ForbiddenException('Insufficient permissions'); - } + if (!memberRoles.length) { + throw new ForbiddenException('Insufficient permissions'); + } - if ( - challenge.status !== ChallengeStatus.COMPLETED && - memberRoles.some( - (r) => r.roleName?.toLowerCase() === UserRole.Submitter.toLowerCase(), - ) && - user.userId !== submission?.memberId - ) { - this.logger.log( - `Submitter ${user.userId} trying to access AI workflow run for other submitters.`, - ); - throw new ForbiddenException('Insufficient permissions'); + if ( + challenge.status !== ChallengeStatus.COMPLETED && + memberRoles.some( + (r) => + r.roleName?.toLowerCase() === UserRole.Submitter.toLowerCase(), + ) && + user.userId !== submission?.memberId + ) { + this.logger.log( + `Submitter ${user.userId} trying to access AI workflow run for other submitters.`, + ); + throw new ForbiddenException('Insufficient permissions'); + } } - } - const items = await this.prisma.aiWorkflowRunItem.findMany({ - where: { workflowRunId: runId }, - include: { - comments: true, - }, - orderBy: { - createdAt: 'asc', - }, - }); + const items = await this.prisma.aiWorkflowRunItem.findMany({ + where: { workflowRunId: runId }, + include: { + comments: true, + }, + orderBy: { + createdAt: 'asc', + }, + }); - return items; + return items; + } catch (e) { + this.logger.debug(e, 'Error on getRunItems'); + } } async updateRunItem( From 88a8e1dde7974a023a6c9938a686cb95a16180e8 Mon Sep 17 00:00:00 2001 From: Hentry Martin Date: Wed, 17 Sep 2025 00:48:29 +0200 Subject: [PATCH 10/20] fix: removed createdAt and createdBy from comment DTO --- src/api/ai-workflow/ai-workflow.service.ts | 178 ++++++++++----------- 1 file changed, 85 insertions(+), 93 deletions(-) diff --git a/src/api/ai-workflow/ai-workflow.service.ts b/src/api/ai-workflow/ai-workflow.service.ts index 0a39068..d737b6d 100644 --- a/src/api/ai-workflow/ai-workflow.service.ts +++ b/src/api/ai-workflow/ai-workflow.service.ts @@ -390,111 +390,103 @@ export class AiWorkflowService { } async getRunItems(workflowId: string, runId: string, user: JwtUser) { - try { - 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 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 }, - include: { workflow: true }, - }); - 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 run = await this.prisma.aiWorkflowRun.findUnique({ + where: { id: runId }, + include: { workflow: true }, + }); + 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 submission = run.submissionId - ? await this.prisma.submission.findUnique({ - where: { id: run.submissionId }, - }) - : null; - const challengeId = submission?.challengeId; + const submission = run.submissionId + ? await this.prisma.submission.findUnique({ + where: { id: run.submissionId }, + }) + : null; + const challengeId = submission?.challengeId; - if (!challengeId) { - this.logger.error( - `Challenge ID not found for submission ${run.submissionId}`, - ); - throw new InternalServerErrorException( - `Challenge ID not found for submission ${run.submissionId}`, - ); - } + if (!challengeId) { + this.logger.error( + `Challenge ID not found for submission ${run.submissionId}`, + ); + throw new InternalServerErrorException( + `Challenge ID not found for submission ${run.submissionId}`, + ); + } - const challenge: ChallengeData = - await this.challengeApiService.getChallengeDetail(challengeId); + const challenge: ChallengeData = + await this.challengeApiService.getChallengeDetail(challengeId); - if (!challenge) { - throw new InternalServerErrorException( - `Challenge with id ${challengeId} was not found!`, - ); - } + if (!challenge) { + throw new InternalServerErrorException( + `Challenge with id ${challengeId} was not found!`, + ); + } - const isM2mOrAdmin = - user.isMachine || user.roles?.includes(UserRole.Admin); - if (!isM2mOrAdmin) { - const requiredRoles = [ - UserRole.Reviewer, - UserRole.ProjectManager, - UserRole.Copilot, - UserRole.Submitter, - ].map((r) => r.toLowerCase()); - - const userRoles = await this.resourceApiService.getMemberResourcesRoles( - challengeId, - user.userId, - ); + const isM2mOrAdmin = user.isMachine || user.roles?.includes(UserRole.Admin); + if (!isM2mOrAdmin) { + const requiredRoles = [ + UserRole.Reviewer, + UserRole.ProjectManager, + UserRole.Copilot, + UserRole.Submitter, + ].map((r) => r.toLowerCase()); - this.logger.debug(userRoles); - const memberRoles = userRoles.filter((resource) => - requiredRoles.some( - (role) => - resource.roleName!.toLowerCase().indexOf(role.toLowerCase()) >= 0, - ), - ); + const userRoles = await this.resourceApiService.getMemberResourcesRoles( + challengeId, + user.userId, + ); - if (!memberRoles.length) { - throw new ForbiddenException('Insufficient permissions'); - } + this.logger.debug(userRoles); + const memberRoles = userRoles.filter((resource) => + requiredRoles.some( + (role) => + resource.roleName!.toLowerCase().indexOf(role.toLowerCase()) >= 0, + ), + ); - if ( - challenge.status !== ChallengeStatus.COMPLETED && - memberRoles.some( - (r) => - r.roleName?.toLowerCase() === UserRole.Submitter.toLowerCase(), - ) && - user.userId !== submission?.memberId - ) { - this.logger.log( - `Submitter ${user.userId} trying to access AI workflow run for other submitters.`, - ); - throw new ForbiddenException('Insufficient permissions'); - } + if (!memberRoles.length) { + throw new ForbiddenException('Insufficient permissions'); } - const items = await this.prisma.aiWorkflowRunItem.findMany({ - where: { workflowRunId: runId }, - include: { - comments: true, - }, - orderBy: { - createdAt: 'asc', - }, - }); - - return items; - } catch (e) { - this.logger.debug(e, 'Error on getRunItems'); + if ( + challenge.status !== ChallengeStatus.COMPLETED && + memberRoles.some( + (r) => r.roleName?.toLowerCase() === UserRole.Submitter.toLowerCase(), + ) && + user.userId !== submission?.memberId + ) { + this.logger.log( + `Submitter ${user.userId} trying to access AI workflow run for other submitters.`, + ); + throw new ForbiddenException('Insufficient permissions'); + } } + + const items = await this.prisma.aiWorkflowRunItem.findMany({ + where: { workflowRunId: runId }, + include: { + comments: true, + }, + orderBy: { + createdAt: 'asc', + }, + }); + + return items; } async updateRunItem( From 7334871bb22d0de41d602d5c1af92e6aad3c6ac3 Mon Sep 17 00:00:00 2001 From: Hentry Martin Date: Wed, 17 Sep 2025 08:09:14 +0200 Subject: [PATCH 11/20] updated from develo[ --- src/api/ai-workflow/ai-workflow.service.ts | 2 ++ 1 file changed, 2 insertions(+) diff --git a/src/api/ai-workflow/ai-workflow.service.ts b/src/api/ai-workflow/ai-workflow.service.ts index d737b6d..0ab506f 100644 --- a/src/api/ai-workflow/ai-workflow.service.ts +++ b/src/api/ai-workflow/ai-workflow.service.ts @@ -451,6 +451,8 @@ export class AiWorkflowService { ); this.logger.debug(userRoles); + this.logger.debug(user); + this.logger.debug(submission); const memberRoles = userRoles.filter((resource) => requiredRoles.some( (role) => From f9b79f4a67c22e68f233c10da327071a42f71d3f Mon Sep 17 00:00:00 2001 From: Hentry Martin Date: Wed, 17 Sep 2025 08:24:27 +0200 Subject: [PATCH 12/20] updated from develop --- src/api/ai-workflow/ai-workflow.service.ts | 2 +- 1 file changed, 1 insertion(+), 1 deletion(-) diff --git a/src/api/ai-workflow/ai-workflow.service.ts b/src/api/ai-workflow/ai-workflow.service.ts index 0ab506f..b0a6a08 100644 --- a/src/api/ai-workflow/ai-workflow.service.ts +++ b/src/api/ai-workflow/ai-workflow.service.ts @@ -469,7 +469,7 @@ export class AiWorkflowService { memberRoles.some( (r) => r.roleName?.toLowerCase() === UserRole.Submitter.toLowerCase(), ) && - user.userId !== submission?.memberId + user.userId?.toString() !== submission?.memberId ) { this.logger.log( `Submitter ${user.userId} trying to access AI workflow run for other submitters.`, From 08c1a0e9df74bd3ed2a9f6d3a0d2ec60a50af575 Mon Sep 17 00:00:00 2001 From: Hentry Martin Date: Wed, 17 Sep 2025 17:50:10 +0200 Subject: [PATCH 13/20] fix: removed toString --- src/api/ai-workflow/ai-workflow.service.ts | 2 +- 1 file changed, 1 insertion(+), 1 deletion(-) diff --git a/src/api/ai-workflow/ai-workflow.service.ts b/src/api/ai-workflow/ai-workflow.service.ts index b0a6a08..0ab506f 100644 --- a/src/api/ai-workflow/ai-workflow.service.ts +++ b/src/api/ai-workflow/ai-workflow.service.ts @@ -469,7 +469,7 @@ export class AiWorkflowService { memberRoles.some( (r) => r.roleName?.toLowerCase() === UserRole.Submitter.toLowerCase(), ) && - user.userId?.toString() !== submission?.memberId + user.userId !== submission?.memberId ) { this.logger.log( `Submitter ${user.userId} trying to access AI workflow run for other submitters.`, From 65837e45a484b8874d2e3db452417833d679ec78 Mon Sep 17 00:00:00 2001 From: Hentry Martin Date: Wed, 17 Sep 2025 19:08:02 +0200 Subject: [PATCH 14/20] fix: lint --- src/api/ai-workflow/ai-workflow.controller.ts | 2 - src/api/ai-workflow/ai-workflow.service.ts | 72 ++----------------- 2 files changed, 6 insertions(+), 68 deletions(-) diff --git a/src/api/ai-workflow/ai-workflow.controller.ts b/src/api/ai-workflow/ai-workflow.controller.ts index 5d71bd5..e0cc4ac 100644 --- a/src/api/ai-workflow/ai-workflow.controller.ts +++ b/src/api/ai-workflow/ai-workflow.controller.ts @@ -255,14 +255,12 @@ export class AiWorkflowController { @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, ); } diff --git a/src/api/ai-workflow/ai-workflow.service.ts b/src/api/ai-workflow/ai-workflow.service.ts index b0a6a08..ef42d52 100644 --- a/src/api/ai-workflow/ai-workflow.service.ts +++ b/src/api/ai-workflow/ai-workflow.service.ts @@ -496,7 +496,6 @@ export class AiWorkflowService { runId: string, itemId: string, patchData: UpdateAiWorkflowRunItemDto, - user: JwtUser, ) { const workflow = await this.prisma.aiWorkflow.findUnique({ where: { id: workflowId }, @@ -545,71 +544,12 @@ export class AiWorkflowService { updateData.questionScore = patchData.questionScore; } - return this.prisma.$transaction(async (tx) => { - await tx.aiWorkflowRunItem.update({ - where: { id: itemId }, - include: { - comments: true, - }, - data: updateData, - }); - - if (patchData.comments) { - for (const comment of patchData.comments) { - if (comment.id) { - const existingComment = - await tx.aiWorkflowRunItemComment.findUnique({ - where: { id: comment.id }, - }); - if (!existingComment) { - this.logger.error(`Comment with id ${comment.id} not found.`); - throw new NotFoundException( - `Comment with id ${comment.id} not found.`, - ); - } - - if ( - existingComment.createdBy !== user.userId && - !user.roles?.includes(UserRole.Admin) - ) { - this.logger.error( - `User ${user.userId} unauthorized to update comment ${comment.id}.`, - ); - throw new ForbiddenException( - `Unauthorized to update comment ${comment.id}.`, - ); - } - - await tx.aiWorkflowRunItemComment.update({ - where: { id: comment.id }, - data: { - content: comment.content, - updatedAt: new Date(), - }, - }); - } else { - await tx.aiWorkflowRunItemComment.create({ - data: { - workflowRunItemId: itemId, - content: comment.content, - parentId: comment.parentId, - createdBy: '', - createdAt: new Date(), - userId: user.userId as string, - updatedAt: new Date(), - updatedBy: '', - }, - }); - } - } - } - - return tx.aiWorkflowRunItem.findUnique({ - where: { id: itemId }, - include: { - comments: true, - }, - }); + return this.prisma.aiWorkflowRunItem.update({ + where: { id: itemId }, + include: { + comments: true, + }, + data: updateData, }); } } From 72af621630d606f95330f11b7a0bded14cf51c02 Mon Sep 17 00:00:00 2001 From: Hentry Martin Date: Wed, 17 Sep 2025 19:20:25 +0200 Subject: [PATCH 15/20] fix: lint --- src/api/ai-workflow/ai-workflow.service.ts | 3 --- 1 file changed, 3 deletions(-) diff --git a/src/api/ai-workflow/ai-workflow.service.ts b/src/api/ai-workflow/ai-workflow.service.ts index ef42d52..3de733b 100644 --- a/src/api/ai-workflow/ai-workflow.service.ts +++ b/src/api/ai-workflow/ai-workflow.service.ts @@ -450,9 +450,6 @@ export class AiWorkflowService { user.userId, ); - this.logger.debug(userRoles); - this.logger.debug(user); - this.logger.debug(submission); const memberRoles = userRoles.filter((resource) => requiredRoles.some( (role) => From 01e2bbcaa4819c0873a3d95d0e58dc546166b9c7 Mon Sep 17 00:00:00 2001 From: Hentry Martin Date: Wed, 17 Sep 2025 22:39:55 +0200 Subject: [PATCH 16/20] fix: allowonly upvotes and downvotes to be updated --- src/api/ai-workflow/ai-workflow.service.ts | 19 +++++++++++++------ src/dto/aiWorkflow.dto.ts | 15 ++++++--------- 2 files changed, 19 insertions(+), 15 deletions(-) diff --git a/src/api/ai-workflow/ai-workflow.service.ts b/src/api/ai-workflow/ai-workflow.service.ts index 3de733b..a0eceb1 100644 --- a/src/api/ai-workflow/ai-workflow.service.ts +++ b/src/api/ai-workflow/ai-workflow.service.ts @@ -526,20 +526,27 @@ export class AiWorkflowService { ); } + const keys = Object.keys(patchData); + + const KeysProhibitedToUpdate = [ + 'scorecardQuestionId', + 'content', + 'questionScore', + ]; + if (keys.some((key) => KeysProhibitedToUpdate.includes(key))) { + throw new BadRequestException( + `Keys like ${KeysProhibitedToUpdate.join(',')} are not allowed to be updated`, + ); + } + const updateData: any = {}; - if (patchData.content !== undefined) { - updateData.content = patchData.content; - } if (patchData.upVotes !== undefined) { updateData.upVotes = patchData.upVotes; } if (patchData.downVotes !== undefined) { updateData.downVotes = patchData.downVotes; } - if (patchData.questionScore !== undefined) { - updateData.questionScore = patchData.questionScore; - } return this.prisma.aiWorkflowRunItem.update({ where: { id: itemId }, diff --git a/src/dto/aiWorkflow.dto.ts b/src/dto/aiWorkflow.dto.ts index 8fe42df..6bc6450 100644 --- a/src/dto/aiWorkflow.dto.ts +++ b/src/dto/aiWorkflow.dto.ts @@ -159,12 +159,9 @@ export class CommentDto { } export class UpdateAiWorkflowRunItemDto extends PartialType( - OmitType(CreateAiWorkflowRunItemDto, ['scorecardQuestionId'] as const), -) { - @ApiProperty({ required: false }) - @IsOptional() - @IsArray() - @ValidateNested({ each: true }) - @Type(() => CommentDto) - comments?: CommentDto[]; -} + OmitType(CreateAiWorkflowRunItemDto, [ + 'scorecardQuestionId', + 'content', + 'questionScore', + ] as const), +) {} From 3bdd63cb59ec7e718ad188e10b635779bc240e15 Mon Sep 17 00:00:00 2001 From: Hentry Martin Date: Wed, 17 Sep 2025 22:40:53 +0200 Subject: [PATCH 17/20] fix: allowonly upvotes and downvotes to be updated --- src/api/ai-workflow/ai-workflow.controller.ts | 8 ++++++++ 1 file changed, 8 insertions(+) diff --git a/src/api/ai-workflow/ai-workflow.controller.ts b/src/api/ai-workflow/ai-workflow.controller.ts index e0cc4ac..ac88b80 100644 --- a/src/api/ai-workflow/ai-workflow.controller.ts +++ b/src/api/ai-workflow/ai-workflow.controller.ts @@ -230,6 +230,14 @@ 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' }) From 7efc25f15dd25d09bcf8deae407e33f1ec6070d5 Mon Sep 17 00:00:00 2001 From: Hentry Martin Date: Wed, 17 Sep 2025 22:58:59 +0200 Subject: [PATCH 18/20] fix: allowonly upvotes and downvotes to be updated --- src/api/ai-workflow/ai-workflow.service.ts | 13 ------------- src/dto/aiWorkflow.dto.ts | 18 ++++++++++++------ 2 files changed, 12 insertions(+), 19 deletions(-) diff --git a/src/api/ai-workflow/ai-workflow.service.ts b/src/api/ai-workflow/ai-workflow.service.ts index a0eceb1..bc3ff89 100644 --- a/src/api/ai-workflow/ai-workflow.service.ts +++ b/src/api/ai-workflow/ai-workflow.service.ts @@ -526,19 +526,6 @@ export class AiWorkflowService { ); } - const keys = Object.keys(patchData); - - const KeysProhibitedToUpdate = [ - 'scorecardQuestionId', - 'content', - 'questionScore', - ]; - if (keys.some((key) => KeysProhibitedToUpdate.includes(key))) { - throw new BadRequestException( - `Keys like ${KeysProhibitedToUpdate.join(',')} are not allowed to be updated`, - ); - } - const updateData: any = {}; if (patchData.upVotes !== undefined) { diff --git a/src/dto/aiWorkflow.dto.ts b/src/dto/aiWorkflow.dto.ts index 6bc6450..06936f8 100644 --- a/src/dto/aiWorkflow.dto.ts +++ b/src/dto/aiWorkflow.dto.ts @@ -11,6 +11,7 @@ import { Min, IsUUID, Max, + IsEmpty, } from 'class-validator'; import { Type, Transform } from 'class-transformer'; @@ -159,9 +160,14 @@ export class CommentDto { } export class UpdateAiWorkflowRunItemDto extends PartialType( - OmitType(CreateAiWorkflowRunItemDto, [ - 'scorecardQuestionId', - 'content', - 'questionScore', - ] as const), -) {} + CreateAiWorkflowRunItemDto, +) { + @IsEmpty({ message: 'scorecardQuestionId cannot be updated' }) + scorecardQuestionId?: never; + + @IsEmpty({ message: 'content cannot be updated' }) + content?: never; + + @IsEmpty({ message: 'questionScore cannot be updated' }) + questionScore?: never; +} From 749617f68942cb20fd1ed4a13e6e5cdf2b52a4a2 Mon Sep 17 00:00:00 2001 From: Hentry Martin Date: Thu, 18 Sep 2025 13:38:43 +0200 Subject: [PATCH 19/20] fix: allow m2m tokens to update certain fields --- src/api/ai-workflow/ai-workflow.service.ts | 22 ++++++++++++++++++++++ src/dto/aiWorkflow.dto.ts | 6 ------ 2 files changed, 22 insertions(+), 6 deletions(-) diff --git a/src/api/ai-workflow/ai-workflow.service.ts b/src/api/ai-workflow/ai-workflow.service.ts index bc3ff89..542e257 100644 --- a/src/api/ai-workflow/ai-workflow.service.ts +++ b/src/api/ai-workflow/ai-workflow.service.ts @@ -493,6 +493,7 @@ export class AiWorkflowService { runId: string, itemId: string, patchData: UpdateAiWorkflowRunItemDto, + user: JwtUser, ) { const workflow = await this.prisma.aiWorkflow.findUnique({ where: { id: workflowId }, @@ -535,6 +536,27 @@ export class AiWorkflowService { 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: { diff --git a/src/dto/aiWorkflow.dto.ts b/src/dto/aiWorkflow.dto.ts index 06936f8..67689a8 100644 --- a/src/dto/aiWorkflow.dto.ts +++ b/src/dto/aiWorkflow.dto.ts @@ -164,10 +164,4 @@ export class UpdateAiWorkflowRunItemDto extends PartialType( ) { @IsEmpty({ message: 'scorecardQuestionId cannot be updated' }) scorecardQuestionId?: never; - - @IsEmpty({ message: 'content cannot be updated' }) - content?: never; - - @IsEmpty({ message: 'questionScore cannot be updated' }) - questionScore?: never; } From 25369e4e27ddf3fb26a841f2766d89edf1cc335b Mon Sep 17 00:00:00 2001 From: Hentry Martin Date: Thu, 18 Sep 2025 13:46:00 +0200 Subject: [PATCH 20/20] fix: allow m2m tokens to update certain fields --- src/api/ai-workflow/ai-workflow.controller.ts | 2 ++ 1 file changed, 2 insertions(+) diff --git a/src/api/ai-workflow/ai-workflow.controller.ts b/src/api/ai-workflow/ai-workflow.controller.ts index ac88b80..29b4b08 100644 --- a/src/api/ai-workflow/ai-workflow.controller.ts +++ b/src/api/ai-workflow/ai-workflow.controller.ts @@ -263,12 +263,14 @@ export class AiWorkflowController { @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, ); }