diff --git a/packages/hoppscotch-backend/src/errors.ts b/packages/hoppscotch-backend/src/errors.ts index 014452ac1..e8a029a2a 100644 --- a/packages/hoppscotch-backend/src/errors.ts +++ b/packages/hoppscotch-backend/src/errors.ts @@ -219,6 +219,13 @@ export const TEAM_REQ_NOT_FOUND = 'team_req/not_found' as const; export const TEAM_REQ_INVALID_TARGET_COLL_ID = 'team_req/invalid_target_id' as const; +/** + * Tried to reorder team request but failed + * (TeamRequestService) + */ +export const TEAM_REQ_REORDERING_FAILED = + 'team_req/reordering_failed' as const; + /** * No Postmark Sender Email defined * (AuthService) diff --git a/packages/hoppscotch-backend/src/pubsub/topicsDefs.ts b/packages/hoppscotch-backend/src/pubsub/topicsDefs.ts index 957a2b831..8c8ac3b18 100644 --- a/packages/hoppscotch-backend/src/pubsub/topicsDefs.ts +++ b/packages/hoppscotch-backend/src/pubsub/topicsDefs.ts @@ -9,7 +9,10 @@ import { CollectionReorderData, TeamCollection, } from 'src/team-collection/team-collection.model'; -import { TeamRequest } from 'src/team-request/team-request.model'; +import { + RequestReorderData, + TeamRequest, +} from 'src/team-request/team-request.model'; import { TeamInvitation } from 'src/team-invitation/team-invitation.model'; import { UserCollection } from '@prisma/client'; import { UserCollectionReorderData } from 'src/user-collection/user-collections.model'; @@ -51,7 +54,10 @@ export type TopicDef = { [topic: `team_coll/${string}/${'coll_moved'}`]: TeamCollection; [topic: `team_coll/${string}/${'coll_order_updated'}`]: CollectionReorderData; [topic: `user_history/${string}/deleted_many`]: number; - [topic: `team_req/${string}/${'req_created' | 'req_updated'}`]: TeamRequest; + [ + topic: `team_req/${string}/${'req_created' | 'req_updated' | 'req_moved'}` + ]: TeamRequest; + [topic: `team_req/${string}/req_order_updated`]: RequestReorderData; [topic: `team_req/${string}/req_deleted`]: string; [topic: `team/${string}/invite_added`]: TeamInvitation; [topic: `team/${string}/invite_removed`]: string; diff --git a/packages/hoppscotch-backend/src/team-request/guards/gql-request-team-member.guard.ts b/packages/hoppscotch-backend/src/team-request/guards/gql-request-team-member.guard.ts index 43bfbfad0..dc01901ea 100644 --- a/packages/hoppscotch-backend/src/team-request/guards/gql-request-team-member.guard.ts +++ b/packages/hoppscotch-backend/src/team-request/guards/gql-request-team-member.guard.ts @@ -2,7 +2,6 @@ import { CanActivate, ExecutionContext, Injectable } from '@nestjs/common'; import { GqlExecutionContext } from '@nestjs/graphql'; import { TeamRequestService } from '../team-request.service'; import { TeamService } from '../../team/team.service'; -import { User } from '../../user/user.model'; import { Reflector } from '@nestjs/core'; import { TeamMemberRole } from '../../team/team.model'; import { @@ -10,8 +9,10 @@ import { BUG_TEAM_REQ_NO_REQ_ID, TEAM_REQ_NOT_REQUIRED_ROLE, TEAM_REQ_NOT_MEMBER, + TEAM_REQ_NOT_FOUND, } from 'src/errors'; import { throwErr } from 'src/utils'; +import * as O from 'fp-ts/Option'; @Injectable() export class GqlRequestTeamMemberGuard implements CanActivate { @@ -38,21 +39,17 @@ export class GqlRequestTeamMemberGuard implements CanActivate { const team = await this.teamRequestService.getTeamOfRequestFromID( requestID, ); + if (O.isNone(team)) throw new Error(TEAM_REQ_NOT_FOUND); - const member = - (await this.teamService.getTeamMember(team.id, user.uid)) ?? - throwErr(TEAM_REQ_NOT_MEMBER); + const member = await this.teamService.getTeamMember( + team.value.id, + user.uid, + ); + if (!member) throwErr(TEAM_REQ_NOT_MEMBER); - if (requireRoles) { - if (requireRoles.includes(member.role)) { - return true; - } else { - throw new Error(TEAM_REQ_NOT_REQUIRED_ROLE); - } - } + if (!(requireRoles && requireRoles.includes(member.role))) + throw new Error(TEAM_REQ_NOT_REQUIRED_ROLE); - if (member) return true; - - throw new Error(TEAM_REQ_NOT_MEMBER); + return true; } } diff --git a/packages/hoppscotch-backend/src/team-request/input-type.args.ts b/packages/hoppscotch-backend/src/team-request/input-type.args.ts new file mode 100644 index 000000000..5c88fd5d9 --- /dev/null +++ b/packages/hoppscotch-backend/src/team-request/input-type.args.ts @@ -0,0 +1,104 @@ +import { Field, ID, InputType, ArgsType } from '@nestjs/graphql'; +import { PaginationArgs } from 'src/types/input-types.args'; + +@InputType() +export class CreateTeamRequestInput { + @Field(() => ID, { + description: 'ID of the team the collection belongs to', + }) + teamID: string; + + @Field({ + description: 'JSON string representing the request data', + }) + request: string; + + @Field({ + description: 'Displayed title of the request', + }) + title: string; +} + +@InputType() +export class UpdateTeamRequestInput { + @Field({ + description: 'JSON string representing the request data', + nullable: true, + }) + request?: string; + + @Field({ + description: 'Displayed title of the request', + nullable: true, + }) + title?: string; +} + +@ArgsType() +export class SearchTeamRequestArgs extends PaginationArgs { + @Field(() => ID, { + description: 'ID of the team to look in', + }) + teamID: string; + + @Field({ + description: 'The title to search for', + }) + searchTerm: string; +} + +@ArgsType() +export class MoveTeamRequestArgs { + @Field(() => ID, { + // for backward compatibility, this field is nullable and undefined as default + nullable: true, + defaultValue: undefined, + description: 'ID of the collection, the request belong to', + }) + srcCollID: string; + + @Field(() => ID, { + description: 'ID of the request to move', + }) + requestID: string; + + @Field(() => ID, { + description: 'ID of the collection, where the request is moving to', + }) + destCollID: string; + + @Field(() => ID, { + nullable: true, + description: + 'ID of the request that comes after the updated request in its new position', + }) + nextRequestID: string; +} + +@ArgsType() +export class UpdateLookUpRequestOrderArgs { + @Field(() => ID, { + description: 'ID of the collection', + }) + collectionID: string; + + @Field(() => ID, { + nullable: true, + description: + 'ID of the request that comes after the updated request in its new position', + }) + nextRequestID: string; + + @Field(() => ID, { + description: 'ID of the request to move', + }) + requestID: string; +} + +@ArgsType() +export class GetTeamRequestInCollectionArgs extends PaginationArgs { + @Field(() => ID, { + description: 'ID of the collection to look in', + }) + collectionID: string; +} diff --git a/packages/hoppscotch-backend/src/team-request/team-request.model.ts b/packages/hoppscotch-backend/src/team-request/team-request.model.ts index 914124f5c..79363b4b5 100644 --- a/packages/hoppscotch-backend/src/team-request/team-request.model.ts +++ b/packages/hoppscotch-backend/src/team-request/team-request.model.ts @@ -1,37 +1,4 @@ -import { ObjectType, Field, ID, InputType } from '@nestjs/graphql'; - -@InputType() -export class CreateTeamRequestInput { - @Field(() => ID, { - description: 'ID of the team the collection belongs to', - }) - teamID: string; - - @Field({ - description: 'JSON string representing the request data', - }) - request: string; - - @Field({ - description: 'Displayed title of the request', - }) - title: string; -} - -@InputType() -export class UpdateTeamRequestInput { - @Field({ - description: 'JSON string representing the request data', - nullable: true, - }) - request?: string; - - @Field({ - description: 'Displayed title of the request', - nullable: true, - }) - title?: string; -} +import { ObjectType, Field, ID } from '@nestjs/graphql'; @ObjectType() export class TeamRequest { @@ -60,3 +27,18 @@ export class TeamRequest { }) title: string; } + +@ObjectType() +export class RequestReorderData { + @Field({ + description: 'Team Request being moved', + }) + request: TeamRequest; + + @Field({ + description: + 'Team Request succeeding the request being moved in its new position', + nullable: true, + }) + nextRequest?: TeamRequest; +} diff --git a/packages/hoppscotch-backend/src/team-request/team-request.resolver.ts b/packages/hoppscotch-backend/src/team-request/team-request.resolver.ts index b0beae42b..e2dcfb3bf 100644 --- a/packages/hoppscotch-backend/src/team-request/team-request.resolver.ts +++ b/packages/hoppscotch-backend/src/team-request/team-request.resolver.ts @@ -8,11 +8,15 @@ import { Subscription, ID, } from '@nestjs/graphql'; +import { RequestReorderData, TeamRequest } from './team-request.model'; import { - TeamRequest, CreateTeamRequestInput, UpdateTeamRequestInput, -} from './team-request.model'; + SearchTeamRequestArgs, + GetTeamRequestInCollectionArgs, + MoveTeamRequestArgs, + UpdateLookUpRequestOrderArgs, +} from './input-type.args'; import { Team, TeamMemberRole } from '../team/team.model'; import { TeamRequestService } from './team-request.service'; import { TeamCollection } from '../team-collection/team-collection.model'; @@ -23,8 +27,8 @@ import { GqlCollectionTeamMemberGuard } from '../team-collection/guards/gql-coll import { RequiresTeamRole } from '../team/decorators/requires-team-role.decorator'; import { GqlTeamMemberGuard } from '../team/guards/gql-team-member.guard'; import { PubSubService } from 'src/pubsub/pubsub.service'; -import { pipe } from 'fp-ts/function'; -import * as TE from 'fp-ts/TaskEither'; +import * as E from 'fp-ts/Either'; +import * as O from 'fp-ts/Option'; import { throwErr } from 'src/utils'; @Resolver(() => TeamRequest) @@ -39,44 +43,40 @@ export class TeamRequestResolver { description: 'Team the request belongs to', complexity: 3, }) - team(@Parent() req: TeamRequest): Promise { - return this.teamRequestService.getTeamOfRequest(req); + async team(@Parent() req: TeamRequest) { + const team = await this.teamRequestService.getTeamOfRequest(req); + if (E.isLeft(team)) throwErr(team.left); + return team.right; } - // @ResolveField(() => TeamCollection, { - // description: 'Collection the request belongs to', - // complexity: 3, - // }) - // collection(@Parent() req: TeamRequest): Promise { - // return this.teamRequestService.getCollectionOfRequest(req); - // } + @ResolveField(() => TeamCollection, { + description: 'Collection the request belongs to', + complexity: 3, + }) + async collection(@Parent() req: TeamRequest) { + const teamCollection = await this.teamRequestService.getCollectionOfRequest( + req, + ); + if (E.isLeft(teamCollection)) throwErr(teamCollection.left); + return teamCollection.right; + } // Query @Query(() => [TeamRequest], { description: 'Search the team for a specific request with title', }) @UseGuards(GqlAuthGuard, GqlTeamMemberGuard) - searchForRequest( - @Args({ - name: 'teamID', - description: 'ID of the team to look in', - type: () => ID, - }) - teamID: string, - @Args({ name: 'searchTerm', description: 'The title to search for' }) - searchTerm: string, - @Args({ - name: 'cursor', - type: () => ID, - description: 'ID of the last returned request or null', - nullable: true, - }) - cursor?: string, - ): Promise { + @RequiresTeamRole( + TeamMemberRole.EDITOR, + TeamMemberRole.OWNER, + TeamMemberRole.VIEWER, + ) + async searchForRequest(@Args() args: SearchTeamRequestArgs) { return this.teamRequestService.searchRequest( - teamID, - searchTerm, - cursor ?? null, + args.teamID, + args.searchTerm, + args.cursor, + args.take, ); } @@ -85,19 +85,26 @@ export class TeamRequestResolver { nullable: true, }) @UseGuards(GqlAuthGuard, GqlRequestTeamMemberGuard) - request( + @RequiresTeamRole( + TeamMemberRole.EDITOR, + TeamMemberRole.OWNER, + TeamMemberRole.VIEWER, + ) + async request( @Args({ name: 'requestID', description: 'ID of the request', type: () => ID, }) requestID: string, - ): Promise { - return this.teamRequestService.getRequest(requestID); + ) { + const teamRequest = await this.teamRequestService.getRequest(requestID); + if (O.isNone(teamRequest)) return null; + return teamRequest.value; } @Query(() => [TeamRequest], { - description: 'Gives a list of requests in the collection', + description: 'Gives a paginated list of requests in the collection', }) @UseGuards(GqlAuthGuard, GqlCollectionTeamMemberGuard) @RequiresTeamRole( @@ -105,7 +112,21 @@ export class TeamRequestResolver { TeamMemberRole.OWNER, TeamMemberRole.VIEWER, ) - requestsInCollection( + async requestsInCollection(@Args() input: GetTeamRequestInCollectionArgs) { + return this.teamRequestService.getRequestsInCollection( + input.collectionID, + input.cursor, + input.take, + ); + } + + // Mutation + @Mutation(() => TeamRequest, { + description: 'Create a team request in the given collection.', + }) + @UseGuards(GqlAuthGuard, GqlCollectionTeamMemberGuard) + @RequiresTeamRole(TeamMemberRole.EDITOR, TeamMemberRole.OWNER) + async createRequestInCollection( @Args({ name: 'collectionID', description: 'ID of the collection', @@ -113,49 +134,29 @@ export class TeamRequestResolver { }) collectionID: string, @Args({ - name: 'cursor', - nullable: true, - type: () => ID, - description: 'ID of the last returned request (for pagination)', + name: 'data', + type: () => CreateTeamRequestInput, + description: + 'The request data (stringified JSON of Hoppscotch request object)', }) - cursor?: string, - ): Promise { - return this.teamRequestService.getRequestsInCollection( + data: CreateTeamRequestInput, + ) { + const teamRequest = await this.teamRequestService.createTeamRequest( collectionID, - cursor ?? null, + data.teamID, + data.title, + data.request, ); + if (E.isLeft(teamRequest)) throwErr(teamRequest.left); + return teamRequest.right; } - // // Mutation - // @Mutation(() => TeamRequest, { - // description: 'Create a request in the given collection.', - // }) - // @UseGuards(GqlAuthGuard, GqlCollectionTeamMemberGuard) - // @RequiresTeamRole(TeamMemberRole.EDITOR, TeamMemberRole.OWNER) - // createRequestInCollection( - // @Args({ - // name: 'collectionID', - // description: 'ID of the collection', - // type: () => ID, - // }) - // collectionID: string, - // @Args({ - // name: 'data', - // type: () => CreateTeamRequestInput, - // description: - // 'The request data (stringified JSON of Hoppscotch request object)', - // }) - // data: CreateTeamRequestInput, - // ): Promise { - // return this.teamRequestService.createTeamRequest(collectionID, data); - // } - @Mutation(() => TeamRequest, { description: 'Update a request with the given ID', }) @UseGuards(GqlAuthGuard, GqlRequestTeamMemberGuard) @RequiresTeamRole(TeamMemberRole.EDITOR, TeamMemberRole.OWNER) - updateRequest( + async updateRequest( @Args({ name: 'requestID', description: 'ID of the request', @@ -169,8 +170,14 @@ export class TeamRequestResolver { 'The updated request data (stringified JSON of Hoppscotch request object)', }) data: UpdateTeamRequestInput, - ): Promise { - return this.teamRequestService.updateTeamRequest(requestID, data); + ) { + const teamRequest = await this.teamRequestService.updateTeamRequest( + requestID, + data.title, + data.request, + ); + if (E.isLeft(teamRequest)) throwErr(teamRequest.left); + return teamRequest.right; } @Mutation(() => Boolean, { @@ -185,35 +192,50 @@ export class TeamRequestResolver { type: () => ID, }) requestID: string, - ): Promise { - await this.teamRequestService.deleteTeamRequest(requestID); + ) { + const isDeleted = await this.teamRequestService.deleteTeamRequest( + requestID, + ); + if (E.isLeft(isDeleted)) throwErr(isDeleted.left); + return isDeleted.right; + } + + @Mutation(() => Boolean, { + description: 'Update the order of requests in the lookup table', + }) + @UseGuards(GqlAuthGuard, GqlRequestTeamMemberGuard) + @RequiresTeamRole(TeamMemberRole.EDITOR, TeamMemberRole.OWNER) + async updateLookUpRequestOrder( + @Args() + args: UpdateLookUpRequestOrderArgs, + ) { + const teamRequest = await this.teamRequestService.moveRequest( + args.collectionID, + args.requestID, + args.collectionID, + args.nextRequestID, + 'updateLookUpRequestOrder', + ); + if (E.isLeft(teamRequest)) throwErr(teamRequest.left); return true; } - // @Mutation(() => TeamRequest, { - // description: 'Move a request to the given collection', - // }) - // @UseGuards(GqlAuthGuard, GqlRequestTeamMemberGuard) - // @RequiresTeamRole(TeamMemberRole.EDITOR, TeamMemberRole.OWNER) - // moveRequest( - // @Args({ - // name: 'requestID', - // description: 'ID of the request to move', - // type: () => ID, - // }) - // requestID: string, - // @Args({ - // name: 'destCollID', - // description: 'ID of the collection to move the request to', - // type: () => ID, - // }) - // destCollID: string, - // ): Promise { - // return pipe( - // this.teamRequestService.moveRequest(requestID, destCollID), - // TE.getOrElse((e) => throwErr(e)), - // )(); - // } + @Mutation(() => TeamRequest, { + description: 'Move a request to the given collection', + }) + @UseGuards(GqlAuthGuard, GqlRequestTeamMemberGuard) + @RequiresTeamRole(TeamMemberRole.EDITOR, TeamMemberRole.OWNER) + async moveRequest(@Args() args: MoveTeamRequestArgs) { + const teamRequest = await this.teamRequestService.moveRequest( + args.srcCollID, + args.requestID, + args.destCollID, + args.nextRequestID, + 'moveRequest', + ); + if (E.isLeft(teamRequest)) throwErr(teamRequest.left); + return teamRequest.right; + } // Subscriptions @Subscription(() => TeamRequest, { @@ -233,7 +255,7 @@ export class TeamRequestResolver { type: () => ID, }) teamID: string, - ): AsyncIterator { + ) { return this.pubsub.asyncIterator(`team_req/${teamID}/req_created`); } @@ -254,7 +276,7 @@ export class TeamRequestResolver { type: () => ID, }) teamID: string, - ): AsyncIterator { + ) { return this.pubsub.asyncIterator(`team_req/${teamID}/req_updated`); } @@ -276,7 +298,51 @@ export class TeamRequestResolver { type: () => ID, }) teamID: string, - ): AsyncIterator { + ) { return this.pubsub.asyncIterator(`team_req/${teamID}/req_deleted`); } + + @Subscription(() => RequestReorderData, { + description: + 'Emitted when a requests position has been changed in its collection', + resolve: (value) => value, + }) + @UseGuards(GqlAuthGuard, GqlTeamMemberGuard) + @RequiresTeamRole( + TeamMemberRole.VIEWER, + TeamMemberRole.EDITOR, + TeamMemberRole.OWNER, + ) + requestOrderUpdated( + @Args({ + name: 'teamID', + description: 'ID of the team to listen to', + type: () => ID, + }) + teamID: string, + ) { + return this.pubsub.asyncIterator(`team_req/${teamID}/req_order_updated`); + } + + @Subscription(() => TeamRequest, { + description: + 'Emitted when a request has been moved from one collection into another', + resolve: (value) => value, + }) + @UseGuards(GqlAuthGuard, GqlTeamMemberGuard) + @RequiresTeamRole( + TeamMemberRole.VIEWER, + TeamMemberRole.EDITOR, + TeamMemberRole.OWNER, + ) + requestMoved( + @Args({ + name: 'teamID', + description: 'ID of the team to listen to', + type: () => ID, + }) + teamID: string, + ) { + return this.pubsub.asyncIterator(`team_req/${teamID}/req_moved`); + } } diff --git a/packages/hoppscotch-backend/src/team-request/team-request.service.spec.ts b/packages/hoppscotch-backend/src/team-request/team-request.service.spec.ts index fbb5880ac..d86ed6be9 100644 --- a/packages/hoppscotch-backend/src/team-request/team-request.service.spec.ts +++ b/packages/hoppscotch-backend/src/team-request/team-request.service.spec.ts @@ -7,99 +7,114 @@ import { TEAM_INVALID_COLL_ID, TEAM_INVALID_ID, TEAM_REQ_NOT_FOUND, + TEAM_REQ_REORDERING_FAILED, } from 'src/errors'; +import * as E from 'fp-ts/Either'; +import * as O from 'fp-ts/Option'; import { mockDeep, mockReset } from 'jest-mock-extended'; -import * as TO from 'fp-ts/TaskOption'; -import { TeamCollection } from 'src/team-collection/team-collection.model'; import { TeamRequest } from './team-request.model'; +import { MoveTeamRequestArgs } from './input-type.args'; +import { + TeamRequest as DbTeamRequest, + Team as DbTeam, + TeamCollection as DbTeamCollection, +} from '@prisma/client'; const mockPrisma = mockDeep(); - const mockTeamService = mockDeep(); - const mockTeamCollectionService = mockDeep(); +const mockPubSub = { publish: jest.fn().mockResolvedValue(null) }; -const mockPubSub = { - publish: jest.fn().mockResolvedValue(null), -}; - +// eslint-disable-next-line @typescript-eslint/ban-ts-comment +// @ts-ignore const teamRequestService = new TeamRequestService( - mockPrisma as any, + mockPrisma, mockTeamService as any, mockTeamCollectionService as any, mockPubSub as any, ); +const team: DbTeam = { + id: 'team-a', + name: 'Team A', +}; +const teamCollection: DbTeamCollection = { + id: 'team-coll-1', + parentID: null, + teamID: team.id, + title: 'Team Collection 1', +}; +const dbTeamRequests: DbTeamRequest[] = []; +for (let i = 1; i <= 10; i++) { + dbTeamRequests.push({ + id: `test-request-${i}`, + collectionID: teamCollection.id, + teamID: team.id, + request: {}, + title: `Test Request ${i}`, + orderIndex: i, + createdOn: new Date(), + updatedOn: new Date(), + }); +} +const teamRequests: TeamRequest[] = dbTeamRequests.map((tr) => ({ + id: tr.id, + collectionID: tr.collectionID, + teamID: tr.teamID, + title: tr.title, + request: JSON.stringify(tr.request), +})); + beforeEach(async () => { mockReset(mockPrisma); }); describe('updateTeamRequest', () => { - test('resolves correctly if title is null', async () => { - mockPrisma.teamRequest.update.mockResolvedValue({ - id: 'testrequest', - collectionID: 'testcoll', - teamID: '3170', - request: '{}', - title: 'Test Request', - }); + test('resolves correctly if title not given in parameter', async () => { + const dbRequest = dbTeamRequests[0]; + mockPrisma.teamRequest.update.mockResolvedValue(dbRequest); await expect( - teamRequestService.updateTeamRequest('testrequest', { - request: '{}', - title: undefined, - }), + teamRequestService.updateTeamRequest( + dbRequest.id, + undefined, // title + JSON.stringify(dbRequest.request), // request + ), ).resolves.toBeDefined(); }); - test('resolves correctly if request is null', async () => { - mockPrisma.teamRequest.update.mockResolvedValue({ - id: 'testrequest', - collectionID: 'testcoll', - teamID: '3170', - request: '{}', - title: 'Test Request', - }); + test('resolves correctly if request not given in parameter', async () => { + const dbRequest = dbTeamRequests[0]; + mockPrisma.teamRequest.update.mockResolvedValue(dbRequest); await expect( - teamRequestService.updateTeamRequest('testrequest', { - request: undefined, - title: 'Test Request', - }), + teamRequestService.updateTeamRequest( + dbRequest.id, + dbRequest.title, + undefined, + ), ).resolves.toBeDefined(); }); test('resolves correctly if both request and title are null', async () => { - mockPrisma.teamRequest.update.mockResolvedValue({ - id: 'testrequest', - collectionID: 'testcoll', - teamID: '3170', - request: '{}', - title: 'Test Request', - }); + const dbRequest = dbTeamRequests[0]; + mockPrisma.teamRequest.update.mockResolvedValue(dbRequest); await expect( - teamRequestService.updateTeamRequest('testrequest', { - request: undefined, - title: undefined, - }), + teamRequestService.updateTeamRequest(dbRequest.id, undefined, undefined), ).resolves.toBeDefined(); }); test('resolves correctly for non-null request and title', async () => { - mockPrisma.teamRequest.update.mockResolvedValue({ - id: 'testrequest', - collectionID: 'testcoll', - teamID: '3170', - request: '{}', - title: 'Test Request', - }); + const dbRequest = dbTeamRequests[0]; + mockPrisma.teamRequest.update.mockResolvedValue(dbRequest); await expect( - teamRequestService.updateTeamRequest('testrequest', { - request: '{}', - title: 'Test Request', - }), + teamRequestService.updateTeamRequest( + dbRequest.id, + dbRequest.title, + JSON.stringify(dbRequest.request), + ), ).resolves.toBeDefined(); }); @@ -107,405 +122,168 @@ describe('updateTeamRequest', () => { mockPrisma.teamRequest.update.mockRejectedValue('RecordNotFound'); await expect( - teamRequestService.updateTeamRequest('invalidtestreq', { - request: undefined, - title: undefined, - }), - ).rejects.toBeDefined(); + teamRequestService.updateTeamRequest( + 'invalidtestreq', + undefined, + undefined, + ), + ).resolves.toEqualLeft(TEAM_REQ_NOT_FOUND); }); test('resolves for valid request id', async () => { - mockPrisma.teamRequest.update.mockResolvedValue({ - id: 'testrequest', - collectionID: 'testcoll', - teamID: '3170', - request: '{}', - title: 'Test Request', - }); + const dbRequest = dbTeamRequests[0]; + mockPrisma.teamRequest.update.mockResolvedValue(dbRequest); await expect( - teamRequestService.updateTeamRequest('testrequest', { - request: undefined, - title: undefined, - }), + teamRequestService.updateTeamRequest(dbRequest.id, undefined, undefined), ).resolves.toBeDefined(); }); test('publishes update to pubsub topic "team_req//req_updated"', async () => { - mockPrisma.teamRequest.update.mockResolvedValue({ - id: 'testrequest', - collectionID: 'testcoll', - teamID: '3170', - request: '{}', - title: 'Test Request', - }); + const dbRequest = dbTeamRequests[0]; + const request = teamRequests[0]; + mockPrisma.teamRequest.update.mockResolvedValue(dbRequest); - const result = await teamRequestService.updateTeamRequest('testrequest', { - request: undefined, - title: undefined, - }); + await teamRequestService.updateTeamRequest( + dbRequest.id, + undefined, + undefined, + ); expect(mockPubSub.publish).toHaveBeenCalledWith( - 'team_req/3170/req_updated', - result, + `team_req/${dbRequest.teamID}/req_updated`, + request, ); }); }); describe('searchRequest', () => { test('resolves with the correct info with a null cursor', async () => { - mockPrisma.teamRequest.findMany.mockResolvedValue([ - { - id: 'testrequest', - collectionID: 'testcoll', - teamID: '3170', - request: '{}', - title: 'Test Request', - }, - ]); + const dbRequest = dbTeamRequests[0]; + mockPrisma.teamRequest.findMany.mockResolvedValue(dbTeamRequests); await expect( - teamRequestService.searchRequest('3170', 'Test', null), + teamRequestService.searchRequest( + dbRequest.teamID, + dbRequest.title, + null, + 10, + ), ).resolves.toBeDefined(); }); test('resolves with an empty array when a match with the search term is not found', async () => { + const dbRequest = dbTeamRequests[0]; mockPrisma.teamRequest.findMany.mockResolvedValue([]); await expect( - teamRequestService.searchRequest('3170', 'Test', null), + teamRequestService.searchRequest( + dbRequest.teamID, + 'unknown_title', + null, + 10, + ), ).resolves.toBeDefined(); }); test('resolves with the correct info with a set cursor', async () => { - mockPrisma.teamRequest.findMany.mockResolvedValue([ - { - id: 'testrequest1', - collectionID: 'testcoll', - teamID: '3170', - request: '{}', - title: 'Test Request 1', - }, - { - id: 'testrequest2', - collectionID: 'testcoll', - teamID: '3170', - request: '{}', - title: 'Test Request 2', - }, - { - id: 'testrequest3', - collectionID: 'testcoll', - teamID: '3170', - request: '{}', - title: 'Test Request 3', - }, - { - id: 'testrequest4', - collectionID: 'testcoll', - teamID: '3170', - request: '{}', - title: 'Test Request 4', - }, - { - id: 'testrequest5', - collectionID: 'testcoll', - teamID: '3170', - request: '{}', - title: 'Test Request 5', - }, - { - id: 'testrequest6', - collectionID: 'testcoll', - teamID: '3170', - request: '{}', - title: 'Test Request 6', - }, - { - id: 'testrequest7', - collectionID: 'testcoll', - teamID: '3170', - request: '{}', - title: 'Test Request 7', - }, - { - id: 'testrequest8', - collectionID: 'testcoll', - teamID: '3170', - request: '{}', - title: 'Test Request 8', - }, - { - id: 'testrequest9', - collectionID: 'testcoll', - teamID: '3170', - request: '{}', - title: 'Test Request 9', - }, - { - id: 'testrequest10', - collectionID: 'testcoll', - teamID: '3170', - request: '{}', - title: 'Test Request 10', - }, - ]); - - const secondColl = ( - await teamRequestService.searchRequest('3170', 'Test', null) - )[1]; - - mockReset(mockPrisma); - mockPrisma.teamRequest.findMany.mockResolvedValue([ - { - id: 'testrequest11', - collectionID: 'testcoll', - teamID: '3170', - request: '{}', - title: 'Test Request 11', - }, - ]); + const dbRequest = dbTeamRequests[0]; + mockPrisma.teamRequest.findMany.mockResolvedValue(dbTeamRequests); await expect( - teamRequestService.searchRequest('3170', 'Test', secondColl.id), + teamRequestService.searchRequest( + dbRequest.teamID, + dbRequest.title, + dbRequest.id, + 10, + ), ).resolves.toBeDefined(); }); - - test('resolves with the first ten elements when a null cursor is entered', async () => { - mockPrisma.teamRequest.findMany.mockResolvedValue([ - { - id: 'testrequest1', - collectionID: 'testcoll', - teamID: '3170', - request: '{}', - title: 'Test Request 1', - }, - { - id: 'testrequest2', - collectionID: 'testcoll', - teamID: '3170', - request: '{}', - title: 'Test Request 2', - }, - { - id: 'testrequest3', - collectionID: 'testcoll', - teamID: '3170', - request: '{}', - title: 'Test Request 3', - }, - { - id: 'testrequest4', - collectionID: 'testcoll', - teamID: '3170', - request: '{}', - title: 'Test Request 4', - }, - { - id: 'testrequest5', - collectionID: 'testcoll', - teamID: '3170', - request: '{}', - title: 'Test Request 5', - }, - { - id: 'testrequest6', - collectionID: 'testcoll', - teamID: '3170', - request: '{}', - title: 'Test Request 6', - }, - { - id: 'testrequest7', - collectionID: 'testcoll', - teamID: '3170', - request: '{}', - title: 'Test Request 7', - }, - { - id: 'testrequest8', - collectionID: 'testcoll', - teamID: '3170', - request: '{}', - title: 'Test Request 8', - }, - { - id: 'testrequest9', - collectionID: 'testcoll', - teamID: '3170', - request: '{}', - title: 'Test Request 9', - }, - { - id: 'testrequest10', - collectionID: 'testcoll', - teamID: '3170', - request: '{}', - title: 'Test Request 10', - }, - ]); - - await expect( - teamRequestService.searchRequest('3170', 'Test', null), - ).resolves.toHaveLength(10); - }); }); describe('deleteTeamRequest', () => { test('rejects if the request id is not found', async () => { - mockPrisma.teamRequest.findUnique.mockResolvedValue(null as any); + mockPrisma.teamRequest.findFirst.mockResolvedValue(null as any); - mockPrisma.teamRequest.delete.mockRejectedValue('RecordNotFound'); + const response = teamRequestService.deleteTeamRequest('invalidrequest'); - await expect( - teamRequestService.deleteTeamRequest('invalidrequest'), - ).rejects.toThrow(TEAM_REQ_NOT_FOUND); + expect(response).resolves.toEqualLeft(TEAM_REQ_NOT_FOUND); expect(mockPrisma.teamRequest.delete).not.toHaveBeenCalled(); }); test('resolves for a valid request id', async () => { - mockPrisma.teamRequest.findUnique.mockResolvedValue({ - id: 'testrequest', - collectionID: 'testcoll', - teamID: '3170', - request: '{}', - title: 'Test Request', - }); - - mockPrisma.teamRequest.delete.mockResolvedValue({ - id: 'testrequest', - collectionID: 'testcoll', - teamID: '3170', - request: '{}', - title: 'Test Request', - }); + const dbRequest = dbTeamRequests[0]; + mockPrisma.teamRequest.findFirst.mockResolvedValue(dbRequest); + mockPrisma.teamRequest.delete.mockResolvedValue(dbRequest); await expect( - teamRequestService.deleteTeamRequest('testrequest'), - ).resolves.toBeUndefined(); + teamRequestService.deleteTeamRequest(dbRequest.id), + ).resolves.toEqualRight(true); }); test('publishes deletion to pubsub topic "team_req//req_deleted"', async () => { - mockPrisma.teamRequest.findUnique.mockResolvedValue({ - id: 'testrequest', - collectionID: 'testcoll', - teamID: '3170', - request: '{}', - title: 'Test Request', - }); + const dbRequest = dbTeamRequests[0]; + mockPrisma.teamRequest.findFirst.mockResolvedValue(dbRequest); + mockPrisma.teamRequest.delete.mockResolvedValue(dbRequest); - mockPrisma.teamRequest.delete.mockResolvedValue({ - id: 'testrequest', - collectionID: 'testcoll', - teamID: '3170', - request: '{}', - title: 'Test Request', - }); + await teamRequestService.deleteTeamRequest(dbRequest.id); expect(mockPubSub.publish).toHaveBeenCalledWith( - 'team_req/3170/req_deleted', - 'testrequest', + `team_req/${dbRequest.teamID}/req_deleted`, + dbRequest.id, ); }); }); describe('createTeamRequest', () => { test('rejects for invalid collection id', async () => { - mockPrisma.teamCollection.findUnique.mockRejectedValue( - TEAM_INVALID_COLL_ID, + mockTeamCollectionService.getTeamOfCollection.mockResolvedValue(null); + + const response = await teamRequestService.createTeamRequest( + 'invalidcollid', + team.id, + 'Test Request', + '{}', ); - mockPrisma.teamRequest.create.mockRejectedValue(null as any); - - await expect( - teamRequestService.createTeamRequest('invalidcollid', { - teamID: '3170', - request: '{}', - title: 'Test Request', - }), - ).rejects.toBeDefined(); - + expect(response).toEqualLeft(TEAM_INVALID_COLL_ID); expect(mockPrisma.teamRequest.create).not.toHaveBeenCalled(); }); test('resolves for valid collection id', async () => { - mockTeamCollectionService.getTeamOfCollection.mockResolvedValue({ - id: 'testcoll', - title: 'Test Collection', - parentID: null, - teamID: '3170', - team: { - id: '3170', - name: 'Test Team', - }, - } as any); + const dbRequest = dbTeamRequests[0]; + const teamRequest = teamRequests[0]; - mockPrisma.teamRequest.create.mockResolvedValue({ - id: 'testrequest', - collectionID: 'testcoll', - teamID: '3170', - request: '{}', - title: 'Test Request', - team: { - id: '3170', - name: 'Test Team', - }, - collection: { - id: 'testcoll', - title: 'Test Collection', - parentID: null, - teamID: '3170', - }, - } as any); + mockTeamCollectionService.getTeamOfCollection.mockResolvedValue(team); + mockPrisma.teamRequest.create.mockResolvedValue(dbRequest); - await expect( - teamRequestService.createTeamRequest('testcoll', { - teamID: '3170', - request: '{}', - title: 'Test Request', - }), - ).resolves.toBeDefined(); + const response = teamRequestService.createTeamRequest( + 'testcoll', + team.id, + teamRequest.title, + teamRequest.request, + ); + + expect(response).resolves.toEqualRight(teamRequest); }); test('publishes creation to pubsub topic "team_req//req_created"', async () => { - mockTeamCollectionService.getTeamOfCollection.mockResolvedValue({ - id: 'testcoll', - title: 'Test Collection', - parentID: null, - teamID: '3170', - team: { - id: '3170', - name: 'Test Team', - }, - } as any); + const dbRequest = dbTeamRequests[0]; + const teamRequest = teamRequests[0]; - mockPrisma.teamRequest.create.mockResolvedValue({ - id: 'testrequest', - collectionID: 'testcoll', - teamID: '3170', - request: '{}', - title: 'Test Request', - team: { - id: '3170', - name: 'Test Team', - }, - collection: { - id: 'testcoll', - title: 'Test Collection', - parentID: null, - teamID: '3170', - }, - } as any); + mockTeamCollectionService.getTeamOfCollection.mockResolvedValue(team); + mockPrisma.teamRequest.create.mockResolvedValue(dbRequest); - const result = await teamRequestService.createTeamRequest('testcoll', { - teamID: '3170', - request: '{}', - title: 'Test Request', - }); + await teamRequestService.createTeamRequest( + 'testcoll', + team.id, + 'Test Request', + '{}', + ); expect(mockPubSub.publish).toHaveBeenCalledWith( - 'team_req/3170/req_created', - result, + `team_req/${dbRequest.teamID}/req_created`, + teamRequest, ); }); }); @@ -515,228 +293,41 @@ describe('getRequestsInCollection', () => { mockPrisma.teamRequest.findMany.mockResolvedValue([]); await expect( - teamRequestService.getRequestsInCollection('invalidcoll', null), + teamRequestService.getRequestsInCollection('invalidCollID', null, 10), ).resolves.toEqual([]); }); test('resolves with the correct info for the collection id and null cursor', async () => { - mockPrisma.teamRequest.findMany.mockResolvedValue([ - { - id: 'testrequest', - collectionID: 'testcoll', - teamID: '3170', - request: '{}', - title: 'Test Request', - }, - ]); + mockPrisma.teamRequest.findMany.mockResolvedValue(dbTeamRequests); - await expect( - teamRequestService.getRequestsInCollection('testcoll', null), - ).resolves.toBeDefined(); + const response = await teamRequestService.getRequestsInCollection( + 'testcoll', + null, + 10, + ); + + expect(response).toEqual(teamRequests); }); test('resolves with the correct info for the collection id and a valid cursor', async () => { - mockPrisma.teamRequest.findMany.mockResolvedValue([ - { - id: 'testrequest1', - collectionID: 'testcoll', - teamID: '3170', - request: '{}', - title: 'Test Request 1', - }, - { - id: 'testrequest2', - collectionID: 'testcoll', - teamID: '3170', - request: '{}', - title: 'Test Request 2', - }, - { - id: 'testrequest3', - collectionID: 'testcoll', - teamID: '3170', - request: '{}', - title: 'Test Request 3', - }, - { - id: 'testrequest4', - collectionID: 'testcoll', - teamID: '3170', - request: '{}', - title: 'Test Request 4', - }, - { - id: 'testrequest5', - collectionID: 'testcoll', - teamID: '3170', - request: '{}', - title: 'Test Request 5', - }, - { - id: 'testrequest6', - collectionID: 'testcoll', - teamID: '3170', - request: '{}', - title: 'Test Request 6', - }, - { - id: 'testrequest7', - collectionID: 'testcoll', - teamID: '3170', - request: '{}', - title: 'Test Request 7', - }, - { - id: 'testrequest8', - collectionID: 'testcoll', - teamID: '3170', - request: '{}', - title: 'Test Request 8', - }, - { - id: 'testrequest9', - collectionID: 'testcoll', - teamID: '3170', - request: '{}', - title: 'Test Request 9', - }, - { - id: 'testrequest10', - collectionID: 'testcoll', - teamID: '3170', - request: '{}', - title: 'Test Request 10', - }, - ]); + mockPrisma.teamRequest.findMany.mockResolvedValue([dbTeamRequests[1]]); - const secondColl = ( - await teamRequestService.getRequestsInCollection('testcoll', null) - )[1]; + const response = teamRequestService.getRequestsInCollection( + dbTeamRequests[1].collectionID, + dbTeamRequests[0].id, + 1, + ); - mockReset(mockPrisma); - mockPrisma.teamRequest.findMany.mockResolvedValue([ - { - id: 'testrequest11', - collectionID: 'testcoll', - teamID: '3170', - request: '{}', - title: 'Test Request 11', - }, - ]); - - await expect( - teamRequestService.getRequestsInCollection('testcoll', secondColl.id), - ).resolves.toBeDefined(); - }); - - test('resolves with the correct info for the collection id and a valid cursor', async () => { - mockPrisma.teamRequest.findMany.mockResolvedValue([ - { - id: 'testrequest1', - collectionID: 'testcoll', - teamID: '3170', - request: '{}', - title: 'Test Request 1', - }, - { - id: 'testrequest2', - collectionID: 'testcoll', - teamID: '3170', - request: '{}', - title: 'Test Request 2', - }, - { - id: 'testrequest3', - collectionID: 'testcoll', - teamID: '3170', - request: '{}', - title: 'Test Request 3', - }, - { - id: 'testrequest4', - collectionID: 'testcoll', - teamID: '3170', - request: '{}', - title: 'Test Request 4', - }, - { - id: 'testrequest5', - collectionID: 'testcoll', - teamID: '3170', - request: '{}', - title: 'Test Request 5', - }, - { - id: 'testrequest6', - collectionID: 'testcoll', - teamID: '3170', - request: '{}', - title: 'Test Request 6', - }, - { - id: 'testrequest7', - collectionID: 'testcoll', - teamID: '3170', - request: '{}', - title: 'Test Request 7', - }, - { - id: 'testrequest8', - collectionID: 'testcoll', - teamID: '3170', - request: '{}', - title: 'Test Request 8', - }, - { - id: 'testrequest9', - collectionID: 'testcoll', - teamID: '3170', - request: '{}', - title: 'Test Request 9', - }, - { - id: 'testrequest10', - collectionID: 'testcoll', - teamID: '3170', - request: '{}', - title: 'Test Request 10', - }, - ]); - - await expect( - teamRequestService.getRequestsInCollection('testcoll', null), - ).resolves.toHaveLength(10); + expect(response).resolves.toEqual([teamRequests[1]]); }); }); describe('getRequest', () => { test('resolves with the correct request info for valid request id', async () => { - mockPrisma.teamRequest.findUnique.mockResolvedValue({ - id: 'testrequest', - collectionID: 'testcoll', - teamID: '3170', - request: '{}', - title: 'Test Request', - team: { - id: '3170', - name: 'Test Team', - }, - collection: { - id: 'testcoll', - title: 'Test Collection', - parentID: null, - teamID: '3170', - }, - } as any); + mockPrisma.teamRequest.findUnique.mockResolvedValue(dbTeamRequests[0]); - await expect(teamRequestService.getRequest('testrequest')).resolves.toEqual( - expect.objectContaining({ - id: 'testrequest', - collectionID: 'testcoll', - teamID: '3170', - request: '"{}"', - title: 'Test Request', - }), + expect(teamRequestService.getRequest('testrequest')).resolves.toEqualSome( + expect.objectContaining(teamRequests[0]), ); }); @@ -745,7 +336,7 @@ describe('getRequest', () => { await expect( teamRequestService.getRequest('testrequest'), - ).resolves.toBeNull(); + ).resolves.toBeNone(); }); }); @@ -753,37 +344,17 @@ describe('getTeamOfRequest', () => { test('rejects for invalid team id', async () => { mockTeamService.getTeamWithID.mockResolvedValue(null as any); - await expect( - teamRequestService.getTeamOfRequest({ - id: 'testrequest', - collectionID: 'testcoll', - teamID: 'invalidteamid', - request: '{}', - title: 'Test Request', - }), - ).rejects.toThrow(TEAM_INVALID_ID); + expect( + teamRequestService.getTeamOfRequest(teamRequests[0]), + ).resolves.toEqualLeft(TEAM_INVALID_ID); }); test('resolves for valid team id', async () => { - mockTeamService.getTeamWithID.mockResolvedValue({ - id: '3170', - name: 'Test Team', - }); + mockTeamService.getTeamWithID.mockResolvedValue(team); - await expect( - teamRequestService.getTeamOfRequest({ - id: 'testrequest', - collectionID: 'testcoll', - teamID: '3170', - request: '{}', - title: 'Test Request', - }), - ).resolves.toEqual( - expect.objectContaining({ - id: '3170', - name: 'Test Team', - }), - ); + expect( + teamRequestService.getTeamOfRequest(teamRequests[0]), + ).resolves.toEqualRight(expect.objectContaining(team)); }); }); @@ -791,41 +362,17 @@ describe('getCollectionOfRequest', () => { test('rejects for invalid collection id', async () => { mockTeamCollectionService.getCollection.mockResolvedValue(null as any); - await expect( - teamRequestService.getCollectionOfRequest({ - id: 'testrequest', - collectionID: 'invalidcollid', - teamID: '3170', - request: '{}', - title: 'Test Request', - }), - ).rejects.toThrow(TEAM_INVALID_COLL_ID); + expect( + teamRequestService.getCollectionOfRequest(teamRequests[0]), + ).resolves.toEqualLeft(TEAM_INVALID_COLL_ID); }); test('resolves for valid collection id', async () => { - mockTeamCollectionService.getCollection.mockResolvedValue({ - id: 'testcoll', - title: 'Test Collection', - parentID: null, - teamID: '3170', - }); + mockTeamCollectionService.getCollection.mockResolvedValue(teamCollection); - await expect( - teamRequestService.getCollectionOfRequest({ - id: 'testrequest', - collectionID: 'testcoll', - teamID: '3170', - request: '{}', - title: 'Test Request', - }), - ).resolves.toEqual( - expect.objectContaining({ - id: 'testcoll', - title: 'Test Collection', - parentID: null, - teamID: '3170', - }), - ); + expect( + teamRequestService.getCollectionOfRequest(teamRequests[0]), + ).resolves.toEqualRight(expect.objectContaining(teamCollection)); }); }); @@ -833,309 +380,314 @@ describe('getTeamOfRequestFromID', () => { test('rejects for invalid request id', async () => { mockPrisma.teamRequest.findUnique.mockResolvedValue(null as any); - await expect( + expect( teamRequestService.getTeamOfRequestFromID('invalidrequest'), - ).rejects.toThrow(TEAM_REQ_NOT_FOUND); + ).resolves.toBeNone(); }); test('resolves for valid request id', async () => { - mockPrisma.teamRequest.findUnique.mockResolvedValue({ - id: 'testrequest', - collectionID: 'testcoll', - teamID: '3170', - request: '{}', - title: 'Test Request', - team: { - id: '3170', - name: 'Test team', - }, - } as any); + mockPrisma.teamRequest.findUnique.mockResolvedValue(dbTeamRequests[0]); - await expect( + expect( teamRequestService.getTeamOfRequestFromID('testrequest'), - ).resolves.toEqual( - expect.objectContaining({ - id: '3170', - name: 'Test team', - }), + ).resolves.toBeDefined(); + }); +}); + +describe('reorderRequests', () => { + test('Should resolve left if transaction throws an error', async () => { + const srcCollID = dbTeamRequests[0].collectionID; + const request = dbTeamRequests[0]; + const destCollID = dbTeamRequests[4].collectionID; + const nextRequest = dbTeamRequests[4]; + + mockPrisma.$transaction.mockRejectedValueOnce(new Error()); + const result = await teamRequestService.reorderRequests( + request, + srcCollID, + nextRequest, + destCollID, + ); + expect(result).toEqual(E.left(TEAM_REQ_REORDERING_FAILED)); + }); + test('Should resolve right and call transaction with the correct data', async () => { + const srcCollID = dbTeamRequests[0].collectionID; + const request = dbTeamRequests[0]; + const destCollID = dbTeamRequests[4].collectionID; + const nextRequest = dbTeamRequests[4]; + + const updatedReq: DbTeamRequest = { + ...request, + collectionID: destCollID, + orderIndex: nextRequest.orderIndex, + }; + + mockPrisma.$transaction.mockResolvedValueOnce(E.right(updatedReq)); + const result = await teamRequestService.reorderRequests( + request, + srcCollID, + nextRequest, + destCollID, + ); + expect(mockPrisma.$transaction).toHaveBeenCalledWith(expect.any(Function)); + expect(result).toEqual(E.right(updatedReq)); + }); +}); + +describe('findRequestAndNextRequest', () => { + test('Should resolve right if the request and the next request are found', async () => { + const args: MoveTeamRequestArgs = { + srcCollID: teamRequests[0].collectionID, + destCollID: teamRequests[4].collectionID, + requestID: teamRequests[0].id, + nextRequestID: teamRequests[4].id, + }; + + mockPrisma.teamRequest.findFirst + .mockResolvedValueOnce(dbTeamRequests[0]) + .mockResolvedValueOnce(dbTeamRequests[4]); + + const result = await teamRequestService.findRequestAndNextRequest( + args.srcCollID, + args.requestID, + args.destCollID, + args.nextRequestID, + ); + + expect(result).toEqualRight({ + request: dbTeamRequests[0], + nextRequest: dbTeamRequests[4], + }); + }); + test('Should resolve right if the request and next request null', () => { + const args: MoveTeamRequestArgs = { + srcCollID: teamRequests[0].collectionID, + destCollID: teamRequests[4].collectionID, + requestID: teamRequests[0].id, + nextRequestID: null, + }; + + mockPrisma.teamRequest.findFirst + .mockResolvedValueOnce(dbTeamRequests[0]) + .mockResolvedValueOnce(null); + + const result = teamRequestService.findRequestAndNextRequest( + args.srcCollID, + args.requestID, + args.destCollID, + args.nextRequestID, + ); + + expect(result).resolves.toEqualRight({ + request: dbTeamRequests[0], + nextRequest: null, + }); + }); + test('Should resolve left if the request is not found', () => { + const args: MoveTeamRequestArgs = { + srcCollID: teamRequests[0].collectionID, + destCollID: teamRequests[4].collectionID, + requestID: 'invalid', + nextRequestID: null, + }; + + mockPrisma.teamRequest.findFirst.mockResolvedValueOnce(null); + + const result = teamRequestService.findRequestAndNextRequest( + args.srcCollID, + args.requestID, + args.destCollID, + args.nextRequestID, + ); + + expect(result).resolves.toEqualLeft(TEAM_REQ_NOT_FOUND); + }); + test('Should resolve left if the nextRequest is not found', () => { + const args: MoveTeamRequestArgs = { + srcCollID: teamRequests[0].collectionID, + destCollID: teamRequests[1].collectionID, + requestID: teamRequests[0].id, + nextRequestID: 'invalid', + }; + + mockPrisma.teamRequest.findFirst + .mockResolvedValueOnce(dbTeamRequests[0]) + .mockResolvedValueOnce(null); + + const result = teamRequestService.findRequestAndNextRequest( + args.srcCollID, + args.requestID, + args.destCollID, + args.nextRequestID, + ); + + expect(result).resolves.toEqualLeft(TEAM_REQ_NOT_FOUND); + }); +}); + +describe('moveRequest', () => { + test('Should resolve right and the request', () => { + const args: MoveTeamRequestArgs = { + srcCollID: teamRequests[0].collectionID, + destCollID: teamRequests[0].collectionID, + requestID: teamRequests[0].id, + nextRequestID: null, + }; + + jest + .spyOn(teamRequestService, 'findRequestAndNextRequest') + .mockResolvedValue( + E.right({ request: dbTeamRequests[0], nextRequest: null }), + ); + jest + .spyOn(teamRequestService, 'reorderRequests') + .mockResolvedValue(E.right(dbTeamRequests[0])); + + const result = teamRequestService.moveRequest( + args.srcCollID, + args.requestID, + args.destCollID, + args.nextRequestID, + 'moveRequest', + ); + + expect(result).resolves.toEqualRight(teamRequests[0]); + }); + + test('Should resolve right and publish message to pubnub if callerFunction is moveRequest', async () => { + const args: MoveTeamRequestArgs = { + srcCollID: teamRequests[0].collectionID, + destCollID: teamRequests[0].collectionID, + requestID: teamRequests[0].id, + nextRequestID: null, + }; + + jest + .spyOn(teamRequestService, 'findRequestAndNextRequest') + .mockResolvedValue( + E.right({ request: dbTeamRequests[0], nextRequest: null }), + ); + jest + .spyOn(teamRequestService, 'reorderRequests') + .mockResolvedValue(E.right(dbTeamRequests[0])); + + await teamRequestService.moveRequest( + args.srcCollID, + args.requestID, + args.destCollID, + args.nextRequestID, + 'moveRequest', + ); + + expect(mockPubSub.publish).toHaveBeenCalledWith( + `team_req/${teamRequests[0].teamID}/req_moved`, + teamRequests[0], ); }); - describe('getRequestTO', () => { - test('should resolve to Some for valid collection ID', async () => { - mockPrisma.teamRequest.findUnique.mockResolvedValueOnce({ - id: 'testrequest', - collectionID: 'testcoll', - teamID: '3170', - request: '{}', - title: 'Test Request', - team: { - id: '3170', - name: 'Test team', - }, - } as any); + test('Should resolve right and publish message to pubnub if callerFunction is updateLookUpRequestOrder', async () => { + const args: MoveTeamRequestArgs = { + srcCollID: teamRequests[0].collectionID, + destCollID: teamRequests[0].collectionID, + requestID: teamRequests[0].id, + nextRequestID: null, + }; - expect(await teamRequestService.getRequestTO('testrequest')()).toBeSome(); - }); + jest + .spyOn(teamRequestService, 'findRequestAndNextRequest') + .mockResolvedValue( + E.right({ request: dbTeamRequests[0], nextRequest: null }), + ); + jest + .spyOn(teamRequestService, 'reorderRequests') + .mockResolvedValue(E.right(dbTeamRequests[0])); - test('should resolve to the correct Some value for a valid collection ID', async () => { - mockPrisma.teamRequest.findUnique.mockResolvedValueOnce({ - id: 'testrequest', - collectionID: 'testcoll', - teamID: '3170', - request: {}, - title: 'Test Request', - team: { - id: '3170', - name: 'Test team', - }, - } as any); + await teamRequestService.moveRequest( + args.srcCollID, + args.requestID, + args.destCollID, + args.nextRequestID, + 'updateLookUpRequestOrder', + ); - expect( - await teamRequestService.getRequestTO('testrequest')(), - ).toEqualSome({ - id: 'testrequest', - teamID: '3170', - collectionID: 'testcoll', - request: '{}', - title: 'Test Request', - }); - }); - - test('should resolve a None value if the the request ID does not exist', async () => { - mockPrisma.teamRequest.findUnique.mockResolvedValueOnce(null); - - expect(await teamRequestService.getRequestTO('testrequest')()).toBeNone(); - }); + expect(mockPubSub.publish).toHaveBeenCalledWith( + `team_req/${teamRequests[0].teamID}/req_order_updated`, + { request: teamRequests[0], nextRequest: null }, + ); }); - describe('moveRequest', () => { - test('resolves to right when the move was valid', async () => { - mockPrisma.teamRequest.findUnique.mockResolvedValueOnce({ - id: 'testrequest', - collectionID: 'testcoll', - teamID: '3170', - request: {}, - title: 'Test Request', - team: { - id: '3170', - name: 'Test team', - }, - } as any); + test('Should resolve left if finding the requests fails', () => { + const args: MoveTeamRequestArgs = { + srcCollID: teamRequests[0].collectionID, + destCollID: teamRequests[0].collectionID, + requestID: teamRequests[0].id, + nextRequestID: null, + }; - mockTeamCollectionService.getCollectionTO.mockImplementationOnce(() => - TO.some({ - id: 'testcoll2', - parentID: 'testcoll', - teamID: '3170', - title: 'Test Team', - }), + jest + .spyOn(teamRequestService, 'findRequestAndNextRequest') + .mockResolvedValue(E.left(TEAM_REQ_NOT_FOUND)); + + expect( + teamRequestService.moveRequest( + args.srcCollID, + args.requestID, + args.destCollID, + args.nextRequestID, + 'moveRequest', + ), + ).resolves.toEqualLeft(TEAM_REQ_NOT_FOUND); + }); + + test('Should resolve left if mismatch team/collection of requests fails', () => { + const args: MoveTeamRequestArgs = { + srcCollID: teamRequests[0].collectionID, + destCollID: teamRequests[0].collectionID, + requestID: teamRequests[0].id, + nextRequestID: null, + }; + + jest + .spyOn(teamRequestService, 'findRequestAndNextRequest') + .mockResolvedValue(E.left(TEAM_REQ_INVALID_TARGET_COLL_ID)); + + expect( + teamRequestService.moveRequest( + args.srcCollID, + args.requestID, + args.destCollID, + args.nextRequestID, + 'moveRequest', + ), + ).resolves.toEqualLeft(TEAM_REQ_INVALID_TARGET_COLL_ID); + }); + + test('Should resolve left if reorder fails', () => { + const args: MoveTeamRequestArgs = { + srcCollID: teamRequests[0].collectionID, + destCollID: teamRequests[0].collectionID, + requestID: teamRequests[0].id, + nextRequestID: null, + }; + + jest + .spyOn(teamRequestService, 'findRequestAndNextRequest') + .mockResolvedValue( + E.right({ request: dbTeamRequests[0], nextRequest: null }), ); - mockPrisma.teamRequest.update.mockResolvedValueOnce({ - id: 'testrequest', - collectionID: 'testcoll2', - teamID: '3170', - request: {}, - title: 'Test Request', - team: { - id: '3170', - name: 'Test team', - }, - } as any); + jest + .spyOn(teamRequestService, 'reorderRequests') + .mockResolvedValue(E.left(TEAM_REQ_REORDERING_FAILED)); - expect( - await teamRequestService.moveRequest('testrequest', 'testcoll2')(), - ).toBeRight(); - }); - - test('resolves to a left with TEAM_REQ_NOT_FOUND if the reqID is invalid', async () => { - mockPrisma.teamRequest.findUnique.mockResolvedValue(null); - - expect( - await teamRequestService.moveRequest( - 'invalidtestrequest', - 'testcoll2', - )(), - ).toEqualLeft(TEAM_REQ_NOT_FOUND); - }); - - test('resolves to the left with TEAM_REQ_INVALID_TARGET_COLL_ID if the collection is invalid', async () => { - mockPrisma.teamRequest.findUnique.mockResolvedValueOnce({ - id: 'testrequest', - collectionID: 'testcoll', - teamID: '3170', - request: {}, - title: 'Test Request', - team: { - id: '3170', - name: 'Test team', - }, - } as any); - - mockTeamCollectionService.getCollectionTO.mockImplementationOnce( - () => TO.none, - ); - - expect( - await teamRequestService.moveRequest( - 'testrequest', - 'invalidcollection', - )(), - ).toEqualLeft(TEAM_REQ_INVALID_TARGET_COLL_ID); - }); - - test('resolves to a left with TEAM_REQ_INVALID_TARGET_ID if the request and destination collection are not in the same team', async () => { - mockPrisma.teamRequest.findUnique.mockResolvedValueOnce({ - id: 'testrequest', - collectionID: 'testcoll', - teamID: '3170', - request: {}, - title: 'Test Request', - team: { - id: '3170', - name: 'Test team', - }, - } as any); - - mockTeamCollectionService.getCollectionTO.mockImplementationOnce(() => - TO.some({ - id: 'testcoll2', - parentID: 'testcoll', - teamID: 'differentteamID', - title: 'Test Team', - }), - ); - - expect( - await teamRequestService.moveRequest('testrequest', 'testcoll2')(), - ).toEqualLeft(TEAM_REQ_INVALID_TARGET_COLL_ID); - }); - - test('resolves to the right with the correct output model object for a valid query', async () => { - mockPrisma.teamRequest.findUnique.mockResolvedValueOnce({ - id: 'testrequest', - collectionID: 'testcoll', - teamID: '3170', - request: {}, - title: 'Test Request', - team: { - id: '3170', - name: 'Test team', - }, - } as any); - - mockTeamCollectionService.getCollectionTO.mockImplementationOnce(() => - TO.some({ - id: 'testcoll2', - parentID: 'testcoll', - teamID: '3170', - title: 'Test Team', - }), - ); - - mockPrisma.teamRequest.update.mockResolvedValueOnce({ - id: 'testrequest', - collectionID: 'testcoll2', - teamID: '3170', - request: {}, - title: 'Test Request', - team: { - id: '3170', - name: 'Test team', - }, - } as any); - - expect( - await teamRequestService.moveRequest('testrequest', 'testcoll2')(), - ).toEqualRight({ - id: 'testrequest', - collectionID: 'testcoll2', - request: '{}', - teamID: '3170', - title: 'Test Request', - }); - }); - - test('publishes to the pubsub on a valid move', async () => { - mockPubSub.publish.mockReset(); - - mockPrisma.teamRequest.findUnique.mockResolvedValueOnce({ - id: 'testrequest', - collectionID: 'testcoll', - teamID: '3170', - request: {}, - title: 'Test Request', - team: { - id: '3170', - name: 'Test team', - }, - } as any); - - mockTeamCollectionService.getCollectionTO.mockImplementationOnce(() => - TO.some({ - id: 'testcoll2', - parentID: 'testcoll', - teamID: '3170', - title: 'Test Team', - }), - ); - - mockPrisma.teamRequest.update.mockResolvedValueOnce({ - id: 'testrequest', - collectionID: 'testcoll2', - teamID: '3170', - request: {}, - title: 'Test Request', - team: { - id: '3170', - name: 'Test team', - }, - } as any); - - await teamRequestService.moveRequest('testrequest', 'testcoll2')(); - - expect(mockPubSub.publish).toHaveBeenCalledTimes(2); - expect(mockPubSub.publish).toHaveBeenNthCalledWith( - 1, - `team_req/3170/req_deleted`, - 'testrequest', - ); - expect(mockPubSub.publish).toHaveBeenNthCalledWith( - 2, - `team_req/3170/req_created`, - { - id: 'testrequest', - collectionID: 'testcoll2', - teamID: '3170', - request: '{}', - title: 'Test Request', - }, - ); - }); - - test('does not publish to the pubsub on an invalid move', async () => { - mockPubSub.publish.mockReset(); - - mockPrisma.teamRequest.findUnique.mockResolvedValueOnce({ - id: 'testrequest', - collectionID: 'testcoll', - teamID: '3170', - request: {}, - title: 'Test Request', - team: { - id: '3170', - name: 'Test team', - }, - } as any); - - mockTeamCollectionService.getCollectionTO.mockImplementationOnce( - () => TO.none, - ); - - await teamRequestService.moveRequest('testrequest', 'testcoll')(); - - expect(mockPubSub.publish).not.toHaveBeenCalled(); - }); + expect( + teamRequestService.moveRequest( + args.srcCollID, + args.requestID, + args.destCollID, + args.nextRequestID, + 'moveRequest', + ), + ).resolves.toEqualLeft(TEAM_REQ_REORDERING_FAILED); }); }); diff --git a/packages/hoppscotch-backend/src/team-request/team-request.service.ts b/packages/hoppscotch-backend/src/team-request/team-request.service.ts index e1345c96c..d03582f7b 100644 --- a/packages/hoppscotch-backend/src/team-request/team-request.service.ts +++ b/packages/hoppscotch-backend/src/team-request/team-request.service.ts @@ -1,26 +1,20 @@ import { Injectable } from '@nestjs/common'; import { TeamService } from '../team/team.service'; import { PrismaService } from '../prisma/prisma.service'; -import { - TeamRequest, - CreateTeamRequestInput, - UpdateTeamRequestInput, -} from './team-request.model'; -import { Team } from '../team/team.model'; +import { TeamRequest } from './team-request.model'; import { TeamCollectionService } from '../team-collection/team-collection.service'; -import { TeamCollection } from '../team-collection/team-collection.model'; import { TEAM_REQ_INVALID_TARGET_COLL_ID, TEAM_INVALID_COLL_ID, TEAM_INVALID_ID, TEAM_REQ_NOT_FOUND, + TEAM_REQ_REORDERING_FAILED, } from 'src/errors'; import { PubSubService } from 'src/pubsub/pubsub.service'; -import { throwErr } from 'src/utils'; -import { pipe } from 'fp-ts/function'; -import * as TO from 'fp-ts/TaskOption'; +import { stringToJson } from 'src/utils'; import * as E from 'fp-ts/Either'; -import { Prisma } from '@prisma/client'; +import * as O from 'fp-ts/Option'; +import { Prisma, TeamRequest as DbTeamRequest } from '@prisma/client'; @Injectable() export class TeamRequestService { @@ -31,299 +25,403 @@ export class TeamRequestService { private readonly pubsub: PubSubService, ) {} - async updateTeamRequest( - requestID: string, - input: UpdateTeamRequestInput, - ): Promise { - const updateData = {}; - if (input.request) updateData['request'] = JSON.parse(input.request); - if (input.title !== null) updateData['title'] = input.title; - - const data = await this.prisma.teamRequest.update({ - where: { - id: requestID, - }, - data: { - ...updateData, - }, - }); - - const result: TeamRequest = { - id: data.id, - collectionID: data.collectionID, - request: JSON.stringify(data.request), - title: data.title, - teamID: data.teamID, + /** + * A helper function to cast the Prisma TeamRequest model to the TeamRequest model + * @param tr TeamRequest model from Prisma + */ + private cast(tr: DbTeamRequest) { + return { + id: tr.id, + collectionID: tr.collectionID, + teamID: tr.teamID, + title: tr.title, + request: JSON.stringify(tr.request), }; - - this.pubsub.publish(`team_req/${data.teamID}/req_updated`, result); - - return result; } + /** + * Update team request + * @param requestID Request ID, which is updating + * @param title Title of the request + * @param request Request body of the request + */ + async updateTeamRequest(requestID: string, title: string, request: string) { + try { + const updateInput: Prisma.TeamRequestUpdateInput = { title }; + if (request) { + const jsonReq = stringToJson(request); + if (E.isLeft(jsonReq)) return E.left(jsonReq.left); + updateInput.request = jsonReq.right; + } + + const updatedTeamReq = await this.prisma.teamRequest.update({ + where: { id: requestID }, + data: updateInput, + }); + + const teamRequest: TeamRequest = this.cast(updatedTeamReq); + + this.pubsub.publish( + `team_req/${teamRequest.teamID}/req_updated`, + teamRequest, + ); + + return E.right(teamRequest); + } catch (e) { + return E.left(TEAM_REQ_NOT_FOUND); + } + } + + /** + * Search team requests + * @param teamID Team ID to search in + * @param searchTerm Search term for the request title + * @param cursor Cursor for pagination + * @param take Number of requests to fetch + */ async searchRequest( teamID: string, searchTerm: string, - cursor: string | null, - ): Promise { - if (!cursor) { - const data = await this.prisma.teamRequest.findMany({ - take: 10, - where: { - teamID, - title: { - contains: searchTerm, - }, - }, - }); - - return data.map((d) => { - return { - title: d.title, - id: d.id, - teamID: d.teamID, - collectionID: d.collectionID, - request: d.request!.toString(), - }; - }); - } else { - const data = await this.prisma.teamRequest.findMany({ - take: 10, - skip: 1, - cursor: { - id: cursor, - }, - where: { - teamID, - title: { - contains: searchTerm, - }, - }, - }); - - return data.map((d) => { - return { - title: d.title, - id: d.id, - teamID: d.teamID, - collectionID: d.collectionID, - request: d.request!.toString(), - }; - }); - } - } - - async deleteTeamRequest(requestID: string): Promise { - const req = await this.getRequest(requestID); - - if (!req) throw new Error(TEAM_REQ_NOT_FOUND); - - await this.prisma.teamRequest.delete({ + cursor: string, + take: number = 10, + ) { + const fetchedRequests = await this.prisma.teamRequest.findMany({ + take: take, + skip: cursor ? 1 : 0, + cursor: cursor ? { id: cursor } : undefined, where: { - id: requestID, + teamID: teamID, + title: { + contains: searchTerm, + }, }, }); - this.pubsub.publish(`team_req/${req.teamID}/req_deleted`, requestID); + const teamRequests = fetchedRequests.map((tr) => this.cast(tr)); + return teamRequests; } - // async createTeamRequest(collectionID: string, input: CreateTeamRequestInput) { - // const team = await this.teamCollectionService.getTeamOfCollection( - // collectionID, - // ); - // if (E.isLeft(team)) return []; + /** + * Delete team request + * @param requestID Request ID to delete + */ + async deleteTeamRequest(requestID: string) { + const dbTeamReq = await this.prisma.teamRequest.findFirst({ + where: { id: requestID }, + }); + if (!dbTeamReq) return E.left(TEAM_REQ_NOT_FOUND); - // const data = await this.prisma.teamRequest.create({ - // data: { - // team: { - // connect: { - // id: team.right.id, - // }, - // }, - // request: JSON.parse(input.request), - // title: input.title, - // collection: { - // connect: { - // id: collectionID, - // }, - // }, - // }, - // }); + await this.prisma.teamRequest.updateMany({ + where: { orderIndex: { gte: dbTeamReq.orderIndex } }, + data: { orderIndex: { decrement: 1 } }, + }); + await this.prisma.teamRequest.delete({ + where: { id: requestID }, + }); - // const result = { - // id: data.id, - // collectionID: data.collectionID, - // title: data.title, - // request: JSON.stringify(data.request), - // teamID: data.teamID, - // }; + this.pubsub.publish(`team_req/${dbTeamReq.teamID}/req_deleted`, requestID); - // this.pubsub.publish(`team_req/${result.teamID}/req_created`, result); + return E.right(true); + } - // return result; - // } + /** + * Create team request + * @param collectionID Collection ID to create the request in + * @param teamID Team ID to create the request in + * @param title Title of the request + * @param request Request body of the request + */ + async createTeamRequest( + collectionID: string, + teamID: string, + title: string, + request: string, + ) { + const team = await this.teamCollectionService.getTeamOfCollection( + collectionID, + ); + if (E.isLeft(team)) return E.left(team.left); + if (team.right.id !== teamID) return E.left(TEAM_INVALID_ID); + const reqCountInColl = await this.getRequestsCountInCollection( + collectionID, + ); + + const createInput: Prisma.TeamRequestCreateInput = { + request: request, + title: title, + orderIndex: reqCountInColl + 1, + team: { connect: { id: team.right.id } }, + collection: { connect: { id: collectionID } }, + }; + + if (request) { + const jsonReq = stringToJson(request); + if (E.isLeft(jsonReq)) return E.left(jsonReq.left); + createInput.request = jsonReq.right; + } + + const dbTeamRequest = await this.prisma.teamRequest.create({ + data: createInput, + }); + const teamRequest = this.cast(dbTeamRequest); + this.pubsub.publish( + `team_req/${teamRequest.teamID}/req_created`, + teamRequest, + ); + + return E.right(teamRequest); + } + + /** + * Fetch team requests by Collection ID + * @param collectionID Collection ID to fetch requests in + * @param cursor Cursor for pagination + * @param take Take number of requests + * @returns + */ async getRequestsInCollection( collectionID: string, - cursor: string | null, - ): Promise { - if (!cursor) { - const res = await this.prisma.teamRequest.findMany({ - take: 10, - where: { - collectionID, - }, - }); - - return res.map((e) => { - return { - id: e.id, - collectionID: e.collectionID, - teamID: e.teamID, - request: JSON.stringify(e.request), - title: e.title, - }; - }); - } else { - const res = await this.prisma.teamRequest.findMany({ - cursor: { - id: cursor, - }, - take: 10, - skip: 1, - where: { - collectionID, - }, - }); - - return res.map((e) => { - return { - id: e.id, - collectionID: e.collectionID, - teamID: e.teamID, - request: JSON.stringify(e.request), - title: e.title, - }; - }); - } - } - - async getRequest(reqID: string): Promise { - const res = await this.prisma.teamRequest.findUnique({ + cursor: string, + take: number = 10, + ) { + const dbTeamRequests = await this.prisma.teamRequest.findMany({ + cursor: cursor ? { id: cursor } : undefined, + take: take, + skip: cursor ? 1 : 0, where: { - id: reqID, + collectionID: collectionID, }, }); - if (!res) return null; - - return { - id: res.id, - teamID: res.teamID, - collectionID: res.collectionID, - request: JSON.stringify(res.request), - title: res.title, - }; + const teamRequests = dbTeamRequests.map((tr) => this.cast(tr)); + return teamRequests; } - getRequestTO(reqID: string): TO.TaskOption { - return pipe( - TO.fromTask(() => this.getRequest(reqID)), - TO.chain(TO.fromNullable), + /** + * Fetch team request by ID + * @param reqID Request ID to fetch + */ + async getRequest(reqID: string) { + try { + const teamRequest = await this.prisma.teamRequest.findUnique({ + where: { id: reqID }, + }); + return O.some(this.cast(teamRequest)); + } catch (e) { + return O.none; + } + } + + /** + * Fetch team by team request + * @param teamRequest Team Request to fetch + */ + async getTeamOfRequest(req: TeamRequest) { + const team = await this.teamService.getTeamWithID(req.teamID); + if (!team) return E.left(TEAM_INVALID_ID); + return E.right(team); + } + + /** + * Fetch team collection by team request + * @param teamRequest Team Request to fetch + */ + async getCollectionOfRequest(req: TeamRequest) { + const teamCollection = await this.teamCollectionService.getCollection( + req.collectionID, ); + if (!teamCollection) return E.left(TEAM_INVALID_COLL_ID); + return E.right(teamCollection); } - async getTeamOfRequest(req: TeamRequest): Promise { - return ( - (await this.teamService.getTeamWithID(req.teamID)) ?? - throwErr(TEAM_INVALID_ID) + /** + * Fetch team by team request ID + * @param reqID Team Request ID to fetch + */ + async getTeamOfRequestFromID(reqID: string) { + const teamRequest = await this.prisma.teamRequest.findUnique({ + where: { id: reqID }, + include: { team: true }, + }); + if (!teamRequest?.team) return O.none; + return O.some(teamRequest.team); + } + + /** + * Move or re-order a request to same/another collection + * @param srcCollID Collection ID, where the request is currently in. For backward compatibility, srcCollID is optional (can be undefined) + * @param requestID ID of the request to be moved + * @param destCollID Collection ID, where the request is to be moved to + * @param nextRequestID ID of the request, which is after the request to be moved. If the request is to be moved to the end of the collection, nextRequestID should be null + */ + async moveRequest( + srcCollID: string, + requestID: string, + destCollID: string, + nextRequestID: string, + callerFunction: 'moveRequest' | 'updateLookUpRequestOrder', + ) { + // step 1: validation and find the request and next request + const twoRequests = await this.findRequestAndNextRequest( + srcCollID, + requestID, + destCollID, + nextRequestID, ); + if (E.isLeft(twoRequests)) return E.left(twoRequests.left); + const { request, nextRequest } = twoRequests.right; + + if (!srcCollID) srcCollID = request.collectionID; // if srcCollID is not provided (for backward compatibility), use the collectionID of the request + + // step 2: perform reordering + const updatedRequest = await this.reorderRequests( + request, + srcCollID, + nextRequest, + destCollID, + ); + if (E.isLeft(updatedRequest)) return E.left(updatedRequest.left); + + const teamReq = this.cast(updatedRequest.right); + + // step 3: publish the event + if (callerFunction === 'moveRequest') { + this.pubsub.publish(`team_req/${teamReq.teamID}/req_moved`, teamReq); + } else if (callerFunction === 'updateLookUpRequestOrder') { + this.pubsub.publish(`team_req/${request.teamID}/req_order_updated`, { + request: this.cast(updatedRequest.right), + nextRequest: nextRequest ? this.cast(nextRequest) : null, + }); + } + + return E.right(teamReq); } - // async getCollectionOfRequest(req: TeamRequest): Promise { - // return ( - // (await this.teamCollectionService.getCollection(req.collectionID)) ?? - // throwErr(TEAM_INVALID_COLL_ID) - // ); - // } + /** + * A helper function to find the request and next request + * @param srcCollID Collection ID, where the request is currently in + * @param requestID ID of the request to be moved + * @param destCollID Collection ID, where the request is to be moved to + * @param nextRequestID ID of the request, which is after the request to be moved. If the request is to be moved to the end of the collection, nextRequestID should be null + */ + async findRequestAndNextRequest( + srcCollID: string, + requestID: string, + destCollID: string, + nextRequestID: string, + ) { + const request = await this.prisma.teamRequest.findFirst({ + where: { id: requestID, collectionID: srcCollID }, + }); + if (!request) return E.left(TEAM_REQ_NOT_FOUND); - async getTeamOfRequestFromID(reqID: string): Promise { - const req = - (await this.prisma.teamRequest.findUnique({ - where: { - id: reqID, - }, - include: { - team: true, - }, - })) ?? throwErr(TEAM_REQ_NOT_FOUND); + let nextRequest = null; + if (nextRequestID) { + nextRequest = await this.prisma.teamRequest.findFirst({ + where: { id: nextRequestID }, + }); + if (!nextRequest) return E.left(TEAM_REQ_NOT_FOUND); - return req.team; + if ( + nextRequest.collectionID !== destCollID || + request.teamID !== nextRequest.teamID + ) { + return E.left(TEAM_REQ_INVALID_TARGET_COLL_ID); + } + } + + return E.right({ request, nextRequest }); } - // moveRequest(reqID: string, destinationCollID: string) { - // return pipe( - // TE.Do, + /** + * A helper function to get the number of requests in a collection + * @param collectionID Collection ID to fetch + */ + async getRequestsCountInCollection(collectionID: string) { + return this.prisma.teamRequest.count({ + where: { collectionID }, + }); + } - // // Check if the request exists - // TE.bind('request', () => - // pipe( - // this.getRequestTO(reqID), - // TE.fromTaskOption(() => TEAM_REQ_NOT_FOUND), - // ), - // ), + /** + * A helper function to reorder requests + * @param request The request to be moved + * @param srcCollID Collection ID, where the request is currently in + * @param nextRequest The request, which is after the request to be moved. If the request is to be moved to the end of the collection, nextRequest should be null + * @param destCollID Collection ID, where the request is to be moved to + */ + async reorderRequests( + request: DbTeamRequest, + srcCollID: string, + nextRequest: DbTeamRequest, + destCollID: string, + ) { + try { + return await this.prisma.$transaction< + E.Left | E.Right + >(async (tx) => { + const isSameCollection = srcCollID === destCollID; + const isMovingUp = nextRequest?.orderIndex < request.orderIndex; // false, if nextRequest is null - // // Check if the destination collection exists (or null) - // TE.bindW('targetCollection', () => - // pipe( - // this.teamCollectionService.getCollectionTO(destinationCollID), - // TE.fromTaskOption(() => TEAM_REQ_INVALID_TARGET_COLL_ID), - // ), - // ), + const nextReqOrderIndex = nextRequest?.orderIndex; + const reqCountInDestColl = nextRequest + ? undefined + : await this.getRequestsCountInCollection(destCollID); - // // Block operation if target collection is not part of the same team - // // as the request - // TE.chainW( - // TE.fromPredicate( - // ({ request, targetCollection }) => - // request.teamID === targetCollection.teamID, - // () => TEAM_REQ_INVALID_TARGET_COLL_ID, - // ), - // ), + // Updating order indexes of other requests in collection(s) + if (isSameCollection) { + const updateFrom = isMovingUp + ? nextReqOrderIndex + : request.orderIndex + 1; + const updateTo = isMovingUp ? request.orderIndex : nextReqOrderIndex; - // // Update the collection - // TE.chain(({ request, targetCollection }) => - // TE.fromTask(() => - // this.prisma.teamRequest.update({ - // where: { - // id: request.id, - // }, - // data: { - // collectionID: targetCollection.id, - // }, - // }), - // ), - // ), + await tx.teamRequest.updateMany({ + where: { + collectionID: srcCollID, + orderIndex: { gte: updateFrom, lt: updateTo }, + }, + data: { + orderIndex: isMovingUp ? { increment: 1 } : { decrement: 1 }, + }, + }); + } else { + await tx.teamRequest.updateMany({ + where: { + collectionID: srcCollID, + orderIndex: { gte: request.orderIndex }, + }, + data: { orderIndex: { decrement: 1 } }, + }); - // // Generate TeamRequest model object - // TE.map( - // (request) => - // { - // id: request.id, - // collectionID: request.collectionID, - // request: JSON.stringify(request.request), - // teamID: request.teamID, - // title: request.title, - // }, - // ), + if (nextRequest) { + await tx.teamRequest.updateMany({ + where: { + collectionID: destCollID, + orderIndex: { gte: nextReqOrderIndex }, + }, + data: { orderIndex: { increment: 1 } }, + }); + } + } - // // Update on PubSub - // TE.chainFirst((req) => { - // this.pubsub.publish(`team_req/${req.teamID}/req_deleted`, req.id); - // this.pubsub.publish(`team_req/${req.teamID}/req_created`, req); + // Updating order index of the request + let adjust: number; + if (isSameCollection) adjust = nextRequest ? (isMovingUp ? 0 : -1) : 0; + else adjust = nextRequest ? 0 : 1; - // return TE.of({}); // We don't care about the return type - // }), - // ); - // } + const newOrderIndex = + (nextReqOrderIndex ?? reqCountInDestColl) + adjust; + + const updatedRequest = await tx.teamRequest.update({ + where: { id: request.id }, + data: { orderIndex: newOrderIndex, collectionID: destCollID }, + }); + + return E.right(updatedRequest); + }); + } catch (err) { + return E.left(TEAM_REQ_REORDERING_FAILED); + } + } }