Files
hoppscotch/packages/hoppscotch-backend/src/team/team.service.ts
Mir Arif Hasan 696cf8490b refactor: removing unused import, commented codes, improved cursor query (#69)
* chore: refactor code in some modules

* refactor: getTeamsOfUser functino

* chore: remove unused import

* chore: revert some changes
2023-04-06 19:54:10 +05:30

534 lines
12 KiB
TypeScript

import { Injectable, OnModuleInit } from '@nestjs/common';
import { TeamMember, TeamMemberRole, Team } from './team.model';
import { PrismaService } from '../prisma/prisma.service';
import { TeamMember as DbTeamMember } from '@prisma/client';
import { UserService } from '../user/user.service';
import { UserDataHandler } from 'src/user/user.data.handler';
import {
TEAM_NAME_INVALID,
TEAM_ONLY_ONE_OWNER,
USER_NOT_FOUND,
TEAM_INVALID_ID,
TEAM_INVALID_ID_OR_USER,
TEAM_MEMBER_NOT_FOUND,
USER_IS_OWNER,
} from '../errors';
import { PubSubService } from '../pubsub/pubsub.service';
import { flow, pipe } from 'fp-ts/function';
import * as TE from 'fp-ts/TaskEither';
import * as TO from 'fp-ts/TaskOption';
import * as O from 'fp-ts/Option';
import * as E from 'fp-ts/Either';
import * as T from 'fp-ts/Task';
import * as A from 'fp-ts/Array';
import { throwErr } from 'src/utils';
import { AuthUser } from '../types/AuthUser';
@Injectable()
export class TeamService implements UserDataHandler, OnModuleInit {
constructor(
private readonly prisma: PrismaService,
private readonly userService: UserService,
private readonly pubsub: PubSubService,
) {}
onModuleInit() {
this.userService.registerUserDataHandler(this);
}
canAllowUserDeletion(user: AuthUser): TO.TaskOption<string> {
return pipe(
this.isUserOwnerRoleInTeams(user.uid),
TO.fromTask,
TO.chain((isOwner) => (isOwner ? TO.some(USER_IS_OWNER) : TO.none)),
);
}
onUserDelete(user: AuthUser): T.Task<void> {
return this.deleteUserFromAllTeams(user.uid);
}
async getCountOfUsersWithRoleInTeam(
teamID: string,
role: TeamMemberRole,
): Promise<number> {
return await this.prisma.teamMember.count({
where: {
teamID,
role,
},
});
}
async addMemberToTeamWithEmail(
teamID: string,
email: string,
role: TeamMemberRole,
): Promise<E.Left<string> | E.Right<TeamMember>> {
const user = await this.userService.findUserByEmail(email);
if (O.isNone(user)) return E.left(USER_NOT_FOUND);
const teamMember = await this.addMemberToTeam(teamID, user.value.uid, role);
return E.right(teamMember);
}
async addMemberToTeam(
teamID: string,
uid: string,
role: TeamMemberRole,
): Promise<TeamMember> {
const teamMember = await this.prisma.teamMember.create({
data: {
userUid: uid,
team: {
connect: {
id: teamID,
},
},
role: role,
},
});
const member: TeamMember = {
membershipID: teamMember.id,
userUid: teamMember.userUid,
role: TeamMemberRole[teamMember.role],
};
this.pubsub.publish(`team/${teamID}/member_added`, member);
return member;
}
async deleteTeam(teamID: string): Promise<E.Left<string> | E.Right<boolean>> {
const team = await this.prisma.team.findUnique({
where: {
id: teamID,
},
});
if (!team) return E.left(TEAM_INVALID_ID);
await this.prisma.teamMember.deleteMany({
where: {
teamID: teamID,
},
});
await this.prisma.team.delete({
where: {
id: teamID,
},
});
return E.right(true);
}
validateTeamName(title: string): E.Left<string> | E.Right<boolean> {
if (!title || title.length < 6) return E.left(TEAM_NAME_INVALID);
return E.right(true);
}
async renameTeam(
teamID: string,
newName: string,
): Promise<E.Left<string> | E.Right<Team>> {
const isValidTitle = this.validateTeamName(newName);
if (E.isLeft(isValidTitle)) return isValidTitle;
try {
const updatedTeam = await this.prisma.team.update({
where: {
id: teamID,
},
data: {
name: newName,
},
});
return E.right(updatedTeam);
} catch (e) {
// Prisma update errors out if it can't find the record
return E.left(TEAM_INVALID_ID);
}
}
async updateTeamMemberRole(
teamID: string,
userUid: string,
newRole: TeamMemberRole,
): Promise<E.Left<string> | E.Right<TeamMember>> {
const ownerCount = await this.prisma.teamMember.count({
where: {
teamID,
role: TeamMemberRole.OWNER,
},
});
const member = await this.prisma.teamMember.findUnique({
where: {
teamID_userUid: {
teamID,
userUid,
},
},
});
if (!member) return E.left(TEAM_MEMBER_NOT_FOUND);
if (
member.role === TeamMemberRole.OWNER &&
newRole != TeamMemberRole.OWNER &&
ownerCount === 1
) {
return E.left(TEAM_ONLY_ONE_OWNER);
}
const result = await this.prisma.teamMember.update({
where: {
teamID_userUid: {
teamID,
userUid,
},
},
data: {
role: newRole,
},
});
const updatedMember: TeamMember = {
membershipID: result.id,
userUid: result.userUid,
role: TeamMemberRole[result.role],
};
this.pubsub.publish(`team/${teamID}/member_updated`, updatedMember);
return E.right(updatedMember);
}
async leaveTeam(
teamID: string,
userUid: string,
): Promise<E.Left<string> | E.Right<boolean>> {
const ownerCount = await this.prisma.teamMember.count({
where: {
teamID,
role: TeamMemberRole.OWNER,
},
});
const member = await this.getTeamMember(teamID, userUid);
if (!member) return E.left(TEAM_INVALID_ID_OR_USER);
if (ownerCount === 1 && member.role === TeamMemberRole.OWNER) {
return E.left(TEAM_ONLY_ONE_OWNER);
}
try {
await this.prisma.teamMember.delete({
where: {
teamID_userUid: {
userUid,
teamID,
},
},
});
} catch (e) {
// Record not found
return E.left(TEAM_INVALID_ID_OR_USER);
}
this.pubsub.publish(`team/${teamID}/member_removed`, userUid);
return E.right(true);
}
async createTeam(
name: string,
creatorUid: string,
): Promise<E.Left<string> | E.Right<Team>> {
const isValidName = this.validateTeamName(name);
if (E.isLeft(isValidName)) return isValidName;
const team = await this.prisma.team.create({
data: {
name: name,
members: {
create: {
userUid: creatorUid,
role: TeamMemberRole.OWNER,
},
},
},
});
return E.right(team);
}
async getTeamsOfUser(uid: string, cursor: string | null): Promise<Team[]> {
if (!cursor) {
const entries = await this.prisma.teamMember.findMany({
take: 10,
where: {
userUid: uid,
},
include: {
team: true,
},
});
return entries.map((entry) => entry.team);
} else {
const entries = await this.prisma.teamMember.findMany({
take: 10,
skip: 1,
cursor: {
teamID_userUid: {
teamID: cursor,
userUid: uid,
},
},
where: {
userUid: uid,
},
include: {
team: true,
},
});
return entries.map((entry) => entry.team);
}
}
async getTeamWithID(teamID: string): Promise<Team | null> {
try {
const team = await this.prisma.team.findUnique({
where: {
id: teamID,
},
});
return team;
} catch (_e) {
return null;
}
}
getTeamWithIDTE(teamID: string): TE.TaskEither<'team/invalid_id', Team> {
return pipe(
() => this.getTeamWithID(teamID),
TE.fromTask,
TE.chain(
TE.fromPredicate(
(x): x is Team => !!x,
() => TEAM_INVALID_ID,
),
),
);
}
/**
* Filters out team members that we weren't able to match
* (also deletes the membership)
* @param members Members to filter against
*/
async filterMismatchedUsers(
teamID: string,
members: TeamMember[],
): Promise<TeamMember[]> {
const memberUsers = await Promise.all(
members.map(async (member) => {
const user = await this.userService.findUserById(member.userUid);
// // TODO:Investigate if a race condition exists that deletes user from teams.
// // Delete the membership if the user doesnt exist
// if (!user) this.leaveTeam(teamID, member.userUid);
if (O.isSome(user)) return member;
else return null;
}),
);
return memberUsers.filter((x) => x !== null) as TeamMember[];
}
async getTeamMember(
teamID: string,
userUid: string,
): Promise<TeamMember | null> {
try {
const teamMember = await this.prisma.teamMember.findUnique({
where: {
teamID_userUid: {
teamID,
userUid,
},
},
});
if (!teamMember) return null;
return <TeamMember>{
membershipID: teamMember.id,
userUid: userUid,
role: TeamMemberRole[teamMember.role],
};
} catch (e) {
return null;
}
}
getTeamMemberTE(teamID: string, userUid: string) {
return pipe(
() => this.getTeamMember(teamID, userUid),
TE.fromTask,
TE.chain(
TE.fromPredicate(
(x): x is TeamMember => !!x,
() => TEAM_MEMBER_NOT_FOUND,
),
),
);
}
async getRoleOfUserInTeam(
teamID: string,
userUid: string,
): Promise<TeamMemberRole | null> {
const teamMember = await this.getTeamMember(teamID, userUid);
return teamMember ? teamMember.role : null;
}
isUserOwnerRoleInTeams(uid: string): T.Task<boolean> {
return pipe(
() =>
this.prisma.teamMember.count({
where: {
userUid: uid,
role: TeamMemberRole.OWNER,
},
take: 1,
}),
T.map((count) => count > 0),
);
}
deleteUserFromAllTeams(uid: string) {
return pipe(
() =>
this.prisma.teamMember.findMany({
where: {
userUid: uid,
},
}),
T.chainFirst(
flow(
A.map((member) => async () => {
const res = await this.leaveTeam(member.teamID, uid);
if (E.isLeft(res)) throwErr(res.left);
return E.right(res);
}),
T.sequenceArray,
),
),
T.map(() => undefined),
);
}
async getTeamMembers(teamID: string): Promise<TeamMember[]> {
const dbTeamMembers = await this.prisma.teamMember.findMany({
where: {
teamID,
},
});
const members = dbTeamMembers.map(
(entry) =>
<TeamMember>{
membershipID: entry.id,
userUid: entry.userUid,
role: TeamMemberRole[entry.role],
},
);
return this.filterMismatchedUsers(teamID, members);
}
/**
* Get a count of members in a team
* @param teamID Team ID
* @returns a count of members in a team
*/
async getCountOfMembersInTeam(teamID: string) {
const memberCount = await this.prisma.teamMember.count({
where: {
teamID: teamID,
},
});
return memberCount;
}
async getMembersOfTeam(
teamID: string,
cursor: string | null,
): Promise<TeamMember[]> {
let teamMembers: DbTeamMember[];
if (!cursor) {
teamMembers = await this.prisma.teamMember.findMany({
take: 10,
where: {
teamID,
},
});
} else {
teamMembers = await this.prisma.teamMember.findMany({
take: 10,
skip: 1,
cursor: {
id: cursor,
},
where: {
teamID,
},
});
}
const members = teamMembers.map(
(entry) =>
<TeamMember>{
membershipID: entry.id,
userUid: entry.userUid,
role: TeamMemberRole[entry.role],
},
);
return this.filterMismatchedUsers(teamID, members);
}
/**
* Fetch all the teams in the `Team` table based on cursor
* @param cursorID string of teamID or undefined
* @param take number of items to query
* @returns an array of `Team` object
*/
async fetchAllTeams(cursorID: string, take: number) {
const options = {
skip: cursorID ? 1 : 0,
take: take,
cursor: cursorID ? { id: cursorID } : undefined,
};
const fetchedTeams = await this.prisma.team.findMany(options);
return fetchedTeams;
}
/**
* Fetch list of all the Teams in the DB
*
* @returns number of teams in the org
*/
async getTeamsCount() {
const teamsCount = await this.prisma.team.count();
return teamsCount;
}
}