diff --git a/apps/backend/apps/admin/src/polygon/collaborator/collaborator.resolver.ts b/apps/backend/apps/admin/src/polygon/collaborator/collaborator.resolver.ts new file mode 100644 index 0000000000..8c1e02466a --- /dev/null +++ b/apps/backend/apps/admin/src/polygon/collaborator/collaborator.resolver.ts @@ -0,0 +1,116 @@ +import { Args, Context, Int, Mutation, Query, Resolver } from '@nestjs/graphql' +import { CollaboratorStatus, CollaboratorRole } from '@prisma/client' +import { AuthenticatedRequest } from '@libs/auth' +import { IDValidationPipe } from '@libs/pipe' +import { CollaboratorService } from './collaborator.service' +import { + CollaboratorInput, + CollaboratorUpdateInput +} from './model/collaborator.input' + +@Resolver() +export class CollaboratorResolver { + constructor(private readonly collaboratorService: CollaboratorService) {} + + @Mutation() + async inviteCollaborator( + @Context('req') req: AuthenticatedRequest, + @Args('polygonId', { type: () => Int }, IDValidationPipe) polygonId: number, + @Args('input') input: CollaboratorInput + ) { + return await this.collaboratorService.inviteCollaborator( + req.user.id, + polygonId, + input + ) + } + + @Query() + async getActiveCollaborator( + @Context('req') req: AuthenticatedRequest, + @Args('polygonId', { type: () => Int }, IDValidationPipe) polygonId: number + ) { + return await this.collaboratorService.getCollaboratorsByStatus( + req.user.id, + polygonId, + CollaboratorStatus.Active + ) + } + + @Query() + async getPendingCollaborator( + @Context('req') req: AuthenticatedRequest, + @Args('polygonId', { type: () => Int }, IDValidationPipe) polygonId: number + ) { + return await this.collaboratorService.getCollaboratorsByStatus( + req.user.id, + polygonId, + CollaboratorStatus.Pending + ) + } + + @Mutation() + async approveInvite( + @Context('req') req: AuthenticatedRequest, + @Args('polygonId', { type: () => Int }, IDValidationPipe) polygonId: number, + @Args('userId', { type: () => Int }, IDValidationPipe) userId: number + ) { + return await this.collaboratorService.approveCollaborator( + req.user.id, + polygonId, + userId + ) + } + + @Mutation() + async rejectInvite( + @Context('req') req: AuthenticatedRequest, + @Args('polygonId', { type: () => Int }, IDValidationPipe) polygonId: number, + @Args('userId', { type: () => Int }, IDValidationPipe) userId: number + ) { + return await this.collaboratorService.rejectCollaborator( + req.user.id, + polygonId, + userId + ) + } + + @Mutation() + async updateCollaboratorRole( + @Context('req') req: AuthenticatedRequest, + @Args('polygonId', { type: () => Int }, IDValidationPipe) polygonId: number, + @Args('input') input: CollaboratorUpdateInput + ) { + return await this.collaboratorService.updateCollaboratorRole( + req.user.id, + polygonId, + input + ) + } + + @Mutation() + async removeCollaborator( + @Context('req') req: AuthenticatedRequest, + @Args('polygonId', { type: () => Int }, IDValidationPipe) polygonId: number, + @Args('userId', { type: () => Int }, IDValidationPipe) userId: number + ) { + return await this.collaboratorService.removeCollaborator( + req.user.id, + polygonId, + userId + ) + } + + @Mutation() + async requestCollaboration( + @Context('req') req: AuthenticatedRequest, + @Args('polygonId', { type: () => Int }, IDValidationPipe) polygonId: number, + @Args('role') role: CollaboratorRole + ) { + return await this.collaboratorService.requestCollaboration( + req.user.id, + polygonId, + role + ) + } +} diff --git a/apps/backend/apps/admin/src/polygon/collaborator/collaborator.service.spec.ts b/apps/backend/apps/admin/src/polygon/collaborator/collaborator.service.spec.ts new file mode 100644 index 0000000000..09ad2acce5 --- /dev/null +++ b/apps/backend/apps/admin/src/polygon/collaborator/collaborator.service.spec.ts @@ -0,0 +1,266 @@ +import { Test, type TestingModule } from '@nestjs/testing' +import { CollaboratorRole, CollaboratorStatus } from '@generated' +import { expect } from 'chai' +import { stub } from 'sinon' +import { ForbiddenAccessException } from '@libs/exception' +import { PrismaService } from '@libs/prisma' +import { CollaboratorService } from './collaborator.service' + +const exampleOwner = { + id: 1, + username: 'owner', + email: 'owner@test.com' +} + +const exampleUser = { + id: 2, + username: 'invitee', + email: 'invitee@test.com' +} + +const exampleEditorUser = { + id: 3, + username: 'editor', + email: 'editor@test.com' +} + +const exampleViewerUser = { + id: 4, + username: 'viewer', + email: 'viewer@test.com' +} + +const examplePendingUser = { + id: 5, + username: 'pending', + email: 'pending@test.com' +} + +const exampleViewerCollaborator = { + id: 1, + problemId: 10, + userId: exampleViewerUser.id, + role: CollaboratorRole.Reviewer, + status: CollaboratorStatus.Active +} + +const exampleEditorCollaborator = { + id: 2, + problemId: 10, + userId: exampleEditorUser.id, + role: CollaboratorRole.Editor, + status: CollaboratorStatus.Active +} + +const examplePendingCollaborator = { + id: 3, + problemId: 10, + userId: examplePendingUser.id, + role: CollaboratorRole.Reviewer, + status: CollaboratorStatus.Pending +} + +const exampleCollaboratorList = [ + exampleViewerCollaborator, + exampleEditorCollaborator, + examplePendingCollaborator +] + +const exampleProblem = { + id: 10, + createdById: exampleOwner.id, + polygonCollaborators: exampleCollaboratorList +} + +const exampleCollaboratorListByStatus = [ + { + role: exampleViewerCollaborator.role, + user: { + id: exampleViewerUser.id, + username: exampleViewerUser.username, + email: exampleViewerUser.email + } + }, + { + role: exampleEditorCollaborator.role, + user: { + id: exampleEditorUser.id, + username: exampleEditorUser.username, + email: exampleEditorUser.email + } + } +] + +const db = { + user: { + findUnique: stub() + }, + polygonProblem: { + findUnique: stub() + }, + polygonCollaborator: { + findFirst: stub(), + findMany: stub(), + create: stub(), + update: stub(), + delete: stub() + } +} + +describe('CollaboratorService', () => { + let service: CollaboratorService + + beforeEach(async () => { + db.user.findUnique.reset() + db.polygonProblem.findUnique.reset() + db.polygonCollaborator.findFirst.reset() + db.polygonCollaborator.findMany.reset() + db.polygonCollaborator.create.reset() + db.polygonCollaborator.update.reset() + db.polygonCollaborator.delete.reset() + const module: TestingModule = await Test.createTestingModule({ + providers: [CollaboratorService, { provide: PrismaService, useValue: db }] + }).compile() + + service = module.get(CollaboratorService) + }) + + it('should be defined', () => { + expect(service).to.be.ok + }) + + describe('inviteCollaborator', () => { + it('owner can invite collaborator', async () => { + db.user.findUnique.resolves({ id: exampleUser.id }) + db.polygonProblem.findUnique.resolves({ + createdById: exampleProblem.createdById + }) + + db.polygonCollaborator.findFirst.resolves(null) + db.polygonCollaborator.create.resolves(exampleViewerCollaborator) + + const result = await service.inviteCollaborator( + exampleOwner.id, + exampleProblem.id, + { + userEmail: exampleUser.email, + role: CollaboratorRole.Reviewer + } + ) + expect(result).to.deep.equal(exampleViewerCollaborator) + }) + + it('Viewer cannot invite collaborator', async () => { + db.user.findUnique.resolves({ id: exampleUser.id }) + db.polygonProblem.findUnique.resolves({ + createdById: exampleProblem.createdById + }) + db.polygonCollaborator.findFirst.resolves(exampleViewerCollaborator) + + await expect( + service.inviteCollaborator(exampleViewerUser.id, exampleProblem.id, { + userEmail: exampleUser.email, + role: CollaboratorRole.Reviewer + }) + ).to.be.rejectedWith(ForbiddenAccessException) + }) + }) + + describe('getCollaboratorsByStatus', () => { + it('return active collaborators', async () => { + db.polygonProblem.findUnique.resolves({ + createdById: exampleProblem.createdById + }) + db.polygonCollaborator.findMany.resolves(exampleCollaboratorListByStatus) + + const result = await service.getCollaboratorsByStatus( + exampleOwner.id, + exampleProblem.id, + CollaboratorStatus.Active + ) + + expect(result).to.deep.equal([ + { + id: exampleViewerUser.id, + username: exampleViewerUser.username, + email: exampleViewerUser.email, + role: exampleViewerCollaborator.role + }, + { + id: exampleEditorUser.id, + username: exampleEditorUser.username, + email: exampleEditorUser.email, + role: exampleEditorCollaborator.role + } + ]) + }) + + it('returns pending collaborators', async () => { + db.polygonProblem.findUnique.resolves({ + createdById: exampleProblem.createdById + }) + db.polygonCollaborator.findMany.resolves([ + { + role: examplePendingCollaborator.role, + user: { + id: examplePendingUser.id, + username: examplePendingUser.username, + email: examplePendingUser.email + } + } + ]) + + const result = await service.getCollaboratorsByStatus( + exampleOwner.id, + exampleProblem.id, + CollaboratorStatus.Pending + ) + + expect(result).to.deep.equal([ + { + id: examplePendingUser.id, + username: examplePendingUser.username, + email: examplePendingUser.email, + role: examplePendingCollaborator.role + } + ]) + }) + }) + describe('updateCollaboratorRole', () => { + it('owner updates collaborator role', async () => { + db.polygonProblem.findUnique.resolves({ + createdById: exampleProblem.createdById + }) + db.polygonCollaborator.findFirst.resolves({ + id: exampleViewerCollaborator.id, + status: exampleViewerCollaborator.status + }) + const updatedCollaborator = { + ...exampleViewerCollaborator, + role: CollaboratorRole.Editor + } + db.polygonCollaborator.update.resolves(updatedCollaborator) + + const result = await service.updateCollaboratorRole( + exampleOwner.id, + exampleProblem.id, + { userId: exampleViewerUser.id, role: CollaboratorRole.Editor } + ) + + expect(result).to.deep.equal(updatedCollaborator) + }) + it('Editor cannot update collaborator role', async () => { + db.polygonProblem.findUnique.resolves({ + createdById: exampleProblem.createdById + }) + + await expect( + service.updateCollaboratorRole( + exampleEditorUser.id, + exampleProblem.id, + { userId: exampleViewerUser.id, role: CollaboratorRole.Editor } + ) + ).to.be.rejectedWith(ForbiddenAccessException) + }) + }) +}) diff --git a/apps/backend/apps/admin/src/polygon/collaborator/collaborator.service.ts b/apps/backend/apps/admin/src/polygon/collaborator/collaborator.service.ts new file mode 100644 index 0000000000..57a6b1f372 --- /dev/null +++ b/apps/backend/apps/admin/src/polygon/collaborator/collaborator.service.ts @@ -0,0 +1,423 @@ +import { Injectable } from '@nestjs/common' +import { CollaboratorRole, CollaboratorStatus, Prisma } from '@prisma/client' +import { + EntityNotExistException, + ForbiddenAccessException, + DuplicateFoundException, + UnprocessableDataException +} from '@libs/exception' +import { PrismaService } from '@libs/prisma' +import { + CollaboratorInput, + CollaboratorUpdateInput +} from './model/collaborator.input' + +@Injectable() +export class CollaboratorService { + constructor(private readonly prisma: PrismaService) {} + + /** + * 해당 polygon 문제에 협업자를 초대합니다. + * + * 협업자 초대는 1. 해당 문제의 소유자 2. status: Active, role:Editor 인 경우에만 가능 + * + * @param {number} inviterId 초대자의 id + * @param {number} polygonId 생성 문제의 id + * @param {CollaboratorInput} model 협업자 id, role + * @returns {polygonCollaborator} 협업자 정보 + * @throws {EntityNotExistException} 아래와 같은 경우 발생합니다. + * -해당 polygonId에 해당하는 문제가 존재하지 않는 경우 + * @throws {ForbiddenAccessException} 아래와 같은 경우 발생합니다. + * -초대자가 문제의 소유자가 아니면서 status: Active, role:Editor가 아닌 경우 + * @throws {DuplicateFoundException} 아래와 같은 경우 발생합니다. + * -이미 초대된 협업자를 초대한 경우 + * -문제 소유자를 초대한 경우 + * @throws {UnprocessableDataException} 아래와 같은 경우 발생합니다. + * - Owner role로 초대을 하는 경우 + */ + async inviteCollaborator( + inviterId: number, + polygonId: number, + input: CollaboratorInput + ) { + const { userEmail, role } = input + + if (role === CollaboratorRole.Owner) { + throw new UnprocessableDataException('Cannot assign Owner role') + } + + const user = await this.prisma.user.findUnique({ + where: { email: userEmail }, + select: { id: true } + }) + if (!user) { + throw new EntityNotExistException('User not found') + } + const userId = user.id + + const problem = await this.prisma.polygonProblem.findUnique({ + where: { id: polygonId }, + select: { createdById: true } + }) + if (!problem) throw new EntityNotExistException('PolygonProblem not found') + + const isOwner = problem.createdById === inviterId + + if (!isOwner) { + const inviterInfo = await this.prisma.polygonCollaborator.findFirst({ + where: { + problemId: polygonId, + userId: inviterId + }, + select: { status: true, role: true } + }) + const canInvite = + inviterInfo?.status === CollaboratorStatus.Active && + inviterInfo?.role === CollaboratorRole.Editor + if (!canInvite) { + throw new ForbiddenAccessException( + 'No permission to invite collaborator' + ) + } + } + + const existing = await this.prisma.polygonCollaborator.findFirst({ + where: { + problemId: polygonId, + userId + }, + select: { id: true } + }) + if (existing) { + throw new DuplicateFoundException('invited existing Collaborator') + } + if (userId === problem.createdById) { + throw new DuplicateFoundException('invited owner to collaborator') + } + const status = isOwner + ? CollaboratorStatus.Active + : CollaboratorStatus.Pending + + try { + return await this.prisma.polygonCollaborator.create({ + data: { + problemId: polygonId, + userId, + role, + status + } + }) + } catch (error) { + if ( + error instanceof Prisma.PrismaClientKnownRequestError && + error.code === 'P2002' + ) { + throw new DuplicateFoundException('Collaborator is already invited') + } + throw error + } + } + + /** + * Status에 따른 협업자 목록을 반환합니다. + * + * status : Pending(수락 대기 중), Active(활성화 됨) + * @param {number} userId 문제 소유자의 id + * @param {number} polygonId 생성 문제의 id + * @param {CollaboratorStatus} status 협업자의 상태 + * @returns {PolygonCollaborator[]} 협업자 목록 + * @throws {EntityNotExistException} 아래와 같은 경우 발생합니다. + * - 해당 polygonId에 해당하는 문제가 존재하지 않는 경우 + * @throws {ForbiddenAccessException} 아래와 같은 경우 발생합니다. + * - 협업자 목록 요청자가 문제 소유자가 아닌 경우 + */ + async getCollaboratorsByStatus( + userId: number, + polygonId: number, + status: CollaboratorStatus + ) { + const problem = await this.prisma.polygonProblem.findUnique({ + where: { id: polygonId }, + select: { createdById: true } + }) + if (!problem) throw new EntityNotExistException('PolygonProblem not found') + + if (problem.createdById !== userId) { + throw new ForbiddenAccessException('No permission to view collaborators') + } + + const collaborators = await this.prisma.polygonCollaborator.findMany({ + where: { + problemId: polygonId, + status + }, + select: { + role: true, + user: { + select: { + username: true, + id: true, + email: true + } + } + } + }) + + return collaborators.map((c) => ({ + username: c.user.username, + id: c.user.id, + email: c.user.email, + role: c.role + })) + } + + /** + * 문제 소유자가 협업 요청을 수락합니다. + * + * @param {number} createdById 문제 소유자의 id + * @param {number} polygonId 생성 문제의 id + * @param {number} userId 협업자의 id + * @returns {polygonCollaborator} 협업자 정보 + * @throws {EntityNotExistException} 아래와 같은 경우 발생합니다. + * -해당 polygonId에 해당하는 문제가 존재하지 않는 경우 + * -해당 userId에 해당하는 협업자가 존재하지 않는 경우 + * @throws {UnprocessableDataException} 아래와 같은 경우 발생합니다. + * -협업자의 status가 Pending이 아닌 경우 + * @throws {ForbiddenAccessException} 아래와 같은 경우 발생합니다. + * -createdById가 해당 문제의 소유자가 아닌 경우 + */ + async approveCollaborator( + createdById: number, + polygonId: number, + userId: number + ) { + const problem = await this.prisma.polygonProblem.findUnique({ + where: { id: polygonId }, + select: { createdById: true } + }) + if (!problem) throw new EntityNotExistException('PolygonProblem not found') + + if (problem.createdById !== createdById) { + throw new ForbiddenAccessException('No permission to approve/reject') + } + const collaborator = await this.prisma.polygonCollaborator.findFirst({ + where: { problemId: polygonId, userId }, + select: { id: true, status: true } + }) + if (!collaborator) { + throw new EntityNotExistException('Collaborator not found') + } + + if (collaborator.status !== CollaboratorStatus.Pending) { + throw new UnprocessableDataException('Invitation is not pending') + } + + return await this.prisma.polygonCollaborator.update({ + where: { id: collaborator.id }, + data: { status: CollaboratorStatus.Active } + }) + } + + /** + * 문제 소유자가 협업 요청을 거절합니다. + * + * @param {number} createdById 문제 소유자의 id + * @param {number} polygonId 생성 문제의 id + * @param {number} userId 협업자의 id + * @returns {polygonCollaborator} 삭제 협업자 정보 + * @throws {EntityNotExistException} 아래와 같은 경우 발생합니다. + * -해당 polygonId에 해당하는 문제가 존재하지 않는 경우 + * -해당 userId에 해당하는 협업자가 존재하지 않는 경우 + * @throws {UnprocessableDataException} 아래와 같은 경우 발생합니다. + * -협업자의 status가 Pending이 아닌 경우 + * @throws {ForbiddenAccessException} 아래와 같은 경우 발생합니다. + * -createdById가 해당 문제의 소유자가 아닌 경우 + */ + async rejectCollaborator( + createdById: number, + polygonId: number, + userId: number + ) { + const problem = await this.prisma.polygonProblem.findUnique({ + where: { id: polygonId }, + select: { createdById: true } + }) + if (!problem) throw new EntityNotExistException('PolygonProblem not found') + + if (problem.createdById !== createdById) { + throw new ForbiddenAccessException('No permission to approve/reject') + } + + const collaborator = await this.prisma.polygonCollaborator.findFirst({ + where: { problemId: polygonId, userId }, + select: { id: true, status: true } + }) + if (!collaborator) + throw new EntityNotExistException('Collaborator not found') + + if (collaborator.status !== CollaboratorStatus.Pending) { + throw new UnprocessableDataException('Invitation is not pending') + } + + return await this.prisma.polygonCollaborator.delete({ + where: { id: collaborator.id } + }) + } + + /** + * 협업자의 role을 변경합니다. + * + * role : Reviewer, Editor로만 변경 + * + * @param {number} inviterId 초대자의 id + * @param {number} polygonId 생성 문제의 id + * @param {CollaboratorInput} input 협업자 id, role + * @returns {polygonCollaborator} 협업자 정보 + * @throws {EntityNotExistException} 아래와 같은 경우 발생합니다. + * -해당 polygonId에 해당하는 문제가 존재하지 않는 경우 + * -해당 userId에 해당하는 협업자가 존재하지 않는 경우 + * @throws {ForbiddenAccessException} 아래와 같은 경우 발생합니다. + * -invitorId가 해당 문제의 소유자가 아닌 경우 + * @throws {UnprocessableDataException} 아래와 같은 경우 발생합니다. + * - Owner role로 변경을 하는 경우 + * - 변경하려는 협업자가 active하지 않는 경우 + */ + async updateCollaboratorRole( + inviterId: number, + polygonId: number, + input: CollaboratorUpdateInput + ) { + const { userId, role } = input + if (role === CollaboratorRole.Owner) { + throw new UnprocessableDataException('Cannot assign Owner role') + } + const problem = await this.prisma.polygonProblem.findUnique({ + where: { id: polygonId }, + select: { createdById: true } + }) + if (!problem) throw new EntityNotExistException('PolygonProblem not found') + + const isOwner = problem.createdById === inviterId + if (!isOwner) + throw new ForbiddenAccessException('No permission to update role') + + const collaborator = await this.prisma.polygonCollaborator.findFirst({ + where: { problemId: polygonId, userId }, + select: { id: true, status: true } + }) + if (!collaborator) { + throw new EntityNotExistException('Collaborator not found') + } + + if (collaborator.status !== CollaboratorStatus.Active) { + throw new UnprocessableDataException('Collaborator is not active') + } + + return await this.prisma.polygonCollaborator.update({ + where: { id: collaborator.id }, + data: { role } + }) + } + + /** + * 협업자를 제거합니다. + * 협업자 제거는 문제 소유자만 가능합니다. + * + * @param {number} createdById 문제 소유자의 id + * @param {number} polygonId 생성 문제의 id + * @param {number} userId 협업자의 id + * @returns {polygonCollaborator} 삭제된 협업자 정보 + * @throws {EntityNotExistException} 아래와 같은 경우 발생합니다. + * -해당 문제 소유자와 polygonId에 해당하는 문제가 존재하지 않는 경우 + * -해당 userId에 대항하는 협업자가 존재하지 않는 경우 + * @throws {ForbiddenAccessException} 아래와 같은 경우 발생합니다. + * -createdById가 해당 문제의 소유자가 아닌 경우 + */ + async removeCollaborator( + createdById: number, + polygonId: number, + userId: number + ) { + const problem = await this.prisma.polygonProblem.findUnique({ + where: { id: polygonId }, + select: { createdById: true } + }) + if (!problem) throw new EntityNotExistException('PolygonProblem not found') + + if (problem.createdById !== createdById) { + throw new ForbiddenAccessException('No permission to remove collaborator') + } + + const collaborator = await this.prisma.polygonCollaborator.findFirst({ + where: { + problemId: polygonId, + userId, + status: CollaboratorStatus.Active + }, + select: { id: true } + }) + if (!collaborator) { + throw new EntityNotExistException('Collaborator not found') + } + return await this.prisma.polygonCollaborator.delete({ + where: { id: collaborator.id } + }) + } + + /** + * 사용자가 협업자가 되기 위해 요청합니다. + * -role: Reviewer, Editor + * + * @param {number} userId 요청자의 id + * @param {number} polygonId 해당 문제의 polygonId + * @param {CollaboratorRole} role 요청하는 역할 + * @returns {polygonCollaborator} 협업자 정보 + * @throws {EntityNotExistException} 아래와 같은 경우 발생합니다. + * -해당 polygonId에 해당하는 문제가 존재하지 않는 경우 + * @throws {DuplicateFoundException} 아래와 같은 경우 발생합니다. + * - 문제 소유자가 요청을 하는 경우 + * - 이미 등록된 협업자가 요청하는 경우 + * @throws {UnprocessableDataException} 아래와 같은 경우 발생합니다. + * - Owner role로 요청을 하는 경우 + */ + async requestCollaboration( + userId: number, + polygonId: number, + role: CollaboratorRole + ) { + const problem = await this.prisma.polygonProblem.findUnique({ + where: { id: polygonId }, + select: { createdById: true } + }) + if (!problem) throw new EntityNotExistException('PolygonProblem not found') + if (userId === problem.createdById) { + throw new DuplicateFoundException('is owner') + } + if (role === CollaboratorRole.Owner) { + throw new UnprocessableDataException('Cannot assign Owner role') + } + const existing = await this.prisma.polygonCollaborator.findFirst({ + where: { problemId: polygonId, userId }, + select: { id: true } + }) + if (existing) throw new DuplicateFoundException('Collaborator') + try { + return await this.prisma.polygonCollaborator.create({ + data: { + problemId: polygonId, + userId, + role, + status: CollaboratorStatus.Pending + } + }) + } catch (error) { + if ( + error instanceof Prisma.PrismaClientKnownRequestError && + error.code === 'P2002' + ) { + throw new DuplicateFoundException('Collaborator is already requested') + } + throw error + } + } +} diff --git a/apps/backend/apps/admin/src/polygon/collaborator/model/collaborator.input.ts b/apps/backend/apps/admin/src/polygon/collaborator/model/collaborator.input.ts new file mode 100644 index 0000000000..f97c0643db --- /dev/null +++ b/apps/backend/apps/admin/src/polygon/collaborator/model/collaborator.input.ts @@ -0,0 +1,21 @@ +import { InputType } from '@nestjs/graphql' +import { CollaboratorRole } from '@generated' +import { IsEmail, IsEnum, IsInt } from 'class-validator' + +@InputType() +export class CollaboratorInput { + @IsEmail() + userEmail: string + + @IsEnum(CollaboratorRole) + role: CollaboratorRole +} + +@InputType() +export class CollaboratorUpdateInput { + @IsInt() + userId: number + + @IsEnum(CollaboratorRole) + role: CollaboratorRole +} diff --git a/apps/backend/apps/admin/src/polygon/polygon.module.ts b/apps/backend/apps/admin/src/polygon/polygon.module.ts index f45388d1c4..866e61a217 100644 --- a/apps/backend/apps/admin/src/polygon/polygon.module.ts +++ b/apps/backend/apps/admin/src/polygon/polygon.module.ts @@ -1,10 +1,17 @@ import { Module } from '@nestjs/common' import { RolesModule } from '@libs/auth' +import { CollaboratorResolver } from './collaborator/collaborator.resolver' +import { CollaboratorService } from './collaborator/collaborator.service' import { PolygonResolver } from './polygon.resolver' import { PolygonService } from './polygon.service' @Module({ imports: [RolesModule], - providers: [PolygonResolver, PolygonService] + providers: [ + PolygonResolver, + CollaboratorResolver, + PolygonService, + CollaboratorService + ] }) export class PolygonModule {} diff --git a/apps/backend/prisma/migrations/20260526100450_rename_viewer_to_reviewer/migration.sql b/apps/backend/prisma/migrations/20260526100450_rename_viewer_to_reviewer/migration.sql new file mode 100644 index 0000000000..fb28fd1098 --- /dev/null +++ b/apps/backend/prisma/migrations/20260526100450_rename_viewer_to_reviewer/migration.sql @@ -0,0 +1,14 @@ +/* + Warnings: + + - The values [Viewer] on the enum `CollaboratorRole` will be removed. If these variants are still used in the database, this will fail. + +*/ +-- AlterEnum +BEGIN; +CREATE TYPE "public"."CollaboratorRole_new" AS ENUM ('Owner', 'Editor', 'Reviewer'); +ALTER TABLE "public"."polygon_collaborator" ALTER COLUMN "role" TYPE "public"."CollaboratorRole_new" USING ("role"::text::"public"."CollaboratorRole_new"); +ALTER TYPE "public"."CollaboratorRole" RENAME TO "CollaboratorRole_old"; +ALTER TYPE "public"."CollaboratorRole_new" RENAME TO "CollaboratorRole"; +DROP TYPE "public"."CollaboratorRole_old"; +COMMIT; diff --git a/apps/backend/prisma/schema.prisma b/apps/backend/prisma/schema.prisma index 56700392f2..5154d84365 100644 --- a/apps/backend/prisma/schema.prisma +++ b/apps/backend/prisma/schema.prisma @@ -1048,7 +1048,7 @@ enum TestFileType { enum CollaboratorRole { Owner Editor - Viewer + Reviewer } enum CollaboratorStatus {