Skip to content

feat(be): implement polygon tool upload#3527

Open
zero1177 wants to merge 12 commits into
mainfrom
t2630-implement-file-upload
Open

feat(be): implement polygon tool upload#3527
zero1177 wants to merge 12 commits into
mainfrom
t2630-implement-file-upload

Conversation

@zero1177
Copy link
Copy Markdown
Contributor

@zero1177 zero1177 commented Apr 3, 2026

요약

Polygon tool 업로드/삭제 기능과 Generator/Validator 실행 요청을 위한 AMQP publish/subscribe 로직을 추가했습니다.

이번 변경에서는 Polygon tool/solution 파일을 S3 path 대신 DB fileContent로 저장하도록 schema를 조정하고, 업로드된 Generator/Validator 코드를 Iris로 전달할 수 있는 메시지 발행 흐름을 구성했습니다. 또한 Iris로부터 돌아오는 Polygon 실행 결과를 백엔드에서 소비할 수 있도록 subscription service와 result DTO 검증 구조를 추가했습니다.

변경사항

  • Polygon tool upload/delete GraphQL mutation 추가
  • Polygon tool 파일을 DB에 저장하는 FileService 추가
    • 최대 파일 크기 제한
    • 재업로드 시 upsert 처리
    • 없는 tool 삭제 시 EntityNotExistException 처리
  • Polygon Generator/Validator 실행 요청 publish 로직 추가
  • PolygonAMQPService 추가
    • generator request publish
    • validator request publish
    • generator result queue subscribe
    • validator result queue subscribe
  • PolygonSubscriptionService 추가
    • OnModuleInit에서 AMQP handler 등록
    • result message 수신
    • class-validator 기반 DTO 검증
    • 이후 비즈니스 로직을 넣을 handler 분리
  • Polygon AMQP request interface / result DTO 추가
  • Prisma schema 변경
    • PolygonSolution.filePath -> fileContent
    • PolygonTool.filePath -> fileContent
  • Polygon 관련 RabbitMQ constants 추가

Flow

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"]
Loading

@zero1177 zero1177 self-assigned this Apr 3, 2026
@zero1177 zero1177 added ⛳️ team-backend 🍊squad-유자차 스쿼드 유자차 작업물입니다 labels Apr 3, 2026
Copy link
Copy Markdown
Contributor

@gemini-code-assist gemini-code-assist Bot left a comment

Choose a reason for hiding this comment

The reason will be displayed to describe this comment to others. Learn more.

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.

Comment thread apps/backend/libs/amqp/src/amqp.module.ts
}
chunks.push(chunk)
}
const fileContent = Buffer.concat(chunks).toString('utf-8')
Copy link
Copy Markdown
Contributor

Choose a reason for hiding this comment

The reason will be displayed to describe this comment to others. Learn more.

medium

파일 내용을 utf-8 문자열로 변환하여 DB에 저장하고 있습니다. PR 설명에 따르면 .cpp 소스코드를 상정하고 있으나, 사용자가 바이너리 파일 등 의도하지 않은 형식의 파일을 업로드할 경우 데이터가 손상될 수 있습니다. 업로드된 파일의 mimetype이나 확장자를 검증하는 로직을 추가하여 의도한 형식의 파일만 처리하도록 하는 것이 안전합니다.

Copy link
Copy Markdown
Contributor Author

Choose a reason for hiding this comment

The reason will be displayed to describe this comment to others. Learn more.

확장자 검증 추가하겠습니다..!

}

async deletePolygonFile(problemId: number, toolType: ToolType) {
return await this.prisma.polygonTool.delete({
Copy link
Copy Markdown
Contributor

Choose a reason for hiding this comment

The reason will be displayed to describe this comment to others. Learn more.

medium

prisma.polygonTool.delete는 삭제하려는 레코드가 존재하지 않을 경우 에러를 발생시킵니다. 해당 도구가 이미 삭제되었거나 존재하지 않는 경우를 대비하여 예외 처리를 추가하거나, 존재 여부를 먼저 확인하는 것이 좋습니다. 단순히 삭제 여부만 중요하다면 deleteMany를 사용하는 것도 방법이지만, 현재 리턴 타입이 PolygonTool이므로 에러 발생 시 적절한 GraphQL 에러(예: NotFoundException)를 던지도록 처리하는 것이 더 명확합니다.

Copilot AI review requested due to automatic review settings May 13, 2026 13:06
Copy link
Copy Markdown
Contributor

Copilot AI left a comment

Choose a reason for hiding this comment

The reason will be displayed to describe this comment to others. Learn more.

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/PolygonTool from filePath (S3 key) to fileContent (TEXT) and add a destructive Prisma migration.
  • Add Polygon RabbitMQ exchange/keys/queues and a new PolygonAMQPService with subscribers and publish helpers (validator at middle priority).
  • Add admin-side FileService (upsert/delete tool source code, 10MB cap), PolygonPublicationService, and resolver mutations uploadPolygonTool / 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.

Comment on lines +1 to +9
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
) {}
Comment on lines +9 to +10
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'
Comment on lines +47 to +50
export const POLYGON_VALIDATOR_MESSAGE_TYPE = 'validator'

export const POLYGON_CHECKER_KEY = 'polygon.checker'
export const POLYGON_CHECKER_MESSAGE_TYPE = 'checker'
Comment on lines +32 to 36
import type {
GeneratorRequest,
ValidatorRequest
} from '@admin/polygon/interface/polygonToolRequest.interface'

Comment on lines +30 to +53
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)
Comment on lines +2 to +16
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;
Comment on lines +25 to +42
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 }
})
Comment on lines +239 to +293
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
)
}
Sign up for free to join this conversation on GitHub. Already have an account? Sign in to comment

Labels

🍊squad-유자차 스쿼드 유자차 작업물입니다 ⛳️ team-backend

Projects

None yet

Development

Successfully merging this pull request may close these issues.

2 participants