From 6bc748a267e76a9915f94df16f0ee484f77c7757 Mon Sep 17 00:00:00 2001 From: Balu Babu Date: Thu, 13 Jul 2023 11:52:19 +0530 Subject: [PATCH] refactor: introduce team-environments into self-host refactored to pseudo-fp format (#3177) --- packages/hoppscotch-backend/src/errors.ts | 7 + .../gql-team-env-team.guard.ts | 73 +-- .../src/team-environments/input-type.args.ts | 41 ++ .../team-environments.resolver.ts | 108 ++--- .../team-environments.service.spec.ts | 193 +++----- .../team-environments.service.ts | 432 +++++++++--------- .../src/team-environments/team.resolver.ts | 2 +- 7 files changed, 412 insertions(+), 444 deletions(-) create mode 100644 packages/hoppscotch-backend/src/team-environments/input-type.args.ts diff --git a/packages/hoppscotch-backend/src/errors.ts b/packages/hoppscotch-backend/src/errors.ts index 7666e50e8..2e45d5033 100644 --- a/packages/hoppscotch-backend/src/errors.ts +++ b/packages/hoppscotch-backend/src/errors.ts @@ -312,6 +312,13 @@ export const SHORTCODE_ALREADY_EXISTS = 'shortcode/already_exists' as const; */ export const TEAM_ENVIRONMENT_NOT_FOUND = 'team_environment/not_found' as const; +/** + * Invalid TEAM ENVIRONMENT name + * (TeamEnvironmentsService) + */ +export const TEAM_ENVIRONMENT_SHORT_NAME = + 'team_environment/short_name' as const; + /** * The user is not a member of the team of the given environment * (GqlTeamEnvTeamGuard) diff --git a/packages/hoppscotch-backend/src/team-environments/gql-team-env-team.guard.ts b/packages/hoppscotch-backend/src/team-environments/gql-team-env-team.guard.ts index 181fd6c76..735ae8f5f 100644 --- a/packages/hoppscotch-backend/src/team-environments/gql-team-env-team.guard.ts +++ b/packages/hoppscotch-backend/src/team-environments/gql-team-env-team.guard.ts @@ -1,15 +1,5 @@ import { CanActivate, ExecutionContext, Injectable } from '@nestjs/common'; import { Reflector } from '@nestjs/core'; -import * as TE from 'fp-ts/TaskEither'; -import * as O from 'fp-ts/Option'; -import * as S from 'fp-ts/string'; -import { pipe } from 'fp-ts/function'; -import { - getAnnotatedRequiredRoles, - getGqlArg, - getUserFromGQLContext, - throwErr, -} from 'src/utils'; import { TeamEnvironmentsService } from './team-environments.service'; import { BUG_AUTH_NO_USER_CTX, @@ -19,6 +9,10 @@ import { TEAM_ENVIRONMENT_NOT_FOUND, } from 'src/errors'; import { TeamService } from 'src/team/team.service'; +import { GqlExecutionContext } from '@nestjs/graphql'; +import * as E from 'fp-ts/Either'; +import { TeamMemberRole } from '@prisma/client'; +import { throwErr } from 'src/utils'; /** * A guard which checks whether the caller of a GQL Operation @@ -33,50 +27,31 @@ export class GqlTeamEnvTeamGuard implements CanActivate { private readonly teamService: TeamService, ) {} - canActivate(context: ExecutionContext): Promise { - return pipe( - TE.Do, + async canActivate(context: ExecutionContext): Promise { + const requireRoles = this.reflector.get( + 'requiresTeamRole', + context.getHandler(), + ); + if (!requireRoles) throw new Error(BUG_TEAM_ENV_GUARD_NO_REQUIRE_ROLES); - TE.bindW('requiredRoles', () => - pipe( - getAnnotatedRequiredRoles(this.reflector, context), - TE.fromOption(() => BUG_TEAM_ENV_GUARD_NO_REQUIRE_ROLES), - ), - ), + const gqlExecCtx = GqlExecutionContext.create(context); - TE.bindW('user', () => - pipe( - getUserFromGQLContext(context), - TE.fromOption(() => BUG_AUTH_NO_USER_CTX), - ), - ), + const { user } = gqlExecCtx.getContext().req; + if (user == undefined) throw new Error(BUG_AUTH_NO_USER_CTX); - TE.bindW('envID', () => - pipe( - getGqlArg('id', context), - O.fromPredicate(S.isString), - TE.fromOption(() => BUG_TEAM_ENV_GUARD_NO_ENV_ID), - ), - ), + const { id } = gqlExecCtx.getArgs<{ id: string }>(); + if (!id) throwErr(BUG_TEAM_ENV_GUARD_NO_ENV_ID); - TE.bindW('membership', ({ envID, user }) => - pipe( - this.teamEnvironmentService.getTeamEnvironment(envID), - TE.fromTaskOption(() => TEAM_ENVIRONMENT_NOT_FOUND), - TE.chainW((env) => - pipe( - this.teamService.getTeamMemberTE(env.teamID, user.uid), - TE.mapLeft(() => TEAM_ENVIRONMENT_NOT_TEAM_MEMBER), - ), - ), - ), - ), + const teamEnvironment = + await this.teamEnvironmentService.getTeamEnvironment(id); + if (E.isLeft(teamEnvironment)) throwErr(TEAM_ENVIRONMENT_NOT_FOUND); - TE.map(({ membership, requiredRoles }) => - requiredRoles.includes(membership.role), - ), + const member = await this.teamService.getTeamMember( + teamEnvironment.right.teamID, + user.uid, + ); + if (!member) throwErr(TEAM_ENVIRONMENT_NOT_TEAM_MEMBER); - TE.getOrElse(throwErr), - )(); + return requireRoles.includes(member.role); } } diff --git a/packages/hoppscotch-backend/src/team-environments/input-type.args.ts b/packages/hoppscotch-backend/src/team-environments/input-type.args.ts new file mode 100644 index 000000000..393865fab --- /dev/null +++ b/packages/hoppscotch-backend/src/team-environments/input-type.args.ts @@ -0,0 +1,41 @@ +import { ArgsType, Field, ID } from '@nestjs/graphql'; + +@ArgsType() +export class CreateTeamEnvironmentArgs { + @Field({ + name: 'name', + description: 'Name of the Team Environment', + }) + name: string; + + @Field(() => ID, { + name: 'teamID', + description: 'ID of the Team', + }) + teamID: string; + + @Field({ + name: 'variables', + description: 'JSON string of the variables object', + }) + variables: string; +} + +@ArgsType() +export class UpdateTeamEnvironmentArgs { + @Field(() => ID, { + name: 'id', + description: 'ID of the Team Environment', + }) + id: string; + @Field({ + name: 'name', + description: 'Name of the Team Environment', + }) + name: string; + @Field({ + name: 'variables', + description: 'JSON string of the variables object', + }) + variables: string; +} diff --git a/packages/hoppscotch-backend/src/team-environments/team-environments.resolver.ts b/packages/hoppscotch-backend/src/team-environments/team-environments.resolver.ts index 6775fb4c3..6bece3854 100644 --- a/packages/hoppscotch-backend/src/team-environments/team-environments.resolver.ts +++ b/packages/hoppscotch-backend/src/team-environments/team-environments.resolver.ts @@ -13,6 +13,11 @@ import { throwErr } from 'src/utils'; import { GqlTeamEnvTeamGuard } from './gql-team-env-team.guard'; import { TeamEnvironment } from './team-environments.model'; import { TeamEnvironmentsService } from './team-environments.service'; +import * as E from 'fp-ts/Either'; +import { + CreateTeamEnvironmentArgs, + UpdateTeamEnvironmentArgs, +} from './input-type.args'; @UseGuards(GqlThrottlerGuard) @Resolver(() => 'TeamEnvironment') @@ -29,29 +34,18 @@ export class TeamEnvironmentsResolver { }) @UseGuards(GqlAuthGuard, GqlTeamMemberGuard) @RequiresTeamRole(TeamMemberRole.OWNER, TeamMemberRole.EDITOR) - createTeamEnvironment( - @Args({ - name: 'name', - description: 'Name of the Team Environment', - }) - name: string, - @Args({ - name: 'teamID', - description: 'ID of the Team', - type: () => ID, - }) - teamID: string, - @Args({ - name: 'variables', - description: 'JSON string of the variables object', - }) - variables: string, + async createTeamEnvironment( + @Args() args: CreateTeamEnvironmentArgs, ): Promise { - return this.teamEnvironmentsService.createTeamEnvironment( - name, - teamID, - variables, - )(); + const teamEnvironment = + await this.teamEnvironmentsService.createTeamEnvironment( + args.name, + args.teamID, + args.variables, + ); + + if (E.isLeft(teamEnvironment)) throwErr(teamEnvironment.left); + return teamEnvironment.right; } @Mutation(() => Boolean, { @@ -59,7 +53,7 @@ export class TeamEnvironmentsResolver { }) @UseGuards(GqlAuthGuard, GqlTeamEnvTeamGuard) @RequiresTeamRole(TeamMemberRole.OWNER, TeamMemberRole.EDITOR) - deleteTeamEnvironment( + async deleteTeamEnvironment( @Args({ name: 'id', description: 'ID of the Team Environment', @@ -67,10 +61,12 @@ export class TeamEnvironmentsResolver { }) id: string, ): Promise { - return pipe( - this.teamEnvironmentsService.deleteTeamEnvironment(id), - TE.getOrElse(throwErr), - )(); + const isDeleted = await this.teamEnvironmentsService.deleteTeamEnvironment( + id, + ); + + if (E.isLeft(isDeleted)) throwErr(isDeleted.left); + return isDeleted.right; } @Mutation(() => TeamEnvironment, { @@ -79,28 +75,19 @@ export class TeamEnvironmentsResolver { }) @UseGuards(GqlAuthGuard, GqlTeamEnvTeamGuard) @RequiresTeamRole(TeamMemberRole.OWNER, TeamMemberRole.EDITOR) - updateTeamEnvironment( - @Args({ - name: 'id', - description: 'ID of the Team Environment', - type: () => ID, - }) - id: string, - @Args({ - name: 'name', - description: 'Name of the Team Environment', - }) - name: string, - @Args({ - name: 'variables', - description: 'JSON string of the variables object', - }) - variables: string, + async updateTeamEnvironment( + @Args() + args: UpdateTeamEnvironmentArgs, ): Promise { - return pipe( - this.teamEnvironmentsService.updateTeamEnvironment(id, name, variables), - TE.getOrElse(throwErr), - )(); + const updatedTeamEnvironment = + await this.teamEnvironmentsService.updateTeamEnvironment( + args.id, + args.name, + args.variables, + ); + + if (E.isLeft(updatedTeamEnvironment)) throwErr(updatedTeamEnvironment.left); + return updatedTeamEnvironment.right; } @Mutation(() => TeamEnvironment, { @@ -108,7 +95,7 @@ export class TeamEnvironmentsResolver { }) @UseGuards(GqlAuthGuard, GqlTeamEnvTeamGuard) @RequiresTeamRole(TeamMemberRole.OWNER, TeamMemberRole.EDITOR) - deleteAllVariablesFromTeamEnvironment( + async deleteAllVariablesFromTeamEnvironment( @Args({ name: 'id', description: 'ID of the Team Environment', @@ -116,10 +103,13 @@ export class TeamEnvironmentsResolver { }) id: string, ): Promise { - return pipe( - this.teamEnvironmentsService.deleteAllVariablesFromTeamEnvironment(id), - TE.getOrElse(throwErr), - )(); + const teamEnvironment = + await this.teamEnvironmentsService.deleteAllVariablesFromTeamEnvironment( + id, + ); + + if (E.isLeft(teamEnvironment)) throwErr(teamEnvironment.left); + return teamEnvironment.right; } @Mutation(() => TeamEnvironment, { @@ -127,7 +117,7 @@ export class TeamEnvironmentsResolver { }) @UseGuards(GqlAuthGuard, GqlTeamEnvTeamGuard) @RequiresTeamRole(TeamMemberRole.OWNER, TeamMemberRole.EDITOR) - createDuplicateEnvironment( + async createDuplicateEnvironment( @Args({ name: 'id', description: 'ID of the Team Environment', @@ -135,10 +125,12 @@ export class TeamEnvironmentsResolver { }) id: string, ): Promise { - return pipe( - this.teamEnvironmentsService.createDuplicateEnvironment(id), - TE.getOrElse(throwErr), - )(); + const res = await this.teamEnvironmentsService.createDuplicateEnvironment( + id, + ); + + if (E.isLeft(res)) throwErr(res.left); + return res.right; } /* Subscriptions */ diff --git a/packages/hoppscotch-backend/src/team-environments/team-environments.service.spec.ts b/packages/hoppscotch-backend/src/team-environments/team-environments.service.spec.ts index 742d4ca3e..719e58007 100644 --- a/packages/hoppscotch-backend/src/team-environments/team-environments.service.spec.ts +++ b/packages/hoppscotch-backend/src/team-environments/team-environments.service.spec.ts @@ -2,7 +2,11 @@ import { mockDeep, mockReset } from 'jest-mock-extended'; import { PrismaService } from 'src/prisma/prisma.service'; import { TeamEnvironment } from './team-environments.model'; import { TeamEnvironmentsService } from './team-environments.service'; -import { TEAM_ENVIRONMENT_NOT_FOUND } from 'src/errors'; +import { + JSON_INVALID, + TEAM_ENVIRONMENT_NOT_FOUND, + TEAM_ENVIRONMENT_SHORT_NAME, +} from 'src/errors'; const mockPrisma = mockDeep(); @@ -31,125 +35,81 @@ beforeEach(() => { describe('TeamEnvironmentsService', () => { describe('getTeamEnvironment', () => { - test('queries the db with the id', async () => { - mockPrisma.teamEnvironment.findFirst.mockResolvedValue(teamEnvironment); - - await teamEnvironmentsService.getTeamEnvironment('123')(); - - expect(mockPrisma.teamEnvironment.findFirst).toHaveBeenCalledWith( - expect.objectContaining({ - where: { - id: '123', - }, - }), + test('should successfully return a TeamEnvironment with valid ID', async () => { + mockPrisma.teamEnvironment.findFirstOrThrow.mockResolvedValueOnce( + teamEnvironment, ); - }); - test('requests prisma to reject the query promise if not found', async () => { - mockPrisma.teamEnvironment.findFirst.mockResolvedValue(teamEnvironment); - - await teamEnvironmentsService.getTeamEnvironment('123')(); - - expect(mockPrisma.teamEnvironment.findFirst).toHaveBeenCalledWith( - expect.objectContaining({ - rejectOnNotFound: true, - }), + const result = await teamEnvironmentsService.getTeamEnvironment( + teamEnvironment.id, ); + expect(result).toEqualRight(teamEnvironment); }); - test('should return a Some of the correct environment if exists', async () => { - mockPrisma.teamEnvironment.findFirst.mockResolvedValue(teamEnvironment); + test('should throw TEAM_ENVIRONMENT_NOT_FOUND with invalid ID', async () => { + mockPrisma.teamEnvironment.findFirstOrThrow.mockRejectedValueOnce( + 'RejectOnNotFound', + ); - const result = await teamEnvironmentsService.getTeamEnvironment('123')(); - - expect(result).toEqualSome(teamEnvironment); - }); - - test('should return a None if the environment does not exist', async () => { - mockPrisma.teamEnvironment.findFirst.mockRejectedValue('NotFoundError'); - - const result = await teamEnvironmentsService.getTeamEnvironment('123')(); - - expect(result).toBeNone(); + const result = await teamEnvironmentsService.getTeamEnvironment( + teamEnvironment.id, + ); + expect(result).toEqualLeft(TEAM_ENVIRONMENT_NOT_FOUND); }); }); + describe('createTeamEnvironment', () => { - test('should create and return a new team environment given a valid name,variable and team ID', async () => { + test('should successfully create and return a new team environment given valid inputs', async () => { mockPrisma.teamEnvironment.create.mockResolvedValue(teamEnvironment); const result = await teamEnvironmentsService.createTeamEnvironment( teamEnvironment.name, teamEnvironment.teamID, JSON.stringify(teamEnvironment.variables), - )(); + ); - expect(result).toEqual({ - id: teamEnvironment.id, - name: teamEnvironment.name, - teamID: teamEnvironment.teamID, + expect(result).toEqualRight({ + ...teamEnvironment, variables: JSON.stringify(teamEnvironment.variables), }); }); - test('should reject if given team ID is invalid', async () => { - mockPrisma.teamEnvironment.create.mockRejectedValue(null as any); + test('should throw TEAM_ENVIRONMENT_SHORT_NAME if input TeamEnvironment name is invalid', async () => { + const result = await teamEnvironmentsService.createTeamEnvironment( + '12', + teamEnvironment.teamID, + JSON.stringify(teamEnvironment.variables), + ); - await expect( - teamEnvironmentsService.createTeamEnvironment( - teamEnvironment.name, - 'invalidteamid', - JSON.stringify(teamEnvironment.variables), - ), - ).rejects.toBeDefined(); - }); - - test('should reject if provided team environment name is not a string', async () => { - mockPrisma.teamEnvironment.create.mockRejectedValue(null as any); - - await expect( - teamEnvironmentsService.createTeamEnvironment( - null as any, - teamEnvironment.teamID, - JSON.stringify(teamEnvironment.variables), - ), - ).rejects.toBeDefined(); - }); - - test('should reject if provided variable is not a string', async () => { - mockPrisma.teamEnvironment.create.mockRejectedValue(null as any); - - await expect( - teamEnvironmentsService.createTeamEnvironment( - teamEnvironment.name, - teamEnvironment.teamID, - null as any, - ), - ).rejects.toBeDefined(); + expect(result).toEqualLeft(TEAM_ENVIRONMENT_SHORT_NAME); }); test('should send pubsub message to "team_environment//created" if team environment is created successfully', async () => { - mockPrisma.teamEnvironment.create.mockResolvedValueOnce(teamEnvironment); + mockPrisma.teamEnvironment.create.mockResolvedValue(teamEnvironment); const result = await teamEnvironmentsService.createTeamEnvironment( teamEnvironment.name, teamEnvironment.teamID, JSON.stringify(teamEnvironment.variables), - )(); + ); expect(mockPubSub.publish).toHaveBeenCalledWith( `team_environment/${teamEnvironment.teamID}/created`, - result, + { + ...teamEnvironment, + variables: JSON.stringify(teamEnvironment.variables), + }, ); }); }); describe('deleteTeamEnvironment', () => { - test('should resolve to true given a valid team environment ID', async () => { + test('should successfully delete a TeamEnvironment with a valid ID', async () => { mockPrisma.teamEnvironment.delete.mockResolvedValueOnce(teamEnvironment); const result = await teamEnvironmentsService.deleteTeamEnvironment( teamEnvironment.id, - )(); + ); expect(result).toEqualRight(true); }); @@ -159,7 +119,7 @@ describe('TeamEnvironmentsService', () => { const result = await teamEnvironmentsService.deleteTeamEnvironment( 'invalidid', - )(); + ); expect(result).toEqualLeft(TEAM_ENVIRONMENT_NOT_FOUND); }); @@ -169,7 +129,7 @@ describe('TeamEnvironmentsService', () => { const result = await teamEnvironmentsService.deleteTeamEnvironment( teamEnvironment.id, - )(); + ); expect(mockPubSub.publish).toHaveBeenCalledWith( `team_environment/${teamEnvironment.teamID}/deleted`, @@ -182,7 +142,7 @@ describe('TeamEnvironmentsService', () => { }); describe('updateVariablesInTeamEnvironment', () => { - test('should add new variable to a team environment', async () => { + test('should successfully add new variable to a team environment', async () => { mockPrisma.teamEnvironment.update.mockResolvedValueOnce({ ...teamEnvironment, variables: [{ key: 'value' }], @@ -192,7 +152,7 @@ describe('TeamEnvironmentsService', () => { teamEnvironment.id, teamEnvironment.name, JSON.stringify([{ key: 'value' }]), - )(); + ); expect(result).toEqualRight({ ...teamEnvironment, @@ -200,7 +160,7 @@ describe('TeamEnvironmentsService', () => { }); }); - test('should add new variable to already existing list of variables in a team environment', async () => { + test('should successfully add new variable to already existing list of variables in a team environment', async () => { mockPrisma.teamEnvironment.update.mockResolvedValueOnce({ ...teamEnvironment, variables: [{ key: 'value' }, { key_2: 'value_2' }], @@ -210,7 +170,7 @@ describe('TeamEnvironmentsService', () => { teamEnvironment.id, teamEnvironment.name, JSON.stringify([{ key: 'value' }, { key_2: 'value_2' }]), - )(); + ); expect(result).toEqualRight({ ...teamEnvironment, @@ -218,7 +178,7 @@ describe('TeamEnvironmentsService', () => { }); }); - test('should edit existing variables in a team environment', async () => { + test('should successfully edit existing variables in a team environment', async () => { mockPrisma.teamEnvironment.update.mockResolvedValueOnce({ ...teamEnvironment, variables: [{ key: '1234' }], @@ -228,7 +188,7 @@ describe('TeamEnvironmentsService', () => { teamEnvironment.id, teamEnvironment.name, JSON.stringify([{ key: '1234' }]), - )(); + ); expect(result).toEqualRight({ ...teamEnvironment, @@ -236,22 +196,7 @@ describe('TeamEnvironmentsService', () => { }); }); - test('should delete existing variable in a team environment', async () => { - mockPrisma.teamEnvironment.update.mockResolvedValueOnce(teamEnvironment); - - const result = await teamEnvironmentsService.updateTeamEnvironment( - teamEnvironment.id, - teamEnvironment.name, - JSON.stringify([{}]), - )(); - - expect(result).toEqualRight({ - ...teamEnvironment, - variables: JSON.stringify([{}]), - }); - }); - - test('should edit name of an existing team environment', async () => { + test('should successfully edit name of an existing team environment', async () => { mockPrisma.teamEnvironment.update.mockResolvedValueOnce({ ...teamEnvironment, variables: [{ key: '123' }], @@ -261,7 +206,7 @@ describe('TeamEnvironmentsService', () => { teamEnvironment.id, teamEnvironment.name, JSON.stringify([{ key: '123' }]), - )(); + ); expect(result).toEqualRight({ ...teamEnvironment, @@ -269,14 +214,24 @@ describe('TeamEnvironmentsService', () => { }); }); - test('should reject to TEAM_ENVIRONMMENT_NOT_FOUND if provided id is invalid', async () => { + test('should throw TEAM_ENVIRONMENT_SHORT_NAME if input TeamEnvironment name is invalid', async () => { + const result = await teamEnvironmentsService.updateTeamEnvironment( + teamEnvironment.id, + '12', + JSON.stringify([{ key: 'value' }]), + ); + + expect(result).toEqualLeft(TEAM_ENVIRONMENT_SHORT_NAME); + }); + + test('should throw TEAM_ENVIRONMMENT_NOT_FOUND if provided id is invalid', async () => { mockPrisma.teamEnvironment.update.mockRejectedValue('RecordNotFound'); const result = await teamEnvironmentsService.updateTeamEnvironment( 'invalidid', teamEnvironment.name, JSON.stringify(teamEnvironment.variables), - )(); + ); expect(result).toEqualLeft(TEAM_ENVIRONMENT_NOT_FOUND); }); @@ -288,7 +243,7 @@ describe('TeamEnvironmentsService', () => { teamEnvironment.id, teamEnvironment.name, JSON.stringify([{ key: 'value' }]), - )(); + ); expect(mockPubSub.publish).toHaveBeenCalledWith( `team_environment/${teamEnvironment.teamID}/updated`, @@ -301,13 +256,13 @@ describe('TeamEnvironmentsService', () => { }); describe('deleteAllVariablesFromTeamEnvironment', () => { - test('should delete all variables in a team environment', async () => { + test('should successfully delete all variables in a team environment', async () => { mockPrisma.teamEnvironment.update.mockResolvedValueOnce(teamEnvironment); const result = await teamEnvironmentsService.deleteAllVariablesFromTeamEnvironment( teamEnvironment.id, - )(); + ); expect(result).toEqualRight({ ...teamEnvironment, @@ -315,13 +270,13 @@ describe('TeamEnvironmentsService', () => { }); }); - test('should reject to TEAM_ENVIRONMMENT_NOT_FOUND if provided id is invalid', async () => { + test('should throw TEAM_ENVIRONMMENT_NOT_FOUND if provided id is invalid', async () => { mockPrisma.teamEnvironment.update.mockRejectedValue('RecordNotFound'); const result = await teamEnvironmentsService.deleteAllVariablesFromTeamEnvironment( 'invalidid', - )(); + ); expect(result).toEqualLeft(TEAM_ENVIRONMENT_NOT_FOUND); }); @@ -332,7 +287,7 @@ describe('TeamEnvironmentsService', () => { const result = await teamEnvironmentsService.deleteAllVariablesFromTeamEnvironment( teamEnvironment.id, - )(); + ); expect(mockPubSub.publish).toHaveBeenCalledWith( `team_environment/${teamEnvironment.teamID}/updated`, @@ -345,7 +300,7 @@ describe('TeamEnvironmentsService', () => { }); describe('createDuplicateEnvironment', () => { - test('should duplicate an existing team environment', async () => { + test('should successfully duplicate an existing team environment', async () => { mockPrisma.teamEnvironment.findFirst.mockResolvedValueOnce( teamEnvironment, ); @@ -357,21 +312,21 @@ describe('TeamEnvironmentsService', () => { const result = await teamEnvironmentsService.createDuplicateEnvironment( teamEnvironment.id, - )(); + ); expect(result).toEqualRight({ - ...teamEnvironment, id: 'newid', + ...teamEnvironment, variables: JSON.stringify(teamEnvironment.variables), }); }); - test('should reject to TEAM_ENVIRONMMENT_NOT_FOUND if provided id is invalid', async () => { + test('should throw TEAM_ENVIRONMMENT_NOT_FOUND if provided id is invalid', async () => { mockPrisma.teamEnvironment.findFirst.mockRejectedValue('NotFoundError'); const result = await teamEnvironmentsService.createDuplicateEnvironment( teamEnvironment.id, - )(); + ); expect(result).toEqualLeft(TEAM_ENVIRONMENT_NOT_FOUND); }); @@ -388,13 +343,13 @@ describe('TeamEnvironmentsService', () => { const result = await teamEnvironmentsService.createDuplicateEnvironment( teamEnvironment.id, - )(); + ); expect(mockPubSub.publish).toHaveBeenCalledWith( `team_environment/${teamEnvironment.teamID}/created`, { - ...teamEnvironment, id: 'newid', + ...teamEnvironment, variables: JSON.stringify([{}]), }, ); diff --git a/packages/hoppscotch-backend/src/team-environments/team-environments.service.ts b/packages/hoppscotch-backend/src/team-environments/team-environments.service.ts index 49e3d7bb1..d50a62a11 100644 --- a/packages/hoppscotch-backend/src/team-environments/team-environments.service.ts +++ b/packages/hoppscotch-backend/src/team-environments/team-environments.service.ts @@ -1,15 +1,14 @@ import { Injectable } from '@nestjs/common'; -import { pipe } from 'fp-ts/function'; -import * as T from 'fp-ts/Task'; -import * as TO from 'fp-ts/TaskOption'; -import * as TE from 'fp-ts/TaskEither'; -import * as A from 'fp-ts/Array'; -import { Prisma } from '@prisma/client'; +import { TeamEnvironment as DBTeamEnvironment, Prisma } from '@prisma/client'; import { PrismaService } from 'src/prisma/prisma.service'; import { PubSubService } from 'src/pubsub/pubsub.service'; import { TeamEnvironment } from './team-environments.model'; -import { TEAM_ENVIRONMENT_NOT_FOUND } from 'src/errors'; - +import { + TEAM_ENVIRONMENT_NOT_FOUND, + TEAM_ENVIRONMENT_SHORT_NAME, +} from 'src/errors'; +import * as E from 'fp-ts/Either'; +import { isValidLength } from 'src/utils'; @Injectable() export class TeamEnvironmentsService { constructor( @@ -17,219 +16,218 @@ export class TeamEnvironmentsService { private readonly pubsub: PubSubService, ) {} - getTeamEnvironment(id: string) { - return TO.tryCatch(() => - this.prisma.teamEnvironment.findFirst({ - where: { id }, + TITLE_LENGTH = 3; + + /** + * TeamEnvironments are saved in the DB in the following way + * [{ key: value }, { key: value },....] + * + */ + + /** + * Typecast a database TeamEnvironment to a TeamEnvironment model + * @param teamEnvironment database TeamEnvironment + * @returns TeamEnvironment model + */ + private cast(teamEnvironment: DBTeamEnvironment): TeamEnvironment { + return { + id: teamEnvironment.id, + name: teamEnvironment.name, + teamID: teamEnvironment.teamID, + variables: JSON.stringify(teamEnvironment.variables), + }; + } + + /** + * Get details of a TeamEnvironment. + * + * @param id TeamEnvironment ID + * @returns Either of a TeamEnvironment or error message + */ + async getTeamEnvironment(id: string) { + try { + const teamEnvironment = + await this.prisma.teamEnvironment.findFirstOrThrow({ + where: { id }, + }); + return E.right(teamEnvironment); + } catch (error) { + return E.left(TEAM_ENVIRONMENT_NOT_FOUND); + } + } + + /** + * Create a new TeamEnvironment. + * + * @param name name of new TeamEnvironment + * @param teamID teamID of new TeamEnvironment + * @param variables JSONified string of contents of new TeamEnvironment + * @returns Either of a TeamEnvironment or error message + */ + async createTeamEnvironment(name: string, teamID: string, variables: string) { + const isTitleValid = isValidLength(name, this.TITLE_LENGTH); + if (!isTitleValid) return E.left(TEAM_ENVIRONMENT_SHORT_NAME); + + const result = await this.prisma.teamEnvironment.create({ + data: { + name: name, + teamID: teamID, + variables: JSON.parse(variables), + }, + }); + + const createdTeamEnvironment = this.cast(result); + + this.pubsub.publish( + `team_environment/${createdTeamEnvironment.teamID}/created`, + createdTeamEnvironment, + ); + + return E.right(createdTeamEnvironment); + } + + /** + * Delete a TeamEnvironment. + * + * @param id TeamEnvironment ID + * @returns Either of boolean or error message + */ + async deleteTeamEnvironment(id: string) { + try { + const result = await this.prisma.teamEnvironment.delete({ + where: { + id: id, + }, + }); + + const deletedTeamEnvironment = this.cast(result); + + this.pubsub.publish( + `team_environment/${deletedTeamEnvironment.teamID}/deleted`, + deletedTeamEnvironment, + ); + + return E.right(true); + } catch (error) { + return E.left(TEAM_ENVIRONMENT_NOT_FOUND); + } + } + + /** + * Update a TeamEnvironment. + * + * @param id TeamEnvironment ID + * @param name TeamEnvironment name + * @param variables JSONified string of contents of new TeamEnvironment + * @returns Either of a TeamEnvironment or error message + */ + async updateTeamEnvironment(id: string, name: string, variables: string) { + try { + const isTitleValid = isValidLength(name, this.TITLE_LENGTH); + if (!isTitleValid) return E.left(TEAM_ENVIRONMENT_SHORT_NAME); + + const result = await this.prisma.teamEnvironment.update({ + where: { id: id }, + data: { + name, + variables: JSON.parse(variables), + }, + }); + + const updatedTeamEnvironment = this.cast(result); + + this.pubsub.publish( + `team_environment/${updatedTeamEnvironment.teamID}/updated`, + updatedTeamEnvironment, + ); + + return E.right(updatedTeamEnvironment); + } catch (error) { + return E.left(TEAM_ENVIRONMENT_NOT_FOUND); + } + } + + /** + * Clear contents of a TeamEnvironment. + * + * @param id TeamEnvironment ID + * @returns Either of a TeamEnvironment or error message + */ + async deleteAllVariablesFromTeamEnvironment(id: string) { + try { + const result = await this.prisma.teamEnvironment.update({ + where: { id: id }, + data: { + variables: [], + }, + }); + + const teamEnvironment = this.cast(result); + + this.pubsub.publish( + `team_environment/${teamEnvironment.teamID}/updated`, + teamEnvironment, + ); + + return E.right(teamEnvironment); + } catch (error) { + return E.left(TEAM_ENVIRONMENT_NOT_FOUND); + } + } + + /** + * Create a duplicate of a existing TeamEnvironment. + * + * @param id TeamEnvironment ID + * @returns Either of a TeamEnvironment or error message + */ + async createDuplicateEnvironment(id: string) { + try { + const environment = await this.prisma.teamEnvironment.findFirst({ + where: { + id: id, + }, rejectOnNotFound: true, - }), - ); + }); + + const result = await this.prisma.teamEnvironment.create({ + data: { + name: environment.name, + teamID: environment.teamID, + variables: environment.variables as Prisma.JsonArray, + }, + }); + + const duplicatedTeamEnvironment = this.cast(result); + + this.pubsub.publish( + `team_environment/${duplicatedTeamEnvironment.teamID}/created`, + duplicatedTeamEnvironment, + ); + + return E.right(duplicatedTeamEnvironment); + } catch (error) { + return E.left(TEAM_ENVIRONMENT_NOT_FOUND); + } } - createTeamEnvironment(name: string, teamID: string, variables: string) { - return pipe( - () => - this.prisma.teamEnvironment.create({ - data: { - name: name, - teamID: teamID, - variables: JSON.parse(variables), - }, - }), - T.chainFirst( - (environment) => () => - this.pubsub.publish( - `team_environment/${environment.teamID}/created`, - { - id: environment.id, - name: environment.name, - teamID: environment.teamID, - variables: JSON.stringify(environment.variables), - }, - ), - ), - T.map((data) => { - return { - id: data.id, - name: data.name, - teamID: data.teamID, - variables: JSON.stringify(data.variables), - }; - }), - ); - } + /** + * Fetch all TeamEnvironments of a team. + * + * @param teamID teamID of new TeamEnvironment + * @returns List of TeamEnvironments + */ + async fetchAllTeamEnvironments(teamID: string) { + const result = await this.prisma.teamEnvironment.findMany({ + where: { + teamID: teamID, + }, + }); + const teamEnvironments = result.map((item) => { + return this.cast(item); + }); - deleteTeamEnvironment(id: string) { - return pipe( - TE.tryCatch( - () => - this.prisma.teamEnvironment.delete({ - where: { - id: id, - }, - }), - () => TEAM_ENVIRONMENT_NOT_FOUND, - ), - TE.chainFirst((environment) => - TE.fromTask(() => - this.pubsub.publish( - `team_environment/${environment.teamID}/deleted`, - { - id: environment.id, - name: environment.name, - teamID: environment.teamID, - variables: JSON.stringify(environment.variables), - }, - ), - ), - ), - TE.map((data) => true), - ); - } - - updateTeamEnvironment(id: string, name: string, variables: string) { - return pipe( - TE.tryCatch( - () => - this.prisma.teamEnvironment.update({ - where: { id: id }, - data: { - name, - variables: JSON.parse(variables), - }, - }), - () => TEAM_ENVIRONMENT_NOT_FOUND, - ), - TE.chainFirst((environment) => - TE.fromTask(() => - this.pubsub.publish( - `team_environment/${environment.teamID}/updated`, - { - id: environment.id, - name: environment.name, - teamID: environment.teamID, - variables: JSON.stringify(environment.variables), - }, - ), - ), - ), - TE.map( - (environment) => - { - id: environment.id, - name: environment.name, - teamID: environment.teamID, - variables: JSON.stringify(environment.variables), - }, - ), - ); - } - - deleteAllVariablesFromTeamEnvironment(id: string) { - return pipe( - TE.tryCatch( - () => - this.prisma.teamEnvironment.update({ - where: { id: id }, - data: { - variables: [], - }, - }), - () => TEAM_ENVIRONMENT_NOT_FOUND, - ), - TE.chainFirst((environment) => - TE.fromTask(() => - this.pubsub.publish( - `team_environment/${environment.teamID}/updated`, - { - id: environment.id, - name: environment.name, - teamID: environment.teamID, - variables: JSON.stringify(environment.variables), - }, - ), - ), - ), - TE.map( - (environment) => - { - id: environment.id, - name: environment.name, - teamID: environment.teamID, - variables: JSON.stringify(environment.variables), - }, - ), - ); - } - - createDuplicateEnvironment(id: string) { - return pipe( - TE.tryCatch( - () => - this.prisma.teamEnvironment.findFirst({ - where: { - id: id, - }, - rejectOnNotFound: true, - }), - () => TEAM_ENVIRONMENT_NOT_FOUND, - ), - TE.chain((environment) => - TE.fromTask(() => - this.prisma.teamEnvironment.create({ - data: { - name: environment.name, - teamID: environment.teamID, - variables: environment.variables as Prisma.JsonArray, - }, - }), - ), - ), - TE.chainFirst((environment) => - TE.fromTask(() => - this.pubsub.publish( - `team_environment/${environment.teamID}/created`, - { - id: environment.id, - name: environment.name, - teamID: environment.teamID, - variables: JSON.stringify(environment.variables), - }, - ), - ), - ), - TE.map( - (environment) => - { - id: environment.id, - name: environment.name, - teamID: environment.teamID, - variables: JSON.stringify(environment.variables), - }, - ), - ); - } - - fetchAllTeamEnvironments(teamID: string) { - return pipe( - () => - this.prisma.teamEnvironment.findMany({ - where: { - teamID: teamID, - }, - }), - T.map( - A.map( - (environment) => - { - id: environment.id, - name: environment.name, - teamID: environment.teamID, - variables: JSON.stringify(environment.variables), - }, - ), - ), - ); + return teamEnvironments; } /** diff --git a/packages/hoppscotch-backend/src/team-environments/team.resolver.ts b/packages/hoppscotch-backend/src/team-environments/team.resolver.ts index fb83db9d7..5416c4d41 100644 --- a/packages/hoppscotch-backend/src/team-environments/team.resolver.ts +++ b/packages/hoppscotch-backend/src/team-environments/team.resolver.ts @@ -11,6 +11,6 @@ export class TeamEnvsTeamResolver { description: 'Returns all Team Environments for the given Team', }) teamEnvironments(@Parent() team: Team): Promise { - return this.teamEnvironmentService.fetchAllTeamEnvironments(team.id)(); + return this.teamEnvironmentService.fetchAllTeamEnvironments(team.id); } }