diff --git a/apps/backend/apps/admin/src/announcement/announcement.resolver.spec.ts b/apps/backend/apps/admin/src/announcement/announcement.resolver.spec.ts new file mode 100644 index 0000000000..f885afb384 --- /dev/null +++ b/apps/backend/apps/admin/src/announcement/announcement.resolver.spec.ts @@ -0,0 +1,23 @@ +import { Test, type TestingModule } from '@nestjs/testing' +import { expect } from 'chai' +import { AnnouncementResolver } from './announcement.resolver' +import { AnnouncementService } from './announcement.service' + +describe('AnnouncementResolver', () => { + let resolver: AnnouncementResolver + + beforeEach(async () => { + const module: TestingModule = await Test.createTestingModule({ + providers: [ + AnnouncementResolver, + { provide: AnnouncementService, useValue: {} } + ] + }).compile() + + resolver = module.get(AnnouncementResolver) + }) + + it('shoulld be defined', () => { + expect(resolver).to.be.ok + }) +}) diff --git a/apps/backend/apps/admin/src/announcement/announcement.resolver.ts b/apps/backend/apps/admin/src/announcement/announcement.resolver.ts index 621ae6d86b..390ca650e0 100644 --- a/apps/backend/apps/admin/src/announcement/announcement.resolver.ts +++ b/apps/backend/apps/admin/src/announcement/announcement.resolver.ts @@ -1,44 +1,91 @@ +import { InternalServerErrorException, Logger } from '@nestjs/common' import { Resolver, Query, Mutation, Args, Int } from '@nestjs/graphql' -import { Announcement } from '@generated' +import { + DuplicateFoundException, + EntityNotExistException +} from '@libs/exception' +import { Announcement } from '@admin/@generated' import { AnnouncementService } from './announcement.service' -import { CreateAnnouncementInput } from './dto/create-announcement.input' -import { UpdateAnnouncementInput } from './dto/update-announcement.input' +import { AnnouncementInput } from './dto/announcement.input' @Resolver(() => Announcement) export class AnnouncementResolver { + private readonly logger = new Logger(AnnouncementResolver.name) constructor(private readonly announcementService: AnnouncementService) {} @Mutation(() => Announcement) - createAnnouncement( + async createAnnouncement( @Args('createAnnouncementInput') - createAnnouncementInput: CreateAnnouncementInput + announcementInput: AnnouncementInput ) { - return this.announcementService.create(createAnnouncementInput) + try { + return await this.announcementService.createAnnouncement( + announcementInput + ) + } catch (error) { + if (error instanceof DuplicateFoundException || EntityNotExistException) { + throw error.convert2HTTPException() + } + this.logger.error(error) + throw new InternalServerErrorException() + } } - @Query(() => [Announcement], { name: 'announcement' }) - findAll() { - return this.announcementService.findAll() + @Query(() => [Announcement], { name: 'getAnnouncements' }) + async getAnnouncements( + @Args('contestId', { type: () => Int }) contestId: number, + @Args('problemId', { type: () => Int, nullable: true }) problemId?: number + ) { + try { + return await this.announcementService.getAnnouncements( + contestId, + problemId + ) + } catch (error) { + this.logger.error(error) + throw new InternalServerErrorException() + } } - @Query(() => Announcement, { name: 'announcement' }) - findOne(@Args('id', { type: () => Int }) id: number) { - return this.announcementService.findOne(id) + @Query(() => Announcement, { name: 'getAnnouncementById' }) + async getAnnouncementById(@Args('id', { type: () => Int }) id: number) { + try { + return await this.announcementService.getAnnouncementById(id) + } catch (error) { + if (error instanceof EntityNotExistException) { + throw error.convert2HTTPException() + } + this.logger.error(error) + throw new InternalServerErrorException() + } } @Mutation(() => Announcement) - updateAnnouncement( - @Args('updateAnnouncementInput') - updateAnnouncementInput: UpdateAnnouncementInput + async updateAnnouncement( + @Args('id', { type: () => Int }) id: number, + @Args('content', { type: () => String }) content: string ) { - return this.announcementService.update( - updateAnnouncementInput.id, - updateAnnouncementInput - ) + try { + return await this.announcementService.updateAnnouncement(id, content) + } catch (error) { + if (error instanceof EntityNotExistException) { + throw error.convert2HTTPException() + } + this.logger.error(error) + throw new InternalServerErrorException() + } } @Mutation(() => Announcement) - removeAnnouncement(@Args('id', { type: () => Int }) id: number) { - return this.announcementService.remove(id) + async removeAnnouncement(@Args('id', { type: () => Int }) id: number) { + try { + return await this.announcementService.removeAnnouncement(id) + } catch (error) { + if (error instanceof EntityNotExistException) { + throw error.convert2HTTPException() + } + this.logger.error(error) + throw new InternalServerErrorException() + } } } diff --git a/apps/backend/apps/admin/src/announcement/announcement.service.spec.ts b/apps/backend/apps/admin/src/announcement/announcement.service.spec.ts new file mode 100644 index 0000000000..4806b7f3b6 --- /dev/null +++ b/apps/backend/apps/admin/src/announcement/announcement.service.spec.ts @@ -0,0 +1,147 @@ +import { Test, type TestingModule } from '@nestjs/testing' +import { faker } from '@faker-js/faker' +import { PrismaClientKnownRequestError } from '@prisma/client/runtime/library' +import { expect } from 'chai' +import { stub } from 'sinon' +import { + DuplicateFoundException, + EntityNotExistException +} from '@libs/exception' +import { PrismaService } from '@libs/prisma' +import { AnnouncementService } from './announcement.service' + +const problemId = faker.number.int() +const contestId = faker.number.int() +const id = faker.number.int() +const announcementInput = { + problemId, + contestId, + content: faker.string.sample() +} + +const announcement = { + ...announcementInput, + id +} + +const contestProblem = { + problemId +} + +const db = { + announcement: { + findFirst: stub(), + findFirstOrThrow: stub(), + findMany: stub().resolves([announcement]), + update: stub(), + delete: stub(), + create: stub().resolves(announcement) + }, + contestProblem: { + findFirst: stub(), + findFirstOrThrow: stub() + } +} + +const PrismaErrorInstance = new PrismaClientKnownRequestError('error', { + code: '4xx', + clientVersion: 'x.x.x' +}) + +PrismaErrorInstance.name = 'NotFoundError' + +describe('AnnouncementService', () => { + let service: AnnouncementService + + beforeEach(async () => { + const module: TestingModule = await Test.createTestingModule({ + providers: [AnnouncementService, { provide: PrismaService, useValue: db }] + }).compile() + + service = module.get(AnnouncementService) + }) + + it('should be defined', () => { + expect(service).to.be.ok + }) + + describe('createAnnouncement', () => { + it('should return created announcement', async () => { + db.announcement.findFirst.resolves(null) + db.contestProblem.findFirstOrThrow.resolves(contestProblem) + + const res = await service.createAnnouncement(announcementInput) + expect(res).to.deep.equal(announcement) + }) + + it('should throw error when given announcement already exists', async () => { + db.announcement.findFirst.resolves(announcement) + + await expect( + service.createAnnouncement(announcementInput) + ).to.be.rejectedWith(DuplicateFoundException) + }) + + it('should throw error when given problemId is invalid', async () => { + db.announcement.findFirst.resolves(null) + db.contestProblem.findFirstOrThrow.rejects(PrismaErrorInstance) + + await expect( + service.createAnnouncement(announcementInput) + ).to.be.rejectedWith(EntityNotExistException) + }) + }) + + describe('getAnnouncements', () => { + it('should return all announcements', async () => { + db.announcement.findMany() + const res = await service.getAnnouncements(problemId) + expect(res).to.deep.equal([announcement]) + }) + }) + + describe('getAnnouncementById', () => { + it('should throw error when given id is invalid', async () => { + db.announcement.findFirstOrThrow.rejects(PrismaErrorInstance) + await expect(service.getAnnouncementById(id)).to.be.rejectedWith( + EntityNotExistException + ) + }) + it('should return an announcement', async () => { + db.announcement.findFirstOrThrow.resolves(announcement) + const res = await service.getAnnouncementById(id) + expect(res).to.deep.equal(announcement) + }) + }) + + describe('updateAnnouncement', () => { + it('should return updated announcement', async () => { + db.announcement.update.resolves(announcement) + const res = await service.updateAnnouncement( + id, + announcementInput.content + ) + expect(res).to.deep.equal(announcement) + }) + it('should throw error when given id is invalid', async () => { + db.announcement.update.rejects(PrismaErrorInstance) + await expect( + service.updateAnnouncement(id, announcementInput.content) + ).to.be.rejectedWith(EntityNotExistException) + }) + }) + + describe('removeAnnouncement', () => { + it('should return deleted announcement', async () => { + db.announcement.delete.resolves(announcement) + const res = await service.removeAnnouncement(id) + expect(res).to.deep.equal(announcement) + }) + it('should throw error when given id is invalid', async () => { + db.announcement.delete.rejects(PrismaErrorInstance) + await expect(service.removeAnnouncement(id)).to.be.rejectedWith( + EntityNotExistException + ) + }) + }) +}) diff --git a/apps/backend/apps/admin/src/announcement/announcement.service.ts b/apps/backend/apps/admin/src/announcement/announcement.service.ts index bf014c9fab..a473509c96 100644 --- a/apps/backend/apps/admin/src/announcement/announcement.service.ts +++ b/apps/backend/apps/admin/src/announcement/announcement.service.ts @@ -1,26 +1,102 @@ -import { Injectable } from '@nestjs/common' -import type { CreateAnnouncementInput } from './dto/create-announcement.input' -import type { UpdateAnnouncementInput } from './dto/update-announcement.input' +import { Injectable, Logger } from '@nestjs/common' +import { PrismaClientKnownRequestError } from '@prisma/client/runtime/library' +import { + EntityNotExistException, + DuplicateFoundException +} from '@libs/exception' +import { PrismaService } from '@libs/prisma' +import type { AnnouncementInput } from './dto/announcement.input' @Injectable() export class AnnouncementService { - create(createAnnouncementInput: CreateAnnouncementInput) { - return createAnnouncementInput + private readonly logger = new Logger(AnnouncementService.name) + + constructor(private readonly prisma: PrismaService) {} + + async createAnnouncement(announcementInput: AnnouncementInput) { + const announcement = await this.prisma.announcement.findFirst({ + where: { + ...(announcementInput.problemId && { + problemId: announcementInput.problemId + }), + contestId: announcementInput.contestId, + content: announcementInput.content + } + }) + if (announcement) throw new DuplicateFoundException('announcement') + + await this.prisma.contestProblem + .findFirstOrThrow({ + where: { + contestId: announcementInput.contestId, + ...(announcementInput.problemId && { + problemId: announcementInput.problemId + }) + } + }) + .catch((error) => { + if (error.name == 'NotFoundError') { + throw new EntityNotExistException('contestProblem') + } + }) + + return await this.prisma.announcement.create({ + data: { + ...(announcementInput.problemId && { + problemId: announcementInput.problemId + }), + contestId: announcementInput.contestId, + content: announcementInput.content + } + }) } - findAll() { - return `This action returns all announcement` + async getAnnouncements(contestId: number, problemId?: number) { + return await this.prisma.announcement.findMany({ + where: { + ...(problemId && { problemId }), + contestId + } + }) } - findOne(id: number) { - return `This action returns a #${id} announcement` + async getAnnouncementById(id: number) { + const announcement = await this.prisma.announcement + .findFirstOrThrow({ + where: { + id + } + }) + .catch((error) => { + if (error.name == 'NotFoundError') { + throw new EntityNotExistException('announcement') + } + }) + return announcement } - update(id: number, updateAnnouncementInput: UpdateAnnouncementInput) { - return updateAnnouncementInput + async updateAnnouncement(id: number, content: string) { + try { + return await this.prisma.announcement.update({ + where: { id }, + data: { content } + }) + } catch (error) { + if (error instanceof PrismaClientKnownRequestError) { + throw new EntityNotExistException('announcement') + } + } } - remove(id: number) { - return `This action removes a #${id} announcement` + async removeAnnouncement(id: number) { + try { + return await this.prisma.announcement.delete({ + where: { id } + }) + } catch (error) { + if (error instanceof PrismaClientKnownRequestError) { + throw new EntityNotExistException('announcement') + } + } } } diff --git a/apps/backend/apps/admin/src/announcement/dto/announcement.input.ts b/apps/backend/apps/admin/src/announcement/dto/announcement.input.ts new file mode 100644 index 0000000000..9ed50c9ff3 --- /dev/null +++ b/apps/backend/apps/admin/src/announcement/dto/announcement.input.ts @@ -0,0 +1,13 @@ +import { InputType, Int, Field } from '@nestjs/graphql' + +@InputType() +export class AnnouncementInput { + @Field(() => Int, { nullable: true }) + problemId?: number + + @Field(() => Int) + contestId: number + + @Field(() => String) + content: string +} diff --git a/apps/backend/apps/admin/src/announcement/dto/create-announcement.input.ts b/apps/backend/apps/admin/src/announcement/dto/create-announcement.input.ts deleted file mode 100644 index 5d59f6ca53..0000000000 --- a/apps/backend/apps/admin/src/announcement/dto/create-announcement.input.ts +++ /dev/null @@ -1,7 +0,0 @@ -import { InputType, Int, Field } from '@nestjs/graphql' - -@InputType() -export class CreateAnnouncementInput { - @Field(() => Int, { description: 'Example field (placeholder)' }) - exampleField: number -} diff --git a/apps/backend/apps/admin/src/announcement/dto/update-announcement.input.ts b/apps/backend/apps/admin/src/announcement/dto/update-announcement.input.ts deleted file mode 100644 index 748bdb4acb..0000000000 --- a/apps/backend/apps/admin/src/announcement/dto/update-announcement.input.ts +++ /dev/null @@ -1,10 +0,0 @@ -import { InputType, Field, Int, PartialType } from '@nestjs/graphql' -import { CreateAnnouncementInput } from './create-announcement.input' - -@InputType() -export class UpdateAnnouncementInput extends PartialType( - CreateAnnouncementInput -) { - @Field(() => Int) - id: number -} diff --git a/apps/backend/apps/admin/src/contest/contest.resolver.ts b/apps/backend/apps/admin/src/contest/contest.resolver.ts index d080adc0d3..eb723cf60f 100644 --- a/apps/backend/apps/admin/src/contest/contest.resolver.ts +++ b/apps/backend/apps/admin/src/contest/contest.resolver.ts @@ -5,7 +5,6 @@ import { ParseBoolPipe } from '@nestjs/common' import { Args, Context, Int, Mutation, Query, Resolver } from '@nestjs/graphql' -import { Contest, ContestProblem } from '@generated' import { PrismaClientKnownRequestError } from '@prisma/client/runtime/library' import { AuthenticatedRequest, UseRolesGuard } from '@libs/auth' import { OPEN_SPACE_ID } from '@libs/constants' @@ -15,6 +14,8 @@ import { UnprocessableDataException } from '@libs/exception' import { CursorValidationPipe, GroupIDPipe, RequiredIntPipe } from '@libs/pipe' +import { ContestProblem } from '@admin/@generated' +import { Contest } from '@admin/@generated' import { ContestService } from './contest.service' import { ContestWithParticipants } from './model/contest-with-participants.model' import { CreateContestInput } from './model/contest.input' diff --git a/apps/backend/apps/admin/src/contest/contest.service.spec.ts b/apps/backend/apps/admin/src/contest/contest.service.spec.ts index b11999cb8f..7cffc05626 100644 --- a/apps/backend/apps/admin/src/contest/contest.service.spec.ts +++ b/apps/backend/apps/admin/src/contest/contest.service.spec.ts @@ -1,8 +1,5 @@ import { CACHE_MANAGER } from '@nestjs/cache-manager' import { Test, type TestingModule } from '@nestjs/testing' -import { ContestProblem, Group } from '@generated' -import { Problem } from '@generated' -import { Contest } from '@generated' import { faker } from '@faker-js/faker' import { Prisma } from '@prisma/client' import type { Cache } from 'cache-manager' @@ -10,6 +7,9 @@ import { expect } from 'chai' import { stub } from 'sinon' import { EntityNotExistException } from '@libs/exception' import { PrismaService } from '@libs/prisma' +import { ContestProblem, Group } from '@admin/@generated' +import { Problem } from '@admin/@generated' +import { Contest } from '@admin/@generated' import { ContestService } from './contest.service' import type { ContestWithParticipants } from './model/contest-with-participants.model' import type { diff --git a/apps/backend/apps/admin/src/contest/contest.service.ts b/apps/backend/apps/admin/src/contest/contest.service.ts index 6939bd9341..8b90f9666b 100644 --- a/apps/backend/apps/admin/src/contest/contest.service.ts +++ b/apps/backend/apps/admin/src/contest/contest.service.ts @@ -4,7 +4,6 @@ import { Injectable, UnprocessableEntityException } from '@nestjs/common' -import type { Contest } from '@generated' import type { ContestProblem } from '@prisma/client' import { Cache } from 'cache-manager' import { @@ -18,6 +17,7 @@ import { UnprocessableDataException } from '@libs/exception' import { PrismaService } from '@libs/prisma' +import type { Contest } from '@admin/@generated' import type { CreateContestInput } from './model/contest.input' import type { UpdateContestInput } from './model/contest.input' import type { PublicizingRequest } from './model/publicizing-request.model' diff --git a/apps/backend/apps/admin/src/group/group.resolver.ts b/apps/backend/apps/admin/src/group/group.resolver.ts index 31eeb01d74..32d7849be7 100644 --- a/apps/backend/apps/admin/src/group/group.resolver.ts +++ b/apps/backend/apps/admin/src/group/group.resolver.ts @@ -1,6 +1,5 @@ import { InternalServerErrorException, Logger } from '@nestjs/common' import { Args, Int, Query, Mutation, Resolver, Context } from '@nestjs/graphql' -import { Group } from '@generated' import { Role } from '@prisma/client' import { AuthenticatedRequest, UseRolesGuard } from '@libs/auth' import { @@ -9,6 +8,7 @@ import { ForbiddenAccessException } from '@libs/exception' import { CursorValidationPipe, GroupIDPipe } from '@libs/pipe' +import { Group } from '@admin/@generated' import { GroupService } from './group.service' import { CreateGroupInput, UpdateGroupInput } from './model/group.input' import { DeletedUserGroup, FindGroup } from './model/group.output' diff --git a/apps/backend/apps/admin/src/group/group.service.spec.ts b/apps/backend/apps/admin/src/group/group.service.spec.ts index 35e35d9cb8..8af848f534 100644 --- a/apps/backend/apps/admin/src/group/group.service.spec.ts +++ b/apps/backend/apps/admin/src/group/group.service.spec.ts @@ -1,7 +1,5 @@ import { CACHE_MANAGER } from '@nestjs/cache-manager' import { Test, type TestingModule } from '@nestjs/testing' -import type { Group } from '@generated' -import type { User } from '@generated' import { faker } from '@faker-js/faker' import { Role } from '@prisma/client' import type { Cache } from 'cache-manager' @@ -15,6 +13,8 @@ import { ForbiddenAccessException } from '@libs/exception' import { PrismaService } from '@libs/prisma' +import type { Group } from '@admin/@generated' +import type { User } from '@admin/@generated' import { GroupService } from './group.service' const userId = faker.number.int() diff --git a/apps/backend/apps/admin/src/group/model/group.input.ts b/apps/backend/apps/admin/src/group/model/group.input.ts index d3cda6a923..a1783c1795 100644 --- a/apps/backend/apps/admin/src/group/model/group.input.ts +++ b/apps/backend/apps/admin/src/group/model/group.input.ts @@ -1,6 +1,6 @@ import { Field } from '@nestjs/graphql' import { InputType } from '@nestjs/graphql' -import { GroupCreateInput, GroupUpdateInput } from '@generated' +import { GroupCreateInput, GroupUpdateInput } from '@admin/@generated' @InputType() class Config { diff --git a/apps/backend/apps/admin/src/group/model/group.output.ts b/apps/backend/apps/admin/src/group/model/group.output.ts index 7016da3465..c539c5cfe4 100644 --- a/apps/backend/apps/admin/src/group/model/group.output.ts +++ b/apps/backend/apps/admin/src/group/model/group.output.ts @@ -1,6 +1,6 @@ import { Field, Int } from '@nestjs/graphql' import { ObjectType } from '@nestjs/graphql' -import { Group } from '@generated' +import { Group } from '@admin/@generated' @ObjectType() export class FindGroup extends Group { diff --git a/apps/backend/apps/admin/src/notice/model/notice.input.ts b/apps/backend/apps/admin/src/notice/model/notice.input.ts index d80cf33f82..f0333401f3 100644 --- a/apps/backend/apps/admin/src/notice/model/notice.input.ts +++ b/apps/backend/apps/admin/src/notice/model/notice.input.ts @@ -1,5 +1,5 @@ import { InputType, PartialType, PickType } from '@nestjs/graphql' -import { NoticeCreateInput } from '@generated' +import { NoticeCreateInput } from '@admin/@generated' @InputType() export class CreateNoticeInput extends PickType(NoticeCreateInput, [ diff --git a/apps/backend/apps/admin/src/notice/notice.resolver.ts b/apps/backend/apps/admin/src/notice/notice.resolver.ts index 49be1822fa..99a9aee6df 100644 --- a/apps/backend/apps/admin/src/notice/notice.resolver.ts +++ b/apps/backend/apps/admin/src/notice/notice.resolver.ts @@ -13,10 +13,10 @@ import { ResolveField, Parent } from '@nestjs/graphql' -import { Group, Notice, User } from '@generated' import { AuthenticatedRequest } from '@libs/auth' import { EntityNotExistException } from '@libs/exception' import { CursorValidationPipe, GroupIDPipe, IDValidationPipe } from '@libs/pipe' +import { Group, Notice, User } from '@admin/@generated' import { GroupService } from '@admin/group/group.service' import { UserService } from '@admin/user/user.service' import { CreateNoticeInput, UpdateNoticeInput } from './model/notice.input' diff --git a/apps/backend/apps/admin/src/notice/notice.service.spec.ts b/apps/backend/apps/admin/src/notice/notice.service.spec.ts index e9b689adbf..047b9a7fb7 100644 --- a/apps/backend/apps/admin/src/notice/notice.service.spec.ts +++ b/apps/backend/apps/admin/src/notice/notice.service.spec.ts @@ -1,11 +1,11 @@ import { Test, type TestingModule } from '@nestjs/testing' -import type { Notice } from '@generated' import { faker } from '@faker-js/faker' import { PrismaClientKnownRequestError } from '@prisma/client/runtime/library' import { expect } from 'chai' import { stub } from 'sinon' import { EntityNotExistException } from '@libs/exception' import { PrismaService } from '@libs/prisma' +import type { Notice } from '@admin/@generated' import type { CreateNoticeInput, UpdateNoticeInput } from './model/notice.input' import { NoticeService } from './notice.service' diff --git a/apps/backend/apps/admin/src/problem/model/problem.input.ts b/apps/backend/apps/admin/src/problem/model/problem.input.ts index 82a7369d98..97067444d3 100644 --- a/apps/backend/apps/admin/src/problem/model/problem.input.ts +++ b/apps/backend/apps/admin/src/problem/model/problem.input.ts @@ -1,7 +1,7 @@ import { Field, InputType, Int } from '@nestjs/graphql' -import { Language, Level } from '@generated' import { ValidatePromise } from 'class-validator' import { GraphQLUpload } from 'graphql-upload' +import { Language, Level } from '@admin/@generated' import type { FileUploadDto } from '../dto/file-upload.dto' import { Template } from './template.input' import { Sample, Testcase } from './testcase.input' diff --git a/apps/backend/apps/admin/src/problem/model/template.input.ts b/apps/backend/apps/admin/src/problem/model/template.input.ts index e19a5ede98..c20b976eff 100644 --- a/apps/backend/apps/admin/src/problem/model/template.input.ts +++ b/apps/backend/apps/admin/src/problem/model/template.input.ts @@ -1,5 +1,5 @@ import { Field, InputType, Int } from '@nestjs/graphql' -import { Language } from '@generated' +import { Language } from '@admin/@generated' @InputType() class Snippet { diff --git a/apps/backend/apps/admin/src/user/user.resolver.ts b/apps/backend/apps/admin/src/user/user.resolver.ts index 8b3ff23a79..b201d4422f 100644 --- a/apps/backend/apps/admin/src/user/user.resolver.ts +++ b/apps/backend/apps/admin/src/user/user.resolver.ts @@ -6,10 +6,10 @@ import { NotFoundException } from '@nestjs/common' import { Resolver, Query, Mutation, Args, Int } from '@nestjs/graphql' -import { UserGroup } from '@generated' -import { User } from '@generated' import { OPEN_SPACE_ID } from '@libs/constants' import { CursorValidationPipe, GroupIDPipe, RequiredIntPipe } from '@libs/pipe' +import { UserGroup } from '@admin/@generated' +import { User } from '@admin/@generated' import { GroupMember } from './model/groupMember.model' import { UserService } from './user.service' diff --git a/apps/backend/apps/admin/src/user/user.service.ts b/apps/backend/apps/admin/src/user/user.service.ts index 173e0363c2..0ddc65ba7f 100644 --- a/apps/backend/apps/admin/src/user/user.service.ts +++ b/apps/backend/apps/admin/src/user/user.service.ts @@ -6,7 +6,6 @@ import { Injectable, NotFoundException } from '@nestjs/common' -import type { UserGroup } from '@generated' import { Role } from '@prisma/client' import { Cache } from 'cache-manager' import { joinGroupCacheKey } from '@libs/cache' @@ -14,6 +13,7 @@ import { JOIN_GROUP_REQUEST_EXPIRE_TIME } from '@libs/constants' import { EntityNotExistException } from '@libs/exception' import { PrismaService } from '@libs/prisma' import type { GroupJoinRequest } from '@libs/types' +import type { UserGroup } from '@admin/@generated' @Injectable() export class UserService { diff --git a/apps/backend/tsconfig.json b/apps/backend/tsconfig.json index f54c34e134..38d0ce4995 100644 --- a/apps/backend/tsconfig.json +++ b/apps/backend/tsconfig.json @@ -20,7 +20,7 @@ "paths": { "@admin/*": ["./apps/admin/src/*"], "@client/*": ["./apps/client/src/*"], - "@generated": ["./apps/admin/src/@generated"], + "@generated": ["apps/admin/src/@generated"], "@libs/prisma": ["./libs/prisma/src/index.ts"], "@libs/cache": ["./libs/cache/src/index.ts"], "@libs/auth": ["./libs/auth/src/index.ts"], diff --git a/collection/admin/Announcement/Create Announcement/Error: Duplicated Announcement.bru b/collection/admin/Announcement/Create Announcement/Error: Duplicated Announcement.bru new file mode 100644 index 0000000000..9b917e6463 --- /dev/null +++ b/collection/admin/Announcement/Create Announcement/Error: Duplicated Announcement.bru @@ -0,0 +1,38 @@ +meta { + name: Error: Duplicated Announcement + type: graphql + seq: 2 +} + +post { + url: {{gqlUrl}} + body: graphql + auth: none +} + +body:graphql { + mutation($problemId: Int, $content: String!) { + createAnnouncement( + createAnnouncementInput: { + problemId: $problemId + content: $content + } + ) { + id + problemId + content + } + } +} + +body:graphql:vars { + { + "problemId": 1, + "contestId": 1, + "content": "this is new announcement" + } +} + +assert { + res.body.errors: isDefined +} diff --git a/collection/admin/Announcement/Create Announcement/Error: Invalid Problem ID.bru b/collection/admin/Announcement/Create Announcement/Error: Invalid Problem ID.bru new file mode 100644 index 0000000000..f30162a931 --- /dev/null +++ b/collection/admin/Announcement/Create Announcement/Error: Invalid Problem ID.bru @@ -0,0 +1,41 @@ +meta { + name: Error: Invalid Problem ID + type: graphql + seq: 3 +} + +post { + url: {{gqlUrl}} + body: graphql + auth: none +} + +body:graphql { + mutation($problemId: Int, $contestId: Int!, $content: String!) { + createAnnouncement( + createAnnouncementInput: { + problemId: $problemId + contestId: $contestId + content: $content + } + ) { + id + problemId + content + } + } + + +} + +body:graphql:vars { + { + "problemId": -9999999, + "contestId": -9999999, + "content": "this is new announcement" + } +} + +assert { + res.body.errors: isDefined +} diff --git a/collection/admin/Announcement/Create Announcement/Succeed.bru b/collection/admin/Announcement/Create Announcement/Succeed.bru new file mode 100644 index 0000000000..fcb54fa064 --- /dev/null +++ b/collection/admin/Announcement/Create Announcement/Succeed.bru @@ -0,0 +1,59 @@ +meta { + name: Succeed + type: graphql + seq: 1 +} + +post { + url: {{gqlUrl}} + body: graphql + auth: none +} + +body:graphql { + mutation($problemId: Int, $contestId: Int!, $content: String!) { + createAnnouncement( + createAnnouncementInput: { + problemId: $problemId + contestId: $contestId + content: $content + } + ) { + id + problemId + content + } + } + + +} + +body:graphql:vars { + { + "problemId": 1, + "contestId": 1, + "content": "this is new announcement" + } +} + +assert { + res.body.data.createAnnouncement: isDefined +} + +docs { + ## Create Announcement + 새로운 Announcement를 생성합니다. + + ### Args + | 이름 | 타입 | 설명| + |--|--|--| + |problemId|int(optional)|Announcement를 추가할 문제의 id| + |contestId|int|Announcement를 추가할 contest의 id| + |content|string|추가할 Announcement의 내용| + + ### Error Case + #### CONFLICT + 추가하려는 것과 problemId, content가 모두 일치하는 Announcement가 이미 존재 + #### NOT_FOUND + 존재하지 않는 problemId +} diff --git a/collection/admin/Announcement/Delete Announcement/Error: Invalid Announcement ID.bru b/collection/admin/Announcement/Delete Announcement/Error: Invalid Announcement ID.bru new file mode 100644 index 0000000000..e962e89ef8 --- /dev/null +++ b/collection/admin/Announcement/Delete Announcement/Error: Invalid Announcement ID.bru @@ -0,0 +1,33 @@ +meta { + name: Error: Invalid Announcement ID + type: graphql + seq: 2 +} + +post { + url: {{gqlUrl}} + body: graphql + auth: none +} + +body:graphql { + mutation($id: Int!) { + removeAnnouncement( + id: $id + ) { + id + problemId + content + } + } +} + +body:graphql:vars { + { + "id": -999999 + } +} + +assert { + res.body.errors: isDefined +} diff --git a/collection/admin/Announcement/Delete Announcement/Succeed.bru b/collection/admin/Announcement/Delete Announcement/Succeed.bru new file mode 100644 index 0000000000..c15ebb727b --- /dev/null +++ b/collection/admin/Announcement/Delete Announcement/Succeed.bru @@ -0,0 +1,47 @@ +meta { + name: Succeed + type: graphql + seq: 1 +} + +post { + url: {{gqlUrl}} + body: graphql + auth: none +} + +body:graphql { + mutation($id: Int!) { + removeAnnouncement( + id: $id + ) { + id + problemId + content + } + } +} + +body:graphql:vars { + { + "id": 1 + } +} + +assert { + res.body.data.removeAnnouncement: isDefined +} + +docs { + ## Delete Announcement + Announcement를 제거합니다. + + ### Args + | 이름 | 타입 | 설명| + |--|--|--| + |id|int|제거할 Announcement id| + + ### Error Case + #### NOT_FOUND + 존재하지 않는 id +} diff --git a/collection/admin/Announcement/Get Announcement/Error: Invalid Announcement ID.bru b/collection/admin/Announcement/Get Announcement/Error: Invalid Announcement ID.bru new file mode 100644 index 0000000000..62252b00bc --- /dev/null +++ b/collection/admin/Announcement/Get Announcement/Error: Invalid Announcement ID.bru @@ -0,0 +1,35 @@ +meta { + name: Error: Invalid Announcement ID + type: graphql + seq: 2 +} + +post { + url: {{gqlUrl}} + body: graphql + auth: none +} + +body:graphql { + query($id: Int!) { + getAnnouncementById( + id: $id + ) { + id + problemId + contestId + content + } + } + +} + +body:graphql:vars { + { + "id": -999999 + } +} + +assert { + res.body.errors: isDefined +} diff --git a/collection/admin/Announcement/Get Announcement/Succeed.bru b/collection/admin/Announcement/Get Announcement/Succeed.bru new file mode 100644 index 0000000000..48b87b80e5 --- /dev/null +++ b/collection/admin/Announcement/Get Announcement/Succeed.bru @@ -0,0 +1,49 @@ +meta { + name: Succeed + type: graphql + seq: 1 +} + +post { + url: {{gqlUrl}} + body: graphql + auth: none +} + +body:graphql { + query($id: Int!) { + getAnnouncementById( + id: $id + ) { + id + problemId + contestId + content + } + } + +} + +body:graphql:vars { + { + "id": 1 + } +} + +assert { + res.body.data.getAnnouncementById: isDefined +} + +docs { + ## Get Announcement + Announcement의 정보를 조회합니다. + + ### Args + | 이름 | 타입 | 설명| + |--|--|--| + |id|int|조회할 Announcement의 id| + + ### Error Case + #### NOT_FOUND + 존재하지 않는 id +} diff --git a/collection/admin/Announcement/Get Announcements/Succeed.bru b/collection/admin/Announcement/Get Announcements/Succeed.bru new file mode 100644 index 0000000000..bbedaac785 --- /dev/null +++ b/collection/admin/Announcement/Get Announcements/Succeed.bru @@ -0,0 +1,49 @@ +meta { + name: Succeed + type: graphql + seq: 1 +} + +post { + url: {{gqlUrl}} + body: graphql + auth: none +} + +body:graphql { + query($contestId: Int!, $problemId: Int) { + getAnnouncements( + contestId: $contestId + problemId: $problemId + ) { + id + problemId + contestId + content + } + } + + +} + +body:graphql:vars { + { + "problemId": 1, + "contestId": 1 + } +} + +assert { + res.body.data.getAnnouncements: isDefined +} + +docs { + ## Get Announcements + 해당 문제에 등록된 모든 Announcement를 조회합니다. + + ### Args + | 이름 | 타입 | 설명| + |--|--|--| + |contestId|int|Announcement를 조회할 contest의 id| + |problemId|int(optional)|Announcement를 조회할 문제의 id| +} diff --git a/collection/admin/Announcement/Update Announcement/Error: Invalid Announcement ID.bru b/collection/admin/Announcement/Update Announcement/Error: Invalid Announcement ID.bru new file mode 100644 index 0000000000..7dc21dccdb --- /dev/null +++ b/collection/admin/Announcement/Update Announcement/Error: Invalid Announcement ID.bru @@ -0,0 +1,36 @@ +meta { + name: Error: Invalid Announcement ID + type: graphql + seq: 2 +} + +post { + url: {{gqlUrl}} + body: graphql + auth: none +} + +body:graphql { + mutation($id: Int!, $content: String!) { + updateAnnouncement( + id: $id + content: $content + ) { + id + problemId + content + } + } + +} + +body:graphql:vars { + { + "id": -99999, + "content": "updated announcement" + } +} + +assert { + res.body.errors: isDefined +} diff --git a/collection/admin/Announcement/Update Announcement/Succeed.bru b/collection/admin/Announcement/Update Announcement/Succeed.bru new file mode 100644 index 0000000000..148011ea34 --- /dev/null +++ b/collection/admin/Announcement/Update Announcement/Succeed.bru @@ -0,0 +1,48 @@ +meta { + name: Succeed + type: graphql + seq: 1 +} + +post { + url: {{gqlUrl}} + body: graphql + auth: none +} + +body:graphql { + mutation($id: Int!, $content: String!) { + updateAnnouncement( + id: $id + content: $content + ) { + id + problemId + content + } + } + + +} + +body:graphql:vars { + { + "id": 1, + "content": "updated announcement" + } +} + +assert { + res.body.data.updateAnnouncement: isDefined +} + +docs { + ## Update Announcment + Announcement의 내용을 업데이트합니다. + + ### Args + | 이름 | 타입 | 설명| + |--|--|--| + |Id|int|Announcement의 id| + |content|string|수정할 Announcement의 내용| +}