Browse Source

feat: introduction of shared-requests (#3476)

* feat: added new property to existing shortcode model in prisma schema

* chore: created shared-requests module

* chore: created shared-request model

* chore: complete sharedRequest query

* chore: completed mutation to create a SharedRequest

* chore: completed subscription to create a SharedRequest

* chore: completed query to fetch all user created shared-requests

* chore: completed mutation to delete a SharedRequest

* chore: completed subscription to delete a SharedRequest

* chore: removed unused dependncues in share-requests module

* chore: added shared-requests into user deletion spec

* test: added all testcases for shared-request module

* test: modified all relevant tests in shortcode module

* chore: added deprecated label to all queries,mutations and subscriptions in the shortcode module

* chore: resolved all comments raised in review

* feat: added ability to update and listen to updates of shared-requests

* chore: added updatedOn field to shortcode model

* chore: fixed issue with updateSharedRequest method

* chore: fixed incorrect value getting updated

* chore: added all test-cases for updateSharedRequest method

* chore: created migration for shared-requests

* chore: moved shared-requests into shortcode module

* chore: added missing import in shortcode tests

* chore: changed properties to embedProperties in shortcode model

* feat: generated migrations file for new schema changes to Shortcodes table

* chore: changed target of old-backend service in docker-compose file

* chore: fixed issue with updatedOn field in shortcodes model

* chore: removed unused dependencies

* fix: handle invalid input for shortcode properties

* Revert "fix: handle invalid input for shortcode properties"

This reverts commit 4dcb0afb184749068a11afbbb0087570d01df5d5.

* chore: changed updateShortcode method name to updateEmbedProperties

* chore: changed target of hoppscotch-old-backend service to prod

---------

Co-authored-by: Mir Arif Hasan <arif.ishan05@gmail.com>
Balu Babu 1 year ago
parent
commit
4caf0053cd

+ 15 - 0
packages/hoppscotch-backend/prisma/migrations/20231106120154_embeds_addition/migration.sql

@@ -0,0 +1,15 @@
+/*
+  Warnings:
+
+  - A unique constraint covering the columns `[id]` on the table `Shortcode` will be added. If there are existing duplicate values, this will fail.
+
+*/
+-- AlterTable
+ALTER TABLE "Shortcode" ADD COLUMN     "embedProperties" JSONB,
+ADD COLUMN     "updatedOn" TIMESTAMP(3) NOT NULL DEFAULT CURRENT_TIMESTAMP;
+
+-- CreateIndex
+CREATE UNIQUE INDEX "Shortcode_id_key" ON "Shortcode"("id");
+
+-- AddForeignKey
+ALTER TABLE "Shortcode" ADD CONSTRAINT "Shortcode_creatorUid_fkey" FOREIGN KEY ("creatorUid") REFERENCES "User"("uid") ON DELETE SET NULL ON UPDATE CASCADE;

+ 8 - 5
packages/hoppscotch-backend/prisma/schema.prisma

@@ -68,11 +68,13 @@ model TeamRequest {
 }
 
 model Shortcode {
-  id         String   @id
-  request    Json
-  creatorUid String?
-  createdOn  DateTime @default(now())
-
+  id              String   @id @unique
+  request         Json
+  embedProperties Json?
+  creatorUid      String?
+  User            User?    @relation(fields: [creatorUid], references: [uid])
+  createdOn       DateTime @default(now())
+  updatedOn       DateTime @updatedAt @default(now())
   @@unique(fields: [id, creatorUid], name: "creator_uid_shortcode_unique")
 }
 
@@ -102,6 +104,7 @@ model User {
   currentGQLSession  Json?
   createdOn          DateTime            @default(now()) @db.Timestamp(3)
   invitedUsers       InvitedUsers[]
+  shortcodes         Shortcode[]
 }
 
 model Account {

+ 21 - 12
packages/hoppscotch-backend/src/errors.ts

@@ -318,18 +318,6 @@ export const TEAM_INVITATION_NOT_FOUND =
  */
 export const SHORTCODE_NOT_FOUND = 'shortcode/not_found' as const;
 
-/**
- * Invalid ShortCode format
- * (ShortcodeService)
- */
-export const SHORTCODE_INVALID_JSON = 'shortcode/invalid_json' as const;
-
-/**
- * ShortCode already exists in DB
- * (ShortcodeService)
- */
-export const SHORTCODE_ALREADY_EXISTS = 'shortcode/already_exists' as const;
-
 /**
  * Invalid or non-existent TEAM ENVIRONMENT ID
  * (TeamEnvironmentsService)
@@ -621,3 +609,24 @@ 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 invalid request JSON format
+ * (ShortcodeService)
+ */
+export const SHORTCODE_INVALID_REQUEST_JSON =
+  'shortcode/request_invalid_format' as const;
+
+/**
+ * SharedRequest invalid properties JSON format
+ * (ShortcodeService)
+ */
+export const SHORTCODE_INVALID_PROPERTIES_JSON =
+  'shortcode/properties_invalid_format' as const;
+
+/**
+ * SharedRequest invalid properties not found
+ * (ShortcodeService)
+ */
+export const SHORTCODE_PROPERTIES_NOT_FOUND =
+  'shortcode/properties_not_found' as const;

+ 3 - 1
packages/hoppscotch-backend/src/pubsub/topicsDefs.ts

@@ -69,5 +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: `shortcode/${string}/${'created' | 'revoked' | 'updated'}`
+  ]: Shortcode;
 };

+ 7 - 1
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;
 
@@ -12,6 +12,12 @@ export class Shortcode {
   })
   request: string;
 
+  @Field({
+    description: 'JSON string representing the properties for an embed',
+    nullable: true,
+  })
+  properties: string;
+
   @Field({
     description: 'Timestamp of when the Shortcode was created',
   })

+ 1 - 9
packages/hoppscotch-backend/src/shortcode/shortcode.module.ts

@@ -1,5 +1,4 @@
 import { Module } from '@nestjs/common';
-import { JwtModule } from '@nestjs/jwt';
 import { PrismaModule } from 'src/prisma/prisma.module';
 import { PubSubModule } from 'src/pubsub/pubsub.module';
 import { UserModule } from 'src/user/user.module';
@@ -7,14 +6,7 @@ import { ShortcodeResolver } from './shortcode.resolver';
 import { ShortcodeService } from './shortcode.service';
 
 @Module({
-  imports: [
-    PrismaModule,
-    UserModule,
-    PubSubModule,
-    JwtModule.register({
-      secret: process.env.JWT_SECRET,
-    }),
-  ],
+  imports: [PrismaModule, UserModule, PubSubModule],
   providers: [ShortcodeService, ShortcodeResolver],
   exports: [ShortcodeService],
 })

+ 48 - 10
packages/hoppscotch-backend/src/shortcode/shortcode.resolver.ts

@@ -1,6 +1,5 @@
 import {
   Args,
-  Context,
   ID,
   Mutation,
   Query,
@@ -11,14 +10,12 @@ import * as E from 'fp-ts/Either';
 import { UseGuards } from '@nestjs/common';
 import { Shortcode } from './shortcode.model';
 import { ShortcodeService } from './shortcode.service';
-import { UserService } from 'src/user/user.service';
 import { throwErr } from 'src/utils';
 import { GqlUser } from 'src/decorators/gql-user.decorator';
 import { GqlAuthGuard } from 'src/guards/gql-auth.guard';
 import { User } from 'src/user/user.model';
 import { PubSubService } from 'src/pubsub/pubsub.service';
 import { AuthUser } from '../types/AuthUser';
-import { JwtService } from '@nestjs/jwt';
 import { PaginationArgs } from 'src/types/input-types.args';
 import { GqlThrottlerGuard } from 'src/guards/gql-throttler.guard';
 import { SkipThrottle } from '@nestjs/throttler';
@@ -28,9 +25,7 @@ import { SkipThrottle } from '@nestjs/throttler';
 export class ShortcodeResolver {
   constructor(
     private readonly shortcodeService: ShortcodeService,
-    private readonly userService: UserService,
     private readonly pubsub: PubSubService,
-    private jwtService: JwtService,
   ) {}
 
   /* Queries */
@@ -64,20 +59,53 @@ export class ShortcodeResolver {
   @Mutation(() => Shortcode, {
     description: 'Create a shortcode for the given request.',
   })
+  @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 updateEmbedProperties(
+    @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.updateEmbedProperties(
+      code,
+      user.uid,
+      properties,
     );
 
     if (E.isLeft(result)) throwErr(result.left);
@@ -114,6 +142,16 @@ 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,

+ 205 - 72
packages/hoppscotch-backend/src/shortcode/shortcode.service.spec.ts

@@ -1,13 +1,15 @@
 import { mockDeep, mockReset } from 'jest-mock-extended';
 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';
 import { UserService } from 'src/user/user.service';
+import { AuthUser } from 'src/types/AuthUser';
 
 const mockPrisma = mockDeep<PrismaService>();
 
@@ -22,7 +24,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,18 +40,34 @@ beforeEach(() => {
 });
 const createdOn = new Date();
 
-const shortCodeWithOutUser = {
+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: '{}',
+  embedProperties: '{}',
   createdOn: createdOn,
-  creatorUid: null,
+  creatorUid: user.uid,
+  updatedOn: createdOn,
 };
 
-const shortCodeWithUser = {
+const mockShortcode = {
   id: '123',
   request: '{}',
+  embedProperties: null,
   createdOn: createdOn,
-  creatorUid: 'user_uid_1',
+  creatorUid: user.uid,
+  updatedOn: createdOn,
 };
 
 const shortcodes = [
@@ -58,33 +76,38 @@ const shortcodes = [
     request: {
       hello: 'there',
     },
-    creatorUid: 'testuser',
+    embedProperties: {
+      foo: 'bar',
+    },
+    creatorUid: user.uid,
     createdOn: new Date(),
+    updatedOn: createdOn,
   },
   {
     id: 'blablabla1',
     request: {
       hello: 'there',
     },
-    creatorUid: 'testuser',
+    embedProperties: {
+      foo: 'bar',
+    },
+    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(<Shortcode>{
-        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.embedProperties),
       });
     });
 
@@ -99,10 +122,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,
       });
@@ -110,20 +133,22 @@ describe('ShortcodeService', () => {
         {
           id: shortcodes[0].id,
           request: JSON.stringify(shortcodes[0].request),
+          properties: JSON.stringify(shortcodes[0].embedProperties),
           createdOn: shortcodes[0].createdOn,
         },
         {
           id: shortcodes[1].id,
           request: JSON.stringify(shortcodes[1].request),
+          properties: JSON.stringify(shortcodes[1].embedProperties),
           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,
       });
@@ -131,6 +156,7 @@ describe('ShortcodeService', () => {
         {
           id: shortcodes[1].id,
           request: JSON.stringify(shortcodes[1].request),
+          properties: JSON.stringify(shortcodes[1].embedProperties),
           createdOn: shortcodes[1].createdOn,
         },
       ]);
@@ -139,7 +165,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,
       });
@@ -171,77 +197,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(<Shortcode>{
+        id: mockEmbed.id,
+        createdOn: mockEmbed.createdOn,
+        request: JSON.stringify(mockEmbed.request),
+        properties: JSON.stringify(mockEmbed.embedProperties),
       });
     });
 
-    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(<Shortcode>{
+        id: mockShortcode.id,
+        createdOn: mockShortcode.createdOn,
+        request: JSON.stringify(mockShortcode.request),
+        properties: mockShortcode.embedProperties,
       });
     });
 
-    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`,
+        <Shortcode>{
+          id: mockShortcode.id,
+          createdOn: mockShortcode.createdOn,
+          request: JSON.stringify(mockShortcode.request),
+          properties: mockShortcode.embedProperties,
+        },
+      );
+    });
+
+    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`,
+        <Shortcode>{
+          id: mockEmbed.id,
+          createdOn: mockEmbed.createdOn,
+          request: JSON.stringify(mockEmbed.request),
+          properties: JSON.stringify(mockEmbed.embedProperties),
         },
       );
     });
   });
 
   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,
           },
         },
       });
@@ -249,52 +309,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.embedProperties),
         },
       );
     });
   });
 
   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);
     });
@@ -303,9 +364,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.updateEmbedProperties(
+        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.updateEmbedProperties(
+        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.updateEmbedProperties(
+        'invalidID',
+        user.uid,
+        '{}',
+      );
+      expect(result).toEqualLeft(SHORTCODE_NOT_FOUND);
+    });
+
+    test('should successfully update a Shortcodes with valid inputs', async () => {
+      mockPrisma.shortcode.update.mockResolvedValueOnce({
+        ...mockEmbed,
+        embedProperties: '{"foo":"bar"}',
+      });
+
+      const result = await shortcodeService.updateEmbedProperties(
+        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,
+        embedProperties: '{"foo":"bar"}',
+      });
+
+      const result = await shortcodeService.updateEmbedProperties(
+        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"}'),
+        },
+      );
+    });
+  });
 });

+ 73 - 15
packages/hoppscotch-backend/src/shortcode/shortcode.service.ts

@@ -1,10 +1,14 @@
 import { Injectable, OnModuleInit } from '@nestjs/common';
 import * as T from 'fp-ts/Task';
-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_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 +50,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 <Shortcode>{
       id: shortcodeInfo.id,
       request: JSON.stringify(shortcodeInfo.request),
+      properties:
+        shortcodeInfo.embedProperties != null
+          ? JSON.stringify(shortcodeInfo.embedProperties)
+          : null,
       createdOn: shortcodeInfo.createdOn,
     };
   }
@@ -94,7 +102,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 +112,22 @@ 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) || !requestData.right)
+      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 +135,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,
+        embedProperties: parsedProperties.right ?? undefined,
+        creatorUid: userInfo.uid,
       },
     });
 
@@ -128,11 +145,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 +173,7 @@ export class ShortcodeService implements UserDataHandler, OnModuleInit {
     });
 
     const fetchedShortCodes: Shortcode[] = shortCodes.map((code) =>
-      this.returnShortCode(code),
+      this.cast(code),
     );
 
     return fetchedShortCodes;
@@ -182,7 +199,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 +222,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 updateEmbedProperties(
+    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: {
+          embedProperties: 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);
+    }
+  }
 }