From 743356e84fdbeb26a1be4fbee59fb4712dcc4911 Mon Sep 17 00:00:00 2001 From: Hentry Martin Date: Thu, 11 Sep 2025 23:51:07 +0200 Subject: [PATCH 1/4] feat: create workflow run items --- .circleci/config.yml | 1 + src/api/ai-workflow/ai-workflow.controller.ts | 16 ++++++ src/api/ai-workflow/ai-workflow.service.ts | 50 +++++++++++++++++++ src/dto/aiWorkflow.dto.ts | 39 ++++++++++++++- src/shared/enums/scopes.enum.ts | 1 + 5 files changed, 106 insertions(+), 1 deletion(-) diff --git a/.circleci/config.yml b/.circleci/config.yml index 4d90337..213228d 100644 --- a/.circleci/config.yml +++ b/.circleci/config.yml @@ -75,6 +75,7 @@ workflows: only: - develop - feat/ai-workflows + - pm-1791 - '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 182d0d3..8e5deee 100644 --- a/src/api/ai-workflow/ai-workflow.controller.ts +++ b/src/api/ai-workflow/ai-workflow.controller.ts @@ -19,6 +19,7 @@ import { AiWorkflowService } from './ai-workflow.service'; import { CreateAiWorkflowDto, UpdateAiWorkflowDto, + CreateAiWorkflowRunItemsDto, } from '../../dto/aiWorkflow.dto'; import { Scopes } from 'src/shared/decorators/scopes.decorator'; import { UserRole } from 'src/shared/enums/userRole.enum'; @@ -44,6 +45,21 @@ export class AiWorkflowController { return this.aiWorkflowService.createWithValidation(createAiWorkflowDto); } + @Post('/:workflowId/runs/:runId/items') + @Scopes(Scope.CreateWorkflowRun) + @ApiOperation({ summary: 'Create AIWorkflowRunItems in batch' }) + @ApiResponse({ status: 201, description: 'AIWorkflowRunItems created successfully.' }) + @ApiResponse({ status: 400, description: 'Bad Request.' }) + @ApiResponse({ status: 401, description: 'Unauthorized.' }) + @ApiResponse({ status: 404, description: 'Workflow or Run not found.' }) + async createRunItems( + @Param('workflowId') workflowId: string, + @Param('runId') runId: string, + @Body(new ValidationPipe({ whitelist: true, transform: true })) createItemsDto: CreateAiWorkflowRunItemsDto, + ) { + return this.aiWorkflowService.createRunItemsBatch(workflowId, runId, createItemsDto.items); + } + @Get(':id') @Roles(UserRole.Admin, UserRole.User, UserRole.Copilot, UserRole.Reviewer) @Scopes(Scope.ReadWorkflow) diff --git a/src/api/ai-workflow/ai-workflow.service.ts b/src/api/ai-workflow/ai-workflow.service.ts index 8fba6b1..9b1f90f 100644 --- a/src/api/ai-workflow/ai-workflow.service.ts +++ b/src/api/ai-workflow/ai-workflow.service.ts @@ -126,4 +126,54 @@ export class AiWorkflowService { data: updateDto, }); } + + async createRunItemsBatch(workflowId: string, runId: string, items: any[]) { + 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}.`); + } + + for (const item of items) { + if (!item.scorecardQuestionId || !item.content) { + this.logger.error(`Invalid item: scorecardQuestionId and content are required.`); + throw new BadRequestException(`Each item must have scorecardQuestionId and content.`); + } + const questionExists = await this.prisma.scorecardQuestion.findUnique({ + where: { id: item.scorecardQuestionId }, + }); + if (!questionExists) { + this.logger.error(`ScorecardQuestion with id ${item.scorecardQuestionId} not found.`); + throw new BadRequestException(`ScorecardQuestion with id ${item.scorecardQuestionId} not found.`); + } + } + + const createdItems = await this.prisma.aiWorkflowRunItem.createMany({ + data: items.map(item => ({ + workflowRunId: runId, + scorecardQuestionId: item.scorecardQuestionId, + content: item.content, + upVotes: item.upVotes ?? 0, + downVotes: item.downVotes ?? 0, + questionScore: item.questionScore ?? null, + createdAt: new Date(), + // TODO: Remove this once prisma middleware implementation is done + createdBy: '', + })), + skipDuplicates: true, + }); + + return { createdCount: createdItems.count }; + } } diff --git a/src/dto/aiWorkflow.dto.ts b/src/dto/aiWorkflow.dto.ts index 6adaf73..e7c407a 100644 --- a/src/dto/aiWorkflow.dto.ts +++ b/src/dto/aiWorkflow.dto.ts @@ -1,5 +1,6 @@ import { ApiProperty, PartialType } from '@nestjs/swagger'; -import { IsString, IsNotEmpty } from 'class-validator'; +import { IsString, IsNotEmpty, IsArray, ValidateNested, IsOptional, IsInt, Min } from 'class-validator'; +import { Type } from 'class-transformer'; export class CreateAiWorkflowDto { @ApiProperty() @@ -39,3 +40,39 @@ export class CreateAiWorkflowDto { } export class UpdateAiWorkflowDto extends PartialType(CreateAiWorkflowDto) {} + +export class CreateAiWorkflowRunItemDto { + @ApiProperty() + @IsString() + @IsNotEmpty() + scorecardQuestionId: string; + + @ApiProperty() + @IsString() + @IsNotEmpty() + content: string; + + @ApiProperty({ required: false }) + @IsOptional() + @IsInt() + @Min(0) + upVotes?: number; + + @ApiProperty({ required: false }) + @IsOptional() + @IsInt() + @Min(0) + downVotes?: number; + + @ApiProperty({ required: false }) + @IsOptional() + questionScore?: number; +} + +export class CreateAiWorkflowRunItemsDto { + @ApiProperty({ type: [CreateAiWorkflowRunItemDto] }) + @IsArray() + @ValidateNested({ each: true }) + @Type(() => CreateAiWorkflowRunItemDto) + items: CreateAiWorkflowRunItemDto[]; +} diff --git a/src/shared/enums/scopes.enum.ts b/src/shared/enums/scopes.enum.ts index 7448970..5f2daff 100644 --- a/src/shared/enums/scopes.enum.ts +++ b/src/shared/enums/scopes.enum.ts @@ -68,6 +68,7 @@ export enum Scope { CreateWorkflow = 'create:workflow', ReadWorkflow = 'read:workflow', UpdateWorkflow = 'update:workflow', + CreateWorkflowRun = 'create:workflow-run', } /** From 6b2808b53e663ab7ce064dc46940ed6fb3fcbf73 Mon Sep 17 00:00:00 2001 From: Hentry Martin Date: Thu, 11 Sep 2025 23:52:44 +0200 Subject: [PATCH 2/4] fix: lint --- src/api/ai-workflow/ai-workflow.controller.ts | 14 +++++++--- src/api/ai-workflow/ai-workflow.service.ts | 26 ++++++++++++++----- src/dto/aiWorkflow.dto.ts | 10 ++++++- 3 files changed, 39 insertions(+), 11 deletions(-) diff --git a/src/api/ai-workflow/ai-workflow.controller.ts b/src/api/ai-workflow/ai-workflow.controller.ts index 8e5deee..2978f98 100644 --- a/src/api/ai-workflow/ai-workflow.controller.ts +++ b/src/api/ai-workflow/ai-workflow.controller.ts @@ -48,16 +48,24 @@ export class AiWorkflowController { @Post('/:workflowId/runs/:runId/items') @Scopes(Scope.CreateWorkflowRun) @ApiOperation({ summary: 'Create AIWorkflowRunItems in batch' }) - @ApiResponse({ status: 201, description: 'AIWorkflowRunItems created successfully.' }) + @ApiResponse({ + status: 201, + description: 'AIWorkflowRunItems created successfully.', + }) @ApiResponse({ status: 400, description: 'Bad Request.' }) @ApiResponse({ status: 401, description: 'Unauthorized.' }) @ApiResponse({ status: 404, description: 'Workflow or Run not found.' }) async createRunItems( @Param('workflowId') workflowId: string, @Param('runId') runId: string, - @Body(new ValidationPipe({ whitelist: true, transform: true })) createItemsDto: CreateAiWorkflowRunItemsDto, + @Body(new ValidationPipe({ whitelist: true, transform: true })) + createItemsDto: CreateAiWorkflowRunItemsDto, ) { - return this.aiWorkflowService.createRunItemsBatch(workflowId, runId, createItemsDto.items); + return this.aiWorkflowService.createRunItemsBatch( + workflowId, + runId, + createItemsDto.items, + ); } @Get(':id') diff --git a/src/api/ai-workflow/ai-workflow.service.ts b/src/api/ai-workflow/ai-workflow.service.ts index 9b1f90f..cbd0946 100644 --- a/src/api/ai-workflow/ai-workflow.service.ts +++ b/src/api/ai-workflow/ai-workflow.service.ts @@ -141,26 +141,38 @@ export class AiWorkflowService { 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}.`); + 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}.`, + ); } for (const item of items) { if (!item.scorecardQuestionId || !item.content) { - this.logger.error(`Invalid item: scorecardQuestionId and content are required.`); - throw new BadRequestException(`Each item must have scorecardQuestionId and content.`); + this.logger.error( + `Invalid item: scorecardQuestionId and content are required.`, + ); + throw new BadRequestException( + `Each item must have scorecardQuestionId and content.`, + ); } const questionExists = await this.prisma.scorecardQuestion.findUnique({ where: { id: item.scorecardQuestionId }, }); if (!questionExists) { - this.logger.error(`ScorecardQuestion with id ${item.scorecardQuestionId} not found.`); - throw new BadRequestException(`ScorecardQuestion with id ${item.scorecardQuestionId} not found.`); + this.logger.error( + `ScorecardQuestion with id ${item.scorecardQuestionId} not found.`, + ); + throw new BadRequestException( + `ScorecardQuestion with id ${item.scorecardQuestionId} not found.`, + ); } } const createdItems = await this.prisma.aiWorkflowRunItem.createMany({ - data: items.map(item => ({ + data: items.map((item) => ({ workflowRunId: runId, scorecardQuestionId: item.scorecardQuestionId, content: item.content, diff --git a/src/dto/aiWorkflow.dto.ts b/src/dto/aiWorkflow.dto.ts index e7c407a..1f1c330 100644 --- a/src/dto/aiWorkflow.dto.ts +++ b/src/dto/aiWorkflow.dto.ts @@ -1,5 +1,13 @@ import { ApiProperty, PartialType } from '@nestjs/swagger'; -import { IsString, IsNotEmpty, IsArray, ValidateNested, IsOptional, IsInt, Min } from 'class-validator'; +import { + IsString, + IsNotEmpty, + IsArray, + ValidateNested, + IsOptional, + IsInt, + Min, +} from 'class-validator'; import { Type } from 'class-transformer'; export class CreateAiWorkflowDto { From b4b8e8a70a79d9a952402bf79fbc411199c4d439 Mon Sep 17 00:00:00 2001 From: Hentry Martin Date: Fri, 12 Sep 2025 01:06:44 +0200 Subject: [PATCH 3/4] fix: lint --- src/api/ai-workflow/ai-workflow.service.ts | 1 - 1 file changed, 1 deletion(-) diff --git a/src/api/ai-workflow/ai-workflow.service.ts b/src/api/ai-workflow/ai-workflow.service.ts index cbd0946..3d7279b 100644 --- a/src/api/ai-workflow/ai-workflow.service.ts +++ b/src/api/ai-workflow/ai-workflow.service.ts @@ -183,7 +183,6 @@ export class AiWorkflowService { // TODO: Remove this once prisma middleware implementation is done createdBy: '', })), - skipDuplicates: true, }); return { createdCount: createdItems.count }; From 6bda99336edfa8a8fb48823a5751beebc93aa882 Mon Sep 17 00:00:00 2001 From: Hentry Martin Date: Fri, 12 Sep 2025 01:13:34 +0200 Subject: [PATCH 4/4] removed circle config --- .circleci/config.yml | 1 - 1 file changed, 1 deletion(-) diff --git a/.circleci/config.yml b/.circleci/config.yml index 213228d..4d90337 100644 --- a/.circleci/config.yml +++ b/.circleci/config.yml @@ -75,7 +75,6 @@ workflows: only: - develop - feat/ai-workflows - - pm-1791 - 'build-prod': context: org-global