diff --git a/.github/workflows/pre-test.yml b/.github/workflows/pre-test.yml index 66ebbdc..aa807c6 100644 --- a/.github/workflows/pre-test.yml +++ b/.github/workflows/pre-test.yml @@ -58,6 +58,7 @@ jobs: webhookUrl: ${{ secrets.DISCORD_WEBHOOK }} e2e-testing: + needs: unit-testing runs-on: ubuntu-latest steps: - name: Repository Checkout diff --git a/.platform/hooks/predeploy/01_database_init.sh b/.platform/hooks/predeploy/01_database_init.sh index cf9a9af..04cdc0a 100644 --- a/.platform/hooks/predeploy/01_database_init.sh +++ b/.platform/hooks/predeploy/01_database_init.sh @@ -1,2 +1,9 @@ #!/usr/bin/env bash -node node_modules/prisma/build/index.js db push \ No newline at end of file + +# Add database migration script +node node_modules/prisma/build/index.js migrate resolve --applied 20240109151719_submission_ispulic +node node_modules/prisma/build/index.js migrate resolve --applied 20240113073649_problem_issue_comment +node node_modules/prisma/build/index.js migrate resolve --applied 20240113161205_problem_issue_comment_user +node node_modules/prisma/build/index.js migrate resolve --applied 20240114010304_chagne_issue_comment_name +node node_modules/prisma/build/index.js migrate resolve --applied 20240114021108_issue_comment_problem_relation +node node_modules/prisma/build/index.js migrate resolve --applied 20240114021239_modify_problem_issue_problem diff --git a/domain/index.ts b/domain/index.ts index 72dac19..ba363a2 100644 --- a/domain/index.ts +++ b/domain/index.ts @@ -3,3 +3,4 @@ export * from './user.domain'; export * from './problem-example.domain'; export * from './submission.domain'; export * from './problem-issue.domain'; +export * from './problem-issue-comment.domain'; diff --git a/domain/problem-issue-comment.domain.ts b/domain/problem-issue-comment.domain.ts new file mode 100644 index 0000000..18cbb30 --- /dev/null +++ b/domain/problem-issue-comment.domain.ts @@ -0,0 +1,19 @@ +import { ApiProperty } from '@nestjs/swagger'; +import { ProblemIssueComment } from '@prisma/client'; + +export class ProblemIssueCommentDomain implements ProblemIssueComment { + @ApiProperty() + id: number; + + @ApiProperty() + content: string; + + @ApiProperty() + issueId: number; + + @ApiProperty() + problemId: number; + + @ApiProperty() + userId: string; +} diff --git a/domain/problem-issue.domain.ts b/domain/problem-issue.domain.ts index 7e55766..31e90e8 100644 --- a/domain/problem-issue.domain.ts +++ b/domain/problem-issue.domain.ts @@ -12,7 +12,7 @@ export class ProblemIssueDomain implements ProblemIssue { content: string; @ApiProperty() - targetId: number; + problemId: number; @ApiProperty() issuerId: string; diff --git a/domain/submission.domain.ts b/domain/submission.domain.ts index 7c28bde..e0a93f9 100644 --- a/domain/submission.domain.ts +++ b/domain/submission.domain.ts @@ -17,6 +17,9 @@ export class SubmissionDomain implements Submission { @ApiProperty() time: number; + @ApiProperty() + isPublic: boolean; + @ApiProperty() languageId: number; diff --git a/prisma/migrations/20231203061100_user_type_migration/migration.sql b/prisma/migrations/20231203061100_user_type_migration/migration.sql deleted file mode 100644 index d8f82b5..0000000 --- a/prisma/migrations/20231203061100_user_type_migration/migration.sql +++ /dev/null @@ -1,17 +0,0 @@ --- CreateTable -CREATE TABLE `user` ( - `id` VARCHAR(191) NOT NULL, - `nickname` VARCHAR(191) NOT NULL, - `password` VARCHAR(191) NOT NULL, - `email` VARCHAR(191) NOT NULL, - `message` VARCHAR(191) NULL, - `github` VARCHAR(191) NULL, - `blog` VARCHAR(191) NULL, - `type` ENUM('Contributer', 'User') NULL DEFAULT 'User', - `createdAt` DATETIME(3) NOT NULL DEFAULT CURRENT_TIMESTAMP(3), - `updatedAt` DATETIME(3) NOT NULL, - - UNIQUE INDEX `user_nickname_key`(`nickname`), - UNIQUE INDEX `user_email_key`(`email`), - PRIMARY KEY (`id`) -) DEFAULT CHARACTER SET utf8mb4 COLLATE utf8mb4_unicode_ci; diff --git a/prisma/migrations/20231209021219_judge/migration.sql b/prisma/migrations/20231209021219_judge/migration.sql deleted file mode 100644 index 4b7b52c..0000000 --- a/prisma/migrations/20231209021219_judge/migration.sql +++ /dev/null @@ -1,35 +0,0 @@ --- AlterTable -ALTER TABLE `user` MODIFY `type` ENUM('Admin', 'Contributer', 'User') NULL DEFAULT 'User'; - --- CreateTable -CREATE TABLE `problem` ( - `id` INTEGER NOT NULL AUTO_INCREMENT, - `title` VARCHAR(191) NOT NULL, - `problem` VARCHAR(191) NOT NULL, - `input` VARCHAR(191) NOT NULL, - `output` VARCHAR(191) NOT NULL, - `timeLimit` INTEGER NOT NULL, - `memoryLimit` INTEGER NOT NULL, - `contributerId` VARCHAR(191) NOT NULL, - `createdAt` DATETIME(3) NOT NULL DEFAULT CURRENT_TIMESTAMP(3), - `updatedAt` DATETIME(3) NOT NULL, - - PRIMARY KEY (`id`) -) DEFAULT CHARACTER SET utf8mb4 COLLATE utf8mb4_unicode_ci; - --- CreateTable -CREATE TABLE `problemexample` ( - `id` INTEGER NOT NULL AUTO_INCREMENT, - `input` VARCHAR(191) NOT NULL, - `output` VARCHAR(191) NOT NULL, - `isPublic` BOOLEAN NOT NULL, - `problemId` INTEGER NULL, - - PRIMARY KEY (`id`) -) DEFAULT CHARACTER SET utf8mb4 COLLATE utf8mb4_unicode_ci; - --- AddForeignKey -ALTER TABLE `problem` ADD CONSTRAINT `problem_contributerId_fkey` FOREIGN KEY (`contributerId`) REFERENCES `user`(`id`) ON DELETE RESTRICT ON UPDATE CASCADE; - --- AddForeignKey -ALTER TABLE `problemexample` ADD CONSTRAINT `problemexample_problemId_fkey` FOREIGN KEY (`problemId`) REFERENCES `problem`(`id`) ON DELETE CASCADE ON UPDATE CASCADE; diff --git a/prisma/migrations/20240102053827_submission_datetime/migration.sql b/prisma/migrations/20240102053827_submission_datetime/migration.sql deleted file mode 100644 index 3155bcd..0000000 --- a/prisma/migrations/20240102053827_submission_datetime/migration.sql +++ /dev/null @@ -1,9 +0,0 @@ -/* - Warnings: - - - Added the required column `updatedAt` to the `submission` table without a default value. This is not possible if the table is not empty. - -*/ --- AlterTable -ALTER TABLE `submission` ADD COLUMN `createdAt` DATETIME(3) NOT NULL DEFAULT CURRENT_TIMESTAMP(3), - ADD COLUMN `updatedAt` DATETIME(3) NOT NULL; diff --git a/prisma/migrations/20240102040949_problem_issue/migration.sql b/prisma/migrations/20240109151719_submission_ispulic/migration.sql similarity index 58% rename from prisma/migrations/20240102040949_problem_issue/migration.sql rename to prisma/migrations/20240109151719_submission_ispulic/migration.sql index 43c09c0..a33e101 100644 --- a/prisma/migrations/20240102040949_problem_issue/migration.sql +++ b/prisma/migrations/20240109151719_submission_ispulic/migration.sql @@ -1,26 +1,39 @@ -/* - Warnings: - - - You are about to drop the `problemexample` table. If the table is not empty, all the data it contains will be lost. - - Added the required column `tags` to the `problem` table without a default value. This is not possible if the table is not empty. +-- CreateTable +CREATE TABLE `user` ( + `id` VARCHAR(191) NOT NULL, + `nickname` VARCHAR(191) NOT NULL, + `password` VARCHAR(191) NOT NULL, + `email` VARCHAR(191) NOT NULL, + `message` VARCHAR(191) NULL, + `github` VARCHAR(191) NULL, + `blog` VARCHAR(191) NULL, + `type` ENUM('Admin', 'Contributer', 'User') NULL DEFAULT 'User', + `createdAt` DATETIME(3) NOT NULL DEFAULT CURRENT_TIMESTAMP(3), + `updatedAt` DATETIME(3) NOT NULL, -*/ --- DropForeignKey -ALTER TABLE `problemexample` DROP FOREIGN KEY `problemexample_problemId_fkey`; + UNIQUE INDEX `user_nickname_key`(`nickname`), + UNIQUE INDEX `user_email_key`(`email`), + PRIMARY KEY (`id`) +) DEFAULT CHARACTER SET utf8mb4 COLLATE utf8mb4_unicode_ci; --- AlterTable -ALTER TABLE `problem` ADD COLUMN `deletedAt` DATETIME(3) NULL, - ADD COLUMN `isArchived` BOOLEAN NOT NULL DEFAULT false, - ADD COLUMN `tags` JSON NOT NULL, - MODIFY `title` VARCHAR(191) NOT NULL DEFAULT 'New Problem', - MODIFY `problem` VARCHAR(191) NOT NULL DEFAULT 'Problem Here', - MODIFY `input` VARCHAR(191) NOT NULL DEFAULT 'Input Here', - MODIFY `output` VARCHAR(191) NOT NULL DEFAULT 'Output Here', - MODIFY `timeLimit` INTEGER NOT NULL DEFAULT 5, - MODIFY `memoryLimit` INTEGER NOT NULL DEFAULT 128; +-- CreateTable +CREATE TABLE `problem` ( + `id` INTEGER NOT NULL AUTO_INCREMENT, + `title` VARCHAR(191) NOT NULL DEFAULT 'New Problem', + `problem` VARCHAR(191) NOT NULL DEFAULT 'Problem Here', + `input` VARCHAR(191) NOT NULL DEFAULT 'Input Here', + `output` VARCHAR(191) NOT NULL DEFAULT 'Output Here', + `timeLimit` INTEGER NOT NULL DEFAULT 5, + `memoryLimit` INTEGER NOT NULL DEFAULT 128, + `contributerId` VARCHAR(191) NOT NULL, + `tags` JSON NOT NULL, + `isArchived` BOOLEAN NOT NULL DEFAULT false, + `deletedAt` DATETIME(3) NULL, + `createdAt` DATETIME(3) NOT NULL DEFAULT CURRENT_TIMESTAMP(3), + `updatedAt` DATETIME(3) NOT NULL, --- DropTable -DROP TABLE `problemexample`; + PRIMARY KEY (`id`) +) DEFAULT CHARACTER SET utf8mb4 COLLATE utf8mb4_unicode_ci; -- CreateTable CREATE TABLE `problem_example` ( @@ -40,12 +53,15 @@ CREATE TABLE `submission` ( `codeLength` INTEGER NOT NULL DEFAULT 0, `memory` DOUBLE NOT NULL DEFAULT 0, `time` DOUBLE NOT NULL DEFAULT 0, + `isPublic` BOOLEAN NOT NULL DEFAULT false, `languageId` INTEGER NOT NULL, `language` VARCHAR(191) NOT NULL, `isCorrect` BOOLEAN NOT NULL, `response` ENUM('CORRECT', 'WRONG_ANSWER', 'TIME_LIMIT_EXCEED', 'COMPILE_ERROR', 'RUNTIME_ERROR_SIGSEGV', 'RUNTIME_ERROR_SIGXFSZ', 'RUNTIME_ERROR_SIGFPE', 'RUNTIME_ERROR_SIGABRT', 'RUNTIME_ERROR_NZEC', 'RUNTIME_ERROR', 'INTERNAL_ERROR', 'EXEC_FORMAT_ERROR', 'MEMORY_LIMIT_EXCEED') NOT NULL, `userId` VARCHAR(191) NOT NULL, `problemId` INTEGER NOT NULL, + `createdAt` DATETIME(3) NOT NULL DEFAULT CURRENT_TIMESTAMP(3), + `updatedAt` DATETIME(3) NOT NULL, PRIMARY KEY (`id`) ) DEFAULT CHARACTER SET utf8mb4 COLLATE utf8mb4_unicode_ci; @@ -61,6 +77,9 @@ CREATE TABLE `problem_issue` ( PRIMARY KEY (`id`) ) DEFAULT CHARACTER SET utf8mb4 COLLATE utf8mb4_unicode_ci; +-- AddForeignKey +ALTER TABLE `problem` ADD CONSTRAINT `problem_contributerId_fkey` FOREIGN KEY (`contributerId`) REFERENCES `user`(`id`) ON DELETE RESTRICT ON UPDATE CASCADE; + -- AddForeignKey ALTER TABLE `problem_example` ADD CONSTRAINT `problem_example_problemId_fkey` FOREIGN KEY (`problemId`) REFERENCES `problem`(`id`) ON DELETE CASCADE ON UPDATE CASCADE; diff --git a/prisma/migrations/20240113073649_problem_issue_comment/migration.sql b/prisma/migrations/20240113073649_problem_issue_comment/migration.sql new file mode 100644 index 0000000..fa5dd1e --- /dev/null +++ b/prisma/migrations/20240113073649_problem_issue_comment/migration.sql @@ -0,0 +1,11 @@ +-- CreateTable +CREATE TABLE `problem_issue_comment` ( + `id` INTEGER NOT NULL AUTO_INCREMENT, + `content` TEXT NOT NULL, + `problemId` INTEGER NOT NULL, + + PRIMARY KEY (`id`) +) DEFAULT CHARACTER SET utf8mb4 COLLATE utf8mb4_unicode_ci; + +-- AddForeignKey +ALTER TABLE `problem_issue_comment` ADD CONSTRAINT `problem_issue_comment_problemId_fkey` FOREIGN KEY (`problemId`) REFERENCES `problem_issue`(`id`) ON DELETE CASCADE ON UPDATE CASCADE; diff --git a/prisma/migrations/20240113161205_problem_issue_comment_user/migration.sql b/prisma/migrations/20240113161205_problem_issue_comment_user/migration.sql new file mode 100644 index 0000000..ef56e12 --- /dev/null +++ b/prisma/migrations/20240113161205_problem_issue_comment_user/migration.sql @@ -0,0 +1,11 @@ +/* + Warnings: + + - Added the required column `userId` to the `problem_issue_comment` table without a default value. This is not possible if the table is not empty. + +*/ +-- AlterTable +ALTER TABLE `problem_issue_comment` ADD COLUMN `userId` VARCHAR(191) NOT NULL; + +-- AddForeignKey +ALTER TABLE `problem_issue_comment` ADD CONSTRAINT `problem_issue_comment_userId_fkey` FOREIGN KEY (`userId`) REFERENCES `user`(`id`) ON DELETE CASCADE ON UPDATE CASCADE; diff --git a/prisma/migrations/20240114010304_chagne_issue_comment_name/migration.sql b/prisma/migrations/20240114010304_chagne_issue_comment_name/migration.sql new file mode 100644 index 0000000..f757948 --- /dev/null +++ b/prisma/migrations/20240114010304_chagne_issue_comment_name/migration.sql @@ -0,0 +1,19 @@ +/* + Warnings: + + - You are about to drop the column `problemId` on the `problem_issue_comment` table. All the data in the column will be lost. + - Added the required column `issueId` to the `problem_issue_comment` table without a default value. This is not possible if the table is not empty. + +*/ +-- DropForeignKey +ALTER TABLE `problem_issue_comment` DROP FOREIGN KEY `problem_issue_comment_problemId_fkey`; + +-- AlterTable +ALTER TABLE `problem_issue` MODIFY `title` VARCHAR(100) NOT NULL DEFAULT 'Title Here'; + +-- AlterTable +ALTER TABLE `problem_issue_comment` DROP COLUMN `problemId`, + ADD COLUMN `issueId` INTEGER NOT NULL; + +-- AddForeignKey +ALTER TABLE `problem_issue_comment` ADD CONSTRAINT `problem_issue_comment_issueId_fkey` FOREIGN KEY (`issueId`) REFERENCES `problem_issue`(`id`) ON DELETE CASCADE ON UPDATE CASCADE; diff --git a/prisma/migrations/20240114021108_issue_comment_problem_relation/migration.sql b/prisma/migrations/20240114021108_issue_comment_problem_relation/migration.sql new file mode 100644 index 0000000..c7a7906 --- /dev/null +++ b/prisma/migrations/20240114021108_issue_comment_problem_relation/migration.sql @@ -0,0 +1,11 @@ +/* + Warnings: + + - Added the required column `problemId` to the `problem_issue_comment` table without a default value. This is not possible if the table is not empty. + +*/ +-- AlterTable +ALTER TABLE `problem_issue_comment` ADD COLUMN `problemId` INTEGER NOT NULL; + +-- AddForeignKey +ALTER TABLE `problem_issue_comment` ADD CONSTRAINT `problem_issue_comment_problemId_fkey` FOREIGN KEY (`problemId`) REFERENCES `problem`(`id`) ON DELETE CASCADE ON UPDATE CASCADE; diff --git a/prisma/migrations/20240114021239_modify_problem_issue_problem/migration.sql b/prisma/migrations/20240114021239_modify_problem_issue_problem/migration.sql new file mode 100644 index 0000000..2d6ec5e --- /dev/null +++ b/prisma/migrations/20240114021239_modify_problem_issue_problem/migration.sql @@ -0,0 +1,16 @@ +/* + Warnings: + + - You are about to drop the column `targetId` on the `problem_issue` table. All the data in the column will be lost. + - Added the required column `problemId` to the `problem_issue` table without a default value. This is not possible if the table is not empty. + +*/ +-- DropForeignKey +ALTER TABLE `problem_issue` DROP FOREIGN KEY `problem_issue_targetId_fkey`; + +-- AlterTable +ALTER TABLE `problem_issue` DROP COLUMN `targetId`, + ADD COLUMN `problemId` INTEGER NOT NULL; + +-- AddForeignKey +ALTER TABLE `problem_issue` ADD CONSTRAINT `problem_issue_problemId_fkey` FOREIGN KEY (`problemId`) REFERENCES `problem`(`id`) ON DELETE CASCADE ON UPDATE CASCADE; diff --git a/prisma/schema.prisma b/prisma/schema.prisma index 8809489..e82a493 100644 --- a/prisma/schema.prisma +++ b/prisma/schema.prisma @@ -12,19 +12,20 @@ datasource db { } model User { - id String @id @default(uuid()) - nickname String @unique + id String @id @default(uuid()) + nickname String @unique password String - email String @unique + email String @unique message String? github String? blog String? - type UserType? @default(User) - createdAt DateTime @default(now()) - updatedAt DateTime @updatedAt + type UserType? @default(User) + createdAt DateTime @default(now()) + updatedAt DateTime @updatedAt problems Problem[] Submission Submission[] Issues ProblemIssue[] + IssueComments ProblemIssueComment[] @@map("user") } @@ -53,9 +54,10 @@ model Problem { tags Json isArchived Boolean @default(false) - deletedAt DateTime? + deletedAt DateTime? - issues ProblemIssue[] + issues ProblemIssue[] + comments ProblemIssueComment[] createdAt DateTime @default(now()) updatedAt DateTime @updatedAt @@ -82,10 +84,10 @@ enum ResponseType { TIME_LIMIT_EXCEED // 5 COMPILE_ERROR // 6 RUNTIME_ERROR_SIGSEGV // 7 - RUNTIME_ERROR_SIGXFSZ //8 + RUNTIME_ERROR_SIGXFSZ // 8 RUNTIME_ERROR_SIGFPE // 9 RUNTIME_ERROR_SIGABRT // 10 - RUNTIME_ERROR_NZEC //11 + RUNTIME_ERROR_NZEC // 11 RUNTIME_ERROR // 12 INTERNAL_ERROR // 13 EXEC_FORMAT_ERROR // 14 @@ -98,6 +100,7 @@ model Submission { codeLength Int @default(0) memory Float @default(0) // KB time Float @default(0) // second + isPublic Boolean @default(false) languageId Int // Language ID language String // Language Name isCorrect Boolean @@ -117,17 +120,33 @@ model Submission { } model ProblemIssue { - id Int @id @default(autoincrement()) - title String @db.VarChar(100) - content String @db.Text - + id Int @id @default(autoincrement()) + title String @db.VarChar(100) @default("Title Here") + content String @db.Text // 1:N relation with Problem - targetId Int - target Problem @relation(references: [id], fields: [targetId], onDelete: Cascade) - + problemId Int + problem Problem @relation(references: [id], fields: [problemId], onDelete: Cascade) // 1:N relation with User - issuerId String - issuer User @relation(references: [id], fields: [issuerId],onDelete: Cascade) + issuerId String + issuer User @relation(references: [id], fields: [issuerId], onDelete: Cascade) + + comments ProblemIssueComment[] @@map("problem_issue") -} \ No newline at end of file +} + +model ProblemIssueComment { + id Int @id @default(autoincrement()) + content String @db.Text + + issueId Int + issue ProblemIssue @relation(fields: [issueId], references: [id], onDelete: Cascade, onUpdate: Cascade) + + userId String + user User @relation(fields: [userId],references: [id],onDelete: Cascade, onUpdate: Cascade) + + problemId Int + problem Problem @relation(references: [id],fields: [problemId]) + + @@map("problem_issue_comment") +} diff --git a/src/auth/auth.controller.e2e-spec.ts b/src/auth/auth.controller.e2e-spec.ts index 4f10c1c..048592f 100644 --- a/src/auth/auth.controller.e2e-spec.ts +++ b/src/auth/auth.controller.e2e-spec.ts @@ -1,11 +1,13 @@ import { INestApplication } from '@nestjs/common'; import { Test, TestingModule } from '@nestjs/testing'; import { AppModule } from 'app/app.module'; +import { PrismaService } from 'app/prisma/prisma.service'; import * as request from 'supertest'; import { userSignupGen } from 'test/mock-generator'; describe('/auth Auth Controller', () => { let app: INestApplication; + let prisma: PrismaService; // Mock user const user1 = userSignupGen(); @@ -16,10 +18,13 @@ describe('/auth Auth Controller', () => { }).compile(); app = testModule.createNestApplication(); + prisma = testModule.get(PrismaService); + await app.init(); }); afterAll(async () => { + await prisma.deleteAll(); await app.close(); }); diff --git a/src/judge/contributer/dto/update-problem.dto.ts b/src/judge/contributer/dto/update-problem.dto.ts index 3d76f5e..99e5862 100644 --- a/src/judge/contributer/dto/update-problem.dto.ts +++ b/src/judge/contributer/dto/update-problem.dto.ts @@ -17,27 +17,27 @@ export class UpdateProblmeDto extends OmitType(ProblemDomain, [ 'deletedAt', ]) { @IsString() - @IsNotEmpty() + @IsOptional() title: string; @IsString() - @IsNotEmpty() + @IsOptional() problem: string; @IsString() - @IsNotEmpty() + @IsOptional() input: string; @IsString() - @IsNotEmpty() + @IsOptional() output: string; @IsNumber() - @IsNotEmpty() + @IsOptional() timeLimit: number; @IsNumber() - @IsNotEmpty() + @IsOptional() memoryLimit: number; @IsArray() diff --git a/src/judge/decorator/comment.guard.ts b/src/judge/decorator/comment.guard.ts new file mode 100644 index 0000000..e69de29 diff --git a/src/judge/decorator/issue.guard.ts b/src/judge/decorator/issue.guard.ts new file mode 100644 index 0000000..03ce3c4 --- /dev/null +++ b/src/judge/decorator/issue.guard.ts @@ -0,0 +1,41 @@ +import { + CanActivate, + ExecutionContext, + Inject, + Injectable, + NotFoundException, +} from '@nestjs/common'; +import { PrismaService } from 'app/prisma/prisma.service'; +import { Request } from 'express'; + +/** + * Problem Issue Id Checker + * Only use for problem issue id required routers ('iid') + * + * Return 404 NotFound Error if problem issue does not found + */ + +@Injectable() +export class ProblemIssueGuard implements CanActivate { + constructor(@Inject(PrismaService) private prisma: PrismaService) {} + + async canActivate(context: ExecutionContext): Promise { + const request = context.switchToHttp().getRequest(); + const issueId = request.params['iid']; + + try { + await this.prisma.problemIssue.findUniqueOrThrow({ + where: { + id: parseInt(issueId), + }, + }); + return true; + } catch (err) { + if (err.code === 'P2025') { + throw new NotFoundException('ISSUE_NOT_FOUND'); + } + throw err; + } + return true; + } +} diff --git a/src/judge/decorator/problem.guard.ts b/src/judge/decorator/problem.guard.ts index 6cc8586..b33697e 100644 --- a/src/judge/decorator/problem.guard.ts +++ b/src/judge/decorator/problem.guard.ts @@ -1,18 +1,18 @@ import { - BadRequestException, CanActivate, ExecutionContext, Inject, Injectable, + NotFoundException, } from '@nestjs/common'; import { PrismaService } from 'app/prisma/prisma.service'; import { Request } from 'express'; /** * Problem Id Checker - * Only use for problem id required routers + * Only use for problem id required routers ('pid') * - * Return 400 BadRequest Error if problem does not exist + * Return 404 NotFound Error if problem does not exist */ @Injectable() @@ -32,7 +32,10 @@ export class ProblemGuard implements CanActivate { }); return true; } catch (err) { - throw new BadRequestException('PROBLEM_NOT_FOUND'); + if (err.code === 'P2025') { + throw new NotFoundException('PROBLEM_NOT_FOUND'); + } + throw err; } } } diff --git a/src/judge/dto/create-problem-issue-comment.dto.ts b/src/judge/dto/create-problem-issue-comment.dto.ts new file mode 100644 index 0000000..3cc3a98 --- /dev/null +++ b/src/judge/dto/create-problem-issue-comment.dto.ts @@ -0,0 +1,12 @@ +import { PickType } from '@nestjs/swagger'; +import { IsNotEmpty, IsString } from 'class-validator'; +import { ProblemIssueCommentDomain } from 'domains/problem-issue-comment.domain'; + +export class CreateProblemIssueCommentDto extends PickType( + ProblemIssueCommentDomain, + ['content'], +) { + @IsString() + @IsNotEmpty() + content: string; +} diff --git a/src/judge/dto/create-problem-issue.dto.ts b/src/judge/dto/create-problem-issue.dto.ts new file mode 100644 index 0000000..157649b --- /dev/null +++ b/src/judge/dto/create-problem-issue.dto.ts @@ -0,0 +1,16 @@ +import { PickType } from '@nestjs/swagger'; +import { IsNotEmpty, IsString } from 'class-validator'; +import { ProblemIssueDomain } from 'domains'; + +export class CreateProblemIssueDto extends PickType(ProblemIssueDomain, [ + 'title', + 'content', +]) { + @IsString() + @IsNotEmpty() + title: string; + + @IsString() + @IsNotEmpty() + content: string; +} diff --git a/src/judge/dto/index.ts b/src/judge/dto/index.ts index a2d318c..255db43 100644 --- a/src/judge/dto/index.ts +++ b/src/judge/dto/index.ts @@ -1,2 +1,5 @@ export * from './submit-problem.dto'; export * from './run-problem.dto'; +export * from './update-submission.dto'; +export * from './create-problem-issue.dto'; +export * from './create-problem-issue-comment.dto'; diff --git a/src/judge/dto/submit-problem.dto.ts b/src/judge/dto/submit-problem.dto.ts index cac96ab..d3d0a03 100644 --- a/src/judge/dto/submit-problem.dto.ts +++ b/src/judge/dto/submit-problem.dto.ts @@ -1,15 +1,20 @@ import { ApiProperty, PickType } from '@nestjs/swagger'; -import { IsNotEmpty, IsNumber, IsString } from 'class-validator'; +import { IsBoolean, IsNotEmpty, IsNumber, IsString } from 'class-validator'; import { SubmissionDomain } from 'domains'; export class SubmitProblemDto extends PickType(SubmissionDomain, [ 'code', 'languageId', + 'isPublic', ]) { @IsString() @IsNotEmpty() code: string; + @IsBoolean() + @IsNotEmpty() + isPublic: boolean; + @IsNumber() @IsNotEmpty() languageId: number; diff --git a/src/judge/dto/update-submission.dto.ts b/src/judge/dto/update-submission.dto.ts new file mode 100644 index 0000000..7f791c2 --- /dev/null +++ b/src/judge/dto/update-submission.dto.ts @@ -0,0 +1,11 @@ +import { PickType } from '@nestjs/swagger'; +import { IsBoolean, IsNotEmpty } from 'class-validator'; +import { SubmissionDomain } from 'domains'; + +export class UpdateSubmissionDto extends PickType(SubmissionDomain, [ + 'isPublic', +]) { + @IsBoolean() + @IsNotEmpty() + isPublic: boolean; +} diff --git a/src/judge/judge.controller.e2e-spec.ts b/src/judge/judge.controller.e2e-spec.ts new file mode 100644 index 0000000..524547e --- /dev/null +++ b/src/judge/judge.controller.e2e-spec.ts @@ -0,0 +1,566 @@ +import { INestApplication } from '@nestjs/common'; +import { Test, TestingModule } from '@nestjs/testing'; +import { InitializeAdmin } from 'app/admin-init'; +import { AppModule } from 'app/app.module'; +import { PrismaService } from 'app/prisma/prisma.service'; +import * as request from 'supertest'; +import { userSignupGen } from 'test/mock-generator'; +import { BearerTokenHeader } from 'test/test-utils'; + +describe('/judge Judge Controller', () => { + let app: INestApplication; + let prisma: PrismaService; + + // Mock User + const user1 = userSignupGen(); + let user1Token: string; + const user2 = userSignupGen(); + let user2Token: string; + let adminToken: string; + let problemId: number; + let exampleId: number; + let submissionId: number; + let issueId: number; // owner: user1 + let issue2Id: number; // owner: user2 + let commentId: number; // owner: user1 + + beforeAll(async () => { + const testModule: TestingModule = await Test.createTestingModule({ + imports: [AppModule], + }).compile(); + + app = testModule.createNestApplication(); + prisma = testModule.get(PrismaService); + + await InitializeAdmin(app); + await app.init(); + }); + + afterAll(async () => { + await prisma.deleteAll(); + await app.close(); + }); + + // Create user1 + describe('/signup POST', () => { + it('should create user1', async () => { + const signup = await request(app.getHttpServer()) + .post('/auth/signup') + .send(user1); + expect(signup.statusCode).toBe(200); + expect(signup.body).not.toBeUndefined(); + user1Token = signup.body['accessToken']; + }); + + it('should create user2', async () => { + const signup = await request(app.getHttpServer()) + .post('/auth/signup') + .send(user2); + + expect(signup.statusCode).toBe(200); + expect(signup.body).not.toBeUndefined(); + user2Token = signup.body['accessToken']; + }); + + it('should signup admin', async () => { + const signin = await request(app.getHttpServer()) + .post('/auth/signin') + .send({ + password: process.env.ADMIN_PW, + email: process.env.ADMIN_EMAIL, + }); + + expect(signin.statusCode).toBe(200); + expect(signin.body).not.toBeUndefined(); + adminToken = signin.body['accessToken']; + }); + it('should generate problem', async () => { + const newProblem = await request(app.getHttpServer()) + .post('/judge/contribute/problems') + .set('Authorization', BearerTokenHeader(adminToken)); + expect(newProblem.statusCode).toBe(201); + problemId = newProblem.body['id']; + }); + + it('should generate problme example', async () => { + const example = await request(app.getHttpServer()) + .post(`/judge/contribute/problems/${problemId}/examples`) + .set('Authorization', BearerTokenHeader(adminToken)); + expect(example.statusCode).toBe(201); + exampleId = example.body['id']; + }); + + it('should patch problem example', async () => { + await request(app.getHttpServer()) + .patch(`/judge/contribute/problems/${problemId}/examples/${exampleId}`) + .set('Authorization', BearerTokenHeader(adminToken)) + .send({ + input: '', + output: 'hello world', + isPublic: true, + }) + .expect(200); + }); + }); + + // Test + describe('/languages GET', () => { + it('should throw if unauthenticated', async () => { + return request(app.getHttpServer()).get('/judge').expect(401); + }); + it('should get language list', async () => { + return request(app.getHttpServer()) + .get('/judge/languages') + .set('Authorization', BearerTokenHeader(user1Token)) + .expect(200); + }); + }); + + describe('/ GET', () => { + it('should throw if unauthenticated', async () => { + return request(app.getHttpServer()).get('/judge').expect(401); + }); + it('should get problem list', async () => { + return request(app.getHttpServer()) + .get('/judge') + .set('Authorization', BearerTokenHeader(user1Token)) + .expect(200); + }); + }); + + describe('/:pid GET', () => { + it('should throw if unauthenticated', async () => { + return request(app.getHttpServer()) + .get(`/judge/${problemId}`) + .expect(401); + }); + it('should get problem info', async () => { + return request(app.getHttpServer()) + .get(`/judge/${problemId}`) + .set('Authorization', BearerTokenHeader(user1Token)) + .expect(200); + }); + }); + + describe('/:pid/run POST', () => { + it('should throw if unauthenticated', async () => { + return request(app.getHttpServer()) + .post(`/judge/${problemId}/run`) + .expect(401); + }); + + it('should run code', async () => { + return request(app.getHttpServer()) + .post(`/judge/${problemId}/run`) + .set('Authorization', BearerTokenHeader(user1Token)) + .send({ + code: "print('hello world')", + languageId: 71, // Python3 + }) + .expect(200); + }); + }); + + describe('/:pid/submissions generate submission POST', () => { + it('should throw if unauthenticated', async () => { + return request(app.getHttpServer()) + .post(`/judge/${problemId}/submit`) + .expect(401); + }); + + it('should generate submission', async () => { + /** + * /submit will be deprecated and change to /submissions + * + */ + const response = await request(app.getHttpServer()) + .post(`/judge/${problemId}/submissions`) + .set('Authorization', BearerTokenHeader(user1Token)) + .send({ + code: "print('hello world')", + isPublic: true, + languageId: 71, + language: 'Python3', + }); + expect(response.statusCode).toBe(200); + submissionId = response.body['id']; + }); + }); + + describe('/:pid/submissions GET', () => { + it('should throw if unauthenticated', async () => { + return request(app.getHttpServer()) + .get(`/judge/${problemId}/submissions`) + .expect(401); + }); + + it('should get user submission', () => { + return request(app.getHttpServer()) + .get(`/judge/${problemId}/submissions`) + .set('Authorization', BearerTokenHeader(user1Token)) + .expect(200); + }); + }); + + describe('/:pid/submissions/:sid GET', () => { + it('should throw if unauthenticated', async () => { + return request(app.getHttpServer()) + .get(`/judge/${problemId}/submissions/${submissionId}`) + .expect(401); + }); + + it('should throw if other try to read submission', async () => { + return request(app.getHttpServer()) + .get(`/judge/${problemId}/submissions/${submissionId}`) + .set('Authorization', BearerTokenHeader(user2Token)) + .expect(404); + }); + + it('should read submission', async () => { + return request(app.getHttpServer()) + .get(`/judge/${problemId}/submissions/${submissionId}`) + .set('Authorization', BearerTokenHeader(user1Token)) + .expect(200); + }); + }); + + describe('/:pid/submissions/public', () => { + it('should throw if unauthenticated', async () => { + return request(app.getHttpServer()) + .get(`/judge/${problemId}/submissions/public`) + .expect(401); + }); + it('should get public submision of problem', async () => { + return request(app.getHttpServer()) + .get(`/judge/${problemId}/submissions/public`) + .set('Authorization', BearerTokenHeader(user1Token)) + .expect(200); + }); + }); + + describe('/:pid/submissions/public/:sid GET', () => { + it('should throw if unauthenticated', async () => { + return request(app.getHttpServer()) + .get(`/judge/${problemId}/submissions/public/${submissionId}`) + .expect(401); + }); + + it('should read user submission', () => { + return request(app.getHttpServer()) + .get(`/judge/${problemId}/submissions/public/${submissionId}`) + .set('Authorization', BearerTokenHeader(user1Token)) + .expect(200); + }); + + it('should read even if other user try to read', () => { + return request(app.getHttpServer()) + .get(`/judge/${problemId}/submissions/public/${submissionId}`) + .set('Authorization', BearerTokenHeader(user2Token)) + .expect(200); + }); + }); + + describe('/:pid/submissions/:sid PATCH', () => { + it('should throw if unauthenticated', async () => { + return request(app.getHttpServer()) + .patch(`/judge/${problemId}/submissions/${submissionId}`) + .expect(401); + }); + + it('should throw if other tries to modify public', () => { + return request(app.getHttpServer()) + .patch(`/judge/${problemId}/submissions/${submissionId}`) + .send({ + isPublic: true, + }) + .set('Authorization', BearerTokenHeader(user2Token)) + .expect(403); + }); + + it('should modify public', () => { + return request(app.getHttpServer()) + .patch(`/judge/${problemId}/submissions/${submissionId}`) + .send({ + isPublic: false, // Change submission's isPublic: false + }) + .set('Authorization', BearerTokenHeader(user1Token)) + .expect(200); + }); + }); + + describe('/:pid/submissions/public/:sid GET', () => { + it('should throw if submission is not public', () => { + return request(app.getHttpServer()) + .get(`/judge/${problemId}/submissions/public/${submissionId}`) + .set('Authorization', BearerTokenHeader(user2Token)) + .expect(404); + }); + }); + + describe('/:pid/issues GET', () => { + it('should throw if not authenticated', () => { + return request(app.getHttpServer()) + .get(`/judge/${problemId}/issues`) + .expect(401); + }); + + it('should list problem issues', () => { + return request(app.getHttpServer()) + .get(`/judge/${problemId}/issues`) + .set('Authorization', BearerTokenHeader(user1Token)) + .expect(200); + }); + }); + + describe('/:pid/issues POST', () => { + it('should throw if not authenticated', () => { + return request(app.getHttpServer()) + .post(`/judge/${problemId}/issues`) + .expect(401); + }); + it('should throw if problem is not found', () => { + return request(app.getHttpServer()) + .post(`/judge/1000000/issues`) + .set('Authorization', BearerTokenHeader(user1Token)) + .expect(404); + }); + it('should create new Issue', async () => { + const response = await request(app.getHttpServer()) + .post(`/judge/${problemId}/issues`) + .set('Authorization', BearerTokenHeader(user1Token)) + .send({ + title: 'New Issue', + content: 'New Issue Content', + }); + expect(response.statusCode).toBe(200); + const response2 = await request(app.getHttpServer()) + .post(`/judge/${problemId}/issues`) + .set('Authorization', BearerTokenHeader(user2Token)) + .send({ + title: 'New Issue2', + content: 'New Issue2 Content', + }); + expect(response2.statusCode).toBe(200); + issueId = response.body['id']; + issue2Id = response2.body['id']; + }); + }); + + describe('/:pid/issues/:iid PATCH', () => { + it('should throw if not authenticated', () => { + return request(app.getHttpServer()) + .patch(`/judge/${problemId}/issues/${issueId}`) + .expect(401); + }); + it('should throw if problem not found', () => { + return request(app.getHttpServer()) + .patch(`/judge/90909/issues/${issueId}`) + .set('Authorization', BearerTokenHeader(user1Token)) + .expect(404); + }); + it('should throw if issue not found', () => { + return request(app.getHttpServer()) + .patch(`/judge/${problemId}/issues/909`) + .set('Authorization', BearerTokenHeader(user1Token)) + .send({ + title: 'Updated Issue', + content: 'Updated Issue Content', + }) + .expect(403); + }); + it('should throw if other tries to modify issue', () => { + return request(app.getHttpServer()) + .patch(`/judge/${problemId}/issues/${issueId}`) + .set('Authorization', BearerTokenHeader(user2Token)) + .send({ + title: 'Updated Issue', + content: 'Updated Issue Content', + }) + .expect(403); + }); + it('should update issue', () => { + return request(app.getHttpServer()) + .patch(`/judge/${problemId}/issues/${issueId}`) + .set('Authorization', BearerTokenHeader(user1Token)) + .send({ + title: 'Updated Issue', + content: 'Updated Issue Content', + }) + .expect(200); + }); + }); + + describe('/:pid/issues/:iid DELETE', () => { + it('should throw if not authenticated', () => { + return request(app.getHttpServer()) + .delete(`/judge/${problemId}/issues/${issueId}`) + .expect(401); + }); + + it('should throw if problem not found', () => { + return request(app.getHttpServer()) + .delete(`/judge/9090/issues/${issueId}`) + .set('Authorization', BearerTokenHeader(user1Token)) + .expect(404); + }); + + it('should throw if issue not found', () => { + return request(app.getHttpServer()) + .delete(`/judge/${problemId}/issues/9090`) + .set('Authorization', BearerTokenHeader(user1Token)) + .expect(403); + }); + + it('should throw if other tries to delete issue', () => { + return request(app.getHttpServer()) + .delete(`/judge/${problemId}/issues/${issueId}`) + .set('Authorization', BearerTokenHeader(user2Token)) + .expect(403); + }); + + it('should remove issue', () => { + return request(app.getHttpServer()) + .delete(`/judge/${problemId}/issues/${issueId}`) + .set('Authorization', BearerTokenHeader(user1Token)) + .expect(200); + }); + }); + + // Issue1 Removed + + describe('/:pid/issues/:iid/comments POST', () => { + it('should throw if not authenticated', () => { + return request(app.getHttpServer()) + .post(`/judge/${problemId}/issues/${issue2Id}/comments`) + .expect(401); + }); + + it('should throw if problem not exist', () => { + return request(app.getHttpServer()) + .post(`/judge/909/issues/${issue2Id}/comments`) + .set('Authorization', BearerTokenHeader(user1Token)) + .expect(404); + }); + + it('should throw if issue not found', () => { + return request(app.getHttpServer()) + .post(`/judge/${problemId}/issues/909090/comments`) + .set('Authorization', BearerTokenHeader(user1Token)) + .send({ + content: 'Comment content', + }) + .expect(403); + }); + + it('should create new commnet', async () => { + const response = await request(app.getHttpServer()) + .post(`/judge/${problemId}/issues/${issue2Id}/comments`) + .set('Authorization', BearerTokenHeader(user1Token)) + .send({ + content: 'Comment content', + }); + expect(response.statusCode).toBe(200); + commentId = response.body['id']; + }); + }); + + describe('/:pid/issues/:iid/comments/:cid PATCH', () => { + it('should throw if not authenticated', () => { + return request(app.getHttpServer()) + .patch(`/judge/${problemId}/issues/${issue2Id}/comments/${commentId}`) + .expect(401); + }); + it('should throw if problem not found', () => { + return request(app.getHttpServer()) + .patch(`/judge/909009/issues/${issue2Id}/comments/${commentId}`) + .set('Authorization', BearerTokenHeader(user1Token)) + .expect(404); + }); + it('should throw if issue not found', () => { + return request(app.getHttpServer()) + .patch(`/judge/${problemId}/issues/909009/comments/${commentId}`) + .set('Authorization', BearerTokenHeader(user1Token)) + .send({ + content: 'Modified Comment', + }) + .expect(403); + }); + it('should throw if comment not found', () => { + return request(app.getHttpServer()) + .patch(`/judge/${problemId}/issues/${issue2Id}/comments/9090`) + .set('Authorization', BearerTokenHeader(user1Token)) + .send({ + content: 'Modified Comment', + }) + .expect(403); + }); + it('should throw if other tries to modify comment', () => { + return request(app.getHttpServer()) + .patch(`/judge/${problemId}/issues/${issue2Id}/comments/${commentId}`) + .set('Authorization', BearerTokenHeader(user2Token)) + .send({ + content: 'Modified Comment', + }) + .expect(403); + }); + it('should update comment', () => { + return request(app.getHttpServer()) + .patch(`/judge/${problemId}/issues/${issue2Id}/comments/${commentId}`) + .set('Authorization', BearerTokenHeader(user1Token)) + .send({ + content: 'Modified Comment', + }) + .expect(200); + }); + }); + + describe('/:pid/issues/:iid/comments/:cid DELETE', () => { + it('should throw if not authenticated', () => { + return request(app.getHttpServer()) + .delete(`/judge/${problemId}/issues/${issue2Id}/comments/${commentId}`) + .expect(401); + }); + it('should throw if problem not found', () => { + return request(app.getHttpServer()) + .delete(`/judge/909009/issues/${issue2Id}/comments/${commentId}`) + .set('Authorization', BearerTokenHeader(user1Token)) + .expect(404); + }); + it('should throw if issue not found', () => { + return request(app.getHttpServer()) + .delete(`/judge/${problemId}/issues/909009/comments/${commentId}`) + .set('Authorization', BearerTokenHeader(user1Token)) + .send({ + content: 'Modified Comment', + }) + .expect(403); + }); + it('should throw if comment not found', () => { + return request(app.getHttpServer()) + .delete(`/judge/${problemId}/issues/${issue2Id}/comments/9090`) + .set('Authorization', BearerTokenHeader(user1Token)) + .send({ + content: 'Modified Comment', + }) + .expect(403); + }); + it('should throw if other tries to modify comment', () => { + return request(app.getHttpServer()) + .delete(`/judge/${problemId}/issues/${issue2Id}/comments/${commentId}`) + .set('Authorization', BearerTokenHeader(user2Token)) + .send({ + content: 'Modified Comment', + }) + .expect(403); + }); + it('should update comment', () => { + return request(app.getHttpServer()) + .delete(`/judge/${problemId}/issues/${issue2Id}/comments/${commentId}`) + .set('Authorization', BearerTokenHeader(user1Token)) + .send({ + content: 'Modified Comment', + }) + .expect(200); + }); + }); +}); diff --git a/src/judge/judge.controller.ts b/src/judge/judge.controller.ts index d934866..dcf4ae0 100644 --- a/src/judge/judge.controller.ts +++ b/src/judge/judge.controller.ts @@ -1,9 +1,12 @@ import { Body, Controller, + Delete, Get, + HttpCode, Param, ParseIntPipe, + Patch, Post, UseGuards, } from '@nestjs/common'; @@ -13,14 +16,20 @@ import { JudgeFilter, JudgeFilterObject, } from './decorator/judge-filter.decorator'; -import { JudgeDocs } from './judge.docs'; -import { JudgeService } from './judge.service'; -import { RunProblemDto, SubmitProblemDto } from './dto'; +import { ProblemGuard } from './decorator/problem.guard'; import { SubmissionFilter, SubmissionFilterObject, } from './decorator/submission-filter.decorator'; -import { ProblemGuard } from './decorator/problem.guard'; +import { + CreateProblemIssueCommentDto, + CreateProblemIssueDto, + RunProblemDto, + SubmitProblemDto, + UpdateSubmissionDto, +} from './dto'; +import { JudgeDocs } from './judge.docs'; +import { JudgeService } from './judge.service'; @Controller() @UseGuards(LocalGuard) @@ -44,12 +53,14 @@ export class JudgeController { } @Get('/:pid') + @UseGuards(ProblemGuard) @JudgeDocs.ReadProblem() readProblem(@Param('pid', ParseIntPipe) pid: number) { return this.judgeService.readProblem(pid); } @Post('/:pid/run') + @HttpCode(200) @UseGuards(ProblemGuard) @JudgeDocs.RunProblem() runProblem( @@ -59,7 +70,8 @@ export class JudgeController { return this.judgeService.runProblem(pid, dto); } - @Post(['/:pid/submit', '/:pid/submission']) + @Post(['/:pid/submit', '/:pid/submissions']) + @HttpCode(200) @UseGuards(ProblemGuard) @JudgeDocs.SubmitProblem() submitProblem( @@ -70,7 +82,7 @@ export class JudgeController { return this.judgeService.submitProblem(uid, pid, dto); } - @Get('/:pid/submission') + @Get('/:pid/submissions') @UseGuards(ProblemGuard) @JudgeDocs.ListUserSubmission() listUserSubmissions( @@ -81,4 +93,141 @@ export class JudgeController { ) { return this.judgeService.listUserSubmissions(uid, pid, filter, pagination); } + + @Get('/:pid/submissions/public') + @UseGuards(ProblemGuard) + @JudgeDocs.ListPublicSubmission() + listPublicSubmission( + @Param('pid', ParseIntPipe) pid: number, + @SubmissionFilter() filter: SubmissionFilterObject, + @Pagination() pagination: PaginateObject, + ) { + return this.judgeService.listPublicSubmission(pid, filter, pagination); + } + + @Get('/:pid/submissions/public/:sid') + @UseGuards(ProblemGuard) + @JudgeDocs.ReadPublicSubmission() + readPublicSubmission( + @Param('pid', ParseIntPipe) pid: number, + @Param('sid', ParseIntPipe) sid: number, + ) { + return this.judgeService.readPublicSubmission(pid, sid); + } + + @Get('/:pid/submissions/:sid') + @UseGuards(ProblemGuard) + @JudgeDocs.ReadUserSubmission() + readUserSubmission( + @GetUser('id') uid: string, + @Param('pid', ParseIntPipe) pid: number, + @Param('sid', ParseIntPipe) sid: number, + ) { + return this.judgeService.readUserSubmission(uid, pid, sid); + } + + @Patch('/:pid/submissions/:sid') + @UseGuards(ProblemGuard) + @JudgeDocs.UpdateUserSubmission() + updateUserSubmission( + @GetUser('id') uid: string, + @Param('pid', ParseIntPipe) pid: number, + @Param('sid', ParseIntPipe) sid: number, + @Body() dto: UpdateSubmissionDto, + ) { + return this.judgeService.updateUserSubmission(uid, pid, sid, dto); + } + + @Get('/:pid/issues') + @UseGuards(ProblemGuard) + @JudgeDocs.ListProblemIssue() + listProblemIssue( + @Param('pid', ParseIntPipe) pid: number, + @Pagination() paginate: PaginateObject, + ) { + return this.judgeService.listProblemIssue(pid, paginate); + } + + @Get('/:pid/issues/:iid') + @UseGuards(ProblemGuard) + @JudgeDocs.ReadProblemIssue() + readProblemIssue( + @Param('pid', ParseIntPipe) pid: number, + @Param('iid', ParseIntPipe) iid: number, + ) { + return this.judgeService.readProblemIssue(pid, iid); + } + + @Post('/:pid/issues') + @HttpCode(200) + @UseGuards(ProblemGuard) + @JudgeDocs.CreateProblemIssue() + createProblemIssue( + @GetUser('id') uid: string, + @Param('pid', ParseIntPipe) pid: number, + @Body() dto: CreateProblemIssueDto, + ) { + return this.judgeService.createProblemIssue(dto, uid, pid); + } + + @Patch('/:pid/issues/:iid') + @UseGuards(ProblemGuard) + @JudgeDocs.UpdateProblemIssue() + updateProblemIssue( + @GetUser('id') uid: string, + @Param('pid', ParseIntPipe) pid: number, + @Param('iid', ParseIntPipe) iid: number, + @Body() dto: CreateProblemIssueDto, + ) { + return this.judgeService.updateProblemIssue(uid, pid, iid, dto); + } + + @Delete('/:pid/issues/:iid') + @UseGuards(ProblemGuard) + @JudgeDocs.DeleteProblemIssue() + deleteProblemIssue( + @GetUser('id') uid: string, + @Param('pid', ParseIntPipe) pid: number, + @Param('iid', ParseIntPipe) iid: number, + ) { + return this.judgeService.deleteProblemIssue(uid, pid, iid); + } + + @Post('/:pid/issues/:iid/comments') + @HttpCode(200) + @UseGuards(ProblemGuard) + @JudgeDocs.CreateProblemIssueComment() + createProblemIssueComment( + @GetUser('id') uid: string, + @Param('pid', ParseIntPipe) pid: number, + @Param('iid', ParseIntPipe) iid: number, + @Body() dto: CreateProblemIssueCommentDto, + ) { + return this.judgeService.createProblemIssueComment(uid, pid, iid, dto); + } + + @Patch('/:pid/issues/:iid/comments/:cid') + @UseGuards(ProblemGuard) + @JudgeDocs.UpdateProblemIssueComment() + updateProblmeIssueComment( + @GetUser('id') uid: string, + @Param('pid', ParseIntPipe) pid: number, + @Param('iid', ParseIntPipe) iid: number, + @Param('cid', ParseIntPipe) cid: number, + @Body() dto: CreateProblemIssueCommentDto, + ) { + return this.judgeService.updateProblemIssueComment(uid, pid, iid, cid, dto); + } + + @Delete('/:pid/issues/:iid/comments/:cid') + @UseGuards(ProblemGuard) + @JudgeDocs.DeleteProblemIssueComment() + deleteProblemIssueComment( + @GetUser('id') uid: string, + @Param('pid', ParseIntPipe) pid: number, + @Param('iid', ParseIntPipe) iid: number, + @Param('cid', ParseIntPipe) cid: number, + ) { + return this.judgeService.deleteProblemIssueComment(uid, pid, iid, cid); + } } diff --git a/src/judge/judge.docs.ts b/src/judge/judge.docs.ts index 5b0cc57..2114e3d 100644 --- a/src/judge/judge.docs.ts +++ b/src/judge/judge.docs.ts @@ -2,21 +2,30 @@ import { applyDecorators } from '@nestjs/common'; import { ApiBadRequestResponse, ApiBearerAuth, + ApiForbiddenResponse, + ApiNotFoundResponse, ApiOkResponse, ApiOperation, ApiTags, } from '@nestjs/swagger'; -import { GetLanguagesResponse } from './response/get-languages.response'; -import { ListProblemResponse } from './response/list-problem.response'; -import { ReadProblemResponse } from './response/read-problem.response'; + +import { PaginationDocs } from 'app/decorator'; +import { ProblemIssueDomain, SubmissionDomain } from 'domains'; +import { SubmissionFilterDocs } from './decorator/submission-filter.decorator'; import { + CreateProblemIssueCommentResponse, + DeleteProblemIssueCommentResponse, + DeleteProblemIssueResponse, + GetLanguagesResponse, + ListProblemIssueResponse, + ListProblemResponse, ListUserSubmissionRepsonse, + ReadProblemResponse, + ReadPublicSubmissionResponse, RunProblemResponse, SubmitProblemResponse, } from './response'; -import { SubmissionFilterDocs } from './decorator/submission-filter.decorator'; -import { PaginationDocs } from 'app/decorator'; -import { JudgeFilterDocs } from './decorator/judge-filter.decorator'; +import { ReadProblemIssueResponse } from './response/read-problem-issue.response'; export class JudgeDocs { public static Controller() { @@ -33,8 +42,6 @@ export class JudgeDocs { return applyDecorators( ApiOperation({ summary: '문제 리스트 출력' }), ApiOkResponse({ type: ListProblemResponse, isArray: true }), - ...PaginationDocs, - ...JudgeFilterDocs, ); } @@ -44,7 +51,7 @@ export class JudgeDocs { ApiOkResponse({ type: ReadProblemResponse, }), - ApiBadRequestResponse({ description: ['PROBLEM_NOT_FOUND'].join(', ') }), + ApiNotFoundResponse({ description: ['PROBLEM_NOT_FOUND'].join(', ') }), ); } @@ -56,8 +63,9 @@ export class JudgeDocs { isArray: true, }), ApiBadRequestResponse({ - description: ['PROBLEM_NOT_FOUND', 'EXAMPLE_NOT_EXIST'].join(', '), + description: ['EXAMPLE_NOT_EXIST'].join(', '), }), + ApiNotFoundResponse({ description: ['PROBLEM_NOT_FOUND'].join(', ') }), ); } @@ -68,17 +76,156 @@ export class JudgeDocs { type: SubmitProblemResponse, }), ApiBadRequestResponse({ - description: ['PROBLEM_NOT_FOUND', 'EXAMPLE_NOT_EXIST'].join(', '), + description: ['EXAMPLE_NOT_EXIST'].join(', '), }), + ApiNotFoundResponse({ description: ['PROBLEM_NOT_FOUND'].join(', ') }), ); } public static ListUserSubmission() { return applyDecorators( ApiOperation({ summary: '사용자 Submission 리스트' }), - ApiOkResponse({ type: ListUserSubmissionRepsonse, isArray: true }), + ApiOkResponse({ type: ListUserSubmissionRepsonse }), + ApiNotFoundResponse({ + description: ['PROBLEM_NOT_FOUND', 'SUBMISSION_NOT_FOUND'].join(', '), + }), ...SubmissionFilterDocs, ...PaginationDocs, ); } + + public static ListPublicSubmission() { + return applyDecorators( + ApiOperation({ summary: '공개된 Submission 리스트' }), + ApiOkResponse({ type: ListUserSubmissionRepsonse }), + ApiNotFoundResponse({ + description: ['PROBLEM_NOT_FOUND', 'SUBMISSION_NOT_FOUND'].join(', '), + }), + ...SubmissionFilterDocs, + ...PaginationDocs, + ); + } + + public static ReadPublicSubmission() { + return applyDecorators( + ApiOperation({ summary: '공개된 Submission 상세보기' }), + ApiOkResponse({ type: ReadPublicSubmissionResponse }), + ApiNotFoundResponse({ + description: ['PROBLEM_NOT_FOUND', 'SUBMISSION_NOT_FOUND'].join(', '), + }), + ); + } + + public static ReadUserSubmission() { + return applyDecorators( + ApiOperation({ summary: '사용자 Submission 상세보기' }), + ApiOkResponse({ type: SubmissionDomain }), + ApiNotFoundResponse({ + description: ['PROBLEM_NOT_FOUND', 'SUBMISSION_NOT_FOUND'].join(', '), + }), + ); + } + + public static UpdateUserSubmission() { + return applyDecorators( + ApiOperation({ summary: '사용자 Submission isPublic 변경' }), + ApiOkResponse({ type: SubmissionDomain }), + ApiForbiddenResponse({ + description: ['SUBMISSION_NOT_FOUND'].join(', '), + }), + ApiNotFoundResponse({ + description: ['PROBLEM_NOT_FOUND'].join(', '), + }), + ); + } + + public static ListProblemIssue() { + return applyDecorators( + ApiOperation({ summary: 'Problem의 Issue 리스트' }), + ApiOkResponse({ type: ListProblemIssueResponse, isArray: true }), + ApiNotFoundResponse({ + description: ['PROBLEM_NOT_FOUND', 'ISSUE_NOT_FOUND'].join(', '), + }), + ...PaginationDocs, + ); + } + + public static ReadProblemIssue() { + return applyDecorators( + ApiOperation({ summary: 'Problem의 Issue 조회' }), + ApiOkResponse({ type: ReadProblemIssueResponse }), + ApiNotFoundResponse({ + description: ['PROBLEM_NOT_FOUND', 'ISSUE_NOT_FOUND'].join(', '), + }), + ); + } + + public static CreateProblemIssue() { + return applyDecorators( + ApiOperation({ summary: 'Problem의 Issue 생성' }), + ApiOkResponse({ type: ProblemIssueDomain }), + ApiNotFoundResponse({ + description: ['PROBLEM_NOT_FOUND'].join(', '), + }), + ); + } + + public static UpdateProblemIssue() { + return applyDecorators( + ApiOperation({ summary: 'Problem Issue 수정' }), + ApiOkResponse({ type: ProblemIssueDomain }), + ApiForbiddenResponse({ + description: ['ISSUE_NOT_FOUND'].join(', '), + }), + ApiNotFoundResponse({ + description: ['PROBLEM_NOT_FOUND'].join(', '), + }), + ); + } + + public static DeleteProblemIssue() { + return applyDecorators( + ApiOperation({ summary: 'Problem Issue 삭제' }), + ApiOkResponse({ type: DeleteProblemIssueResponse }), + ApiForbiddenResponse({ + description: ['ISSUE_NOT_FOUND'].join(', '), + }), + ApiNotFoundResponse({ + description: ['PROBLEM_NOT_FOUND'].join(', '), + }), + ); + } + + public static CreateProblemIssueComment() { + return applyDecorators( + ApiOperation({ summary: 'Problem Issue Comment 생성' }), + ApiOkResponse({ type: CreateProblemIssueCommentResponse }), + ApiForbiddenResponse({ + description: ['ISSUE_NOT_FOUND'].join(', '), + }), + ApiNotFoundResponse({ description: ['PROBLEM_NOT_FOUND'].join(', ') }), + ); + } + + public static UpdateProblemIssueComment() { + return applyDecorators( + ApiOperation({ summary: 'Problem Issue Comment 수정' }), + ApiOkResponse({ type: CreateProblemIssueCommentResponse }), + ApiForbiddenResponse({ + description: ['ISSUE_COMMENT_NOT_FOUND'].join(', '), + }), + ApiNotFoundResponse({ description: ['PROBLEM_NOT_FOUND'].join(', ') }), + ); + } + + public static DeleteProblemIssueComment() { + return applyDecorators( + ApiOperation({ summary: 'Problem Issue Comment 삭제' }), + ApiOkResponse({ type: DeleteProblemIssueCommentResponse }), + ApiForbiddenResponse({ + description: ['ISSUE_COMMENT_NOT_FOUND'].join(', '), + }), + ApiNotFoundResponse({ description: ['PROBLEM_NOT_FOUND'].join(', ') }), + ); + } } diff --git a/src/judge/judge.service.spec.ts b/src/judge/judge.service.spec.ts new file mode 100644 index 0000000..9e1a6b0 --- /dev/null +++ b/src/judge/judge.service.spec.ts @@ -0,0 +1,33 @@ +import { PrismaService } from 'app/prisma/prisma.service'; +import { JudgeService } from './judge.service'; +import { userSignupGen } from 'test/mock-generator'; +import { Test, TestingModule } from '@nestjs/testing'; +import { PrismaModule } from 'app/prisma/prisma.module'; +import { Judge0Module } from 'judge/judge0'; + +describe('JudgeService', () => { + let service: JudgeService; + let prisma: PrismaService; + + const user1 = userSignupGen(); + const user2 = userSignupGen(); + + beforeAll(async () => { + const module: TestingModule = await Test.createTestingModule({ + imports: [PrismaModule, Judge0Module], + providers: [JudgeService], + }).compile(); + + service = module.get(JudgeService); + prisma = module.get(PrismaService); + }); + afterAll(async () => { + await prisma.deleteAll(); + }); + + describe('Test', () => { + it('Test', () => { + expect(true); + }); + }); +}); diff --git a/src/judge/judge.service.ts b/src/judge/judge.service.ts index adea984..739721e 100644 --- a/src/judge/judge.service.ts +++ b/src/judge/judge.service.ts @@ -1,12 +1,32 @@ -import { BadRequestException, Injectable } from '@nestjs/common'; +import { + BadRequestException, + ForbiddenException, + Injectable, + NotFoundException, +} from '@nestjs/common'; import { PaginateObject } from 'app/decorator'; import { PrismaService } from 'app/prisma/prisma.service'; import { Judge0Service } from 'judge/judge0'; import { JudgeFilterObject } from './decorator/judge-filter.decorator'; import { SubmissionFilterObject } from './decorator/submission-filter.decorator'; -import { RunProblemDto, SubmitProblemDto } from './dto'; +import { + CreateProblemIssueCommentDto, + CreateProblemIssueDto, + RunProblemDto, + SubmitProblemDto, + UpdateSubmissionDto, +} from './dto'; import { GetLanguagesResponse } from './response/get-languages.response'; +/** + * Prisma 2025 -> Target entity not found + * + * + * Problem not found -> 404 + * Problem comment, issue not found -> 403 + * Problem exist but with wrong issue id or comment id -> 403 + */ + @Injectable() export class JudgeService { constructor(private prisma: PrismaService, private judge0: Judge0Service) {} @@ -117,7 +137,6 @@ export class JudgeService { // If example not exist -> prevent submit throw new BadRequestException('EXAMPLE_NOT_EXIST'); } - const results = await Promise.all( examples.map((example) => { return this.judge0.submit( @@ -221,6 +240,7 @@ export class JudgeService { time: data.time, languageId: dto.languageId, language: dto.language, + isPublic: dto.isPublic, isCorrect: data.isCorrect, response: data.description, userId: uid, @@ -235,6 +255,22 @@ export class JudgeService { filter: SubmissionFilterObject, pagination: PaginateObject, ) { + const aggregate = await this.prisma.submission.groupBy({ + by: ['response'], + _count: true, + where: { + userId: uid, + problemId: pid, + }, + }); + const aggregationMap = { + all: 0, + }; + aggregate.map((group) => { + aggregationMap.all += group._count; + aggregationMap[group.response] = group._count; + }); + // Take submission List const submissionList = await this.prisma.submission.findMany({ skip: pagination.skip, @@ -245,8 +281,321 @@ export class JudgeService { where: { ...filter.Where, userId: uid, + problemId: pid, + }, + }); + return { + aggregate: aggregationMap, + data: submissionList, + }; + } + + async listPublicSubmission( + pid: number, + filter: SubmissionFilterObject, + pagination: PaginateObject, + ) { + const aggregate = await this.prisma.submission.groupBy({ + by: ['response'], + _count: true, + where: { + isPublic: true, + problemId: pid, }, }); - return submissionList; + const aggregationMap = { + all: 0, + }; + aggregate.map((group) => { + aggregationMap.all += group._count; + aggregationMap[group.response] = group._count; + }); + + const submissions = await this.prisma.submission.findMany({ + skip: pagination.skip, + take: pagination.take, + orderBy: { + ...filter.Orderby, + }, + where: { + ...filter.Where, + isPublic: true, + problemId: pid, + }, + include: { + user: { + select: { + id: true, + nickname: true, + email: true, + }, + }, + }, + }); + return { + aggreate: aggregationMap, + data: submissions, + }; + } + + async readPublicSubmission(pid: number, sid: number) { + try { + const submission = await this.prisma.submission.findUniqueOrThrow({ + where: { + id: sid, + problemId: pid, + isPublic: true, + }, + include: { + user: { + select: { + id: true, + nickname: true, + email: true, + }, + }, + }, + }); + return submission; + } catch (err) { + // P2025 -> If find unique but not found + //https://www.prisma.io/docs/orm/prisma-migrate/getting-started#baseline-your-production-environment + if (err.code === 'P2025') { + throw new NotFoundException('SUBMISSION_NOT_FOUND'); + } + throw err; + } + } + + async readUserSubmission(uid: string, pid: number, sid: number) { + try { + return await this.prisma.submission.findUniqueOrThrow({ + where: { + id: sid, + problemId: pid, + userId: uid, + }, + }); + } catch (err) { + if (err.code === 'P2025') { + throw new NotFoundException('SUBMISSION_NOT_FOUND'); + } + throw err; + } + } + + async updateUserSubmission( + uid: string, + pid: number, + sid: number, + dto: UpdateSubmissionDto, + ) { + try { + return await this.prisma.submission.update({ + where: { + id: sid, + problemId: pid, + userId: uid, + }, + data: { + ...dto, + }, + }); + } catch (err) { + if (err.code === 'P2025') { + throw new ForbiddenException('SUBMISSION_NOT_FOUND'); + } + throw err; + } + } + + async listProblemIssue(pid: number, paginate: PaginateObject) { + return await this.prisma.problemIssue.findMany({ + ...paginate, + where: { + problemId: pid, + }, + include: { + issuer: { + select: { + id: true, + nickname: true, + }, + }, + problem: { + select: { + id: true, + title: true, + }, + }, + }, + }); + } + + async readProblemIssue(pid: number, iid: number) { + return await this.prisma.problemIssue.findUniqueOrThrow({ + where: { + id: iid, + problemId: pid, + }, + include: { + issuer: { + select: { + id: true, + nickname: true, + }, + }, + problem: { + select: { + id: true, + title: true, + }, + }, + comments: true, + }, + }); + } + + async createProblemIssue( + dto: CreateProblemIssueDto, + uid: string, + pid: number, + ) { + return await this.prisma.problemIssue.create({ + data: { + ...dto, + problemId: pid, + issuerId: uid, + }, + }); + } + + async updateProblemIssue( + uid: string, + pid: number, + iid: number, + dto: CreateProblemIssueDto, + ) { + try { + return await this.prisma.problemIssue.update({ + where: { + id: iid, + problemId: pid, + issuerId: uid, + }, + data: { + ...dto, + }, + }); + } catch (err) { + if (err.code === 'P2025') { + throw new ForbiddenException('ISSUE_NOT_FOUND'); + } + throw err; + } + } + + async deleteProblemIssue(uid: string, pid: number, iid: number) { + try { + return await this.prisma.problemIssue.delete({ + where: { + id: iid, + problemId: pid, + issuerId: uid, + }, + select: { + id: true, + title: true, + }, + }); + } catch (err) { + if (err.code === 'P2025') { + throw new ForbiddenException('ISSUE_NOT_FOUND'); + } + throw err; + } + } + + async createProblemIssueComment( + uid: string, + pid: number, + iid: number, + dto: CreateProblemIssueCommentDto, + ) { + try { + await this.prisma.problemIssue.findUniqueOrThrow({ + where: { + id: iid, + }, + }); + } catch (err) { + if (err.code === 'P2025') { + throw new ForbiddenException('ISSUE_NOT_FOUND'); + } + throw err; + } + // Create new issue comment + return await this.prisma.problemIssueComment.create({ + data: { + userId: uid, + issueId: iid, + problemId: pid, + content: dto.content, + }, + }); + } + + async updateProblemIssueComment( + uid: string, + pid: number, + iid: number, + cid: number, + dto: CreateProblemIssueCommentDto, + ) { + try { + return await this.prisma.problemIssueComment.update({ + where: { + id: cid, + userId: uid, + problemId: pid, + issueId: iid, + }, + data: { + content: dto.content, + }, + }); + } catch (err) { + if (err.code === 'P2025') { + throw new ForbiddenException('ISSUE_COMMENT_NOT_FOUND'); + } + throw err; + } + } + + async deleteProblemIssueComment( + uid: string, + pid: number, + iid: number, + cid: number, + ) { + try { + return await this.prisma.problemIssueComment.delete({ + where: { + id: cid, + userId: uid, + problemId: pid, + issueId: iid, + }, + select: { + id: true, + content: true, + }, + }); + } catch (err) { + if (err.code === 'P2025') { + throw new ForbiddenException('ISSUE_COMMENT_NOT_FOUND'); + } + throw err; + } } } diff --git a/src/judge/response/create-problem-issue-comment.response.ts b/src/judge/response/create-problem-issue-comment.response.ts new file mode 100644 index 0000000..2b0c949 --- /dev/null +++ b/src/judge/response/create-problem-issue-comment.response.ts @@ -0,0 +1,3 @@ +import { ProblemIssueCommentDomain } from 'domains/problem-issue-comment.domain'; + +export class CreateProblemIssueCommentResponse extends ProblemIssueCommentDomain {} diff --git a/src/judge/response/delete-problem-issue-comment.response.ts b/src/judge/response/delete-problem-issue-comment.response.ts new file mode 100644 index 0000000..f0e9263 --- /dev/null +++ b/src/judge/response/delete-problem-issue-comment.response.ts @@ -0,0 +1,7 @@ +import { PickType } from '@nestjs/swagger'; +import { ProblemIssueCommentDomain } from 'domains/problem-issue-comment.domain'; + +export class DeleteProblemIssueCommentResponse extends PickType( + ProblemIssueCommentDomain, + ['id', 'content'], +) {} diff --git a/src/judge/response/delete-problem-issue.response.ts b/src/judge/response/delete-problem-issue.response.ts new file mode 100644 index 0000000..980a273 --- /dev/null +++ b/src/judge/response/delete-problem-issue.response.ts @@ -0,0 +1,7 @@ +import { PickType } from '@nestjs/swagger'; +import { ProblemIssueDomain } from 'domains'; + +export class DeleteProblemIssueResponse extends PickType(ProblemIssueDomain, [ + 'id', + 'title', +]) {} diff --git a/src/judge/response/index.ts b/src/judge/response/index.ts index 2cbb29e..d608dd3 100644 --- a/src/judge/response/index.ts +++ b/src/judge/response/index.ts @@ -4,3 +4,11 @@ export * from './read-problem.response'; export * from './submit-problem.response'; export * from './run-problem.response'; export * from './list-user-submission.response'; +export * from './get-languages.response'; +export * from './list-problem.response'; +export * from './read-problem.response'; +export * from './delete-problem-issue.response'; +export * from './list-problem-issue.response'; +export * from './read-public-submission.response'; +export * from './create-problem-issue-comment.response'; +export * from './delete-problem-issue-comment.response'; diff --git a/src/judge/response/list-problem-issue.response.ts b/src/judge/response/list-problem-issue.response.ts new file mode 100644 index 0000000..6b47d33 --- /dev/null +++ b/src/judge/response/list-problem-issue.response.ts @@ -0,0 +1,20 @@ +import { ApiProperty, PickType } from '@nestjs/swagger'; +import { ProblemDomain, ProblemIssueDomain, UserDomain } from 'domains'; + +export class ListProblemIssueResponseIssuer extends PickType(UserDomain, [ + 'id', + 'nickname', +]) {} + +export class ListProblemIssueResponseTarget extends PickType(ProblemDomain, [ + 'id', + 'title', +]) {} + +export class ListProblemIssueResponse extends ProblemIssueDomain { + @ApiProperty({ type: ListProblemIssueResponseIssuer }) + issuer: ListProblemIssueResponseIssuer; + + @ApiProperty({ type: ListProblemIssueResponseTarget }) + target: ListProblemIssueResponseTarget; +} diff --git a/src/judge/response/list-public-submission.response.ts b/src/judge/response/list-public-submission.response.ts new file mode 100644 index 0000000..dc24aae --- /dev/null +++ b/src/judge/response/list-public-submission.response.ts @@ -0,0 +1 @@ +// Share value with 'list-user-submission.response.ts diff --git a/src/judge/response/list-user-submission.response.ts b/src/judge/response/list-user-submission.response.ts index bd1eeae..3587ce4 100644 --- a/src/judge/response/list-user-submission.response.ts +++ b/src/judge/response/list-user-submission.response.ts @@ -1,3 +1,76 @@ -import { SubmissionDomain } from 'domains'; +import { ApiProperty, PickType } from '@nestjs/swagger'; +import { $Enums } from '@prisma/client'; +import { EnumFields } from 'app/type'; +import { SubmissionDomain, UserDomain } from 'domains'; -export class ListUserSubmissionRepsonse extends SubmissionDomain {} +export class ListUserSubmissionAggregate + implements EnumFields<$Enums.ResponseType, number> +{ + @ApiProperty() + all: number; + + @ApiProperty() + CORRECT: number; + + @ApiProperty() + WRONG_ANSWER: number; + + @ApiProperty() + TIME_LIMIT_EXCEED: number; + + @ApiProperty() + COMPILE_ERROR: number; + + @ApiProperty() + RUNTIME_ERROR_SIGSEGV: number; + + @ApiProperty() + RUNTIME_ERROR_SIGXFSZ: number; + + @ApiProperty() + RUNTIME_ERROR_SIGFPE: number; + + @ApiProperty() + RUNTIME_ERROR_SIGABRT: number; + + @ApiProperty() + RUNTIME_ERROR_NZEC: number; + + @ApiProperty() + RUNTIME_ERROR: number; + + @ApiProperty() + INTERNAL_ERROR: number; + + @ApiProperty() + EXEC_FORMAT_ERROR: number; + + @ApiProperty() + MEMORY_LIMIT_EXCEED: number; +} + +export class ListUserSubmissionUserData extends PickType(UserDomain, [ + 'id', + 'nickname', + 'email', +]) {} + +export class ListUserSubmissionData extends SubmissionDomain { + @ApiProperty({ + type: ListUserSubmissionUserData, + }) + user: ListUserSubmissionUserData; +} + +export class ListUserSubmissionRepsonse { + @ApiProperty({ + type: ListUserSubmissionAggregate, + }) + aggregate: ListUserSubmissionAggregate; + + @ApiProperty({ + type: ListUserSubmissionData, + isArray: true, + }) + data: ListUserSubmissionData[]; +} diff --git a/src/judge/response/read-problem-issue.response.ts b/src/judge/response/read-problem-issue.response.ts new file mode 100644 index 0000000..ece48fd --- /dev/null +++ b/src/judge/response/read-problem-issue.response.ts @@ -0,0 +1,13 @@ +// Share response with ListProblemIssueResponse + +import { ApiProperty } from '@nestjs/swagger'; +import { ProblemIssueCommentDomain } from 'domains/problem-issue-comment.domain'; +import { ListProblemIssueResponse } from './list-problem-issue.response'; + +export class ReadProblemIssueResponse extends ListProblemIssueResponse { + @ApiProperty({ + isArray: true, + type: ProblemIssueCommentDomain, + }) + comments: ProblemIssueCommentDomain; +} diff --git a/src/judge/response/read-public-submission.response.ts b/src/judge/response/read-public-submission.response.ts new file mode 100644 index 0000000..08b1df3 --- /dev/null +++ b/src/judge/response/read-public-submission.response.ts @@ -0,0 +1,15 @@ +import { ApiProperty, PickType } from '@nestjs/swagger'; +import { SubmissionDomain, UserDomain } from 'domains'; + +class ReadPublicSubmissionResponseUser extends PickType(UserDomain, [ + 'id', + 'nickname', + 'email', +]) {} + +export class ReadPublicSubmissionResponse extends SubmissionDomain { + @ApiProperty({ + type: ReadPublicSubmissionResponseUser, + }) + user: ReadPublicSubmissionResponseUser; +} diff --git a/src/prisma/prisma.service.ts b/src/prisma/prisma.service.ts index 2a71c0e..dd00e1c 100644 --- a/src/prisma/prisma.service.ts +++ b/src/prisma/prisma.service.ts @@ -84,6 +84,8 @@ export class PrismaService this.problemExample.deleteMany(), this.problem.deleteMany(), this.user.deleteMany(), + this.problemIssue.deleteMany(), + this.problemIssueComment.deleteMany(), ]); } } diff --git a/src/type.ts b/src/type.ts index f54edee..b8cbec1 100644 --- a/src/type.ts +++ b/src/type.ts @@ -2,3 +2,10 @@ export type JwtPayload = { id: string; email: string; }; + +// Common Prisma Enum type + +// Convert enum to interface(type) +export type EnumFields = { + [key in T]: K; +}; diff --git a/src/user/user.controller.e2e-spec.ts b/src/user/user.controller.e2e-spec.ts index 2eb8164..0d2e284 100644 --- a/src/user/user.controller.e2e-spec.ts +++ b/src/user/user.controller.e2e-spec.ts @@ -6,9 +6,11 @@ import { userSignupGen } from 'test/mock-generator'; import * as request from 'supertest'; import { BearerTokenHeader } from 'test/test-utils'; import { User } from '@prisma/client'; +import { PrismaService } from 'app/prisma/prisma.service'; describe('/user User Controller', () => { let app: INestApplication; + let prisma: PrismaService; // MockUser const user1 = userSignupGen(); @@ -24,11 +26,14 @@ describe('/user User Controller', () => { // Initialize nest application app = testModule.createNestApplication(); + prisma = testModule.get(PrismaService); + await InitializeAdmin(app); await app.init(); }); afterAll(async () => { + await prisma.deleteAll(); await app.close(); }); diff --git a/test/jest-e2e.json b/test/jest-e2e.json index f29a1e4..49d07be 100644 --- a/test/jest-e2e.json +++ b/test/jest-e2e.json @@ -7,11 +7,11 @@ "^.+\\.(t|j)s$": "ts-jest" }, "coverageDirectory": "../e2e-coverage", - "collectCoverageFrom": ["**/*.(t|j)s"], + "collectCoverageFrom": ["**/*\\.controller\\.(t|j)s", "**/*\\.dto\\.(t|j)s"], "moduleNameMapper": { "app/(.*)": "/../src/$1", "test/(.*)": "/../test/$1", - "domains": "/../domain/$1", + "domains": "/../domain", "domains/(.*)": "/../domain/$1", "s3/aws-s3": "/../libs/aws-s3/src", "s3/aws-s3/(.*)": "/../libs/aws-s3/src/$1", diff --git a/test/jest-unit.json b/test/jest-unit.json index 88f2e9a..5e769e1 100644 --- a/test/jest-unit.json +++ b/test/jest-unit.json @@ -7,11 +7,11 @@ "^.+\\.(t|j)s$": "ts-jest" }, "coverageDirectory": "../unit-coverage", - "collectCoverageFrom": ["**/*.(t|j)s"], + "collectCoverageFrom": ["**/*\\.service\\.(t|j)s"], "moduleNameMapper": { "app/(.*)": "/../src/$1", "test/(.*)": "/../test/$1", - "domains": "/../domain/$1", + "domains": "/../domain", "domains/(.*)": "/../domain/$1", "s3/aws-s3": "/../libs/aws-s3/src", "s3/aws-s3/(.*)": "/../libs/aws-s3/src/$1",