diff --git a/apps/api/src/app/bridge/bridge.module.ts b/apps/api/src/app/bridge/bridge.module.ts index fb2632c223d..f7474399309 100644 --- a/apps/api/src/app/bridge/bridge.module.ts +++ b/apps/api/src/app/bridge/bridge.module.ts @@ -4,6 +4,8 @@ import { CreateMessageTemplate, CreateWorkflow, DeleteMessageTemplate, + DeleteWorkflowUseCase, + GetWorkflowByIdsUseCase, UpdateChange, UpdateMessageTemplate, UpdateWorkflow, @@ -18,6 +20,9 @@ import { USECASES } from './usecases'; const PROVIDERS = [ CreateWorkflow, UpdateWorkflow, + GetWorkflowByIdsUseCase, + DeleteWorkflowUseCase, + UpsertControlValuesUseCase, CreateMessageTemplate, UpdateMessageTemplate, DeleteMessageTemplate, diff --git a/apps/api/src/app/bridge/usecases/delete-workflow/delete-workflow.command.ts b/apps/api/src/app/bridge/usecases/delete-workflow/delete-workflow.command.ts deleted file mode 100644 index be7b5bfec89..00000000000 --- a/apps/api/src/app/bridge/usecases/delete-workflow/delete-workflow.command.ts +++ /dev/null @@ -1,8 +0,0 @@ -import { IsDefined, IsMongoId } from 'class-validator'; -import { EnvironmentWithUserCommand } from '@novu/application-generic'; - -export class DeleteWorkflowCommand extends EnvironmentWithUserCommand { - @IsDefined() - @IsMongoId() - workflowId: string; -} diff --git a/apps/api/src/app/bridge/usecases/delete-workflow/delete-workflow.usecase.ts b/apps/api/src/app/bridge/usecases/delete-workflow/delete-workflow.usecase.ts deleted file mode 100644 index f7dc973193e..00000000000 --- a/apps/api/src/app/bridge/usecases/delete-workflow/delete-workflow.usecase.ts +++ /dev/null @@ -1,72 +0,0 @@ -import { Injectable, NotFoundException } from '@nestjs/common'; - -import { NotificationTemplateRepository, NotificationTemplateEntity } from '@novu/dal'; -import { - buildNotificationTemplateIdentifierKey, - buildNotificationTemplateKey, - DeleteMessageTemplate, - DeleteMessageTemplateCommand, - InvalidateCacheService, -} from '@novu/application-generic'; -import { WorkflowTypeEnum } from '@novu/shared'; - -import { DeleteWorkflowCommand } from './delete-workflow.command'; - -@Injectable() -export class DeleteWorkflow { - constructor( - private notificationTemplateRepository: NotificationTemplateRepository, - private invalidateCache: InvalidateCacheService, - private deleteMessageTemplate: DeleteMessageTemplate - ) {} - - async execute(command: DeleteWorkflowCommand) { - const workflow = await this.notificationTemplateRepository.findOne({ - _environmentId: command.environmentId, - _id: command.workflowId, - }); - if (!workflow) { - throw new NotFoundException(`Could not find workflow with id ${command.workflowId}`); - } - - for (const step of workflow.steps) { - await this.deleteMessageTemplate.execute( - DeleteMessageTemplateCommand.create({ - organizationId: command.organizationId, - environmentId: command.environmentId, - userId: command.userId, - messageTemplateId: step._templateId, - workflowType: WorkflowTypeEnum.BRIDGE, - }) - ); - } - - await this.notificationTemplateRepository.delete({ - _environmentId: command.environmentId, - _id: command.workflowId, - }); - - const item: NotificationTemplateEntity = ( - await this.notificationTemplateRepository.findDeleted({ - _environmentId: command.environmentId, - _id: command.workflowId, - }) - )?.[0]; - - await this.invalidateCache.invalidateByKey({ - key: buildNotificationTemplateKey({ - _id: item._id, - _environmentId: command.environmentId, - }), - }); - - await this.invalidateCache.invalidateByKey({ - key: buildNotificationTemplateIdentifierKey({ - templateIdentifier: item.triggers[0].identifier, - _environmentId: command.environmentId, - }), - }); - - return true; - } -} diff --git a/apps/api/src/app/bridge/usecases/delete-workflow/index.ts b/apps/api/src/app/bridge/usecases/delete-workflow/index.ts deleted file mode 100644 index 074779cd338..00000000000 --- a/apps/api/src/app/bridge/usecases/delete-workflow/index.ts +++ /dev/null @@ -1,2 +0,0 @@ -export * from './delete-workflow.command'; -export * from './delete-workflow.usecase'; diff --git a/apps/api/src/app/bridge/usecases/index.ts b/apps/api/src/app/bridge/usecases/index.ts index fd739b87184..55f402cd999 100644 --- a/apps/api/src/app/bridge/usecases/index.ts +++ b/apps/api/src/app/bridge/usecases/index.ts @@ -1,15 +1,6 @@ -import { UpsertControlValuesUseCase } from '@novu/application-generic'; -import { DeleteWorkflow } from './delete-workflow'; import { GetBridgeStatus } from './get-bridge-status'; import { PreviewStep } from './preview-step'; import { StoreControlValuesUseCase } from './store-control-values'; import { Sync } from './sync'; -export const USECASES = [ - DeleteWorkflow, - GetBridgeStatus, - PreviewStep, - StoreControlValuesUseCase, - Sync, - UpsertControlValuesUseCase, -]; +export const USECASES = [GetBridgeStatus, PreviewStep, StoreControlValuesUseCase, Sync]; diff --git a/apps/api/src/app/bridge/usecases/sync/sync.usecase.ts b/apps/api/src/app/bridge/usecases/sync/sync.usecase.ts index c57d59149b8..ed9ea7701fc 100644 --- a/apps/api/src/app/bridge/usecases/sync/sync.usecase.ts +++ b/apps/api/src/app/bridge/usecases/sync/sync.usecase.ts @@ -10,7 +10,10 @@ import { AnalyticsService, CreateWorkflow, CreateWorkflowCommand, + DeleteWorkflowCommand, + DeleteWorkflowUseCase, ExecuteBridgeRequest, + InvalidateCacheService, NotificationStep, UpdateWorkflow, UpdateWorkflowCommand, @@ -26,7 +29,6 @@ import { import { DiscoverOutput, DiscoverStepOutput, DiscoverWorkflowOutput, GetActionEnum } from '@novu/framework/internal'; import { SyncCommand } from './sync.command'; -import { DeleteWorkflow, DeleteWorkflowCommand } from '../delete-workflow'; import { CreateBridgeResponseDto } from '../../dtos/create-bridge-response.dto'; @Injectable() @@ -34,12 +36,13 @@ export class Sync { constructor( private createWorkflowUsecase: CreateWorkflow, private updateWorkflowUsecase: UpdateWorkflow, - private deleteWorkflow: DeleteWorkflow, + private deleteWorkflowUseCase: DeleteWorkflowUseCase, private notificationTemplateRepository: NotificationTemplateRepository, private notificationGroupRepository: NotificationGroupRepository, private environmentRepository: EnvironmentRepository, private executeBridgeRequest: ExecuteBridgeRequest, private analyticsService: AnalyticsService, + private invalidateCacheService: InvalidateCacheService, private upsertPreferences: UpsertPreferences ) {} async execute(command: SyncCommand): Promise { @@ -111,21 +114,19 @@ export class Sync { createdWorkflows: NotificationTemplateEntity[] ): Promise { const persistedWorkflowIdsInBridge = createdWorkflows.map((i) => i._id); - const workflowsToDelete = await this.findAllWorkflowsWithOtherIds(command, persistedWorkflowIdsInBridge); - - await Promise.all( - workflowsToDelete?.map((workflow) => { - return this.deleteWorkflow.execute( - DeleteWorkflowCommand.create({ - environmentId: command.environmentId, - organizationId: command.organizationId, - userId: command.userId, - workflowId: workflow._id, - }) - ); - }) + const deleteWorkflowFromStoragePromises = workflowsToDelete.map((workflow) => + this.deleteWorkflowUseCase.execute( + DeleteWorkflowCommand.create({ + environmentId: command.environmentId, + organizationId: command.organizationId, + userId: command.userId, + identifierOrInternalId: workflow._id, + }) + ) ); + + await Promise.all([...deleteWorkflowFromStoragePromises]); } private async findAllWorkflowsWithOtherIds( diff --git a/apps/api/src/app/workflows-v2/usecases/delete-workflow/delete-workflow.command.ts b/apps/api/src/app/workflows-v2/usecases/delete-workflow/delete-workflow.command.ts deleted file mode 100644 index 55195e4968b..00000000000 --- a/apps/api/src/app/workflows-v2/usecases/delete-workflow/delete-workflow.command.ts +++ /dev/null @@ -1,8 +0,0 @@ -import { EnvironmentWithUserObjectCommand } from '@novu/application-generic'; -import { IsDefined, IsString } from 'class-validator'; - -export class DeleteWorkflowCommand extends EnvironmentWithUserObjectCommand { - @IsString() - @IsDefined() - identifierOrInternalId: string; -} diff --git a/apps/api/src/app/workflows-v2/usecases/delete-workflow/delete-workflow.usecase.ts b/apps/api/src/app/workflows-v2/usecases/delete-workflow/delete-workflow.usecase.ts deleted file mode 100644 index 039406adf5d..00000000000 --- a/apps/api/src/app/workflows-v2/usecases/delete-workflow/delete-workflow.usecase.ts +++ /dev/null @@ -1,61 +0,0 @@ -import { Injectable } from '@nestjs/common'; - -import { - ControlValuesRepository, - MessageTemplateRepository, - NotificationTemplateEntity, - NotificationTemplateRepository, -} from '@novu/dal'; - -import { DeleteWorkflowCommand } from './delete-workflow.command'; -import { GetWorkflowByIdsUseCase } from '../get-workflow-by-ids/get-workflow-by-ids.usecase'; -import { GetWorkflowByIdsCommand } from '../get-workflow-by-ids/get-workflow-by-ids.command'; - -@Injectable() -export class DeleteWorkflowUseCase { - constructor( - private notificationTemplateRepository: NotificationTemplateRepository, - private messageTemplateRepository: MessageTemplateRepository, - private getWorkflowByIdsUseCase: GetWorkflowByIdsUseCase, - private controlValuesRepository: ControlValuesRepository - ) {} - - async execute(command: DeleteWorkflowCommand): Promise { - const workflowEntity: NotificationTemplateEntity | null = await this.getWorkflowByIdsUseCase.execute( - GetWorkflowByIdsCommand.create({ - ...command, - identifierOrInternalId: command.identifierOrInternalId, - }) - ); - - await this.deleteRelatedEntities(command, workflowEntity); - } - - private async deleteRelatedEntities(command: DeleteWorkflowCommand, workflow: NotificationTemplateEntity) { - await this.controlValuesRepository.deleteMany({ - _environmentId: command.user.environmentId, - _organizationId: command.user.organizationId, - _workflowId: workflow._id, - }); - await this.removeMessageTemplatesIfNeeded(workflow, command); - await this.notificationTemplateRepository.delete(buildDeleteQuery(command, workflow._id)); - } - - private async removeMessageTemplatesIfNeeded(workflow: NotificationTemplateEntity, command: DeleteWorkflowCommand) { - if (workflow.steps.length > 0) { - for (const step of workflow.steps) { - await this.messageTemplateRepository.deleteById({ - _id: step._templateId, - _environmentId: command.user.environmentId, - }); - } - } - } -} -function buildDeleteQuery(command: DeleteWorkflowCommand, _workflowId: string) { - return { - _id: _workflowId, - _organizationId: command.user.organizationId, - _environmentId: command.user.environmentId, - }; -} diff --git a/apps/api/src/app/workflows-v2/usecases/generate-preview/generate-preview.usecase.ts b/apps/api/src/app/workflows-v2/usecases/generate-preview/generate-preview.usecase.ts index 4781c689add..ce2a0231d82 100644 --- a/apps/api/src/app/workflows-v2/usecases/generate-preview/generate-preview.usecase.ts +++ b/apps/api/src/app/workflows-v2/usecases/generate-preview/generate-preview.usecase.ts @@ -12,10 +12,10 @@ import { } from '@novu/shared'; import { merge } from 'lodash/fp'; import _ = require('lodash'); +import { GetWorkflowByIdsUseCase } from '@novu/application-generic'; import { GeneratePreviewCommand } from './generate-preview-command'; import { PreviewStep, PreviewStepCommand } from '../../../bridge/usecases/preview-step'; import { StepMissingControlsException, StepNotFoundException } from '../../exceptions/step-not-found-exception'; -import { GetWorkflowByIdsUseCase } from '../get-workflow-by-ids/get-workflow-by-ids.usecase'; import { OriginMissingException, StepIdMissingException } from './step-id-missing.exception'; import { BuildDefaultPayloadUseCase } from '../build-payload-from-placeholder'; import { FrameworkPreviousStepsOutputState } from '../../../bridge/usecases/preview-step/preview-step.command'; @@ -129,7 +129,9 @@ export class GeneratePreviewUsecase { private async getWorkflowUserIdentifierFromWorkflowObject(command: GeneratePreviewCommand) { const persistedWorkflow = await this.getWorkflowByIdsUseCase.execute({ identifierOrInternalId: command.workflowId, - user: command.user, + environmentId: command.user.environmentId, + organizationId: command.user.organizationId, + userId: command.user._id, }); const { steps } = persistedWorkflow; const step = steps.find((stepDto) => stepDto._id === command.stepDatabaseId); diff --git a/apps/api/src/app/workflows-v2/usecases/get-step-schema/get-step-data.usecase.ts b/apps/api/src/app/workflows-v2/usecases/get-step-schema/get-step-data.usecase.ts index 3470e42dd9a..c494f1b2e1c 100644 --- a/apps/api/src/app/workflows-v2/usecases/get-step-schema/get-step-data.usecase.ts +++ b/apps/api/src/app/workflows-v2/usecases/get-step-schema/get-step-data.usecase.ts @@ -1,9 +1,10 @@ import { BadRequestException, Injectable } from '@nestjs/common'; import { ControlValuesLevelEnum, JSONSchemaDto, StepDataDto } from '@novu/shared'; import { ControlValuesRepository, NotificationStepEntity, NotificationTemplateEntity } from '@novu/dal'; +import { GetWorkflowByIdsUseCase } from '@novu/application-generic'; + import { GetStepDataCommand } from './get-step-data.command'; import { mapStepTypeToResult } from '../../shared'; -import { GetWorkflowByIdsUseCase } from '../get-workflow-by-ids/get-workflow-by-ids.usecase'; import { InvalidStepException } from '../../exceptions/invalid-step.exception'; import { BuildDefaultPayloadUseCase } from '../build-payload-from-placeholder'; import { buildJSONSchema } from '../../shared/build-string-schema'; @@ -50,7 +51,9 @@ export class GetStepDataUsecase { private async fetchWorkflow(command: GetStepDataCommand) { const workflow = await this.getWorkflowByIdsUseCase.execute({ identifierOrInternalId: command.identifierOrInternalId, - user: command.user, + environmentId: command.user.environmentId, + organizationId: command.user.organizationId, + userId: command.user._id, }); if (!workflow) { diff --git a/apps/api/src/app/workflows-v2/usecases/get-workflow-by-ids/get-workflow-by-ids.usecase.ts b/apps/api/src/app/workflows-v2/usecases/get-workflow-by-ids/get-workflow-by-ids.usecase.ts deleted file mode 100644 index 7ec03fc4c7c..00000000000 --- a/apps/api/src/app/workflows-v2/usecases/get-workflow-by-ids/get-workflow-by-ids.usecase.ts +++ /dev/null @@ -1,34 +0,0 @@ -import { Injectable } from '@nestjs/common'; - -import { NotificationTemplateEntity, NotificationTemplateRepository } from '@novu/dal'; - -import { GetWorkflowByIdsCommand } from './get-workflow-by-ids.command'; -import { WorkflowNotFoundException } from '../../exceptions/workflow-not-found-exception'; - -@Injectable() -export class GetWorkflowByIdsUseCase { - constructor(private notificationTemplateRepository: NotificationTemplateRepository) {} - async execute(command: GetWorkflowByIdsCommand): Promise { - const isInternalId = NotificationTemplateRepository.isInternalId(command.identifierOrInternalId); - - let workflowEntity: NotificationTemplateEntity | null; - - if (isInternalId) { - workflowEntity = await this.notificationTemplateRepository.findById( - command.identifierOrInternalId, - command.user.environmentId - ); - } else { - workflowEntity = await this.notificationTemplateRepository.findByTriggerIdentifier( - command.user.environmentId, - command.identifierOrInternalId - ); - } - - if (!workflowEntity) { - throw new WorkflowNotFoundException(command.identifierOrInternalId); - } - - return workflowEntity; - } -} diff --git a/apps/api/src/app/workflows-v2/usecases/get-workflow/get-workflow.usecase.ts b/apps/api/src/app/workflows-v2/usecases/get-workflow/get-workflow.usecase.ts index ba1835d043d..dbc80d1082c 100644 --- a/apps/api/src/app/workflows-v2/usecases/get-workflow/get-workflow.usecase.ts +++ b/apps/api/src/app/workflows-v2/usecases/get-workflow/get-workflow.usecase.ts @@ -2,11 +2,15 @@ import { Injectable } from '@nestjs/common'; import { NotificationTemplateEntity } from '@novu/dal'; import { WorkflowResponseDto } from '@novu/shared'; -import { GetPreferences, GetPreferencesCommand } from '@novu/application-generic'; +import { + GetPreferences, + GetPreferencesCommand, + GetWorkflowByIdsCommand, + GetWorkflowByIdsUseCase, +} from '@novu/application-generic'; + import { GetWorkflowCommand } from './get-workflow.command'; import { toResponseWorkflowDto } from '../../mappers/notification-template-mapper'; -import { GetWorkflowByIdsUseCase } from '../get-workflow-by-ids/get-workflow-by-ids.usecase'; -import { GetWorkflowByIdsCommand } from '../get-workflow-by-ids/get-workflow-by-ids.command'; @Injectable() export class GetWorkflowUseCase { @@ -15,9 +19,11 @@ export class GetWorkflowUseCase { private getPreferencesUseCase: GetPreferences ) {} async execute(command: GetWorkflowCommand): Promise { - const workflowEntity: NotificationTemplateEntity | null = await this.getWorkflowByIdsUseCase.execute( + const workflowEntity: NotificationTemplateEntity = await this.getWorkflowByIdsUseCase.execute( GetWorkflowByIdsCommand.create({ - ...command, + environmentId: command.user.environmentId, + organizationId: command.user.organizationId, + userId: command.user._id, identifierOrInternalId: command.identifierOrInternalId, }) ); diff --git a/apps/api/src/app/workflows-v2/usecases/sync-to-environment/sync-to-environment.usecase.ts b/apps/api/src/app/workflows-v2/usecases/sync-to-environment/sync-to-environment.usecase.ts index 9205f323ec3..ab766dec611 100644 --- a/apps/api/src/app/workflows-v2/usecases/sync-to-environment/sync-to-environment.usecase.ts +++ b/apps/api/src/app/workflows-v2/usecases/sync-to-environment/sync-to-environment.usecase.ts @@ -13,11 +13,11 @@ import { } from '@novu/shared'; import { PreferencesEntity, PreferencesRepository } from '@novu/dal'; import { SyncToEnvironmentCommand } from './sync-to-environment.command'; -import { GetWorkflowByIdsCommand } from '../get-workflow-by-ids/get-workflow-by-ids.command'; import { UpsertWorkflowUseCase } from '../upsert-workflow/upsert-workflow.usecase'; import { UpsertWorkflowCommand } from '../upsert-workflow/upsert-workflow.command'; import { GetWorkflowUseCase } from '../get-workflow/get-workflow.usecase'; import { GetStepDataUsecase } from '../get-step-schema/get-step-data.usecase'; +import { GetWorkflowCommand } from '../get-workflow/get-workflow.command'; /** * This usecase is used to sync a workflow from one environment to another. @@ -72,7 +72,7 @@ export class SyncToEnvironmentUseCase { private async getWorkflowToClone(command: SyncToEnvironmentCommand): Promise { return this.getWorkflowUseCase.execute( - GetWorkflowByIdsCommand.create({ + GetWorkflowCommand.create({ user: command.user, identifierOrInternalId: command.identifierOrInternalId, }) @@ -85,7 +85,7 @@ export class SyncToEnvironmentUseCase { ): Promise { try { return await this.getWorkflowUseCase.execute( - GetWorkflowByIdsCommand.create({ + GetWorkflowCommand.create({ user: { ...command.user, environmentId: command.targetEnvironmentId }, identifierOrInternalId: externalId, }) diff --git a/apps/api/src/app/workflows-v2/usecases/test-data/test-data.usecase.ts b/apps/api/src/app/workflows-v2/usecases/test-data/test-data.usecase.ts index 2d873c40989..f31df60928e 100644 --- a/apps/api/src/app/workflows-v2/usecases/test-data/test-data.usecase.ts +++ b/apps/api/src/app/workflows-v2/usecases/test-data/test-data.usecase.ts @@ -8,9 +8,8 @@ import { WorkflowTestDataResponseDto, } from '@novu/shared'; +import { GetWorkflowByIdsCommand, GetWorkflowByIdsUseCase } from '@novu/application-generic'; import { WorkflowTestDataCommand } from './test-data.command'; -import { GetWorkflowByIdsUseCase } from '../get-workflow-by-ids/get-workflow-by-ids.usecase'; -import { GetWorkflowByIdsCommand } from '../get-workflow-by-ids/get-workflow-by-ids.command'; import { BuildDefaultPayloadUseCase } from '../build-payload-from-placeholder'; import { buildJSONSchema } from '../../shared/build-string-schema'; @@ -36,7 +35,9 @@ export class WorkflowTestDataUseCase { private async fetchWorkflow(command: WorkflowTestDataCommand): Promise { return await this.getWorkflowByIdsUseCase.execute( GetWorkflowByIdsCommand.create({ - ...command, + environmentId: command.user.environmentId, + organizationId: command.user.organizationId, + userId: command.user._id, identifierOrInternalId: command.identifierOrInternalId, }) ); diff --git a/apps/api/src/app/workflows-v2/usecases/upsert-workflow/upsert-workflow.usecase.ts b/apps/api/src/app/workflows-v2/usecases/upsert-workflow/upsert-workflow.usecase.ts index 0c27b50258b..5a18e6d8080 100644 --- a/apps/api/src/app/workflows-v2/usecases/upsert-workflow/upsert-workflow.usecase.ts +++ b/apps/api/src/app/workflows-v2/usecases/upsert-workflow/upsert-workflow.usecase.ts @@ -22,6 +22,8 @@ import { UpsertPreferences, UpsertUserWorkflowPreferencesCommand, UpsertWorkflowPreferencesCommand, + GetWorkflowByIdsUseCase, + GetWorkflowByIdsCommand, } from '@novu/application-generic'; import { CreateWorkflowDto, @@ -42,8 +44,6 @@ import { import { UpsertWorkflowCommand } from './upsert-workflow.command'; import { StepUpsertMechanismFailedMissingIdException } from '../../exceptions/step-upsert-mechanism-failed-missing-id.exception'; import { toResponseWorkflowDto } from '../../mappers/notification-template-mapper'; -import { GetWorkflowByIdsUseCase } from '../get-workflow-by-ids/get-workflow-by-ids.usecase'; -import { GetWorkflowByIdsCommand } from '../get-workflow-by-ids/get-workflow-by-ids.command'; import { stepTypeToDefaultDashboardControlSchema } from '../../shared'; import { ValidateAndPersistWorkflowIssuesUsecase } from './validate-and-persist-workflow-issues.usecase'; @@ -97,7 +97,9 @@ export class UpsertWorkflowUseCase { return await this.getWorkflowByIdsUseCase.execute( GetWorkflowByIdsCommand.create({ - ...command, + environmentId: command.user.environmentId, + organizationId: command.user.organizationId, + userId: command.user._id, identifierOrInternalId: command.identifierOrInternalId, }) ); diff --git a/apps/api/src/app/workflows-v2/workflow.controller.ts b/apps/api/src/app/workflows-v2/workflow.controller.ts index d50367164b3..7a5d747bd39 100644 --- a/apps/api/src/app/workflows-v2/workflow.controller.ts +++ b/apps/api/src/app/workflows-v2/workflow.controller.ts @@ -28,7 +28,7 @@ import { WorkflowTestDataResponseDto, SyncWorkflowDto, } from '@novu/shared'; -import { UserAuthGuard, UserSession } from '@novu/application-generic'; +import { DeleteWorkflowCommand, DeleteWorkflowUseCase, UserAuthGuard, UserSession } from '@novu/application-generic'; import { ApiCommonResponses } from '../shared/framework/response.decorator'; import { UserAuthentication } from '../shared/framework/swagger/api.key.security'; import { GetWorkflowCommand } from './usecases/get-workflow/get-workflow.command'; @@ -37,8 +37,6 @@ import { UpsertWorkflowCommand } from './usecases/upsert-workflow/upsert-workflo import { GetWorkflowUseCase } from './usecases/get-workflow/get-workflow.usecase'; import { ListWorkflowsUseCase } from './usecases/list-workflows/list-workflow.usecase'; import { ListWorkflowsCommand } from './usecases/list-workflows/list-workflows.command'; -import { DeleteWorkflowUseCase } from './usecases/delete-workflow/delete-workflow.usecase'; -import { DeleteWorkflowCommand } from './usecases/delete-workflow/delete-workflow.command'; import { SyncToEnvironmentUseCase } from './usecases/sync-to-environment/sync-to-environment.usecase'; import { SyncToEnvironmentCommand } from './usecases/sync-to-environment/sync-to-environment.command'; import { GeneratePreviewUsecase } from './usecases/generate-preview/generate-preview.usecase'; @@ -138,7 +136,12 @@ export class WorkflowController { @Param('workflowId', ParseSlugIdPipe) workflowId: IdentifierOrInternalId ) { await this.deleteWorkflowUsecase.execute( - DeleteWorkflowCommand.create({ identifierOrInternalId: workflowId, user }) + DeleteWorkflowCommand.create({ + identifierOrInternalId: workflowId, + environmentId: user.environmentId, + organizationId: user.organizationId, + userId: user._id, + }) ); } diff --git a/apps/api/src/app/workflows-v2/workflow.module.ts b/apps/api/src/app/workflows-v2/workflow.module.ts index f973a3a745f..3895e9ef05a 100644 --- a/apps/api/src/app/workflows-v2/workflow.module.ts +++ b/apps/api/src/app/workflows-v2/workflow.module.ts @@ -5,7 +5,10 @@ import { UpdateWorkflow, UpsertControlValuesUseCase, UpsertPreferences, + DeleteWorkflowUseCase, + GetWorkflowByIdsUseCase, } from '@novu/application-generic'; + import { SharedModule } from '../shared/shared.module'; import { MessageTemplateModule } from '../message-template/message-template.module'; import { ChangeModule } from '../change/change.module'; @@ -14,8 +17,6 @@ import { IntegrationModule } from '../integrations/integrations.module'; import { WorkflowController } from './workflow.controller'; import { UpsertWorkflowUseCase } from './usecases/upsert-workflow/upsert-workflow.usecase'; import { ListWorkflowsUseCase } from './usecases/list-workflows/list-workflow.usecase'; -import { DeleteWorkflowUseCase } from './usecases/delete-workflow/delete-workflow.usecase'; -import { GetWorkflowByIdsUseCase } from './usecases/get-workflow-by-ids/get-workflow-by-ids.usecase'; import { SyncToEnvironmentUseCase } from './usecases/sync-to-environment/sync-to-environment.usecase'; import { BridgeModule } from '../bridge'; import { GeneratePreviewUsecase } from './usecases/generate-preview/generate-preview.usecase'; diff --git a/libs/application-generic/src/usecases/create-workflow/create-workflow.usecase.ts b/libs/application-generic/src/usecases/create-workflow/create-workflow.usecase.ts index 460a85c6e8b..658c455bfb9 100644 --- a/libs/application-generic/src/usecases/create-workflow/create-workflow.usecase.ts +++ b/libs/application-generic/src/usecases/create-workflow/create-workflow.usecase.ts @@ -34,7 +34,12 @@ import { NotificationStepVariantCommand, } from './create-workflow.command'; import { CreateChange, CreateChangeCommand } from '../create-change'; -import { AnalyticsService } from '../../services'; +import { + AnalyticsService, + buildNotificationTemplateIdentifierKey, + buildNotificationTemplateKey, + InvalidateCacheService, +} from '../../services'; import { ContentService } from '../../services/content.service'; import { isVariantEmpty } from '../../utils/variants'; import { @@ -55,6 +60,8 @@ export class CreateWorkflow { @Inject(forwardRef(() => AnalyticsService)) private analyticsService: AnalyticsService, private logger: PinoLogger, + @Inject(forwardRef(() => InvalidateCacheService)) + private invalidateCache: InvalidateCacheService, protected moduleRef: ModuleRef, ) {} @@ -309,6 +316,19 @@ export class CreateWorkflow { ...(command.data ? { data: command.data } : {}), }); + await this.invalidateCache.invalidateByKey({ + key: buildNotificationTemplateIdentifierKey({ + templateIdentifier: savedWorkflow.triggers[0].identifier, + _environmentId: command.environmentId, + }), + }); + await this.invalidateCache.invalidateByKey({ + key: buildNotificationTemplateKey({ + _id: savedWorkflow._id, + _environmentId: command.environmentId, + }), + }); + const item = await this.notificationTemplateRepository.findById( savedWorkflow._id, command.environmentId, diff --git a/libs/application-generic/src/usecases/workflow/delete-workflow/delete-workflow.command.ts b/libs/application-generic/src/usecases/workflow/delete-workflow/delete-workflow.command.ts new file mode 100644 index 00000000000..38edd8a1a41 --- /dev/null +++ b/libs/application-generic/src/usecases/workflow/delete-workflow/delete-workflow.command.ts @@ -0,0 +1,8 @@ +import { IsDefined, IsString } from 'class-validator'; +import { EnvironmentWithUserCommand } from '../../../commands'; + +export class DeleteWorkflowCommand extends EnvironmentWithUserCommand { + @IsString() + @IsDefined() + identifierOrInternalId: string; +} diff --git a/libs/application-generic/src/usecases/workflow/delete-workflow/delete-workflow.usecase.ts b/libs/application-generic/src/usecases/workflow/delete-workflow/delete-workflow.usecase.ts new file mode 100644 index 00000000000..34e0aad53bd --- /dev/null +++ b/libs/application-generic/src/usecases/workflow/delete-workflow/delete-workflow.usecase.ts @@ -0,0 +1,114 @@ +import { Injectable } from '@nestjs/common'; + +import { + ControlValuesRepository, + MessageTemplateRepository, + NotificationTemplateEntity, + NotificationTemplateRepository, + PreferencesRepository, +} from '@novu/dal'; +import { PreferencesTypeEnum, WorkflowOriginEnum } from '@novu/shared'; + +import { DeleteWorkflowCommand } from './delete-workflow.command'; +import { InvalidateCacheService } from '../../../services/cache/invalidate-cache.service'; +import { GetWorkflowByIdsUseCase } from '../get-workflow-by-ids/get-workflow-by-ids.usecase'; +import { GetWorkflowByIdsCommand } from '../get-workflow-by-ids/get-workflow-by-ids.command'; +import { + buildNotificationTemplateIdentifierKey, + buildNotificationTemplateKey, +} from '../../../services/cache/key-builders'; + +@Injectable() +export class DeleteWorkflowUseCase { + constructor( + private notificationTemplateRepository: NotificationTemplateRepository, + private messageTemplateRepository: MessageTemplateRepository, + private getWorkflowByIdsUseCase: GetWorkflowByIdsUseCase, + private preferencesRepository: PreferencesRepository, + private invalidateCache: InvalidateCacheService, + private controlValuesRepository: ControlValuesRepository, + ) {} + + async execute(command: DeleteWorkflowCommand): Promise { + const workflowEntity = await this.getWorkflowByIdsUseCase.execute( + GetWorkflowByIdsCommand.create({ + ...command, + identifierOrInternalId: command.identifierOrInternalId, + }), + ); + + await this.invalidateCacheForWorkflow(workflowEntity, command); + + await this.deleteRelatedEntities(command, workflowEntity); + } + + private async deleteRelatedEntities( + command: DeleteWorkflowCommand, + workflow: NotificationTemplateEntity, + ) { + await this.notificationTemplateRepository.withTransaction(async () => { + await this.controlValuesRepository.deleteMany({ + _environmentId: command.environmentId, + _organizationId: command.organizationId, + _workflowId: workflow._id, + }); + + if (workflow.steps.length > 0) { + for (const step of workflow.steps) { + await this.messageTemplateRepository.deleteById({ + _id: step._templateId, + _environmentId: command.environmentId, + }); + } + } + + if (workflow.origin === WorkflowOriginEnum.EXTERNAL) { + await this.preferencesRepository.delete({ + _environmentId: command.environmentId, + _organizationId: command.organizationId, + _templateId: workflow._id, + type: PreferencesTypeEnum.WORKFLOW_RESOURCE, + }); + } else if (this.isNovuCloud(workflow)) { + await this.preferencesRepository.delete({ + _environmentId: command.environmentId, + _organizationId: command.organizationId, + _templateId: workflow._id, + type: PreferencesTypeEnum.USER_WORKFLOW, + }); + } + + await this.notificationTemplateRepository.delete({ + _id: workflow._id, + _organizationId: command.organizationId, + _environmentId: command.environmentId, + }); + }); + } + + private async invalidateCacheForWorkflow( + workflow: NotificationTemplateEntity, + command: DeleteWorkflowCommand, + ) { + await this.invalidateCache.invalidateByKey({ + key: buildNotificationTemplateKey({ + _id: workflow._id, + _environmentId: command.environmentId, + }), + }); + + await this.invalidateCache.invalidateByKey({ + key: buildNotificationTemplateIdentifierKey({ + templateIdentifier: workflow.triggers[0].identifier, + _environmentId: command.environmentId, + }), + }); + } + + private isNovuCloud(workflow: NotificationTemplateEntity) { + return ( + workflow.origin === WorkflowOriginEnum.NOVU_CLOUD || + workflow.origin === WorkflowOriginEnum.NOVU_CLOUD_V1 + ); + } +} diff --git a/apps/api/src/app/workflows-v2/usecases/get-workflow-by-ids/get-workflow-by-ids.command.ts b/libs/application-generic/src/usecases/workflow/get-workflow-by-ids/get-workflow-by-ids.command.ts similarity index 66% rename from apps/api/src/app/workflows-v2/usecases/get-workflow-by-ids/get-workflow-by-ids.command.ts rename to libs/application-generic/src/usecases/workflow/get-workflow-by-ids/get-workflow-by-ids.command.ts index 1f18e3d03e5..1106bfe96ef 100644 --- a/apps/api/src/app/workflows-v2/usecases/get-workflow-by-ids/get-workflow-by-ids.command.ts +++ b/libs/application-generic/src/usecases/workflow/get-workflow-by-ids/get-workflow-by-ids.command.ts @@ -1,7 +1,7 @@ -import { EnvironmentWithUserObjectCommand } from '@novu/application-generic'; import { IsDefined, IsString } from 'class-validator'; +import { EnvironmentWithUserCommand } from '../../../commands'; -export class GetWorkflowByIdsCommand extends EnvironmentWithUserObjectCommand { +export class GetWorkflowByIdsCommand extends EnvironmentWithUserCommand { @IsString() @IsDefined() identifierOrInternalId: string; diff --git a/libs/application-generic/src/usecases/workflow/get-workflow-by-ids/get-workflow-by-ids.usecase.ts b/libs/application-generic/src/usecases/workflow/get-workflow-by-ids/get-workflow-by-ids.usecase.ts new file mode 100644 index 00000000000..faf9b3b417d --- /dev/null +++ b/libs/application-generic/src/usecases/workflow/get-workflow-by-ids/get-workflow-by-ids.usecase.ts @@ -0,0 +1,46 @@ +import { Injectable, NotFoundException } from '@nestjs/common'; + +import { + NotificationTemplateEntity, + NotificationTemplateRepository, +} from '@novu/dal'; + +import { GetWorkflowByIdsCommand } from './get-workflow-by-ids.command'; + +@Injectable() +export class GetWorkflowByIdsUseCase { + constructor( + private notificationTemplateRepository: NotificationTemplateRepository, + ) {} + async execute( + command: GetWorkflowByIdsCommand, + ): Promise { + const isInternalId = NotificationTemplateRepository.isInternalId( + command.identifierOrInternalId, + ); + + let workflowEntity: NotificationTemplateEntity | null; + + if (isInternalId) { + workflowEntity = await this.notificationTemplateRepository.findById( + command.identifierOrInternalId, + command.environmentId, + ); + } else { + workflowEntity = + await this.notificationTemplateRepository.findByTriggerIdentifier( + command.environmentId, + command.identifierOrInternalId, + ); + } + + if (!workflowEntity) { + throw new NotFoundException({ + message: 'Workflow cannot be found', + workflowId: command.identifierOrInternalId, + }); + } + + return workflowEntity; + } +} diff --git a/libs/application-generic/src/usecases/workflow/index.ts b/libs/application-generic/src/usecases/workflow/index.ts index 98abaece3af..7d7bdbea93f 100644 --- a/libs/application-generic/src/usecases/workflow/index.ts +++ b/libs/application-generic/src/usecases/workflow/index.ts @@ -1,2 +1,6 @@ export * from './update-workflow/update-workflow.usecase'; export * from './update-workflow/update-workflow.command'; +export * from './delete-workflow/delete-workflow.usecase'; +export * from './delete-workflow/delete-workflow.command'; +export * from './get-workflow-by-ids/get-workflow-by-ids.usecase'; +export * from './get-workflow-by-ids/get-workflow-by-ids.command'; diff --git a/libs/dal/src/repositories/base-repository.ts b/libs/dal/src/repositories/base-repository.ts index 76f734f55c2..c6a748f9734 100644 --- a/libs/dal/src/repositories/base-repository.ts +++ b/libs/dal/src/repositories/base-repository.ts @@ -348,6 +348,15 @@ export class BaseRepository { return plainToInstance(this.entity, JSON.parse(JSON.stringify(data))); } + /* + * Note about parallelism in transactions + * + * Running operations in parallel is not supported during a transaction. + * The use of Promise.all, Promise.allSettled, Promise.race, etc. to parallelize operations + * inside a transaction is undefined behaviour and should be avoided. + * + * Refer to https://mongoosejs.com/docs/transactions.html#note-about-parallelism-in-transactions + */ async withTransaction(fn: Parameters[0]) { return (await this._model.db.startSession()).withTransaction(fn); }