From 1b00ff6c9e0b3b5ecf744fa815ba2f7f82cc3807 Mon Sep 17 00:00:00 2001 From: Balu Babu Date: Mon, 6 Nov 2023 13:08:59 +0530 Subject: [PATCH] chore: moved shared-requests into shortcode module --- packages/hoppscotch-backend/src/app.module.ts | 2 - packages/hoppscotch-backend/src/errors.ts | 24 +- packages/hoppscotch-backend/src/gql-schema.ts | 2 - .../src/pubsub/topicsDefs.ts | 6 +- .../shared-request/shared-request.module.ts | 12 - .../shared-request/shared-request.resolver.ts | 168 ------- .../shared-request.service.spec.ts | 473 ------------------ .../shared-request/shared-request.service.ts | 259 ---------- .../shared-request/shared-requests.model.ts | 25 - .../src/shortcode/shortcode.model.ts | 10 +- .../src/shortcode/shortcode.resolver.ts | 59 ++- .../src/shortcode/shortcode.service.spec.ts | 272 +++++++--- .../src/shortcode/shortcode.service.ts | 87 +++- 13 files changed, 339 insertions(+), 1060 deletions(-) delete mode 100644 packages/hoppscotch-backend/src/shared-request/shared-request.module.ts delete mode 100644 packages/hoppscotch-backend/src/shared-request/shared-request.resolver.ts delete mode 100644 packages/hoppscotch-backend/src/shared-request/shared-request.service.spec.ts delete mode 100644 packages/hoppscotch-backend/src/shared-request/shared-request.service.ts delete mode 100644 packages/hoppscotch-backend/src/shared-request/shared-requests.model.ts diff --git a/packages/hoppscotch-backend/src/app.module.ts b/packages/hoppscotch-backend/src/app.module.ts index 344cf09fe..3d659351f 100644 --- a/packages/hoppscotch-backend/src/app.module.ts +++ b/packages/hoppscotch-backend/src/app.module.ts @@ -20,7 +20,6 @@ import { ShortcodeModule } from './shortcode/shortcode.module'; import { COOKIES_NOT_FOUND } from './errors'; import { ThrottlerModule } from '@nestjs/throttler'; import { AppController } from './app.controller'; -import { SharedRequestModule } from './shared-request/shared-request.module'; @Module({ imports: [ @@ -78,7 +77,6 @@ import { SharedRequestModule } from './shared-request/shared-request.module'; TeamInvitationModule, UserCollectionModule, ShortcodeModule, - SharedRequestModule, ], providers: [GQLComplexityPlugin], controllers: [AppController], diff --git a/packages/hoppscotch-backend/src/errors.ts b/packages/hoppscotch-backend/src/errors.ts index dcfa32ba3..3314acf59 100644 --- a/packages/hoppscotch-backend/src/errors.ts +++ b/packages/hoppscotch-backend/src/errors.ts @@ -622,29 +622,23 @@ export const MAILER_SMTP_URL_UNDEFINED = 'mailer/smtp_url_undefined' as const; export const MAILER_FROM_ADDRESS_UNDEFINED = 'mailer/from_address_undefined' as const; -/** - * SharedRequest not found in DB - * (SharedRequestService) - */ -export const SHARED_REQUEST_NOT_FOUND = 'shared_request/not_found' as const; - /** * SharedRequest invalid request JSON format - * (SharedRequestService) + * (ShortcodeService) */ -export const SHARED_REQUEST_INVALID_REQUEST_JSON = - 'shared_request/request_invalid_format' as const; +export const SHORTCODE_INVALID_REQUEST_JSON = + 'shortcode/request_invalid_format' as const; /** * SharedRequest invalid properties JSON format - * (SharedRequestService) + * (ShortcodeService) */ -export const SHARED_REQUEST_INVALID_PROPERTIES_JSON = - 'shared_request/properties_invalid_format' as const; +export const SHORTCODE_INVALID_PROPERTIES_JSON = + 'shortcode/properties_invalid_format' as const; /** * SharedRequest invalid properties not found - * (SharedRequestService) + * (ShortcodeService) */ -export const SHARED_REQUEST_PROPERTIES_NOT_FOUND = - 'shared_request/properties_not_found' as const; +export const SHORTCODE_PROPERTIES_NOT_FOUND = + 'shortcode/properties_not_found' as const; diff --git a/packages/hoppscotch-backend/src/gql-schema.ts b/packages/hoppscotch-backend/src/gql-schema.ts index d86ef015d..0fedcd7f6 100644 --- a/packages/hoppscotch-backend/src/gql-schema.ts +++ b/packages/hoppscotch-backend/src/gql-schema.ts @@ -27,7 +27,6 @@ import { UserRequestUserCollectionResolver } from './user-request/resolvers/user import { UserEnvsUserResolver } from './user-environment/user.resolver'; import { UserHistoryUserResolver } from './user-history/user.resolver'; import { UserSettingsUserResolver } from './user-settings/user.resolver'; -import { SharedRequestResolver } from './shared-request/shared-request.resolver'; /** * All the resolvers present in the application. @@ -57,7 +56,6 @@ const RESOLVERS = [ UserRequestUserCollectionResolver, UserSettingsResolver, UserSettingsUserResolver, - SharedRequestResolver, ]; /** diff --git a/packages/hoppscotch-backend/src/pubsub/topicsDefs.ts b/packages/hoppscotch-backend/src/pubsub/topicsDefs.ts index c0ccf764c..b0cc8a854 100644 --- a/packages/hoppscotch-backend/src/pubsub/topicsDefs.ts +++ b/packages/hoppscotch-backend/src/pubsub/topicsDefs.ts @@ -27,7 +27,6 @@ import { UserCollectionReorderData, } from 'src/user-collection/user-collections.model'; import { Shortcode } from 'src/shortcode/shortcode.model'; -import { SharedRequest } from 'src/shared-request/shared-requests.model'; // A custom message type that defines the topic and the corresponding payload. // For every module that publishes a subscription add its type def and the possible subscription type. @@ -70,8 +69,7 @@ export type TopicDef = { [topic: `team_req/${string}/req_deleted`]: string; [topic: `team/${string}/invite_added`]: TeamInvitation; [topic: `team/${string}/invite_removed`]: string; - [topic: `shortcode/${string}/${'created' | 'revoked'}`]: Shortcode; [ - topic: `shared_request/${string}/${'created' | 'revoked' | 'updated'}` - ]: SharedRequest; + topic: `shortcode/${string}/${'created' | 'revoked' | 'updated'}` + ]: Shortcode; }; diff --git a/packages/hoppscotch-backend/src/shared-request/shared-request.module.ts b/packages/hoppscotch-backend/src/shared-request/shared-request.module.ts deleted file mode 100644 index b184707e0..000000000 --- a/packages/hoppscotch-backend/src/shared-request/shared-request.module.ts +++ /dev/null @@ -1,12 +0,0 @@ -import { Module } from '@nestjs/common'; -import { SharedRequestService } from './shared-request.service'; -import { SharedRequestResolver } from './shared-request.resolver'; -import { PubSubModule } from 'src/pubsub/pubsub.module'; -import { PrismaModule } from 'src/prisma/prisma.module'; -import { UserModule } from 'src/user/user.module'; - -@Module({ - imports: [PrismaModule, PubSubModule, UserModule], - providers: [SharedRequestService, SharedRequestResolver], -}) -export class SharedRequestModule {} diff --git a/packages/hoppscotch-backend/src/shared-request/shared-request.resolver.ts b/packages/hoppscotch-backend/src/shared-request/shared-request.resolver.ts deleted file mode 100644 index b1a7ead8b..000000000 --- a/packages/hoppscotch-backend/src/shared-request/shared-request.resolver.ts +++ /dev/null @@ -1,168 +0,0 @@ -import { - Args, - ID, - Resolver, - Query, - Mutation, - Subscription, -} from '@nestjs/graphql'; -import { SharedRequest } from './shared-requests.model'; -import { GqlThrottlerGuard } from 'src/guards/gql-throttler.guard'; -import { UseGuards } from '@nestjs/common'; -import { SharedRequestService } from './shared-request.service'; -import { PubSubService } from 'src/pubsub/pubsub.service'; -import * as E from 'fp-ts/Either'; -import { GqlAuthGuard } from 'src/guards/gql-auth.guard'; -import { throwErr } from 'src/utils'; -import { GqlUser } from 'src/decorators/gql-user.decorator'; -import { AuthUser } from 'src/types/AuthUser'; -import { SkipThrottle } from '@nestjs/throttler'; -import { PaginationArgs } from 'src/types/input-types.args'; - -@UseGuards(GqlThrottlerGuard) -@Resolver(() => SharedRequest) -export class SharedRequestResolver { - constructor( - private readonly sharedRequestService: SharedRequestService, - private readonly pubsub: PubSubService, - ) {} - - /* Queries */ - @Query(() => SharedRequest, { - description: 'Resolves and returns a shared-request data', - }) - async sharedRequest( - @Args({ - name: 'code', - type: () => ID, - description: 'The shared-request to fetch', - }) - code: string, - ) { - const result = await this.sharedRequestService.getSharedRequest(code); - - if (E.isLeft(result)) throwErr(result.left); - return result.right; - } - - @Query(() => [SharedRequest], { - description: 'List all shared-request the current user has generated', - }) - @UseGuards(GqlAuthGuard) - async mySharedRequests( - @GqlUser() user: AuthUser, - @Args() args: PaginationArgs, - ) { - return this.sharedRequestService.fetchUserSharedRequests(user.uid, args); - } - - /* Mutations */ - @Mutation(() => SharedRequest, { - description: 'Create a shared-request for the given request.', - }) - @UseGuards(GqlAuthGuard) - async createSharedRequest( - @GqlUser() user: AuthUser, - @Args({ - name: 'request', - description: 'JSON string of the request object', - }) - request: string, - @Args({ - name: 'properties', - description: 'JSON string of the properties of the embed', - nullable: true, - }) - properties: string, - ) { - const result = await this.sharedRequestService.createSharedRequest( - request, - properties, - user, - ); - - if (E.isLeft(result)) throwErr(result.left); - return result.right; - } - - @Mutation(() => SharedRequest, { - description: 'Update a user generated shared-request', - }) - @UseGuards(GqlAuthGuard) - async updateSharedRequest( - @GqlUser() user: AuthUser, - @Args({ - name: 'code', - type: () => ID, - description: 'The shared-request to update', - }) - code: string, - @Args({ - name: 'properties', - description: 'JSON string of the properties of the embed', - }) - properties: string, - ) { - const result = await this.sharedRequestService.updateSharedRequest( - code, - user.uid, - properties, - ); - - if (E.isLeft(result)) throwErr(result.left); - return result.right; - } - - @Mutation(() => Boolean, { - description: 'Revoke a user generated shared-request', - }) - @UseGuards(GqlAuthGuard) - async revokeSharedRequest( - @GqlUser() user: AuthUser, - @Args({ - name: 'code', - type: () => ID, - description: 'The shared-request to resolve', - }) - code: string, - ) { - const result = await this.sharedRequestService.revokeSharedRequest( - code, - user.uid, - ); - - if (E.isLeft(result)) throwErr(result.left); - return result.right; - } - - /* Subscriptions */ - @Subscription(() => SharedRequest, { - description: 'Listen for shared-request creation', - resolve: (value) => value, - }) - @SkipThrottle() - @UseGuards(GqlAuthGuard) - mySharedRequestCreated(@GqlUser() user: AuthUser) { - return this.pubsub.asyncIterator(`shared_request/${user.uid}/created`); - } - - @Subscription(() => SharedRequest, { - description: 'Listen for shared-request updates', - resolve: (value) => value, - }) - @SkipThrottle() - @UseGuards(GqlAuthGuard) - mySharedRequestUpdated(@GqlUser() user: AuthUser) { - return this.pubsub.asyncIterator(`shared_request/${user.uid}/updated`); - } - - @Subscription(() => SharedRequest, { - description: 'Listen for shared-request deletion', - resolve: (value) => value, - }) - @SkipThrottle() - @UseGuards(GqlAuthGuard) - mySharedRequestRevoked(@GqlUser() user: AuthUser) { - return this.pubsub.asyncIterator(`shared_request/${user.uid}/revoked`); - } -} diff --git a/packages/hoppscotch-backend/src/shared-request/shared-request.service.spec.ts b/packages/hoppscotch-backend/src/shared-request/shared-request.service.spec.ts deleted file mode 100644 index 3908d7efd..000000000 --- a/packages/hoppscotch-backend/src/shared-request/shared-request.service.spec.ts +++ /dev/null @@ -1,473 +0,0 @@ -import { mockDeep, mockReset } from 'jest-mock-extended'; -import { PrismaService } from '../prisma/prisma.service'; -import { - SHARED_REQUEST_INVALID_PROPERTIES_JSON, - SHARED_REQUEST_INVALID_REQUEST_JSON, - SHARED_REQUEST_NOT_FOUND, - SHARED_REQUEST_PROPERTIES_NOT_FOUND, -} from 'src/errors'; -import { UserService } from 'src/user/user.service'; -import { SharedRequestService } from './shared-request.service'; -import { SharedRequest } from './shared-requests.model'; -import { AuthUser } from 'src/types/AuthUser'; - -const mockPrisma = mockDeep(); - -const mockPubSub = { - publish: jest.fn().mockResolvedValue(null), -}; - -const mockDocFunc = jest.fn(); - -const mockUserService = new UserService(mockPrisma as any, mockPubSub as any); - -// eslint-disable-next-line @typescript-eslint/ban-ts-comment -// @ts-ignore -const sharedRequestsService = new SharedRequestService( - mockPrisma, - mockPubSub as any, - mockUserService, -); - -beforeEach(() => { - mockReset(mockPrisma); - mockPubSub.publish.mockClear(); -}); -const createdOn = new Date(); - -const user: AuthUser = { - uid: '123344', - email: 'dwight@dundermifflin.com', - displayName: 'Dwight Schrute', - photoURL: 'https://en.wikipedia.org/wiki/Dwight_Schrute', - isAdmin: false, - refreshToken: 'hbfvdkhjbvkdvdfjvbnkhjb', - createdOn: createdOn, - currentGQLSession: {}, - currentRESTSession: {}, -}; - -const mockEmbed = { - id: '123', - request: '{}', - properties: '{}', - createdOn: createdOn, - creatorUid: user.uid, - updatedOn: createdOn, -}; - -const mockShortcode = { - id: '123', - request: '{}', - properties: null, - createdOn: createdOn, - creatorUid: user.uid, - updatedOn: createdOn, -}; - -const sharedRequests = [ - { - id: 'blablabla', - request: { - hello: 'there', - }, - properties: { - foo: 'bar', - }, - creatorUid: user.uid, - createdOn: new Date(), - updatedOn: createdOn, - }, - { - id: 'blablabla1', - request: { - hello: 'there', - }, - properties: { - foo: 'bar', - }, - creatorUid: user.uid, - createdOn: new Date(), - updatedOn: createdOn, - }, -]; - -describe('SharedRequestService', () => { - describe('getSharedRequest', () => { - test('should return a valid SharedRequest with valid SharedRequest ID', async () => { - mockPrisma.shortcode.findFirstOrThrow.mockResolvedValueOnce(mockEmbed); - - const result = await sharedRequestsService.getSharedRequest(mockEmbed.id); - expect(result).toEqualRight({ - id: mockEmbed.id, - createdOn: mockEmbed.createdOn, - request: JSON.stringify(mockEmbed.request), - properties: JSON.stringify(mockEmbed.properties), - }); - }); - - test('should throw SHARED_REQUEST_NOT_FOUND error when SharedRequest ID is invalid', async () => { - mockPrisma.shortcode.findFirstOrThrow.mockRejectedValueOnce( - 'NotFoundError', - ); - - const result = await sharedRequestsService.getSharedRequest('invalidID'); - expect(result).toEqualLeft(SHARED_REQUEST_NOT_FOUND); - }); - }); - - describe('createSharedRequest', () => { - test('should throw SHARED_REQUEST_INVALID_REQUEST_JSON error if incoming request data is invalid', async () => { - const result = await sharedRequestsService.createSharedRequest( - 'invalidRequest', - null, - user, - ); - expect(result).toEqualLeft(SHARED_REQUEST_INVALID_REQUEST_JSON); - }); - - test('should throw SHARED_REQUEST_INVALID_PROPERTIES_JSON error if incoming properties data is invalid', async () => { - const result = await sharedRequestsService.createSharedRequest( - '{}', - 'invalid_data', - user, - ); - expect(result).toEqualLeft(SHARED_REQUEST_INVALID_PROPERTIES_JSON); - }); - - test('should successfully create a new Embed with valid user uid', async () => { - // generateUniqueShortCodeID --> getSharedRequest - mockPrisma.shortcode.findFirstOrThrow.mockRejectedValueOnce( - 'NotFoundError', - ); - mockPrisma.shortcode.create.mockResolvedValueOnce(mockEmbed); - - const result = await sharedRequestsService.createSharedRequest( - '{}', - '{}', - user, - ); - expect(result).toEqualRight({ - id: mockEmbed.id, - createdOn: mockEmbed.createdOn, - request: JSON.stringify(mockEmbed.request), - properties: JSON.stringify(mockEmbed.properties), - }); - }); - - test('should successfully create a new ShortCode with valid user uid', async () => { - // generateUniqueShortCodeID --> getSharedRequest - mockPrisma.shortcode.findFirstOrThrow.mockRejectedValueOnce( - 'NotFoundError', - ); - mockPrisma.shortcode.create.mockResolvedValueOnce(mockShortcode); - - const result = await sharedRequestsService.createSharedRequest( - '{}', - null, - user, - ); - expect(result).toEqualRight({ - id: mockShortcode.id, - createdOn: mockShortcode.createdOn, - request: JSON.stringify(mockShortcode.request), - properties: mockShortcode.properties, - }); - }); - - test('should send pubsub message to `shared_request/{uid}/created` on successful creation of a Shortcode', async () => { - // generateUniqueShortCodeID --> getSharedRequest - mockPrisma.shortcode.findFirstOrThrow.mockRejectedValueOnce( - 'NotFoundError', - ); - mockPrisma.shortcode.create.mockResolvedValueOnce(mockShortcode); - - const result = await sharedRequestsService.createSharedRequest( - '{}', - null, - user, - ); - - expect(mockPubSub.publish).toHaveBeenCalledWith( - `shared_request/${mockShortcode.creatorUid}/created`, - { - id: mockShortcode.id, - createdOn: mockShortcode.createdOn, - request: JSON.stringify(mockShortcode.request), - properties: mockShortcode.properties, - }, - ); - }); - - test('should send pubsub message to `shared_request/{uid}/created` on successful creation of an Embed', async () => { - // generateUniqueShortCodeID --> getSharedRequest - mockPrisma.shortcode.findFirstOrThrow.mockRejectedValueOnce( - 'NotFoundError', - ); - mockPrisma.shortcode.create.mockResolvedValueOnce(mockEmbed); - - const result = await sharedRequestsService.createSharedRequest( - '{}', - '{}', - user, - ); - - expect(mockPubSub.publish).toHaveBeenCalledWith( - `shared_request/${mockEmbed.creatorUid}/created`, - { - id: mockEmbed.id, - createdOn: mockEmbed.createdOn, - request: JSON.stringify(mockEmbed.request), - properties: JSON.stringify(mockEmbed.properties), - }, - ); - }); - }); - - describe('fetchUserSharedRequests', () => { - test('should return list of SharedRequests with valid inputs and no cursor', async () => { - mockPrisma.shortcode.findMany.mockResolvedValueOnce(sharedRequests); - - const result = await sharedRequestsService.fetchUserSharedRequests( - user.uid, - { - cursor: null, - take: 10, - }, - ); - expect(result).toEqual([ - { - id: sharedRequests[0].id, - request: JSON.stringify(sharedRequests[0].request), - properties: JSON.stringify(sharedRequests[0].properties), - createdOn: sharedRequests[0].createdOn, - }, - { - id: sharedRequests[1].id, - request: JSON.stringify(sharedRequests[1].request), - properties: JSON.stringify(sharedRequests[1].properties), - createdOn: sharedRequests[1].createdOn, - }, - ]); - }); - - test('should return list of SharedRequests with valid inputs and cursor', async () => { - mockPrisma.shortcode.findMany.mockResolvedValue([sharedRequests[1]]); - - const result = await sharedRequestsService.fetchUserSharedRequests( - user.uid, - { - cursor: 'blablabla', - take: 10, - }, - ); - expect(result).toEqual([ - { - id: sharedRequests[1].id, - request: JSON.stringify(sharedRequests[1].request), - properties: JSON.stringify(sharedRequests[1].properties), - createdOn: sharedRequests[1].createdOn, - }, - ]); - }); - - test('should return an empty array for an invalid cursor', async () => { - mockPrisma.shortcode.findMany.mockResolvedValue([]); - - const result = await sharedRequestsService.fetchUserSharedRequests( - user.uid, - { - cursor: 'invalidcursor', - take: 10, - }, - ); - - expect(result).toHaveLength(0); - }); - - test('should return an empty array for an invalid user id and null cursor', async () => { - mockPrisma.shortcode.findMany.mockResolvedValue([]); - - const result = await sharedRequestsService.fetchUserSharedRequests( - 'invalidid', - { - cursor: null, - take: 10, - }, - ); - - expect(result).toHaveLength(0); - }); - - test('should return an empty array for an invalid user id and an invalid cursor', async () => { - mockPrisma.shortcode.findMany.mockResolvedValue([]); - - const result = await sharedRequestsService.fetchUserSharedRequests( - 'invalidid', - { - cursor: 'invalidcursor', - take: 10, - }, - ); - - expect(result).toHaveLength(0); - }); - }); - - describe('revokeSharedRequest', () => { - test('should return true on successful deletion of SharedRequest with valid inputs', async () => { - mockPrisma.shortcode.delete.mockResolvedValueOnce(mockEmbed); - - const result = await sharedRequestsService.revokeSharedRequest( - mockEmbed.id, - mockEmbed.creatorUid, - ); - - expect(mockPrisma.shortcode.delete).toHaveBeenCalledWith({ - where: { - creator_uid_shortcode_unique: { - creatorUid: mockEmbed.creatorUid, - id: mockEmbed.id, - }, - }, - }); - - expect(result).toEqualRight(true); - }); - - test('should return SHARED_REQUEST_NOT_FOUND error when SharedRequest is invalid and user uid is valid', async () => { - mockPrisma.shortcode.delete.mockRejectedValue('RecordNotFound'); - expect( - sharedRequestsService.revokeSharedRequest('invalid', 'testuser'), - ).resolves.toEqualLeft(SHARED_REQUEST_NOT_FOUND); - }); - - test('should return SHARED_REQUEST_NOT_FOUND error when SharedRequest is valid and user uid is invalid', async () => { - mockPrisma.shortcode.delete.mockRejectedValue('RecordNotFound'); - expect( - sharedRequestsService.revokeSharedRequest( - 'blablablabla', - 'invalidUser', - ), - ).resolves.toEqualLeft(SHARED_REQUEST_NOT_FOUND); - }); - - test('should return SHARED_REQUEST_NOT_FOUND error when both SharedRequest and user uid are invalid', async () => { - mockPrisma.shortcode.delete.mockRejectedValue('RecordNotFound'); - expect( - sharedRequestsService.revokeSharedRequest('invalid', 'invalid'), - ).resolves.toEqualLeft(SHARED_REQUEST_NOT_FOUND); - }); - - test('should send pubsub message to `shared_request/{uid}/revoked` on successful deletion of SharedRequest', async () => { - mockPrisma.shortcode.delete.mockResolvedValueOnce(mockEmbed); - - const result = await sharedRequestsService.revokeSharedRequest( - mockEmbed.id, - mockEmbed.creatorUid, - ); - - expect(mockPubSub.publish).toHaveBeenCalledWith( - `shared_request/${mockEmbed.creatorUid}/revoked`, - { - id: mockEmbed.id, - createdOn: mockEmbed.createdOn, - request: JSON.stringify(mockEmbed.request), - properties: JSON.stringify(mockEmbed.properties), - }, - ); - }); - }); - - describe('deleteUserSharedRequests', () => { - test('should successfully delete all users SharedRequests with valid user uid', async () => { - mockPrisma.shortcode.deleteMany.mockResolvedValueOnce({ count: 1 }); - - const result = await sharedRequestsService.deleteUserSharedRequests( - mockEmbed.creatorUid, - ); - expect(result).toEqual(1); - }); - - test('should return 0 when user uid is invalid', async () => { - mockPrisma.shortcode.deleteMany.mockResolvedValueOnce({ count: 0 }); - - const result = await sharedRequestsService.deleteUserSharedRequests( - mockEmbed.creatorUid, - ); - expect(result).toEqual(0); - }); - }); - - describe('updateSharedRequest', () => { - test('should return SHARED_REQUEST_PROPERTIES_NOT_FOUND error when updatedProps in invalid', async () => { - const result = await sharedRequestsService.updateSharedRequest( - mockEmbed.id, - user.uid, - '', - ); - expect(result).toEqualLeft(SHARED_REQUEST_PROPERTIES_NOT_FOUND); - }); - - test('should return SHARED_REQUEST_PROPERTIES_NOT_FOUND error when updatedProps in invalid JSON format', async () => { - const result = await sharedRequestsService.updateSharedRequest( - mockEmbed.id, - user.uid, - '{kk', - ); - expect(result).toEqualLeft(SHARED_REQUEST_INVALID_PROPERTIES_JSON); - }); - - test('should return SHARED_REQUEST_NOT_FOUND error when SharedRequest ID is invalid', async () => { - mockPrisma.shortcode.update.mockRejectedValue('RecordNotFound'); - const result = await sharedRequestsService.updateSharedRequest( - 'invalidID', - user.uid, - '{}', - ); - expect(result).toEqualLeft(SHARED_REQUEST_NOT_FOUND); - }); - - test('should successfully update a SharedRequests with valid inputs', async () => { - mockPrisma.shortcode.update.mockResolvedValueOnce({ - ...mockEmbed, - properties: '{"foo":"bar"}', - }); - - const result = await sharedRequestsService.updateSharedRequest( - mockEmbed.id, - user.uid, - '{"foo":"bar"}', - ); - expect(result).toEqualRight({ - id: mockEmbed.id, - createdOn: mockEmbed.createdOn, - request: JSON.stringify(mockEmbed.request), - properties: JSON.stringify('{"foo":"bar"}'), - }); - }); - - test('should send pubsub message to `shared_request/{uid}/updated` on successful Update of SharedRequest', async () => { - mockPrisma.shortcode.update.mockResolvedValueOnce({ - ...mockEmbed, - properties: '{"foo":"bar"}', - }); - - const result = await sharedRequestsService.updateSharedRequest( - mockEmbed.id, - user.uid, - '{"foo":"bar"}', - ); - - expect(mockPubSub.publish).toHaveBeenCalledWith( - `shared_request/${mockEmbed.creatorUid}/updated`, - { - id: mockEmbed.id, - createdOn: mockEmbed.createdOn, - request: JSON.stringify(mockEmbed.request), - properties: JSON.stringify('{"foo":"bar"}'), - }, - ); - }); - }); -}); diff --git a/packages/hoppscotch-backend/src/shared-request/shared-request.service.ts b/packages/hoppscotch-backend/src/shared-request/shared-request.service.ts deleted file mode 100644 index fc64d1c54..000000000 --- a/packages/hoppscotch-backend/src/shared-request/shared-request.service.ts +++ /dev/null @@ -1,259 +0,0 @@ -import { Injectable, OnModuleInit } from '@nestjs/common'; -import { PrismaService } from 'src/prisma/prisma.service'; -import { PubSubService } from 'src/pubsub/pubsub.service'; -import { Shortcode as DBSharedRequest } from '@prisma/client'; -import * as E from 'fp-ts/Either'; -import * as TO from 'fp-ts/TaskOption'; -import * as T from 'fp-ts/Task'; -import { SharedRequest } from './shared-requests.model'; -import { - SHARED_REQUEST_INVALID_PROPERTIES_JSON, - SHARED_REQUEST_INVALID_REQUEST_JSON, - SHARED_REQUEST_NOT_FOUND, - SHARED_REQUEST_PROPERTIES_NOT_FOUND, -} from 'src/errors'; -import { stringToJson } from 'src/utils'; -import { AuthUser } from 'src/types/AuthUser'; -import { PaginationArgs } from 'src/types/input-types.args'; -import { UserDataHandler } from 'src/user/user.data.handler'; -import { UserService } from 'src/user/user.service'; - -const SHORT_CODE_LENGTH = 12; -const SHORT_CODE_CHARS = - 'abcdefghijklmnopqrstuvwxyzABCDEFGHIJKLMNOPQRSTUVWXYZ0123456789'; -@Injectable() -export class SharedRequestService implements UserDataHandler, OnModuleInit { - constructor( - private readonly prisma: PrismaService, - private readonly pubsub: PubSubService, - private readonly userService: UserService, - ) {} - - onModuleInit() { - this.userService.registerUserDataHandler(this); - } - - canAllowUserDeletion(user: AuthUser): TO.TaskOption { - return TO.none; - } - - onUserDelete(user: AuthUser): T.Task { - return async () => { - await this.deleteUserSharedRequests(user.uid); - }; - } - - /** - * Delete all the Users SharedRequests - * @param uid User Uid - * @returns number of all deleted user SharedRequests - */ - async deleteUserSharedRequests(uid: string) { - const deletedShortCodes = await this.prisma.shortcode.deleteMany({ - where: { - creatorUid: uid, - }, - }); - - return deletedShortCodes.count; - } - - /** - * Converts a Prisma SharedRequest type into the SharedRequest model - * - * @param sharedRequestInfo Prisma SharedRequest type - * @returns GQL SharedRequest - */ - private cast(sharedRequestInfo: DBSharedRequest): SharedRequest { - return { - id: sharedRequestInfo.id, - request: JSON.stringify(sharedRequestInfo.request), - properties: - sharedRequestInfo.properties != null - ? JSON.stringify(sharedRequestInfo.properties) - : null, - createdOn: sharedRequestInfo.createdOn, - }; - } - /** - * Generate a shortcode - * - * @returns generated shortcode - */ - private generateShortCodeID(): string { - let result = ''; - for (let i = 0; i < SHORT_CODE_LENGTH; i++) { - result += - SHORT_CODE_CHARS[Math.floor(Math.random() * SHORT_CODE_CHARS.length)]; - } - return result; - } - - /** - * Check to see if ShortCode is already present in DB - * - * @returns Shortcode - */ - private async generateUniqueShortCodeID() { - while (true) { - const code = this.generateShortCodeID(); - - const data = await this.getSharedRequest(code); - if (E.isLeft(data)) return E.right(code); - } - } - - /** - * Fetch details regarding a SharedRequest - * - * @param sharedRequestID SharedRequest - * @returns Either of SharedRequest details or error - */ - async getSharedRequest(sharedRequestID: string) { - try { - const sharedRequest = await this.prisma.shortcode.findFirstOrThrow({ - where: { id: sharedRequestID }, - }); - return E.right(this.cast(sharedRequest)); - } catch (error) { - return E.left(SHARED_REQUEST_NOT_FOUND); - } - } - - /** - * Create a new SharedRequest - * - * @param request JSON string of request details - * @param properties JSON string of embed properties, if present - * @returns Either of SharedRequest or error - */ - async createSharedRequest( - request: string, - properties: string | null = null, - userInfo: AuthUser, - ) { - const requestData = stringToJson(request); - if (E.isLeft(requestData)) - return E.left(SHARED_REQUEST_INVALID_REQUEST_JSON); - - const parsedProperties = stringToJson(properties); - if (E.isLeft(parsedProperties)) - return E.left(SHARED_REQUEST_INVALID_PROPERTIES_JSON); - - const generatedShortCode = await this.generateUniqueShortCodeID(); - if (E.isLeft(generatedShortCode)) return E.left(generatedShortCode.left); - - const createdSharedRequest = await this.prisma.shortcode.create({ - data: { - id: generatedShortCode.right, - request: requestData.right, - properties: parsedProperties.right ?? undefined, - creatorUid: userInfo.uid, - }, - }); - - this.pubsub.publish( - `shared_request/${createdSharedRequest.creatorUid}/created`, - this.cast(createdSharedRequest), - ); - - return E.right(this.cast(createdSharedRequest)); - } - - /** - * Fetch SharedRequest created by a User - * - * @param uid User Uid - * @param args Pagination arguments - * @returns Array of SharedRequest - */ - async fetchUserSharedRequests(uid: string, args: PaginationArgs) { - const sharedRequests = await this.prisma.shortcode.findMany({ - where: { - creatorUid: uid, - }, - orderBy: { - createdOn: 'desc', - }, - skip: args.cursor ? 1 : 0, - take: args.take, - cursor: args.cursor ? { id: args.cursor } : undefined, - }); - - const fetchedSharedRequests: SharedRequest[] = sharedRequests.map((code) => - this.cast(code), - ); - - return fetchedSharedRequests; - } - - /** - * Delete a SharedRequest - * - * @param sharedRequestID SharedRequest ID - * @param uid User Uid - * @returns Boolean on successful deletion - */ - async revokeSharedRequest(sharedRequestID: string, uid: string) { - try { - const deletedSharedRequest = await this.prisma.shortcode.delete({ - where: { - creator_uid_shortcode_unique: { - creatorUid: uid, - id: sharedRequestID, - }, - }, - }); - - this.pubsub.publish( - `shared_request/${deletedSharedRequest.creatorUid}/revoked`, - this.cast(deletedSharedRequest), - ); - - return E.right(true); - } catch (error) { - return E.left(SHARED_REQUEST_NOT_FOUND); - } - } - - /** - * Update a created SharedRequest - * @param sharedRequestID SharedRequest ID - * @param uid User Uid - * @returns Updated SharedRequest - */ - async updateSharedRequest( - sharedRequestID: string, - uid: string, - updatedProps: string, - ) { - if (!updatedProps) return E.left(SHARED_REQUEST_PROPERTIES_NOT_FOUND); - - const parsedProperties = stringToJson(updatedProps); - if (E.isLeft(parsedProperties) || !parsedProperties.right) - return E.left(SHARED_REQUEST_INVALID_PROPERTIES_JSON); - - try { - const updatedSharedRequest = await this.prisma.shortcode.update({ - where: { - creator_uid_shortcode_unique: { - creatorUid: uid, - id: sharedRequestID, - }, - }, - data: { - properties: parsedProperties.right, - }, - }); - - this.pubsub.publish( - `shared_request/${updatedSharedRequest.creatorUid}/updated`, - this.cast(updatedSharedRequest), - ); - - return E.right(this.cast(updatedSharedRequest)); - } catch (error) { - return E.left(SHARED_REQUEST_NOT_FOUND); - } - } -} diff --git a/packages/hoppscotch-backend/src/shared-request/shared-requests.model.ts b/packages/hoppscotch-backend/src/shared-request/shared-requests.model.ts deleted file mode 100644 index 014934125..000000000 --- a/packages/hoppscotch-backend/src/shared-request/shared-requests.model.ts +++ /dev/null @@ -1,25 +0,0 @@ -import { Field, ID, ObjectType } from '@nestjs/graphql'; - -@ObjectType() -export class SharedRequest { - @Field(() => ID, { - description: 'The 12 digit alphanumeric code', - }) - id: string; - - @Field({ - description: 'JSON string representing the request data', - }) - request: string; - - @Field({ - description: 'JSON string representing the properties for an embed', - nullable: true, - }) - properties: string; - - @Field({ - description: 'Timestamp of when the SharedRequest was created', - }) - createdOn: Date; -} diff --git a/packages/hoppscotch-backend/src/shortcode/shortcode.model.ts b/packages/hoppscotch-backend/src/shortcode/shortcode.model.ts index 79ab01325..249a56cf1 100644 --- a/packages/hoppscotch-backend/src/shortcode/shortcode.model.ts +++ b/packages/hoppscotch-backend/src/shortcode/shortcode.model.ts @@ -3,7 +3,7 @@ import { Field, ID, ObjectType } from '@nestjs/graphql'; @ObjectType() export class Shortcode { @Field(() => ID, { - description: 'The shortcode. 12 digit alphanumeric.', + description: 'The 12 digit alphanumeric code', }) id: string; @@ -13,7 +13,13 @@ export class Shortcode { request: string; @Field({ - description: 'Timestamp of when the Shortcode was created', + description: 'JSON string representing the properties for an embed', + nullable: true, + }) + properties: string; + + @Field({ + description: 'Timestamp of when the SharedRequest was created', }) createdOn: Date; } diff --git a/packages/hoppscotch-backend/src/shortcode/shortcode.resolver.ts b/packages/hoppscotch-backend/src/shortcode/shortcode.resolver.ts index 902355a10..81f7410ce 100644 --- a/packages/hoppscotch-backend/src/shortcode/shortcode.resolver.ts +++ b/packages/hoppscotch-backend/src/shortcode/shortcode.resolver.ts @@ -37,7 +37,6 @@ export class ShortcodeResolver { @Query(() => Shortcode, { description: 'Resolves and returns a shortcode data', nullable: true, - deprecationReason: 'Use SharedRequests instead', }) async shortcode( @Args({ @@ -55,7 +54,6 @@ export class ShortcodeResolver { @Query(() => [Shortcode], { description: 'List all shortcodes the current user has generated', - deprecationReason: 'Use SharedRequests instead', }) @UseGuards(GqlAuthGuard) async myShortcodes(@GqlUser() user: AuthUser, @Args() args: PaginationArgs) { @@ -65,22 +63,54 @@ export class ShortcodeResolver { /* Mutations */ @Mutation(() => Shortcode, { description: 'Create a shortcode for the given request.', - deprecationReason: 'Use SharedRequests instead', }) + @UseGuards(GqlAuthGuard) async createShortcode( + @GqlUser() user: AuthUser, @Args({ name: 'request', description: 'JSON string of the request object', }) request: string, - @Context() ctx: any, + @Args({ + name: 'properties', + description: 'JSON string of the properties of the embed', + nullable: true, + }) + properties: string, ) { - const decodedAccessToken = this.jwtService.verify( - ctx.req.cookies['access_token'], - ); const result = await this.shortcodeService.createShortcode( request, - decodedAccessToken?.sub, + properties, + user, + ); + + if (E.isLeft(result)) throwErr(result.left); + return result.right; + } + + @Mutation(() => Shortcode, { + description: 'Update a user generated Shortcode', + }) + @UseGuards(GqlAuthGuard) + async updateShortcode( + @GqlUser() user: AuthUser, + @Args({ + name: 'code', + type: () => ID, + description: 'The Shortcode to update', + }) + code: string, + @Args({ + name: 'properties', + description: 'JSON string of the properties of the embed', + }) + properties: string, + ) { + const result = await this.shortcodeService.updateShortcode( + code, + user.uid, + properties, ); if (E.isLeft(result)) throwErr(result.left); @@ -89,7 +119,6 @@ export class ShortcodeResolver { @Mutation(() => Boolean, { description: 'Revoke a user generated shortcode', - deprecationReason: 'Use SharedRequests instead', }) @UseGuards(GqlAuthGuard) async revokeShortcode( @@ -111,7 +140,6 @@ export class ShortcodeResolver { @Subscription(() => Shortcode, { description: 'Listen for shortcode creation', resolve: (value) => value, - deprecationReason: 'Use SharedRequests instead', }) @SkipThrottle() @UseGuards(GqlAuthGuard) @@ -119,10 +147,19 @@ export class ShortcodeResolver { return this.pubsub.asyncIterator(`shortcode/${user.uid}/created`); } + @Subscription(() => Shortcode, { + description: 'Listen for Shortcode updates', + resolve: (value) => value, + }) + @SkipThrottle() + @UseGuards(GqlAuthGuard) + myShortcodesUpdated(@GqlUser() user: AuthUser) { + return this.pubsub.asyncIterator(`shortcode/${user.uid}/updated`); + } + @Subscription(() => Shortcode, { description: 'Listen for shortcode deletion', resolve: (value) => value, - deprecationReason: 'Use SharedRequests instead', }) @SkipThrottle() @UseGuards(GqlAuthGuard) diff --git a/packages/hoppscotch-backend/src/shortcode/shortcode.service.spec.ts b/packages/hoppscotch-backend/src/shortcode/shortcode.service.spec.ts index 36dcb36f6..43a097a8e 100644 --- a/packages/hoppscotch-backend/src/shortcode/shortcode.service.spec.ts +++ b/packages/hoppscotch-backend/src/shortcode/shortcode.service.spec.ts @@ -3,7 +3,10 @@ import { PrismaService } from '../prisma/prisma.service'; import { SHORTCODE_ALREADY_EXISTS, SHORTCODE_INVALID_JSON, + SHORTCODE_INVALID_PROPERTIES_JSON, + SHORTCODE_INVALID_REQUEST_JSON, SHORTCODE_NOT_FOUND, + SHORTCODE_PROPERTIES_NOT_FOUND, } from 'src/errors'; import { Shortcode } from './shortcode.model'; import { ShortcodeService } from './shortcode.service'; @@ -22,7 +25,7 @@ const mockFB = { doc: mockDocFunc, }, }; -const mockUserService = new UserService(mockFB as any, mockPubSub as any); +const mockUserService = new UserService(mockPrisma as any, mockPubSub as any); // eslint-disable-next-line @typescript-eslint/ban-ts-comment // @ts-ignore @@ -38,20 +41,34 @@ beforeEach(() => { }); const createdOn = new Date(); -const shortCodeWithOutUser = { - id: '123', - request: '{}', - properties: null, +const user: AuthUser = { + uid: '123344', + email: 'dwight@dundermifflin.com', + displayName: 'Dwight Schrute', + photoURL: 'https://en.wikipedia.org/wiki/Dwight_Schrute', + isAdmin: false, + refreshToken: 'hbfvdkhjbvkdvdfjvbnkhjb', createdOn: createdOn, - creatorUid: null, + currentGQLSession: {}, + currentRESTSession: {}, }; -const shortCodeWithUser = { +const mockEmbed = { + id: '123', + request: '{}', + properties: '{}', + createdOn: createdOn, + creatorUid: user.uid, + updatedOn: createdOn, +}; + +const mockShortcode = { id: '123', request: '{}', properties: null, createdOn: createdOn, - creatorUid: 'user_uid_1', + creatorUid: user.uid, + updatedOn: createdOn, }; const shortcodes = [ @@ -63,8 +80,9 @@ const shortcodes = [ properties: { foo: 'bar', }, - creatorUid: 'testuser', + creatorUid: user.uid, createdOn: new Date(), + updatedOn: createdOn, }, { id: 'blablabla1', @@ -74,25 +92,23 @@ const shortcodes = [ properties: { foo: 'bar', }, - creatorUid: 'testuser', + creatorUid: user.uid, createdOn: new Date(), + updatedOn: createdOn, }, ]; describe('ShortcodeService', () => { describe('getShortCode', () => { - test('should return a valid shortcode with valid shortcode ID', async () => { - mockPrisma.shortcode.findFirstOrThrow.mockResolvedValueOnce( - shortCodeWithOutUser, - ); + test('should return a valid Shortcode with valid Shortcode ID', async () => { + mockPrisma.shortcode.findFirstOrThrow.mockResolvedValueOnce(mockEmbed); - const result = await shortcodeService.getShortCode( - shortCodeWithOutUser.id, - ); + const result = await shortcodeService.getShortCode(mockEmbed.id); expect(result).toEqualRight({ - id: shortCodeWithOutUser.id, - createdOn: shortCodeWithOutUser.createdOn, - request: JSON.stringify(shortCodeWithOutUser.request), + id: mockEmbed.id, + createdOn: mockEmbed.createdOn, + request: JSON.stringify(mockEmbed.request), + properties: JSON.stringify(mockEmbed.properties), }); }); @@ -107,10 +123,10 @@ describe('ShortcodeService', () => { }); describe('fetchUserShortCodes', () => { - test('should return list of shortcodes with valid inputs and no cursor', async () => { + test('should return list of Shortcode with valid inputs and no cursor', async () => { mockPrisma.shortcode.findMany.mockResolvedValueOnce(shortcodes); - const result = await shortcodeService.fetchUserShortCodes('testuser', { + const result = await shortcodeService.fetchUserShortCodes(user.uid, { cursor: null, take: 10, }); @@ -118,20 +134,22 @@ describe('ShortcodeService', () => { { id: shortcodes[0].id, request: JSON.stringify(shortcodes[0].request), + properties: JSON.stringify(shortcodes[0].properties), createdOn: shortcodes[0].createdOn, }, { id: shortcodes[1].id, request: JSON.stringify(shortcodes[1].request), + properties: JSON.stringify(shortcodes[1].properties), createdOn: shortcodes[1].createdOn, }, ]); }); - test('should return list of shortcodes with valid inputs and cursor', async () => { + test('should return list of Shortcode with valid inputs and cursor', async () => { mockPrisma.shortcode.findMany.mockResolvedValue([shortcodes[1]]); - const result = await shortcodeService.fetchUserShortCodes('testuser', { + const result = await shortcodeService.fetchUserShortCodes(user.uid, { cursor: 'blablabla', take: 10, }); @@ -139,6 +157,7 @@ describe('ShortcodeService', () => { { id: shortcodes[1].id, request: JSON.stringify(shortcodes[1].request), + properties: JSON.stringify(shortcodes[1].properties), createdOn: shortcodes[1].createdOn, }, ]); @@ -147,7 +166,7 @@ describe('ShortcodeService', () => { test('should return an empty array for an invalid cursor', async () => { mockPrisma.shortcode.findMany.mockResolvedValue([]); - const result = await shortcodeService.fetchUserShortCodes('testuser', { + const result = await shortcodeService.fetchUserShortCodes(user.uid, { cursor: 'invalidcursor', take: 10, }); @@ -179,77 +198,111 @@ describe('ShortcodeService', () => { }); describe('createShortcode', () => { - test('should throw SHORTCODE_INVALID_JSON error if incoming request data is invalid', async () => { + test('should throw SHORTCODE_INVALID_REQUEST_JSON error if incoming request data is invalid', async () => { const result = await shortcodeService.createShortcode( 'invalidRequest', - 'user_uid_1', + null, + user, ); - expect(result).toEqualLeft(SHORTCODE_INVALID_JSON); + expect(result).toEqualLeft(SHORTCODE_INVALID_REQUEST_JSON); }); - test('should successfully create a new shortcode with valid user uid', async () => { - // generateUniqueShortCodeID --> getShortCode + test('should throw SHORTCODE_INVALID_PROPERTIES_JSON error if incoming properties data is invalid', async () => { + const result = await shortcodeService.createShortcode( + '{}', + 'invalid_data', + user, + ); + expect(result).toEqualLeft(SHORTCODE_INVALID_PROPERTIES_JSON); + }); + + test('should successfully create a new Embed with valid user uid', async () => { + // generateUniqueShortCodeID --> getShortcode mockPrisma.shortcode.findFirstOrThrow.mockRejectedValueOnce( 'NotFoundError', ); - mockPrisma.shortcode.create.mockResolvedValueOnce(shortCodeWithUser); + mockPrisma.shortcode.create.mockResolvedValueOnce(mockEmbed); - const result = await shortcodeService.createShortcode('{}', 'user_uid_1'); - expect(result).toEqualRight({ - id: shortCodeWithUser.id, - createdOn: shortCodeWithUser.createdOn, - request: JSON.stringify(shortCodeWithUser.request), + const result = await shortcodeService.createShortcode('{}', '{}', user); + expect(result).toEqualRight({ + id: mockEmbed.id, + createdOn: mockEmbed.createdOn, + request: JSON.stringify(mockEmbed.request), + properties: JSON.stringify(mockEmbed.properties), }); }); - test('should successfully create a new shortcode with null user uid', async () => { - // generateUniqueShortCodeID --> getShortCode + test('should successfully create a new ShortCode with valid user uid', async () => { + // generateUniqueShortCodeID --> getShortcode mockPrisma.shortcode.findFirstOrThrow.mockRejectedValueOnce( 'NotFoundError', ); - mockPrisma.shortcode.create.mockResolvedValueOnce(shortCodeWithUser); + mockPrisma.shortcode.create.mockResolvedValueOnce(mockShortcode); - const result = await shortcodeService.createShortcode('{}', null); - expect(result).toEqualRight({ - id: shortCodeWithUser.id, - createdOn: shortCodeWithUser.createdOn, - request: JSON.stringify(shortCodeWithOutUser.request), + const result = await shortcodeService.createShortcode('{}', null, user); + expect(result).toEqualRight({ + id: mockShortcode.id, + createdOn: mockShortcode.createdOn, + request: JSON.stringify(mockShortcode.request), + properties: mockShortcode.properties, }); }); - test('should send pubsub message to `shortcode/{uid}/created` on successful creation of shortcode', async () => { - // generateUniqueShortCodeID --> getShortCode + test('should send pubsub message to `shortcode/{uid}/created` on successful creation of a Shortcode', async () => { + // generateUniqueShortCodeID --> getShortcode mockPrisma.shortcode.findFirstOrThrow.mockRejectedValueOnce( 'NotFoundError', ); - mockPrisma.shortcode.create.mockResolvedValueOnce(shortCodeWithUser); + mockPrisma.shortcode.create.mockResolvedValueOnce(mockShortcode); + + const result = await shortcodeService.createShortcode('{}', null, user); - const result = await shortcodeService.createShortcode('{}', 'user_uid_1'); expect(mockPubSub.publish).toHaveBeenCalledWith( - `shortcode/${shortCodeWithUser.creatorUid}/created`, - { - id: shortCodeWithUser.id, - createdOn: shortCodeWithUser.createdOn, - request: JSON.stringify(shortCodeWithUser.request), + `shortcode/${mockShortcode.creatorUid}/created`, + { + id: mockShortcode.id, + createdOn: mockShortcode.createdOn, + request: JSON.stringify(mockShortcode.request), + properties: mockShortcode.properties, + }, + ); + }); + + test('should send pubsub message to `shortcode/{uid}/created` on successful creation of an Embed', async () => { + // generateUniqueShortCodeID --> getShortcode + mockPrisma.shortcode.findFirstOrThrow.mockRejectedValueOnce( + 'NotFoundError', + ); + mockPrisma.shortcode.create.mockResolvedValueOnce(mockEmbed); + + const result = await shortcodeService.createShortcode('{}', '{}', user); + + expect(mockPubSub.publish).toHaveBeenCalledWith( + `shortcode/${mockEmbed.creatorUid}/created`, + { + id: mockEmbed.id, + createdOn: mockEmbed.createdOn, + request: JSON.stringify(mockEmbed.request), + properties: JSON.stringify(mockEmbed.properties), }, ); }); }); describe('revokeShortCode', () => { - test('should return true on successful deletion of shortcode with valid inputs', async () => { - mockPrisma.shortcode.delete.mockResolvedValueOnce(shortCodeWithUser); + test('should return true on successful deletion of Shortcode with valid inputs', async () => { + mockPrisma.shortcode.delete.mockResolvedValueOnce(mockEmbed); const result = await shortcodeService.revokeShortCode( - shortCodeWithUser.id, - shortCodeWithUser.creatorUid, + mockEmbed.id, + mockEmbed.creatorUid, ); expect(mockPrisma.shortcode.delete).toHaveBeenCalledWith({ where: { creator_uid_shortcode_unique: { - creatorUid: shortCodeWithUser.creatorUid, - id: shortCodeWithUser.id, + creatorUid: mockEmbed.creatorUid, + id: mockEmbed.id, }, }, }); @@ -257,52 +310,53 @@ describe('ShortcodeService', () => { expect(result).toEqualRight(true); }); - test('should return SHORTCODE_NOT_FOUND error when shortcode is invalid and user uid is valid', async () => { + test('should return SHORTCODE_NOT_FOUND error when Shortcode is invalid and user uid is valid', async () => { mockPrisma.shortcode.delete.mockRejectedValue('RecordNotFound'); expect( shortcodeService.revokeShortCode('invalid', 'testuser'), ).resolves.toEqualLeft(SHORTCODE_NOT_FOUND); }); - test('should return SHORTCODE_NOT_FOUND error when shortcode is valid and user uid is invalid', async () => { + test('should return SHORTCODE_NOT_FOUND error when Shortcode is valid and user uid is invalid', async () => { mockPrisma.shortcode.delete.mockRejectedValue('RecordNotFound'); expect( shortcodeService.revokeShortCode('blablablabla', 'invalidUser'), ).resolves.toEqualLeft(SHORTCODE_NOT_FOUND); }); - test('should return SHORTCODE_NOT_FOUND error when both shortcode and user uid are invalid', async () => { + test('should return SHORTCODE_NOT_FOUND error when both Shortcode and user uid are invalid', async () => { mockPrisma.shortcode.delete.mockRejectedValue('RecordNotFound'); expect( shortcodeService.revokeShortCode('invalid', 'invalid'), ).resolves.toEqualLeft(SHORTCODE_NOT_FOUND); }); - test('should send pubsub message to `shortcode/{uid}/revoked` on successful deletion of shortcode', async () => { - mockPrisma.shortcode.delete.mockResolvedValueOnce(shortCodeWithUser); + test('should send pubsub message to `shortcode/{uid}/revoked` on successful deletion of Shortcode', async () => { + mockPrisma.shortcode.delete.mockResolvedValueOnce(mockEmbed); const result = await shortcodeService.revokeShortCode( - shortCodeWithUser.id, - shortCodeWithUser.creatorUid, + mockEmbed.id, + mockEmbed.creatorUid, ); expect(mockPubSub.publish).toHaveBeenCalledWith( - `shortcode/${shortCodeWithUser.creatorUid}/revoked`, + `shortcode/${mockEmbed.creatorUid}/revoked`, { - id: shortCodeWithUser.id, - createdOn: shortCodeWithUser.createdOn, - request: JSON.stringify(shortCodeWithUser.request), + id: mockEmbed.id, + createdOn: mockEmbed.createdOn, + request: JSON.stringify(mockEmbed.request), + properties: JSON.stringify(mockEmbed.properties), }, ); }); }); describe('deleteUserShortCodes', () => { - test('should successfully delete all users shortcodes with valid user uid', async () => { + test('should successfully delete all users Shortcodes with valid user uid', async () => { mockPrisma.shortcode.deleteMany.mockResolvedValueOnce({ count: 1 }); const result = await shortcodeService.deleteUserShortCodes( - shortCodeWithUser.creatorUid, + mockEmbed.creatorUid, ); expect(result).toEqual(1); }); @@ -311,9 +365,81 @@ describe('ShortcodeService', () => { mockPrisma.shortcode.deleteMany.mockResolvedValueOnce({ count: 0 }); const result = await shortcodeService.deleteUserShortCodes( - shortCodeWithUser.creatorUid, + mockEmbed.creatorUid, ); expect(result).toEqual(0); }); }); + + describe('updateShortcode', () => { + test('should return SHORTCODE_PROPERTIES_NOT_FOUND error when updatedProps in invalid', async () => { + const result = await shortcodeService.updateShortcode( + mockEmbed.id, + user.uid, + '', + ); + expect(result).toEqualLeft(SHORTCODE_PROPERTIES_NOT_FOUND); + }); + + test('should return SHORTCODE_PROPERTIES_NOT_FOUND error when updatedProps in invalid JSON format', async () => { + const result = await shortcodeService.updateShortcode( + mockEmbed.id, + user.uid, + '{kk', + ); + expect(result).toEqualLeft(SHORTCODE_INVALID_PROPERTIES_JSON); + }); + + test('should return SHORTCODE_NOT_FOUND error when Shortcode ID is invalid', async () => { + mockPrisma.shortcode.update.mockRejectedValue('RecordNotFound'); + const result = await shortcodeService.updateShortcode( + 'invalidID', + user.uid, + '{}', + ); + expect(result).toEqualLeft(SHORTCODE_NOT_FOUND); + }); + + test('should successfully update a Shortcodes with valid inputs', async () => { + mockPrisma.shortcode.update.mockResolvedValueOnce({ + ...mockEmbed, + properties: '{"foo":"bar"}', + }); + + const result = await shortcodeService.updateShortcode( + mockEmbed.id, + user.uid, + '{"foo":"bar"}', + ); + expect(result).toEqualRight({ + id: mockEmbed.id, + createdOn: mockEmbed.createdOn, + request: JSON.stringify(mockEmbed.request), + properties: JSON.stringify('{"foo":"bar"}'), + }); + }); + + test('should send pubsub message to `shortcode/{uid}/updated` on successful Update of Shortcode', async () => { + mockPrisma.shortcode.update.mockResolvedValueOnce({ + ...mockEmbed, + properties: '{"foo":"bar"}', + }); + + const result = await shortcodeService.updateShortcode( + mockEmbed.id, + user.uid, + '{"foo":"bar"}', + ); + + expect(mockPubSub.publish).toHaveBeenCalledWith( + `shortcode/${mockEmbed.creatorUid}/updated`, + { + id: mockEmbed.id, + createdOn: mockEmbed.createdOn, + request: JSON.stringify(mockEmbed.request), + properties: JSON.stringify('{"foo":"bar"}'), + }, + ); + }); + }); }); diff --git a/packages/hoppscotch-backend/src/shortcode/shortcode.service.ts b/packages/hoppscotch-backend/src/shortcode/shortcode.service.ts index 87fb5405a..fcf153494 100644 --- a/packages/hoppscotch-backend/src/shortcode/shortcode.service.ts +++ b/packages/hoppscotch-backend/src/shortcode/shortcode.service.ts @@ -4,7 +4,13 @@ import * as O from 'fp-ts/Option'; import * as TO from 'fp-ts/TaskOption'; import * as E from 'fp-ts/Either'; import { PrismaService } from 'src/prisma/prisma.service'; -import { SHORTCODE_INVALID_JSON, SHORTCODE_NOT_FOUND } from 'src/errors'; +import { + SHORTCODE_INVALID_JSON, + SHORTCODE_INVALID_PROPERTIES_JSON, + SHORTCODE_INVALID_REQUEST_JSON, + SHORTCODE_NOT_FOUND, + SHORTCODE_PROPERTIES_NOT_FOUND, +} from 'src/errors'; import { UserDataHandler } from 'src/user/user.data.handler'; import { Shortcode } from './shortcode.model'; import { Shortcode as DBShortCode } from '@prisma/client'; @@ -46,10 +52,14 @@ export class ShortcodeService implements UserDataHandler, OnModuleInit { * @param shortcodeInfo Prisma Shortcode type * @returns GQL Shortcode */ - private returnShortCode(shortcodeInfo: DBShortCode): Shortcode { + private cast(shortcodeInfo: DBShortCode): Shortcode { return { id: shortcodeInfo.id, request: JSON.stringify(shortcodeInfo.request), + properties: + shortcodeInfo.properties != null + ? JSON.stringify(shortcodeInfo.properties) + : null, createdOn: shortcodeInfo.createdOn, }; } @@ -94,7 +104,7 @@ export class ShortcodeService implements UserDataHandler, OnModuleInit { const shortcodeInfo = await this.prisma.shortcode.findFirstOrThrow({ where: { id: shortcode }, }); - return E.right(this.returnShortCode(shortcodeInfo)); + return E.right(this.cast(shortcodeInfo)); } catch (error) { return E.left(SHORTCODE_NOT_FOUND); } @@ -104,14 +114,21 @@ export class ShortcodeService implements UserDataHandler, OnModuleInit { * Create a new ShortCode * * @param request JSON string of request details - * @param userUID user UID, if present + * @param userInfo user UI + * @param properties JSON string of embed properties, if present * @returns Either of ShortCode or error */ - async createShortcode(request: string, userUID: string | null) { - const shortcodeData = stringToJson(request); - if (E.isLeft(shortcodeData)) return E.left(SHORTCODE_INVALID_JSON); + async createShortcode( + request: string, + properties: string | null = null, + userInfo: AuthUser, + ) { + const requestData = stringToJson(request); + if (E.isLeft(requestData)) return E.left(SHORTCODE_INVALID_REQUEST_JSON); - const user = await this.userService.findUserById(userUID); + const parsedProperties = stringToJson(properties); + if (E.isLeft(parsedProperties)) + return E.left(SHORTCODE_INVALID_PROPERTIES_JSON); const generatedShortCode = await this.generateUniqueShortCodeID(); if (E.isLeft(generatedShortCode)) return E.left(generatedShortCode.left); @@ -119,8 +136,9 @@ export class ShortcodeService implements UserDataHandler, OnModuleInit { const createdShortCode = await this.prisma.shortcode.create({ data: { id: generatedShortCode.right, - request: shortcodeData.right, - creatorUid: O.isNone(user) ? null : user.value.uid, + request: requestData.right, + properties: parsedProperties.right ?? undefined, + creatorUid: userInfo.uid, }, }); @@ -128,11 +146,11 @@ export class ShortcodeService implements UserDataHandler, OnModuleInit { if (createdShortCode.creatorUid) { this.pubsub.publish( `shortcode/${createdShortCode.creatorUid}/created`, - this.returnShortCode(createdShortCode), + this.cast(createdShortCode), ); } - return E.right(this.returnShortCode(createdShortCode)); + return E.right(this.cast(createdShortCode)); } /** @@ -156,7 +174,7 @@ export class ShortcodeService implements UserDataHandler, OnModuleInit { }); const fetchedShortCodes: Shortcode[] = shortCodes.map((code) => - this.returnShortCode(code), + this.cast(code), ); return fetchedShortCodes; @@ -182,7 +200,7 @@ export class ShortcodeService implements UserDataHandler, OnModuleInit { this.pubsub.publish( `shortcode/${deletedShortCodes.creatorUid}/revoked`, - this.returnShortCode(deletedShortCodes), + this.cast(deletedShortCodes), ); return E.right(true); @@ -205,4 +223,45 @@ export class ShortcodeService implements UserDataHandler, OnModuleInit { return deletedShortCodes.count; } + + /** + * Update a created Shortcode + * @param shortcodeID Shortcode ID + * @param uid User Uid + * @returns Updated Shortcode + */ + async updateShortcode( + shortcodeID: string, + uid: string, + updatedProps: string, + ) { + if (!updatedProps) return E.left(SHORTCODE_PROPERTIES_NOT_FOUND); + + const parsedProperties = stringToJson(updatedProps); + if (E.isLeft(parsedProperties) || !parsedProperties.right) + return E.left(SHORTCODE_INVALID_PROPERTIES_JSON); + + try { + const updatedShortcode = await this.prisma.shortcode.update({ + where: { + creator_uid_shortcode_unique: { + creatorUid: uid, + id: shortcodeID, + }, + }, + data: { + properties: parsedProperties.right, + }, + }); + + this.pubsub.publish( + `shortcode/${updatedShortcode.creatorUid}/updated`, + this.cast(updatedShortcode), + ); + + return E.right(this.cast(updatedShortcode)); + } catch (error) { + return E.left(SHORTCODE_NOT_FOUND); + } + } }