diff --git a/prisma/migrate.ts b/prisma/migrate.ts index c51dd16..3e80a6e 100644 --- a/prisma/migrate.ts +++ b/prisma/migrate.ts @@ -45,7 +45,7 @@ const modelMappingKeys = [ 'review_item_comment', 'llm_provider', 'llm_model', - 'ai_workflow' + 'ai_workflow', ]; const subModelMappingKeys = { review_item_comment: ['reviewItemComment', 'appeal', 'appealResponse'], @@ -813,27 +813,26 @@ async function processType(type: string, subtype?: string) { } case 'scorecard': { console.log(`[${type}][${file}] Processing file`); - const processedData = jsonData[key] - .map((sc) => { - const id = nanoid(14); - scorecardIdMap.set(sc.scorecard_id, id); - return { - id: id, - legacyId: sc.scorecard_id, - status: scorecardStatusMap[sc.scorecard_status_id], - type: scorecardTypeMap[sc.scorecard_type_id], - challengeTrack: projectCategoryMap[sc.project_category_id].type, - challengeType: projectCategoryMap[sc.project_category_id].name, - name: sc.name, - version: sc.version, - minScore: parseFloat(sc.min_score), - maxScore: parseFloat(sc.max_score), - createdAt: new Date(sc.create_date), - createdBy: sc.create_user, - updatedAt: new Date(sc.modify_date), - updatedBy: sc.modify_user, - }; - }); + const processedData = jsonData[key].map((sc) => { + const id = nanoid(14); + scorecardIdMap.set(sc.scorecard_id, id); + return { + id: id, + legacyId: sc.scorecard_id, + status: scorecardStatusMap[sc.scorecard_status_id], + type: scorecardTypeMap[sc.scorecard_type_id], + challengeTrack: projectCategoryMap[sc.project_category_id].type, + challengeType: projectCategoryMap[sc.project_category_id].name, + name: sc.name, + version: sc.version, + minScore: parseFloat(sc.min_score), + maxScore: parseFloat(sc.max_score), + createdAt: new Date(sc.create_date), + createdBy: sc.create_user, + updatedAt: new Date(sc.modify_date), + updatedBy: sc.modify_user, + }; + }); const totalBatches = Math.ceil(processedData.length / batchSize); for (let i = 0; i < processedData.length; i += batchSize) { const batchIndex = i / batchSize + 1; @@ -1350,13 +1349,9 @@ async function processType(type: string, subtype?: string) { case 'llm_provider': { console.log(`[${type}][${subtype}][${file}] Processing file`); const idToLegacyIdMap = {}; - const processedData = jsonData[key] - .map((c) => { + const processedData = jsonData[key].map((c) => { const id = nanoid(14); - llmProviderIdMap.set( - c.llm_provider_id, - id, - ); + llmProviderIdMap.set(c.llm_provider_id, id); idToLegacyIdMap[id] = c.llm_provider_id; return { id: id, @@ -1387,9 +1382,7 @@ async function processType(type: string, subtype?: string) { data: item, }) .catch((err) => { - llmProviderIdMap.delete( - idToLegacyIdMap[item.id], - ); + llmProviderIdMap.delete(idToLegacyIdMap[item.id]); console.error( `[${type}][${subtype}][${file}] Error code: ${err.code}, LegacyId: ${idToLegacyIdMap[item.id]}`, ); @@ -1402,15 +1395,11 @@ async function processType(type: string, subtype?: string) { case 'llm_model': { console.log(`[${type}][${subtype}][${file}] Processing file`); const idToLegacyIdMap = {}; - const processedData = jsonData[key] - .map((c) => { + const processedData = jsonData[key].map((c) => { const id = nanoid(14); - llmModelIdMap.set( - c.llm_model_id, - id, - ); + llmModelIdMap.set(c.llm_model_id, id); idToLegacyIdMap[id] = c.llm_model_id; - console.log(llmProviderIdMap.get(c.provider_id), 'c.provider_id') + console.log(llmProviderIdMap.get(c.provider_id), 'c.provider_id'); return { id: id, providerId: llmProviderIdMap.get(c.provider_id), @@ -1423,7 +1412,7 @@ async function processType(type: string, subtype?: string) { }; }); - console.log(llmProviderIdMap, processedData, 'processedData') + console.log(llmProviderIdMap, processedData, 'processedData'); const totalBatches = Math.ceil(processedData.length / batchSize); for (let i = 0; i < processedData.length; i += batchSize) { @@ -1446,9 +1435,7 @@ async function processType(type: string, subtype?: string) { data: item, }) .catch((err) => { - llmModelIdMap.delete( - idToLegacyIdMap[item.id], - ); + llmModelIdMap.delete(idToLegacyIdMap[item.id]); console.error( `[${type}][${subtype}][${file}] Error code: ${err.code}, LegacyId: ${idToLegacyIdMap[item.id]}`, ); @@ -1461,13 +1448,9 @@ async function processType(type: string, subtype?: string) { case 'ai_workflow': { console.log(`[${type}][${subtype}][${file}] Processing file`); const idToLegacyIdMap = {}; - const processedData = jsonData[key] - .map((c) => { + const processedData = jsonData[key].map((c) => { const id = nanoid(14); - aiWorkflowIdMap.set( - c.ai_workflow_id, - id, - ); + aiWorkflowIdMap.set(c.ai_workflow_id, id); idToLegacyIdMap[id] = c.ai_workflow_id; return { id: id, @@ -1506,9 +1489,7 @@ async function processType(type: string, subtype?: string) { data: item, }) .catch((err) => { - aiWorkflowIdMap.delete( - idToLegacyIdMap[item.id], - ); + aiWorkflowIdMap.delete(idToLegacyIdMap[item.id]); console.error( `[${type}][${subtype}][${file}] Error code: ${err.code}, LegacyId: ${idToLegacyIdMap[item.id]}`, ); @@ -1687,7 +1668,7 @@ migrate() { key: 'submissionIdMap', value: submissionIdMap }, { key: 'llmProviderIdMap', value: llmProviderIdMap }, { key: 'llmModelIdMap', value: llmModelIdMap }, - { key: 'aiWorkflowIdMap', value: aiWorkflowIdMap } + { key: 'aiWorkflowIdMap', value: aiWorkflowIdMap }, ].forEach((f) => { if (!fs.existsSync('.tmp')) { fs.mkdirSync('.tmp'); diff --git a/src/api/ai-workflow/ai-workflow.controller.ts b/src/api/ai-workflow/ai-workflow.controller.ts new file mode 100644 index 0000000..e1e61e4 --- /dev/null +++ b/src/api/ai-workflow/ai-workflow.controller.ts @@ -0,0 +1,33 @@ +import { Controller, Post, Body } from '@nestjs/common'; +import { + ApiBearerAuth, + ApiTags, + ApiOperation, + ApiResponse, +} from '@nestjs/swagger'; +import { AiWorkflowService } from './ai-workflow.service'; +import { CreateAiWorkflowDto } from '../../dto/aiWorkflow.dto'; +import { Scopes } from 'src/shared/decorators/scopes.decorator'; +import { UserRole } from 'src/shared/enums/userRole.enum'; +import { Scope } from 'src/shared/enums/scopes.enum'; +import { Roles } from 'src/shared/guards/tokenRoles.guard'; + +@ApiTags('ai_workflow') +@ApiBearerAuth() +@Controller('/workflows') +export class AiWorkflowController { + constructor(private readonly aiWorkflowService: AiWorkflowService) {} + + @Post() + @Roles(UserRole.Admin) + @Scopes(Scope.CreateWorkflow) + @ApiOperation({ summary: 'Create a new AI workflow' }) + @ApiResponse({ + status: 201, + description: 'The AI workflow has been successfully created.', + }) + @ApiResponse({ status: 403, description: 'Forbidden.' }) + async create(@Body() createAiWorkflowDto: CreateAiWorkflowDto) { + return this.aiWorkflowService.createWithValidation(createAiWorkflowDto); + } +} diff --git a/src/api/ai-workflow/ai-workflow.service.ts b/src/api/ai-workflow/ai-workflow.service.ts new file mode 100644 index 0000000..6e82176 --- /dev/null +++ b/src/api/ai-workflow/ai-workflow.service.ts @@ -0,0 +1,58 @@ +import { Injectable, BadRequestException } from '@nestjs/common'; +import { PrismaService } from '../../shared/modules/global/prisma.service'; +import { CreateAiWorkflowDto } from '../../dto/aiWorkflow.dto'; +import { ScorecardStatus } from 'src/dto/scorecard.dto'; + +@Injectable() +export class AiWorkflowService { + constructor(private readonly prisma: PrismaService) {} + + async scorecardExists(scorecardId: string): Promise { + const count = await this.prisma.scorecard.count({ + where: { id: scorecardId, status: ScorecardStatus.ACTIVE }, + }); + return count > 0; + } + + async llmModelExists(llmId: string): Promise { + const count = await this.prisma.llmModel.count({ + where: { id: llmId }, + }); + return count > 0; + } + + async createWithValidation(createAiWorkflowDto: CreateAiWorkflowDto) { + const { scorecardId, llmId, name, description, defUrl, gitId, gitOwner } = + createAiWorkflowDto; + + const scorecardExists = await this.scorecardExists(scorecardId); + if (!scorecardExists) { + throw new BadRequestException( + `Scorecard with id ${scorecardId} does not exist or is not active.`, + ); + } + + const llmExists = await this.llmModelExists(llmId); + if (!llmExists) { + throw new BadRequestException( + `LLM model with id ${llmId} does not exist.`, + ); + } + + return this.prisma.aiWorkflow.create({ + data: { + defUrl, + description, + gitId, + gitOwner, + name, + scorecardId, + llmId, + // TODO: This has to be removed once the prisma middleware is implemented + createdBy: '', + updatedAt: '', + updatedBy: '', + }, + }); + } +} diff --git a/src/api/api.module.ts b/src/api/api.module.ts index d78d250..2d9d30d 100644 --- a/src/api/api.module.ts +++ b/src/api/api.module.ts @@ -25,6 +25,8 @@ import { WebhookController } from './webhook/webhook.controller'; import { WebhookService } from './webhook/webhook.service'; import { GiteaWebhookAuthGuard } from '../shared/guards/gitea-webhook-auth.guard'; import { ScoreCardService } from './scorecard/scorecard.service'; +import { AiWorkflowService } from './ai-workflow/ai-workflow.service'; +import { AiWorkflowController } from './ai-workflow/ai-workflow.controller'; @Module({ imports: [HttpModule, GlobalProvidersModule, FileUploadModule], @@ -42,6 +44,7 @@ import { ScoreCardService } from './scorecard/scorecard.service'; ReviewApplicationController, ReviewHistoryController, WebhookController, + AiWorkflowController, ], providers: [ ReviewOpportunityService, @@ -53,6 +56,7 @@ import { ScoreCardService } from './scorecard/scorecard.service'; ScoreCardService, SubmissionService, ReviewSummationService, + AiWorkflowService, ], }) export class ApiModule {} diff --git a/src/dto/aiWorkflow.dto.ts b/src/dto/aiWorkflow.dto.ts new file mode 100644 index 0000000..5f16fa4 --- /dev/null +++ b/src/dto/aiWorkflow.dto.ts @@ -0,0 +1,39 @@ +import { ApiProperty } from '@nestjs/swagger'; +import { IsString, IsNotEmpty } from 'class-validator'; + +export class CreateAiWorkflowDto { + @ApiProperty() + @IsString() + @IsNotEmpty() + name: string; + + @ApiProperty() + @IsString() + @IsNotEmpty() + llmId: string; + + @ApiProperty() + @IsString() + @IsNotEmpty() + description: string; + + @ApiProperty() + @IsString() + @IsNotEmpty() + defUrl: string; + + @ApiProperty() + @IsString() + @IsNotEmpty() + gitId: string; + + @ApiProperty() + @IsString() + @IsNotEmpty() + gitOwner: string; + + @ApiProperty() + @IsString() + @IsNotEmpty() + scorecardId: string; +} diff --git a/src/shared/enums/scopes.enum.ts b/src/shared/enums/scopes.enum.ts index 4424271..bb258cc 100644 --- a/src/shared/enums/scopes.enum.ts +++ b/src/shared/enums/scopes.enum.ts @@ -63,6 +63,9 @@ export enum Scope { UpdateSubmission = 'update:submission', DeleteSubmission = 'delete:submission', AllSubmission = 'all:submission', + + // AI workflow scopes + CreateWorkflow = 'create:workflow', } /**