diff --git a/src/modules/bot/bot.controller.ts b/src/modules/bot/bot.controller.ts index 1acd40b..431c8b6 100644 --- a/src/modules/bot/bot.controller.ts +++ b/src/modules/bot/bot.controller.ts @@ -30,6 +30,7 @@ import { diskStorage } from 'multer'; import { Request } from 'express'; import { extname } from 'path'; import fs from 'fs'; +import { DeleteBotsDTO } from './dto/delete-bot-dto'; const editFileName = (req: Request, file: Express.Multer.File, callback) => { @@ -335,15 +336,26 @@ export class BotController { return this.botService.update(id, updateBotDto); } - @Delete(':id') + @Delete() @UseInterceptors( AddResponseObjectInterceptor, AddAdminHeaderInterceptor, AddOwnerInfoInterceptor, AddROToResponseInterceptor, ) - remove(@Param('id') id: string) { - return this.botService.remove(id); + async remove(@Body() body: DeleteBotsDTO) { + return await this.botService.remove(body); + } + + @Delete(':botId') + @UseInterceptors( + AddResponseObjectInterceptor, + AddAdminHeaderInterceptor, + AddOwnerInfoInterceptor, + AddROToResponseInterceptor, + ) + async removeOne(@Param('botId') botId: string) { + return await this.botService.removeOne(botId); } @Get(':botId/broadcastReport') diff --git a/src/modules/bot/bot.service.spec.ts b/src/modules/bot/bot.service.spec.ts index 9d3372e..bc44914 100644 --- a/src/modules/bot/bot.service.spec.ts +++ b/src/modules/bot/bot.service.spec.ts @@ -44,8 +44,31 @@ const MockPrismaService = { } }, count: () => 10, - update: jest.fn() - } + update: jest.fn(), + deleteMany: (filter) => { + deletedIds.push({'bot': filter.where.id.in}); + } + }, + service: { + deleteMany: (filter) => { + deletedIds.push({'service': filter.where.id.in}); + } + }, + userSegment: { + deleteMany: (filter) => { + deletedIds.push({'userSegment': filter.where.id.in}); + } + }, + transformerConfig: { + deleteMany: (filter) => { + deletedIds.push({'transformerConfig': filter.where.id.in}); + } + }, + conversationLogic: { + deleteMany: (filter) => { + deletedIds.push({'conversationLogic': filter.where.id.in}); + } + }, } class MockConfigService { @@ -320,6 +343,9 @@ const mockConfig = { "totalRecords": 1 }; +// Used for delete bot testing +let deletedIds: any[] = [] + describe('BotService', () => { let botService: BotService; let configService: ConfigService; @@ -506,7 +532,7 @@ describe('BotService', () => { }); it('bot update throws NotFoundException when non existent bot is updated',async () => { - fetchMock.getOnce(`${configService.get('UCI_CORE_BASE_URL')}${configService.get('CAFFINE_INVALIDATE_ENDPOINT')}`, + fetchMock.deleteOnce(`${configService.get('UCI_CORE_BASE_URL')}${configService.get('CAFFINE_INVALIDATE_ENDPOINT')}`, true ); expect(botService.update('testBotIdNotExisting', { @@ -605,4 +631,97 @@ describe('BotService', () => { ).toBe(true); fetchMock.restore(); }); + + it('bot delete with bot id list works as expected', async () => { + fetchMock.delete(`${configService.get('UCI_CORE_BASE_URL')}${configService.get('CAFFINE_INVALIDATE_ENDPOINT')}`, + true + ); + mockBotsDb[0].status = BotStatus.DISABLED; + await botService.remove({ids: ['testId'], endDate: null}); + expect(deletedIds).toEqual( + [ + {'service': ['testId']}, + {'userSegment': ['testUserId']}, + {'transformerConfig': ['testTransformerId']}, + {'conversationLogic': ['testLogicId']}, + {'bot': ['testId']}, + ] + ); + deletedIds = []; + await botService.remove({ids: ['nonExisting'], endDate: null}); + expect(deletedIds).toEqual( + [ + {'service': []}, + {'userSegment': []}, + {'transformerConfig': []}, + {'conversationLogic': []}, + {'bot': []}, + ] + ); + deletedIds = []; + expect(fetchMock.called( + `${configService.get('UCI_CORE_BASE_URL')}${configService.get('CAFFINE_INVALIDATE_ENDPOINT')}` + )) + .toBe(true); + mockBotsDb[0].status = BotStatus.ENABLED; + fetchMock.restore(); + }); + + it('bot delete with endDate works as expected', async () => { + fetchMock.delete(`${configService.get('UCI_CORE_BASE_URL')}${configService.get('CAFFINE_INVALIDATE_ENDPOINT')}`, + true + ); + mockBotsDb[0].status = BotStatus.DISABLED; + await botService.remove({ids: null, endDate: '2025-12-01'}); + expect(deletedIds).toEqual( + [ + {'service': ['testId']}, + {'userSegment': ['testUserId']}, + {'transformerConfig': ['testTransformerId']}, + {'conversationLogic': ['testLogicId']}, + {'bot': ['testId']}, + ] + ); + deletedIds = []; + await botService.remove({ids: null, endDate: '2023-12-01'}); + expect(deletedIds).toEqual( + [ + {'service': []}, + {'userSegment': []}, + {'transformerConfig': []}, + {'conversationLogic': []}, + {'bot': []}, + ] + ); + expect(fetchMock.called( + `${configService.get('UCI_CORE_BASE_URL')}${configService.get('CAFFINE_INVALIDATE_ENDPOINT')}` + )) + .toBe(true); + deletedIds = []; + mockBotsDb[0].status = BotStatus.ENABLED; + fetchMock.restore(); + }); + + it('bot delete only deletes disabled bots', async () => { + fetchMock.delete(`${configService.get('UCI_CORE_BASE_URL')}${configService.get('CAFFINE_INVALIDATE_ENDPOINT')}`, + true + ); + mockBotsDb[0].status = BotStatus.ENABLED; + await botService.remove({ids: ['testId'], endDate: null}); + expect(deletedIds).toEqual( + [ + {'service': []}, + {'userSegment': []}, + {'transformerConfig': []}, + {'conversationLogic': []}, + {'bot': []}, + ] + ); + expect(fetchMock.called( + `${configService.get('UCI_CORE_BASE_URL')}${configService.get('CAFFINE_INVALIDATE_ENDPOINT')}` + )) + .toBe(true); + deletedIds = []; + fetchMock.restore(); + }); }); diff --git a/src/modules/bot/bot.service.ts b/src/modules/bot/bot.service.ts index bbe92f5..16a3152 100644 --- a/src/modules/bot/bot.service.ts +++ b/src/modules/bot/bot.service.ts @@ -14,6 +14,7 @@ const limit = pLimit(1); import fs from 'fs'; import FormData from 'form-data'; import { Cache } from 'cache-manager'; +import { DeleteBotsDTO } from './dto/delete-bot-dto'; @Injectable() export class BotService { @@ -237,7 +238,7 @@ export class BotService { } } - async findAllUnresolved(): Promise[] | null>> { + }>[]> { const startTime = performance.now(); const cacheKey = `unresolved_bots_data`; const cachedBots = await this.cacheManager.get(cacheKey); @@ -503,14 +504,6 @@ export class BotService { } async update(id: string, updateBotDto: any) { - const inbound_base = this.configService.get('UCI_CORE_BASE_URL'); - const caffine_invalidate_endpoint = this.configService.get('CAFFINE_INVALIDATE_ENDPOINT'); - const transaction_layer_auth_token = this.configService.get('AUTHORIZATION_KEY_TRANSACTION_LAYER'); - if (!inbound_base || !caffine_invalidate_endpoint || !transaction_layer_auth_token) { - this.logger.error(`Missing configuration: inbound endpoint: ${inbound_base}, caffine endpoint: ${caffine_invalidate_endpoint} or transaction layer auth token.`); - throw new InternalServerErrorException(); - } - const caffine_reset_url = `${inbound_base}${caffine_invalidate_endpoint}`; const existingBot = await this.findOne(id); if (!existingBot) { throw new NotFoundException("Bot does not exist!") @@ -547,7 +540,20 @@ export class BotService { data: updateBotDto, }); await this.cacheManager.reset(); - await fetch(caffine_reset_url, {method: 'DELETE', headers: {'Authorization': transaction_layer_auth_token}}) + await this.invalidateTransactionLayerCache(); + return updatedBot; + } + + async invalidateTransactionLayerCache() { + const inbound_base = this.configService.get('UCI_CORE_BASE_URL'); + const caffine_invalidate_endpoint = this.configService.get('CAFFINE_INVALIDATE_ENDPOINT'); + const transaction_layer_auth_token = this.configService.get('AUTHORIZATION_KEY_TRANSACTION_LAYER'); + if (!inbound_base || !caffine_invalidate_endpoint || !transaction_layer_auth_token) { + this.logger.error(`Missing configuration: inbound endpoint: ${inbound_base}, caffine reset endpoint: ${caffine_invalidate_endpoint} or transaction layer auth token.`); + throw new InternalServerErrorException(); + } + const caffine_reset_url = `${inbound_base}${caffine_invalidate_endpoint}`; + return fetch(caffine_reset_url, {method: 'DELETE', headers: {'Authorization': transaction_layer_auth_token}}) .then((resp) => { if (resp.ok) { return resp.json(); @@ -561,11 +567,108 @@ export class BotService { this.logger.error(`Got failure response from inbound on cache invalidation endpoint ${caffine_reset_url}. Error: ${err}`); throw new ServiceUnavailableException('Could not invalidate cache after update!'); }); - return updatedBot; } - remove(id: string) { - return `This action removes a #${id} adapter`; + async remove(deleteBotsDTO: DeleteBotsDTO) { + let botIds = new Set(); + if (deleteBotsDTO.ids) { + botIds = new Set(deleteBotsDTO.ids); + } + const endDate = deleteBotsDTO.endDate; + if ((!botIds || botIds.size == 0) && !endDate) { + throw new BadRequestException('Bot ids or endDate need to be provided!'); + } + let parsedEndDate: Date; + if (endDate) { + const dateRegex: RegExp = /^\d{4}-\d{2}-\d{2}$/; + if (!dateRegex.test(endDate)) { + throw new BadRequestException(`Bad date format! Please provide date in 'yyyy-mm-dd' format.`) + } + try { + parsedEndDate = new Date(endDate); + } + catch (err) { + throw new BadRequestException(`Invalid date! Please enter a valid date.`) + } + } + + const allBots = await this.findAllUnresolved(); + const requiredBotIds: string[] = [], requiredServiceIds: string[] = [], + requiredUserIds: string[] = [], requiredLogicIds: string[] = [], + requiredTransformerConfigIds: string[] = []; + allBots.forEach(bot => { + if (bot.status == BotStatus.DISABLED) { + const currentParsedEndDate = new Date(bot.endDate!); + if ( + (botIds.has(bot.id) && !endDate) || + (endDate && (parsedEndDate.getTime() >= currentParsedEndDate.getTime()) && botIds.size == 0) || + (botIds.has(bot.id) && (endDate && (parsedEndDate.getTime() >= currentParsedEndDate.getTime()))) + ) { + requiredBotIds.push(bot.id); + if (bot.logicIDs.length > 0) { + requiredLogicIds.push(bot.logicIDs[0].id); + if (bot.logicIDs[0].transformers.length > 0) { + requiredTransformerConfigIds.push(bot.logicIDs[0].transformers[0].id); + } + } + if (bot.users.length > 0) { + requiredUserIds.push(bot.users[0].id); + if (bot.users[0].all != null) { + requiredServiceIds.push(bot.users[0].all.id); + } + } + } + } + }); + const deletePromises = [ + this.prisma.service.deleteMany({ + where: { + id: { + in: requiredServiceIds, + } + } + }), + this.prisma.userSegment.deleteMany({ + where: { + id: { + in: requiredUserIds, + } + } + }), + this.prisma.transformerConfig.deleteMany({ + where: { + id: { + in: requiredTransformerConfigIds, + } + } + }), + this.prisma.conversationLogic.deleteMany({ + where: { + id: { + in: requiredLogicIds, + } + } + }), + this.prisma.bot.deleteMany({ + where: { + id: { + in: requiredBotIds, + } + } + }), + ]; + + return Promise.all(deletePromises) + .then(() => { + return this.invalidateTransactionLayerCache(); + }) + .catch((err) => { + throw err; + }); + } + + async removeOne(botId: string) { + return this.remove({ids: [botId], endDate: null}); } async getBroadcastReport(botId: string, limit: number, nextPage: string) { diff --git a/src/modules/bot/dto/delete-bot-dto.ts b/src/modules/bot/dto/delete-bot-dto.ts new file mode 100644 index 0000000..8a1eaae --- /dev/null +++ b/src/modules/bot/dto/delete-bot-dto.ts @@ -0,0 +1,4 @@ +export class DeleteBotsDTO { + ids: string[] | undefined | null; + endDate: string | undefined | null; +}