Skip to content
This repository has been archived by the owner on Nov 10, 2023. It is now read-only.

Commit

Permalink
Add Create question route and service (#16)
Browse files Browse the repository at this point in the history
* 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
  • Loading branch information
XiaoXuxxxx committed Aug 3, 2023
1 parent 95ac788 commit 7b03201
Show file tree
Hide file tree
Showing 23 changed files with 337 additions and 5 deletions.
9 changes: 9 additions & 0 deletions packages/proto/grading/entity.proto
Original file line number Diff line number Diff line change
Expand Up @@ -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;
Expand Down
8 changes: 8 additions & 0 deletions packages/proto/grading/message.proto
Original file line number Diff line number Diff line change
Expand Up @@ -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;
Expand Down
1 change: 1 addition & 0 deletions packages/proto/grading/service.proto
Original file line number Diff line number Diff line change
Expand Up @@ -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);
}
10 changes: 10 additions & 0 deletions packages/proto/workspace/entity.proto
Original file line number Diff line number Diff line change
Expand Up @@ -31,3 +31,13 @@ message Question {
int32 workspaceId = 8;
uint64 createdAt = 9;
}

// Omit<Question, 'id' | 'detailPath' | 'createdAt'>,
message CreateWorkspaceQuestion {
string name = 1;
string description = 2;
double memoryLimit = 3;
double timeLimit = 4;
string level = 5;
int32 workspaceId = 6;
}
8 changes: 8 additions & 0 deletions packages/proto/workspace/message.proto
Original file line number Diff line number Diff line change
Expand Up @@ -43,3 +43,11 @@ message GetQuestionByIdRequest {
message GetQuestionByIdResponse {
Question question = 1;
}

message CreateWorkspaceQuestionRequest {
CreateWorkspaceQuestion question = 1;
}

message CreateWorkspaceQuestionResponse {
Question question = 1;
}
1 change: 1 addition & 0 deletions packages/proto/workspace/service.proto
Original file line number Diff line number Diff line change
Expand Up @@ -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);
}
2 changes: 2 additions & 0 deletions packages/shared-external/src/workspace/index.ts
Original file line number Diff line number Diff line change
Expand Up @@ -13,3 +13,5 @@ export type PublicQuestion = Omit<Question, 'workspaceId'> & {
lastSubmitted: number,
status: QuestionStatus,
};

export type PublicCreatedQuestion = Question & { score: number };
12 changes: 12 additions & 0 deletions packages/shared-internal/src/grading/message.ts
Original file line number Diff line number Diff line change
Expand Up @@ -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[],
Expand Down
12 changes: 11 additions & 1 deletion packages/shared-internal/src/workspace/message.ts
Original file line number Diff line number Diff line change
@@ -1,4 +1,6 @@
import { WorkspaceWithParticipants, Workspace, Question } from './entity';
import {
WorkspaceWithParticipants, Workspace, Question,
} from './entity';

export type ValidateUserInWorkspaceRequest = {
userId: string,
Expand Down Expand Up @@ -41,3 +43,11 @@ export type GetQuestionByIdRequest = {
export type GetQuestionByIdResponse = {
question: Question,
};

export type CreateWorkspaceQuestionRequest = {
question: Omit<Question, 'id' | 'detailPath' | 'createdAt'>,
};

export type CreateWorkspaceQuestionResponse = {
question: Question,
};
47 changes: 46 additions & 1 deletion services/gateway/src/controllers/WorkspaceController.ts
Original file line number Diff line number Diff line change
@@ -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';
Expand All @@ -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 {
Expand Down Expand Up @@ -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<PublicCreatedQuestion> {
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,
};
}

}
6 changes: 6 additions & 0 deletions services/gateway/src/services/GradingService.ts
Original file line number Diff line number Diff line change
@@ -1,4 +1,6 @@
import {
CreateGradingQuestionRequest,
CreateGradingQuestionResponse,
GetQuestionSummaryByIdsRequest, GetQuestionSummaryByIdsResponse,
GetRankingResponse,
GetSubmissionsByQuestionIdRequest, GetSubmissionsByQuestionIdResponse, GradeRequest,
Expand All @@ -25,4 +27,8 @@ export interface GradingService {

getRanking(any: unknown): Observable<GetRankingResponse>;

createGradingQuestion(
data: CreateGradingQuestionRequest,
): Observable<CreateGradingQuestionResponse>;

}
6 changes: 6 additions & 0 deletions services/gateway/src/services/WorkspaceService.ts
Original file line number Diff line number Diff line change
@@ -1,4 +1,6 @@
import {
CreateWorkspaceQuestionRequest,
CreateWorkspaceQuestionResponse,
GetAllWorkspacesByUserIdRequest, GetAllWorkspacesByUserIdResponse, GetQuestionByIdRequest,
GetQuestionByIdResponse, GetQuestionsByWorkspaceIdRequest, GetQuestionsByWorkspaceIdResponse,
GetWorkspaceByIdRequest, GetWorkspaceByIdResponse, ValidateQuestionInWorkspaceRequest,
Expand All @@ -24,4 +26,8 @@ export interface WorkspaceService {

getQuestionById(data: GetQuestionByIdRequest): Observable<GetQuestionByIdResponse>;

createWorkspaceQuestion(
data: CreateWorkspaceQuestionRequest
): Observable<CreateWorkspaceQuestionResponse>;

}
18 changes: 18 additions & 0 deletions services/gateway/src/utils/decorators/HeaderDecorator.ts
Original file line number Diff line number Diff line change
@@ -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;
},
);
43 changes: 43 additions & 0 deletions services/gateway/src/utils/dtos/QuestionDtos.ts
Original file line number Diff line number Diff line change
@@ -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;

}
37 changes: 37 additions & 0 deletions services/gateway/src/utils/guards/WorkspaceOwnerGuard.ts
Original file line number Diff line number Diff line change
@@ -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>('WorkspaceService');
}

public async canActivate(context: ExecutionContext): Promise<boolean> {
const request = context.switchToHttp().getRequest<FastifyRequest>();

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;
}

}
10 changes: 10 additions & 0 deletions services/grading/src/controllers/GradingController.ts
Original file line number Diff line number Diff line change
@@ -1,6 +1,8 @@
import { Controller } from '@nestjs/common';
import { EventPattern, GrpcMethod } from '@nestjs/microservices';
import {
CreateGradingQuestionRequest,
CreateGradingQuestionResponse,
GetQuestionSummaryByIdsRequest,
GetQuestionSummaryByIdsResponse,
GetRankingResponse,
Expand Down Expand Up @@ -56,6 +58,14 @@ export class GradingController {
return { submissions };
}

@GrpcMethod('GradingService')
public async createGradingQuestion(
data: CreateGradingQuestionRequest,
): Promise<CreateGradingQuestionResponse> {
const question = await this.gradingService.createGradingQuestion(data.question);
return { question };
}

// TODO: hardcoded for BMH2023
@GrpcMethod('GradingService')
public async getRanking(): Promise<GetRankingResponse> {
Expand Down
2 changes: 2 additions & 0 deletions services/grading/src/modules/GradingModule.ts
Original file line number Diff line number Diff line change
Expand Up @@ -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: [
Expand Down Expand Up @@ -44,6 +45,7 @@ import { ResultRepository } from '@/repositories/ResultRepository';
ResultRepository,
SubmissionRepository,
TestcaseRepository,
QuestionRepository,

QueueSerivce,
GradingService,
Expand Down
20 changes: 20 additions & 0 deletions services/grading/src/repositories/QuestionRepository.ts
Original file line number Diff line number Diff line change
@@ -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<Question> {
const createdQuestion = await this.prismaService.question.create({ data: question });

return createdQuestion;
}

}
6 changes: 5 additions & 1 deletion services/grading/src/repositories/TestcaseRepository.ts
Original file line number Diff line number Diff line change
@@ -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()
Expand All @@ -15,4 +15,8 @@ export class TestcaseRepository {
return this.prismaService.testcase.findMany({ where: { questionId } });
}

public createTestcaseByQuestionId(testcase: Prisma.TestcaseCreateInput): Promise<Testcase> {
return this.prismaService.testcase.create({ data: testcase });
}

}

0 comments on commit 7b03201

Please sign in to comment.