feat(be): implement polygon tool upload#3527
Conversation
There was a problem hiding this comment.
Code Review
This pull request introduces functionality for managing Polygon tool files, including uploading and deleting tools with content stored directly in the database and notifying other services via RabbitMQ. Key feedback includes addressing a missing class definition for PolygonAMQPService that prevents compilation, implementing file type validation to avoid data corruption when converting uploads to UTF-8 strings, and adding error handling for the deletion of non-existent records to prevent runtime exceptions.
| } | ||
| chunks.push(chunk) | ||
| } | ||
| const fileContent = Buffer.concat(chunks).toString('utf-8') |
| } | ||
|
|
||
| async deletePolygonFile(problemId: number, toolType: ToolType) { | ||
| return await this.prisma.polygonTool.delete({ |
There was a problem hiding this comment.
… & update rabbitmq constants
There was a problem hiding this comment.
Pull request overview
Implements the upload pipeline for Polygon "tools" (generator/validator/checker) and lays down the RabbitMQ plumbing needed for the admin backend to ask Iris to execute them. The schema is migrated from S3-based file paths to in-DB source content, a new PolygonAMQPService exposes publish/subscribe helpers for two new result queues, and admin-side services + a resolver wire file uploads and execution-trigger mutations together.
Changes:
- Migrate
PolygonSolution/PolygonToolfromfilePath(S3 key) tofileContent(TEXT) and add a destructive Prisma migration. - Add Polygon RabbitMQ exchange/keys/queues and a new
PolygonAMQPServicewith subscribers and publish helpers (validator at middle priority). - Add admin-side
FileService(upsert/delete tool source code, 10MB cap),PolygonPublicationService, and resolver mutationsuploadPolygonTool/deletePolygonTool.
Reviewed changes
Copilot reviewed 12 out of 12 changed files in this pull request and generated 10 comments.
Show a summary per file
| File | Description |
|---|---|
| apps/backend/prisma/schema.prisma | Replace filePath with fileContent on PolygonSolution and PolygonTool. |
| apps/backend/prisma/migrations/20260416151922_fix_file_path_into_file_content/migration.sql | Drop file_path and add NOT NULL file_content columns. |
| apps/backend/libs/constants/src/rabbitmq.constants.ts | Add Polygon exchange, routing keys, queues, and message-type constants. |
| apps/backend/libs/amqp/src/amqp.service.ts | New PolygonAMQPService with generator/validator subscribers, publishers, and handler registration. |
| apps/backend/libs/amqp/src/amqp.module.ts | Register and export PolygonAMQPService. |
| apps/backend/apps/admin/src/polygon/polygon.service.ts | Delegate upload/delete to FileService and run-* to PolygonPublicationService. |
| apps/backend/apps/admin/src/polygon/polygon.resolver.ts | Add uploadPolygonTool and deletePolygonTool GraphQL mutations. |
| apps/backend/apps/admin/src/polygon/polygon.module.ts | Add AMQPModule import and FileService provider. |
| apps/backend/apps/admin/src/polygon/polygon-pub.service.ts | New service that loads tool/solution rows and publishes execution requests. |
| apps/backend/apps/admin/src/polygon/file/file.service.ts | Stream-read file uploads with a 10MB cap and upsert/delete PolygonTool rows. |
| apps/backend/apps/admin/src/polygon/interface/polygonToolRequest.interface.ts | Define GeneratorRequest / ValidatorRequest payload shapes for Iris. |
| apps/backend/apps/admin/src/polygon/interface/polygonToolResult.interface.ts | Define GeneratorResultMessage / ValidatorResultMessage payload shapes from Iris. |
💡 Add Copilot custom instructions for smarter, more guided reviews. Learn how to get started.
| import { Language, ToolType } from '@prisma/client' | ||
| import type { PolygonAMQPService } from '@libs/amqp' | ||
| import type { PrismaService } from '@libs/prisma' | ||
|
|
||
| export class PolygonPublicationService { | ||
| constructor( | ||
| private readonly prisma: PrismaService, | ||
| private readonly amqpService: PolygonAMQPService | ||
| ) {} |
| imports: [RolesModule, AMQPModule], | ||
| providers: [PolygonResolver, PolygonService, FileService] |
| @@ -1,10 +1,34 @@ | |||
| import { Resolver } from '@nestjs/graphql' | |||
| import { Args, Int, Mutation, Resolver } from '@nestjs/graphql' | |||
| import { ToolType } from '@prisma/client' | |||
| export const POLYGON_VALIDATOR_MESSAGE_TYPE = 'validator' | ||
|
|
||
| export const POLYGON_CHECKER_KEY = 'polygon.checker' | ||
| export const POLYGON_CHECKER_MESSAGE_TYPE = 'checker' |
| import type { | ||
| GeneratorRequest, | ||
| ValidatorRequest | ||
| } from '@admin/polygon/interface/polygonToolRequest.interface' | ||
|
|
| await this.amqpService.publishGeneratorMessage({ | ||
| problemId, | ||
| generatorLanguage: Language.Cpp, | ||
| generatorCode: generator.fileContent, | ||
| generatorArgs, | ||
| solutionLanguage: solution.language, | ||
| solutionCode: solution.fileContent, | ||
| testCaseCount | ||
| }) | ||
| } | ||
|
|
||
| async publishValidatorMessage(problemId: number) { | ||
| const validator = await this.prisma.polygonTool.findUniqueOrThrow({ | ||
| where: { | ||
| // eslint-disable-next-line @typescript-eslint/naming-convention | ||
| problemId_toolType: { problemId, toolType: ToolType.Validator } | ||
| } | ||
| }) | ||
|
|
||
| await this.amqpService.publishValidatorMessage({ | ||
| problemId, | ||
| language: Language.Cpp, | ||
| validatorCode: validator.fileContent | ||
| }) |
| file: FileUpload | ||
| ) { | ||
| //DB에 파일 저장 | ||
| await this.fileService.uploadPolygonToolFile(problemId, toolType, file) |
| Warnings: | ||
|
|
||
| - You are about to drop the column `file_path` on the `polygon_solution` table. All the data in the column will be lost. | ||
| - You are about to drop the column `file_path` on the `polygon_tool` table. All the data in the column will be lost. | ||
| - Added the required column `file_content` to the `polygon_solution` table without a default value. This is not possible if the table is not empty. | ||
| - Added the required column `file_content` to the `polygon_tool` table without a default value. This is not possible if the table is not empty. | ||
|
|
||
| */ | ||
| -- AlterTable | ||
| ALTER TABLE "public"."polygon_solution" DROP COLUMN "file_path", | ||
| ADD COLUMN "file_content" TEXT NOT NULL; | ||
|
|
||
| -- AlterTable | ||
| ALTER TABLE "public"."polygon_tool" DROP COLUMN "file_path", | ||
| ADD COLUMN "file_content" TEXT NOT NULL; |
| const chunks: Buffer[] = [] | ||
| let total = 0 | ||
| for await (const chunk of createReadStream()) { | ||
| total += chunk.length | ||
| if (total > MAX_TOOL_FILE_SIZE) { | ||
| throw new UnprocessableDataException('File size exceeds maximum limit') | ||
| } | ||
| chunks.push(chunk) | ||
| } | ||
| const fileContent = Buffer.concat(chunks).toString('utf-8') | ||
|
|
||
| // (problemId, toolType) unique — 재업로드 시 갱신 | ||
| const tool = await this.prisma.polygonTool.upsert({ | ||
| // eslint-disable-next-line @typescript-eslint/naming-convention | ||
| where: { problemId_toolType: { problemId, toolType } }, | ||
| update: { fileName: filename, fileContent }, | ||
| create: { problemId, toolType, fileName: filename, fileContent } | ||
| }) |
| startGeneratorSubscription() { | ||
| //결과메시지 도착하면 콜백 실행됨 | ||
| this.amqpConnection.createSubscriber( | ||
| //@golevelup/nestjs-rabbitmq 버전이 업데이트 되면서 생긴 문제? | ||
| async (msg: object | undefined) => { | ||
| try { | ||
| if (!msg) return //undefined인 경우 메시지 큐에서 제거 | ||
| //onGenerateResult 핸들러가 등록되어 있으면 | ||
| if (this.messageHandlers?.onGenerateResult) { | ||
| await this.messageHandlers.onGenerateResult(msg) //onGenerateResult() 실행 | ||
| } | ||
| } catch (error) { | ||
| this.logger.error( | ||
| error, | ||
| 'Unexpected error in handling generator result message' | ||
| ) | ||
| return new Nack() | ||
| } | ||
| }, | ||
| { | ||
| exchange: POLYGON_EXCHANGE, | ||
| routingKey: POLYGON_GENERATOR_RESULT_KEY, //결과 큐를 분리할건지 통합할건지 조율해야됨. | ||
| queue: POLYGON_GENERATOR_RESULT_QUEUE | ||
| }, | ||
| ORIGIN_HANDLER_NAME | ||
| ) | ||
| } | ||
|
|
||
| startValidatorSubscription() { | ||
| //결과메시지 도착하면 콜백 실행됨 | ||
| this.amqpConnection.createSubscriber( | ||
| //@golevelup/nestjs-rabbitmq 버전이 업데이트 되면서 생긴 문제? | ||
| async (msg: object | undefined) => { | ||
| try { | ||
| if (!msg) return //undefined인 경우 메시지 큐에서 제거 | ||
| //onValidateResult 핸들러가 등록되어 있으면 | ||
| if (this.messageHandlers?.onValidateResult) { | ||
| await this.messageHandlers.onValidateResult(msg) //onValidateResult() 실행 | ||
| } | ||
| } catch (error) { | ||
| this.logger.error( | ||
| error, | ||
| 'Unexpected error in handling validator result message' | ||
| ) | ||
| return new Nack() | ||
| } | ||
| }, | ||
| { | ||
| exchange: POLYGON_EXCHANGE, | ||
| routingKey: POLYGON_VALIDATOR_RESULT_KEY, //결과 큐를 분리할건지 통합할건지 조율해야됨. | ||
| queue: POLYGON_VALIDATOR_RESULT_QUEUE | ||
| }, | ||
| ORIGIN_HANDLER_NAME | ||
| ) | ||
| } |
요약
Polygon tool 업로드/삭제 기능과 Generator/Validator 실행 요청을 위한 AMQP publish/subscribe 로직을 추가했습니다.
이번 변경에서는 Polygon tool/solution 파일을 S3 path 대신 DB
fileContent로 저장하도록 schema를 조정하고, 업로드된 Generator/Validator 코드를 Iris로 전달할 수 있는 메시지 발행 흐름을 구성했습니다. 또한 Iris로부터 돌아오는 Polygon 실행 결과를 백엔드에서 소비할 수 있도록 subscription service와 result DTO 검증 구조를 추가했습니다.변경사항
FileService추가EntityNotExistException처리PolygonAMQPService추가PolygonSubscriptionService추가OnModuleInit에서 AMQP handler 등록class-validator기반 DTO 검증PolygonSolution.filePath->fileContentPolygonTool.filePath->fileContentFlow
flowchart TD A["Admin uploads Polygon tool"] --> B["GraphQL Mutation: uploadPolygonTool"] B --> C["PolygonService"] C --> D["FileService"] D --> E["polygonTool upsert<br/>fileName + fileContent"] F["Run Generator / Validator"] --> G["PolygonPublicationService"] G --> H["Load tool / solution from DB"] H --> I["PolygonAMQPService"] I --> J["Publish request to Iris<br/>polygon.generator / polygon.validator"] K["Iris returns result message"] --> L["PolygonAMQPService subscriber"] L --> M["PolygonSubscriptionService"] M --> N["plainToInstance + validateOrReject"] N --> O{"Message valid?"} O -- "No" --> P["Log validation error<br/>throw for Nack"] O -- "Yes" --> Q["handleGeneratorResult / handleValidatorResult"] Q --> R["TODO: Persist result / update Polygon state"]