feat: team module added
This commit is contained in:
@@ -8,6 +8,7 @@ import { UserSettingsModule } from './user-settings/user-settings.module';
|
||||
import { UserEnvironmentsModule } from './user-environment/user-environments.module';
|
||||
import { UserHistoryModule } from './user-history/user-history.module';
|
||||
import { subscriptionContextCookieParser } from './auth/helper';
|
||||
import { TeamModule } from './team/team.module';
|
||||
|
||||
@Module({
|
||||
imports: [
|
||||
@@ -45,6 +46,7 @@ import { subscriptionContextCookieParser } from './auth/helper';
|
||||
UserSettingsModule,
|
||||
UserEnvironmentsModule,
|
||||
UserHistoryModule,
|
||||
TeamModule,
|
||||
],
|
||||
providers: [GQLComplexityPlugin],
|
||||
})
|
||||
|
||||
@@ -2,6 +2,7 @@ import { User } from 'src/user/user.model';
|
||||
import { UserSettings } from 'src/user-settings/user-settings.model';
|
||||
import { UserEnvironment } from '../user-environment/user-environments.model';
|
||||
import { UserHistory } from '../user-history/user-history.model';
|
||||
import { TeamMember } from 'src/team/team.model';
|
||||
|
||||
// A custom message type that defines the topic and the corresponding payload.
|
||||
// For every module that publishes a subscription add its type def and the possible subscription type.
|
||||
@@ -15,5 +16,8 @@ export type TopicDef = {
|
||||
[
|
||||
topic: `user_history/${string}/${'created' | 'updated' | 'deleted'}`
|
||||
]: UserHistory;
|
||||
[topic: `team/${string}/member_removed`]: string;
|
||||
[topic: `team/${string}/member_added`]: TeamMember;
|
||||
[topic: `team/${string}/member_updated`]: TeamMember;
|
||||
[topic: `user_history/${string}/deleted_many`]: number;
|
||||
};
|
||||
|
||||
@@ -0,0 +1,5 @@
|
||||
import { TeamMemberRole } from '@prisma/client';
|
||||
import { SetMetadata } from '@nestjs/common';
|
||||
|
||||
export const RequiresTeamRole = (...roles: TeamMemberRole[]) =>
|
||||
SetMetadata('requiresTeamRole', roles);
|
||||
@@ -0,0 +1,47 @@
|
||||
import { CanActivate, ExecutionContext, Injectable } from '@nestjs/common';
|
||||
import { Reflector } from '@nestjs/core';
|
||||
import { TeamService } from '../team.service';
|
||||
import { TeamMemberRole } from '../team.model';
|
||||
import { GqlExecutionContext } from '@nestjs/graphql';
|
||||
import { User } from '../../user/user.model';
|
||||
import {
|
||||
TEAM_NOT_REQUIRED_ROLE,
|
||||
BUG_AUTH_NO_USER_CTX,
|
||||
BUG_TEAM_NO_REQUIRE_TEAM_ROLE,
|
||||
BUG_TEAM_NO_TEAM_ID,
|
||||
} from 'src/errors';
|
||||
|
||||
@Injectable()
|
||||
export class GqlTeamMemberGuard implements CanActivate {
|
||||
constructor(
|
||||
private readonly reflector: Reflector,
|
||||
private readonly teamService: TeamService,
|
||||
) {}
|
||||
|
||||
async canActivate(context: ExecutionContext): Promise<boolean> {
|
||||
const requireRoles = this.reflector.get<TeamMemberRole[]>(
|
||||
'requiresTeamRole',
|
||||
context.getHandler(),
|
||||
);
|
||||
|
||||
if (!requireRoles) throw new Error(BUG_TEAM_NO_REQUIRE_TEAM_ROLE);
|
||||
|
||||
const gqlExecCtx = GqlExecutionContext.create(context);
|
||||
|
||||
const { user } = gqlExecCtx.getContext().req;
|
||||
|
||||
if (user == undefined) throw new Error(BUG_AUTH_NO_USER_CTX);
|
||||
|
||||
const { teamID } = gqlExecCtx.getArgs<{ teamID: string }>();
|
||||
|
||||
if (!teamID) throw new Error(BUG_TEAM_NO_TEAM_ID);
|
||||
|
||||
const status = await this.teamService.getTeamMember(teamID, user.uid);
|
||||
|
||||
if (!status) throw new Error('team/member_not_found');
|
||||
|
||||
if (requireRoles.includes(status.role)) return true;
|
||||
|
||||
throw new Error(TEAM_NOT_REQUIRED_ROLE);
|
||||
}
|
||||
}
|
||||
24
packages/hoppscotch-backend/src/team/team-member.resolver.ts
Normal file
24
packages/hoppscotch-backend/src/team/team-member.resolver.ts
Normal file
@@ -0,0 +1,24 @@
|
||||
import { Resolver, ResolveField, Parent } from '@nestjs/graphql';
|
||||
import { TeamMember } from './team.model';
|
||||
import { UserService } from 'src/user/user.service';
|
||||
import { User } from '../user/user.model';
|
||||
import { throwErr } from 'src/utils';
|
||||
import { USER_NOT_FOUND } from 'src/errors';
|
||||
import * as O from 'fp-ts/Option';
|
||||
|
||||
@Resolver(() => TeamMember)
|
||||
export class TeamMemberResolver {
|
||||
constructor(private readonly userService: UserService) {}
|
||||
|
||||
@ResolveField(() => User)
|
||||
async user(@Parent() teamMember: TeamMember): Promise<User> {
|
||||
const member = await this.userService.findUserById(teamMember.userUid);
|
||||
if (O.isNone(member)) throwErr(USER_NOT_FOUND);
|
||||
|
||||
return {
|
||||
...member.value,
|
||||
currentRESTSession: JSON.stringify(member.value.currentRESTSession),
|
||||
currentGQLSession: JSON.stringify(member.value.currentGQLSession),
|
||||
};
|
||||
}
|
||||
}
|
||||
39
packages/hoppscotch-backend/src/team/team.model.ts
Normal file
39
packages/hoppscotch-backend/src/team/team.model.ts
Normal file
@@ -0,0 +1,39 @@
|
||||
import { ObjectType, Field, ID, registerEnumType } from '@nestjs/graphql';
|
||||
|
||||
@ObjectType()
|
||||
export class Team {
|
||||
@Field(() => ID, {
|
||||
description: 'ID of the team',
|
||||
})
|
||||
id: string;
|
||||
|
||||
@Field(() => String, {
|
||||
description: 'Displayed name of the team',
|
||||
})
|
||||
name: string;
|
||||
}
|
||||
|
||||
@ObjectType()
|
||||
export class TeamMember {
|
||||
@Field(() => ID, {
|
||||
description: 'Membership ID of the Team Member'
|
||||
})
|
||||
membershipID: string;
|
||||
|
||||
userUid: string;
|
||||
|
||||
@Field(() => TeamMemberRole, {
|
||||
description: 'Role of the given team member in the given team',
|
||||
})
|
||||
role: TeamMemberRole;
|
||||
}
|
||||
|
||||
export enum TeamMemberRole {
|
||||
OWNER = 'OWNER',
|
||||
VIEWER = 'VIEWER',
|
||||
EDITOR = 'EDITOR',
|
||||
}
|
||||
|
||||
registerEnumType(TeamMemberRole, {
|
||||
name: 'TeamMemberRole',
|
||||
});
|
||||
20
packages/hoppscotch-backend/src/team/team.module.ts
Normal file
20
packages/hoppscotch-backend/src/team/team.module.ts
Normal file
@@ -0,0 +1,20 @@
|
||||
import { Module } from '@nestjs/common';
|
||||
import { TeamService } from './team.service';
|
||||
import { TeamResolver } from './team.resolver';
|
||||
import { UserModule } from '../user/user.module';
|
||||
import { TeamMemberResolver } from './team-member.resolver';
|
||||
import { GqlTeamMemberGuard } from './guards/gql-team-member.guard';
|
||||
import { PrismaModule } from '../prisma/prisma.module';
|
||||
import { PubSubModule } from '../pubsub/pubsub.module';
|
||||
|
||||
@Module({
|
||||
imports: [UserModule ,PubSubModule , PrismaModule ],
|
||||
providers: [
|
||||
TeamService,
|
||||
TeamResolver,
|
||||
TeamMemberResolver,
|
||||
GqlTeamMemberGuard,
|
||||
],
|
||||
exports: [TeamService, GqlTeamMemberGuard],
|
||||
})
|
||||
export class TeamModule {}
|
||||
364
packages/hoppscotch-backend/src/team/team.resolver.ts
Normal file
364
packages/hoppscotch-backend/src/team/team.resolver.ts
Normal file
@@ -0,0 +1,364 @@
|
||||
import { Team, TeamMember, TeamMemberRole } from './team.model';
|
||||
import {
|
||||
Resolver,
|
||||
ResolveField,
|
||||
Args,
|
||||
Parent,
|
||||
Query,
|
||||
Mutation,
|
||||
Int,
|
||||
Subscription,
|
||||
ID,
|
||||
} from '@nestjs/graphql';
|
||||
import { TeamService } from './team.service';
|
||||
import { User } from '../user/user.model';
|
||||
import { GqlAuthGuard } from '../guards/gql-auth.guard';
|
||||
import { GqlUser } from '../decorators/gql-user.decorator';
|
||||
import { UseGuards } from '@nestjs/common';
|
||||
import { RequiresTeamRole } from './decorators/requires-team-role.decorator';
|
||||
import { GqlTeamMemberGuard } from './guards/gql-team-member.guard';
|
||||
import { PubSubService } from '../pubsub/pubsub.service';
|
||||
import * as E from 'fp-ts/Either';
|
||||
import { throwErr } from 'src/utils';
|
||||
import { AuthUser } from 'src/types/AuthUser';
|
||||
|
||||
@Resolver(() => Team)
|
||||
export class TeamResolver {
|
||||
constructor(
|
||||
private readonly teamService: TeamService,
|
||||
private readonly pubsub: PubSubService,
|
||||
) {}
|
||||
|
||||
// Field Resolvers
|
||||
// TODO: Deprecate this
|
||||
@ResolveField(() => [TeamMember], {
|
||||
description: 'Returns the list of members of a team',
|
||||
complexity: 10,
|
||||
})
|
||||
members(
|
||||
@Parent() team: Team,
|
||||
@Args({
|
||||
name: 'cursor',
|
||||
type: () => ID,
|
||||
description:
|
||||
'The ID of the last returned team member entry (used for pagination)',
|
||||
nullable: true,
|
||||
})
|
||||
cursor?: string,
|
||||
): Promise<TeamMember[]> {
|
||||
return this.teamService.getMembersOfTeam(team.id, cursor ?? null);
|
||||
}
|
||||
|
||||
@ResolveField(() => [TeamMember], {
|
||||
description: 'Returns the list of members of a team',
|
||||
complexity: 10,
|
||||
})
|
||||
teamMembers(@Parent() team: Team): Promise<TeamMember[]> {
|
||||
return this.teamService.getTeamMembers(team.id);
|
||||
}
|
||||
|
||||
@ResolveField(() => TeamMemberRole, {
|
||||
description: 'The role of the current user in the team',
|
||||
})
|
||||
@UseGuards(GqlAuthGuard)
|
||||
myRole(
|
||||
@Parent() team: Team,
|
||||
@GqlUser() user: AuthUser,
|
||||
): Promise<TeamMemberRole | null> {
|
||||
return this.teamService.getRoleOfUserInTeam(team.id, user.uid);
|
||||
}
|
||||
|
||||
@ResolveField(() => Int, {
|
||||
description: 'The number of users with the OWNER role in the team',
|
||||
})
|
||||
ownersCount(@Parent() team: Team): Promise<number> {
|
||||
return this.teamService.getCountOfUsersWithRoleInTeam(
|
||||
team.id,
|
||||
TeamMemberRole.OWNER,
|
||||
);
|
||||
}
|
||||
|
||||
@ResolveField(() => Int, {
|
||||
description: 'The number of users with the EDITOR role in the team',
|
||||
})
|
||||
editorsCount(@Parent() team: Team): Promise<number> {
|
||||
return this.teamService.getCountOfUsersWithRoleInTeam(
|
||||
team.id,
|
||||
TeamMemberRole.EDITOR,
|
||||
);
|
||||
}
|
||||
|
||||
@ResolveField(() => Int, {
|
||||
description: 'The number of users with the VIEWER role in the team',
|
||||
})
|
||||
viewersCount(@Parent() team: Team): Promise<number> {
|
||||
return this.teamService.getCountOfUsersWithRoleInTeam(
|
||||
team.id,
|
||||
TeamMemberRole.VIEWER,
|
||||
);
|
||||
}
|
||||
|
||||
// Query
|
||||
@Query(() => [Team], {
|
||||
description: 'List of teams that the executing user belongs to.',
|
||||
})
|
||||
@UseGuards(GqlAuthGuard)
|
||||
myTeams(
|
||||
@GqlUser() user: AuthUser,
|
||||
@Args({
|
||||
name: 'cursor',
|
||||
type: () => ID,
|
||||
description:
|
||||
'The ID of the last returned team entry (used for pagination)',
|
||||
nullable: true,
|
||||
})
|
||||
cursor?: string,
|
||||
): Promise<Team[]> {
|
||||
return this.teamService.getTeamsOfUser(user.uid, cursor ?? null);
|
||||
}
|
||||
|
||||
@Query(() => Team, {
|
||||
description: 'Returns the detail of the team with the given ID',
|
||||
nullable: true,
|
||||
})
|
||||
@UseGuards(GqlAuthGuard, GqlTeamMemberGuard)
|
||||
@RequiresTeamRole(
|
||||
TeamMemberRole.VIEWER,
|
||||
TeamMemberRole.EDITOR,
|
||||
TeamMemberRole.OWNER,
|
||||
)
|
||||
team(
|
||||
@Args({
|
||||
name: 'teamID',
|
||||
type: () => ID,
|
||||
description: 'ID of the team to check',
|
||||
})
|
||||
teamID: string,
|
||||
): Promise<Team | null> {
|
||||
return this.teamService.getTeamWithID(teamID);
|
||||
}
|
||||
|
||||
// Mutation
|
||||
@Mutation(() => Team, {
|
||||
description: 'Creates a team owned by the executing user',
|
||||
})
|
||||
@UseGuards(GqlAuthGuard)
|
||||
async createTeam(
|
||||
@GqlUser() user: AuthUser,
|
||||
@Args({ name: 'name', description: 'Displayed name of the team' })
|
||||
name: string,
|
||||
): Promise<Team> {
|
||||
const team = await this.teamService.createTeam(name, user.uid);
|
||||
if (E.isLeft(team)) throwErr(team.left);
|
||||
return team.right;
|
||||
}
|
||||
|
||||
@Mutation(() => Boolean, {
|
||||
description: 'Leaves a team the executing user is a part of',
|
||||
})
|
||||
@UseGuards(GqlAuthGuard)
|
||||
async leaveTeam(
|
||||
@GqlUser() user: AuthUser,
|
||||
@Args({
|
||||
name: 'teamID',
|
||||
description: 'ID of the Team to leave',
|
||||
type: () => ID,
|
||||
})
|
||||
teamID: string,
|
||||
): Promise<boolean> {
|
||||
const isUserLeft = await this.teamService.leaveTeam(teamID, user.uid);
|
||||
if (E.isLeft(isUserLeft)) throwErr(isUserLeft.left);
|
||||
return isUserLeft.right;
|
||||
}
|
||||
|
||||
@Mutation(() => Boolean, {
|
||||
description: 'Removes the team member from the team',
|
||||
})
|
||||
@UseGuards(GqlAuthGuard, GqlTeamMemberGuard)
|
||||
@RequiresTeamRole(TeamMemberRole.OWNER)
|
||||
async removeTeamMember(
|
||||
@GqlUser() _user: AuthUser,
|
||||
@Args({
|
||||
name: 'teamID',
|
||||
description: 'ID of the Team to remove user from',
|
||||
type: () => ID,
|
||||
})
|
||||
teamID: string,
|
||||
@Args({
|
||||
name: 'userUid',
|
||||
description: 'ID of the user to remove from the given team',
|
||||
type: () => ID,
|
||||
})
|
||||
userUid: string,
|
||||
): Promise<boolean> {
|
||||
const isRemoved = await this.teamService.leaveTeam(teamID, userUid);
|
||||
if (E.isLeft(isRemoved)) throwErr(isRemoved.left);
|
||||
return isRemoved.right;
|
||||
}
|
||||
|
||||
@Mutation(() => Team, {
|
||||
description: 'Renames a team',
|
||||
})
|
||||
@UseGuards(GqlAuthGuard, GqlTeamMemberGuard)
|
||||
@RequiresTeamRole(TeamMemberRole.OWNER)
|
||||
async renameTeam(
|
||||
@Args({ name: 'teamID', description: 'ID of the team', type: () => ID })
|
||||
teamID: string,
|
||||
@Args({ name: 'newName', description: 'The updated name of the team' })
|
||||
newName: string,
|
||||
): Promise<Team> {
|
||||
const team = await this.teamService.renameTeam(teamID, newName);
|
||||
if (E.isLeft(team)) throwErr(team.left);
|
||||
return team.right;
|
||||
}
|
||||
|
||||
@Mutation(() => Boolean, {
|
||||
description: 'Deletes the team',
|
||||
})
|
||||
@UseGuards(GqlAuthGuard, GqlTeamMemberGuard)
|
||||
@RequiresTeamRole(TeamMemberRole.OWNER)
|
||||
async deleteTeam(
|
||||
@Args({ name: 'teamID', description: 'ID of the team', type: () => ID })
|
||||
teamID: string,
|
||||
): Promise<boolean> {
|
||||
const isDeleted = await this.teamService.deleteTeam(teamID);
|
||||
if (E.isLeft(isDeleted)) throwErr(isDeleted.left);
|
||||
return isDeleted.right;
|
||||
}
|
||||
|
||||
@Mutation(() => TeamMember, {
|
||||
description: 'Adds a team member to the team via email',
|
||||
deprecationReason:
|
||||
'This is only present for backwards compatibility and will be removed soon use team invitations instead',
|
||||
})
|
||||
@RequiresTeamRole(TeamMemberRole.OWNER)
|
||||
@UseGuards(GqlAuthGuard, GqlTeamMemberGuard)
|
||||
async addTeamMemberByEmail(
|
||||
@Args({
|
||||
name: 'teamID',
|
||||
description: 'ID of the team to add to',
|
||||
type: () => ID,
|
||||
})
|
||||
teamID: string,
|
||||
@Args({
|
||||
name: 'userEmail',
|
||||
description: 'Email of the user to add to team',
|
||||
})
|
||||
userEmail: string,
|
||||
@Args({
|
||||
name: 'userRole',
|
||||
description: 'The role of the user to add in the team',
|
||||
type: () => TeamMemberRole,
|
||||
})
|
||||
role: TeamMemberRole,
|
||||
): Promise<TeamMember> {
|
||||
const teamMember = await this.teamService.addMemberToTeamWithEmail(
|
||||
teamID,
|
||||
userEmail,
|
||||
role,
|
||||
);
|
||||
if (E.isLeft(teamMember)) throwErr(teamMember.left);
|
||||
return teamMember.right;
|
||||
}
|
||||
|
||||
@Mutation(() => TeamMember, {
|
||||
description: 'Update role of a team member the executing user owns',
|
||||
})
|
||||
@RequiresTeamRole(TeamMemberRole.OWNER)
|
||||
@UseGuards(GqlAuthGuard, GqlTeamMemberGuard)
|
||||
async updateTeamMemberRole(
|
||||
@Args({
|
||||
name: 'teamID',
|
||||
description: 'ID of the affected team',
|
||||
type: () => ID,
|
||||
})
|
||||
teamID: string,
|
||||
@Args({
|
||||
name: 'userUid',
|
||||
description: 'UID of the affected user',
|
||||
type: () => ID,
|
||||
})
|
||||
userUid: string,
|
||||
@Args({
|
||||
name: 'newRole',
|
||||
description: 'Updated role value',
|
||||
type: () => TeamMemberRole,
|
||||
})
|
||||
newRole: TeamMemberRole,
|
||||
): Promise<TeamMember> {
|
||||
const teamMember = await this.teamService.updateTeamMemberRole(
|
||||
teamID,
|
||||
userUid,
|
||||
newRole,
|
||||
);
|
||||
if (E.isLeft(teamMember)) throwErr(teamMember.left);
|
||||
return teamMember.right;
|
||||
}
|
||||
|
||||
// Subscriptions
|
||||
@Subscription(() => TeamMember, {
|
||||
description:
|
||||
'Listen to when a new team member being added to the team. The emitted value is the new team member added.',
|
||||
resolve: (value) => value,
|
||||
})
|
||||
@RequiresTeamRole(
|
||||
TeamMemberRole.OWNER,
|
||||
TeamMemberRole.EDITOR,
|
||||
TeamMemberRole.VIEWER,
|
||||
)
|
||||
@UseGuards(GqlAuthGuard, GqlTeamMemberGuard)
|
||||
teamMemberAdded(
|
||||
@Args({
|
||||
name: 'teamID',
|
||||
description: 'ID of the team to listen to',
|
||||
type: () => ID,
|
||||
})
|
||||
teamID: string,
|
||||
): AsyncIterator<TeamMember> {
|
||||
return this.pubsub.asyncIterator(`team/${teamID}/member_added`);
|
||||
}
|
||||
|
||||
@Subscription(() => TeamMember, {
|
||||
description:
|
||||
'Listen to when a team member status has been updated. The emitted value is the new team member status',
|
||||
resolve: (value) => value,
|
||||
})
|
||||
@RequiresTeamRole(
|
||||
TeamMemberRole.OWNER,
|
||||
TeamMemberRole.EDITOR,
|
||||
TeamMemberRole.VIEWER,
|
||||
)
|
||||
@UseGuards(GqlAuthGuard, GqlTeamMemberGuard)
|
||||
teamMemberUpdated(
|
||||
@Args({
|
||||
name: 'teamID',
|
||||
description: 'ID of the team to listen to',
|
||||
type: () => ID,
|
||||
})
|
||||
teamID: string,
|
||||
): AsyncIterator<TeamMember> {
|
||||
return this.pubsub.asyncIterator(`team/${teamID}/member_updated`);
|
||||
}
|
||||
|
||||
@Subscription(() => ID, {
|
||||
description:
|
||||
'Listen to when a team member has been removed. The emitted value is the uid of the user removed',
|
||||
resolve: (value) => value,
|
||||
})
|
||||
@RequiresTeamRole(
|
||||
TeamMemberRole.OWNER,
|
||||
TeamMemberRole.EDITOR,
|
||||
TeamMemberRole.VIEWER,
|
||||
)
|
||||
@UseGuards(GqlAuthGuard, GqlTeamMemberGuard)
|
||||
teamMemberRemoved(
|
||||
@Args({
|
||||
name: 'teamID',
|
||||
description: 'ID of the team to listen to',
|
||||
type: () => ID,
|
||||
})
|
||||
teamID: string,
|
||||
): AsyncIterator<string> {
|
||||
return this.pubsub.asyncIterator(`team/${teamID}/member_removed`);
|
||||
}
|
||||
}
|
||||
920
packages/hoppscotch-backend/src/team/team.service.spec.ts
Normal file
920
packages/hoppscotch-backend/src/team/team.service.spec.ts
Normal file
@@ -0,0 +1,920 @@
|
||||
import { TeamService } from './team.service';
|
||||
import { PrismaService } from '../prisma/prisma.service';
|
||||
import { Team, TeamMember, TeamMemberRole } from './team.model';
|
||||
import { TeamMember as DbTeamMember } from '@prisma/client';
|
||||
import {
|
||||
USER_NOT_FOUND,
|
||||
TEAM_INVALID_ID,
|
||||
TEAM_NAME_INVALID,
|
||||
TEAM_ONLY_ONE_OWNER,
|
||||
TEAM_INVALID_ID_OR_USER,
|
||||
} from '../errors';
|
||||
import { mockDeep, mockReset } from 'jest-mock-extended';
|
||||
|
||||
const mockPrisma = mockDeep<PrismaService>();
|
||||
|
||||
const mockUserService = {
|
||||
getUserWithEmail: jest.fn(),
|
||||
getUserForUID: jest.fn(),
|
||||
authenticateWithIDToken: jest.fn(),
|
||||
};
|
||||
|
||||
const mockPubSub = {
|
||||
publish: jest.fn().mockResolvedValue(null),
|
||||
};
|
||||
|
||||
const teamService = new TeamService(
|
||||
mockPrisma as any,
|
||||
mockUserService as any,
|
||||
mockPubSub as any,
|
||||
);
|
||||
|
||||
beforeEach(async () => {
|
||||
mockReset(mockPrisma);
|
||||
});
|
||||
|
||||
const team: Team = {
|
||||
id: 'teamID',
|
||||
name: 'teamName',
|
||||
};
|
||||
const dbTeamMember: DbTeamMember = {
|
||||
id: 'teamMemberID',
|
||||
role: TeamMemberRole.VIEWER,
|
||||
userUid: 'userUid',
|
||||
teamID: team.id,
|
||||
};
|
||||
const teamMember: TeamMember = {
|
||||
membershipID: dbTeamMember.id,
|
||||
role: TeamMemberRole[dbTeamMember.role],
|
||||
userUid: dbTeamMember.userUid,
|
||||
};
|
||||
|
||||
describe('getCountOfUsersWithRoleInTeam', () => {
|
||||
test('resolves to the correct count of owners in a team', async () => {
|
||||
mockPrisma.teamMember.count.mockResolvedValue(2);
|
||||
|
||||
await expect(
|
||||
teamService.getCountOfUsersWithRoleInTeam(
|
||||
dbTeamMember.teamID,
|
||||
TeamMemberRole.OWNER,
|
||||
),
|
||||
).resolves.toEqual(2);
|
||||
|
||||
expect(mockPrisma.teamMember.count).toHaveBeenCalledWith({
|
||||
where: {
|
||||
teamID: dbTeamMember.teamID,
|
||||
role: TeamMemberRole.OWNER,
|
||||
},
|
||||
});
|
||||
});
|
||||
|
||||
test('resolves to the correct count of viewers in a team', async () => {
|
||||
mockPrisma.teamMember.count.mockResolvedValue(2);
|
||||
|
||||
await expect(
|
||||
teamService.getCountOfUsersWithRoleInTeam(
|
||||
dbTeamMember.teamID,
|
||||
TeamMemberRole.VIEWER,
|
||||
),
|
||||
).resolves.toEqual(2);
|
||||
|
||||
expect(mockPrisma.teamMember.count).toHaveBeenCalledWith({
|
||||
where: {
|
||||
teamID: dbTeamMember.teamID,
|
||||
role: TeamMemberRole.VIEWER,
|
||||
},
|
||||
});
|
||||
});
|
||||
|
||||
test('resolves to the correct count of editors in a team', async () => {
|
||||
mockPrisma.teamMember.count.mockResolvedValue(2);
|
||||
|
||||
await expect(
|
||||
teamService.getCountOfUsersWithRoleInTeam(
|
||||
dbTeamMember.teamID,
|
||||
TeamMemberRole.EDITOR,
|
||||
),
|
||||
).resolves.toEqual(2);
|
||||
|
||||
expect(mockPrisma.teamMember.count).toHaveBeenCalledWith({
|
||||
where: {
|
||||
teamID: dbTeamMember.teamID,
|
||||
role: TeamMemberRole.EDITOR,
|
||||
},
|
||||
});
|
||||
});
|
||||
});
|
||||
|
||||
describe('addMemberToTeam', () => {
|
||||
test('resolves when proper team id is given', () => {
|
||||
mockPrisma.teamMember.create.mockResolvedValue(dbTeamMember);
|
||||
|
||||
expect(
|
||||
teamService.addMemberToTeam(
|
||||
dbTeamMember.teamID,
|
||||
dbTeamMember.userUid,
|
||||
TeamMemberRole[dbTeamMember.role],
|
||||
),
|
||||
).resolves.toEqual(expect.objectContaining(teamMember));
|
||||
});
|
||||
|
||||
test('makes the update in the database', async () => {
|
||||
mockPrisma.teamMember.create.mockResolvedValue(dbTeamMember);
|
||||
|
||||
await teamService.addMemberToTeam(
|
||||
dbTeamMember.teamID,
|
||||
dbTeamMember.userUid,
|
||||
TeamMemberRole[dbTeamMember.role],
|
||||
);
|
||||
|
||||
expect(mockPrisma.teamMember.create).toHaveBeenCalledWith({
|
||||
data: {
|
||||
userUid: dbTeamMember.userUid,
|
||||
team: {
|
||||
connect: {
|
||||
id: dbTeamMember.teamID,
|
||||
},
|
||||
},
|
||||
role: TeamMemberRole[dbTeamMember.role],
|
||||
},
|
||||
});
|
||||
});
|
||||
|
||||
test('fires "team/<team_id>/member_added" pubsub message with correct payload', async () => {
|
||||
mockPrisma.teamMember.create.mockResolvedValue(dbTeamMember);
|
||||
|
||||
const member = await teamService.addMemberToTeam(
|
||||
dbTeamMember.teamID,
|
||||
dbTeamMember.userUid,
|
||||
TeamMemberRole[dbTeamMember.role],
|
||||
);
|
||||
|
||||
expect(mockPubSub.publish).toHaveBeenCalledWith(
|
||||
`team/${dbTeamMember.teamID}/member_added`,
|
||||
member,
|
||||
);
|
||||
});
|
||||
});
|
||||
|
||||
describe('addMemberToTeamWithEmail', () => {
|
||||
afterEach(() => {
|
||||
mockUserService.getUserWithEmail.mockClear();
|
||||
mockUserService.authenticateWithIDToken.mockClear();
|
||||
mockUserService.authenticateWithIDToken.mockClear();
|
||||
});
|
||||
|
||||
test('resolves when user with email exists', () => {
|
||||
mockUserService.getUserWithEmail.mockResolvedValueOnce({
|
||||
uid: dbTeamMember.userUid,
|
||||
});
|
||||
mockPrisma.teamMember.create.mockResolvedValue(dbTeamMember);
|
||||
|
||||
const result = teamService.addMemberToTeamWithEmail(
|
||||
dbTeamMember.teamID,
|
||||
'test@hoppscotch.io',
|
||||
TeamMemberRole[dbTeamMember.role],
|
||||
);
|
||||
return expect(result).resolves.toBeDefined();
|
||||
});
|
||||
|
||||
test("rejects with user with email doesn't exist with USER_NOT_FOUND", () => {
|
||||
mockUserService.getUserWithEmail.mockResolvedValue(null);
|
||||
|
||||
const result = teamService.addMemberToTeamWithEmail(
|
||||
dbTeamMember.teamID,
|
||||
'test@hoppscotch.io',
|
||||
TeamMemberRole[dbTeamMember.role],
|
||||
);
|
||||
return expect(result).resolves.toEqualLeft(USER_NOT_FOUND);
|
||||
});
|
||||
|
||||
test('makes update in the database', async () => {
|
||||
mockUserService.getUserWithEmail.mockResolvedValueOnce({
|
||||
uid: dbTeamMember.userUid,
|
||||
});
|
||||
mockPrisma.teamMember.create.mockResolvedValue(dbTeamMember);
|
||||
|
||||
await teamService.addMemberToTeamWithEmail(
|
||||
dbTeamMember.teamID,
|
||||
'test@hoppscotch.io',
|
||||
TeamMemberRole[dbTeamMember.role],
|
||||
);
|
||||
|
||||
expect(mockPrisma.teamMember.create).toHaveBeenCalledWith({
|
||||
data: {
|
||||
userUid: dbTeamMember.userUid,
|
||||
team: {
|
||||
connect: {
|
||||
id: dbTeamMember.teamID,
|
||||
},
|
||||
},
|
||||
role: TeamMemberRole[dbTeamMember.role],
|
||||
},
|
||||
});
|
||||
});
|
||||
|
||||
test('fires "team/<team_id>/member_added" pubsub message with correct payload', async () => {
|
||||
mockUserService.getUserWithEmail.mockResolvedValueOnce({
|
||||
uid: dbTeamMember.userUid,
|
||||
});
|
||||
mockPrisma.teamMember.create.mockResolvedValue(dbTeamMember);
|
||||
|
||||
await teamService.addMemberToTeamWithEmail(
|
||||
dbTeamMember.teamID,
|
||||
'test@hoppscotch.io',
|
||||
TeamMemberRole[dbTeamMember.role],
|
||||
);
|
||||
|
||||
expect(mockPubSub.publish).toHaveBeenCalledWith(
|
||||
`team/${dbTeamMember.teamID}/member_added`,
|
||||
teamMember,
|
||||
);
|
||||
});
|
||||
});
|
||||
|
||||
describe('deleteTeam', () => {
|
||||
test('resolves for proper deletion', async () => {
|
||||
mockPrisma.team.findUnique.mockResolvedValue(team);
|
||||
mockPrisma.teamMember.deleteMany.mockResolvedValue({
|
||||
count: 10,
|
||||
});
|
||||
mockPrisma.team.delete.mockResolvedValue(team);
|
||||
|
||||
const result = await teamService.deleteTeam(team.id);
|
||||
return expect(result).toEqualRight(true);
|
||||
});
|
||||
|
||||
test('performs deletion on database', async () => {
|
||||
mockPrisma.team.findUnique.mockResolvedValue(team);
|
||||
mockPrisma.teamMember.deleteMany.mockResolvedValue({
|
||||
count: 10,
|
||||
});
|
||||
mockPrisma.team.delete.mockResolvedValue(team);
|
||||
|
||||
await teamService.deleteTeam(team.id);
|
||||
|
||||
expect(mockPrisma.team.delete).toHaveBeenCalledWith({
|
||||
where: {
|
||||
id: team.id,
|
||||
},
|
||||
});
|
||||
});
|
||||
|
||||
test('rejects for invalid team id', async () => {
|
||||
mockPrisma.team.findUnique.mockResolvedValue(null);
|
||||
|
||||
// If invalid team ID, team member deletes nothing (count 0)
|
||||
mockPrisma.teamMember.deleteMany.mockResolvedValue({
|
||||
count: 0,
|
||||
});
|
||||
|
||||
// TODO: Confirm RecordNotFound works like this
|
||||
mockPrisma.team.delete.mockRejectedValue('RecordNotFound');
|
||||
|
||||
// Team will not find and reject
|
||||
const result = await teamService.deleteTeam(team.id);
|
||||
return expect(result).toEqualLeft(TEAM_INVALID_ID);
|
||||
});
|
||||
});
|
||||
|
||||
describe('renameTeam', () => {
|
||||
test('resolves for proper rename', () => {
|
||||
const newTeamName = 'Rename';
|
||||
|
||||
mockPrisma.team.update.mockResolvedValue({
|
||||
...team,
|
||||
name: newTeamName,
|
||||
});
|
||||
|
||||
return expect(
|
||||
teamService.renameTeam(team.id, newTeamName),
|
||||
).resolves.toBeDefined();
|
||||
});
|
||||
|
||||
test('resolves with team structure', () => {
|
||||
const newTeamName = 'Rename';
|
||||
|
||||
mockPrisma.team.update.mockResolvedValue({
|
||||
...team,
|
||||
name: newTeamName,
|
||||
});
|
||||
|
||||
return expect(
|
||||
teamService.renameTeam(team.id, newTeamName),
|
||||
).resolves.toEqualRight(
|
||||
expect.objectContaining({
|
||||
...team,
|
||||
name: newTeamName,
|
||||
}),
|
||||
);
|
||||
});
|
||||
|
||||
test('performs rename on database', async () => {
|
||||
const newTeamName = 'Rename';
|
||||
|
||||
mockPrisma.team.update.mockResolvedValue({
|
||||
...team,
|
||||
name: newTeamName,
|
||||
});
|
||||
|
||||
await teamService.renameTeam(team.id, newTeamName);
|
||||
|
||||
expect(mockPrisma.team.update).toHaveBeenCalledWith({
|
||||
where: {
|
||||
id: team.id,
|
||||
},
|
||||
data: {
|
||||
name: newTeamName,
|
||||
},
|
||||
});
|
||||
});
|
||||
|
||||
test('rejects for invalid team id with TEAM_INVALID_ID', () => {
|
||||
const newTeamName = 'Rename';
|
||||
// If invalid team id, update fails with RecordNotFound
|
||||
mockPrisma.team.update.mockRejectedValue('RecordNotFound');
|
||||
|
||||
return expect(
|
||||
teamService.renameTeam(team.id, newTeamName),
|
||||
).resolves.toEqualLeft(TEAM_INVALID_ID);
|
||||
});
|
||||
|
||||
test('rejects for new team name length < 6 with TEAM_NAME_INVALID', () => {
|
||||
const newTeamName = 'smol';
|
||||
|
||||
// Prisma doesn't care about the team name length, so it will resolve
|
||||
mockPrisma.team.update.mockResolvedValue({
|
||||
...team,
|
||||
name: newTeamName,
|
||||
});
|
||||
|
||||
return expect(
|
||||
teamService.renameTeam(team.id, newTeamName),
|
||||
).resolves.toEqualLeft(TEAM_NAME_INVALID);
|
||||
});
|
||||
});
|
||||
|
||||
describe('updateTeamMemberRole', () => {
|
||||
/**
|
||||
* Test Scenario:
|
||||
* 3 users (testuid1 thru 3) having each of the roles
|
||||
* (OWNER, VIEWER, EDITOR)
|
||||
* in Team with id 3170
|
||||
*/
|
||||
|
||||
test('updates the role', async () => {
|
||||
const newRole = TeamMemberRole.EDITOR;
|
||||
|
||||
mockPrisma.teamMember.count.mockResolvedValue(1);
|
||||
mockPrisma.teamMember.findUnique.mockResolvedValue({
|
||||
...dbTeamMember,
|
||||
role: TeamMemberRole[dbTeamMember.role],
|
||||
});
|
||||
mockPrisma.teamMember.update.mockResolvedValue({
|
||||
...dbTeamMember,
|
||||
role: newRole,
|
||||
});
|
||||
|
||||
await teamService.updateTeamMemberRole(
|
||||
dbTeamMember.teamID,
|
||||
dbTeamMember.userUid,
|
||||
newRole,
|
||||
);
|
||||
|
||||
expect(mockPrisma.teamMember.update).toHaveBeenCalledWith({
|
||||
where: {
|
||||
teamID_userUid: {
|
||||
teamID: dbTeamMember.teamID,
|
||||
userUid: dbTeamMember.userUid,
|
||||
},
|
||||
},
|
||||
data: {
|
||||
role: newRole,
|
||||
},
|
||||
});
|
||||
});
|
||||
|
||||
test('returns the updated details', () => {
|
||||
const newRole = TeamMemberRole.EDITOR;
|
||||
|
||||
mockPrisma.teamMember.count.mockResolvedValue(1);
|
||||
mockPrisma.teamMember.findUnique.mockResolvedValue(dbTeamMember);
|
||||
mockPrisma.teamMember.update.mockResolvedValue({
|
||||
...dbTeamMember,
|
||||
role: newRole,
|
||||
});
|
||||
|
||||
return expect(
|
||||
teamService.updateTeamMemberRole(
|
||||
dbTeamMember.teamID,
|
||||
dbTeamMember.userUid,
|
||||
newRole,
|
||||
),
|
||||
).resolves.toEqualRight({ ...teamMember, role: newRole });
|
||||
});
|
||||
|
||||
test('rejects if you change the status of the sole owner to non-owner status with TEAM_ONLY_ONE_OWNER', () => {
|
||||
mockPrisma.teamMember.count.mockResolvedValue(1);
|
||||
mockPrisma.teamMember.findUnique.mockResolvedValue({
|
||||
...dbTeamMember,
|
||||
role: TeamMemberRole.OWNER,
|
||||
});
|
||||
|
||||
// Prisma doesn't care if it goes through
|
||||
mockPrisma.teamMember.update.mockResolvedValue(dbTeamMember);
|
||||
|
||||
return expect(
|
||||
teamService.updateTeamMemberRole(
|
||||
dbTeamMember.teamID,
|
||||
dbTeamMember.userUid,
|
||||
TeamMemberRole[dbTeamMember.role],
|
||||
),
|
||||
).resolves.toEqualLeft(TEAM_ONLY_ONE_OWNER);
|
||||
});
|
||||
|
||||
test('resolves if you change the status of the sole owner to owner status (no change)', () => {
|
||||
mockPrisma.teamMember.count.mockResolvedValue(1);
|
||||
mockPrisma.teamMember.findUnique.mockResolvedValue({
|
||||
...dbTeamMember,
|
||||
role: TeamMemberRole.OWNER,
|
||||
});
|
||||
mockPrisma.teamMember.update.mockResolvedValue({
|
||||
...dbTeamMember,
|
||||
role: TeamMemberRole.OWNER,
|
||||
});
|
||||
|
||||
return expect(
|
||||
teamService.updateTeamMemberRole(
|
||||
dbTeamMember.teamID,
|
||||
dbTeamMember.userUid,
|
||||
TeamMemberRole[TeamMemberRole.OWNER],
|
||||
),
|
||||
).resolves.toBeDefined();
|
||||
});
|
||||
|
||||
test('resolves if you change the status of an owner but there are other owners', async () => {
|
||||
mockPrisma.teamMember.count.mockResolvedValue(2);
|
||||
mockPrisma.teamMember.findUnique.mockResolvedValue({
|
||||
...dbTeamMember,
|
||||
role: TeamMemberRole.OWNER,
|
||||
});
|
||||
mockPrisma.teamMember.update.mockResolvedValue(dbTeamMember);
|
||||
|
||||
// Set another user as the owner
|
||||
await teamService.updateTeamMemberRole(
|
||||
dbTeamMember.teamID,
|
||||
'testuid2',
|
||||
TeamMemberRole.OWNER,
|
||||
);
|
||||
|
||||
await expect(
|
||||
teamService.updateTeamMemberRole(
|
||||
dbTeamMember.teamID,
|
||||
dbTeamMember.userUid,
|
||||
TeamMemberRole[dbTeamMember.role],
|
||||
),
|
||||
).resolves.toBeDefined();
|
||||
});
|
||||
|
||||
test('fires "team/<team_id>/member_updated" pubsub message with correct payload', async () => {
|
||||
const newRole = TeamMemberRole.EDITOR;
|
||||
|
||||
mockPrisma.teamMember.count.mockResolvedValue(2);
|
||||
mockPrisma.teamMember.findUnique.mockResolvedValue(dbTeamMember);
|
||||
mockPrisma.teamMember.update.mockResolvedValue({
|
||||
...dbTeamMember,
|
||||
role: newRole,
|
||||
});
|
||||
|
||||
await teamService.updateTeamMemberRole(
|
||||
dbTeamMember.teamID,
|
||||
dbTeamMember.userUid,
|
||||
newRole,
|
||||
);
|
||||
|
||||
expect(mockPubSub.publish).toHaveBeenCalledWith(
|
||||
`team/${dbTeamMember.teamID}/member_updated`,
|
||||
{
|
||||
...teamMember,
|
||||
role: newRole,
|
||||
},
|
||||
);
|
||||
});
|
||||
});
|
||||
|
||||
describe('leaveTeam', () => {
|
||||
/*
|
||||
Same scenario as above:
|
||||
3 users (testuid1 thru 3) with respectively
|
||||
OWNER, VIEWER and EDITOR roles in team with id 3170
|
||||
*/
|
||||
|
||||
test('removes the user if valid credentials given', async () => {
|
||||
mockPrisma.teamMember.count.mockResolvedValue(2);
|
||||
mockPrisma.teamMember.findUnique.mockResolvedValue(dbTeamMember);
|
||||
mockPrisma.teamMember.delete.mockResolvedValue(dbTeamMember);
|
||||
|
||||
await teamService.leaveTeam(dbTeamMember.teamID, dbTeamMember.userUid);
|
||||
|
||||
expect(mockPrisma.teamMember.delete).toHaveBeenCalledWith({
|
||||
where: {
|
||||
teamID_userUid: {
|
||||
teamID: dbTeamMember.teamID,
|
||||
userUid: dbTeamMember.userUid,
|
||||
},
|
||||
},
|
||||
});
|
||||
});
|
||||
|
||||
test('rejects if invalid teamId with TEAM_INVALID_ID_OR_USER', () => {
|
||||
// Invalid team id will return 0 count
|
||||
mockPrisma.teamMember.count.mockResolvedValue(0);
|
||||
|
||||
// getTeamMember returns null if no match
|
||||
mockPrisma.teamMember.findUnique.mockResolvedValue(null);
|
||||
|
||||
// Deletion rejects with RecordNotFound when no match
|
||||
mockPrisma.teamMember.delete.mockRejectedValue('RecordNotFound');
|
||||
|
||||
return expect(
|
||||
teamService.leaveTeam('31700', dbTeamMember.userUid),
|
||||
).resolves.toEqualLeft(TEAM_INVALID_ID_OR_USER);
|
||||
});
|
||||
|
||||
test('rejects if invalid userUid with TEAM_INVALID_ID_OR_USER', () => {
|
||||
// Invalid team id will return proper count
|
||||
mockPrisma.teamMember.count.mockResolvedValue(1);
|
||||
|
||||
// getTeamMember returns null if no match
|
||||
mockPrisma.teamMember.findUnique.mockResolvedValue(null);
|
||||
|
||||
// Deletion rejects with RecordNotFound when no match
|
||||
mockPrisma.teamMember.delete.mockRejectedValue('RecordNotFound');
|
||||
|
||||
return expect(
|
||||
teamService.leaveTeam(dbTeamMember.teamID, 'testuid3'),
|
||||
).resolves.toEqualLeft(TEAM_INVALID_ID_OR_USER);
|
||||
});
|
||||
|
||||
test('rejects if the removed user is the sole owner of the team with TEAM_ONLY_ONE_OWNER', () => {
|
||||
mockPrisma.teamMember.count.mockResolvedValue(1);
|
||||
mockPrisma.teamMember.findUnique.mockResolvedValue({
|
||||
...dbTeamMember,
|
||||
role: TeamMemberRole.OWNER,
|
||||
});
|
||||
|
||||
// Prisma does not care
|
||||
mockPrisma.teamMember.delete.mockResolvedValue({
|
||||
...dbTeamMember,
|
||||
role: TeamMemberRole.OWNER,
|
||||
});
|
||||
|
||||
return expect(
|
||||
teamService.leaveTeam(dbTeamMember.teamID, dbTeamMember.userUid),
|
||||
).resolves.toEqualLeft(TEAM_ONLY_ONE_OWNER);
|
||||
});
|
||||
|
||||
test('resolves if the removed user is an owner (but not the sole) of the team', async () => {
|
||||
mockPrisma.teamMember.count.mockResolvedValue(2);
|
||||
mockPrisma.teamMember.findUnique.mockResolvedValue({
|
||||
...dbTeamMember,
|
||||
role: TeamMemberRole.OWNER,
|
||||
});
|
||||
mockPrisma.teamMember.delete.mockResolvedValue({
|
||||
...dbTeamMember,
|
||||
role: TeamMemberRole.OWNER,
|
||||
});
|
||||
|
||||
await expect(
|
||||
teamService.leaveTeam(dbTeamMember.teamID, dbTeamMember.userUid),
|
||||
).resolves.toEqualRight(true);
|
||||
});
|
||||
|
||||
test('fires "team/<team_id>/member_removed" pubsub message with correct payload', async () => {
|
||||
mockPrisma.teamMember.count.mockResolvedValue(2);
|
||||
mockPrisma.teamMember.findUnique.mockResolvedValue(dbTeamMember);
|
||||
mockPrisma.teamMember.delete.mockResolvedValue(dbTeamMember);
|
||||
|
||||
await teamService.leaveTeam(dbTeamMember.teamID, dbTeamMember.userUid);
|
||||
|
||||
expect(mockPubSub.publish).toHaveBeenCalledWith(
|
||||
`team/${dbTeamMember.teamID}/member_removed`,
|
||||
dbTeamMember.userUid,
|
||||
);
|
||||
});
|
||||
});
|
||||
|
||||
describe('createTeam', () => {
|
||||
test('adds the new team to the db', async () => {
|
||||
mockPrisma.team.create.mockResolvedValue(team);
|
||||
|
||||
await teamService.createTeam(team.name, dbTeamMember.userUid);
|
||||
|
||||
expect(mockPrisma.team.create).toHaveBeenCalledWith(
|
||||
expect.objectContaining({
|
||||
data: expect.objectContaining({
|
||||
name: team.name,
|
||||
}),
|
||||
}),
|
||||
);
|
||||
});
|
||||
|
||||
test('adds the creator to team and set them as OWNER', async () => {
|
||||
mockPrisma.team.create.mockResolvedValue(team);
|
||||
|
||||
await teamService.createTeam(team.name, dbTeamMember.userUid);
|
||||
|
||||
expect(mockPrisma.team.create).toHaveBeenCalledWith(
|
||||
expect.objectContaining({
|
||||
data: expect.objectContaining({
|
||||
members: {
|
||||
create: {
|
||||
userUid: dbTeamMember.userUid,
|
||||
role: TeamMemberRole.OWNER,
|
||||
},
|
||||
},
|
||||
}),
|
||||
}),
|
||||
);
|
||||
});
|
||||
|
||||
test('resolves with the team info', () => {
|
||||
mockPrisma.team.create.mockResolvedValue(team);
|
||||
|
||||
return expect(
|
||||
teamService.createTeam(team.name, dbTeamMember.userUid),
|
||||
).resolves.toEqualRight(expect.objectContaining(team));
|
||||
});
|
||||
|
||||
test('rejects for team name length < 6 with TEAM_NAME_INVALID', () => {
|
||||
const newName = 'smol';
|
||||
|
||||
// Prisma doesn't care
|
||||
mockPrisma.team.create.mockResolvedValue({
|
||||
...team,
|
||||
name: newName,
|
||||
});
|
||||
|
||||
return expect(
|
||||
teamService.createTeam(newName, dbTeamMember.userUid),
|
||||
).resolves.toEqualLeft(TEAM_NAME_INVALID);
|
||||
});
|
||||
});
|
||||
|
||||
describe('getTeamWithID', () => {
|
||||
test('resolves for a proper team id with the proper details', () => {
|
||||
mockPrisma.team.findUnique.mockResolvedValue(team);
|
||||
|
||||
return expect(teamService.getTeamWithID(team.id)).resolves.toEqual(
|
||||
expect.objectContaining(team),
|
||||
);
|
||||
});
|
||||
|
||||
test('resolves for a invalid team id as null', () => {
|
||||
// Prisma would reject with RecordNotFound
|
||||
mockPrisma.team.findUnique.mockRejectedValue('RecordNotFound');
|
||||
|
||||
return expect(teamService.getTeamWithID('3171')).resolves.toBeNull();
|
||||
});
|
||||
});
|
||||
|
||||
describe('getTeamMember', () => {
|
||||
test('resolves for a proper team id and user uid and returns the info', () => {
|
||||
mockPrisma.teamMember.findUnique.mockResolvedValue(dbTeamMember);
|
||||
|
||||
return expect(
|
||||
teamService.getTeamMember(dbTeamMember.teamID, dbTeamMember.userUid),
|
||||
).resolves.toEqual(expect.objectContaining(teamMember));
|
||||
});
|
||||
|
||||
test('resolves for a invalid team id and proper uid and returns null', () => {
|
||||
// If not found, prisma rejects with RecordNotFound
|
||||
mockPrisma.teamMember.findUnique.mockRejectedValue('RecordNotFound');
|
||||
|
||||
return expect(
|
||||
teamService.getTeamMember(dbTeamMember.teamID, 'testuid'),
|
||||
).resolves.toBeNull();
|
||||
});
|
||||
});
|
||||
|
||||
describe('getRoleOfUserInTeam', () => {
|
||||
test('resolves with the correct role value', () => {
|
||||
mockPrisma.teamMember.findUnique.mockResolvedValue(dbTeamMember);
|
||||
|
||||
return expect(
|
||||
teamService.getRoleOfUserInTeam(
|
||||
dbTeamMember.teamID,
|
||||
dbTeamMember.userUid,
|
||||
),
|
||||
).resolves.toEqual(dbTeamMember.role);
|
||||
});
|
||||
|
||||
test('resolves with null if user is not found in team', () => {
|
||||
mockPrisma.teamMember.findUnique.mockRejectedValue('RecordNotFound');
|
||||
|
||||
return expect(
|
||||
teamService.getRoleOfUserInTeam(dbTeamMember.teamID, 'nottestuid'),
|
||||
).resolves.toBeNull();
|
||||
});
|
||||
|
||||
test('resolves with null if team does not exist', () => {
|
||||
mockPrisma.teamMember.findUnique.mockRejectedValue('RecordNotFound');
|
||||
|
||||
return expect(
|
||||
teamService.getRoleOfUserInTeam('invalidteam', dbTeamMember.userUid),
|
||||
).resolves.toBeNull();
|
||||
});
|
||||
});
|
||||
|
||||
describe('getMembersOfTeam', () => {
|
||||
test('resolves for the team id and null cursor with the first page', async () => {
|
||||
mockPrisma.teamMember.findMany.mockResolvedValue([]);
|
||||
await teamService.getMembersOfTeam(team.id, null);
|
||||
|
||||
expect(mockPrisma.teamMember.findMany).toHaveBeenCalledWith({
|
||||
take: 10,
|
||||
where: {
|
||||
teamID: team.id,
|
||||
},
|
||||
});
|
||||
});
|
||||
|
||||
test('resolves for the team id and proper cursor with pagination', async () => {
|
||||
const cursor = 'secondpage';
|
||||
|
||||
mockPrisma.teamMember.findMany.mockResolvedValue([]);
|
||||
await teamService.getMembersOfTeam(team.id, cursor);
|
||||
|
||||
expect(mockPrisma.teamMember.findMany).toHaveBeenCalledWith({
|
||||
take: 10,
|
||||
skip: 1,
|
||||
cursor: {
|
||||
id: cursor,
|
||||
},
|
||||
where: {
|
||||
teamID: team.id,
|
||||
},
|
||||
});
|
||||
});
|
||||
|
||||
test('resolves with an empty array for invalid team id and null cursor', () => {
|
||||
// findMany returns an empty array if no matches are found
|
||||
mockPrisma.teamMember.findMany.mockResolvedValue([]);
|
||||
|
||||
return expect(
|
||||
teamService.getMembersOfTeam('invalidteamid', null),
|
||||
).resolves.toHaveLength(0);
|
||||
});
|
||||
|
||||
test('resolves with an empty array for an invalid team id and invalid cursor', () => {
|
||||
// findMany returns an empty array if no matches are found
|
||||
mockPrisma.teamMember.findMany.mockResolvedValue([]);
|
||||
|
||||
return expect(
|
||||
teamService.getMembersOfTeam('invalidteamid', 'invalidcursor'),
|
||||
).resolves.toHaveLength(0);
|
||||
});
|
||||
});
|
||||
|
||||
describe('getTeamsOfUser', () => {
|
||||
test('resolves with the first 10 elements when no cursor is given', async () => {
|
||||
mockPrisma.teamMember.findMany.mockResolvedValue([]);
|
||||
|
||||
await teamService.getTeamsOfUser(dbTeamMember.userUid, null);
|
||||
|
||||
expect(mockPrisma.teamMember.findMany).toHaveBeenCalledWith({
|
||||
take: 10,
|
||||
where: {
|
||||
userUid: dbTeamMember.userUid,
|
||||
},
|
||||
include: {
|
||||
team: true,
|
||||
},
|
||||
});
|
||||
});
|
||||
|
||||
test('resolves as expected for paginated requests with cursor', async () => {
|
||||
const cursor = 'secondpage';
|
||||
|
||||
mockPrisma.teamMember.findMany.mockResolvedValue([]);
|
||||
await teamService.getTeamsOfUser(dbTeamMember.userUid, cursor);
|
||||
|
||||
expect(mockPrisma.teamMember.findMany).toHaveBeenCalledWith({
|
||||
take: 10,
|
||||
skip: 1,
|
||||
cursor: {
|
||||
teamID_userUid: {
|
||||
teamID: cursor,
|
||||
userUid: dbTeamMember.userUid,
|
||||
},
|
||||
},
|
||||
where: {
|
||||
userUid: dbTeamMember.userUid,
|
||||
},
|
||||
include: {
|
||||
team: true,
|
||||
},
|
||||
});
|
||||
});
|
||||
|
||||
test('resolves with an empty array for an invalid cursor', () => {
|
||||
// Invalid cursors return an empty array
|
||||
mockPrisma.teamMember.findMany.mockResolvedValue([]);
|
||||
|
||||
return expect(
|
||||
teamService.getTeamsOfUser(dbTeamMember.userUid, 'invalidcursor'),
|
||||
).resolves.toHaveLength(0);
|
||||
});
|
||||
|
||||
test('resolves with an empty array for invalid user id and null cursor', () => {
|
||||
mockPrisma.teamMember.findMany.mockResolvedValue([]);
|
||||
|
||||
return expect(
|
||||
teamService.getTeamsOfUser('invalidid', null),
|
||||
).resolves.toHaveLength(0);
|
||||
});
|
||||
|
||||
test('resolves with an empty array for invalid user id and invalid cursor', () => {
|
||||
mockPrisma.teamMember.findMany.mockResolvedValue([]);
|
||||
|
||||
return expect(
|
||||
teamService.getTeamsOfUser('invalidId', 'invalidCursor'),
|
||||
).resolves.toHaveLength(0);
|
||||
});
|
||||
});
|
||||
|
||||
describe('deleteUserFromAllTeams', () => {
|
||||
test('should return undefined when a valid uid is passed and user is deleted from all teams', async () => {
|
||||
mockPrisma.teamMember.findMany.mockResolvedValue([dbTeamMember]);
|
||||
mockPrisma.teamMember.count.mockResolvedValue(2);
|
||||
mockPrisma.teamMember.findUnique.mockResolvedValue(dbTeamMember);
|
||||
|
||||
const result = await teamService.deleteUserFromAllTeams(
|
||||
dbTeamMember.userUid,
|
||||
)();
|
||||
|
||||
expect(mockPrisma.teamMember.findMany).toHaveBeenCalledWith({
|
||||
where: {
|
||||
userUid: dbTeamMember.userUid,
|
||||
},
|
||||
});
|
||||
|
||||
expect(result).toBeUndefined();
|
||||
});
|
||||
|
||||
test('should return undefined when user has no data or the uid is invalid', async () => {
|
||||
mockPrisma.teamMember.findMany.mockResolvedValue([]);
|
||||
|
||||
const result = await teamService.deleteUserFromAllTeams(
|
||||
dbTeamMember.userUid,
|
||||
)();
|
||||
|
||||
expect(mockPrisma.teamMember.findMany).toHaveBeenCalledWith({
|
||||
where: {
|
||||
userUid: dbTeamMember.userUid,
|
||||
},
|
||||
});
|
||||
|
||||
expect(result).toBeUndefined();
|
||||
});
|
||||
|
||||
test('should reject when user is an OWNER in a team with only 1 member', async () => {
|
||||
mockPrisma.teamMember.findMany.mockResolvedValue([dbTeamMember]);
|
||||
mockPrisma.teamMember.count.mockResolvedValue(1);
|
||||
mockPrisma.teamMember.findUnique.mockResolvedValue({
|
||||
...dbTeamMember,
|
||||
role: TeamMemberRole.OWNER,
|
||||
});
|
||||
|
||||
const result = teamService.deleteUserFromAllTeams(dbTeamMember.userUid)();
|
||||
|
||||
await expect(result).rejects.toThrowError(TEAM_ONLY_ONE_OWNER);
|
||||
expect(mockPrisma.teamMember.findMany).toHaveBeenCalledWith({
|
||||
where: {
|
||||
userUid: dbTeamMember.userUid,
|
||||
},
|
||||
});
|
||||
});
|
||||
|
||||
test('should reject when a valid uid is passed but fetching teamMember details errors out', async () => {
|
||||
mockPrisma.teamMember.findMany.mockResolvedValue([
|
||||
{
|
||||
...dbTeamMember,
|
||||
role: TeamMemberRole.OWNER,
|
||||
},
|
||||
]);
|
||||
mockPrisma.teamMember.count.mockResolvedValue(2);
|
||||
|
||||
// findUnique while getTeamMember() is called errors out
|
||||
mockPrisma.teamMember.findUnique.mockRejectedValueOnce('NotFoundError');
|
||||
|
||||
const result = teamService.deleteUserFromAllTeams(dbTeamMember.userUid);
|
||||
|
||||
await expect(result).rejects.toThrowError(TEAM_INVALID_ID_OR_USER);
|
||||
expect(mockPrisma.teamMember.findMany).toHaveBeenCalledWith({
|
||||
where: {
|
||||
userUid: dbTeamMember.userUid,
|
||||
},
|
||||
});
|
||||
});
|
||||
});
|
||||
492
packages/hoppscotch-backend/src/team/team.service.ts
Normal file
492
packages/hoppscotch-backend/src/team/team.service.ts
Normal file
@@ -0,0 +1,492 @@
|
||||
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 { User } from 'src/user/user.model';
|
||||
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';
|
||||
|
||||
@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: User): TO.TaskOption<string> {
|
||||
return pipe(
|
||||
this.isUserOwnerRoleInTeams(user.uid),
|
||||
TO.fromTask,
|
||||
TO.chain((isOwner) => (isOwner ? TO.some(USER_IS_OWNER) : TO.none)),
|
||||
);
|
||||
}
|
||||
|
||||
onUserDelete(user: User): 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);
|
||||
}
|
||||
|
||||
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);
|
||||
}
|
||||
}
|
||||
11
packages/hoppscotch-backend/src/user/user.data.handler.ts
Normal file
11
packages/hoppscotch-backend/src/user/user.data.handler.ts
Normal file
@@ -0,0 +1,11 @@
|
||||
import * as T from "fp-ts/Task"
|
||||
import * as TO from "fp-ts/TaskOption"
|
||||
import { User } from "src/user/user.model"
|
||||
|
||||
/**
|
||||
* Defines how external services should handle User Data and User data related operations and actions.
|
||||
*/
|
||||
export interface UserDataHandler {
|
||||
canAllowUserDeletion: (user: User) => TO.TaskOption<string>
|
||||
onUserDelete: (user: User) => T.Task<void>
|
||||
}
|
||||
@@ -8,6 +8,7 @@ import { SessionType, User } from './user.model';
|
||||
import { USER_UPDATE_FAILED } from 'src/errors';
|
||||
import { PubSubService } from 'src/pubsub/pubsub.service';
|
||||
import { stringToJson } from 'src/utils';
|
||||
import { UserDataHandler } from './user.data.handler';
|
||||
|
||||
@Injectable()
|
||||
export class UserService {
|
||||
@@ -16,13 +17,19 @@ export class UserService {
|
||||
private readonly pubsub: PubSubService,
|
||||
) {}
|
||||
|
||||
private userDataHandlers: UserDataHandler[] = [];
|
||||
|
||||
registerUserDataHandler(handler: UserDataHandler) {
|
||||
this.userDataHandlers.push(handler);
|
||||
}
|
||||
|
||||
/**
|
||||
* Find User with given email id
|
||||
*
|
||||
* @param email User's email
|
||||
* @returns Option of found User
|
||||
*/
|
||||
async findUserByEmail(email: string) {
|
||||
async findUserByEmail(email: string): Promise<O.None | O.Some<AuthUser>> {
|
||||
try {
|
||||
const user = await this.prisma.user.findUniqueOrThrow({
|
||||
where: {
|
||||
@@ -41,7 +48,7 @@ export class UserService {
|
||||
* @param userUid User ID
|
||||
* @returns Option of found User
|
||||
*/
|
||||
async findUserById(userUid: string) {
|
||||
async findUserById(userUid: string): Promise<O.None | O.Some<AuthUser>> {
|
||||
try {
|
||||
const user = await this.prisma.user.findUniqueOrThrow({
|
||||
where: {
|
||||
|
||||
Reference in New Issue
Block a user