refactor: Refactoring of Team Request with Reordering (HBE-151) (#20)

* feat: createdOn, updatedOn added in team-request schema and updateTeamReq resolver refactored

* feat: resolver name changed for updateTeamRequest

* refactor: searchForTeamRequest resolver

* refactor: some functions refactored

* refactor: team-request service and subscriptions

* refactor: update GqlRequestTeamMemberGuard

* feat: team request reordering add

* feat: handle exception on update Team Request

* chore: change some return statement

* fix: change field name of MoveTeamRequestArgs

* feat: publish message update for reorder team req

* test: fix all the exists cases

* fix: add return statement

* test: add few functions test cases

* feat: made backward compatible

* fix: team-member guard for retrive user

* fix: few bugs

* chore: destructured parameters in service methods

* test: fix test cases

* feat: updateLookUpRequestOrder resolver added

* test: fix test cases

* chore: improved code consistency

* fix: feedback changes

* fix: main changes
This commit is contained in:
Mir Arif Hasan
2023-03-09 20:59:39 +06:00
committed by GitHub
parent 2a715d5348
commit 7d3b2c064a
8 changed files with 1159 additions and 1347 deletions

View File

@@ -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)

View File

@@ -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;

View File

@@ -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 {
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;
}
}

View File

@@ -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;
}

View File

@@ -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;
}

View File

@@ -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<Team> {
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<TeamCollection> {
// 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<TeamRequest[]> {
@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<TeamRequest | null> {
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<TeamRequest[]> {
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<TeamRequest> {
// 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<TeamRequest> {
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<boolean> {
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<TeamRequest> {
// 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<TeamRequest> {
) {
return this.pubsub.asyncIterator(`team_req/${teamID}/req_created`);
}
@@ -254,7 +276,7 @@ export class TeamRequestResolver {
type: () => ID,
})
teamID: string,
): AsyncIterator<TeamRequest> {
) {
return this.pubsub.asyncIterator(`team_req/${teamID}/req_updated`);
}
@@ -276,7 +298,51 @@ export class TeamRequestResolver {
type: () => ID,
})
teamID: string,
): AsyncIterator<string> {
) {
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`);
}
}

View File

@@ -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<TeamRequest> {
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<TeamRequest[]> {
if (!cursor) {
const data = await this.prisma.teamRequest.findMany({
take: 10,
cursor: string,
take: number = 10,
) {
const fetchedRequests = await this.prisma.teamRequest.findMany({
take: take,
skip: cursor ? 1 : 0,
cursor: cursor ? { id: cursor } : undefined,
where: {
teamID,
teamID: 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(),
};
});
}
const teamRequests = fetchedRequests.map((tr) => this.cast(tr));
return teamRequests;
}
async deleteTeamRequest(requestID: string): Promise<void> {
const req = await this.getRequest(requestID);
if (!req) throw new Error(TEAM_REQ_NOT_FOUND);
/**
* 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);
await this.prisma.teamRequest.updateMany({
where: { orderIndex: { gte: dbTeamReq.orderIndex } },
data: { orderIndex: { decrement: 1 } },
});
await this.prisma.teamRequest.delete({
where: {
id: requestID,
},
where: { id: requestID },
});
this.pubsub.publish(`team_req/${req.teamID}/req_deleted`, requestID);
this.pubsub.publish(`team_req/${dbTeamReq.teamID}/req_deleted`, requestID);
return E.right(true);
}
// async createTeamRequest(collectionID: string, input: CreateTeamRequestInput) {
// const team = await this.teamCollectionService.getTeamOfCollection(
// collectionID,
// );
// if (E.isLeft(team)) return [];
/**
* 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 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,
// },
// },
// },
// });
const reqCountInColl = await this.getRequestsCountInCollection(
collectionID,
);
// const result = {
// id: data.id,
// collectionID: data.collectionID,
// title: data.title,
// request: JSON.stringify(data.request),
// teamID: data.teamID,
// };
const createInput: Prisma.TeamRequestCreateInput = {
request: request,
title: title,
orderIndex: reqCountInColl + 1,
team: { connect: { id: team.right.id } },
collection: { connect: { id: collectionID } },
};
// this.pubsub.publish(`team_req/${result.teamID}/req_created`, result);
if (request) {
const jsonReq = stringToJson(request);
if (E.isLeft(jsonReq)) return E.left(jsonReq.left);
createInput.request = jsonReq.right;
}
// return result;
// }
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<TeamRequest[]> {
if (!cursor) {
const res = await this.prisma.teamRequest.findMany({
take: 10,
cursor: string,
take: number = 10,
) {
const dbTeamRequests = await this.prisma.teamRequest.findMany({
cursor: cursor ? { id: cursor } : undefined,
take: take,
skip: cursor ? 1 : 0,
where: {
collectionID,
collectionID: collectionID,
},
});
return res.map((e) => {
return {
id: e.id,
collectionID: e.collectionID,
teamID: e.teamID,
request: JSON.stringify(e.request),
title: e.title,
};
const teamRequests = dbTeamRequests.map((tr) => this.cast(tr));
return teamRequests;
}
/**
* 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);
}
/**
* 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);
}
/**
* 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);
let nextRequest = null;
if (nextRequestID) {
nextRequest = await this.prisma.teamRequest.findFirst({
where: { id: nextRequestID },
});
if (!nextRequest) return E.left(TEAM_REQ_NOT_FOUND);
if (
nextRequest.collectionID !== destCollID ||
request.teamID !== nextRequest.teamID
) {
return E.left(TEAM_REQ_INVALID_TARGET_COLL_ID);
}
}
return E.right({ request, nextRequest });
}
/**
* 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 },
});
}
/**
* 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<string> | E.Right<DbTeamRequest>
>(async (tx) => {
const isSameCollection = srcCollID === destCollID;
const isMovingUp = nextRequest?.orderIndex < request.orderIndex; // false, if nextRequest is null
const nextReqOrderIndex = nextRequest?.orderIndex;
const reqCountInDestColl = nextRequest
? undefined
: await this.getRequestsCountInCollection(destCollID);
// Updating order indexes of other requests in collection(s)
if (isSameCollection) {
const updateFrom = isMovingUp
? nextReqOrderIndex
: request.orderIndex + 1;
const updateTo = isMovingUp ? request.orderIndex : nextReqOrderIndex;
await tx.teamRequest.updateMany({
where: {
collectionID: srcCollID,
orderIndex: { gte: updateFrom, lt: updateTo },
},
data: {
orderIndex: isMovingUp ? { increment: 1 } : { decrement: 1 },
},
});
} else {
const res = await this.prisma.teamRequest.findMany({
cursor: {
id: cursor,
},
take: 10,
skip: 1,
await tx.teamRequest.updateMany({
where: {
collectionID,
collectionID: srcCollID,
orderIndex: { gte: request.orderIndex },
},
data: { orderIndex: { decrement: 1 } },
});
return res.map((e) => {
return {
id: e.id,
collectionID: e.collectionID,
teamID: e.teamID,
request: JSON.stringify(e.request),
title: e.title,
};
if (nextRequest) {
await tx.teamRequest.updateMany({
where: {
collectionID: destCollID,
orderIndex: { gte: nextReqOrderIndex },
},
data: { orderIndex: { increment: 1 } },
});
}
}
async getRequest(reqID: string): Promise<TeamRequest | null> {
const res = await this.prisma.teamRequest.findUnique({
where: {
id: reqID,
},
// Updating order index of the request
let adjust: number;
if (isSameCollection) adjust = nextRequest ? (isMovingUp ? 0 : -1) : 0;
else adjust = nextRequest ? 0 : 1;
const newOrderIndex =
(nextReqOrderIndex ?? reqCountInDestColl) + adjust;
const updatedRequest = await tx.teamRequest.update({
where: { id: request.id },
data: { orderIndex: newOrderIndex, collectionID: destCollID },
});
if (!res) return null;
return {
id: res.id,
teamID: res.teamID,
collectionID: res.collectionID,
request: JSON.stringify(res.request),
title: res.title,
};
return E.right(updatedRequest);
});
} catch (err) {
return E.left(TEAM_REQ_REORDERING_FAILED);
}
getRequestTO(reqID: string): TO.TaskOption<TeamRequest> {
return pipe(
TO.fromTask(() => this.getRequest(reqID)),
TO.chain(TO.fromNullable),
);
}
async getTeamOfRequest(req: TeamRequest): Promise<Team> {
return (
(await this.teamService.getTeamWithID(req.teamID)) ??
throwErr(TEAM_INVALID_ID)
);
}
// async getCollectionOfRequest(req: TeamRequest): Promise<TeamCollection> {
// return (
// (await this.teamCollectionService.getCollection(req.collectionID)) ??
// throwErr(TEAM_INVALID_COLL_ID)
// );
// }
async getTeamOfRequestFromID(reqID: string): Promise<Team> {
const req =
(await this.prisma.teamRequest.findUnique({
where: {
id: reqID,
},
include: {
team: true,
},
})) ?? throwErr(TEAM_REQ_NOT_FOUND);
return req.team;
}
// moveRequest(reqID: string, destinationCollID: string) {
// return pipe(
// TE.Do,
// // Check if the request exists
// TE.bind('request', () =>
// pipe(
// this.getRequestTO(reqID),
// TE.fromTaskOption(() => TEAM_REQ_NOT_FOUND),
// ),
// ),
// // Check if the destination collection exists (or null)
// TE.bindW('targetCollection', () =>
// pipe(
// this.teamCollectionService.getCollectionTO(destinationCollID),
// TE.fromTaskOption(() => TEAM_REQ_INVALID_TARGET_COLL_ID),
// ),
// ),
// // 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,
// ),
// ),
// // Update the collection
// TE.chain(({ request, targetCollection }) =>
// TE.fromTask(() =>
// this.prisma.teamRequest.update({
// where: {
// id: request.id,
// },
// data: {
// collectionID: targetCollection.id,
// },
// }),
// ),
// ),
// // Generate TeamRequest model object
// TE.map(
// (request) =>
// <TeamRequest>{
// id: request.id,
// collectionID: request.collectionID,
// request: JSON.stringify(request.request),
// teamID: request.teamID,
// title: request.title,
// },
// ),
// // 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);
// return TE.of({}); // We don't care about the return type
// }),
// );
// }
}