Browse Source

refactor: introduce team-environments into self-host refactored to pseudo-fp format (#3177)

Balu Babu 1 year ago
parent
commit
6bc748a267

+ 7 - 0
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)

+ 24 - 49
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<boolean> {
-    return pipe(
-      TE.Do,
+  async canActivate(context: ExecutionContext): Promise<boolean> {
+    const requireRoles = this.reflector.get<TeamMemberRole[]>(
+      '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);
   }
 }

+ 41 - 0
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;
+}

+ 50 - 58
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<TeamEnvironment> {
-    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<boolean> {
-    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<TeamEnvironment> {
-    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<TeamEnvironment> {
-    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<TeamEnvironment> {
-    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 */

+ 74 - 119
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<PrismaService>();
 
@@ -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);
-
-      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')();
+    test('should throw TEAM_ENVIRONMENT_NOT_FOUND with invalid ID', async () => {
+      mockPrisma.teamEnvironment.findFirstOrThrow.mockRejectedValueOnce(
+        'RejectOnNotFound',
+      );
 
-      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(<TeamEnvironment>{
-        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);
-
-      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);
+    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,
-          teamEnvironment.teamID,
-          null as any,
-        ),
-      ).rejects.toBeDefined();
+      expect(result).toEqualLeft(TEAM_ENVIRONMENT_SHORT_NAME);
     });
 
     test('should send pubsub message to "team_environment/<teamID>/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>{
         ...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>{
         ...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>{
         ...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>{
-        ...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>{
         ...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>{
         ...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>{
-        ...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([{}]),
         },
       );

+ 206 - 208
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 },
-        rejectOnNotFound: true,
-      }),
-    );
+  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),
+    };
   }
 
-  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`,
-            <TeamEnvironment>{
-              id: environment.id,
-              name: environment.name,
-              teamID: environment.teamID,
-              variables: JSON.stringify(environment.variables),
-            },
-          ),
-      ),
-      T.map((data) => {
-        return <TeamEnvironment>{
-          id: data.id,
-          name: data.name,
-          teamID: data.teamID,
-          variables: JSON.stringify(data.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);
+    }
   }
 
-  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`,
-            <TeamEnvironment>{
-              id: environment.id,
-              name: environment.name,
-              teamID: environment.teamID,
-              variables: JSON.stringify(environment.variables),
-            },
-          ),
-        ),
-      ),
-      TE.map((data) => true),
+  /**
+   *  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);
   }
 
-  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`,
-            <TeamEnvironment>{
-              id: environment.id,
-              name: environment.name,
-              teamID: environment.teamID,
-              variables: JSON.stringify(environment.variables),
-            },
-          ),
-        ),
-      ),
-      TE.map(
-        (environment) =>
-          <TeamEnvironment>{
-            id: environment.id,
-            name: environment.name,
-            teamID: environment.teamID,
-            variables: JSON.stringify(environment.variables),
-          },
-      ),
-    );
+  /**
+   * 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);
+    }
   }
 
-  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`,
-            <TeamEnvironment>{
-              id: environment.id,
-              name: environment.name,
-              teamID: environment.teamID,
-              variables: JSON.stringify(environment.variables),
-            },
-          ),
-        ),
-      ),
-      TE.map(
-        (environment) =>
-          <TeamEnvironment>{
-            id: environment.id,
-            name: environment.name,
-            teamID: environment.teamID,
-            variables: JSON.stringify(environment.variables),
-          },
-      ),
-    );
+  /**
+   * 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);
+    }
   }
 
-  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`,
-            <TeamEnvironment>{
-              id: environment.id,
-              name: environment.name,
-              teamID: environment.teamID,
-              variables: JSON.stringify(environment.variables),
-            },
-          ),
-        ),
-      ),
-      TE.map(
-        (environment) =>
-          <TeamEnvironment>{
-            id: environment.id,
-            name: environment.name,
-            teamID: environment.teamID,
-            variables: JSON.stringify(environment.variables),
-          },
-      ),
-    );
+  /**
+   * 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);
+    }
   }
 
-  fetchAllTeamEnvironments(teamID: string) {
-    return pipe(
-      () =>
-        this.prisma.teamEnvironment.findMany({
-          where: {
-            teamID: teamID,
-          },
-        }),
-      T.map(
-        A.map(
-          (environment) =>
-            <TeamEnvironment>{
-              id: environment.id,
-              name: environment.name,
-              teamID: environment.teamID,
-              variables: JSON.stringify(environment.variables),
-            },
-        ),
-      ),
-    );
+  /**
+   * 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);
+    }
+  }
+
+  /**
+   * 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);
+    });
+
+    return teamEnvironments;
   }
 
   /**

+ 1 - 1
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<TeamEnvironment[]> {
-    return this.teamEnvironmentService.fetchAllTeamEnvironments(team.id)();
+    return this.teamEnvironmentService.fetchAllTeamEnvironments(team.id);
   }
 }