From 7b032015ad6e84f4b11f829444a494c5fe9bbc21 Mon Sep 17 00:00:00 2001 From: Aonrok <73125291+XiaoXuxxxx@users.noreply.github.com> Date: Thu, 3 Aug 2023 15:40:58 +0700 Subject: [PATCH] Add Create question route and service (#16) * feat: add create grading question proto and types * feat: add create workspace question proto and types * feat: add create grading question repository * feat: add create grading testcase repository * feat: add create grading question route and service * feat: create update workspace question repo * feat: add create workspace question service and route * feat: add guard for workspace owner only * feat: add gateway route and service for create question * feat: add header validation in create question route * feat: add response type of create question --- packages/proto/grading/entity.proto | 9 ++++ packages/proto/grading/message.proto | 8 ++++ packages/proto/grading/service.proto | 1 + packages/proto/workspace/entity.proto | 10 ++++ packages/proto/workspace/message.proto | 8 ++++ packages/proto/workspace/service.proto | 1 + .../shared-external/src/workspace/index.ts | 2 + .../shared-internal/src/grading/message.ts | 12 +++++ .../shared-internal/src/workspace/message.ts | 12 ++++- .../src/controllers/WorkspaceController.ts | 47 ++++++++++++++++++- .../gateway/src/services/GradingService.ts | 6 +++ .../gateway/src/services/WorkspaceService.ts | 6 +++ .../src/utils/decorators/HeaderDecorator.ts | 18 +++++++ .../gateway/src/utils/dtos/QuestionDtos.ts | 43 +++++++++++++++++ .../src/utils/guards/WorkspaceOwnerGuard.ts | 37 +++++++++++++++ .../src/controllers/GradingController.ts | 10 ++++ services/grading/src/modules/GradingModule.ts | 2 + .../src/repositories/QuestionRepository.ts | 20 ++++++++ .../src/repositories/TestcaseRepository.ts | 6 ++- .../grading/src/services/GradingService.ts | 32 ++++++++++++- .../src/controllers/WorkspaceController.ts | 10 ++++ .../src/repositories/QuestionRepository.ts | 10 ++++ .../src/services/WorkspaceService.ts | 32 ++++++++++++- 23 files changed, 337 insertions(+), 5 deletions(-) create mode 100644 services/gateway/src/utils/decorators/HeaderDecorator.ts create mode 100644 services/gateway/src/utils/dtos/QuestionDtos.ts create mode 100644 services/gateway/src/utils/guards/WorkspaceOwnerGuard.ts create mode 100644 services/grading/src/repositories/QuestionRepository.ts diff --git a/packages/proto/grading/entity.proto b/packages/proto/grading/entity.proto index 87e4ed5..cc40d70 100644 --- a/packages/proto/grading/entity.proto +++ b/packages/proto/grading/entity.proto @@ -27,6 +27,15 @@ message QuestionSummary { string status = 3; } +message CreateGradingQuestion { + int32 questionId = 1; + int32 workspaceId = 2; + int32 score = 3; + string name = 4; + double memoryLimit = 5; + double timeLimit = 6; +} + // TODO: hardcoded for BMH2023 message Rank { string userId = 1; diff --git a/packages/proto/grading/message.proto b/packages/proto/grading/message.proto index 15f2aff..2a1e945 100644 --- a/packages/proto/grading/message.proto +++ b/packages/proto/grading/message.proto @@ -49,6 +49,14 @@ message GetQuestionSummaryByIdsResponse { repeated QuestionSummary questionSummaries = 1; } +message CreateGradingQuestionRequest { + CreateGradingQuestion question = 1; +} + +message CreateGradingQuestionResponse { + CreateGradingQuestion question = 1; +} + // TODO: hardcoded for BMH2023 message GetRankingResponse { repeated Rank ranks = 1; diff --git a/packages/proto/grading/service.proto b/packages/proto/grading/service.proto index 77c9efa..96b31dc 100644 --- a/packages/proto/grading/service.proto +++ b/packages/proto/grading/service.proto @@ -10,6 +10,7 @@ service GradingService { rpc Submit(SubmitRequest) returns (SubmitResponse); rpc Grade(GradeRequest) returns (GradeResponse); rpc GetQuestionSummaryByIds(GetQuestionSummaryByIdsRequest) returns (GetQuestionSummaryByIdsResponse); + rpc CreateGradingQuestion(CreateGradingQuestionRequest) returns (CreateGradingQuestionResponse); // TODO: hardcoded for BMH2023 rpc GetRanking(Empty) returns (GetRankingResponse); } diff --git a/packages/proto/workspace/entity.proto b/packages/proto/workspace/entity.proto index 6bd0acc..feab643 100644 --- a/packages/proto/workspace/entity.proto +++ b/packages/proto/workspace/entity.proto @@ -31,3 +31,13 @@ message Question { int32 workspaceId = 8; uint64 createdAt = 9; } + +// Omit, +message CreateWorkspaceQuestion { + string name = 1; + string description = 2; + double memoryLimit = 3; + double timeLimit = 4; + string level = 5; + int32 workspaceId = 6; +} diff --git a/packages/proto/workspace/message.proto b/packages/proto/workspace/message.proto index 8436bf5..99c5a0e 100644 --- a/packages/proto/workspace/message.proto +++ b/packages/proto/workspace/message.proto @@ -43,3 +43,11 @@ message GetQuestionByIdRequest { message GetQuestionByIdResponse { Question question = 1; } + +message CreateWorkspaceQuestionRequest { + CreateWorkspaceQuestion question = 1; +} + +message CreateWorkspaceQuestionResponse { + Question question = 1; +} diff --git a/packages/proto/workspace/service.proto b/packages/proto/workspace/service.proto index 8a6e453..266ac36 100644 --- a/packages/proto/workspace/service.proto +++ b/packages/proto/workspace/service.proto @@ -11,4 +11,5 @@ service WorkspaceService { rpc GetWorkspaceById(GetWorkspaceByIdRequest) returns (GetWorkspaceByIdResponse); rpc GetQuestionsByWorkspaceId(GetQuestionsByWorkspaceIdRequest) returns (GetQuestionsByWorkspaceIdResponse); rpc GetQuestionById(GetQuestionByIdRequest) returns (GetQuestionByIdResponse); + rpc CreateWorkspaceQuestion(CreateWorkspaceQuestionRequest) returns (CreateWorkspaceQuestionResponse); } diff --git a/packages/shared-external/src/workspace/index.ts b/packages/shared-external/src/workspace/index.ts index caaabf2..534af54 100644 --- a/packages/shared-external/src/workspace/index.ts +++ b/packages/shared-external/src/workspace/index.ts @@ -13,3 +13,5 @@ export type PublicQuestion = Omit & { lastSubmitted: number, status: QuestionStatus, }; + +export type PublicCreatedQuestion = Question & { score: number }; diff --git a/packages/shared-internal/src/grading/message.ts b/packages/shared-internal/src/grading/message.ts index 04741f1..3d3722c 100644 --- a/packages/shared-internal/src/grading/message.ts +++ b/packages/shared-internal/src/grading/message.ts @@ -56,6 +56,18 @@ export type GetQuestionSummaryByIdsResponse = { questionSummaries: QuestionSummary[], }; +export type CreateGradingQuestionRequest = { + question: { + questionId: number, + workspaceId: number, + score: number, + timeLimit: number, + memoryLimit: number, + } +}; + +export type CreateGradingQuestionResponse = CreateGradingQuestionRequest; + // TODO: hardcoded for BMH2023 export type GetRankingResponse = { ranks: Rank[], diff --git a/packages/shared-internal/src/workspace/message.ts b/packages/shared-internal/src/workspace/message.ts index af0abe5..3a7ed5f 100644 --- a/packages/shared-internal/src/workspace/message.ts +++ b/packages/shared-internal/src/workspace/message.ts @@ -1,4 +1,6 @@ -import { WorkspaceWithParticipants, Workspace, Question } from './entity'; +import { + WorkspaceWithParticipants, Workspace, Question, +} from './entity'; export type ValidateUserInWorkspaceRequest = { userId: string, @@ -41,3 +43,11 @@ export type GetQuestionByIdRequest = { export type GetQuestionByIdResponse = { question: Question, }; + +export type CreateWorkspaceQuestionRequest = { + question: Omit, +}; + +export type CreateWorkspaceQuestionResponse = { + question: Question, +}; diff --git a/services/gateway/src/controllers/WorkspaceController.ts b/services/gateway/src/controllers/WorkspaceController.ts index 7de2679..b3fead0 100644 --- a/services/gateway/src/controllers/WorkspaceController.ts +++ b/services/gateway/src/controllers/WorkspaceController.ts @@ -1,12 +1,13 @@ import { Controller, Inject, Get, - UseGuards, Param, + UseGuards, Param, Post, } from '@nestjs/common'; import { ClientGrpc } from '@nestjs/microservices'; import { firstValueFrom, forkJoin, map } from 'rxjs'; import { PublicUser, PublicWorkspaceWithParticipants, PublicQuestion, PublicWorkspace, + PublicCreatedQuestion, } from '@codern/external'; import { WorkspaceService } from '@/services/WorkspaceService'; import { User } from '@/utils/decorators/AuthDecorator'; @@ -18,6 +19,9 @@ import { workspaceWithParticipants, } from '@/utils/Serializer'; import { GradingService } from '@/services/GradingService'; +import { CreateQuestionDto } from '@/utils/dtos/QuestionDtos'; +import { RequestHeader } from '@/utils/decorators/HeaderDecorator'; +import { WorkspaceOwnerGuard } from '@/utils/guards/WorkspaceOwnerGuard'; @Controller('/workspaces') export class WorkspaceController { @@ -112,4 +116,45 @@ export class WorkspaceController { return publicQuestions([question], questionSummaries)[0]; } + @Post('/:workspaceId/questions') + @UseGuards(AuthGuard, WorkspaceOwnerGuard) + public async createQuestion( + @Param('workspaceId') workspaceId: number, + @RequestHeader(CreateQuestionDto) headers: CreateQuestionDto, + ): Promise { + const { + name, description, timeLimit, memoryLimit, level, score, + } = headers; + + const { question: createdWorkspaceQuestion } = await firstValueFrom( + this.workspaceService.createWorkspaceQuestion({ + question: { + workspaceId, + name, + description, + timeLimit, + memoryLimit, + level, + }, + }), + ); + + const { question: createdGradingQuestion } = await firstValueFrom( + this.gradingService.createGradingQuestion({ + question: { + questionId: createdWorkspaceQuestion.id, + workspaceId, + timeLimit, + memoryLimit, + score, + }, + }), + ); + + return { + ...createdWorkspaceQuestion, + score: createdGradingQuestion.score, + }; + } + } diff --git a/services/gateway/src/services/GradingService.ts b/services/gateway/src/services/GradingService.ts index 884c3b7..9494069 100644 --- a/services/gateway/src/services/GradingService.ts +++ b/services/gateway/src/services/GradingService.ts @@ -1,4 +1,6 @@ import { + CreateGradingQuestionRequest, + CreateGradingQuestionResponse, GetQuestionSummaryByIdsRequest, GetQuestionSummaryByIdsResponse, GetRankingResponse, GetSubmissionsByQuestionIdRequest, GetSubmissionsByQuestionIdResponse, GradeRequest, @@ -25,4 +27,8 @@ export interface GradingService { getRanking(any: unknown): Observable; + createGradingQuestion( + data: CreateGradingQuestionRequest, + ): Observable; + } diff --git a/services/gateway/src/services/WorkspaceService.ts b/services/gateway/src/services/WorkspaceService.ts index 3ec552e..f11991c 100644 --- a/services/gateway/src/services/WorkspaceService.ts +++ b/services/gateway/src/services/WorkspaceService.ts @@ -1,4 +1,6 @@ import { + CreateWorkspaceQuestionRequest, + CreateWorkspaceQuestionResponse, GetAllWorkspacesByUserIdRequest, GetAllWorkspacesByUserIdResponse, GetQuestionByIdRequest, GetQuestionByIdResponse, GetQuestionsByWorkspaceIdRequest, GetQuestionsByWorkspaceIdResponse, GetWorkspaceByIdRequest, GetWorkspaceByIdResponse, ValidateQuestionInWorkspaceRequest, @@ -24,4 +26,8 @@ export interface WorkspaceService { getQuestionById(data: GetQuestionByIdRequest): Observable; + createWorkspaceQuestion( + data: CreateWorkspaceQuestionRequest + ): Observable; + } diff --git a/services/gateway/src/utils/decorators/HeaderDecorator.ts b/services/gateway/src/utils/decorators/HeaderDecorator.ts new file mode 100644 index 0000000..74fece1 --- /dev/null +++ b/services/gateway/src/utils/decorators/HeaderDecorator.ts @@ -0,0 +1,18 @@ +import { BadRequestException, createParamDecorator, ExecutionContext } from '@nestjs/common'; +import { plainToInstance } from 'class-transformer'; +import { validate } from 'class-validator'; + +export const RequestHeader = createParamDecorator( + async (value: any, ctx: ExecutionContext) => { + const { headers } = ctx.switchToHttp().getRequest(); + + const dto = plainToInstance(value, headers, { excludeExtraneousValues: true }); + + const errors = await validate(dto); + if (errors.length > 0) { + throw new BadRequestException('Invalid request headers'); + } + + return dto; + }, +); diff --git a/services/gateway/src/utils/dtos/QuestionDtos.ts b/services/gateway/src/utils/dtos/QuestionDtos.ts new file mode 100644 index 0000000..c1877db --- /dev/null +++ b/services/gateway/src/utils/dtos/QuestionDtos.ts @@ -0,0 +1,43 @@ +import { QuestionLevel } from '@codern/internal'; +import { Expose, Transform } from 'class-transformer'; +import { + IsDefined, IsEnum, IsInt, IsString, +} from 'class-validator'; + +export class CreateQuestionDto { + + @IsString() + @IsDefined() + @Expose({ name: 'question-name' }) + name!: string; + + @IsString() + @IsDefined() + @Expose({ name: 'question-description' }) + description!: string; + + @Transform(({ value }) => Number.parseInt(value, 10)) + @IsInt() + @IsDefined() + @Expose({ name: 'question-memory-limit' }) + memoryLimit!: number; + + @Transform(({ value }) => Number.parseInt(value, 10)) + @IsInt({ message: 'invalid request header `question-time-limit`' }) + @IsDefined() + @Expose({ name: 'question-time-limit' }) + timeLimit!: number; + + @Transform(({ value }) => String(value).toUpperCase()) + @IsEnum(QuestionLevel) + @IsDefined() + @Expose({ name: 'question-level' }) + level!: QuestionLevel; + + @Transform(({ value }) => Number.parseInt(value, 10)) + @IsInt() + @IsDefined() + @Expose({ name: 'question-score' }) + score!: number; + +} diff --git a/services/gateway/src/utils/guards/WorkspaceOwnerGuard.ts b/services/gateway/src/utils/guards/WorkspaceOwnerGuard.ts new file mode 100644 index 0000000..64e250e --- /dev/null +++ b/services/gateway/src/utils/guards/WorkspaceOwnerGuard.ts @@ -0,0 +1,37 @@ +import { + BadRequestException, + CanActivate, ExecutionContext, Inject, Injectable, +} from '@nestjs/common'; +import { ClientGrpc } from '@nestjs/microservices'; +import { FastifyRequest } from 'fastify'; +import { firstValueFrom } from 'rxjs'; +import { WorkspaceService } from '@/services/WorkspaceService'; + +type WorkspaceGuardParams = { + workspaceId: string | undefined, + questionId: string | undefined, +}; + +@Injectable() +export class WorkspaceOwnerGuard implements CanActivate { + + private readonly workspaceService: WorkspaceService; + + public constructor(@Inject('WORKSPACE_PACKAGE') client: ClientGrpc) { + this.workspaceService = client.getService('WorkspaceService'); + } + + public async canActivate(context: ExecutionContext): Promise { + const request = context.switchToHttp().getRequest(); + + const { workspaceId } = request.params as WorkspaceGuardParams; + if (!workspaceId) throw new BadRequestException(); + + const { workspace } = await firstValueFrom( + this.workspaceService.getWorkspaceById({ workspaceId: Number(workspaceId) }), + ); + + return workspace.ownerId === request.user.id; + } + +} diff --git a/services/grading/src/controllers/GradingController.ts b/services/grading/src/controllers/GradingController.ts index caf6bce..bb71560 100644 --- a/services/grading/src/controllers/GradingController.ts +++ b/services/grading/src/controllers/GradingController.ts @@ -1,6 +1,8 @@ import { Controller } from '@nestjs/common'; import { EventPattern, GrpcMethod } from '@nestjs/microservices'; import { + CreateGradingQuestionRequest, + CreateGradingQuestionResponse, GetQuestionSummaryByIdsRequest, GetQuestionSummaryByIdsResponse, GetRankingResponse, @@ -56,6 +58,14 @@ export class GradingController { return { submissions }; } + @GrpcMethod('GradingService') + public async createGradingQuestion( + data: CreateGradingQuestionRequest, + ): Promise { + const question = await this.gradingService.createGradingQuestion(data.question); + return { question }; + } + // TODO: hardcoded for BMH2023 @GrpcMethod('GradingService') public async getRanking(): Promise { diff --git a/services/grading/src/modules/GradingModule.ts b/services/grading/src/modules/GradingModule.ts index 7f310ca..3cfb5b0 100644 --- a/services/grading/src/modules/GradingModule.ts +++ b/services/grading/src/modules/GradingModule.ts @@ -9,6 +9,7 @@ import { GradingService } from '@/services/GradingService'; import { QueueSerivce } from '@/services/QueueService'; import { TestcaseRepository } from '@/repositories/TestcaseRepository'; import { ResultRepository } from '@/repositories/ResultRepository'; +import { QuestionRepository } from '@/repositories/QuestionRepository'; @Module({ imports: [ @@ -44,6 +45,7 @@ import { ResultRepository } from '@/repositories/ResultRepository'; ResultRepository, SubmissionRepository, TestcaseRepository, + QuestionRepository, QueueSerivce, GradingService, diff --git a/services/grading/src/repositories/QuestionRepository.ts b/services/grading/src/repositories/QuestionRepository.ts new file mode 100644 index 0000000..9500bb0 --- /dev/null +++ b/services/grading/src/repositories/QuestionRepository.ts @@ -0,0 +1,20 @@ +import { Injectable } from '@nestjs/common'; +import { Prisma, Question } from '@prisma/client'; +import { PrismaService } from '@/services/PrismaService'; + +@Injectable() +export class QuestionRepository { + + private readonly prismaService: PrismaService; + + public constructor(prismaService: PrismaService) { + this.prismaService = prismaService; + } + + public async createQuestion(question: Prisma.QuestionCreateInput): Promise { + const createdQuestion = await this.prismaService.question.create({ data: question }); + + return createdQuestion; + } + +} diff --git a/services/grading/src/repositories/TestcaseRepository.ts b/services/grading/src/repositories/TestcaseRepository.ts index 75d450c..eab0e0c 100644 --- a/services/grading/src/repositories/TestcaseRepository.ts +++ b/services/grading/src/repositories/TestcaseRepository.ts @@ -1,5 +1,5 @@ import { Injectable } from '@nestjs/common'; -import { Testcase } from '@prisma/client'; +import { Prisma, Testcase } from '@prisma/client'; import { PrismaService } from '@/services/PrismaService'; @Injectable() @@ -15,4 +15,8 @@ export class TestcaseRepository { return this.prismaService.testcase.findMany({ where: { questionId } }); } + public createTestcaseByQuestionId(testcase: Prisma.TestcaseCreateInput): Promise { + return this.prismaService.testcase.create({ data: testcase }); + } + } diff --git a/services/grading/src/services/GradingService.ts b/services/grading/src/services/GradingService.ts index ddd7a6a..36e4a53 100644 --- a/services/grading/src/services/GradingService.ts +++ b/services/grading/src/services/GradingService.ts @@ -3,7 +3,7 @@ import { ExpectedNotFoundError, SubmitResponse, Language, QuestionSummary, QuestionStatus, ExpectedInvalidError, ResultStatus, SubmissionWithResults, Result, - ResultMetadata, Rank, + ResultMetadata, Rank, CreateGradingQuestionRequest, } from '@codern/internal'; import { Timestamp } from '@codern/shared'; import { Submission } from '@prisma/client'; @@ -14,6 +14,7 @@ import { GradingFile, QueueSerivce } from '@/services/QueueService'; import { TestcaseRepository } from '@/repositories/TestcaseRepository'; import { GradingError } from '@/utils/errors/GradingError'; import { ResultRepository } from '@/repositories/ResultRepository'; +import { QuestionRepository } from '@/repositories/QuestionRepository'; type SubmissionWithQuestionStatus = Submission & { questionStatus: QuestionStatus }; type QuestionStatusWeightMap = { [status in QuestionStatus]: number }; @@ -27,6 +28,7 @@ export class GradingService { private readonly submissionRepository: SubmissionRepository; private readonly testcaseRepository: TestcaseRepository; private readonly resultRepository: ResultRepository; + private readonly questionRepository: QuestionRepository; public constructor( configService: ConfigService, @@ -35,6 +37,7 @@ export class GradingService { submissionRepository: SubmissionRepository, testcaseRepository: TestcaseRepository, resultRepository: ResultRepository, + questionRepository: QuestionRepository, ) { this.configService = configService; this.httpService = httpService; @@ -42,6 +45,7 @@ export class GradingService { this.submissionRepository = submissionRepository; this.testcaseRepository = testcaseRepository; this.resultRepository = resultRepository; + this.questionRepository = questionRepository; } public async submit( @@ -246,6 +250,32 @@ export class GradingService { .getSubmissionsWithResultsByQuestionId(id, userId) as Promise; } + public async createGradingQuestion( + question: CreateGradingQuestionRequest['question'], + ): Promise { + const createdQuestion = await this.questionRepository.createQuestion({ + id: question.questionId, + workspaceId: question.workspaceId, + memoryLimit: question.memoryLimit, + score: question.score, + timeLimit: question.timeLimit, + }); + + await this.testcaseRepository.createTestcaseByQuestionId({ + filePath: `/workspaces/${question.workspaceId}/questions/${question.questionId}/testcase.zip`, + question: { + connect: { + id: question.questionId, + }, + }, + }); + + return { + ...createdQuestion, + questionId: question.questionId, + }; + } + // TODO: hardcoded for BMH2023 public getRanking(): Promise { return this.submissionRepository.getRanking(); diff --git a/services/workspace/src/controllers/WorkspaceController.ts b/services/workspace/src/controllers/WorkspaceController.ts index 2bb8467..f034f4a 100644 --- a/services/workspace/src/controllers/WorkspaceController.ts +++ b/services/workspace/src/controllers/WorkspaceController.ts @@ -6,6 +6,8 @@ import { GetWorkspaceByIdRequest, GetWorkspaceByIdResponse, ValidateUserInWorkspaceRequest, ValidateQuestionInWorkspaceRequest, Question, + CreateWorkspaceQuestionRequest, + CreateWorkspaceQuestionResponse, } from '@codern/internal'; import { WorkspaceService } from '@/services/WorkspaceService'; @@ -60,4 +62,12 @@ export class WorkspaceController { return { question: question as Question }; } + @GrpcMethod('WorkspaceService') + public async createWorkspaceQuestion( + data: CreateWorkspaceQuestionRequest, + ): Promise { + const question = await this.workspaceService.createQuestion(data.question); + return { question: question as Question }; + } + } diff --git a/services/workspace/src/repositories/QuestionRepository.ts b/services/workspace/src/repositories/QuestionRepository.ts index be779eb..6fdf589 100644 --- a/services/workspace/src/repositories/QuestionRepository.ts +++ b/services/workspace/src/repositories/QuestionRepository.ts @@ -23,4 +23,14 @@ export class QuestionRepository { return this.prismaService.question.findUnique({ where: { id } }); } + public updateQuestionById( + questionId: number, + question: Prisma.QuestionUpdateInput, + ): Promise { + return this.prismaService.question.update({ + data: question, + where: { id: questionId }, + }); + } + } diff --git a/services/workspace/src/services/WorkspaceService.ts b/services/workspace/src/services/WorkspaceService.ts index 0b42b7d..0706f4d 100644 --- a/services/workspace/src/services/WorkspaceService.ts +++ b/services/workspace/src/services/WorkspaceService.ts @@ -1,7 +1,13 @@ import { Injectable } from '@nestjs/common'; -import { ExpectedNotFoundError, ExpectedForbiddenError, WorkspaceWithParticipants } from '@codern/internal'; +import { + ExpectedNotFoundError, + ExpectedForbiddenError, + WorkspaceWithParticipants, + CreateWorkspaceQuestionRequest, +} from '@codern/internal'; import { Workspace, Question } from '@prisma/client'; import { ConfigService } from '@nestjs/config'; +import { Timestamp } from '@codern/shared/src/time'; import { QuestionRepository } from '@/repositories/QuestionRepository'; import { WorkspaceRepository } from '@/repositories/WorkspaceRepository'; import { WorkspaceError } from '@/utils/errors/WorkspaceError'; @@ -77,4 +83,28 @@ export class WorkspaceService { return this.toPublicQuestionDetailUrl(question); } + public async createQuestion(question: CreateWorkspaceQuestionRequest['question']): Promise { + const createdQuestion = await this.questionRepository.createQuestion({ + name: question.name, + description: question.description, + timeLimit: question.timeLimit, + memoryLimit: question.memoryLimit, + level: question.level, + detailPath: '', + createdAt: Timestamp.now(), + workspace: { + connect: { + id: question.workspaceId, + }, + }, + }); + + const updatedPathQuestion = await this.questionRepository.updateQuestionById( + createdQuestion.id, + { detailPath: `workspaces/${question.workspaceId}/questions/${createdQuestion.id}/question.md` }, + ); + + return updatedPathQuestion; + } + }