From e30a6c9db5a6d683bc8ee0eeccfe5e6108d9f18f Mon Sep 17 00:00:00 2001 From: Mir Arif Hasan Date: Mon, 16 Dec 2024 14:33:09 +0600 Subject: [PATCH] feat(backend): add the ability to disable tracking request history (#4594) HSB-505 --- .../src/admin/admin.module.ts | 2 + .../src/admin/admin.resolver.ts | 15 ++++- .../src/admin/admin.service.spec.ts | 14 +++++ .../src/admin/admin.service.ts | 13 ++++ .../src/admin/infra.resolver.ts | 31 +++++++--- packages/hoppscotch-backend/src/errors.ts | 14 ++++- .../src/infra-config/helper.ts | 6 ++ .../src/infra-config/infra-config.module.ts | 3 +- .../src/infra-config/infra-config.resolver.ts | 43 ++++++++++++- .../infra-config/infra-config.service.spec.ts | 60 +++++++++++++++++++ .../src/infra-config/infra-config.service.ts | 32 ++++++++++ .../src/pubsub/topicsDefs.ts | 9 +-- .../src/types/InfraConfig.ts | 2 + .../user-history-feature-flag.guard.ts | 21 +++++++ .../src/user-history/user-history.module.ts | 3 +- .../src/user-history/user-history.resolver.ts | 13 +++- .../user-history/user-history.service.spec.ts | 24 ++++++++ .../src/user-history/user-history.service.ts | 16 +++++ 18 files changed, 301 insertions(+), 20 deletions(-) create mode 100644 packages/hoppscotch-backend/src/user-history/user-history-feature-flag.guard.ts diff --git a/packages/hoppscotch-backend/src/admin/admin.module.ts b/packages/hoppscotch-backend/src/admin/admin.module.ts index 9b4afd53..aa5c0d15 100644 --- a/packages/hoppscotch-backend/src/admin/admin.module.ts +++ b/packages/hoppscotch-backend/src/admin/admin.module.ts @@ -12,6 +12,7 @@ import { TeamRequestModule } from '../team-request/team-request.module'; import { InfraResolver } from './infra.resolver'; import { ShortcodeModule } from 'src/shortcode/shortcode.module'; import { InfraConfigModule } from 'src/infra-config/infra-config.module'; +import { UserHistoryModule } from 'src/user-history/user-history.module'; @Module({ imports: [ @@ -25,6 +26,7 @@ import { InfraConfigModule } from 'src/infra-config/infra-config.module'; TeamRequestModule, ShortcodeModule, InfraConfigModule, + UserHistoryModule, ], providers: [InfraResolver, AdminResolver, AdminService], exports: [AdminService], diff --git a/packages/hoppscotch-backend/src/admin/admin.resolver.ts b/packages/hoppscotch-backend/src/admin/admin.resolver.ts index bfc7af88..35e02718 100644 --- a/packages/hoppscotch-backend/src/admin/admin.resolver.ts +++ b/packages/hoppscotch-backend/src/admin/admin.resolver.ts @@ -117,9 +117,8 @@ export class AdminResolver { }) userUIDs: string[], ): Promise { - const deletionResults = await this.adminService.removeUserAccounts( - userUIDs, - ); + const deletionResults = + await this.adminService.removeUserAccounts(userUIDs); if (E.isLeft(deletionResults)) throwErr(deletionResults.left); return deletionResults.right; } @@ -360,6 +359,16 @@ export class AdminResolver { return true; } + @Mutation(() => Boolean, { + description: 'Revoke all User History', + }) + @UseGuards(GqlAuthGuard, GqlAdminGuard) + async revokeAllUserHistoryByAdmin(): Promise { + const isDeleted = await this.adminService.deleteAllUserHistory(); + if (E.isLeft(isDeleted)) throwErr(isDeleted.left); + return true; + } + /* Subscriptions */ @Subscription(() => InvitedUser, { diff --git a/packages/hoppscotch-backend/src/admin/admin.service.spec.ts b/packages/hoppscotch-backend/src/admin/admin.service.spec.ts index a3bcf3c1..a4370d44 100644 --- a/packages/hoppscotch-backend/src/admin/admin.service.spec.ts +++ b/packages/hoppscotch-backend/src/admin/admin.service.spec.ts @@ -22,6 +22,7 @@ import { ShortcodeService } from 'src/shortcode/shortcode.service'; import { ConfigService } from '@nestjs/config'; import { OffsetPaginationArgs } from 'src/types/input-types.args'; import * as E from 'fp-ts/Either'; +import { UserHistoryService } from 'src/user-history/user-history.service'; const mockPrisma = mockDeep(); const mockPubSub = mockDeep(); @@ -34,6 +35,7 @@ const mockTeamCollectionService = mockDeep(); const mockMailerService = mockDeep(); const mockShortcodeService = mockDeep(); const mockConfigService = mockDeep(); +const mockUserHistoryService = mockDeep(); const adminService = new AdminService( mockUserService, @@ -47,6 +49,7 @@ const adminService = new AdminService( mockMailerService, mockShortcodeService, mockConfigService, + mockUserHistoryService, ); const invitedUsers: InvitedUsers[] = [ @@ -292,4 +295,15 @@ describe('AdminService', () => { expect(result).toEqual(10); }); }); + + describe('deleteAllUserHistory', () => { + test('should resolve right and delete all user history', async () => { + mockUserHistoryService.deleteAllHistories.mockResolvedValueOnce( + E.right(true), + ); + + const result = await adminService.deleteAllUserHistory(); + expect(result).toEqualRight(true); + }); + }); }); diff --git a/packages/hoppscotch-backend/src/admin/admin.service.ts b/packages/hoppscotch-backend/src/admin/admin.service.ts index 33e7d886..8bd257d8 100644 --- a/packages/hoppscotch-backend/src/admin/admin.service.ts +++ b/packages/hoppscotch-backend/src/admin/admin.service.ts @@ -31,6 +31,7 @@ import { ShortcodeService } from 'src/shortcode/shortcode.service'; import { ConfigService } from '@nestjs/config'; import { OffsetPaginationArgs } from 'src/types/input-types.args'; import { UserDeletionResult } from 'src/user/user.model'; +import { UserHistoryService } from 'src/user-history/user-history.service'; @Injectable() export class AdminService { @@ -46,6 +47,7 @@ export class AdminService { private readonly mailerService: MailerService, private readonly shortcodeService: ShortcodeService, private readonly configService: ConfigService, + private readonly userHistoryService: UserHistoryService, ) {} /** @@ -650,4 +652,15 @@ export class AdminService { if (E.isLeft(result)) return E.left(result.left); return E.right(result.right); } + + /** + * Delete all user history + * @returns Boolean on successful deletion + */ + async deleteAllUserHistory() { + const result = await this.userHistoryService.deleteAllHistories(); + + if (E.isLeft(result)) return E.left(result.left); + return E.right(result.right); + } } diff --git a/packages/hoppscotch-backend/src/admin/infra.resolver.ts b/packages/hoppscotch-backend/src/admin/infra.resolver.ts index 084f7bb9..045c1a3f 100644 --- a/packages/hoppscotch-backend/src/admin/infra.resolver.ts +++ b/packages/hoppscotch-backend/src/admin/infra.resolver.ts @@ -214,9 +214,8 @@ export class InfraResolver { }) teamID: string, ) { - const invitations = await this.adminService.pendingInvitationCountInTeam( - teamID, - ); + const invitations = + await this.adminService.pendingInvitationCountInTeam(teamID); return invitations; } @@ -352,9 +351,8 @@ export class InfraResolver { }) providerInfo: EnableAndDisableSSOArgs[], ) { - const isUpdated = await this.infraConfigService.enableAndDisableSSO( - providerInfo, - ); + const isUpdated = + await this.infraConfigService.enableAndDisableSSO(providerInfo); if (E.isLeft(isUpdated)) throwErr(isUpdated.left); return true; @@ -372,7 +370,26 @@ export class InfraResolver { }) status: ServiceStatus, ) { - const isUpdated = await this.infraConfigService.enableAndDisableSMTP( + const isUpdated = + await this.infraConfigService.enableAndDisableSMTP(status); + if (E.isLeft(isUpdated)) throwErr(isUpdated.left); + return true; + } + + @Mutation(() => Boolean, { + description: 'Enable or Disable User History Storing in DB', + }) + @UseGuards(GqlAuthGuard, GqlAdminGuard) + async toggleUserHistoryStore( + @Args({ + name: 'status', + type: () => ServiceStatus, + description: 'Toggle User History Store', + }) + status: ServiceStatus, + ) { + const isUpdated = await this.infraConfigService.toggleServiceStatus( + InfraConfigEnum.USER_HISTORY_STORE_ENABLED, status, ); if (E.isLeft(isUpdated)) throwErr(isUpdated.left); diff --git a/packages/hoppscotch-backend/src/errors.ts b/packages/hoppscotch-backend/src/errors.ts index 75d0a7b4..5279d2bf 100644 --- a/packages/hoppscotch-backend/src/errors.ts +++ b/packages/hoppscotch-backend/src/errors.ts @@ -492,7 +492,19 @@ export const USER_ENVIRONMENT_INVALID_ENVIRONMENT_NAME = */ export const USER_HISTORY_NOT_FOUND = 'user_history/history_not_found' as const; -/* +/** + * User history deletion failed + * (UserHistoryService) + */ +export const USER_HISTORY_DELETION_FAILED = + 'user_history/deletion_failed' as const; + +/** + * User history feature flag is disabled + * (UserHistoryService) + */ +export const USER_HISTORY_FEATURE_FLAG_DISABLED = + 'user_history/feature_flag_disabled'; /** * Invalid Request Type in History diff --git a/packages/hoppscotch-backend/src/infra-config/helper.ts b/packages/hoppscotch-backend/src/infra-config/helper.ts index b8e059e4..6348e51d 100644 --- a/packages/hoppscotch-backend/src/infra-config/helper.ts +++ b/packages/hoppscotch-backend/src/infra-config/helper.ts @@ -262,6 +262,12 @@ export async function getDefaultInfraConfigs(): Promise { lastSyncedEnvFileValue: null, isEncrypted: false, }, + { + name: InfraConfigEnum.USER_HISTORY_STORE_ENABLED, + value: 'true', + lastSyncedEnvFileValue: null, + isEncrypted: false, + }, ]; return infraConfigDefaultObjs; diff --git a/packages/hoppscotch-backend/src/infra-config/infra-config.module.ts b/packages/hoppscotch-backend/src/infra-config/infra-config.module.ts index 71eb26f7..521f10c9 100644 --- a/packages/hoppscotch-backend/src/infra-config/infra-config.module.ts +++ b/packages/hoppscotch-backend/src/infra-config/infra-config.module.ts @@ -3,9 +3,10 @@ import { InfraConfigService } from './infra-config.service'; import { PrismaModule } from 'src/prisma/prisma.module'; import { SiteController } from './infra-config.controller'; import { InfraConfigResolver } from './infra-config.resolver'; +import { PubSubModule } from 'src/pubsub/pubsub.module'; @Module({ - imports: [PrismaModule], + imports: [PrismaModule, PubSubModule], providers: [InfraConfigResolver, InfraConfigService], exports: [InfraConfigService], controllers: [SiteController], diff --git a/packages/hoppscotch-backend/src/infra-config/infra-config.resolver.ts b/packages/hoppscotch-backend/src/infra-config/infra-config.resolver.ts index 4f062abd..aeb08cd3 100644 --- a/packages/hoppscotch-backend/src/infra-config/infra-config.resolver.ts +++ b/packages/hoppscotch-backend/src/infra-config/infra-config.resolver.ts @@ -1,14 +1,24 @@ import { UseGuards } from '@nestjs/common'; -import { Query, Resolver } from '@nestjs/graphql'; +import { Args, Query, Resolver, Subscription } from '@nestjs/graphql'; import { GqlThrottlerGuard } from 'src/guards/gql-throttler.guard'; import { InfraConfig } from './infra-config.model'; import { InfraConfigService } from './infra-config.service'; import { GqlAuthGuard } from 'src/guards/gql-auth.guard'; +import { SkipThrottle } from '@nestjs/throttler'; +import { PubSubService } from 'src/pubsub/pubsub.service'; +import { InfraConfigEnum } from 'src/types/InfraConfig'; +import * as E from 'fp-ts/Either'; +import { throwErr } from 'src/utils'; @UseGuards(GqlThrottlerGuard) @Resolver(() => InfraConfig) export class InfraConfigResolver { - constructor(private infraConfigService: InfraConfigService) {} + constructor( + private infraConfigService: InfraConfigService, + private pubsub: PubSubService, + ) {} + + /* Query */ @Query(() => Boolean, { description: 'Check if the SMTP is enabled or not', @@ -17,4 +27,33 @@ export class InfraConfigResolver { isSMTPEnabled() { return this.infraConfigService.isSMTPEnabled(); } + + @Query(() => InfraConfig, { + description: 'Check if user history is enabled or not', + }) + @UseGuards(GqlAuthGuard) + async isUserHistoryEnabled() { + const isEnabled = await this.infraConfigService.isUserHistoryEnabled(); + if (E.isLeft(isEnabled)) throwErr(isEnabled.left); + return isEnabled.right; + } + + /* Subscriptions */ + + @Subscription(() => String, { + description: 'Subscription for infra config update', + resolve: (value) => value, + }) + @SkipThrottle() + @UseGuards(GqlAuthGuard) + infraConfigUpdate( + @Args({ + name: 'configName', + description: 'Infra config key', + type: () => InfraConfigEnum, + }) + configName: string, + ) { + return this.pubsub.asyncIterator(`infra_config/${configName}/updated`); + } } diff --git a/packages/hoppscotch-backend/src/infra-config/infra-config.service.spec.ts b/packages/hoppscotch-backend/src/infra-config/infra-config.service.spec.ts index a8576b41..f1c65f70 100644 --- a/packages/hoppscotch-backend/src/infra-config/infra-config.service.spec.ts +++ b/packages/hoppscotch-backend/src/infra-config/infra-config.service.spec.ts @@ -11,15 +11,20 @@ import { ConfigService } from '@nestjs/config'; import * as helper from './helper'; import { InfraConfig as dbInfraConfig } from '@prisma/client'; import { InfraConfig } from './infra-config.model'; +import { PubSubService } from 'src/pubsub/pubsub.service'; +import { ServiceStatus } from './helper'; +import * as E from 'fp-ts/Either'; const mockPrisma = mockDeep(); const mockConfigService = mockDeep(); +const mockPubsub = mockDeep(); // eslint-disable-next-line @typescript-eslint/ban-ts-comment // @ts-ignore const infraConfigService = new InfraConfigService( mockPrisma, mockConfigService, + mockPubsub, ); const INITIALIZED_DATE_CONST = new Date(); @@ -243,4 +248,59 @@ describe('InfraConfigService', () => { ); }); }); + + describe('toggleServiceStatus', () => { + it('should toggle the service status', async () => { + const configName = infraConfigs[0].name; + const configStatus = ServiceStatus.DISABLE; + + jest + .spyOn(infraConfigService, 'update') + .mockResolvedValueOnce( + E.right({ name: configName, value: configStatus }), + ); + + expect( + await infraConfigService.toggleServiceStatus(configName, configStatus), + ).toEqualRight(true); + }); + it('should publish the updated config value', async () => { + const configName = infraConfigs[0].name; + const configStatus = ServiceStatus.DISABLE; + + jest + .spyOn(infraConfigService, 'update') + .mockResolvedValueOnce( + E.right({ name: configName, value: configStatus }), + ); + + await infraConfigService.toggleServiceStatus(configName, configStatus); + + expect(mockPubsub.publish).toHaveBeenCalledTimes(1); + expect(mockPubsub.publish).toHaveBeenCalledWith( + 'infra_config/GOOGLE_CLIENT_ID/updated', + configStatus, + ); + }); + }); + + describe('isUserHistoryEnabled', () => { + it('should return true if the user history is enabled', async () => { + const response = { + name: InfraConfigEnum.USER_HISTORY_STORE_ENABLED, + value: ServiceStatus.ENABLE, + }; + + jest.spyOn(infraConfigService, 'get').mockResolvedValueOnce( + E.right({ + name: InfraConfigEnum.USER_HISTORY_STORE_ENABLED, + value: ServiceStatus.ENABLE, + }), + ); + + expect(await infraConfigService.isUserHistoryEnabled()).toEqualRight( + response, + ); + }); + }); }); diff --git a/packages/hoppscotch-backend/src/infra-config/infra-config.service.ts b/packages/hoppscotch-backend/src/infra-config/infra-config.service.ts index cf4d0ac0..d0749f53 100644 --- a/packages/hoppscotch-backend/src/infra-config/infra-config.service.ts +++ b/packages/hoppscotch-backend/src/infra-config/infra-config.service.ts @@ -33,12 +33,14 @@ import { } from './helper'; import { EnableAndDisableSSOArgs, InfraConfigArgs } from './input-args'; import { AuthProvider } from 'src/auth/helper'; +import { PubSubService } from 'src/pubsub/pubsub.service'; @Injectable() export class InfraConfigService implements OnModuleInit { constructor( private readonly prisma: PrismaService, private readonly configService: ConfigService, + private readonly pubsub: PubSubService, ) {} // Following fields are not updatable by `infraConfigs` Mutation. Use dedicated mutations for these fields instead. @@ -48,6 +50,7 @@ export class InfraConfigService implements OnModuleInit { InfraConfigEnum.ANALYTICS_USER_ID, InfraConfigEnum.IS_FIRST_TIME_INFRA_SETUP, InfraConfigEnum.MAILER_SMTP_ENABLE, + InfraConfigEnum.USER_HISTORY_STORE_ENABLED, ]; // Following fields can not be fetched by `infraConfigs` Query. Use dedicated queries for these fields instead. EXCLUDE_FROM_FETCH_CONFIGS = [ @@ -132,6 +135,17 @@ export class InfraConfigService implements OnModuleInit { * @returns InfraConfig model */ cast(dbInfraConfig: DBInfraConfig) { + switch (dbInfraConfig.name) { + case InfraConfigEnum.USER_HISTORY_STORE_ENABLED: + dbInfraConfig.value = + dbInfraConfig.value === 'true' + ? ServiceStatus.ENABLE + : ServiceStatus.DISABLE; + break; + default: + break; + } + const plainValue = dbInfraConfig.isEncrypted ? decrypt(dbInfraConfig.value) : dbInfraConfig.value; @@ -342,6 +356,11 @@ export class InfraConfigService implements OnModuleInit { ); if (E.isLeft(isUpdated)) return E.left(isUpdated.left); + this.pubsub.publish( + `infra_config/${configName}/updated`, + isUpdated.right.value, + ); + return E.right(true); } @@ -453,6 +472,19 @@ export class InfraConfigService implements OnModuleInit { ); } + /** + * Check if user history is enabled or not + * @returns InfraConfig model + */ + async isUserHistoryEnabled() { + const infraConfig = await this.get( + InfraConfigEnum.USER_HISTORY_STORE_ENABLED, + ); + + if (E.isLeft(infraConfig)) return E.left(infraConfig.left); + return E.right(infraConfig.right); + } + /** * Reset all the InfraConfigs to their default values (from .env) */ diff --git a/packages/hoppscotch-backend/src/pubsub/topicsDefs.ts b/packages/hoppscotch-backend/src/pubsub/topicsDefs.ts index b3617122..d9b7b246 100644 --- a/packages/hoppscotch-backend/src/pubsub/topicsDefs.ts +++ b/packages/hoppscotch-backend/src/pubsub/topicsDefs.ts @@ -43,9 +43,10 @@ export type TopicDef = { topic: `user_request/${string}/${'created' | 'updated' | 'deleted'}` ]: UserRequest; [topic: `user_request/${string}/${'moved'}`]: UserRequestReorderData; - [ - topic: `user_history/${string}/${'created' | 'updated' | 'deleted'}` - ]: UserHistory; + [topic: `user_history/${string}/${'created' | 'updated' | 'deleted'}`]: + | UserHistory + | boolean; + [topic: `user_history/${string}/deleted_many`]: UserHistoryDeletedManyData; [ topic: `user_coll/${string}/${'created' | 'updated' | 'moved'}` ]: UserCollection; @@ -63,7 +64,6 @@ export type TopicDef = { [topic: `team_coll/${string}/${'coll_removed'}`]: string; [topic: `team_coll/${string}/${'coll_moved'}`]: TeamCollection; [topic: `team_coll/${string}/${'coll_order_updated'}`]: CollectionReorderData; - [topic: `user_history/${string}/deleted_many`]: UserHistoryDeletedManyData; [ topic: `team_req/${string}/${'req_created' | 'req_updated' | 'req_moved'}` ]: TeamRequest; @@ -74,4 +74,5 @@ export type TopicDef = { [ topic: `shortcode/${string}/${'created' | 'revoked' | 'updated'}` ]: Shortcode; + [topic: `infra_config/${string}/${'updated'}`]: string; }; diff --git a/packages/hoppscotch-backend/src/types/InfraConfig.ts b/packages/hoppscotch-backend/src/types/InfraConfig.ts index 8174d47c..30da8336 100644 --- a/packages/hoppscotch-backend/src/types/InfraConfig.ts +++ b/packages/hoppscotch-backend/src/types/InfraConfig.ts @@ -32,4 +32,6 @@ export enum InfraConfigEnum { ALLOW_ANALYTICS_COLLECTION = 'ALLOW_ANALYTICS_COLLECTION', ANALYTICS_USER_ID = 'ANALYTICS_USER_ID', IS_FIRST_TIME_INFRA_SETUP = 'IS_FIRST_TIME_INFRA_SETUP', + + USER_HISTORY_STORE_ENABLED = 'USER_HISTORY_STORE_ENABLED', } diff --git a/packages/hoppscotch-backend/src/user-history/user-history-feature-flag.guard.ts b/packages/hoppscotch-backend/src/user-history/user-history-feature-flag.guard.ts new file mode 100644 index 00000000..d878b4a2 --- /dev/null +++ b/packages/hoppscotch-backend/src/user-history/user-history-feature-flag.guard.ts @@ -0,0 +1,21 @@ +import { CanActivate, ExecutionContext, Injectable } from '@nestjs/common'; +import { USER_HISTORY_FEATURE_FLAG_DISABLED } from 'src/errors'; +import { InfraConfigService } from 'src/infra-config/infra-config.service'; +import { throwErr } from 'src/utils'; +import * as E from 'fp-ts/Either'; +import { ServiceStatus } from 'src/infra-config/helper'; + +@Injectable() +export class UserHistoryFeatureFlagGuard implements CanActivate { + constructor(private readonly infraConfigService: InfraConfigService) {} + + async canActivate(context: ExecutionContext): Promise { + const isEnabled = await this.infraConfigService.isUserHistoryEnabled(); + if (E.isLeft(isEnabled)) throwErr(isEnabled.left); + + if (isEnabled.right.value !== ServiceStatus.ENABLE) + throwErr(USER_HISTORY_FEATURE_FLAG_DISABLED); + + return true; + } +} diff --git a/packages/hoppscotch-backend/src/user-history/user-history.module.ts b/packages/hoppscotch-backend/src/user-history/user-history.module.ts index aaa20bfe..ce2d6884 100644 --- a/packages/hoppscotch-backend/src/user-history/user-history.module.ts +++ b/packages/hoppscotch-backend/src/user-history/user-history.module.ts @@ -5,9 +5,10 @@ import { UserModule } from '../user/user.module'; import { UserHistoryUserResolver } from './user.resolver'; import { UserHistoryResolver } from './user-history.resolver'; import { UserHistoryService } from './user-history.service'; +import { InfraConfigModule } from 'src/infra-config/infra-config.module'; @Module({ - imports: [PrismaModule, PubSubModule, UserModule], + imports: [PrismaModule, PubSubModule, UserModule, InfraConfigModule], providers: [UserHistoryResolver, UserHistoryService, UserHistoryUserResolver], exports: [UserHistoryService], }) diff --git a/packages/hoppscotch-backend/src/user-history/user-history.resolver.ts b/packages/hoppscotch-backend/src/user-history/user-history.resolver.ts index a07b26f8..13551a11 100644 --- a/packages/hoppscotch-backend/src/user-history/user-history.resolver.ts +++ b/packages/hoppscotch-backend/src/user-history/user-history.resolver.ts @@ -11,8 +11,9 @@ import { throwErr } from '../utils'; import * as E from 'fp-ts/Either'; import { GqlThrottlerGuard } from 'src/guards/gql-throttler.guard'; import { SkipThrottle } from '@nestjs/throttler'; +import { UserHistoryFeatureFlagGuard } from './user-history-feature-flag.guard'; -@UseGuards(GqlThrottlerGuard) +@UseGuards(GqlThrottlerGuard, UserHistoryFeatureFlagGuard) @Resolver() export class UserHistoryResolver { constructor( @@ -156,4 +157,14 @@ export class UserHistoryResolver { userHistoryDeletedMany(@GqlUser() user: User) { return this.pubsub.asyncIterator(`user_history/${user.uid}/deleted_many`); } + + @Subscription(() => Boolean, { + description: 'Listen for All User History deleted', + resolve: (value) => value, + }) + @SkipThrottle() + @UseGuards(GqlAuthGuard) + userHistoryAllDeleted() { + return this.pubsub.asyncIterator(`user_history/all/deleted`); + } } diff --git a/packages/hoppscotch-backend/src/user-history/user-history.service.spec.ts b/packages/hoppscotch-backend/src/user-history/user-history.service.spec.ts index e5356392..c9ccba49 100644 --- a/packages/hoppscotch-backend/src/user-history/user-history.service.spec.ts +++ b/packages/hoppscotch-backend/src/user-history/user-history.service.spec.ts @@ -493,6 +493,30 @@ describe('UserHistoryService', () => { ); }); }); + describe('deleteAllHistories', () => { + test('Should resolve right and delete all user history', async () => { + mockPrisma.userHistory.deleteMany.mockResolvedValueOnce({ + count: 2, + }); + + return expect(await userHistoryService.deleteAllHistories()).toEqualRight( + true, + ); + }); + test('Should publish all user history delete event', async () => { + mockPrisma.userHistory.deleteMany.mockResolvedValueOnce({ + count: 2, + }); + + await userHistoryService.deleteAllHistories(); + + expect(mockPubSub.publish).toHaveBeenCalledTimes(1); + return expect(mockPubSub.publish).toHaveBeenCalledWith( + `user_history/all/deleted`, + true, + ); + }); + }); describe('validateReqType', () => { test('Should resolve right when a valid REST ReqType is provided', async () => { return expect(userHistoryService.validateReqType('REST')).toEqualRight( diff --git a/packages/hoppscotch-backend/src/user-history/user-history.service.ts b/packages/hoppscotch-backend/src/user-history/user-history.service.ts index ba643c4a..19d65ae4 100644 --- a/packages/hoppscotch-backend/src/user-history/user-history.service.ts +++ b/packages/hoppscotch-backend/src/user-history/user-history.service.ts @@ -6,6 +6,7 @@ import { ReqType } from 'src/types/RequestTypes'; import * as E from 'fp-ts/Either'; import * as O from 'fp-ts/Option'; import { + USER_HISTORY_DELETION_FAILED, USER_HISTORY_INVALID_REQ_TYPE, USER_HISTORY_NOT_FOUND, } from '../errors'; @@ -188,6 +189,21 @@ export class UserHistoryService { return E.right(deletionInfo); } + /** + * Delete all user history from DB + * @returns a boolean + */ + async deleteAllHistories() { + try { + await this.prisma.userHistory.deleteMany(); + } catch (error) { + return E.left(USER_HISTORY_DELETION_FAILED); + } + + this.pubsub.publish('user_history/all/deleted', true); + return E.right(true); + } + /** * Fetch a user history based on history ID. * @param id User History ID