diff --git a/.github/workflows/cd-prod.yml b/.github/workflows/cd-prod.yml index 0dd837ffa2..960954f964 100644 --- a/.github/workflows/cd-prod.yml +++ b/.github/workflows/cd-prod.yml @@ -109,9 +109,13 @@ jobs: - name: Create Terraform variable file working-directory: ./infra/deploy run: | - echo "${{ secrets.TFVARS }}" >> terraform.tfvars - echo "${{ secrets.OAUTH_GITHUB }}" >> terraform.tfvars - echo "${{ secrets.OAUTH_KAKAO }}" >> terraform.tfvars + echo $TFVARS >> terraform.tfvars + echo $OAUTH_GITHUB >> terraform.tfvars + echo $OAUTH_KAKAO >> terraform.tfvars + env: + TFVARS: ${{ secrets.TFVARS }} + OAUTH_GITHUB: ${{ secrets.OAUTH_GITHUB }} + OAUTH_KAKAO: ${{ secrets.OAUTH_KAKAO }} - name: Terraform Init working-directory: ./infra/deploy diff --git a/backend/apps/admin/src/contest/contest.resolver.ts b/backend/apps/admin/src/contest/contest.resolver.ts index 071f25e421..6dbc9338ce 100644 --- a/backend/apps/admin/src/contest/contest.resolver.ts +++ b/backend/apps/admin/src/contest/contest.resolver.ts @@ -1,11 +1,13 @@ import { InternalServerErrorException, Logger, + NotFoundException, ParseBoolPipe } from '@nestjs/common' import { Args, Context, Int, Mutation, Query, Resolver } from '@nestjs/graphql' import { ContestProblem } from '@generated' import { Contest } from '@generated' +import { PrismaClientKnownRequestError } from '@prisma/client/runtime/library' import { AuthenticatedRequest, UseRolesGuard } from '@libs/auth' import { OPEN_SPACE_ID } from '@libs/constants' import { @@ -15,6 +17,7 @@ import { } from '@libs/exception' import { CursorValidationPipe, GroupIDPipe, RequiredIntPipe } from '@libs/pipe' import { ContestService } from './contest.service' +import { ContestWithParticipants } from './model/contest-with-participants.model' import { CreateContestInput } from './model/contest.input' import { UpdateContestInput } from './model/contest.input' import { PublicizingRequest } from './model/publicizing-request.model' @@ -25,7 +28,7 @@ export class ContestResolver { private readonly logger = new Logger(ContestResolver.name) constructor(private readonly contestService: ContestService) {} - @Query(() => [Contest]) + @Query(() => [ContestWithParticipants]) async getContests( @Args( 'take', @@ -45,6 +48,23 @@ export class ContestResolver { return await this.contestService.getContests(take, groupId, cursor) } + @Query(() => ContestWithParticipants) + async getContest( + @Args('contestId', { type: () => Int }, new RequiredIntPipe('contestId')) + contestId: number + ) { + try { + return await this.contestService.getContest(contestId) + } catch (error) { + if ( + error instanceof PrismaClientKnownRequestError && + error.code == 'P2025' + ) { + throw new NotFoundException(error.message) + } + } + } + @Mutation(() => Contest) async createContest( @Args('input') input: CreateContestInput, @@ -96,7 +116,7 @@ export class ContestResolver { @Mutation(() => Contest) async deleteContest( @Args('groupId', { type: () => Int }, GroupIDPipe) groupId: number, - @Args('contestId', { type: () => Int }, new RequiredIntPipe('contestId')) + @Args('contestId', { type: () => Int }) contestId: number ) { try { @@ -119,7 +139,7 @@ export class ContestResolver { @Mutation(() => PublicizingRequest) async createPublicizingRequest( @Args('groupId', { type: () => Int }, GroupIDPipe) groupId: number, - @Args('contestId', { type: () => Int }, new RequiredIntPipe('contestId')) + @Args('contestId', { type: () => Int }) contestId: number ) { try { @@ -142,7 +162,7 @@ export class ContestResolver { @Mutation(() => PublicizingResponse) @UseRolesGuard() async handlePublicizingRequest( - @Args('contestId', { type: () => Int }, new RequiredIntPipe('contestId')) + @Args('contestId', { type: () => Int }) contestId: number, @Args('isAccepted', ParseBoolPipe) isAccepted: boolean ) { @@ -163,7 +183,7 @@ export class ContestResolver { @Mutation(() => [ContestProblem]) async importProblemsToContest( @Args('groupId', { type: () => Int }, GroupIDPipe) groupId: number, - @Args('contestId', { type: () => Int }, new RequiredIntPipe('contestId')) + @Args('contestId', { type: () => Int }) contestId: number, @Args('problemIds', { type: () => [Int] }) problemIds: number[] ) { diff --git a/backend/apps/admin/src/contest/contest.service.spec.ts b/backend/apps/admin/src/contest/contest.service.spec.ts index 2fca5c01cb..4b6f0f4ca9 100644 --- a/backend/apps/admin/src/contest/contest.service.spec.ts +++ b/backend/apps/admin/src/contest/contest.service.spec.ts @@ -11,6 +11,7 @@ import { stub } from 'sinon' import { EntityNotExistException } from '@libs/exception' import { PrismaService } from '@libs/prisma' import { ContestService } from './contest.service' +import type { ContestWithParticipants } from './model/contest-with-participants.model' import type { CreateContestInput, UpdateContestInput @@ -21,6 +22,10 @@ const contestId = 1 const userId = 1 const groupId = 1 const problemId = 2 +const startTime = faker.date.past() +const endTime = faker.date.future() +const createTime = faker.date.past() +const updateTime = faker.date.past() const contest: Contest = { id: contestId, @@ -28,14 +33,51 @@ const contest: Contest = { groupId, title: 'title', description: 'description', - startTime: faker.date.past(), - endTime: faker.date.future(), + startTime, + endTime, config: { isVisible: true, isRankVisible: true }, - createTime: faker.date.past(), - updateTime: faker.date.past() + createTime, + updateTime +} + +const contestWithCount = { + id: contestId, + createdById: userId, + groupId, + title: 'title', + description: 'description', + startTime, + endTime, + config: { + isVisible: true, + isRankVisible: true + }, + createTime, + updateTime, + // eslint-disable-next-line @typescript-eslint/naming-convention + _count: { + contestRecord: 10 + } +} + +const contestWithParticipants: ContestWithParticipants = { + id: contestId, + createdById: userId, + groupId, + title: 'title', + description: 'description', + startTime, + endTime, + config: { + isVisible: true, + isRankVisible: true + }, + createTime, + updateTime, + participants: 10 } const group: Group = { @@ -61,6 +103,7 @@ const problem: Problem = { inputDescription: 'inputdescription', outputDescription: 'outputdescription', hint: 'hint', + isVisible: true, template: [], languages: ['C'], timeLimit: 10000, @@ -175,10 +218,10 @@ describe('ContestService', () => { describe('getContests', () => { it('should return an array of contests', async () => { - db.contest.findMany.resolves([contest]) + db.contest.findMany.resolves([contestWithCount]) const res = await service.getContests(5, 2, 0) - expect(res).to.deep.equal([contest]) + expect(res).to.deep.equal([contestWithParticipants]) }) }) diff --git a/backend/apps/admin/src/contest/contest.service.ts b/backend/apps/admin/src/contest/contest.service.ts index 956d806ad1..d0b1e86bab 100644 --- a/backend/apps/admin/src/contest/contest.service.ts +++ b/backend/apps/admin/src/contest/contest.service.ts @@ -33,11 +33,44 @@ export class ContestService { async getContests(take: number, groupId: number, cursor: number | null) { const paginator = this.prisma.getPaginator(cursor) - return await this.prisma.contest.findMany({ + const contests = await this.prisma.contest.findMany({ ...paginator, where: { groupId }, - take + take, + include: { + // eslint-disable-next-line @typescript-eslint/naming-convention + _count: { + select: { contestRecord: true } + } + } + }) + + return contests.map((contest) => { + const { _count, ...data } = contest + return { + ...data, + participants: _count.contestRecord + } + }) + } + + async getContest(contestId: number) { + const { _count, ...data } = await this.prisma.contest.findFirstOrThrow({ + where: { + id: contestId + }, + include: { + // eslint-disable-next-line @typescript-eslint/naming-convention + _count: { + select: { contestRecord: true } + } + } }) + + return { + ...data, + participants: _count.contestRecord + } } async createContest( diff --git a/backend/apps/admin/src/contest/model/contest-with-participants.model.ts b/backend/apps/admin/src/contest/model/contest-with-participants.model.ts new file mode 100644 index 0000000000..9fcd0fd275 --- /dev/null +++ b/backend/apps/admin/src/contest/model/contest-with-participants.model.ts @@ -0,0 +1,8 @@ +import { Field, Int, ObjectType } from '@nestjs/graphql' +import { Contest } from '@admin/@generated' + +@ObjectType({ description: 'contestWithParticipants' }) +export class ContestWithParticipants extends Contest { + @Field(() => Int) + participants: number +} diff --git a/backend/apps/admin/src/problem/mock/mock.ts b/backend/apps/admin/src/problem/mock/mock.ts index e07ac5fbaf..12974ba5a4 100644 --- a/backend/apps/admin/src/problem/mock/mock.ts +++ b/backend/apps/admin/src/problem/mock/mock.ts @@ -42,7 +42,8 @@ export const problems: Problem[] = [ updateTime: faker.date.past(), exposeTime: faker.date.anytime(), inputExamples: [], - outputExamples: [] + outputExamples: [], + isVisible: true }, { id: 2, @@ -66,7 +67,8 @@ export const problems: Problem[] = [ updateTime: faker.date.past(), exposeTime: faker.date.anytime(), inputExamples: [], - outputExamples: [] + outputExamples: [], + isVisible: true } ] @@ -119,7 +121,8 @@ export const importedProblems: Problem[] = [ updateTime: faker.date.past(), exposeTime: faker.date.anytime(), inputExamples: [], - outputExamples: [] + outputExamples: [], + isVisible: true }, { id: 33, @@ -159,6 +162,7 @@ export const importedProblems: Problem[] = [ updateTime: faker.date.past(), exposeTime: faker.date.anytime(), inputExamples: [], - outputExamples: [] + outputExamples: [], + isVisible: true } ] diff --git a/backend/apps/admin/src/problem/model/problem.input.ts b/backend/apps/admin/src/problem/model/problem.input.ts index fe5b34ea18..b227ed7340 100644 --- a/backend/apps/admin/src/problem/model/problem.input.ts +++ b/backend/apps/admin/src/problem/model/problem.input.ts @@ -23,6 +23,9 @@ export class CreateProblemInput { @Field(() => String, { nullable: false }) hint!: string + @Field(() => Boolean, { defaultValue: true }) + isVisible!: boolean + @Field(() => [Template], { nullable: false }) template!: Array