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/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 { 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/errors.ts b/packages/hoppscotch-backend/src/errors.ts index 794a06db5..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) @@ -155,6 +161,60 @@ 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_DOES_NOT_EXISTS = + 'user_environment/global_env_does_not_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_DOES_NOT_EXISTS = + 'user_environment/user_env_does_not_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; +/* + +/** + * User environment invalid environment name + * (UserEnvironmentsService) + */ +export const USER_ENVIRONMENT_INVALID_ENVIRONMENT_NAME = + 'user_environment/user_env_invalid_env_name' as const; /* |------------------------------------| diff --git a/packages/hoppscotch-backend/src/prisma/prisma.service.ts b/packages/hoppscotch-backend/src/prisma/prisma.service.ts index 5b962c430..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/client/scripts/default-index'; +import { PrismaClient } from '@prisma/client'; @Injectable() export class PrismaService diff --git a/packages/hoppscotch-backend/src/subscription-handler.ts b/packages/hoppscotch-backend/src/subscription-handler.ts new file mode 100644 index 000000000..a47730c3b --- /dev/null +++ b/packages/hoppscotch-backend/src/subscription-handler.ts @@ -0,0 +1,43 @@ +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 new file mode 100644 index 000000000..5d669e075 --- /dev/null +++ b/packages/hoppscotch-backend/src/types/custom-module-types.ts @@ -0,0 +1,4 @@ +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/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; 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/user-environment/user-environments.model.ts b/packages/hoppscotch-backend/src/user-environment/user-environments.model.ts new file mode 100644 index 000000000..be99821e3 --- /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; // 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', + }) + variables: string; // JSON string of the variables object (format:[{ key: "bla", value: "bla_val" }, ...] ) which will be received from the client + + @Field({ + description: '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..d82a1e899 --- /dev/null +++ b/packages/hoppscotch-backend/src/user-environment/user-environments.module.ts @@ -0,0 +1,20 @@ +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'; +import { SubscriptionHandler } from '../subscription-handler'; + +@Module({ + imports: [PrismaModule, PubSubModule, UserModule], + providers: [ + UserEnvironmentsResolver, + UserEnvironmentsService, + UserEnvsUserResolver, + SubscriptionHandler, + ], + 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..f053905bb --- /dev/null +++ b/packages/hoppscotch-backend/src/user-environment/user-environments.resolver.ts @@ -0,0 +1,207 @@ +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 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, + ): Promise { + const isGlobal = false; + const userEnvironment = + await this.userEnvironmentsService.createUserEnvironment( + user.uid, + name, + variables, + isGlobal, + ); + if (E.isLeft(userEnvironment)) throwErr(userEnvironment.left); + return userEnvironment.right; + } + + @Mutation(() => UserEnvironment, { + 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 userEnvironment = + await this.userEnvironmentsService.createUserEnvironment( + user.uid, + null, + 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( + @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(() => Boolean, { + description: 'Deletes a users personal environment', + }) + @UseGuards(GqlAuthGuard) + async deleteUserEnvironment( + @GqlUser() user: User, + @Args({ + name: 'id', + description: 'ID of the user environment', + type: () => ID, + }) + id: string, + ): Promise { + const userEnvironment = + await this.userEnvironmentsService.deleteUserEnvironment(user.uid, id); + if (E.isLeft(userEnvironment)) throwErr(userEnvironment.left); + return userEnvironment.right; + } + + @Mutation(() => Number, { + description: 'Deletes all of users 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 clearGlobalEnvironments( + @GqlUser() user: User, + @Args({ + name: 'id', + description: 'ID of the users global environment', + type: () => ID, + }) + id: string, + ): Promise { + const userEnvironment = + await this.userEnvironmentsService.clearGlobalEnvironments(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(@GqlUser() user: User) { + return this.pubsub.asyncIterator(`user_environment/${user.uid}/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 deletion', + resolve: (value) => value, + }) + @UseGuards(GqlAuthGuard) + userEnvironmentDeleted( + @Args({ + name: 'id', + description: 'Environment id', + type: () => ID, + }) + id: string, + ) { + return this.pubsub.asyncIterator(`user_environment/${id}/deleted`); + } + + @Subscription(() => Number, { + 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 new file mode 100644 index 000000000..894872cde --- /dev/null +++ b/packages/hoppscotch-backend/src/user-environment/user-environments.service.spec.ts @@ -0,0 +1,576 @@ +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_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'; +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 = [ + { + 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_DOES_NOT_EXISTS); + }); + }); + + describe('createUserEnvironment', () => { + 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, + }; + + 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); + + mockPrisma.userEnvironment.create.mockResolvedValueOnce({ + userUid: 'abc123', + id: '123', + name: null, + variables: [{}], + isGlobal: true, + }); + + const result: UserEnvironment = { + userUid: 'abc123', + id: '123', + name: null, + variables: JSON.stringify([{}]), + isGlobal: true, + }; + + return expect( + await userEnvironmentsService.createUserEnvironment( + 'abc123', + null, + '[{}]', + 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, + }); + + return expect( + await userEnvironmentsService.createUserEnvironment( + 'abc123', + null, + '[{}]', + 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 ', 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, + }; + + return expect( + await userEnvironmentsService.updateUserEnvironment( + 'abc123', + 'test', + '[{}]', + ), + ).toEqualRight(result); + }); + + 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: null, + variables: JSON.stringify([{}]), + isGlobal: true, + }; + + return expect( + await userEnvironmentsService.updateUserEnvironment( + 'abc123', + null, + '[{}]', + ), + ).toEqualRight(result); + }); + + test('Should resolve left and not update a users environment if env doesnt exist ', async () => { + mockPrisma.userEnvironment.update.mockRejectedValueOnce( + 'RejectOnNotFound', + ); + + return expect( + await userEnvironmentsService.updateUserEnvironment( + 'abc123', + 'test', + '[{}]', + ), + ).toEqualLeft(USER_ENVIRONMENT_ENV_DOES_NOT_EXISTS); + }); + + test('Should 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 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 ', async () => { + mockPrisma.userEnvironment.findFirst.mockResolvedValueOnce(null); + mockPrisma.userEnvironment.delete.mockResolvedValueOnce({ + userUid: 'abc123', + id: 'env1', + name: 'en1', + variables: [{}], + isGlobal: false, + }); + + return expect( + await userEnvironmentsService.deleteUserEnvironment('abc123', 'env1'), + ).toEqualRight(true); + }); + + 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_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 publish a subscription with a count of deleted environments', async () => { + mockPrisma.userEnvironment.deleteMany.mockResolvedValueOnce({ + count: 1, + }); + + await userEnvironmentsService.deleteUserEnvironments('abc123'); + + return expect(mockSubscriptionHandler.publish).toHaveBeenCalledWith( + `user_environment/abc123`, + SubscriptionType.DeleteMany, + 1, + ); + }); + }); + + 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, + }); + + const result: UserEnvironment = { + userUid: 'abc123', + id: 'env1', + name: 'en1', + variables: JSON.stringify([]), + isGlobal: true, + }; + + return expect( + await userEnvironmentsService.clearGlobalEnvironments('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_DOES_NOT_EXISTS); + }); + + 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: 'env1', + name: 'en1', + variables: JSON.stringify([]), + isGlobal: true, + }; + + await userEnvironmentsService.clearGlobalEnvironments('abc123', 'env1'); + + return expect(mockSubscriptionHandler.publish).toHaveBeenCalledWith( + `user_environment/${result.id}`, + SubscriptionType.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 new file mode 100644 index 000000000..a08549e0c --- /dev/null +++ b/packages/hoppscotch-backend/src/user-environment/user-environments.service.ts @@ -0,0 +1,285 @@ +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 * as O from 'fp-ts/Option'; +import { + USER_ENVIRONMENT_ENV_DOES_NOT_EXISTS, + USER_ENVIRONMENT_GLOBAL_ENV_DOES_NOT_EXISTS, + USER_ENVIRONMENT_GLOBAL_ENV_DELETION_FAILED, + 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'; +import { SubscriptionType } from '../types/subscription-types'; +import { stringToJson } from '../utils'; + +@Injectable() +export class UserEnvironmentsService { + constructor( + private readonly prisma: PrismaService, + private readonly pubsub: PubSubService, + private readonly subscriptionHandler: SubscriptionHandler, + ) {} + + /** + * Fetch personal user environments + * @param uid Users uid + * @returns array of users personal 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; + } + + /** + * 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_DOES_NOT_EXISTS); + } + + /** + * Create a personal or global user environment + * @param uid Users uid + * @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 + */ + async createUserEnvironment( + uid: string, + name: string, + 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 (!O.isNone(globalEnvExists)) + return E.left(USER_ENVIRONMENT_GLOBAL_ENV_EXISTS); + } + 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: envVariables.right, + 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.subscriptionHandler.publish( + `user_environment/${userEnvironment.userUid}`, + SubscriptionType.Created, + userEnvironment, + ); + 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) { + 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: envVariables.right, + }, + }); + + 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, + ); + return E.right(updatedUserEnvironment); + } catch (e) { + return E.left(USER_ENVIRONMENT_ENV_DOES_NOT_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(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 (O.isSome(globalEnvExists)) { + const globalEnv = globalEnvExists.value; + if (globalEnv.id === id) { + return E.left(USER_ENVIRONMENT_GLOBAL_ENV_DELETION_FAILED); + } + } + 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 deletion + await this.subscriptionHandler.publish( + `user_environment/${deletedUserEnvironment.id}`, + SubscriptionType.Deleted, + deletedUserEnvironment, + ); + return E.right(true); + } catch (e) { + return E.left(USER_ENVIRONMENT_ENV_DOES_NOT_EXISTS); + } + } + + /** + * Deletes all existing personal user environments + * @param uid user uid + * @returns a count of environments deleted + */ + async deleteUserEnvironments(uid: string) { + const deletedEnvironments = await this.prisma.userEnvironment.deleteMany({ + where: { + userUid: uid, + isGlobal: false, + }, + }); + + await this.subscriptionHandler.publish( + `user_environment/${uid}`, + SubscriptionType.DeleteMany, + deletedEnvironments.count, + ); + + return deletedEnvironments.count; + } + + /** + * Removes all existing variables in a users global environment + * @param uid users uid + * @param id environment id + * @returns an `` of environments deleted + */ + async clearGlobalEnvironments(uid: string, id: string) { + const globalEnvExists = await this.checkForExistingGlobalEnv(uid); + if (O.isNone(globalEnvExists)) + return E.left(USER_ENVIRONMENT_GLOBAL_ENV_DOES_NOT_EXISTS); + + 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, + ); + 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 + private async checkForExistingGlobalEnv(uid: string) { + const globalEnv = await this.prisma.userEnvironment.findFirst({ + where: { + userUid: uid, + isGlobal: true, + }, + }); + + if (globalEnv == null) return O.none; + return O.some(globalEnv); + } +} 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..0d6d31cf4 --- /dev/null +++ b/packages/hoppscotch-backend/src/user-environment/user.resolver.ts @@ -0,0 +1,29 @@ +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 { + 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 the users global environments', + }) + async globalEnvironments( + @Parent() user: User, + ): Promise { + const userEnvironment = + await this.userEnvironmentsService.fetchUserGlobalEnvironment(user.uid); + if (E.isLeft(userEnvironment)) throwErr(userEnvironment.left); + return userEnvironment.right; + } +} 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); + } +} diff --git a/packages/hoppscotch-backend/tsconfig.json b/packages/hoppscotch-backend/tsconfig.json index adb614cab..02420b5f7 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": true, } }