From b4b63f86d9c4436b65651a6389b4539b29bab679 Mon Sep 17 00:00:00 2001 From: ankitsridhar16 Date: Tue, 13 Dec 2022 13:14:13 +0530 Subject: [PATCH 01/35] feat: added user environment prisma schema --- .../migration.sql | 129 ++++++++++++++++++ .../prisma/migrations/migration_lock.toml | 3 + .../hoppscotch-backend/prisma/schema.prisma | 10 ++ 3 files changed, 142 insertions(+) create mode 100644 packages/hoppscotch-backend/prisma/migrations/20221213074249_create_user_environments/migration.sql create mode 100644 packages/hoppscotch-backend/prisma/migrations/migration_lock.toml diff --git a/packages/hoppscotch-backend/prisma/migrations/20221213074249_create_user_environments/migration.sql b/packages/hoppscotch-backend/prisma/migrations/20221213074249_create_user_environments/migration.sql new file mode 100644 index 000000000..09af18a03 --- /dev/null +++ b/packages/hoppscotch-backend/prisma/migrations/20221213074249_create_user_environments/migration.sql @@ -0,0 +1,129 @@ +-- CreateEnum +CREATE TYPE "TeamMemberRole" AS ENUM ('OWNER', 'VIEWER', 'EDITOR'); + +-- CreateTable +CREATE TABLE "Team" ( + "id" TEXT NOT NULL, + "name" TEXT NOT NULL, + + CONSTRAINT "Team_pkey" PRIMARY KEY ("id") +); + +-- CreateTable +CREATE TABLE "TeamMember" ( + "id" TEXT NOT NULL, + "role" "TeamMemberRole" NOT NULL, + "userUid" TEXT NOT NULL, + "teamID" TEXT NOT NULL, + + CONSTRAINT "TeamMember_pkey" PRIMARY KEY ("id") +); + +-- CreateTable +CREATE TABLE "TeamInvitation" ( + "id" TEXT NOT NULL, + "teamID" TEXT NOT NULL, + "creatorUid" TEXT NOT NULL, + "inviteeEmail" TEXT NOT NULL, + "inviteeRole" "TeamMemberRole" NOT NULL, + + CONSTRAINT "TeamInvitation_pkey" PRIMARY KEY ("id") +); + +-- CreateTable +CREATE TABLE "TeamCollection" ( + "id" TEXT NOT NULL, + "parentID" TEXT, + "teamID" TEXT NOT NULL, + "title" TEXT NOT NULL, + + CONSTRAINT "TeamCollection_pkey" PRIMARY KEY ("id") +); + +-- CreateTable +CREATE TABLE "TeamRequest" ( + "id" TEXT NOT NULL, + "collectionID" TEXT NOT NULL, + "teamID" TEXT NOT NULL, + "title" TEXT NOT NULL, + "request" JSONB NOT NULL, + + CONSTRAINT "TeamRequest_pkey" PRIMARY KEY ("id") +); + +-- CreateTable +CREATE TABLE "Shortcode" ( + "id" TEXT NOT NULL, + "request" JSONB NOT NULL, + "creatorUid" TEXT, + "createdOn" TIMESTAMP(3) NOT NULL DEFAULT CURRENT_TIMESTAMP, + + CONSTRAINT "Shortcode_pkey" PRIMARY KEY ("id") +); + +-- CreateTable +CREATE TABLE "TeamEnvironment" ( + "id" TEXT NOT NULL, + "teamID" TEXT NOT NULL, + "name" TEXT NOT NULL, + "variables" JSONB NOT NULL, + + CONSTRAINT "TeamEnvironment_pkey" PRIMARY KEY ("id") +); + +-- CreateTable +CREATE TABLE "User" ( + "uid" TEXT NOT NULL, + "displayName" TEXT, + "email" TEXT, + "photoURL" TEXT, + + CONSTRAINT "User_pkey" PRIMARY KEY ("uid") +); + +-- CreateTable +CREATE TABLE "UserEnvironment" ( + "id" TEXT NOT NULL, + "userUid" TEXT NOT NULL, + "name" TEXT, + "variables" JSONB NOT NULL, + "isGlobal" BOOLEAN NOT NULL, + + CONSTRAINT "UserEnvironment_pkey" PRIMARY KEY ("id") +); + +-- CreateIndex +CREATE UNIQUE INDEX "TeamMember_teamID_userUid_key" ON "TeamMember"("teamID", "userUid"); + +-- CreateIndex +CREATE INDEX "TeamInvitation_teamID_idx" ON "TeamInvitation"("teamID"); + +-- CreateIndex +CREATE UNIQUE INDEX "TeamInvitation_teamID_inviteeEmail_key" ON "TeamInvitation"("teamID", "inviteeEmail"); + +-- CreateIndex +CREATE UNIQUE INDEX "Shortcode_id_creatorUid_key" ON "Shortcode"("id", "creatorUid"); + +-- AddForeignKey +ALTER TABLE "TeamMember" ADD CONSTRAINT "TeamMember_teamID_fkey" FOREIGN KEY ("teamID") REFERENCES "Team"("id") ON DELETE CASCADE ON UPDATE CASCADE; + +-- AddForeignKey +ALTER TABLE "TeamInvitation" ADD CONSTRAINT "TeamInvitation_teamID_fkey" FOREIGN KEY ("teamID") REFERENCES "Team"("id") ON DELETE CASCADE ON UPDATE CASCADE; + +-- AddForeignKey +ALTER TABLE "TeamCollection" ADD CONSTRAINT "TeamCollection_parentID_fkey" FOREIGN KEY ("parentID") REFERENCES "TeamCollection"("id") ON DELETE SET NULL ON UPDATE CASCADE; + +-- AddForeignKey +ALTER TABLE "TeamCollection" ADD CONSTRAINT "TeamCollection_teamID_fkey" FOREIGN KEY ("teamID") REFERENCES "Team"("id") ON DELETE CASCADE ON UPDATE CASCADE; + +-- AddForeignKey +ALTER TABLE "TeamRequest" ADD CONSTRAINT "TeamRequest_collectionID_fkey" FOREIGN KEY ("collectionID") REFERENCES "TeamCollection"("id") ON DELETE CASCADE ON UPDATE CASCADE; + +-- AddForeignKey +ALTER TABLE "TeamRequest" ADD CONSTRAINT "TeamRequest_teamID_fkey" FOREIGN KEY ("teamID") REFERENCES "Team"("id") ON DELETE CASCADE ON UPDATE CASCADE; + +-- AddForeignKey +ALTER TABLE "TeamEnvironment" ADD CONSTRAINT "TeamEnvironment_teamID_fkey" FOREIGN KEY ("teamID") REFERENCES "Team"("id") ON DELETE CASCADE ON UPDATE CASCADE; + +-- AddForeignKey +ALTER TABLE "UserEnvironment" ADD CONSTRAINT "UserEnvironment_userUid_fkey" FOREIGN KEY ("userUid") REFERENCES "User"("uid") ON DELETE CASCADE ON UPDATE CASCADE; diff --git a/packages/hoppscotch-backend/prisma/migrations/migration_lock.toml b/packages/hoppscotch-backend/prisma/migrations/migration_lock.toml new file mode 100644 index 000000000..fbffa92c2 --- /dev/null +++ b/packages/hoppscotch-backend/prisma/migrations/migration_lock.toml @@ -0,0 +1,3 @@ +# Please do not edit this file manually +# It should be added in your version-control system (i.e. Git) +provider = "postgresql" \ No newline at end of file diff --git a/packages/hoppscotch-backend/prisma/schema.prisma b/packages/hoppscotch-backend/prisma/schema.prisma index 947d158a8..0d6e7c56d 100644 --- a/packages/hoppscotch-backend/prisma/schema.prisma +++ b/packages/hoppscotch-backend/prisma/schema.prisma @@ -83,6 +83,16 @@ model User { displayName String? email String? photoURL String? + UserEnvironments UserEnvironment[] +} + +model UserEnvironment { + id String @id @default(cuid()) + userUid String + user User @relation(fields: [userUid], references: [uid], onDelete: Cascade) + name String? + variables Json + isGlobal Boolean } enum TeamMemberRole { From ce94255a9edd41fe7b119217207775eec9ae9c82 Mon Sep 17 00:00:00 2001 From: ankitsridhar16 Date: Tue, 13 Dec 2022 13:27:51 +0530 Subject: [PATCH 02/35] feat: added user environment user environments resolvers, service files --- .../user-environments.model.ts | 30 ++ .../user-environments.module.ts | 18 ++ .../user-environments.resolver.ts | 187 +++++++++++++ .../user-environments.service.ts | 264 ++++++++++++++++++ .../src/user-environment/user.resolver.ts | 27 ++ 5 files changed, 526 insertions(+) create mode 100644 packages/hoppscotch-backend/src/user-environment/user-environments.model.ts create mode 100644 packages/hoppscotch-backend/src/user-environment/user-environments.module.ts create mode 100644 packages/hoppscotch-backend/src/user-environment/user-environments.resolver.ts create mode 100644 packages/hoppscotch-backend/src/user-environment/user-environments.service.ts create mode 100644 packages/hoppscotch-backend/src/user-environment/user.resolver.ts diff --git a/packages/hoppscotch-backend/src/user-environment/user-environments.model.ts b/packages/hoppscotch-backend/src/user-environment/user-environments.model.ts new file mode 100644 index 000000000..552299f60 --- /dev/null +++ b/packages/hoppscotch-backend/src/user-environment/user-environments.model.ts @@ -0,0 +1,30 @@ +import { Field, ID, ObjectType } from '@nestjs/graphql'; + +@ObjectType() +export class UserEnvironment { + @Field(() => ID, { + description: 'ID of the User Environment', + }) + id: string; + + @Field(() => ID, { + description: 'ID of the user this environment belongs to', + }) + userUid: string; + + @Field(() => String, { + nullable: true, + description: 'Name of the environment', + }) + name: string | null | undefined; + + @Field({ + description: 'All variables present in the environment', + }) + variables: string; // JSON string of the variables object (format:[{ key: "bla", value: "bla_val" }, ...] ) which will be received from the client + + @Field({ + description: 'isGlobal flag to indicate the environment is global or not', + }) + isGlobal: boolean; +} diff --git a/packages/hoppscotch-backend/src/user-environment/user-environments.module.ts b/packages/hoppscotch-backend/src/user-environment/user-environments.module.ts new file mode 100644 index 000000000..04f391682 --- /dev/null +++ b/packages/hoppscotch-backend/src/user-environment/user-environments.module.ts @@ -0,0 +1,18 @@ +import { Module } from '@nestjs/common'; +import { PrismaModule } from '../prisma/prisma.module'; +import { PubSubModule } from '../pubsub/pubsub.module'; +import { UserModule } from '../user/user.module'; +import { UserEnvsUserResolver } from './user.resolver'; +import { UserEnvironmentsResolver } from './user-environments.resolver'; +import { UserEnvironmentsService } from './user-environments.service'; + +@Module({ + imports: [PrismaModule, PubSubModule, UserModule], + providers: [ + UserEnvironmentsResolver, + UserEnvironmentsService, + UserEnvsUserResolver, + ], + exports: [UserEnvironmentsService], +}) +export class UserEnvironmentsModule {} diff --git a/packages/hoppscotch-backend/src/user-environment/user-environments.resolver.ts b/packages/hoppscotch-backend/src/user-environment/user-environments.resolver.ts new file mode 100644 index 000000000..bf1383f7d --- /dev/null +++ b/packages/hoppscotch-backend/src/user-environment/user-environments.resolver.ts @@ -0,0 +1,187 @@ +import { Args, ID, Mutation, Resolver, Subscription } from '@nestjs/graphql'; +import { PubSubService } from '../pubsub/pubsub.service'; +import { UserEnvironment } from './user-environments.model'; +import { UseGuards } from '@nestjs/common'; +import { GqlAuthGuard } from '../guards/gql-auth.guard'; +import { GqlUser } from '../decorators/gql-user.decorator'; +import { User } from '../user/user.model'; +import { UserEnvironmentsService } from './user-environments.service'; +import * as E from 'fp-ts/Either'; +import { throwErr } from 'src/utils'; + +@Resolver() +export class UserEnvironmentsResolver { + constructor( + private readonly userEnvironmentsService: UserEnvironmentsService, + private readonly pubsub: PubSubService, + ) {} + + /* Mutations */ + + @Mutation(() => UserEnvironment, { + description: + 'Create a new personal or global user environment for given user uid', + }) + @UseGuards(GqlAuthGuard) + async createUserEnvironment( + @GqlUser() user: User, + @Args({ + name: 'name', + description: + 'Name of the User Environment, if global send an empty string', + }) + name: string, + @Args({ + name: 'variables', + description: 'JSON string of the variables object', + }) + variables: string, + @Args({ + name: 'isGlobal', + description: 'isGlobal flag to indicate personal or global environment', + }) + isGlobal: boolean, + ): Promise { + const userEnvironment = + await this.userEnvironmentsService.createUserEnvironment( + user.uid, + name, + variables, + isGlobal, + ); + if (E.isLeft(userEnvironment)) throwErr(userEnvironment.left); + return userEnvironment.right; + } + + @Mutation(() => UserEnvironment, { + description: + 'Update a users personal or global environment based on environment id', + }) + @UseGuards(GqlAuthGuard) + async updateUserEnvironment( + @Args({ + name: 'id', + description: 'ID of the user environment', + type: () => ID, + }) + id: string, + @Args({ + name: 'name', + description: + 'Name of the User Environment, if global send an empty string', + }) + name: string, + @Args({ + name: 'variables', + description: 'JSON string of the variables object', + }) + variables: string, + ): Promise { + const userEnvironment = + await this.userEnvironmentsService.updateUserEnvironment( + id, + name, + variables, + ); + if (E.isLeft(userEnvironment)) throwErr(userEnvironment.left); + return userEnvironment.right; + } + + @Mutation(() => UserEnvironment, { + description: 'Deletes a users personal environment based on environment id', + }) + @UseGuards(GqlAuthGuard) + async deleteUserEnvironment( + @Args({ + name: 'id', + description: 'ID of the user environment', + type: () => ID, + }) + id: string, + ): Promise { + const userEnvironment = + await this.userEnvironmentsService.deleteUserEnvironment(id); + if (E.isLeft(userEnvironment)) throwErr(userEnvironment.left); + return userEnvironment.right; + } + + @Mutation(() => Number, { + description: 'Deletes users all personal environments', + }) + @UseGuards(GqlAuthGuard) + async deleteUserEnvironments(@GqlUser() user: User): Promise { + return await this.userEnvironmentsService.deleteUserEnvironments(user.uid); + } + + @Mutation(() => UserEnvironment, { + description: 'Deletes all variables inside a users global environment', + }) + @UseGuards(GqlAuthGuard) + async deleteAllVariablesFromUsersGlobalEnvironment( + @GqlUser() user: User, + @Args({ + name: 'id', + description: 'ID of the users global environment', + type: () => ID, + }) + id: string, + ): Promise { + const userEnvironment = + await this.userEnvironmentsService.deleteAllVariablesFromUsersGlobalEnvironment( + user.uid, + id, + ); + if (E.isLeft(userEnvironment)) throwErr(userEnvironment.left); + return userEnvironment.right; + } + + /* Subscriptions */ + + @Subscription(() => UserEnvironment, { + description: 'Listen for User Environment Creation', + resolve: (value) => value, + }) + @UseGuards(GqlAuthGuard) + userEnvironmentCreated( + @Args({ + name: 'userUid', + description: 'users uid', + type: () => ID, + }) + userUid: string, + ) { + return this.pubsub.asyncIterator(`user_environment/${userUid}/created`); + } + + @Subscription(() => UserEnvironment, { + description: 'Listen for User Environment updates', + resolve: (value) => value, + }) + @UseGuards(GqlAuthGuard) + userEnvironmentUpdated( + @Args({ + name: 'id', + description: 'environment id', + type: () => ID, + }) + id: string, + ) { + return this.pubsub.asyncIterator(`user_environment/${id}/updated`); + } + + @Subscription(() => UserEnvironment, { + description: 'Listen for User Environment updates', + resolve: (value) => value, + }) + @UseGuards(GqlAuthGuard) + userEnvironmentDeleted( + @Args({ + name: 'id', + description: 'environment id', + type: () => ID, + }) + id: string, + ) { + return this.pubsub.asyncIterator(`user_environment/${id}/deleted`); + } +} diff --git a/packages/hoppscotch-backend/src/user-environment/user-environments.service.ts b/packages/hoppscotch-backend/src/user-environment/user-environments.service.ts new file mode 100644 index 000000000..fa322602e --- /dev/null +++ b/packages/hoppscotch-backend/src/user-environment/user-environments.service.ts @@ -0,0 +1,264 @@ +import { Injectable } from '@nestjs/common'; + +import { UserEnvironment } from './user-environments.model'; +import { PrismaService } from '../prisma/prisma.service'; +import { PubSubService } from '../pubsub/pubsub.service'; +import * as E from 'fp-ts/Either'; + +enum SubscriptionType { + Created = 'created', + Updated = 'updated', + Deleted = 'deleted', +} + +@Injectable() +export class UserEnvironmentsService { + constructor( + private readonly prisma: PrismaService, + private readonly pubsub: PubSubService, + ) {} + + /** + * Fetch personal and global user environments based on `isGlobal` flag + * @param uid Users uid + * @returns array of users personal and global environments + */ + async fetchUserEnvironments(uid: string) { + const environments = await this.prisma.userEnvironment.findMany({ + where: { + userUid: uid, + isGlobal: false, + }, + }); + + const userEnvironments: UserEnvironment[] = []; + environments.forEach((environment) => { + userEnvironments.push({ + userUid: environment.userUid, + id: environment.id, + name: environment.name, + variables: JSON.stringify(environment.variables), + isGlobal: environment.isGlobal, + }); + }); + return userEnvironments; + } + + /** + * Create a personal or global user environment + * @param uid Users uid + * @param name environments name + * @param variables environment variables + * @param isGlobal flag to indicate type of environment to create + * @returns an `UserEnvironment` object + */ + async createUserEnvironment( + uid: string, + name: string, + variables: string, + isGlobal: boolean, + ) { + if (isGlobal) { + const globalEnvExists = await this.checkForExistingGlobalEnv(uid); + if (E.isRight(globalEnvExists)) return E.left('global env exits'); + } + + const createdEnvironment = await this.prisma.userEnvironment.create({ + data: { + userUid: uid, + name: name, + variables: JSON.parse(variables), + isGlobal: isGlobal, + }, + }); + + const userEnvironment: UserEnvironment = { + userUid: createdEnvironment.userUid, + id: createdEnvironment.id, + name: createdEnvironment.name, + variables: JSON.stringify(createdEnvironment.variables), + isGlobal: createdEnvironment.isGlobal, + }; + // Publish subscription for environment creation + await this.publishUserEnvironmentCreatedSubscription( + userEnvironment, + SubscriptionType.Created, + ); + + return E.right(userEnvironment); + } + + /** + * Update an existing personal or global user environment + * @param id environment id + * @param name environments name + * @param variables environment variables + * @returns an Either of `UserEnvironment` or error + */ + async updateUserEnvironment(id: string, name: string, variables: string) { + try { + const updatedEnvironment = await this.prisma.userEnvironment.update({ + where: { id: id }, + data: { + name: name, + variables: JSON.parse(variables), + }, + }); + + const updatedUserEnvironment: UserEnvironment = { + userUid: updatedEnvironment.userUid, + id: updatedEnvironment.id, + name: updatedEnvironment.name, + variables: JSON.stringify(updatedEnvironment.variables), + isGlobal: updatedEnvironment.isGlobal, + }; + // Publish subscription for environment creation + await this.publishUserEnvironmentCreatedSubscription( + updatedUserEnvironment, + SubscriptionType.Updated, + ); + return E.right(updatedUserEnvironment); + } catch (e) { + return E.left('user_env not found'); + } + } + + /** + * Delete an existing personal user environment based on environment id + * @param id environment id + * @returns an Either of deleted `UserEnvironment` or error + */ + async deleteUserEnvironment(id: string) { + try { + const deletedEnvironment = await this.prisma.userEnvironment.delete({ + where: { + id: id, + }, + }); + + const deletedUserEnvironment: UserEnvironment = { + userUid: deletedEnvironment.userUid, + id: deletedEnvironment.id, + name: deletedEnvironment.name, + variables: JSON.stringify(deletedEnvironment.variables), + isGlobal: deletedEnvironment.isGlobal, + }; + // Publish subscription for environment creation + await this.publishUserEnvironmentCreatedSubscription( + deletedUserEnvironment, + SubscriptionType.Deleted, + ); + return E.right(deletedUserEnvironment); + } catch (e) { + return E.left('user_env not found'); + } + } + + /** + * Deletes all existing personal user environments + * @param id environment id + * @param isGlobal flag to indicate type of environment to delete + * @returns a count of environments deleted + */ + async deleteUserEnvironments(uid: string) { + const deletedEnvironments = await this.prisma.userEnvironment.deleteMany({ + where: { + userUid: uid, + isGlobal: false, + }, + }); + return deletedEnvironments.count; + } + + async deleteAllVariablesFromUsersGlobalEnvironment(uid: string, id: string) { + const globalEnvExists = await this.checkForExistingGlobalEnv(uid); + if (E.isRight(globalEnvExists) && !E.isLeft(globalEnvExists)) { + const env = globalEnvExists.right; + if (env.id === id) { + try { + const updatedEnvironment = await this.prisma.userEnvironment.update({ + where: { id: id }, + data: { + variables: [], + }, + }); + const updatedUserEnvironment: UserEnvironment = { + userUid: updatedEnvironment.userUid, + id: updatedEnvironment.id, + name: updatedEnvironment.name, + variables: JSON.stringify(updatedEnvironment.variables), + isGlobal: updatedEnvironment.isGlobal, + }; + // Publish subscription for environment creation + await this.publishUserEnvironmentCreatedSubscription( + updatedUserEnvironment, + SubscriptionType.Updated, + ); + return E.right(updatedUserEnvironment); + } catch (e) { + return E.left('user_env not found'); + } + } + } + return E.left('mismatch'); + } + + // Method to publish subscriptions based on the subscription type of the environment + async publishUserEnvironmentCreatedSubscription( + userEnv: UserEnvironment, + subscriptionType: SubscriptionType, + ) { + switch (subscriptionType) { + case SubscriptionType.Created: + await this.pubsub.publish( + `user_environment/${userEnv.userUid}/created`, + userEnv, + ); + break; + case SubscriptionType.Updated: + await this.pubsub.publish( + `user_environment/${userEnv.id}/updated`, + userEnv, + ); + break; + case SubscriptionType.Deleted: + await this.pubsub.publish( + `user_environment/${userEnv.id}/deleted`, + userEnv, + ); + break; + default: + break; + } + } + + private async checkForExistingGlobalEnv(uid: string) { + const globalEnv = await this.prisma.userEnvironment.findFirst({ + where: { + userUid: uid, + isGlobal: true, + }, + }); + if (globalEnv === null) return E.left('global env not exist'); + + return E.right(globalEnv); + } + + async fetchUserGlobalEnvironments(uid: string) { + const globalEnvironment = await this.prisma.userEnvironment.findFirst({ + where: { + userUid: uid, + isGlobal: true, + }, + rejectOnNotFound: true, + }); + + return { + userUid: globalEnvironment.userUid, + id: globalEnvironment.id, + name: globalEnvironment.name, + variables: JSON.stringify(globalEnvironment.variables), + isGlobal: globalEnvironment.isGlobal, + }; + } +} diff --git a/packages/hoppscotch-backend/src/user-environment/user.resolver.ts b/packages/hoppscotch-backend/src/user-environment/user.resolver.ts new file mode 100644 index 000000000..97acdac2c --- /dev/null +++ b/packages/hoppscotch-backend/src/user-environment/user.resolver.ts @@ -0,0 +1,27 @@ +import { Parent, ResolveField, Resolver } from '@nestjs/graphql'; +import { User } from 'src/user/user.model'; +import { UserEnvironment } from './user-environments.model'; +import { UserEnvironmentsService } from './user-environments.service'; + +@Resolver(() => User) +export class UserEnvsUserResolver { + constructor(private userEnvironmentsService: UserEnvironmentsService) {} + @ResolveField(() => [UserEnvironment], { + description: 'Returns a list of users personal environments', + }) + async environments(@Parent() user: User): Promise { + return await this.userEnvironmentsService.fetchUserEnvironments(user.uid); + } + + @ResolveField(() => UserEnvironment, { + description: + 'Returns a list of user variables inside a global environments', + }) + async globalEnvironments( + @Parent() user: User, + ): Promise { + return await this.userEnvironmentsService.fetchUserGlobalEnvironments( + user.uid, + ); + } +} From 8e038f6944e42b0c4b2cb6740d14b637dcdabcb1 Mon Sep 17 00:00:00 2001 From: ankitsridhar16 Date: Tue, 13 Dec 2022 16:10:58 +0530 Subject: [PATCH 03/35] feat: added user environments to app module --- packages/hoppscotch-backend/docker-compose.yml | 6 +++--- packages/hoppscotch-backend/src/app.module.ts | 2 ++ packages/hoppscotch-backend/src/prisma/prisma.service.ts | 2 +- 3 files changed, 6 insertions(+), 4 deletions(-) diff --git a/packages/hoppscotch-backend/docker-compose.yml b/packages/hoppscotch-backend/docker-compose.yml index 9b22e6f75..72bd1e8ac 100644 --- a/packages/hoppscotch-backend/docker-compose.yml +++ b/packages/hoppscotch-backend/docker-compose.yml @@ -6,9 +6,9 @@ services: - PRODUCTION=false - DATABASE_URL=postgresql://postgres:testpass@dev-db:5432/hoppscotch?connect_timeout=300 - PORT=3000 - volumes: - - .:/usr/src/app - - /usr/src/app/node_modules/ +# volumes: +# - .:/usr/src/app +# - /usr/src/app/node_modules/ depends_on: - dev-db ports: diff --git a/packages/hoppscotch-backend/src/app.module.ts b/packages/hoppscotch-backend/src/app.module.ts index 7614320b9..b9281e55d 100644 --- a/packages/hoppscotch-backend/src/app.module.ts +++ b/packages/hoppscotch-backend/src/app.module.ts @@ -3,6 +3,7 @@ import { GraphQLModule } from '@nestjs/graphql'; import { ApolloDriver, ApolloDriverConfig } from '@nestjs/apollo'; import { UserModule } from './user/user.module'; import { GQLComplexityPlugin } from './plugins/GQLComplexityPlugin'; +import { UserEnvironmentsModule } from './user-environment/user-environments.module'; @Module({ imports: [ @@ -44,6 +45,7 @@ import { GQLComplexityPlugin } from './plugins/GQLComplexityPlugin'; driver: ApolloDriver, }), UserModule, + UserEnvironmentsModule, ], providers: [GQLComplexityPlugin], }) diff --git a/packages/hoppscotch-backend/src/prisma/prisma.service.ts b/packages/hoppscotch-backend/src/prisma/prisma.service.ts index 5b962c430..70d38eebb 100644 --- a/packages/hoppscotch-backend/src/prisma/prisma.service.ts +++ b/packages/hoppscotch-backend/src/prisma/prisma.service.ts @@ -1,5 +1,5 @@ import { Injectable, OnModuleInit, OnModuleDestroy } from '@nestjs/common'; -import { PrismaClient } from '@prisma/client/scripts/default-index'; +import { PrismaClient } from 'prisma/prisma-client/scripts/default-index'; @Injectable() export class PrismaService From 08cc7114ac421584c20568fc64818d0f6818ae7a Mon Sep 17 00:00:00 2001 From: ankitsridhar16 Date: Thu, 15 Dec 2022 23:21:21 +0530 Subject: [PATCH 04/35] chore: minor changes to Dockerfile --- packages/hoppscotch-backend/Dockerfile | 4 ++-- packages/hoppscotch-backend/docker-compose.yml | 6 +++--- 2 files changed, 5 insertions(+), 5 deletions(-) diff --git a/packages/hoppscotch-backend/Dockerfile b/packages/hoppscotch-backend/Dockerfile index ab0c88f42..ed9736551 100644 --- a/packages/hoppscotch-backend/Dockerfile +++ b/packages/hoppscotch-backend/Dockerfile @@ -6,10 +6,10 @@ WORKDIR /usr/src/app RUN npm i -g pnpm # Prisma bits -COPY prisma ./ +COPY prisma ./prisma/ RUN pnpx prisma generate -# # NPM package install +# # PNPM package install COPY . . RUN pnpm i diff --git a/packages/hoppscotch-backend/docker-compose.yml b/packages/hoppscotch-backend/docker-compose.yml index 72bd1e8ac..9b22e6f75 100644 --- a/packages/hoppscotch-backend/docker-compose.yml +++ b/packages/hoppscotch-backend/docker-compose.yml @@ -6,9 +6,9 @@ services: - PRODUCTION=false - DATABASE_URL=postgresql://postgres:testpass@dev-db:5432/hoppscotch?connect_timeout=300 - PORT=3000 -# volumes: -# - .:/usr/src/app -# - /usr/src/app/node_modules/ + volumes: + - .:/usr/src/app + - /usr/src/app/node_modules/ depends_on: - dev-db ports: From 3392b1a1caab6701282d17650470db3d445a903e Mon Sep 17 00:00:00 2001 From: ankitsridhar16 Date: Thu, 15 Dec 2022 23:21:57 +0530 Subject: [PATCH 05/35] chore: minor changes to Dockerfilefor prisma service --- packages/hoppscotch-backend/src/prisma/prisma.service.ts | 2 +- 1 file changed, 1 insertion(+), 1 deletion(-) diff --git a/packages/hoppscotch-backend/src/prisma/prisma.service.ts b/packages/hoppscotch-backend/src/prisma/prisma.service.ts index 70d38eebb..8febf1b5b 100644 --- a/packages/hoppscotch-backend/src/prisma/prisma.service.ts +++ b/packages/hoppscotch-backend/src/prisma/prisma.service.ts @@ -1,5 +1,5 @@ import { Injectable, OnModuleInit, OnModuleDestroy } from '@nestjs/common'; -import { PrismaClient } from 'prisma/prisma-client/scripts/default-index'; +import { PrismaClient } from '@prisma/client'; @Injectable() export class PrismaService From 73532e41c5876c2951900bfaa76b0cc6db48193e Mon Sep 17 00:00:00 2001 From: ankitsridhar16 Date: Thu, 15 Dec 2022 23:23:13 +0530 Subject: [PATCH 06/35] chore: added jest and jest related setup files to support jest fp-ts --- packages/hoppscotch-backend/global.d.ts | 1 + packages/hoppscotch-backend/jest.setup.js | 1 + packages/hoppscotch-backend/package.json | 12 +++++++++--- packages/hoppscotch-backend/tsconfig.json | 3 ++- 4 files changed, 13 insertions(+), 4 deletions(-) create mode 100644 packages/hoppscotch-backend/global.d.ts create mode 100644 packages/hoppscotch-backend/jest.setup.js diff --git a/packages/hoppscotch-backend/global.d.ts b/packages/hoppscotch-backend/global.d.ts new file mode 100644 index 000000000..78769ebc0 --- /dev/null +++ b/packages/hoppscotch-backend/global.d.ts @@ -0,0 +1 @@ +import '@relmify/jest-fp-ts'; diff --git a/packages/hoppscotch-backend/jest.setup.js b/packages/hoppscotch-backend/jest.setup.js new file mode 100644 index 000000000..562234114 --- /dev/null +++ b/packages/hoppscotch-backend/jest.setup.js @@ -0,0 +1 @@ +require('@relmify/jest-fp-ts'); \ No newline at end of file diff --git a/packages/hoppscotch-backend/package.json b/packages/hoppscotch-backend/package.json index 6e3a31005..4f67b07a8 100644 --- a/packages/hoppscotch-backend/package.json +++ b/packages/hoppscotch-backend/package.json @@ -18,7 +18,8 @@ "test:watch": "jest --watch", "test:cov": "jest --coverage", "test:debug": "node --inspect-brk -r tsconfig-paths/register -r ts-node/register node_modules/.bin/jest --runInBand", - "test:e2e": "jest --config ./test/jest-e2e.json" + "test:e2e": "jest --config ./test/jest-e2e.json", + "postinstall": "prisma generate" }, "dependencies": { "@nestjs/apollo": "^10.1.6", @@ -29,11 +30,13 @@ "@prisma/client": "^4.7.1", "apollo-server-express": "^3.11.1", "apollo-server-plugin-base": "^3.7.1", + "express": "^4.17.1", "fp-ts": "^2.13.1", "graphql": "^15.5.0", "graphql-query-complexity": "^0.12.0", "graphql-redis-subscriptions": "^2.5.0", "graphql-subscriptions": "^2.0.0", + "io-ts": "^2.2.16", "ioredis": "^5.2.4", "prisma": "^4.7.1", "reflect-metadata": "^0.1.13", @@ -44,8 +47,9 @@ "@nestjs/cli": "^9.1.5", "@nestjs/schematics": "^9.0.3", "@nestjs/testing": "^9.2.1", + "@relmify/jest-fp-ts": "^2.0.2", "@types/express": "^4.17.14", - "@types/jest": "29.2.3", + "@types/jest": "^27.5.2", "@types/node": "^18.11.10", "@types/supertest": "^2.0.12", "@typescript-eslint/eslint-plugin": "^5.45.0", @@ -53,7 +57,8 @@ "eslint": "^8.29.0", "eslint-config-prettier": "^8.5.0", "eslint-plugin-prettier": "^4.2.1", - "jest": "29.3.1", + "jest": "^29.3.1", + "jest-mock-extended": "^3.0.1", "prettier": "^2.8.0", "source-map-support": "^0.5.21", "supertest": "^6.3.2", @@ -69,6 +74,7 @@ "json", "ts" ], + "setupFilesAfterEnv": ["@relmify/jest-fp-ts"], "rootDir": "src", "testRegex": ".*\\.spec\\.ts$", "transform": { diff --git a/packages/hoppscotch-backend/tsconfig.json b/packages/hoppscotch-backend/tsconfig.json index adb614cab..22cc39ab4 100644 --- a/packages/hoppscotch-backend/tsconfig.json +++ b/packages/hoppscotch-backend/tsconfig.json @@ -12,10 +12,11 @@ "baseUrl": "./", "incremental": true, "skipLibCheck": true, + "strict": false, "strictNullChecks": false, "noImplicitAny": false, "strictBindCallApply": false, "forceConsistentCasingInFileNames": false, - "noFallthroughCasesInSwitch": false + "noFallthroughCasesInSwitch": false, } } From 164c2463f556e13b9d7842fdf208ee158cca5b8e Mon Sep 17 00:00:00 2001 From: ankitsridhar16 Date: Thu, 15 Dec 2022 23:24:09 +0530 Subject: [PATCH 07/35] chore: added error messages for user environment related errors --- packages/hoppscotch-backend/src/errors.ts | 46 +++++++++++++++++++++++ 1 file changed, 46 insertions(+) diff --git a/packages/hoppscotch-backend/src/errors.ts b/packages/hoppscotch-backend/src/errors.ts index 794a06db5..62664d3f7 100644 --- a/packages/hoppscotch-backend/src/errors.ts +++ b/packages/hoppscotch-backend/src/errors.ts @@ -155,6 +155,52 @@ export const TEAM_ENVIRONMMENT_NOT_FOUND = export const TEAM_ENVIRONMENT_NOT_TEAM_MEMBER = 'team_environment/not_team_member' as const; +/** + * Global environment doesnt exists for the user + * (UserEnvironmentsService) + */ +export const USER_ENVIRONMENT_GLOBAL_ENV_DOESNT_EXISTS = + 'user_environment/global_env_doesnt_exists' as const; +/* + +/** + * Global environment already exists for the user + * (UserEnvironmentsService) + */ +export const USER_ENVIRONMENT_GLOBAL_ENV_EXISTS = + 'user_environment/global_env_already_exists' as const; +/* + +/** + * User environment doesn't exist for the user + * (UserEnvironmentsService) + */ +export const USER_ENVIRONMENT_ENV_DOESNT_EXISTS = + 'user_environment/user_env_doesnt_exists' as const; +/* + +/** + * Cannot delete the global user environment + * (UserEnvironmentsService) + */ +export const USER_ENVIRONMENT_GLOBAL_ENV_DELETION_FAILED = + 'user_environment/user_env_global_env_deletion_failed' as const; +/* + +/** + * User environment is not a global environment + * (UserEnvironmentsService) + */ +export const USER_ENVIRONMENT_IS_NOT_GLOBAL = + 'user_environment/user_env_is_not_global' as const; +/* + +/** + * User environment update failed + * (UserEnvironmentsService) + */ +export const USER_ENVIRONMENT_UPDATE_FAILED = + 'user_environment/user_env_update_failed' as const; /* |------------------------------------| From 6bd4fd91ffdcb8542a16bad122dfef2fcce07779 Mon Sep 17 00:00:00 2001 From: ankitsridhar16 Date: Thu, 15 Dec 2022 23:25:21 +0530 Subject: [PATCH 08/35] chore: updated user resolver with updated service methods --- .../src/user-environment/user.resolver.ts | 9 ++++++--- 1 file changed, 6 insertions(+), 3 deletions(-) diff --git a/packages/hoppscotch-backend/src/user-environment/user.resolver.ts b/packages/hoppscotch-backend/src/user-environment/user.resolver.ts index 97acdac2c..3dbfc949b 100644 --- a/packages/hoppscotch-backend/src/user-environment/user.resolver.ts +++ b/packages/hoppscotch-backend/src/user-environment/user.resolver.ts @@ -2,6 +2,8 @@ import { Parent, ResolveField, Resolver } from '@nestjs/graphql'; import { User } from 'src/user/user.model'; import { UserEnvironment } from './user-environments.model'; import { UserEnvironmentsService } from './user-environments.service'; +import * as E from 'fp-ts/Either'; +import { throwErr } from '../utils'; @Resolver(() => User) export class UserEnvsUserResolver { @@ -20,8 +22,9 @@ export class UserEnvsUserResolver { async globalEnvironments( @Parent() user: User, ): Promise { - return await this.userEnvironmentsService.fetchUserGlobalEnvironments( - user.uid, - ); + const userEnvironment = + await this.userEnvironmentsService.fetchUserGlobalEnvironment(user.uid); + if (E.isLeft(userEnvironment)) throwErr(userEnvironment.left); + return userEnvironment.right; } } From 9fb9fd4568172cdf10f1395376933e4a52e5de87 Mon Sep 17 00:00:00 2001 From: ankitsridhar16 Date: Thu, 15 Dec 2022 23:29:51 +0530 Subject: [PATCH 09/35] chore: added test files for user environment service --- .../user-environments.service.spec.ts | 515 ++++++++++++++++++ 1 file changed, 515 insertions(+) create mode 100644 packages/hoppscotch-backend/src/user-environment/user-environments.service.spec.ts diff --git a/packages/hoppscotch-backend/src/user-environment/user-environments.service.spec.ts b/packages/hoppscotch-backend/src/user-environment/user-environments.service.spec.ts new file mode 100644 index 000000000..ca5078005 --- /dev/null +++ b/packages/hoppscotch-backend/src/user-environment/user-environments.service.spec.ts @@ -0,0 +1,515 @@ +import { UserEnvironment } from './user-environments.model'; +import { mockDeep, mockReset } from 'jest-mock-extended'; +import { PrismaService } from '../prisma/prisma.service'; +import { UserEnvironmentsService } from './user-environments.service'; +import { + USER_ENVIRONMENT_ENV_DOESNT_EXISTS, + USER_ENVIRONMENT_GLOBAL_ENV_DELETION_FAILED, + USER_ENVIRONMENT_GLOBAL_ENV_EXISTS, +} from '../errors'; +import { PubSubService } from '../pubsub/pubsub.service'; + +const mockPrisma = mockDeep(); +const mockPubSub = mockDeep(); + +// eslint-disable-next-line @typescript-eslint/ban-ts-comment +// @ts-ignore +const userEnvironmentsService = new UserEnvironmentsService( + mockPrisma, + mockPubSub as any, +); + +enum SubscriptionType { + Created = 'created', + Updated = 'updated', + Deleted = 'deleted', +} + +const userPersonalEnvironments = [ + { + userUiD: 'abc123', + id: '123', + name: 'test', + variables: [{}], + isGlobal: false, + }, + { + userUiD: 'abc123', + id: '1234', + name: 'test2', + variables: [{}], + isGlobal: false, + }, +]; + +beforeEach(() => { + mockReset(mockPrisma); + mockPubSub.publish.mockClear(); +}); + +describe('UserEnvironmentsService', () => { + describe('fetchUserEnvironments', () => { + test('Should return a list of users personal environments', async () => { + mockPrisma.userEnvironment.findMany.mockResolvedValueOnce([ + { + userUid: 'abc123', + id: '123', + name: 'test', + variables: [{}], + isGlobal: false, + }, + { + userUid: 'abc123', + id: '1234', + name: 'test2', + variables: [{}], + isGlobal: false, + }, + ]); + + const userEnvironments: UserEnvironment[] = [ + { + userUid: userPersonalEnvironments[0].userUiD, + id: userPersonalEnvironments[0].id, + name: userPersonalEnvironments[0].name, + variables: JSON.stringify(userPersonalEnvironments[0].variables), + isGlobal: userPersonalEnvironments[0].isGlobal, + }, + { + userUid: userPersonalEnvironments[1].userUiD, + id: userPersonalEnvironments[1].id, + name: userPersonalEnvironments[1].name, + variables: JSON.stringify(userPersonalEnvironments[1].variables), + isGlobal: userPersonalEnvironments[1].isGlobal, + }, + ]; + return expect( + await userEnvironmentsService.fetchUserEnvironments('abc123'), + ).toEqual(userEnvironments); + }); + + test('Should return an empty list of users personal environments', async () => { + mockPrisma.userEnvironment.findMany.mockResolvedValueOnce([]); + + return expect( + await userEnvironmentsService.fetchUserEnvironments('testuser'), + ).toEqual([]); + }); + + test('Should return an empty list of users personal environments if user uid is invalid', async () => { + mockPrisma.userEnvironment.findMany.mockResolvedValueOnce([]); + + return expect( + await userEnvironmentsService.fetchUserEnvironments('invaliduid'), + ).toEqual([]); + }); + }); + + describe('fetchUserGlobalEnvironment', () => { + test('Should resolve right and return a Global Environment for the uid', async () => { + mockPrisma.userEnvironment.findFirst.mockResolvedValueOnce({ + id: 'genv1', + userUid: 'abc', + name: '', + variables: [{}], + isGlobal: true, + }); + + expect( + await userEnvironmentsService.fetchUserGlobalEnvironment('abc'), + ).toEqualRight({ + id: 'genv1', + userUid: 'abc', + name: '', + variables: JSON.stringify([{}]), + isGlobal: true, + }); + }); + + test('Should resolve left and return an error if global env it doesnt exists', async () => { + mockPrisma.userEnvironment.findFirst.mockResolvedValueOnce(null); + + expect( + await userEnvironmentsService.fetchUserGlobalEnvironment('abc'), + ).toEqualLeft(USER_ENVIRONMENT_ENV_DOESNT_EXISTS); + }); + }); + + describe('createUserEnvironment', () => { + test( + 'Should resolve right and create a users personal environment and return a `UserEnvironment` object ' + + 'and publish a subscription', + async () => { + mockPrisma.userEnvironment.create.mockResolvedValueOnce({ + userUid: 'abc123', + id: '123', + name: 'test', + variables: [{}], + isGlobal: false, + }); + + const result: UserEnvironment = { + userUid: 'abc123', + id: '123', + name: 'test', + variables: JSON.stringify([{}]), + isGlobal: false, + }; + + await userEnvironmentsService.publishUserEnvironmentSubscription( + result, + SubscriptionType.Created, + ); + + return expect( + await userEnvironmentsService.createUserEnvironment( + 'abc123', + 'test', + '[{}]', + false, + ), + ).toEqualRight(result); + }, + ); + + test( + 'Should resolve right and create a new users global environment and return a `UserEnvironment` object ' + + 'and publish a subscription', + async () => { + mockPrisma.userEnvironment.findFirst.mockResolvedValueOnce(null); + + mockPrisma.userEnvironment.create.mockResolvedValueOnce({ + userUid: 'abc123', + id: '123', + name: 'testgenv', + variables: [{}], + isGlobal: true, + }); + + const result: UserEnvironment = { + userUid: 'abc123', + id: '123', + name: 'testgenv', + variables: JSON.stringify([{}]), + isGlobal: true, + }; + + await userEnvironmentsService.publishUserEnvironmentSubscription( + result, + SubscriptionType.Created, + ); + return expect( + await userEnvironmentsService.createUserEnvironment( + 'abc123', + 'test', + '[{}]', + true, + ), + ).toEqualRight(result); + }, + ); + + test( + 'Should resolve left and not create a new users global environment if existing global env exists ' + + 'and not publish a subscription', + async () => { + mockPrisma.userEnvironment.findFirst.mockResolvedValueOnce({ + userUid: 'abc123', + id: '123', + name: 'testgenv', + variables: [{}], + isGlobal: true, + }); + + return expect( + await userEnvironmentsService.createUserEnvironment( + 'abc123', + 'test', + '[{}]', + true, + ), + ).toEqualLeft(USER_ENVIRONMENT_GLOBAL_ENV_EXISTS); + }, + ); + }); + + describe('UpdateUserEnvironment', () => { + test( + 'should resolve right and update a users personal or environment and return a `UserEnvironment` object ' + + 'and publish a subscription', + async () => { + mockPrisma.userEnvironment.update.mockResolvedValueOnce({ + userUid: 'abc123', + id: '123', + name: 'test', + variables: [{}], + isGlobal: false, + }); + + const result: UserEnvironment = { + userUid: 'abc123', + id: '123', + name: 'test', + variables: JSON.stringify([{}]), + isGlobal: false, + }; + + await userEnvironmentsService.publishUserEnvironmentSubscription( + result, + SubscriptionType.Updated, + ); + return expect( + await userEnvironmentsService.updateUserEnvironment( + 'abc123', + 'test', + '[{}]', + ), + ).toEqualRight(result); + }, + ); + + test( + 'should resolve right and update a users global environment and return a `UserEnvironment` object ' + + 'and publish a subscription', + async () => { + mockPrisma.userEnvironment.update.mockResolvedValueOnce({ + userUid: 'abc123', + id: '123', + name: '', + variables: [{}], + isGlobal: true, + }); + + const result: UserEnvironment = { + userUid: 'abc123', + id: '123', + name: '', + variables: JSON.stringify([{}]), + isGlobal: true, + }; + + await userEnvironmentsService.publishUserEnvironmentSubscription( + result, + SubscriptionType.Updated, + ); + return expect( + await userEnvironmentsService.updateUserEnvironment( + 'abc123', + '', + '[{}]', + ), + ).toEqualRight(result); + }, + ); + + test( + 'should resolve left and not update a users environment if env doesnt exist ' + + 'and publish a subscription', + async () => { + mockPrisma.userEnvironment.update.mockRejectedValueOnce({}); + + return expect( + await userEnvironmentsService.updateUserEnvironment( + 'abc123', + 'test', + '[{}]', + ), + ).toEqualLeft(USER_ENVIRONMENT_ENV_DOESNT_EXISTS); + }, + ); + }); + + describe('deleteUserEnvironment', () => { + test( + 'Should resolve right and delete a users personal environment and return a `UserEnvironment` object ' + + 'and publish a subscription', + async () => { + mockPrisma.userEnvironment.findFirst.mockResolvedValueOnce(null); + mockPrisma.userEnvironment.delete.mockResolvedValueOnce({ + userUid: 'abc123', + id: 'env1', + name: 'en1', + variables: [{}], + isGlobal: false, + }); + + const result: UserEnvironment = { + userUid: 'abc123', + id: 'env1', + name: 'en1', + variables: JSON.stringify([{}]), + isGlobal: false, + }; + + await userEnvironmentsService.publishUserEnvironmentSubscription( + result, + SubscriptionType.Deleted, + ); + + return expect( + await userEnvironmentsService.deleteUserEnvironment('abc123', 'env1'), + ).toEqualRight(result); + }, + ); + + test('Should resolve left and return an error when deleting a global user environment ', async () => { + mockPrisma.userEnvironment.findFirst.mockResolvedValueOnce({ + userUid: 'abc123', + id: 'genv1', + name: 'en1', + variables: [{}], + isGlobal: true, + }); + + return expect( + await userEnvironmentsService.deleteUserEnvironment('abc123', 'genv1'), + ).toEqualLeft(USER_ENVIRONMENT_GLOBAL_ENV_DELETION_FAILED); + }); + + test('Should resolve left and return an error when deleting an invalid user environment ', async () => { + mockPrisma.userEnvironment.delete.mockResolvedValueOnce(null); + + return expect( + await userEnvironmentsService.deleteUserEnvironment('abc123', 'env1'), + ).toEqualLeft(USER_ENVIRONMENT_ENV_DOESNT_EXISTS); + }); + }); + + describe('deleteUserEnvironments', () => { + test('Should return a count of users personal environment deleted', async () => { + mockPrisma.userEnvironment.deleteMany.mockResolvedValueOnce({ + count: 1, + }); + + return expect( + await userEnvironmentsService.deleteUserEnvironments('abc123'), + ).toEqual(1); + }); + }); + + describe('deleteAllVariablesFromUsersGlobalEnvironment', () => { + test( + 'Should resolve right and delete all variables inside users global environment and return a `UserEnvironment` object ' + + 'and publish a subscription', + async () => { + mockPrisma.userEnvironment.findFirst.mockResolvedValueOnce({ + userUid: 'abc123', + id: 'env1', + name: 'en1', + variables: [{}], + isGlobal: true, + }); + + mockPrisma.userEnvironment.update.mockResolvedValueOnce({ + userUid: 'abc123', + id: 'env1', + name: 'en1', + variables: [], + isGlobal: true, + }); + + const result: UserEnvironment = { + userUid: 'abc123', + id: 'env1', + name: 'en1', + variables: JSON.stringify([]), + isGlobal: true, + }; + + await userEnvironmentsService.publishUserEnvironmentSubscription( + result, + SubscriptionType.Updated, + ); + + return expect( + await userEnvironmentsService.deleteAllVariablesFromUsersGlobalEnvironment( + 'abc123', + 'env1', + ), + ).toEqualRight(result); + }, + ); + + test('Should resolve left and return an error if global environment id and passed id dont match ', async () => { + mockPrisma.userEnvironment.findFirst.mockResolvedValueOnce({ + userUid: 'abc123', + id: 'genv2', + name: 'en1', + variables: [{}], + isGlobal: true, + }); + + return expect( + await userEnvironmentsService.deleteUserEnvironment('abc123', 'genv1'), + ).toEqualLeft(USER_ENVIRONMENT_ENV_DOESNT_EXISTS); + }); + }); + + describe('publishUserEnvironmentSubscription', () => { + test('Should publish a created subscription', async () => { + const result: UserEnvironment = { + userUid: 'abc123', + id: '123', + name: '', + variables: JSON.stringify([{}]), + isGlobal: true, + }; + + await mockPubSub.publish( + `user_environment/${result.userUid}/created`, + result, + ); + + return expect( + await userEnvironmentsService.publishUserEnvironmentSubscription( + result, + SubscriptionType.Created, + ), + ).toBeUndefined(); + }); + + test('Should publish a updated subscription', async () => { + const result: UserEnvironment = { + userUid: 'abc123', + id: '123', + name: '', + variables: JSON.stringify([{}]), + isGlobal: true, + }; + + await mockPubSub.publish( + `user_environment/${result.userUid}/updated`, + result, + ); + + return expect( + await userEnvironmentsService.publishUserEnvironmentSubscription( + result, + SubscriptionType.Updated, + ), + ).toBeUndefined(); + }); + + test('Should publish a deleted subscription', async () => { + const result: UserEnvironment = { + userUid: 'abc123', + id: '123', + name: '', + variables: JSON.stringify([{}]), + isGlobal: true, + }; + + await mockPubSub.publish( + `user_environment/${result.userUid}/deleted`, + result, + ); + + return expect( + await userEnvironmentsService.publishUserEnvironmentSubscription( + result, + SubscriptionType.Deleted, + ), + ).toBeUndefined(); + }); + }); +}); From c87690f378650756559ec176fc5349f3221e243b Mon Sep 17 00:00:00 2001 From: ankitsridhar16 Date: Thu, 15 Dec 2022 23:31:12 +0530 Subject: [PATCH 10/35] chore: update resolvers and service files --- .../user-environments.resolver.ts | 5 +- .../user-environments.service.ts | 110 ++++++++++++------ 2 files changed, 75 insertions(+), 40 deletions(-) diff --git a/packages/hoppscotch-backend/src/user-environment/user-environments.resolver.ts b/packages/hoppscotch-backend/src/user-environment/user-environments.resolver.ts index bf1383f7d..76af01688 100644 --- a/packages/hoppscotch-backend/src/user-environment/user-environments.resolver.ts +++ b/packages/hoppscotch-backend/src/user-environment/user-environments.resolver.ts @@ -92,6 +92,7 @@ export class UserEnvironmentsResolver { }) @UseGuards(GqlAuthGuard) async deleteUserEnvironment( + @GqlUser() user: User, @Args({ name: 'id', description: 'ID of the user environment', @@ -100,7 +101,7 @@ export class UserEnvironmentsResolver { id: string, ): Promise { const userEnvironment = - await this.userEnvironmentsService.deleteUserEnvironment(id); + await this.userEnvironmentsService.deleteUserEnvironment(user.uid, id); if (E.isLeft(userEnvironment)) throwErr(userEnvironment.left); return userEnvironment.right; } @@ -170,7 +171,7 @@ export class UserEnvironmentsResolver { } @Subscription(() => UserEnvironment, { - description: 'Listen for User Environment updates', + description: 'Listen for User Environment deletion', resolve: (value) => value, }) @UseGuards(GqlAuthGuard) diff --git a/packages/hoppscotch-backend/src/user-environment/user-environments.service.ts b/packages/hoppscotch-backend/src/user-environment/user-environments.service.ts index fa322602e..b09d9a5e3 100644 --- a/packages/hoppscotch-backend/src/user-environment/user-environments.service.ts +++ b/packages/hoppscotch-backend/src/user-environment/user-environments.service.ts @@ -1,10 +1,18 @@ import { Injectable } from '@nestjs/common'; - import { UserEnvironment } from './user-environments.model'; import { PrismaService } from '../prisma/prisma.service'; import { PubSubService } from '../pubsub/pubsub.service'; import * as E from 'fp-ts/Either'; +import { + USER_ENVIRONMENT_ENV_DOESNT_EXISTS, + USER_ENVIRONMENT_GLOBAL_ENV_DELETION_FAILED, + USER_ENVIRONMENT_GLOBAL_ENV_DOESNT_EXISTS, + USER_ENVIRONMENT_GLOBAL_ENV_EXISTS, + USER_ENVIRONMENT_IS_NOT_GLOBAL, + USER_ENVIRONMENT_UPDATE_FAILED, +} from '../errors'; +// Contains constants for the subscription types we send to pubsub service enum SubscriptionType { Created = 'created', Updated = 'updated', @@ -19,9 +27,9 @@ export class UserEnvironmentsService { ) {} /** - * Fetch personal and global user environments based on `isGlobal` flag + * Fetch personal user environments * @param uid Users uid - * @returns array of users personal and global environments + * @returns array of users personal environments */ async fetchUserEnvironments(uid: string) { const environments = await this.prisma.userEnvironment.findMany({ @@ -44,6 +52,32 @@ export class UserEnvironmentsService { return userEnvironments; } + /** + * Fetch users global environment + * @param uid Users uid + * @returns an `UserEnvironment` object + */ + async fetchUserGlobalEnvironment(uid: string) { + const globalEnvironment = await this.prisma.userEnvironment.findFirst({ + where: { + userUid: uid, + isGlobal: true, + }, + }); + + if (globalEnvironment != null) { + return E.right({ + userUid: globalEnvironment.userUid, + id: globalEnvironment.id, + name: globalEnvironment.name, + variables: JSON.stringify(globalEnvironment.variables), + isGlobal: globalEnvironment.isGlobal, + }); + } + + return E.left(USER_ENVIRONMENT_ENV_DOESNT_EXISTS); + } + /** * Create a personal or global user environment * @param uid Users uid @@ -58,9 +92,11 @@ export class UserEnvironmentsService { variables: string, isGlobal: boolean, ) { + // Check for existing global env for a user if exists error out to avoid recreation if (isGlobal) { const globalEnvExists = await this.checkForExistingGlobalEnv(uid); - if (E.isRight(globalEnvExists)) return E.left('global env exits'); + if (E.isRight(globalEnvExists)) + return E.left(USER_ENVIRONMENT_GLOBAL_ENV_EXISTS); } const createdEnvironment = await this.prisma.userEnvironment.create({ @@ -80,7 +116,7 @@ export class UserEnvironmentsService { isGlobal: createdEnvironment.isGlobal, }; // Publish subscription for environment creation - await this.publishUserEnvironmentCreatedSubscription( + await this.publishUserEnvironmentSubscription( userEnvironment, SubscriptionType.Created, ); @@ -112,24 +148,33 @@ export class UserEnvironmentsService { variables: JSON.stringify(updatedEnvironment.variables), isGlobal: updatedEnvironment.isGlobal, }; - // Publish subscription for environment creation - await this.publishUserEnvironmentCreatedSubscription( + // Publish subscription for environment update + await this.publishUserEnvironmentSubscription( updatedUserEnvironment, SubscriptionType.Updated, ); return E.right(updatedUserEnvironment); } catch (e) { - return E.left('user_env not found'); + return E.left(USER_ENVIRONMENT_ENV_DOESNT_EXISTS); } } /** * Delete an existing personal user environment based on environment id + * @param uid users uid * @param id environment id * @returns an Either of deleted `UserEnvironment` or error */ - async deleteUserEnvironment(id: string) { + async deleteUserEnvironment(uid: string, id: string) { try { + // check if id is of a global environment if it is, don't delete and error out + const globalEnvExists = await this.checkForExistingGlobalEnv(uid); + if (E.isRight(globalEnvExists)) { + const globalEnv = globalEnvExists.right; + if (globalEnv.id === id) { + return E.left(USER_ENVIRONMENT_GLOBAL_ENV_DELETION_FAILED); + } + } const deletedEnvironment = await this.prisma.userEnvironment.delete({ where: { id: id, @@ -144,20 +189,19 @@ export class UserEnvironmentsService { isGlobal: deletedEnvironment.isGlobal, }; // Publish subscription for environment creation - await this.publishUserEnvironmentCreatedSubscription( + await this.publishUserEnvironmentSubscription( deletedUserEnvironment, SubscriptionType.Deleted, ); return E.right(deletedUserEnvironment); } catch (e) { - return E.left('user_env not found'); + return E.left(USER_ENVIRONMENT_ENV_DOESNT_EXISTS); } } /** * Deletes all existing personal user environments - * @param id environment id - * @param isGlobal flag to indicate type of environment to delete + * @param uid user uid * @returns a count of environments deleted */ async deleteUserEnvironments(uid: string) { @@ -170,9 +214,15 @@ export class UserEnvironmentsService { return deletedEnvironments.count; } + /** + * Deletes all existing variables in a users global environment + * @param uid users uid + * @param id environment id + * @returns an `` of environments deleted + */ async deleteAllVariablesFromUsersGlobalEnvironment(uid: string, id: string) { const globalEnvExists = await this.checkForExistingGlobalEnv(uid); - if (E.isRight(globalEnvExists) && !E.isLeft(globalEnvExists)) { + if (E.isRight(globalEnvExists)) { const env = globalEnvExists.right; if (env.id === id) { try { @@ -190,21 +240,21 @@ export class UserEnvironmentsService { isGlobal: updatedEnvironment.isGlobal, }; // Publish subscription for environment creation - await this.publishUserEnvironmentCreatedSubscription( + await this.publishUserEnvironmentSubscription( updatedUserEnvironment, SubscriptionType.Updated, ); return E.right(updatedUserEnvironment); } catch (e) { - return E.left('user_env not found'); + return E.left(USER_ENVIRONMENT_UPDATE_FAILED); } - } + } else return E.left(USER_ENVIRONMENT_IS_NOT_GLOBAL); } - return E.left('mismatch'); + return E.left(USER_ENVIRONMENT_ENV_DOESNT_EXISTS); } // Method to publish subscriptions based on the subscription type of the environment - async publishUserEnvironmentCreatedSubscription( + async publishUserEnvironmentSubscription( userEnv: UserEnvironment, subscriptionType: SubscriptionType, ) { @@ -232,6 +282,7 @@ export class UserEnvironmentsService { } } + // Method to check for existing global environments for a given user uid private async checkForExistingGlobalEnv(uid: string) { const globalEnv = await this.prisma.userEnvironment.findFirst({ where: { @@ -239,26 +290,9 @@ export class UserEnvironmentsService { isGlobal: true, }, }); - if (globalEnv === null) return E.left('global env not exist'); + if (globalEnv === null) + return E.left(USER_ENVIRONMENT_GLOBAL_ENV_DOESNT_EXISTS); return E.right(globalEnv); } - - async fetchUserGlobalEnvironments(uid: string) { - const globalEnvironment = await this.prisma.userEnvironment.findFirst({ - where: { - userUid: uid, - isGlobal: true, - }, - rejectOnNotFound: true, - }); - - return { - userUid: globalEnvironment.userUid, - id: globalEnvironment.id, - name: globalEnvironment.name, - variables: JSON.stringify(globalEnvironment.variables), - isGlobal: globalEnvironment.isGlobal, - }; - } } From f58d5d28cfbd60e4469596e46626132e33407921 Mon Sep 17 00:00:00 2001 From: ankitsridhar16 Date: Thu, 19 Jan 2023 01:31:38 +0530 Subject: [PATCH 11/35] chore: added error messages and updated existing error messages --- packages/hoppscotch-backend/src/errors.ts | 12 ++++++++++-- 1 file changed, 10 insertions(+), 2 deletions(-) diff --git a/packages/hoppscotch-backend/src/errors.ts b/packages/hoppscotch-backend/src/errors.ts index 62664d3f7..b95e25b14 100644 --- a/packages/hoppscotch-backend/src/errors.ts +++ b/packages/hoppscotch-backend/src/errors.ts @@ -159,7 +159,7 @@ export const TEAM_ENVIRONMENT_NOT_TEAM_MEMBER = * Global environment doesnt exists for the user * (UserEnvironmentsService) */ -export const USER_ENVIRONMENT_GLOBAL_ENV_DOESNT_EXISTS = +export const USER_ENVIRONMENT_GLOBAL_ENV_DOES_NOT_EXISTS = 'user_environment/global_env_doesnt_exists' as const; /* @@ -175,7 +175,7 @@ export const USER_ENVIRONMENT_GLOBAL_ENV_EXISTS = * User environment doesn't exist for the user * (UserEnvironmentsService) */ -export const USER_ENVIRONMENT_ENV_DOESNT_EXISTS = +export const USER_ENVIRONMENT_ENV_DOES_NOT_EXISTS = 'user_environment/user_env_doesnt_exists' as const; /* @@ -203,6 +203,14 @@ export const USER_ENVIRONMENT_UPDATE_FAILED = 'user_environment/user_env_update_failed' as const; /* +/** + * User environment invalid environment name + * (UserEnvironmentsService) + */ +export const USER_ENVIRONMENT_INVALID_ENVIRONMENT_NAME = + 'user_environment/user_env_invalid_env_name' as const; +/* + |------------------------------------| |Server errors that are actually bugs| |------------------------------------| From 9e25aa1f9f329b2e293bc7879e32f7fb7003549a Mon Sep 17 00:00:00 2001 From: ankitsridhar16 Date: Thu, 19 Jan 2023 01:34:11 +0530 Subject: [PATCH 12/35] feat: introducing new subscription handler interface and user defined/primitive types --- .../src/subscription-handler.ts | 49 +++++++++++++++++++ .../src/types/module-types.ts | 4 ++ .../src/types/primitive-types.ts | 1 + 3 files changed, 54 insertions(+) create mode 100644 packages/hoppscotch-backend/src/subscription-handler.ts create mode 100644 packages/hoppscotch-backend/src/types/module-types.ts create mode 100644 packages/hoppscotch-backend/src/types/primitive-types.ts diff --git a/packages/hoppscotch-backend/src/subscription-handler.ts b/packages/hoppscotch-backend/src/subscription-handler.ts new file mode 100644 index 000000000..f07e62c89 --- /dev/null +++ b/packages/hoppscotch-backend/src/subscription-handler.ts @@ -0,0 +1,49 @@ +import { Injectable } from '@nestjs/common'; +import { PubSubService } from './pubsub/pubsub.service'; +import { PrimitiveTypes } from './types/primitive-types'; +import { ModuleTypes } from './types/module-types'; + +// Custom generic type to indicate the type of module +type ModuleType = PrimitiveTypes | ModuleTypes; + +// Contains constants for the subscription types we send to pubsub service +enum SubscriptionType { + Created = 'created', + Updated = 'updated', + Deleted = 'deleted', + DeleteMany = 'delete_many', +} + +@Injectable() +export class SubscriptionHandler { + constructor(private readonly pubsub: PubSubService) {} + + /** + * Publishes a subscription using the pubsub module + * @param topic a string containing the module name, a uid and the type of subscription + * @param moduleType type of the module model being called + * @returns a promise of type void + */ + async publish( + topic: string, + subscriptionType: SubscriptionType, + moduleType: ModuleType, + ) { + switch (subscriptionType) { + case SubscriptionType.Created: + await this.pubsub.publish(`${topic}/created`, moduleType); + break; + case SubscriptionType.Updated: + await this.pubsub.publish(`${topic}/updated`, moduleType); + break; + case SubscriptionType.Deleted: + await this.pubsub.publish(`${topic}/deleted`, moduleType); + break; + case SubscriptionType.DeleteMany: + await this.pubsub.publish(`${topic}/delete_many`, moduleType); + break; + default: + break; + } + } +} diff --git a/packages/hoppscotch-backend/src/types/module-types.ts b/packages/hoppscotch-backend/src/types/module-types.ts new file mode 100644 index 000000000..356fcc816 --- /dev/null +++ b/packages/hoppscotch-backend/src/types/module-types.ts @@ -0,0 +1,4 @@ +import { UserEnvironment } from '../user-environment/user-environments.model'; +import { User } from '../user/user.model'; + +export type ModuleTypes = UserEnvironment | User; diff --git a/packages/hoppscotch-backend/src/types/primitive-types.ts b/packages/hoppscotch-backend/src/types/primitive-types.ts new file mode 100644 index 000000000..11918cf33 --- /dev/null +++ b/packages/hoppscotch-backend/src/types/primitive-types.ts @@ -0,0 +1 @@ +export type PrimitiveTypes = number | string | boolean; From 80fdc6005bbc53048ad47436ca9535db8da19861 Mon Sep 17 00:00:00 2001 From: ankitsridhar16 Date: Thu, 19 Jan 2023 01:35:21 +0530 Subject: [PATCH 13/35] chore: added comments to certain fields --- .../src/user-environment/user-environments.model.ts | 4 ++-- 1 file changed, 2 insertions(+), 2 deletions(-) diff --git a/packages/hoppscotch-backend/src/user-environment/user-environments.model.ts b/packages/hoppscotch-backend/src/user-environment/user-environments.model.ts index 552299f60..be99821e3 100644 --- a/packages/hoppscotch-backend/src/user-environment/user-environments.model.ts +++ b/packages/hoppscotch-backend/src/user-environment/user-environments.model.ts @@ -16,7 +16,7 @@ export class UserEnvironment { nullable: true, description: 'Name of the environment', }) - name: string | null | undefined; + name: string | null | undefined; // types have a union to avoid TS warnings and field is nullable when it is global env @Field({ description: 'All variables present in the environment', @@ -24,7 +24,7 @@ export class UserEnvironment { variables: string; // JSON string of the variables object (format:[{ key: "bla", value: "bla_val" }, ...] ) which will be received from the client @Field({ - description: 'isGlobal flag to indicate the environment is global or not', + description: 'Flag to indicate the environment is global or not', }) isGlobal: boolean; } From 29e74a2c9ec2f631f573fdb749df212e9971a4c3 Mon Sep 17 00:00:00 2001 From: ankitsridhar16 Date: Thu, 19 Jan 2023 01:36:31 +0530 Subject: [PATCH 14/35] feat: added subscription handler as a provider for user environment module --- .../src/user-environment/user-environments.module.ts | 2 ++ 1 file changed, 2 insertions(+) diff --git a/packages/hoppscotch-backend/src/user-environment/user-environments.module.ts b/packages/hoppscotch-backend/src/user-environment/user-environments.module.ts index 04f391682..d82a1e899 100644 --- a/packages/hoppscotch-backend/src/user-environment/user-environments.module.ts +++ b/packages/hoppscotch-backend/src/user-environment/user-environments.module.ts @@ -5,6 +5,7 @@ import { UserModule } from '../user/user.module'; import { UserEnvsUserResolver } from './user.resolver'; import { UserEnvironmentsResolver } from './user-environments.resolver'; import { UserEnvironmentsService } from './user-environments.service'; +import { SubscriptionHandler } from '../subscription-handler'; @Module({ imports: [PrismaModule, PubSubModule, UserModule], @@ -12,6 +13,7 @@ import { UserEnvironmentsService } from './user-environments.service'; UserEnvironmentsResolver, UserEnvironmentsService, UserEnvsUserResolver, + SubscriptionHandler, ], exports: [UserEnvironmentsService], }) From 6aa66e99b51e5914e4174c43a341d2cd1f75bd22 Mon Sep 17 00:00:00 2001 From: ankitsridhar16 Date: Thu, 19 Jan 2023 12:48:25 +0530 Subject: [PATCH 15/35] chore: added review changes for description --- .../hoppscotch-backend/src/user-environment/user.resolver.ts | 3 +-- 1 file changed, 1 insertion(+), 2 deletions(-) diff --git a/packages/hoppscotch-backend/src/user-environment/user.resolver.ts b/packages/hoppscotch-backend/src/user-environment/user.resolver.ts index 3dbfc949b..0d6d31cf4 100644 --- a/packages/hoppscotch-backend/src/user-environment/user.resolver.ts +++ b/packages/hoppscotch-backend/src/user-environment/user.resolver.ts @@ -16,8 +16,7 @@ export class UserEnvsUserResolver { } @ResolveField(() => UserEnvironment, { - description: - 'Returns a list of user variables inside a global environments', + description: 'Returns the users global environments', }) async globalEnvironments( @Parent() user: User, From 3e9295f31303c5a963ba80461446dd14001eacb5 Mon Sep 17 00:00:00 2001 From: ankitsridhar16 Date: Thu, 19 Jan 2023 12:50:20 +0530 Subject: [PATCH 16/35] chore: added review changes for updating mutations and naming for descriptions --- .../user-environments.resolver.ts | 56 ++++++++++++------- 1 file changed, 36 insertions(+), 20 deletions(-) diff --git a/packages/hoppscotch-backend/src/user-environment/user-environments.resolver.ts b/packages/hoppscotch-backend/src/user-environment/user-environments.resolver.ts index 76af01688..c73156600 100644 --- a/packages/hoppscotch-backend/src/user-environment/user-environments.resolver.ts +++ b/packages/hoppscotch-backend/src/user-environment/user-environments.resolver.ts @@ -19,8 +19,7 @@ export class UserEnvironmentsResolver { /* Mutations */ @Mutation(() => UserEnvironment, { - description: - 'Create a new personal or global user environment for given user uid', + description: 'Create a new personal user environment for given user uid', }) @UseGuards(GqlAuthGuard) async createUserEnvironment( @@ -36,12 +35,8 @@ export class UserEnvironmentsResolver { description: 'JSON string of the variables object', }) variables: string, - @Args({ - name: 'isGlobal', - description: 'isGlobal flag to indicate personal or global environment', - }) - isGlobal: boolean, ): Promise { + const isGlobal = false; const userEnvironment = await this.userEnvironmentsService.createUserEnvironment( user.uid, @@ -54,8 +49,32 @@ export class UserEnvironmentsResolver { } @Mutation(() => UserEnvironment, { - description: - 'Update a users personal or global environment based on environment id', + description: 'Create a new global user environment for given user uid', + }) + @UseGuards(GqlAuthGuard) + async createUserGlobalEnvironment( + @GqlUser() user: User, + @Args({ + name: 'variables', + description: 'JSON string of the variables object', + }) + variables: string, + ): Promise { + const isGlobal = true; + const globalEnvName: string = null; + const userEnvironment = + await this.userEnvironmentsService.createUserEnvironment( + user.uid, + globalEnvName, + variables, + isGlobal, + ); + if (E.isLeft(userEnvironment)) throwErr(userEnvironment.left); + return userEnvironment.right; + } + + @Mutation(() => UserEnvironment, { + description: 'Updates a users personal or global environment', }) @UseGuards(GqlAuthGuard) async updateUserEnvironment( @@ -88,7 +107,7 @@ export class UserEnvironmentsResolver { } @Mutation(() => UserEnvironment, { - description: 'Deletes a users personal environment based on environment id', + description: 'Deletes a users personal environment', }) @UseGuards(GqlAuthGuard) async deleteUserEnvironment( @@ -99,7 +118,7 @@ export class UserEnvironmentsResolver { type: () => ID, }) id: string, - ): Promise { + ): Promise { const userEnvironment = await this.userEnvironmentsService.deleteUserEnvironment(user.uid, id); if (E.isLeft(userEnvironment)) throwErr(userEnvironment.left); @@ -110,7 +129,7 @@ export class UserEnvironmentsResolver { description: 'Deletes users all personal environments', }) @UseGuards(GqlAuthGuard) - async deleteUserEnvironments(@GqlUser() user: User): Promise { + async deleteUserEnvironments(@GqlUser() user: User): Promise { return await this.userEnvironmentsService.deleteUserEnvironments(user.uid); } @@ -118,7 +137,7 @@ export class UserEnvironmentsResolver { description: 'Deletes all variables inside a users global environment', }) @UseGuards(GqlAuthGuard) - async deleteAllVariablesFromUsersGlobalEnvironment( + async clearGlobalEnvironments( @GqlUser() user: User, @Args({ name: 'id', @@ -128,10 +147,7 @@ export class UserEnvironmentsResolver { id: string, ): Promise { const userEnvironment = - await this.userEnvironmentsService.deleteAllVariablesFromUsersGlobalEnvironment( - user.uid, - id, - ); + await this.userEnvironmentsService.clearGlobalEnvironments(user.uid, id); if (E.isLeft(userEnvironment)) throwErr(userEnvironment.left); return userEnvironment.right; } @@ -146,7 +162,7 @@ export class UserEnvironmentsResolver { userEnvironmentCreated( @Args({ name: 'userUid', - description: 'users uid', + description: 'Users uid', type: () => ID, }) userUid: string, @@ -162,7 +178,7 @@ export class UserEnvironmentsResolver { userEnvironmentUpdated( @Args({ name: 'id', - description: 'environment id', + description: 'Environment id', type: () => ID, }) id: string, @@ -178,7 +194,7 @@ export class UserEnvironmentsResolver { userEnvironmentDeleted( @Args({ name: 'id', - description: 'environment id', + description: 'Environment id', type: () => ID, }) id: string, From 4aad8d36a999379be958db0c1ff265bdda4b66fd Mon Sep 17 00:00:00 2001 From: ankitsridhar16 Date: Thu, 19 Jan 2023 12:53:40 +0530 Subject: [PATCH 17/35] chore: updated comment for Subscription Type and updated JSDoc for publish --- packages/hoppscotch-backend/src/subscription-handler.ts | 5 +++-- 1 file changed, 3 insertions(+), 2 deletions(-) diff --git a/packages/hoppscotch-backend/src/subscription-handler.ts b/packages/hoppscotch-backend/src/subscription-handler.ts index f07e62c89..fba5551a4 100644 --- a/packages/hoppscotch-backend/src/subscription-handler.ts +++ b/packages/hoppscotch-backend/src/subscription-handler.ts @@ -6,7 +6,7 @@ import { ModuleTypes } from './types/module-types'; // Custom generic type to indicate the type of module type ModuleType = PrimitiveTypes | ModuleTypes; -// Contains constants for the subscription types we send to pubsub service +// Contains constants for the subscription types we use in Subscription Handler enum SubscriptionType { Created = 'created', Updated = 'updated', @@ -20,7 +20,8 @@ export class SubscriptionHandler { /** * Publishes a subscription using the pubsub module - * @param topic a string containing the module name, a uid and the type of subscription + * @param topic a string containing the module name, an uid and the type of subscription + * @param subscriptionType type of subscription being called * @param moduleType type of the module model being called * @returns a promise of type void */ From d10ed664bfb7d362d1f7481a524c2be1617e181f Mon Sep 17 00:00:00 2001 From: ankitsridhar16 Date: Thu, 19 Jan 2023 12:56:41 +0530 Subject: [PATCH 18/35] chore: updated test cases with requested changes to handle publishing seperately --- .../user-environments.service.spec.ts | 667 ++++++++++-------- 1 file changed, 366 insertions(+), 301 deletions(-) diff --git a/packages/hoppscotch-backend/src/user-environment/user-environments.service.spec.ts b/packages/hoppscotch-backend/src/user-environment/user-environments.service.spec.ts index ca5078005..f41287f1d 100644 --- a/packages/hoppscotch-backend/src/user-environment/user-environments.service.spec.ts +++ b/packages/hoppscotch-backend/src/user-environment/user-environments.service.spec.ts @@ -3,26 +3,31 @@ import { mockDeep, mockReset } from 'jest-mock-extended'; import { PrismaService } from '../prisma/prisma.service'; import { UserEnvironmentsService } from './user-environments.service'; import { - USER_ENVIRONMENT_ENV_DOESNT_EXISTS, + USER_ENVIRONMENT_ENV_DOES_NOT_EXISTS, USER_ENVIRONMENT_GLOBAL_ENV_DELETION_FAILED, USER_ENVIRONMENT_GLOBAL_ENV_EXISTS, + USER_ENVIRONMENT_INVALID_ENVIRONMENT_NAME, } from '../errors'; import { PubSubService } from '../pubsub/pubsub.service'; +import { SubscriptionHandler } from '../subscription-handler'; const mockPrisma = mockDeep(); const mockPubSub = mockDeep(); +const mockSubscriptionHandler = mockDeep(); // eslint-disable-next-line @typescript-eslint/ban-ts-comment // @ts-ignore const userEnvironmentsService = new UserEnvironmentsService( mockPrisma, mockPubSub as any, + mockSubscriptionHandler, ); enum SubscriptionType { Created = 'created', Updated = 'updated', Deleted = 'deleted', + DeleteMany = 'delete_many', } const userPersonalEnvironments = [ @@ -131,228 +136,303 @@ describe('UserEnvironmentsService', () => { expect( await userEnvironmentsService.fetchUserGlobalEnvironment('abc'), - ).toEqualLeft(USER_ENVIRONMENT_ENV_DOESNT_EXISTS); + ).toEqualLeft(USER_ENVIRONMENT_ENV_DOES_NOT_EXISTS); }); }); describe('createUserEnvironment', () => { - test( - 'Should resolve right and create a users personal environment and return a `UserEnvironment` object ' + - 'and publish a subscription', - async () => { - mockPrisma.userEnvironment.create.mockResolvedValueOnce({ - userUid: 'abc123', - id: '123', - name: 'test', - variables: [{}], - isGlobal: false, - }); + test('Should resolve right and create a users personal environment and return a `UserEnvironment` object ', async () => { + mockPrisma.userEnvironment.create.mockResolvedValueOnce({ + userUid: 'abc123', + id: '123', + name: 'test', + variables: [{}], + isGlobal: false, + }); - const result: UserEnvironment = { - userUid: 'abc123', - id: '123', - name: 'test', - variables: JSON.stringify([{}]), - isGlobal: false, - }; + const result: UserEnvironment = { + userUid: 'abc123', + id: '123', + name: 'test', + variables: JSON.stringify([{}]), + isGlobal: false, + }; - await userEnvironmentsService.publishUserEnvironmentSubscription( - result, - SubscriptionType.Created, - ); + return expect( + await userEnvironmentsService.createUserEnvironment( + 'abc123', + 'test', + '[{}]', + false, + ), + ).toEqualRight(result); + }); - return expect( - await userEnvironmentsService.createUserEnvironment( - 'abc123', - 'test', - '[{}]', - false, - ), - ).toEqualRight(result); - }, - ); + test('Should resolve right and create a new users global environment and return a `UserEnvironment` object ', async () => { + mockPrisma.userEnvironment.findFirst.mockResolvedValueOnce(null); - test( - 'Should resolve right and create a new users global environment and return a `UserEnvironment` object ' + - 'and publish a subscription', - async () => { - mockPrisma.userEnvironment.findFirst.mockResolvedValueOnce(null); + mockPrisma.userEnvironment.create.mockResolvedValueOnce({ + userUid: 'abc123', + id: '123', + name: null, + variables: [{}], + isGlobal: true, + }); - mockPrisma.userEnvironment.create.mockResolvedValueOnce({ - userUid: 'abc123', - id: '123', - name: 'testgenv', - variables: [{}], - isGlobal: true, - }); + const result: UserEnvironment = { + userUid: 'abc123', + id: '123', + name: null, + variables: JSON.stringify([{}]), + isGlobal: true, + }; - const result: UserEnvironment = { - userUid: 'abc123', - id: '123', - name: 'testgenv', - variables: JSON.stringify([{}]), - isGlobal: true, - }; + return expect( + await userEnvironmentsService.createUserEnvironment( + 'abc123', + null, + '[{}]', + true, + ), + ).toEqualRight(result); + }); - await userEnvironmentsService.publishUserEnvironmentSubscription( - result, - SubscriptionType.Created, - ); - return expect( - await userEnvironmentsService.createUserEnvironment( - 'abc123', - 'test', - '[{}]', - true, - ), - ).toEqualRight(result); - }, - ); + test('Should resolve left and not create a new users global environment if existing global env exists ', async () => { + mockPrisma.userEnvironment.findFirst.mockResolvedValueOnce({ + userUid: 'abc123', + id: '123', + name: null, + variables: [{}], + isGlobal: true, + }); - test( - 'Should resolve left and not create a new users global environment if existing global env exists ' + - 'and not publish a subscription', - async () => { - mockPrisma.userEnvironment.findFirst.mockResolvedValueOnce({ - userUid: 'abc123', - id: '123', - name: 'testgenv', - variables: [{}], - isGlobal: true, - }); + return expect( + await userEnvironmentsService.createUserEnvironment( + 'abc123', + null, + '[{}]', + true, + ), + ).toEqualLeft(USER_ENVIRONMENT_GLOBAL_ENV_EXISTS); + }); - return expect( - await userEnvironmentsService.createUserEnvironment( - 'abc123', - 'test', - '[{}]', - true, - ), - ).toEqualLeft(USER_ENVIRONMENT_GLOBAL_ENV_EXISTS); - }, - ); + test('Should resolve left when an invalid personal environment name has been passed', async () => { + return expect( + await userEnvironmentsService.createUserEnvironment( + 'abc123', + null, + '[{}]', + false, + ), + ).toEqualLeft(USER_ENVIRONMENT_INVALID_ENVIRONMENT_NAME); + }); + + test('Should create a users personal environment and publish a created subscription', async () => { + mockPrisma.userEnvironment.create.mockResolvedValueOnce({ + userUid: 'abc123', + id: '123', + name: 'test', + variables: [{}], + isGlobal: false, + }); + + const result: UserEnvironment = { + userUid: 'abc123', + id: '123', + name: 'test', + variables: JSON.stringify([{}]), + isGlobal: false, + }; + + await userEnvironmentsService.createUserEnvironment( + 'abc123', + 'test', + '[{}]', + false, + ); + + return expect(mockSubscriptionHandler.publish).toHaveBeenCalledWith( + `user_environment/${result.userUid}`, + SubscriptionType.Created, + result, + ); + }); + + test('Should create a users global environment and publish a created subscription', async () => { + mockPrisma.userEnvironment.create.mockResolvedValueOnce({ + userUid: 'abc123', + id: '123', + name: '', + variables: [{}], + isGlobal: true, + }); + + const result: UserEnvironment = { + userUid: 'abc123', + id: '123', + name: '', + variables: JSON.stringify([{}]), + isGlobal: true, + }; + + await userEnvironmentsService.createUserEnvironment( + 'abc123', + '', + '[{}]', + true, + ); + + return expect(mockSubscriptionHandler.publish).toHaveBeenCalledWith( + `user_environment/${result.userUid}`, + SubscriptionType.Created, + result, + ); + }); }); describe('UpdateUserEnvironment', () => { - test( - 'should resolve right and update a users personal or environment and return a `UserEnvironment` object ' + - 'and publish a subscription', - async () => { - mockPrisma.userEnvironment.update.mockResolvedValueOnce({ - userUid: 'abc123', - id: '123', - name: 'test', - variables: [{}], - isGlobal: false, - }); + test('Should resolve right and update a users personal or environment and return a `UserEnvironment` object ', async () => { + mockPrisma.userEnvironment.update.mockResolvedValueOnce({ + userUid: 'abc123', + id: '123', + name: 'test', + variables: [{}], + isGlobal: false, + }); - const result: UserEnvironment = { - userUid: 'abc123', - id: '123', - name: 'test', - variables: JSON.stringify([{}]), - isGlobal: false, - }; + const result: UserEnvironment = { + userUid: 'abc123', + id: '123', + name: 'test', + variables: JSON.stringify([{}]), + isGlobal: false, + }; - await userEnvironmentsService.publishUserEnvironmentSubscription( - result, - SubscriptionType.Updated, - ); - return expect( - await userEnvironmentsService.updateUserEnvironment( - 'abc123', - 'test', - '[{}]', - ), - ).toEqualRight(result); - }, - ); + return expect( + await userEnvironmentsService.updateUserEnvironment( + 'abc123', + 'test', + '[{}]', + ), + ).toEqualRight(result); + }); - test( - 'should resolve right and update a users global environment and return a `UserEnvironment` object ' + - 'and publish a subscription', - async () => { - mockPrisma.userEnvironment.update.mockResolvedValueOnce({ - userUid: 'abc123', - id: '123', - name: '', - variables: [{}], - isGlobal: true, - }); + test('Should resolve right and update a users global environment and return a `UserEnvironment` object ', async () => { + mockPrisma.userEnvironment.update.mockResolvedValueOnce({ + userUid: 'abc123', + id: '123', + name: null, + variables: [{}], + isGlobal: true, + }); - const result: UserEnvironment = { - userUid: 'abc123', - id: '123', - name: '', - variables: JSON.stringify([{}]), - isGlobal: true, - }; + const result: UserEnvironment = { + userUid: 'abc123', + id: '123', + name: null, + variables: JSON.stringify([{}]), + isGlobal: true, + }; - await userEnvironmentsService.publishUserEnvironmentSubscription( - result, - SubscriptionType.Updated, - ); - return expect( - await userEnvironmentsService.updateUserEnvironment( - 'abc123', - '', - '[{}]', - ), - ).toEqualRight(result); - }, - ); + return expect( + await userEnvironmentsService.updateUserEnvironment( + 'abc123', + null, + '[{}]', + ), + ).toEqualRight(result); + }); - test( - 'should resolve left and not update a users environment if env doesnt exist ' + - 'and publish a subscription', - async () => { - mockPrisma.userEnvironment.update.mockRejectedValueOnce({}); + test('Should resolve left and not update a users environment if env doesnt exist ', async () => { + mockPrisma.userEnvironment.update.mockRejectedValueOnce({}); - return expect( - await userEnvironmentsService.updateUserEnvironment( - 'abc123', - 'test', - '[{}]', - ), - ).toEqualLeft(USER_ENVIRONMENT_ENV_DOESNT_EXISTS); - }, - ); + return expect( + await userEnvironmentsService.updateUserEnvironment( + 'abc123', + 'test', + '[{}]', + ), + ).toEqualLeft(USER_ENVIRONMENT_ENV_DOES_NOT_EXISTS); + }); + + test('Should resolve right, update a users personal environment and publish an updated subscription ', async () => { + mockPrisma.userEnvironment.update.mockResolvedValueOnce({ + userUid: 'abc123', + id: '123', + name: 'test', + variables: [{}], + isGlobal: false, + }); + + const result: UserEnvironment = { + userUid: 'abc123', + id: '123', + name: 'test', + variables: JSON.stringify([{}]), + isGlobal: false, + }; + + await userEnvironmentsService.updateUserEnvironment( + 'abc123', + 'test', + '[{}]', + ); + + return expect(mockSubscriptionHandler.publish).toHaveBeenCalledWith( + `user_environment/${result.id}`, + SubscriptionType.Updated, + result, + ); + }); + + test('Should resolve right, update a users global environment and publish an updated subscription ', async () => { + mockPrisma.userEnvironment.update.mockResolvedValueOnce({ + userUid: 'abc123', + id: '123', + name: null, + variables: [{}], + isGlobal: true, + }); + + const result: UserEnvironment = { + userUid: 'abc123', + id: '123', + name: null, + variables: JSON.stringify([{}]), + isGlobal: true, + }; + + await userEnvironmentsService.updateUserEnvironment( + 'abc123', + null, + '[{}]', + ); + + return expect(mockSubscriptionHandler.publish).toHaveBeenCalledWith( + `user_environment/${result.id}`, + SubscriptionType.Updated, + result, + ); + }); }); describe('deleteUserEnvironment', () => { - test( - 'Should resolve right and delete a users personal environment and return a `UserEnvironment` object ' + - 'and publish a subscription', - async () => { - mockPrisma.userEnvironment.findFirst.mockResolvedValueOnce(null); - mockPrisma.userEnvironment.delete.mockResolvedValueOnce({ - userUid: 'abc123', - id: 'env1', - name: 'en1', - variables: [{}], - isGlobal: false, - }); + test('Should resolve right and delete a users personal environment and return a `UserEnvironment` object ', async () => { + mockPrisma.userEnvironment.findFirst.mockResolvedValueOnce(null); + mockPrisma.userEnvironment.delete.mockResolvedValueOnce({ + userUid: 'abc123', + id: 'env1', + name: 'en1', + variables: [{}], + isGlobal: false, + }); - const result: UserEnvironment = { - userUid: 'abc123', - id: 'env1', - name: 'en1', - variables: JSON.stringify([{}]), - isGlobal: false, - }; + return expect( + await userEnvironmentsService.deleteUserEnvironment('abc123', 'env1'), + ).toEqualRight(true); + }); - await userEnvironmentsService.publishUserEnvironmentSubscription( - result, - SubscriptionType.Deleted, - ); - - return expect( - await userEnvironmentsService.deleteUserEnvironment('abc123', 'env1'), - ).toEqualRight(result); - }, - ); - - test('Should resolve left and return an error when deleting a global user environment ', async () => { + test('Should resolve left and return an error when deleting a global user environment', async () => { mockPrisma.userEnvironment.findFirst.mockResolvedValueOnce({ userUid: 'abc123', id: 'genv1', @@ -366,71 +446,90 @@ describe('UserEnvironmentsService', () => { ).toEqualLeft(USER_ENVIRONMENT_GLOBAL_ENV_DELETION_FAILED); }); - test('Should resolve left and return an error when deleting an invalid user environment ', async () => { + test('Should resolve left and return an error when deleting an invalid user environment', async () => { mockPrisma.userEnvironment.delete.mockResolvedValueOnce(null); return expect( await userEnvironmentsService.deleteUserEnvironment('abc123', 'env1'), - ).toEqualLeft(USER_ENVIRONMENT_ENV_DOESNT_EXISTS); + ).toEqualLeft(USER_ENVIRONMENT_ENV_DOES_NOT_EXISTS); + }); + + test('Should resolve right, delete a users personal environment and publish a deleted subscription', async () => { + mockPrisma.userEnvironment.findFirst.mockResolvedValueOnce(null); + mockPrisma.userEnvironment.delete.mockResolvedValueOnce({ + userUid: 'abc123', + id: 'env1', + name: 'en1', + variables: [{}], + isGlobal: false, + }); + + const result: UserEnvironment = { + userUid: 'abc123', + id: 'env1', + name: 'en1', + variables: JSON.stringify([{}]), + isGlobal: false, + }; + + await userEnvironmentsService.deleteUserEnvironment('abc123', 'env1'); + + return expect(mockSubscriptionHandler.publish).toHaveBeenCalledWith( + `user_environment/${result.id}`, + SubscriptionType.Deleted, + result, + ); }); }); describe('deleteUserEnvironments', () => { - test('Should return a count of users personal environment deleted', async () => { + test('Should publish a subscription with a count of deleted environments', async () => { mockPrisma.userEnvironment.deleteMany.mockResolvedValueOnce({ count: 1, }); - return expect( - await userEnvironmentsService.deleteUserEnvironments('abc123'), - ).toEqual(1); + await userEnvironmentsService.deleteUserEnvironments('abc123'); + + return expect(mockSubscriptionHandler.publish).toHaveBeenCalledWith( + `user_environment/abc123`, + SubscriptionType.DeleteMany, + 1, + ); }); }); - describe('deleteAllVariablesFromUsersGlobalEnvironment', () => { - test( - 'Should resolve right and delete all variables inside users global environment and return a `UserEnvironment` object ' + - 'and publish a subscription', - async () => { - mockPrisma.userEnvironment.findFirst.mockResolvedValueOnce({ - userUid: 'abc123', - id: 'env1', - name: 'en1', - variables: [{}], - isGlobal: true, - }); + describe('clearGlobalEnvironments', () => { + test('Should resolve right and delete all variables inside users global environment and return a `UserEnvironment` object', async () => { + mockPrisma.userEnvironment.findFirst.mockResolvedValueOnce({ + userUid: 'abc123', + id: 'env1', + name: 'en1', + variables: [{}], + isGlobal: true, + }); - mockPrisma.userEnvironment.update.mockResolvedValueOnce({ - userUid: 'abc123', - id: 'env1', - name: 'en1', - variables: [], - isGlobal: true, - }); + mockPrisma.userEnvironment.update.mockResolvedValueOnce({ + userUid: 'abc123', + id: 'env1', + name: 'en1', + variables: [], + isGlobal: true, + }); - const result: UserEnvironment = { - userUid: 'abc123', - id: 'env1', - name: 'en1', - variables: JSON.stringify([]), - isGlobal: true, - }; + const result: UserEnvironment = { + userUid: 'abc123', + id: 'env1', + name: 'en1', + variables: JSON.stringify([]), + isGlobal: true, + }; - await userEnvironmentsService.publishUserEnvironmentSubscription( - result, - SubscriptionType.Updated, - ); + return expect( + await userEnvironmentsService.clearGlobalEnvironments('abc123', 'env1'), + ).toEqualRight(result); + }); - return expect( - await userEnvironmentsService.deleteAllVariablesFromUsersGlobalEnvironment( - 'abc123', - 'env1', - ), - ).toEqualRight(result); - }, - ); - - test('Should resolve left and return an error if global environment id and passed id dont match ', async () => { + test('Should resolve left and return an error if global environment id and passed id dont match', async () => { mockPrisma.userEnvironment.findFirst.mockResolvedValueOnce({ userUid: 'abc123', id: 'genv2', @@ -441,75 +540,41 @@ describe('UserEnvironmentsService', () => { return expect( await userEnvironmentsService.deleteUserEnvironment('abc123', 'genv1'), - ).toEqualLeft(USER_ENVIRONMENT_ENV_DOESNT_EXISTS); + ).toEqualLeft(USER_ENVIRONMENT_ENV_DOES_NOT_EXISTS); }); - }); - describe('publishUserEnvironmentSubscription', () => { - test('Should publish a created subscription', async () => { + test('Should resolve right,delete all variables inside users global environment and publish an updated subscription', async () => { + mockPrisma.userEnvironment.findFirst.mockResolvedValueOnce({ + userUid: 'abc123', + id: 'env1', + name: 'en1', + variables: [{}], + isGlobal: true, + }); + + mockPrisma.userEnvironment.update.mockResolvedValueOnce({ + userUid: 'abc123', + id: 'env1', + name: 'en1', + variables: [], + isGlobal: true, + }); + const result: UserEnvironment = { userUid: 'abc123', - id: '123', - name: '', - variables: JSON.stringify([{}]), + id: 'env1', + name: 'en1', + variables: JSON.stringify([]), isGlobal: true, }; - await mockPubSub.publish( - `user_environment/${result.userUid}/created`, + await userEnvironmentsService.clearGlobalEnvironments('abc123', 'env1'); + + return expect(mockSubscriptionHandler.publish).toHaveBeenCalledWith( + `user_environment/${result.id}`, + SubscriptionType.Updated, result, ); - - return expect( - await userEnvironmentsService.publishUserEnvironmentSubscription( - result, - SubscriptionType.Created, - ), - ).toBeUndefined(); - }); - - test('Should publish a updated subscription', async () => { - const result: UserEnvironment = { - userUid: 'abc123', - id: '123', - name: '', - variables: JSON.stringify([{}]), - isGlobal: true, - }; - - await mockPubSub.publish( - `user_environment/${result.userUid}/updated`, - result, - ); - - return expect( - await userEnvironmentsService.publishUserEnvironmentSubscription( - result, - SubscriptionType.Updated, - ), - ).toBeUndefined(); - }); - - test('Should publish a deleted subscription', async () => { - const result: UserEnvironment = { - userUid: 'abc123', - id: '123', - name: '', - variables: JSON.stringify([{}]), - isGlobal: true, - }; - - await mockPubSub.publish( - `user_environment/${result.userUid}/deleted`, - result, - ); - - return expect( - await userEnvironmentsService.publishUserEnvironmentSubscription( - result, - SubscriptionType.Deleted, - ), - ).toBeUndefined(); }); }); }); From 0a469f4ccf7c2c6fa64536ceedbbd69c090f4257 Mon Sep 17 00:00:00 2001 From: ankitsridhar16 Date: Thu, 19 Jan 2023 12:57:59 +0530 Subject: [PATCH 19/35] chore: introduced subscription handler and fixed requested review changes --- .../user-environments.service.ts | 149 ++++++++---------- 1 file changed, 68 insertions(+), 81 deletions(-) diff --git a/packages/hoppscotch-backend/src/user-environment/user-environments.service.ts b/packages/hoppscotch-backend/src/user-environment/user-environments.service.ts index b09d9a5e3..7bf8c7ae1 100644 --- a/packages/hoppscotch-backend/src/user-environment/user-environments.service.ts +++ b/packages/hoppscotch-backend/src/user-environment/user-environments.service.ts @@ -3,20 +3,24 @@ import { UserEnvironment } from './user-environments.model'; import { PrismaService } from '../prisma/prisma.service'; import { PubSubService } from '../pubsub/pubsub.service'; import * as E from 'fp-ts/Either'; +import * as O from 'fp-ts/Option'; import { - USER_ENVIRONMENT_ENV_DOESNT_EXISTS, + USER_ENVIRONMENT_ENV_DOES_NOT_EXISTS, + USER_ENVIRONMENT_GLOBAL_ENV_DOES_NOT_EXISTS, USER_ENVIRONMENT_GLOBAL_ENV_DELETION_FAILED, - USER_ENVIRONMENT_GLOBAL_ENV_DOESNT_EXISTS, USER_ENVIRONMENT_GLOBAL_ENV_EXISTS, USER_ENVIRONMENT_IS_NOT_GLOBAL, USER_ENVIRONMENT_UPDATE_FAILED, + USER_ENVIRONMENT_INVALID_ENVIRONMENT_NAME, } from '../errors'; +import { SubscriptionHandler } from '../subscription-handler'; -// Contains constants for the subscription types we send to pubsub service +// Contains constants for the subscription types we send to subscription handler enum SubscriptionType { Created = 'created', Updated = 'updated', Deleted = 'deleted', + DeleteMany = 'delete_many', } @Injectable() @@ -24,6 +28,7 @@ export class UserEnvironmentsService { constructor( private readonly prisma: PrismaService, private readonly pubsub: PubSubService, + private readonly subscriptionHandler: SubscriptionHandler, ) {} /** @@ -75,13 +80,13 @@ export class UserEnvironmentsService { }); } - return E.left(USER_ENVIRONMENT_ENV_DOESNT_EXISTS); + return E.left(USER_ENVIRONMENT_ENV_DOES_NOT_EXISTS); } /** * Create a personal or global user environment * @param uid Users uid - * @param name environments name + * @param name environments name, null if the environment is global * @param variables environment variables * @param isGlobal flag to indicate type of environment to create * @returns an `UserEnvironment` object @@ -95,9 +100,11 @@ export class UserEnvironmentsService { // Check for existing global env for a user if exists error out to avoid recreation if (isGlobal) { const globalEnvExists = await this.checkForExistingGlobalEnv(uid); - if (E.isRight(globalEnvExists)) + if (!O.isNone(globalEnvExists)) return E.left(USER_ENVIRONMENT_GLOBAL_ENV_EXISTS); } + if (name === null && !isGlobal) + return E.left(USER_ENVIRONMENT_INVALID_ENVIRONMENT_NAME); const createdEnvironment = await this.prisma.userEnvironment.create({ data: { @@ -116,11 +123,11 @@ export class UserEnvironmentsService { isGlobal: createdEnvironment.isGlobal, }; // Publish subscription for environment creation - await this.publishUserEnvironmentSubscription( - userEnvironment, + await this.subscriptionHandler.publish( + `user_environment/${userEnvironment.userUid}`, SubscriptionType.Created, + userEnvironment, ); - return E.right(userEnvironment); } @@ -149,13 +156,14 @@ export class UserEnvironmentsService { isGlobal: updatedEnvironment.isGlobal, }; // Publish subscription for environment update - await this.publishUserEnvironmentSubscription( - updatedUserEnvironment, + await this.subscriptionHandler.publish( + `user_environment/${updatedUserEnvironment.id}`, SubscriptionType.Updated, + updatedUserEnvironment, ); return E.right(updatedUserEnvironment); } catch (e) { - return E.left(USER_ENVIRONMENT_ENV_DOESNT_EXISTS); + return E.left(USER_ENVIRONMENT_ENV_DOES_NOT_EXISTS); } } @@ -169,8 +177,8 @@ export class UserEnvironmentsService { try { // check if id is of a global environment if it is, don't delete and error out const globalEnvExists = await this.checkForExistingGlobalEnv(uid); - if (E.isRight(globalEnvExists)) { - const globalEnv = globalEnvExists.right; + if (O.isSome(globalEnvExists)) { + const globalEnv = globalEnvExists.value; if (globalEnv.id === id) { return E.left(USER_ENVIRONMENT_GLOBAL_ENV_DELETION_FAILED); } @@ -188,14 +196,16 @@ export class UserEnvironmentsService { variables: JSON.stringify(deletedEnvironment.variables), isGlobal: deletedEnvironment.isGlobal, }; - // Publish subscription for environment creation - await this.publishUserEnvironmentSubscription( - deletedUserEnvironment, + + // Publish subscription for environment deletion + await this.subscriptionHandler.publish( + `user_environment/${deletedUserEnvironment.id}`, SubscriptionType.Deleted, + deletedUserEnvironment, ); - return E.right(deletedUserEnvironment); + return E.right(true); } catch (e) { - return E.left(USER_ENVIRONMENT_ENV_DOESNT_EXISTS); + return E.left(USER_ENVIRONMENT_ENV_DOES_NOT_EXISTS); } } @@ -211,75 +221,53 @@ export class UserEnvironmentsService { isGlobal: false, }, }); - return deletedEnvironments.count; + + await this.subscriptionHandler.publish( + `user_environment/${uid}`, + SubscriptionType.DeleteMany, + deletedEnvironments.count, + ); } /** - * Deletes all existing variables in a users global environment + * Removes all existing variables in a users global environment * @param uid users uid * @param id environment id * @returns an `` of environments deleted */ - async deleteAllVariablesFromUsersGlobalEnvironment(uid: string, id: string) { + async clearGlobalEnvironments(uid: string, id: string) { const globalEnvExists = await this.checkForExistingGlobalEnv(uid); - if (E.isRight(globalEnvExists)) { - const env = globalEnvExists.right; - if (env.id === id) { - try { - const updatedEnvironment = await this.prisma.userEnvironment.update({ - where: { id: id }, - data: { - variables: [], - }, - }); - const updatedUserEnvironment: UserEnvironment = { - userUid: updatedEnvironment.userUid, - id: updatedEnvironment.id, - name: updatedEnvironment.name, - variables: JSON.stringify(updatedEnvironment.variables), - isGlobal: updatedEnvironment.isGlobal, - }; - // Publish subscription for environment creation - await this.publishUserEnvironmentSubscription( - updatedUserEnvironment, - SubscriptionType.Updated, - ); - return E.right(updatedUserEnvironment); - } catch (e) { - return E.left(USER_ENVIRONMENT_UPDATE_FAILED); - } - } else return E.left(USER_ENVIRONMENT_IS_NOT_GLOBAL); - } - return E.left(USER_ENVIRONMENT_ENV_DOESNT_EXISTS); - } + if (O.isNone(globalEnvExists)) + return E.left(USER_ENVIRONMENT_GLOBAL_ENV_DOES_NOT_EXISTS); - // Method to publish subscriptions based on the subscription type of the environment - async publishUserEnvironmentSubscription( - userEnv: UserEnvironment, - subscriptionType: SubscriptionType, - ) { - switch (subscriptionType) { - case SubscriptionType.Created: - await this.pubsub.publish( - `user_environment/${userEnv.userUid}/created`, - userEnv, + const env = globalEnvExists.value; + if (env.id === id) { + try { + const updatedEnvironment = await this.prisma.userEnvironment.update({ + where: { id: id }, + data: { + variables: [], + }, + }); + const updatedUserEnvironment: UserEnvironment = { + userUid: updatedEnvironment.userUid, + id: updatedEnvironment.id, + name: updatedEnvironment.name, + variables: JSON.stringify(updatedEnvironment.variables), + isGlobal: updatedEnvironment.isGlobal, + }; + + // Publish subscription for environment update + await this.subscriptionHandler.publish( + `user_environment/${updatedUserEnvironment.id}`, + SubscriptionType.Updated, + updatedUserEnvironment, ); - break; - case SubscriptionType.Updated: - await this.pubsub.publish( - `user_environment/${userEnv.id}/updated`, - userEnv, - ); - break; - case SubscriptionType.Deleted: - await this.pubsub.publish( - `user_environment/${userEnv.id}/deleted`, - userEnv, - ); - break; - default: - break; - } + return E.right(updatedUserEnvironment); + } catch (e) { + return E.left(USER_ENVIRONMENT_UPDATE_FAILED); + } + } else return E.left(USER_ENVIRONMENT_IS_NOT_GLOBAL); } // Method to check for existing global environments for a given user uid @@ -290,9 +278,8 @@ export class UserEnvironmentsService { isGlobal: true, }, }); - if (globalEnv === null) - return E.left(USER_ENVIRONMENT_GLOBAL_ENV_DOESNT_EXISTS); - return E.right(globalEnv); + if (globalEnv == null) return O.none; + return O.some(globalEnv); } } From 2252048d2e4c04d7a1efa4cdfbb31d52f8cc774f Mon Sep 17 00:00:00 2001 From: ankitsridhar16 Date: Thu, 19 Jan 2023 16:20:12 +0530 Subject: [PATCH 20/35] chore: updated module type name --- .../src/types/{module-types.ts => custom-module-types.ts} | 2 +- 1 file changed, 1 insertion(+), 1 deletion(-) rename packages/hoppscotch-backend/src/types/{module-types.ts => custom-module-types.ts} (68%) diff --git a/packages/hoppscotch-backend/src/types/module-types.ts b/packages/hoppscotch-backend/src/types/custom-module-types.ts similarity index 68% rename from packages/hoppscotch-backend/src/types/module-types.ts rename to packages/hoppscotch-backend/src/types/custom-module-types.ts index 356fcc816..5d669e075 100644 --- a/packages/hoppscotch-backend/src/types/module-types.ts +++ b/packages/hoppscotch-backend/src/types/custom-module-types.ts @@ -1,4 +1,4 @@ import { UserEnvironment } from '../user-environment/user-environments.model'; import { User } from '../user/user.model'; -export type ModuleTypes = UserEnvironment | User; +export type CustomModuleTypes = UserEnvironment | User; From 86aa0251abd34c820a723b01ff53b8221d6c02c4 Mon Sep 17 00:00:00 2001 From: ankitsridhar16 Date: Thu, 19 Jan 2023 16:21:05 +0530 Subject: [PATCH 21/35] chore: made review changes --- packages/hoppscotch-backend/src/errors.ts | 10 ++++++++-- 1 file changed, 8 insertions(+), 2 deletions(-) diff --git a/packages/hoppscotch-backend/src/errors.ts b/packages/hoppscotch-backend/src/errors.ts index b95e25b14..a03e437c2 100644 --- a/packages/hoppscotch-backend/src/errors.ts +++ b/packages/hoppscotch-backend/src/errors.ts @@ -8,6 +8,12 @@ export const EMAIL_FAILED = 'email/failed' as const; */ export const AUTH_FAIL = 'auth/fail'; +/** + * Invalid JSON + * (Utils) + */ +export const JSON_INVALID = 'json_invalid'; + /** * Tried to delete an user data document from fb firestore but failed. * (FirebaseService) @@ -160,7 +166,7 @@ export const TEAM_ENVIRONMENT_NOT_TEAM_MEMBER = * (UserEnvironmentsService) */ export const USER_ENVIRONMENT_GLOBAL_ENV_DOES_NOT_EXISTS = - 'user_environment/global_env_doesnt_exists' as const; + 'user_environment/global_env_does_not_exists' as const; /* /** @@ -176,7 +182,7 @@ export const USER_ENVIRONMENT_GLOBAL_ENV_EXISTS = * (UserEnvironmentsService) */ export const USER_ENVIRONMENT_ENV_DOES_NOT_EXISTS = - 'user_environment/user_env_doesnt_exists' as const; + 'user_environment/user_env_does_not_exists' as const; /* /** From 669f8b043116f9be4fd09a79cfad331262cdbe61 Mon Sep 17 00:00:00 2001 From: ankitsridhar16 Date: Thu, 19 Jan 2023 16:22:49 +0530 Subject: [PATCH 22/35] chore: made review changes for resolvers and introduced stringToJson from user-settings --- .../user-environments.resolver.ts | 27 ++++++++++--------- .../user-environments.service.spec.ts | 16 +++++------ .../user-environments.service.ts | 18 ++++++------- 3 files changed, 29 insertions(+), 32 deletions(-) diff --git a/packages/hoppscotch-backend/src/user-environment/user-environments.resolver.ts b/packages/hoppscotch-backend/src/user-environment/user-environments.resolver.ts index c73156600..17cda8902 100644 --- a/packages/hoppscotch-backend/src/user-environment/user-environments.resolver.ts +++ b/packages/hoppscotch-backend/src/user-environment/user-environments.resolver.ts @@ -61,11 +61,10 @@ export class UserEnvironmentsResolver { variables: string, ): Promise { const isGlobal = true; - const globalEnvName: string = null; const userEnvironment = await this.userEnvironmentsService.createUserEnvironment( user.uid, - globalEnvName, + null, variables, isGlobal, ); @@ -126,7 +125,7 @@ export class UserEnvironmentsResolver { } @Mutation(() => Number, { - description: 'Deletes users all personal environments', + description: 'Deletes all of users personal environments', }) @UseGuards(GqlAuthGuard) async deleteUserEnvironments(@GqlUser() user: User): Promise { @@ -159,15 +158,8 @@ export class UserEnvironmentsResolver { resolve: (value) => value, }) @UseGuards(GqlAuthGuard) - userEnvironmentCreated( - @Args({ - name: 'userUid', - description: 'Users uid', - type: () => ID, - }) - userUid: string, - ) { - return this.pubsub.asyncIterator(`user_environment/${userUid}/created`); + userEnvironmentCreated(@GqlUser() user: User) { + return this.pubsub.asyncIterator(`user_environment/${user.uid}/created`); } @Subscription(() => UserEnvironment, { @@ -201,4 +193,15 @@ export class UserEnvironmentsResolver { ) { return this.pubsub.asyncIterator(`user_environment/${id}/deleted`); } + + @Subscription(() => UserEnvironment, { + description: 'Listen for User Environment DeleteMany', + resolve: (value) => value, + }) + @UseGuards(GqlAuthGuard) + userEnvironmentDeleteMany(@GqlUser() user: User) { + return this.pubsub.asyncIterator( + `user_environment/${user.uid}/delete_many`, + ); + } } diff --git a/packages/hoppscotch-backend/src/user-environment/user-environments.service.spec.ts b/packages/hoppscotch-backend/src/user-environment/user-environments.service.spec.ts index f41287f1d..894872cde 100644 --- a/packages/hoppscotch-backend/src/user-environment/user-environments.service.spec.ts +++ b/packages/hoppscotch-backend/src/user-environment/user-environments.service.spec.ts @@ -10,6 +10,7 @@ import { } from '../errors'; import { PubSubService } from '../pubsub/pubsub.service'; import { SubscriptionHandler } from '../subscription-handler'; +import { SubscriptionType } from '../types/subscription-types'; const mockPrisma = mockDeep(); const mockPubSub = mockDeep(); @@ -23,13 +24,6 @@ const userEnvironmentsService = new UserEnvironmentsService( mockSubscriptionHandler, ); -enum SubscriptionType { - Created = 'created', - Updated = 'updated', - Deleted = 'deleted', - DeleteMany = 'delete_many', -} - const userPersonalEnvironments = [ { userUiD: 'abc123', @@ -344,7 +338,9 @@ describe('UserEnvironmentsService', () => { }); test('Should resolve left and not update a users environment if env doesnt exist ', async () => { - mockPrisma.userEnvironment.update.mockRejectedValueOnce({}); + mockPrisma.userEnvironment.update.mockRejectedValueOnce( + 'RejectOnNotFound', + ); return expect( await userEnvironmentsService.updateUserEnvironment( @@ -355,7 +351,7 @@ describe('UserEnvironmentsService', () => { ).toEqualLeft(USER_ENVIRONMENT_ENV_DOES_NOT_EXISTS); }); - test('Should resolve right, update a users personal environment and publish an updated subscription ', async () => { + test('Should update a users personal environment and publish an updated subscription ', async () => { mockPrisma.userEnvironment.update.mockResolvedValueOnce({ userUid: 'abc123', id: '123', @@ -385,7 +381,7 @@ describe('UserEnvironmentsService', () => { ); }); - test('Should resolve right, update a users global environment and publish an updated subscription ', async () => { + test('Should update a users global environment and publish an updated subscription ', async () => { mockPrisma.userEnvironment.update.mockResolvedValueOnce({ userUid: 'abc123', id: '123', diff --git a/packages/hoppscotch-backend/src/user-environment/user-environments.service.ts b/packages/hoppscotch-backend/src/user-environment/user-environments.service.ts index 7bf8c7ae1..290b414da 100644 --- a/packages/hoppscotch-backend/src/user-environment/user-environments.service.ts +++ b/packages/hoppscotch-backend/src/user-environment/user-environments.service.ts @@ -14,14 +14,8 @@ import { USER_ENVIRONMENT_INVALID_ENVIRONMENT_NAME, } from '../errors'; import { SubscriptionHandler } from '../subscription-handler'; - -// Contains constants for the subscription types we send to subscription handler -enum SubscriptionType { - Created = 'created', - Updated = 'updated', - Deleted = 'deleted', - DeleteMany = 'delete_many', -} +import { SubscriptionType } from '../types/subscription-types'; +import { stringToJson } from '../utils'; @Injectable() export class UserEnvironmentsService { @@ -106,11 +100,13 @@ export class UserEnvironmentsService { if (name === null && !isGlobal) return E.left(USER_ENVIRONMENT_INVALID_ENVIRONMENT_NAME); + const envVariables = stringToJson(variables); + if (E.isLeft(envVariables)) return E.left(envVariables.left); const createdEnvironment = await this.prisma.userEnvironment.create({ data: { userUid: uid, name: name, - variables: JSON.parse(variables), + variables: envVariables.right, isGlobal: isGlobal, }, }); @@ -139,12 +135,14 @@ export class UserEnvironmentsService { * @returns an Either of `UserEnvironment` or error */ async updateUserEnvironment(id: string, name: string, variables: string) { + const envVariables = stringToJson(variables); + if (E.isLeft(envVariables)) return E.left(envVariables.left); try { const updatedEnvironment = await this.prisma.userEnvironment.update({ where: { id: id }, data: { name: name, - variables: JSON.parse(variables), + variables: envVariables.right, }, }); From f6f4547af364b5eff70ac83b9fa5e2bed28be2e7 Mon Sep 17 00:00:00 2001 From: ankitsridhar16 Date: Thu, 19 Jan 2023 16:24:31 +0530 Subject: [PATCH 23/35] chore: introduced string to json from user-settings and moved types --- .../src/subscription-handler.ts | 17 +++++------------ .../src/types/subscription-types.ts | 7 +++++++ packages/hoppscotch-backend/src/utils.ts | 17 +++++++++++++++++ 3 files changed, 29 insertions(+), 12 deletions(-) create mode 100644 packages/hoppscotch-backend/src/types/subscription-types.ts diff --git a/packages/hoppscotch-backend/src/subscription-handler.ts b/packages/hoppscotch-backend/src/subscription-handler.ts index fba5551a4..a47730c3b 100644 --- a/packages/hoppscotch-backend/src/subscription-handler.ts +++ b/packages/hoppscotch-backend/src/subscription-handler.ts @@ -1,18 +1,11 @@ import { Injectable } from '@nestjs/common'; import { PubSubService } from './pubsub/pubsub.service'; import { PrimitiveTypes } from './types/primitive-types'; -import { ModuleTypes } from './types/module-types'; +import { CustomModuleTypes } from './types/custom-module-types'; +import { SubscriptionType } from './types/subscription-types'; // Custom generic type to indicate the type of module -type ModuleType = PrimitiveTypes | ModuleTypes; - -// Contains constants for the subscription types we use in Subscription Handler -enum SubscriptionType { - Created = 'created', - Updated = 'updated', - Deleted = 'deleted', - DeleteMany = 'delete_many', -} +type ModuleType = PrimitiveTypes | CustomModuleTypes; @Injectable() export class SubscriptionHandler { @@ -20,8 +13,8 @@ export class SubscriptionHandler { /** * Publishes a subscription using the pubsub module - * @param topic a string containing the module name, an uid and the type of subscription - * @param subscriptionType type of subscription being called + * @param topic a string containing the "module_name/identifier" + * @param subscriptionType type of subscription being published * @param moduleType type of the module model being called * @returns a promise of type void */ diff --git a/packages/hoppscotch-backend/src/types/subscription-types.ts b/packages/hoppscotch-backend/src/types/subscription-types.ts new file mode 100644 index 000000000..ee7ab75f2 --- /dev/null +++ b/packages/hoppscotch-backend/src/types/subscription-types.ts @@ -0,0 +1,7 @@ +// Contains constants for the subscription types we use in Subscription Handler +export enum SubscriptionType { + Created = 'created', + Updated = 'updated', + Deleted = 'deleted', + DeleteMany = 'delete_many', +} diff --git a/packages/hoppscotch-backend/src/utils.ts b/packages/hoppscotch-backend/src/utils.ts index a4756f5e6..570313640 100644 --- a/packages/hoppscotch-backend/src/utils.ts +++ b/packages/hoppscotch-backend/src/utils.ts @@ -4,8 +4,10 @@ import { pipe } from 'fp-ts/lib/function'; import * as O from 'fp-ts/Option'; import * as TE from 'fp-ts/TaskEither'; import * as T from 'fp-ts/Task'; +import * as E from 'fp-ts/Either'; import { User } from './user/user.model'; import * as A from 'fp-ts/Array'; +import { JSON_INVALID } from './errors'; /** * A workaround to throw an exception in an expression. @@ -108,3 +110,18 @@ export const taskEitherValidateArraySeq = ( TE.getApplicativeTaskValidation(T.ApplicativeSeq, A.getMonoid()), ), ); + +/** + * String to JSON parser + * @param {str} str The string to parse + * @returns {E.Right | E.Left<"json_invalid">} An Either of the parsed JSON + */ +export function stringToJson( + str: string, +): E.Right | E.Left { + try { + return E.right(JSON.parse(str)); + } catch (err) { + return E.left(JSON_INVALID); + } +} From ca5404a93b61c3361379bb74833110bdb29efbff Mon Sep 17 00:00:00 2001 From: ankitsridhar16 Date: Thu, 19 Jan 2023 16:25:36 +0530 Subject: [PATCH 24/35] chore: updated ts config with review changes --- packages/hoppscotch-backend/tsconfig.json | 2 +- 1 file changed, 1 insertion(+), 1 deletion(-) diff --git a/packages/hoppscotch-backend/tsconfig.json b/packages/hoppscotch-backend/tsconfig.json index 22cc39ab4..02420b5f7 100644 --- a/packages/hoppscotch-backend/tsconfig.json +++ b/packages/hoppscotch-backend/tsconfig.json @@ -17,6 +17,6 @@ "noImplicitAny": false, "strictBindCallApply": false, "forceConsistentCasingInFileNames": false, - "noFallthroughCasesInSwitch": false, + "noFallthroughCasesInSwitch": true, } } From 8929b37dbe685d06683874f0fd13ce7581375e4f Mon Sep 17 00:00:00 2001 From: Balu Babu Date: Fri, 20 Jan 2023 04:26:05 +0530 Subject: [PATCH 25/35] fix: changed return type of deleteUserEnvironment mutation to boolean in user-environments resolver --- .../src/user-environment/user-environments.resolver.ts | 2 +- 1 file changed, 1 insertion(+), 1 deletion(-) diff --git a/packages/hoppscotch-backend/src/user-environment/user-environments.resolver.ts b/packages/hoppscotch-backend/src/user-environment/user-environments.resolver.ts index 17cda8902..bdb18ab1f 100644 --- a/packages/hoppscotch-backend/src/user-environment/user-environments.resolver.ts +++ b/packages/hoppscotch-backend/src/user-environment/user-environments.resolver.ts @@ -105,7 +105,7 @@ export class UserEnvironmentsResolver { return userEnvironment.right; } - @Mutation(() => UserEnvironment, { + @Mutation(() => Boolean, { description: 'Deletes a users personal environment', }) @UseGuards(GqlAuthGuard) From da9fcd10876d3b9cfaf7ddca65bac9189c092e27 Mon Sep 17 00:00:00 2001 From: Balu Babu Date: Fri, 20 Jan 2023 04:36:30 +0530 Subject: [PATCH 26/35] refactor: changed the return type of deleteUserEnvironments method to number from void --- .../src/user-environment/user-environments.resolver.ts | 2 +- .../src/user-environment/user-environments.service.ts | 2 ++ 2 files changed, 3 insertions(+), 1 deletion(-) diff --git a/packages/hoppscotch-backend/src/user-environment/user-environments.resolver.ts b/packages/hoppscotch-backend/src/user-environment/user-environments.resolver.ts index bdb18ab1f..45530694d 100644 --- a/packages/hoppscotch-backend/src/user-environment/user-environments.resolver.ts +++ b/packages/hoppscotch-backend/src/user-environment/user-environments.resolver.ts @@ -128,7 +128,7 @@ export class UserEnvironmentsResolver { description: 'Deletes all of users personal environments', }) @UseGuards(GqlAuthGuard) - async deleteUserEnvironments(@GqlUser() user: User): Promise { + async deleteUserEnvironments(@GqlUser() user: User): Promise { return await this.userEnvironmentsService.deleteUserEnvironments(user.uid); } diff --git a/packages/hoppscotch-backend/src/user-environment/user-environments.service.ts b/packages/hoppscotch-backend/src/user-environment/user-environments.service.ts index 290b414da..a08549e0c 100644 --- a/packages/hoppscotch-backend/src/user-environment/user-environments.service.ts +++ b/packages/hoppscotch-backend/src/user-environment/user-environments.service.ts @@ -225,6 +225,8 @@ export class UserEnvironmentsService { SubscriptionType.DeleteMany, deletedEnvironments.count, ); + + return deletedEnvironments.count; } /** From cde0ba11fa70833975b920d190586a035c7dce45 Mon Sep 17 00:00:00 2001 From: Balu Babu Date: Fri, 20 Jan 2023 04:55:35 +0530 Subject: [PATCH 27/35] fix: changed the return type of delete many subscription to number from user-environment type --- .../src/user-environment/user-environments.resolver.ts | 2 +- 1 file changed, 1 insertion(+), 1 deletion(-) diff --git a/packages/hoppscotch-backend/src/user-environment/user-environments.resolver.ts b/packages/hoppscotch-backend/src/user-environment/user-environments.resolver.ts index 45530694d..f053905bb 100644 --- a/packages/hoppscotch-backend/src/user-environment/user-environments.resolver.ts +++ b/packages/hoppscotch-backend/src/user-environment/user-environments.resolver.ts @@ -194,7 +194,7 @@ export class UserEnvironmentsResolver { return this.pubsub.asyncIterator(`user_environment/${id}/deleted`); } - @Subscription(() => UserEnvironment, { + @Subscription(() => Number, { description: 'Listen for User Environment DeleteMany', resolve: (value) => value, }) From a0006f73acc2364ac5528f96cb2cff329eb700f3 Mon Sep 17 00:00:00 2001 From: ankitsridhar16 Date: Fri, 20 Jan 2023 14:55:26 +0530 Subject: [PATCH 28/35] chore: added message types to PubSub Service --- .../src/pubsub/pubsub.service.ts | 6 +++++- .../src/pubsub/subscriptionTopicsDefs.ts | 14 ++++++++++++++ .../{primitive-types.ts => primitiveTypes.ts} | 0 3 files changed, 19 insertions(+), 1 deletion(-) create mode 100644 packages/hoppscotch-backend/src/pubsub/subscriptionTopicsDefs.ts rename packages/hoppscotch-backend/src/types/{primitive-types.ts => primitiveTypes.ts} (100%) diff --git a/packages/hoppscotch-backend/src/pubsub/pubsub.service.ts b/packages/hoppscotch-backend/src/pubsub/pubsub.service.ts index 475768712..854ef2e92 100644 --- a/packages/hoppscotch-backend/src/pubsub/pubsub.service.ts +++ b/packages/hoppscotch-backend/src/pubsub/pubsub.service.ts @@ -4,6 +4,7 @@ import { default as Redis, RedisOptions } from 'ioredis'; import { RedisPubSub } from 'graphql-redis-subscriptions'; import { PubSub as LocalPubSub } from 'graphql-subscriptions'; +import { MessageType } from './subscriptionTopicsDefs'; /** * RedisPubSub uses JSON parsing for back and forth conversion, which loses Date objects, hence this reviver brings them back @@ -70,7 +71,10 @@ export class PubSubService implements OnModuleInit { return this.pubsub.asyncIterator(topic, options); } - async publish(topic: string, payload: any) { + async publish( + topic: T, + payload: MessageType[T], + ) { await this.pubsub.publish(topic, payload); } } diff --git a/packages/hoppscotch-backend/src/pubsub/subscriptionTopicsDefs.ts b/packages/hoppscotch-backend/src/pubsub/subscriptionTopicsDefs.ts new file mode 100644 index 000000000..c860d55a4 --- /dev/null +++ b/packages/hoppscotch-backend/src/pubsub/subscriptionTopicsDefs.ts @@ -0,0 +1,14 @@ +import { UserEnvironment } from '../user-environment/user-environments.model'; +import { PrimitiveTypes } from '../types/primitiveTypes'; + +// 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. +export type MessageType = { + [ + topic: `user_environment/${string}/${ + | 'created' + | 'updated' + | 'deleted' + | 'deleted_many'}` + ]: UserEnvironment | PrimitiveTypes; // Returning a number hence having a union with `PrimitiveTypes`. +}; diff --git a/packages/hoppscotch-backend/src/types/primitive-types.ts b/packages/hoppscotch-backend/src/types/primitiveTypes.ts similarity index 100% rename from packages/hoppscotch-backend/src/types/primitive-types.ts rename to packages/hoppscotch-backend/src/types/primitiveTypes.ts From bc55af27a725f08aab75dc643420eeb033f8e3ff Mon Sep 17 00:00:00 2001 From: ankitsridhar16 Date: Fri, 20 Jan 2023 14:56:28 +0530 Subject: [PATCH 29/35] chore: updated user environment to use PubSub instead of SubscriptionHandler --- .../user-environments.module.ts | 2 - .../user-environments.service.spec.ts | 39 +++++++------------ .../user-environments.service.ts | 33 +++++++--------- 3 files changed, 29 insertions(+), 45 deletions(-) diff --git a/packages/hoppscotch-backend/src/user-environment/user-environments.module.ts b/packages/hoppscotch-backend/src/user-environment/user-environments.module.ts index d82a1e899..04f391682 100644 --- a/packages/hoppscotch-backend/src/user-environment/user-environments.module.ts +++ b/packages/hoppscotch-backend/src/user-environment/user-environments.module.ts @@ -5,7 +5,6 @@ import { UserModule } from '../user/user.module'; import { UserEnvsUserResolver } from './user.resolver'; import { UserEnvironmentsResolver } from './user-environments.resolver'; import { UserEnvironmentsService } from './user-environments.service'; -import { SubscriptionHandler } from '../subscription-handler'; @Module({ imports: [PrismaModule, PubSubModule, UserModule], @@ -13,7 +12,6 @@ import { SubscriptionHandler } from '../subscription-handler'; UserEnvironmentsResolver, UserEnvironmentsService, UserEnvsUserResolver, - SubscriptionHandler, ], exports: [UserEnvironmentsService], }) diff --git a/packages/hoppscotch-backend/src/user-environment/user-environments.service.spec.ts b/packages/hoppscotch-backend/src/user-environment/user-environments.service.spec.ts index 894872cde..849b589e1 100644 --- a/packages/hoppscotch-backend/src/user-environment/user-environments.service.spec.ts +++ b/packages/hoppscotch-backend/src/user-environment/user-environments.service.spec.ts @@ -9,19 +9,15 @@ import { USER_ENVIRONMENT_INVALID_ENVIRONMENT_NAME, } from '../errors'; import { PubSubService } from '../pubsub/pubsub.service'; -import { SubscriptionHandler } from '../subscription-handler'; -import { SubscriptionType } from '../types/subscription-types'; const mockPrisma = mockDeep(); const mockPubSub = mockDeep(); -const mockSubscriptionHandler = mockDeep(); // eslint-disable-next-line @typescript-eslint/ban-ts-comment // @ts-ignore const userEnvironmentsService = new UserEnvironmentsService( mockPrisma, mockPubSub as any, - mockSubscriptionHandler, ); const userPersonalEnvironments = [ @@ -245,9 +241,8 @@ describe('UserEnvironmentsService', () => { false, ); - return expect(mockSubscriptionHandler.publish).toHaveBeenCalledWith( - `user_environment/${result.userUid}`, - SubscriptionType.Created, + return expect(mockPubSub.publish).toHaveBeenCalledWith( + `user_environment/${result.userUid}/created`, result, ); }); @@ -276,9 +271,8 @@ describe('UserEnvironmentsService', () => { true, ); - return expect(mockSubscriptionHandler.publish).toHaveBeenCalledWith( - `user_environment/${result.userUid}`, - SubscriptionType.Created, + return expect(mockPubSub.publish).toHaveBeenCalledWith( + `user_environment/${result.userUid}/created`, result, ); }); @@ -374,9 +368,8 @@ describe('UserEnvironmentsService', () => { '[{}]', ); - return expect(mockSubscriptionHandler.publish).toHaveBeenCalledWith( - `user_environment/${result.id}`, - SubscriptionType.Updated, + return expect(mockPubSub.publish).toHaveBeenCalledWith( + `user_environment/${result.id}/updated`, result, ); }); @@ -404,9 +397,8 @@ describe('UserEnvironmentsService', () => { '[{}]', ); - return expect(mockSubscriptionHandler.publish).toHaveBeenCalledWith( - `user_environment/${result.id}`, - SubscriptionType.Updated, + return expect(mockPubSub.publish).toHaveBeenCalledWith( + `user_environment/${result.id}/updated`, result, ); }); @@ -470,9 +462,8 @@ describe('UserEnvironmentsService', () => { await userEnvironmentsService.deleteUserEnvironment('abc123', 'env1'); - return expect(mockSubscriptionHandler.publish).toHaveBeenCalledWith( - `user_environment/${result.id}`, - SubscriptionType.Deleted, + return expect(mockPubSub.publish).toHaveBeenCalledWith( + `user_environment/${result.id}/deleted`, result, ); }); @@ -486,9 +477,8 @@ describe('UserEnvironmentsService', () => { await userEnvironmentsService.deleteUserEnvironments('abc123'); - return expect(mockSubscriptionHandler.publish).toHaveBeenCalledWith( - `user_environment/abc123`, - SubscriptionType.DeleteMany, + return expect(mockPubSub.publish).toHaveBeenCalledWith( + `user_environment/${'abc123'}/deleted_many`, 1, ); }); @@ -566,9 +556,8 @@ describe('UserEnvironmentsService', () => { await userEnvironmentsService.clearGlobalEnvironments('abc123', 'env1'); - return expect(mockSubscriptionHandler.publish).toHaveBeenCalledWith( - `user_environment/${result.id}`, - SubscriptionType.Updated, + return expect(mockPubSub.publish).toHaveBeenCalledWith( + `user_environment/${result.id}/updated`, result, ); }); diff --git a/packages/hoppscotch-backend/src/user-environment/user-environments.service.ts b/packages/hoppscotch-backend/src/user-environment/user-environments.service.ts index a08549e0c..872395351 100644 --- a/packages/hoppscotch-backend/src/user-environment/user-environments.service.ts +++ b/packages/hoppscotch-backend/src/user-environment/user-environments.service.ts @@ -13,8 +13,6 @@ import { USER_ENVIRONMENT_UPDATE_FAILED, USER_ENVIRONMENT_INVALID_ENVIRONMENT_NAME, } from '../errors'; -import { SubscriptionHandler } from '../subscription-handler'; -import { SubscriptionType } from '../types/subscription-types'; import { stringToJson } from '../utils'; @Injectable() @@ -22,7 +20,6 @@ export class UserEnvironmentsService { constructor( private readonly prisma: PrismaService, private readonly pubsub: PubSubService, - private readonly subscriptionHandler: SubscriptionHandler, ) {} /** @@ -119,9 +116,8 @@ export class UserEnvironmentsService { isGlobal: createdEnvironment.isGlobal, }; // Publish subscription for environment creation - await this.subscriptionHandler.publish( - `user_environment/${userEnvironment.userUid}`, - SubscriptionType.Created, + await this.pubsub.publish( + `user_environment/${userEnvironment.userUid}/created`, userEnvironment, ); return E.right(userEnvironment); @@ -154,9 +150,8 @@ export class UserEnvironmentsService { isGlobal: updatedEnvironment.isGlobal, }; // Publish subscription for environment update - await this.subscriptionHandler.publish( - `user_environment/${updatedUserEnvironment.id}`, - SubscriptionType.Updated, + await this.pubsub.publish( + `user_environment/${updatedUserEnvironment.id}/updated`, updatedUserEnvironment, ); return E.right(updatedUserEnvironment); @@ -196,9 +191,8 @@ export class UserEnvironmentsService { }; // Publish subscription for environment deletion - await this.subscriptionHandler.publish( - `user_environment/${deletedUserEnvironment.id}`, - SubscriptionType.Deleted, + await this.pubsub.publish( + `user_environment/${deletedUserEnvironment.id}/deleted`, deletedUserEnvironment, ); return E.right(true); @@ -220,9 +214,13 @@ export class UserEnvironmentsService { }, }); - await this.subscriptionHandler.publish( - `user_environment/${uid}`, - SubscriptionType.DeleteMany, + // await this.subscriptionHandler.publish( + // `user_environment/${uid}`, + // SubscriptionType.DeleteMany, + // deletedEnvironments.count, + // ); + await this.pubsub.publish( + `user_environment/${uid}/deleted_many`, deletedEnvironments.count, ); @@ -258,9 +256,8 @@ export class UserEnvironmentsService { }; // Publish subscription for environment update - await this.subscriptionHandler.publish( - `user_environment/${updatedUserEnvironment.id}`, - SubscriptionType.Updated, + await this.pubsub.publish( + `user_environment/${updatedUserEnvironment.id}/updated`, updatedUserEnvironment, ); return E.right(updatedUserEnvironment); From 0bed5cd99a519ba39a7d08a8e15680c8291f5189 Mon Sep 17 00:00:00 2001 From: ankitsridhar16 Date: Fri, 20 Jan 2023 14:57:47 +0530 Subject: [PATCH 30/35] chore: removed subscription handler related files --- .../src/subscription-handler.ts | 43 ------------------- .../src/types/custom-module-types.ts | 4 -- .../src/types/subscription-types.ts | 7 --- 3 files changed, 54 deletions(-) delete mode 100644 packages/hoppscotch-backend/src/subscription-handler.ts delete mode 100644 packages/hoppscotch-backend/src/types/custom-module-types.ts delete mode 100644 packages/hoppscotch-backend/src/types/subscription-types.ts diff --git a/packages/hoppscotch-backend/src/subscription-handler.ts b/packages/hoppscotch-backend/src/subscription-handler.ts deleted file mode 100644 index a47730c3b..000000000 --- a/packages/hoppscotch-backend/src/subscription-handler.ts +++ /dev/null @@ -1,43 +0,0 @@ -import { Injectable } from '@nestjs/common'; -import { PubSubService } from './pubsub/pubsub.service'; -import { PrimitiveTypes } from './types/primitive-types'; -import { CustomModuleTypes } from './types/custom-module-types'; -import { SubscriptionType } from './types/subscription-types'; - -// Custom generic type to indicate the type of module -type ModuleType = PrimitiveTypes | CustomModuleTypes; - -@Injectable() -export class SubscriptionHandler { - constructor(private readonly pubsub: PubSubService) {} - - /** - * Publishes a subscription using the pubsub module - * @param topic a string containing the "module_name/identifier" - * @param subscriptionType type of subscription being published - * @param moduleType type of the module model being called - * @returns a promise of type void - */ - async publish( - topic: string, - subscriptionType: SubscriptionType, - moduleType: ModuleType, - ) { - switch (subscriptionType) { - case SubscriptionType.Created: - await this.pubsub.publish(`${topic}/created`, moduleType); - break; - case SubscriptionType.Updated: - await this.pubsub.publish(`${topic}/updated`, moduleType); - break; - case SubscriptionType.Deleted: - await this.pubsub.publish(`${topic}/deleted`, moduleType); - break; - case SubscriptionType.DeleteMany: - await this.pubsub.publish(`${topic}/delete_many`, moduleType); - break; - default: - break; - } - } -} diff --git a/packages/hoppscotch-backend/src/types/custom-module-types.ts b/packages/hoppscotch-backend/src/types/custom-module-types.ts deleted file mode 100644 index 5d669e075..000000000 --- a/packages/hoppscotch-backend/src/types/custom-module-types.ts +++ /dev/null @@ -1,4 +0,0 @@ -import { UserEnvironment } from '../user-environment/user-environments.model'; -import { User } from '../user/user.model'; - -export type CustomModuleTypes = UserEnvironment | User; diff --git a/packages/hoppscotch-backend/src/types/subscription-types.ts b/packages/hoppscotch-backend/src/types/subscription-types.ts deleted file mode 100644 index ee7ab75f2..000000000 --- a/packages/hoppscotch-backend/src/types/subscription-types.ts +++ /dev/null @@ -1,7 +0,0 @@ -// Contains constants for the subscription types we use in Subscription Handler -export enum SubscriptionType { - Created = 'created', - Updated = 'updated', - Deleted = 'deleted', - DeleteMany = 'delete_many', -} From 298b960ef75054cd13c83a6cac13edbc46919364 Mon Sep 17 00:00:00 2001 From: ankitsridhar16 Date: Fri, 20 Jan 2023 15:18:04 +0530 Subject: [PATCH 31/35] chore: removed comment --- .../src/user-environment/user-environments.service.ts | 6 +----- 1 file changed, 1 insertion(+), 5 deletions(-) diff --git a/packages/hoppscotch-backend/src/user-environment/user-environments.service.ts b/packages/hoppscotch-backend/src/user-environment/user-environments.service.ts index 872395351..9daa9cb46 100644 --- a/packages/hoppscotch-backend/src/user-environment/user-environments.service.ts +++ b/packages/hoppscotch-backend/src/user-environment/user-environments.service.ts @@ -214,11 +214,7 @@ export class UserEnvironmentsService { }, }); - // await this.subscriptionHandler.publish( - // `user_environment/${uid}`, - // SubscriptionType.DeleteMany, - // deletedEnvironments.count, - // ); + // Publish subscription for multiple environment deletions await this.pubsub.publish( `user_environment/${uid}/deleted_many`, deletedEnvironments.count, From 6da85fd286b9aa06d6ce4ccc716d8b5104dbe0ce Mon Sep 17 00:00:00 2001 From: ankitsridhar16 Date: Fri, 20 Jan 2023 16:12:45 +0530 Subject: [PATCH 32/35] chore: updated types to def and changed deleted many as a new indexed type --- .../hoppscotch-backend/src/pubsub/pubsub.service.ts | 7 ++----- .../src/pubsub/subscriptionTopicsDefs.ts | 12 ++++-------- 2 files changed, 6 insertions(+), 13 deletions(-) diff --git a/packages/hoppscotch-backend/src/pubsub/pubsub.service.ts b/packages/hoppscotch-backend/src/pubsub/pubsub.service.ts index 854ef2e92..ba86342e1 100644 --- a/packages/hoppscotch-backend/src/pubsub/pubsub.service.ts +++ b/packages/hoppscotch-backend/src/pubsub/pubsub.service.ts @@ -4,7 +4,7 @@ import { default as Redis, RedisOptions } from 'ioredis'; import { RedisPubSub } from 'graphql-redis-subscriptions'; import { PubSub as LocalPubSub } from 'graphql-subscriptions'; -import { MessageType } from './subscriptionTopicsDefs'; +import { TopicDef } from './subscriptionTopicsDefs'; /** * RedisPubSub uses JSON parsing for back and forth conversion, which loses Date objects, hence this reviver brings them back @@ -71,10 +71,7 @@ export class PubSubService implements OnModuleInit { return this.pubsub.asyncIterator(topic, options); } - async publish( - topic: T, - payload: MessageType[T], - ) { + async publish(topic: T, payload: TopicDef[T]) { await this.pubsub.publish(topic, payload); } } diff --git a/packages/hoppscotch-backend/src/pubsub/subscriptionTopicsDefs.ts b/packages/hoppscotch-backend/src/pubsub/subscriptionTopicsDefs.ts index c860d55a4..4a1fbcb27 100644 --- a/packages/hoppscotch-backend/src/pubsub/subscriptionTopicsDefs.ts +++ b/packages/hoppscotch-backend/src/pubsub/subscriptionTopicsDefs.ts @@ -1,14 +1,10 @@ import { UserEnvironment } from '../user-environment/user-environments.model'; -import { PrimitiveTypes } from '../types/primitiveTypes'; // 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. -export type MessageType = { +export type TopicDef = { [ - topic: `user_environment/${string}/${ - | 'created' - | 'updated' - | 'deleted' - | 'deleted_many'}` - ]: UserEnvironment | PrimitiveTypes; // Returning a number hence having a union with `PrimitiveTypes`. + topic: `user_environment/${string}/${'created' | 'updated' | 'deleted'}` + ]: UserEnvironment; + [topic: `user_environment/${string}/deleted_many`]: number; }; From 606e0120ee3f9d9677eb0e80ade2a0dfba467fdd Mon Sep 17 00:00:00 2001 From: ankitsridhar16 Date: Fri, 20 Jan 2023 16:13:35 +0530 Subject: [PATCH 33/35] chore: removed the logic for primitiveTypes as it is unused --- packages/hoppscotch-backend/src/types/primitiveTypes.ts | 1 - 1 file changed, 1 deletion(-) delete mode 100644 packages/hoppscotch-backend/src/types/primitiveTypes.ts diff --git a/packages/hoppscotch-backend/src/types/primitiveTypes.ts b/packages/hoppscotch-backend/src/types/primitiveTypes.ts deleted file mode 100644 index 11918cf33..000000000 --- a/packages/hoppscotch-backend/src/types/primitiveTypes.ts +++ /dev/null @@ -1 +0,0 @@ -export type PrimitiveTypes = number | string | boolean; From 480a34c0f76d8c806a99d152b9d494f3fab1e941 Mon Sep 17 00:00:00 2001 From: ankitsridhar16 Date: Mon, 23 Jan 2023 09:54:20 +0530 Subject: [PATCH 34/35] fix: updated resolver pubsub topic name --- .../src/user-environment/user-environments.resolver.ts | 2 +- 1 file changed, 1 insertion(+), 1 deletion(-) diff --git a/packages/hoppscotch-backend/src/user-environment/user-environments.resolver.ts b/packages/hoppscotch-backend/src/user-environment/user-environments.resolver.ts index f053905bb..cccdf11cd 100644 --- a/packages/hoppscotch-backend/src/user-environment/user-environments.resolver.ts +++ b/packages/hoppscotch-backend/src/user-environment/user-environments.resolver.ts @@ -201,7 +201,7 @@ export class UserEnvironmentsResolver { @UseGuards(GqlAuthGuard) userEnvironmentDeleteMany(@GqlUser() user: User) { return this.pubsub.asyncIterator( - `user_environment/${user.uid}/delete_many`, + `user_environment/${user.uid}/deleted_many`, ); } } From cb1b13bdb47689a90a7394b371a9e55cf9e512fe Mon Sep 17 00:00:00 2001 From: ankitsridhar16 Date: Mon, 23 Jan 2023 11:49:41 +0530 Subject: [PATCH 35/35] chore: updated filename to topicDefs --- packages/hoppscotch-backend/src/pubsub/pubsub.service.ts | 2 +- .../src/pubsub/{subscriptionTopicsDefs.ts => topicsDefs.ts} | 0 2 files changed, 1 insertion(+), 1 deletion(-) rename packages/hoppscotch-backend/src/pubsub/{subscriptionTopicsDefs.ts => topicsDefs.ts} (100%) diff --git a/packages/hoppscotch-backend/src/pubsub/pubsub.service.ts b/packages/hoppscotch-backend/src/pubsub/pubsub.service.ts index ba86342e1..bec396028 100644 --- a/packages/hoppscotch-backend/src/pubsub/pubsub.service.ts +++ b/packages/hoppscotch-backend/src/pubsub/pubsub.service.ts @@ -4,7 +4,7 @@ import { default as Redis, RedisOptions } from 'ioredis'; import { RedisPubSub } from 'graphql-redis-subscriptions'; import { PubSub as LocalPubSub } from 'graphql-subscriptions'; -import { TopicDef } from './subscriptionTopicsDefs'; +import { TopicDef } from './topicsDefs'; /** * RedisPubSub uses JSON parsing for back and forth conversion, which loses Date objects, hence this reviver brings them back diff --git a/packages/hoppscotch-backend/src/pubsub/subscriptionTopicsDefs.ts b/packages/hoppscotch-backend/src/pubsub/topicsDefs.ts similarity index 100% rename from packages/hoppscotch-backend/src/pubsub/subscriptionTopicsDefs.ts rename to packages/hoppscotch-backend/src/pubsub/topicsDefs.ts