diff --git a/.idea/inspectionProfiles/Project_Default.xml b/.idea/inspectionProfiles/Project_Default.xml
index f48722a4d38..f41fd256934 100644
--- a/.idea/inspectionProfiles/Project_Default.xml
+++ b/.idea/inspectionProfiles/Project_Default.xml
@@ -9,4 +9,4 @@
-
\ No newline at end of file
+
diff --git a/.idea/nx-angular-config.xml b/.idea/nx-angular-config.xml
index 977a418d1fe..f7b1f29c249 100644
--- a/.idea/nx-angular-config.xml
+++ b/.idea/nx-angular-config.xml
@@ -5,4 +5,4 @@
-
\ No newline at end of file
+
diff --git a/.idea/prettier.xml b/.idea/prettier.xml
index 60c07b5acc7..3ee746c37a6 100644
--- a/.idea/prettier.xml
+++ b/.idea/prettier.xml
@@ -5,4 +5,4 @@
-
\ No newline at end of file
+
diff --git a/.idea/runConfigurations/API.xml b/.idea/runConfigurations/API.xml
index b920c3c1f8c..8ba789fdc20 100644
--- a/.idea/runConfigurations/API.xml
+++ b/.idea/runConfigurations/API.xml
@@ -9,4 +9,4 @@
-
\ No newline at end of file
+
diff --git a/apps/api/src/.env.test b/apps/api/src/.env.test
index 756290d8681..2efb832184e 100644
--- a/apps/api/src/.env.test
+++ b/apps/api/src/.env.test
@@ -102,7 +102,6 @@ API_RATE_LIMIT_MAXIMUM_FREE_GLOBAL=
API_RATE_LIMIT_MAXIMUM_UNLIMITED_TRIGGER=
API_RATE_LIMIT_MAXIMUM_UNLIMITED_CONFIGURATION=
API_RATE_LIMIT_MAXIMUM_UNLIMITED_GLOBAL=
-
IS_USE_MERGED_DIGEST_ID_ENABLED=true
HUBSPOT_INVITE_NUDGE_EMAIL_USER_LIST_ID=
diff --git a/apps/api/src/app.module.ts b/apps/api/src/app.module.ts
index a170f19b91d..8dbece871b0 100644
--- a/apps/api/src/app.module.ts
+++ b/apps/api/src/app.module.ts
@@ -1,5 +1,5 @@
/* eslint-disable global-require */
-import { DynamicModule, HttpException, Module, Logger, Provider } from '@nestjs/common';
+import { DynamicModule, HttpException, Logger, Module, Provider } from '@nestjs/common';
import { RavenInterceptor, RavenModule } from 'nest-raven';
import { APP_INTERCEPTOR } from '@nestjs/core';
import { Type } from '@nestjs/common/interfaces/type.interface';
@@ -15,7 +15,6 @@ import { HealthModule } from './app/health/health.module';
import { OrganizationModule } from './app/organization/organization.module';
import { EnvironmentsModule } from './app/environments/environments.module';
import { ExecutionDetailsModule } from './app/execution-details/execution-details.module';
-import { WorkflowModule } from './app/workflows/workflow.module';
import { EventsModule } from './app/events/events.module';
import { WidgetsModule } from './app/widgets/widgets.module';
import { NotificationModule } from './app/notifications/notification.module';
@@ -44,6 +43,8 @@ import { InboxModule } from './app/inbox/inbox.module';
import { BridgeModule } from './app/bridge/bridge.module';
import { PreferencesModule } from './app/preferences';
import { StepSchemasModule } from './app/step-schemas/step-schemas.module';
+import { WorkflowModule } from './app/workflows-v2/workflow.module';
+import { WorkflowModuleV1 } from './app/workflows-v1/workflow-v1.module';
const enterpriseImports = (): Array | ForwardReference> => {
const modules: Array | ForwardReference> = [];
@@ -78,7 +79,7 @@ const baseModules: Array | Forward
HealthModule,
EnvironmentsModule,
ExecutionDetailsModule,
- WorkflowModule,
+ WorkflowModuleV1,
EventsModule,
WidgetsModule,
InboxModule,
@@ -105,6 +106,7 @@ const baseModules: Array | Forward
TracingModule.register(packageJson.name, packageJson.version),
BridgeModule,
PreferencesModule,
+ WorkflowModule,
];
const enterpriseModules = enterpriseImports();
diff --git a/apps/api/src/app/blueprint/blueprint.module.ts b/apps/api/src/app/blueprint/blueprint.module.ts
index d4267e7f330..369e101e004 100644
--- a/apps/api/src/app/blueprint/blueprint.module.ts
+++ b/apps/api/src/app/blueprint/blueprint.module.ts
@@ -1,12 +1,12 @@
import { MiddlewareConsumer, Module, NestModule } from '@nestjs/common';
import { SharedModule } from '../shared/shared.module';
-import { WorkflowModule } from '../workflows/workflow.module';
import { USE_CASES } from './usecases';
import { BlueprintController } from './blueprint.controller';
+import { WorkflowModuleV1 } from '../workflows-v1/workflow-v1.module';
@Module({
- imports: [SharedModule, WorkflowModule],
+ imports: [SharedModule, WorkflowModuleV1],
controllers: [BlueprintController],
providers: [...USE_CASES],
exports: [...USE_CASES],
diff --git a/apps/api/src/app/blueprint/e2e/get-blueprints-by-id.e2e.ts b/apps/api/src/app/blueprint/e2e/get-blueprints-by-id.e2e.ts
index 5c8de0fc4cf..234a85b1596 100644
--- a/apps/api/src/app/blueprint/e2e/get-blueprints-by-id.e2e.ts
+++ b/apps/api/src/app/blueprint/e2e/get-blueprints-by-id.e2e.ts
@@ -12,7 +12,7 @@ import {
} from '@novu/shared';
import { GroupedBlueprintResponse } from '../dto/grouped-blueprint.response.dto';
-import { CreateWorkflowRequestDto } from '../../workflows/dto';
+import { CreateWorkflowRequestDto } from '../../workflows-v1/dto';
describe('Get blueprints by id - /blueprints/:templateId (GET)', async () => {
let session: UserSession;
diff --git a/apps/api/src/app/blueprint/e2e/get-grouped-blueprints.e2e.ts b/apps/api/src/app/blueprint/e2e/get-grouped-blueprints.e2e.ts
index 73ce89ac140..94b8e20ba9e 100644
--- a/apps/api/src/app/blueprint/e2e/get-grouped-blueprints.e2e.ts
+++ b/apps/api/src/app/blueprint/e2e/get-grouped-blueprints.e2e.ts
@@ -20,10 +20,10 @@ import {
} from '@novu/application-generic';
import { GroupedBlueprintResponse } from '../dto/grouped-blueprint.response.dto';
-import { CreateWorkflowRequestDto } from '../../workflows/dto';
import { GetGroupedBlueprints, POPULAR_TEMPLATES_ID_LIST } from '../usecases/get-grouped-blueprints';
// eslint-disable-next-line import/no-namespace
import * as blueprintStaticModule from '../usecases/get-grouped-blueprints/consts';
+import { CreateWorkflowRequestDto } from '../../workflows-v1/dto';
describe('Get grouped notification template blueprints - /blueprints/group-by-category (GET)', async () => {
let session: UserSession;
diff --git a/apps/api/src/app/bridge/bridge.controller.ts b/apps/api/src/app/bridge/bridge.controller.ts
index 199d83d4955..4422f578a61 100644
--- a/apps/api/src/app/bridge/bridge.controller.ts
+++ b/apps/api/src/app/bridge/bridge.controller.ts
@@ -14,13 +14,13 @@ import {
UseInterceptors,
} from '@nestjs/common';
-import { UserSessionData, ControlVariablesLevelEnum, WorkflowTypeEnum } from '@novu/shared';
+import { ControlVariablesLevelEnum, UserSessionData, WorkflowTypeEnum } from '@novu/shared';
import { AnalyticsService, ExternalApiAccessible, UserAuthGuard, UserSession } from '@novu/application-generic';
-import { EnvironmentRepository, NotificationTemplateRepository, ControlVariablesRepository } from '@novu/dal';
+import { ControlValuesRepository, EnvironmentRepository, NotificationTemplateRepository } from '@novu/dal';
import { ApiExcludeController } from '@nestjs/swagger';
-import { StoreControlVariables, StoreControlVariablesCommand } from './usecases/store-control-variables';
+import { StoreControlVariablesCommand, StoreControlVariablesUseCase } from './usecases/store-control-variables';
import { PreviewStep, PreviewStepCommand } from './usecases/preview-step';
import { SyncCommand } from './usecases/sync';
import { Sync } from './usecases/sync/sync.usecase';
@@ -40,8 +40,8 @@ export class BridgeController {
private validateBridgeUrlUsecase: GetBridgeStatus,
private environmentRepository: EnvironmentRepository,
private notificationTemplateRepository: NotificationTemplateRepository,
- private controlVariablesRepository: ControlVariablesRepository,
- private storeControlVariables: StoreControlVariables,
+ private controlValuesRepository: ControlValuesRepository,
+ private storeControlVariablesUseCase: StoreControlVariablesUseCase,
private previewStep: PreviewStep,
private analyticsService: AnalyticsService
) {}
@@ -175,7 +175,7 @@ export class BridgeController {
throw new NotFoundException('Workflow not found');
}
- const result = await this.controlVariablesRepository.findOne({
+ const result = await this.controlValuesRepository.findOne({
_environmentId: user.environmentId,
_organizationId: user.organizationId,
_workflowId: workflowExist._id,
@@ -195,7 +195,7 @@ export class BridgeController {
@UserSession() user: UserSessionData,
@Body() body: any
) {
- return this.storeControlVariables.execute(
+ return this.storeControlVariablesUseCase.execute(
StoreControlVariablesCommand.create({
stepId,
workflowId,
diff --git a/apps/api/src/app/bridge/bridge.module.ts b/apps/api/src/app/bridge/bridge.module.ts
index f98181d95f0..fb2632c223d 100644
--- a/apps/api/src/app/bridge/bridge.module.ts
+++ b/apps/api/src/app/bridge/bridge.module.ts
@@ -7,6 +7,7 @@ import {
UpdateChange,
UpdateMessageTemplate,
UpdateWorkflow,
+ UpsertControlValuesUseCase,
UpsertPreferences,
} from '@novu/application-generic';
import { PreferencesRepository } from '@novu/dal';
@@ -24,6 +25,7 @@ const PROVIDERS = [
UpdateChange,
PreferencesRepository,
UpsertPreferences,
+ UpsertControlValuesUseCase,
];
@Module({
diff --git a/apps/api/src/app/bridge/shared/types.ts b/apps/api/src/app/bridge/shared/types.ts
index a27c9e65491..3d631696b93 100644
--- a/apps/api/src/app/bridge/shared/types.ts
+++ b/apps/api/src/app/bridge/shared/types.ts
@@ -21,6 +21,7 @@ export interface IWorkflowDefineStep {
type: StepType;
inputs: IStepControl;
+
controls: IStepControl;
outputs: IStepOutput;
diff --git a/apps/api/src/app/bridge/usecases/index.ts b/apps/api/src/app/bridge/usecases/index.ts
index 0f9b8b45d7e..1ec761cb7ac 100644
--- a/apps/api/src/app/bridge/usecases/index.ts
+++ b/apps/api/src/app/bridge/usecases/index.ts
@@ -1,7 +1,15 @@
+import { UpsertControlValuesUseCase } from '@novu/application-generic';
import { DeleteWorkflow } from './delete-workflow';
import { GetBridgeStatus } from './get-bridge-status';
import { PreviewStep } from './preview-step';
-import { StoreControlVariables } from './store-control-variables';
+import { StoreControlVariablesUseCase } from './store-control-variables';
import { Sync } from './sync';
-export const USECASES = [DeleteWorkflow, GetBridgeStatus, PreviewStep, StoreControlVariables, Sync];
+export const USECASES = [
+ DeleteWorkflow,
+ GetBridgeStatus,
+ PreviewStep,
+ StoreControlVariablesUseCase,
+ Sync,
+ UpsertControlValuesUseCase,
+];
diff --git a/apps/api/src/app/bridge/usecases/store-control-variables/store-control-variables.usecase.ts b/apps/api/src/app/bridge/usecases/store-control-variables/store-control-variables.usecase.ts
index 4597300991a..e018a330635 100644
--- a/apps/api/src/app/bridge/usecases/store-control-variables/store-control-variables.usecase.ts
+++ b/apps/api/src/app/bridge/usecases/store-control-variables/store-control-variables.usecase.ts
@@ -1,33 +1,19 @@
import { Injectable } from '@nestjs/common';
-import _ from 'lodash';
import defaults from 'json-schema-defaults';
-import { NotificationTemplateRepository, ControlVariablesRepository } from '@novu/dal';
-import { ApiException } from '@novu/application-generic';
-import { ControlVariablesLevelEnum } from '@novu/shared';
+import { NotificationTemplateRepository } from '@novu/dal';
+import { JsonSchema } from '@novu/framework';
+import { ApiException, UpsertControlValuesCommand, UpsertControlValuesUseCase } from '@novu/application-generic';
import { StoreControlVariablesCommand } from './store-control-variables.command';
@Injectable()
-export class StoreControlVariables {
+export class StoreControlVariablesUseCase {
constructor(
- private controlVariablesRepository: ControlVariablesRepository,
- private notificationTemplateRepository: NotificationTemplateRepository
+ private notificationTemplateRepository: NotificationTemplateRepository,
+ private upsertControlValuesUseCase: UpsertControlValuesUseCase
) {}
- private difference(object, base) {
- const changes = (objectControl, baseControl) => {
- return _.transform(objectControl, function (result, value, key) {
- if (!_.isEqual(value, base[key])) {
- // eslint-disable-next-line no-param-reassign
- result[key] = _.isObject(value) && _.isObject(baseControl[key]) ? changes(value, baseControl[key]) : value;
- }
- });
- };
-
- return changes(object, base);
- }
-
async execute(command: StoreControlVariablesCommand) {
const workflowExist = await this.notificationTemplateRepository.findByTriggerIdentifier(
command.environmentId,
@@ -40,60 +26,24 @@ export class StoreControlVariables {
const step = workflowExist?.steps.find((item) => item.stepId === command.stepId);
- if (!step) {
+ if (!step || !step._id) {
throw new ApiException('Step not found');
}
- const stepDefault = defaults(
+ const stepDefaultControls = defaults(
(step.template as any)?.controls?.schema || (step.template as any)?.inputs?.schema,
{}
+ ) as JsonSchema;
+
+ return await this.upsertControlValuesUseCase.execute(
+ UpsertControlValuesCommand.create({
+ organizationId: command.organizationId,
+ environmentId: command.environmentId,
+ notificationStepEntity: step,
+ workflowId: workflowExist._id,
+ newControlValues: command.variables,
+ controlSchemas: { schema: stepDefaultControls },
+ })
);
-
- const variables = this.difference(command.variables, stepDefault);
-
- const found = await this.controlVariablesRepository.findOne({
- _environmentId: command.environmentId,
- _workflowId: workflowExist._id,
- level: ControlVariablesLevelEnum.STEP_CONTROLS,
- priority: 0,
- stepId: command.stepId,
- });
-
- if (found) {
- await this.controlVariablesRepository.update(
- {
- _id: found._id,
- _organizationId: command.organizationId,
- _environmentId: command.environmentId,
- },
- {
- level: ControlVariablesLevelEnum.STEP_CONTROLS,
- priority: 0,
- inputs: variables,
- controls: variables,
- workflowId: command.workflowId,
- stepId: command.stepId,
- }
- );
-
- return this.controlVariablesRepository.findOne({
- _id: found._id,
- _organizationId: command.organizationId,
- _environmentId: command.environmentId,
- });
- }
-
- return await this.controlVariablesRepository.create({
- _organizationId: command.organizationId,
- _environmentId: command.environmentId,
- _workflowId: workflowExist._id,
- _stepId: step._id,
- level: ControlVariablesLevelEnum.STEP_CONTROLS,
- priority: 0,
- inputs: variables,
- controls: variables,
- workflowId: command.workflowId,
- stepId: command.stepId,
- });
}
}
diff --git a/apps/api/src/app/bridge/usecases/sync/sync.command.ts b/apps/api/src/app/bridge/usecases/sync/sync.command.ts
index 28a1d583719..4372651021f 100644
--- a/apps/api/src/app/bridge/usecases/sync/sync.command.ts
+++ b/apps/api/src/app/bridge/usecases/sync/sync.command.ts
@@ -1,8 +1,8 @@
-import { IsString, ValidateNested } from 'class-validator';
+import { IsDefined, IsOptional, IsString, ValidateNested } from 'class-validator';
import { Type } from 'class-transformer';
import { EnvironmentWithUserCommand, IStepControl } from '@novu/application-generic';
-import { NotificationTemplateCustomData, IPreferenceChannels, StepType } from '@novu/shared';
+import { IPreferenceChannels, NotificationTemplateCustomData, StepType } from '@novu/shared';
import { IStepOutput, IWorkflowDefineStep } from '../../shared';
@@ -103,8 +103,16 @@ export interface ICreateBridges {
}
export class SyncCommand extends EnvironmentWithUserCommand implements ICreateBridges {
+ @IsOptional()
+ @ValidateNested({ each: true })
+ @Type(() => WorkflowDefine)
workflows?: WorkflowDefine[];
+
+ @IsString()
+ @IsDefined()
bridgeUrl: string;
+ @IsOptional()
+ @IsString()
source?: string;
}
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 e68659ed86e..1f4a087e86a 100644
--- a/apps/api/src/app/bridge/usecases/sync/sync.usecase.ts
+++ b/apps/api/src/app/bridge/usecases/sync/sync.usecase.ts
@@ -1,25 +1,25 @@
import { BadRequestException, Injectable } from '@nestjs/common';
import {
- NotificationTemplateRepository,
EnvironmentRepository,
NotificationGroupRepository,
NotificationTemplateEntity,
+ NotificationTemplateRepository,
} from '@novu/dal';
import {
AnalyticsService,
CreateWorkflow,
CreateWorkflowCommand,
+ ExecuteBridgeRequest,
+ GetFeatureFlag,
+ GetFeatureFlagCommand,
NotificationStep,
UpdateWorkflow,
UpdateWorkflowCommand,
- ExecuteBridgeRequest,
UpsertPreferences,
UpsertWorkflowPreferencesCommand,
- GetFeatureFlag,
- GetFeatureFlagCommand,
} from '@novu/application-generic';
-import { FeatureFlagsKeysEnum, WorkflowTypeEnum } from '@novu/shared';
+import { FeatureFlagsKeysEnum, WorkflowCreationSourceEnum, WorkflowOriginEnum, WorkflowTypeEnum } from '@novu/shared';
import { DiscoverOutput, DiscoverStepOutput, DiscoverWorkflowOutput, GetActionEnum } from '@novu/framework';
import { SyncCommand } from './sync.command';
@@ -175,13 +175,14 @@ export class Sync {
savedWorkflow = await this.createWorkflowUsecase.execute(
CreateWorkflowCommand.create({
+ origin: WorkflowOriginEnum.EXTERNAL,
notificationGroupId,
draft: !isWorkflowActive,
environmentId: command.environmentId,
organizationId: command.organizationId,
userId: command.userId,
name: workflow.workflowId,
- __source: 'bridge',
+ __source: WorkflowCreationSourceEnum.BRIDGE,
type: WorkflowTypeEnum.BRIDGE,
steps: this.mapSteps(workflow.steps),
/** @deprecated */
diff --git a/apps/api/src/app/change/e2e/get-changes.e2e.ts b/apps/api/src/app/change/e2e/get-changes.e2e.ts
index 7406192ba12..9de6949bb70 100644
--- a/apps/api/src/app/change/e2e/get-changes.e2e.ts
+++ b/apps/api/src/app/change/e2e/get-changes.e2e.ts
@@ -8,8 +8,7 @@ import {
FieldOperatorEnum,
} from '@novu/shared';
import { UserSession } from '@novu/testing';
-
-import { CreateWorkflowRequestDto, UpdateWorkflowRequestDto } from '../../workflows/dto';
+import { CreateWorkflowRequestDto, UpdateWorkflowRequestDto } from '../../workflows-v1/dto';
describe('Get changes', () => {
let session: UserSession;
@@ -58,9 +57,9 @@ describe('Get changes', () => {
await session.applyChanges();
const updateData: UpdateWorkflowRequestDto = {
- name: testTemplate.name,
- tags: testTemplate.tags,
- description: testTemplate.description,
+ name: testTemplate.name || '',
+ tags: testTemplate.tags || [],
+ description: testTemplate.description || '',
steps: [],
notificationGroupId: session.notificationGroups[0]._id,
};
diff --git a/apps/api/src/app/change/e2e/promote-changes.e2e.ts b/apps/api/src/app/change/e2e/promote-changes.e2e.ts
index 0c7f5b029b8..7bc134b2456 100644
--- a/apps/api/src/app/change/e2e/promote-changes.e2e.ts
+++ b/apps/api/src/app/change/e2e/promote-changes.e2e.ts
@@ -20,8 +20,7 @@ import {
TemplateVariableTypeEnum,
} from '@novu/shared';
import { UserSession } from '@novu/testing';
-
-import { CreateWorkflowRequestDto, UpdateWorkflowRequestDto } from '../../workflows/dto';
+import { CreateWorkflowRequestDto, UpdateWorkflowRequestDto } from '../../workflows-v1/dto';
describe('Promote changes', () => {
let session: UserSession;
@@ -224,9 +223,9 @@ describe('Promote changes', () => {
let { body } = await session.testAgent.post(`/v1/workflows`).send(testTemplate);
const updateData: UpdateWorkflowRequestDto = {
- name: testTemplate.name,
- tags: testTemplate.tags,
- description: testTemplate.description,
+ name: testTemplate.name || '',
+ tags: testTemplate.tags || [],
+ description: testTemplate.description || '',
steps: [],
notificationGroupId: session.notificationGroups[0]._id,
};
diff --git a/apps/api/src/app/change/e2e/promote-layout-changes.e2e.ts b/apps/api/src/app/change/e2e/promote-layout-changes.e2e.ts
index 7806ac6a337..155ec91014a 100644
--- a/apps/api/src/app/change/e2e/promote-layout-changes.e2e.ts
+++ b/apps/api/src/app/change/e2e/promote-layout-changes.e2e.ts
@@ -78,26 +78,27 @@ describe('Promote Layout Changes', () => {
});
const prodEnv = await getProductionEnvironment();
+ expect(prodEnv).to.be.ok;
const prodLayout = await layoutRepository.findOne({
- _environmentId: prodEnv._id,
+ _environmentId: prodEnv?._id!,
_parentId: layoutId,
});
expect(prodLayout).to.be.ok;
- expect(prodLayout._parentId).to.eql(devLayout._id);
- expect(prodLayout._environmentId).to.eql(prodEnv._id);
- expect(prodLayout._organizationId).to.eql(session.organization._id);
- expect(prodLayout._creatorId).to.eql(session.user._id);
- expect(prodLayout.name).to.eql(layoutName);
- expect(prodLayout.identifier).to.eql(layoutIdentifier);
- expect(prodLayout.content).to.eql(content);
+ expect(prodLayout?._parentId).to.eql(devLayout._id);
+ expect(prodLayout?._environmentId).to.eql(prodEnv?._id);
+ expect(prodLayout?._organizationId).to.eql(session.organization._id);
+ expect(prodLayout?._creatorId).to.eql(session.user._id);
+ expect(prodLayout?.name).to.eql(layoutName);
+ expect(prodLayout?.identifier).to.eql(layoutIdentifier);
+ expect(prodLayout?.content).to.eql(content);
// TODO: Awful but it comes from the repository directly.
- const { _id: _, ...prodVariables } = prodLayout.variables?.[0] as any;
+ const { _id: _, ...prodVariables } = prodLayout?.variables?.[0] as any;
expect(prodVariables).to.deep.include(variables[0]);
- expect(prodLayout.contentType).to.eql(devLayout.contentType);
- expect(prodLayout.isDefault).to.eql(isDefault);
- expect(prodLayout.channel).to.eql(devLayout.channel);
+ expect(prodLayout?.contentType).to.eql(devLayout.contentType);
+ expect(prodLayout?.isDefault).to.eql(isDefault);
+ expect(prodLayout?.channel).to.eql(devLayout.channel);
});
it('should promote the updates done to a layout existing to production', async () => {
@@ -212,26 +213,27 @@ describe('Promote Layout Changes', () => {
});
const prodEnv = await getProductionEnvironment();
+ expect(prodEnv).to.be.ok;
const prodLayout = await layoutRepository.findOne({
- _environmentId: prodEnv._id,
+ _environmentId: prodEnv?._id!,
_parentId: layoutId,
});
expect(prodLayout).to.be.ok;
- expect(prodLayout._parentId).to.eql(patchedLayout._id);
- expect(prodLayout._environmentId).to.eql(prodEnv._id);
- expect(prodLayout._organizationId).to.eql(session.organization._id);
- expect(prodLayout._creatorId).to.eql(session.user._id);
- expect(prodLayout.name).to.eql(updatedLayoutName);
- expect(prodLayout.identifier).to.eql(updatedLayoutIdentifier);
- expect(prodLayout.content).to.eql(updatedContent);
+ expect(prodLayout?._parentId).to.eql(patchedLayout._id);
+ expect(prodLayout?._environmentId).to.eql(prodEnv?._id!);
+ expect(prodLayout?._organizationId).to.eql(session.organization._id);
+ expect(prodLayout?._creatorId).to.eql(session.user._id);
+ expect(prodLayout?.name).to.eql(updatedLayoutName);
+ expect(prodLayout?.identifier).to.eql(updatedLayoutIdentifier);
+ expect(prodLayout?.content).to.eql(updatedContent);
// TODO: Awful but it comes from the repository directly.
- const { _id, ...prodVariables } = prodLayout.variables?.[0] as any;
+ const { _id, ...prodVariables } = prodLayout?.variables?.[0] as any;
expect(prodVariables).to.deep.include(updatedVariables[0]);
- expect(prodLayout.contentType).to.eql(patchedLayout.contentType);
- expect(prodLayout.isDefault).to.eql(updatedIsDefault);
- expect(prodLayout.channel).to.eql(patchedLayout.channel);
+ expect(prodLayout?.contentType).to.eql(patchedLayout.contentType);
+ expect(prodLayout?.isDefault).to.eql(updatedIsDefault);
+ expect(prodLayout?.channel).to.eql(patchedLayout.channel);
});
it('should promote the deletion of a layout to production', async () => {
@@ -315,9 +317,10 @@ describe('Promote Layout Changes', () => {
});
const prodEnv = await getProductionEnvironment();
+ expect(prodEnv).to.be.ok;
const prodLayout = await layoutRepository.findOne({
- _environmentId: prodEnv._id,
+ _environmentId: prodEnv?._id!,
_parentId: layoutId,
});
diff --git a/apps/api/src/app/environments/e2e/guard-check.e2e.ts b/apps/api/src/app/environments/e2e/guard-check.e2e.ts
index 5452337baca..dc1ae774254 100644
--- a/apps/api/src/app/environments/e2e/guard-check.e2e.ts
+++ b/apps/api/src/app/environments/e2e/guard-check.e2e.ts
@@ -1,8 +1,7 @@
import { UserSession } from '@novu/testing';
import { expect } from 'chai';
import { NotificationTemplateRepository } from '@novu/dal';
-
-import { CreateWorkflowRequestDto } from '../../workflows/dto/create-workflow.request.dto';
+import { CreateWorkflowRequestDto } from '../../workflows-v1/dto';
describe('Environment - Check Root Environment Guard', async () => {
let session: UserSession;
diff --git a/apps/api/src/app/feeds/e2e/create-feed.e2e.ts b/apps/api/src/app/feeds/e2e/create-feed.e2e.ts
index 0aa83fd80e1..f4350cc8f64 100644
--- a/apps/api/src/app/feeds/e2e/create-feed.e2e.ts
+++ b/apps/api/src/app/feeds/e2e/create-feed.e2e.ts
@@ -2,7 +2,7 @@ import { expect } from 'chai';
import { UserSession } from '@novu/testing';
import { StepTypeEnum } from '@novu/shared';
import { FeedRepository } from '@novu/dal';
-import { CreateWorkflowRequestDto, UpdateWorkflowRequestDto } from '../../workflows/dto';
+import { CreateWorkflowRequestDto, UpdateWorkflowRequestDto } from '../../workflows-v1/dto';
describe('Create A Feed - /feeds (POST)', async () => {
let session: UserSession;
diff --git a/apps/api/src/app/feeds/e2e/delete-feed.e2e.ts b/apps/api/src/app/feeds/e2e/delete-feed.e2e.ts
index 677e52b4aae..53ef8c00d7b 100644
--- a/apps/api/src/app/feeds/e2e/delete-feed.e2e.ts
+++ b/apps/api/src/app/feeds/e2e/delete-feed.e2e.ts
@@ -2,7 +2,7 @@ import { expect } from 'chai';
import { UserSession, NotificationTemplateService } from '@novu/testing';
import { StepTypeEnum } from '@novu/shared';
import { FeedRepository, MessageTemplateRepository, NotificationTemplateRepository } from '@novu/dal';
-import { CreateWorkflowRequestDto } from '../../workflows/dto';
+import { CreateWorkflowRequestDto } from '../../workflows-v1/dto';
describe('Delete A Feed - /feeds (POST)', async () => {
let session: UserSession;
@@ -49,7 +49,8 @@ describe('Delete A Feed - /feeds (POST)', async () => {
_id: newFeedId,
});
- expect(feed.name).to.equal(`Test name`);
+ expect(feed).to.be.ok;
+ expect(feed?.name).to.equal(`Test name`);
const { body: deletedBody } = await session.testAgent.delete(`/v1/feeds/${newFeedId}`).send();
expect(deletedBody.data).to.be.ok;
diff --git a/apps/api/src/app/inbox/inbox.controller.ts b/apps/api/src/app/inbox/inbox.controller.ts
index 50f888cfe52..85d94b295db 100644
--- a/apps/api/src/app/inbox/inbox.controller.ts
+++ b/apps/api/src/app/inbox/inbox.controller.ts
@@ -1,4 +1,4 @@
-import { Body, Controller, Get, HttpCode, HttpStatus, Post, Patch, Query, UseGuards, Param } from '@nestjs/common';
+import { Body, Controller, Get, HttpCode, HttpStatus, Param, Patch, Post, Query, UseGuards } from '@nestjs/common';
import { ApiExcludeController } from '@nestjs/swagger';
import { AuthGuard } from '@nestjs/passport';
import { SubscriberEntity } from '@novu/dal';
diff --git a/apps/api/src/app/partner-integrations/usecases/process-vercel-webhook/process-vercel-webhook.usecase.ts b/apps/api/src/app/partner-integrations/usecases/process-vercel-webhook/process-vercel-webhook.usecase.ts
index 5ef8dbdc473..6ba5ac8eec8 100644
--- a/apps/api/src/app/partner-integrations/usecases/process-vercel-webhook/process-vercel-webhook.usecase.ts
+++ b/apps/api/src/app/partner-integrations/usecases/process-vercel-webhook/process-vercel-webhook.usecase.ts
@@ -1,12 +1,14 @@
import { BadRequestException, Injectable, Logger } from '@nestjs/common';
+import crypto from 'node:crypto';
+
import {
- EnvironmentRepository,
- EnvironmentEntity,
CommunityOrganizationRepository,
- MemberRepository,
CommunityUserRepository,
+ EnvironmentEntity,
+ EnvironmentRepository,
+ MemberRepository,
} from '@novu/dal';
-import crypto from 'node:crypto';
+
import { ApiException } from '../../../shared/exceptions/api.exception';
import { ProcessVercelWebhookCommand } from './process-vercel-webhook.command';
import { Sync } from '../../../bridge/usecases/sync';
diff --git a/apps/api/src/app/preferences/preferences.controller.ts b/apps/api/src/app/preferences/preferences.controller.ts
index 5f2cc322923..24f6a54a3f8 100644
--- a/apps/api/src/app/preferences/preferences.controller.ts
+++ b/apps/api/src/app/preferences/preferences.controller.ts
@@ -16,9 +16,9 @@ import {
GetPreferences,
GetPreferencesCommand,
UpsertPreferences,
+ UpsertUserWorkflowPreferencesCommand,
UserAuthGuard,
UserSession,
- UpsertUserWorkflowPreferencesCommand,
} from '@novu/application-generic';
import { FeatureFlagsKeysEnum, UserSessionData } from '@novu/shared';
import { ApiExcludeController } from '@nestjs/swagger';
diff --git a/apps/api/src/app/shared/shared.module.ts b/apps/api/src/app/shared/shared.module.ts
index 593d50bffa8..2ac8cfcfd55 100644
--- a/apps/api/src/app/shared/shared.module.ts
+++ b/apps/api/src/app/shared/shared.module.ts
@@ -2,7 +2,7 @@
import { Module } from '@nestjs/common';
import {
ChangeRepository,
- ControlVariablesRepository,
+ ControlValuesRepository,
DalService,
EnvironmentRepository,
ExecutionDetailsRepository,
@@ -18,6 +18,7 @@ import {
NotificationRepository,
NotificationTemplateRepository,
OrganizationRepository,
+ PreferencesRepository,
SubscriberPreferenceRepository,
SubscriberRepository,
TenantRepository,
@@ -31,22 +32,22 @@ import {
cacheService,
CacheServiceHealthIndicator,
ComputeJobWaitDurationService,
+ CreateExecutionDetails,
createNestLoggingModuleOptions,
DalServiceHealthIndicator,
distributedLockService,
+ ExecuteBridgeRequest,
+ ExecutionLogRoute,
featureFlagsService,
getFeatureFlag,
+ injectCommunityAuthProviders,
InvalidateCacheService,
LoggerModule,
QueuesModule,
storageService,
- ExecutionLogRoute,
- CreateExecutionDetails,
- injectCommunityAuthProviders,
- ExecuteBridgeRequest,
} from '@novu/application-generic';
-import { JobTopicNameEnum, isClerkEnabled } from '@novu/shared';
+import { isClerkEnabled, JobTopicNameEnum } from '@novu/shared';
import packageJson from '../../../package.json';
function getDynamicAuthProviders() {
@@ -82,7 +83,8 @@ const DAL_MODELS = [
TopicSubscribersRepository,
TenantRepository,
WorkflowOverrideRepository,
- ControlVariablesRepository,
+ ControlValuesRepository,
+ PreferencesRepository,
...getDynamicAuthProviders(),
];
diff --git a/apps/api/src/app/widgets/dtos/message-response.dto.ts b/apps/api/src/app/widgets/dtos/message-response.dto.ts
index db44cf79fc3..f9abaddca12 100644
--- a/apps/api/src/app/widgets/dtos/message-response.dto.ts
+++ b/apps/api/src/app/widgets/dtos/message-response.dto.ts
@@ -4,14 +4,14 @@ import {
ChannelCTATypeEnum,
ChannelTypeEnum,
EmailBlockTypeEnum,
- MessageActionStatusEnum,
- TextAlignEnum,
IMessage,
- IMessageCTA,
IMessageAction,
+ IMessageCTA,
+ MessageActionStatusEnum,
+ TextAlignEnum,
} from '@novu/shared';
import { SubscriberResponseDto } from '../../subscribers/dtos';
-import { WorkflowResponse } from '../../workflows/dto/workflow-response.dto';
+import { WorkflowResponse } from '../../workflows-v1/dto/workflow-response.dto';
class EmailBlockStyles {
@ApiProperty({
diff --git a/apps/api/src/app/workflows/dto/change-workflow-status-request.dto.ts b/apps/api/src/app/workflows-v1/dto/change-workflow-status-request.dto.ts
similarity index 79%
rename from apps/api/src/app/workflows/dto/change-workflow-status-request.dto.ts
rename to apps/api/src/app/workflows-v1/dto/change-workflow-status-request.dto.ts
index 072178d59ae..870430bb420 100644
--- a/apps/api/src/app/workflows/dto/change-workflow-status-request.dto.ts
+++ b/apps/api/src/app/workflows-v1/dto/change-workflow-status-request.dto.ts
@@ -1,6 +1,9 @@
import { ApiProperty } from '@nestjs/swagger';
import { IsBoolean, IsDefined } from 'class-validator';
+/**
+ * @deprecated use dto's in /workflows directory
+ */
export class ChangeWorkflowStatusRequestDto {
@ApiProperty()
@IsDefined()
diff --git a/apps/api/src/app/workflows/dto/create-workflow.request.dto.ts b/apps/api/src/app/workflows-v1/dto/create-workflow.request.dto.ts
similarity index 96%
rename from apps/api/src/app/workflows/dto/create-workflow.request.dto.ts
rename to apps/api/src/app/workflows-v1/dto/create-workflow.request.dto.ts
index dcd7885392b..d95cc9a8abb 100644
--- a/apps/api/src/app/workflows/dto/create-workflow.request.dto.ts
+++ b/apps/api/src/app/workflows-v1/dto/create-workflow.request.dto.ts
@@ -10,6 +10,10 @@ import {
import { PreferenceChannels } from '../../shared/dtos/preference-channels';
import { NotificationStep } from '../../shared/dtos/notification-step';
+/**
+ * @deprecated use dto's in /workflows directory
+ */
+
export class CreateWorkflowRequestDto implements ICreateWorkflowDto {
@ApiProperty()
@IsString()
diff --git a/apps/api/src/app/workflows/dto/index.ts b/apps/api/src/app/workflows-v1/dto/index.ts
similarity index 100%
rename from apps/api/src/app/workflows/dto/index.ts
rename to apps/api/src/app/workflows-v1/dto/index.ts
diff --git a/apps/api/src/app/workflows/dto/update-workflow-request.dto.ts b/apps/api/src/app/workflows-v1/dto/update-workflow-request.dto.ts
similarity index 95%
rename from apps/api/src/app/workflows/dto/update-workflow-request.dto.ts
rename to apps/api/src/app/workflows-v1/dto/update-workflow-request.dto.ts
index 2cb4a9205d8..690e4ff07aa 100644
--- a/apps/api/src/app/workflows/dto/update-workflow-request.dto.ts
+++ b/apps/api/src/app/workflows-v1/dto/update-workflow-request.dto.ts
@@ -4,6 +4,10 @@ import { ApiProperty, ApiPropertyOptional } from '@nestjs/swagger';
import { PreferenceChannels } from '../../shared/dtos/preference-channels';
import { NotificationStep } from '../../shared/dtos/notification-step';
+/**
+ * @deprecated use dto's in /workflows directory
+ */
+
export class UpdateWorkflowRequestDto implements IUpdateWorkflowDto {
@ApiProperty()
@IsString()
diff --git a/apps/api/src/app/workflows/dto/variables.response.dto.ts b/apps/api/src/app/workflows-v1/dto/variables.response.dto.ts
similarity index 76%
rename from apps/api/src/app/workflows/dto/variables.response.dto.ts
rename to apps/api/src/app/workflows-v1/dto/variables.response.dto.ts
index ef1e551269c..bb4f037363f 100644
--- a/apps/api/src/app/workflows/dto/variables.response.dto.ts
+++ b/apps/api/src/app/workflows-v1/dto/variables.response.dto.ts
@@ -1,5 +1,9 @@
import { ApiProperty } from '@nestjs/swagger';
+/**
+ * @deprecated use dto's in /workflows directory
+ */
+
export class VariablesResponseDto {
@ApiProperty()
translations: Record;
diff --git a/apps/api/src/app/workflows/dto/workflow-response.dto.ts b/apps/api/src/app/workflows-v1/dto/workflow-response.dto.ts
similarity index 97%
rename from apps/api/src/app/workflows/dto/workflow-response.dto.ts
rename to apps/api/src/app/workflows-v1/dto/workflow-response.dto.ts
index e5b970a2ac5..eab52bb9674 100644
--- a/apps/api/src/app/workflows/dto/workflow-response.dto.ts
+++ b/apps/api/src/app/workflows-v1/dto/workflow-response.dto.ts
@@ -11,6 +11,10 @@ import {
import { NotificationStep } from '../../shared/dtos/notification-step';
import { PreferenceChannels } from '../../shared/dtos/preference-channels';
+/**
+ * @deprecated use dto's in /workflows directory
+ */
+
export class NotificationGroup {
@ApiPropertyOptional()
_id?: string;
diff --git a/apps/api/src/app/workflows/dto/workflows-request.dto.ts b/apps/api/src/app/workflows-v1/dto/workflows-request.dto.ts
similarity index 85%
rename from apps/api/src/app/workflows/dto/workflows-request.dto.ts
rename to apps/api/src/app/workflows-v1/dto/workflows-request.dto.ts
index 2258ab4cf71..e85bc68a9e3 100644
--- a/apps/api/src/app/workflows/dto/workflows-request.dto.ts
+++ b/apps/api/src/app/workflows-v1/dto/workflows-request.dto.ts
@@ -1,5 +1,9 @@
import { PaginationWithFiltersRequestDto } from '../../shared/dtos/pagination-with-filters-request';
+/**
+ * @deprecated use dto's in /workflows directory
+ */
+
export class WorkflowsRequestDto extends PaginationWithFiltersRequestDto({
defaultLimit: 10,
maxLimit: 100,
diff --git a/apps/api/src/app/workflows/dto/workflows.response.dto.ts b/apps/api/src/app/workflows-v1/dto/workflows.response.dto.ts
similarity index 83%
rename from apps/api/src/app/workflows/dto/workflows.response.dto.ts
rename to apps/api/src/app/workflows-v1/dto/workflows.response.dto.ts
index 956112607d7..d1cf7cb42e1 100644
--- a/apps/api/src/app/workflows/dto/workflows.response.dto.ts
+++ b/apps/api/src/app/workflows-v1/dto/workflows.response.dto.ts
@@ -1,6 +1,10 @@
import { ApiProperty } from '@nestjs/swagger';
import { WorkflowResponse } from './workflow-response.dto';
+/**
+ * @deprecated use dto's in /workflows directory
+ */
+
export class WorkflowsResponseDto {
@ApiProperty()
totalCount: number;
diff --git a/apps/api/src/app/workflows/e2e/change-template-status.e2e.ts b/apps/api/src/app/workflows-v1/e2e/change-template-status.e2e.ts
similarity index 100%
rename from apps/api/src/app/workflows/e2e/change-template-status.e2e.ts
rename to apps/api/src/app/workflows-v1/e2e/change-template-status.e2e.ts
diff --git a/apps/api/src/app/workflows/e2e/create-notification-templates.e2e.ts b/apps/api/src/app/workflows-v1/e2e/create-notification-templates.e2e.ts
similarity index 100%
rename from apps/api/src/app/workflows/e2e/create-notification-templates.e2e.ts
rename to apps/api/src/app/workflows-v1/e2e/create-notification-templates.e2e.ts
diff --git a/apps/api/src/app/workflows/e2e/delete-notification-template.e2e.ts b/apps/api/src/app/workflows-v1/e2e/delete-notification-template.e2e.ts
similarity index 100%
rename from apps/api/src/app/workflows/e2e/delete-notification-template.e2e.ts
rename to apps/api/src/app/workflows-v1/e2e/delete-notification-template.e2e.ts
diff --git a/apps/api/src/app/workflows/e2e/get-notification-template.e2e.ts b/apps/api/src/app/workflows-v1/e2e/get-notification-template.e2e.ts
similarity index 100%
rename from apps/api/src/app/workflows/e2e/get-notification-template.e2e.ts
rename to apps/api/src/app/workflows-v1/e2e/get-notification-template.e2e.ts
diff --git a/apps/api/src/app/workflows/e2e/get-notification-templates.e2e.ts b/apps/api/src/app/workflows-v1/e2e/get-notification-templates.e2e.ts
similarity index 100%
rename from apps/api/src/app/workflows/e2e/get-notification-templates.e2e.ts
rename to apps/api/src/app/workflows-v1/e2e/get-notification-templates.e2e.ts
diff --git a/apps/api/src/app/workflows/e2e/update-notification-template.e2e.ts b/apps/api/src/app/workflows-v1/e2e/update-notification-template.e2e.ts
similarity index 100%
rename from apps/api/src/app/workflows/e2e/update-notification-template.e2e.ts
rename to apps/api/src/app/workflows-v1/e2e/update-notification-template.e2e.ts
diff --git a/apps/api/src/app/workflows/notification-template.controller.ts b/apps/api/src/app/workflows-v1/notification-template.controller.ts
similarity index 97%
rename from apps/api/src/app/workflows/notification-template.controller.ts
rename to apps/api/src/app/workflows-v1/notification-template.controller.ts
index c872204a2fb..8a1a03aa14c 100644
--- a/apps/api/src/app/workflows/notification-template.controller.ts
+++ b/apps/api/src/app/workflows-v1/notification-template.controller.ts
@@ -11,7 +11,7 @@ import {
UseGuards,
UseInterceptors,
} from '@nestjs/common';
-import { UserSessionData, WorkflowTypeEnum } from '@novu/shared';
+import { UserSessionData, WorkflowOriginEnum, WorkflowTypeEnum } from '@novu/shared';
import {
CreateWorkflow,
CreateWorkflowCommand,
@@ -40,6 +40,10 @@ import { CreateWorkflowQuery } from './queries';
import { DeleteNotificationTemplateCommand } from './usecases/delete-notification-template/delete-notification-template.command';
import { UserAuthentication } from '../shared/framework/swagger/api.key.security';
+/**
+ * @deprecated use controller in /workflows directory
+ */
+
@ApiCommonResponses()
@ApiExcludeController()
@Controller('/notification-templates')
@@ -191,6 +195,7 @@ export class NotificationTemplateController {
data: body.data,
__source: query?.__source,
type: WorkflowTypeEnum.REGULAR,
+ origin: WorkflowOriginEnum.NOVU_CLOUD,
})
);
}
diff --git a/apps/api/src/app/workflows/queries/CreateWorkflowQuery.ts b/apps/api/src/app/workflows-v1/queries/CreateWorkflowQuery.ts
similarity index 50%
rename from apps/api/src/app/workflows/queries/CreateWorkflowQuery.ts
rename to apps/api/src/app/workflows-v1/queries/CreateWorkflowQuery.ts
index 4517d59b37e..c7cd6adc2a3 100644
--- a/apps/api/src/app/workflows/queries/CreateWorkflowQuery.ts
+++ b/apps/api/src/app/workflows-v1/queries/CreateWorkflowQuery.ts
@@ -1,3 +1,6 @@
+/**
+ * @deprecated use dto's in /workflows directory
+ */
export class CreateWorkflowQuery {
__source?: string;
}
diff --git a/apps/api/src/app/workflows/queries/index.ts b/apps/api/src/app/workflows-v1/queries/index.ts
similarity index 100%
rename from apps/api/src/app/workflows/queries/index.ts
rename to apps/api/src/app/workflows-v1/queries/index.ts
diff --git a/apps/api/src/app/workflows/usecases/change-template-active-status/change-template-active-status.command.ts b/apps/api/src/app/workflows-v1/usecases/change-template-active-status/change-template-active-status.command.ts
similarity index 86%
rename from apps/api/src/app/workflows/usecases/change-template-active-status/change-template-active-status.command.ts
rename to apps/api/src/app/workflows-v1/usecases/change-template-active-status/change-template-active-status.command.ts
index bfd21a354fc..12ca673dbed 100644
--- a/apps/api/src/app/workflows/usecases/change-template-active-status/change-template-active-status.command.ts
+++ b/apps/api/src/app/workflows-v1/usecases/change-template-active-status/change-template-active-status.command.ts
@@ -2,7 +2,11 @@ import { IsBoolean, IsDefined, IsMongoId } from 'class-validator';
import { EnvironmentWithUserCommand } from '../../../shared/commands/project.command';
/**
- * DEPRECATED:
+ * @deprecated use dto's in /workflows directory
+ */
+
+/**
+ * @deprecated
* This command is deprecated and will be removed in the future.
* Please use the ChangeWorkflowActiveStatusCommand instead.
*/
diff --git a/apps/api/src/app/workflows/usecases/change-template-active-status/change-template-active-status.usecase.ts b/apps/api/src/app/workflows-v1/usecases/change-template-active-status/change-template-active-status.usecase.ts
similarity index 95%
rename from apps/api/src/app/workflows/usecases/change-template-active-status/change-template-active-status.usecase.ts
rename to apps/api/src/app/workflows-v1/usecases/change-template-active-status/change-template-active-status.usecase.ts
index 0bbe8c57961..fc36363aa0c 100644
--- a/apps/api/src/app/workflows/usecases/change-template-active-status/change-template-active-status.usecase.ts
+++ b/apps/api/src/app/workflows-v1/usecases/change-template-active-status/change-template-active-status.usecase.ts
@@ -1,5 +1,5 @@
import { BadRequestException, Injectable, NotFoundException } from '@nestjs/common';
-import { NotificationTemplateEntity, NotificationTemplateRepository, ChangeRepository } from '@novu/dal';
+import { ChangeRepository, NotificationTemplateEntity, NotificationTemplateRepository } from '@novu/dal';
import { ChangeEntityTypeEnum } from '@novu/shared';
import {
buildNotificationTemplateIdentifierKey,
@@ -12,7 +12,7 @@ import {
import { ChangeTemplateActiveStatusCommand } from './change-template-active-status.command';
/**
- * DEPRECATED:
+ * @deprecated
* This usecase is deprecated and will be removed in the future.
* Please use the ChangeWorkflowActiveStatus usecase instead.
*/
diff --git a/apps/api/src/app/workflows/usecases/delete-notification-template/delete-notification-template.command.ts b/apps/api/src/app/workflows-v1/usecases/delete-notification-template/delete-notification-template.command.ts
similarity index 83%
rename from apps/api/src/app/workflows/usecases/delete-notification-template/delete-notification-template.command.ts
rename to apps/api/src/app/workflows-v1/usecases/delete-notification-template/delete-notification-template.command.ts
index a28f76bd078..250cc9bf356 100644
--- a/apps/api/src/app/workflows/usecases/delete-notification-template/delete-notification-template.command.ts
+++ b/apps/api/src/app/workflows-v1/usecases/delete-notification-template/delete-notification-template.command.ts
@@ -1,9 +1,9 @@
-import { IsDefined, IsEnum, IsMongoId, IsString } from 'class-validator';
+import { IsDefined, IsEnum, IsMongoId } from 'class-validator';
import { WorkflowTypeEnum } from '@novu/shared';
import { EnvironmentWithUserCommand } from '../../../shared/commands/project.command';
/**
- * @deprecated:
+ * @deprecated
* This command is deprecated and will be removed in the future.
* Please use the GetWorkflowCommand instead.
*/
diff --git a/apps/api/src/app/workflows/usecases/delete-notification-template/delete-notification-template.usecase.ts b/apps/api/src/app/workflows-v1/usecases/delete-notification-template/delete-notification-template.usecase.ts
similarity index 99%
rename from apps/api/src/app/workflows/usecases/delete-notification-template/delete-notification-template.usecase.ts
rename to apps/api/src/app/workflows-v1/usecases/delete-notification-template/delete-notification-template.usecase.ts
index cf6be8ac869..d7c96c6d7f0 100644
--- a/apps/api/src/app/workflows/usecases/delete-notification-template/delete-notification-template.usecase.ts
+++ b/apps/api/src/app/workflows-v1/usecases/delete-notification-template/delete-notification-template.usecase.ts
@@ -16,7 +16,7 @@ import { ApiException } from '../../../shared/exceptions/api.exception';
import { DeleteNotificationTemplateCommand } from './delete-notification-template.command';
/**
- * DEPRECATED:
+ * @deprecated
* This usecase is deprecated and will be removed in the future.
* Please use the DeleteWorkflow usecase instead.
*/
diff --git a/apps/api/src/app/workflows/usecases/get-active-integrations-status/get-active-integrations-status.command.ts b/apps/api/src/app/workflows-v1/usecases/get-active-integrations-status/get-active-integrations-status.command.ts
similarity index 83%
rename from apps/api/src/app/workflows/usecases/get-active-integrations-status/get-active-integrations-status.command.ts
rename to apps/api/src/app/workflows-v1/usecases/get-active-integrations-status/get-active-integrations-status.command.ts
index da72548335c..1543bdc2b2e 100644
--- a/apps/api/src/app/workflows/usecases/get-active-integrations-status/get-active-integrations-status.command.ts
+++ b/apps/api/src/app/workflows-v1/usecases/get-active-integrations-status/get-active-integrations-status.command.ts
@@ -1,6 +1,9 @@
import { NotificationTemplateEntity } from '@novu/dal';
import { EnvironmentWithUserCommand } from '../../../shared/commands/project.command';
+/**
+ * @deprecated use commands in /workflows directory
+ */
export class GetActiveIntegrationsStatusCommand extends EnvironmentWithUserCommand {
workflows: NotificationTemplateEntity | NotificationTemplateEntity[];
}
diff --git a/apps/api/src/app/workflows/usecases/get-active-integrations-status/get-active-integrations-status.spec.ts b/apps/api/src/app/workflows-v1/usecases/get-active-integrations-status/get-active-integrations-status.spec.ts
similarity index 95%
rename from apps/api/src/app/workflows/usecases/get-active-integrations-status/get-active-integrations-status.spec.ts
rename to apps/api/src/app/workflows-v1/usecases/get-active-integrations-status/get-active-integrations-status.spec.ts
index 88086e6cdb3..5842fa226f6 100644
--- a/apps/api/src/app/workflows/usecases/get-active-integrations-status/get-active-integrations-status.spec.ts
+++ b/apps/api/src/app/workflows-v1/usecases/get-active-integrations-status/get-active-integrations-status.spec.ts
@@ -4,7 +4,7 @@ import { IntegrationService, NotificationTemplateService, UserSession } from '@n
import { expect } from 'chai';
import { SharedModule } from '../../../shared/shared.module';
import { WorkflowResponse } from '../../dto/workflow-response.dto';
-import { WorkflowModule } from '../../workflow.module';
+import { WorkflowModuleV1 } from '../../workflow-v1.module';
import { GetActiveIntegrationsStatusCommand } from './get-active-integrations-status.command';
import { GetActiveIntegrationsStatus } from './get-active-integrations-status.usecase';
@@ -14,7 +14,7 @@ describe('Get Active Integrations Status', function () {
beforeEach(async () => {
const moduleRef = await Test.createTestingModule({
- imports: [WorkflowModule, SharedModule],
+ imports: [WorkflowModuleV1, SharedModule],
providers: [],
}).compile();
diff --git a/apps/api/src/app/workflows/usecases/get-active-integrations-status/get-active-integrations-status.usecase.ts b/apps/api/src/app/workflows-v1/usecases/get-active-integrations-status/get-active-integrations-status.usecase.ts
similarity index 99%
rename from apps/api/src/app/workflows/usecases/get-active-integrations-status/get-active-integrations-status.usecase.ts
rename to apps/api/src/app/workflows-v1/usecases/get-active-integrations-status/get-active-integrations-status.usecase.ts
index 9c5d58a1eae..b8d1626b16f 100644
--- a/apps/api/src/app/workflows/usecases/get-active-integrations-status/get-active-integrations-status.usecase.ts
+++ b/apps/api/src/app/workflows-v1/usecases/get-active-integrations-status/get-active-integrations-status.usecase.ts
@@ -18,6 +18,9 @@ import { GetActiveIntegrationsStatusCommand } from './get-active-integrations-st
import { IntegrationResponseDto } from '../../../integrations/dtos/integration-response.dto';
import { WorkflowResponse } from '../../dto/workflow-response.dto';
+/**
+ * @deprecated use usecases in /workflows directory
+ */
@Injectable()
export class GetActiveIntegrationsStatus {
constructor(
diff --git a/apps/api/src/app/workflows/usecases/get-notification-template/get-notification-template.command.ts b/apps/api/src/app/workflows-v1/usecases/get-notification-template/get-notification-template.command.ts
similarity index 96%
rename from apps/api/src/app/workflows/usecases/get-notification-template/get-notification-template.command.ts
rename to apps/api/src/app/workflows-v1/usecases/get-notification-template/get-notification-template.command.ts
index 2c2d551e255..e0110b34896 100644
--- a/apps/api/src/app/workflows/usecases/get-notification-template/get-notification-template.command.ts
+++ b/apps/api/src/app/workflows-v1/usecases/get-notification-template/get-notification-template.command.ts
@@ -2,7 +2,7 @@ import { IsDefined, IsString } from 'class-validator';
import { EnvironmentWithUserCommand } from '../../../shared/commands/project.command';
/**
- * DEPRECATED:
+ * @deprecated
* This command is deprecated and will be removed in the future.
* Please use the GetWorkflowCommand instead.
*/
diff --git a/apps/api/src/app/workflows/usecases/get-notification-template/get-notification-template.usecase.ts b/apps/api/src/app/workflows-v1/usecases/get-notification-template/get-notification-template.usecase.ts
similarity index 98%
rename from apps/api/src/app/workflows/usecases/get-notification-template/get-notification-template.usecase.ts
rename to apps/api/src/app/workflows-v1/usecases/get-notification-template/get-notification-template.usecase.ts
index 904e3a19cfe..5ffd1f2bb7c 100644
--- a/apps/api/src/app/workflows/usecases/get-notification-template/get-notification-template.usecase.ts
+++ b/apps/api/src/app/workflows-v1/usecases/get-notification-template/get-notification-template.usecase.ts
@@ -3,7 +3,7 @@ import { NotificationTemplateEntity, NotificationTemplateRepository } from '@nov
import { GetNotificationTemplateCommand } from './get-notification-template.command';
/**
- * DEPRECATED:
+ * @deprecated
* This usecase is deprecated and will be removed in the future.
* Please use the GetWorkflow usecase instead.
*/
diff --git a/apps/api/src/app/workflows/usecases/get-notification-templates/get-notification-templates.command.ts b/apps/api/src/app/workflows-v1/usecases/get-notification-templates/get-notification-templates.command.ts
similarity index 96%
rename from apps/api/src/app/workflows/usecases/get-notification-templates/get-notification-templates.command.ts
rename to apps/api/src/app/workflows-v1/usecases/get-notification-templates/get-notification-templates.command.ts
index 7b1f645336c..ab26e021794 100644
--- a/apps/api/src/app/workflows/usecases/get-notification-templates/get-notification-templates.command.ts
+++ b/apps/api/src/app/workflows-v1/usecases/get-notification-templates/get-notification-templates.command.ts
@@ -3,7 +3,7 @@ import { IsNumber, IsOptional, IsString } from 'class-validator';
import { EnvironmentWithUserCommand } from '../../../shared/commands/project.command';
/**
- * DEPRECATED:
+ * @deprecated
* This command is deprecated and will be removed in the future.
* Please use the GetWorkflowsCommand instead.
*/
diff --git a/apps/api/src/app/workflows/usecases/get-notification-templates/get-notification-templates.usecase.ts b/apps/api/src/app/workflows-v1/usecases/get-notification-templates/get-notification-templates.usecase.ts
similarity index 99%
rename from apps/api/src/app/workflows/usecases/get-notification-templates/get-notification-templates.usecase.ts
rename to apps/api/src/app/workflows-v1/usecases/get-notification-templates/get-notification-templates.usecase.ts
index 08012bf1d8c..bb2e251d151 100644
--- a/apps/api/src/app/workflows/usecases/get-notification-templates/get-notification-templates.usecase.ts
+++ b/apps/api/src/app/workflows-v1/usecases/get-notification-templates/get-notification-templates.usecase.ts
@@ -5,8 +5,9 @@ import { WorkflowsResponseDto } from '../../dto/workflows.response.dto';
import { GetActiveIntegrationsStatus } from '../get-active-integrations-status/get-active-integrations-status.usecase';
import { WorkflowResponse } from '../../dto/workflow-response.dto';
import { GetActiveIntegrationsStatusCommand } from '../get-active-integrations-status/get-active-integrations-status.command';
+
/**
- * DEPRECATED:
+ * D@deprecated
* This usecase is deprecated and will be removed in the future.
* Please use the GetWorkflows usecase instead.
*/
diff --git a/apps/api/src/app/workflows/usecases/get-workflow-variables/get-workflow-variables.command.ts b/apps/api/src/app/workflows-v1/usecases/get-workflow-variables/get-workflow-variables.command.ts
similarity index 100%
rename from apps/api/src/app/workflows/usecases/get-workflow-variables/get-workflow-variables.command.ts
rename to apps/api/src/app/workflows-v1/usecases/get-workflow-variables/get-workflow-variables.command.ts
diff --git a/apps/api/src/app/workflows/usecases/get-workflow-variables/get-workflow-variables.usecase.ts b/apps/api/src/app/workflows-v1/usecases/get-workflow-variables/get-workflow-variables.usecase.ts
similarity index 96%
rename from apps/api/src/app/workflows/usecases/get-workflow-variables/get-workflow-variables.usecase.ts
rename to apps/api/src/app/workflows-v1/usecases/get-workflow-variables/get-workflow-variables.usecase.ts
index 2d0a72c6554..fd1df64e923 100644
--- a/apps/api/src/app/workflows/usecases/get-workflow-variables/get-workflow-variables.usecase.ts
+++ b/apps/api/src/app/workflows-v1/usecases/get-workflow-variables/get-workflow-variables.usecase.ts
@@ -6,6 +6,9 @@ import { buildVariablesKey, CachedEntity } from '@novu/application-generic';
import { ApiException } from '../../../shared/exceptions/api.exception';
import { GetWorkflowVariablesCommand } from './get-workflow-variables.command';
+/**
+ * @deprecated use usecases in /workflows directory
+ */
@Injectable()
export class GetWorkflowVariables {
constructor(private moduleRef: ModuleRef) {}
diff --git a/apps/api/src/app/workflows/usecases/index.ts b/apps/api/src/app/workflows-v1/usecases/index.ts
similarity index 100%
rename from apps/api/src/app/workflows/usecases/index.ts
rename to apps/api/src/app/workflows-v1/usecases/index.ts
diff --git a/apps/api/src/app/workflows/workflow.controller.ts b/apps/api/src/app/workflows-v1/workflow-v1.controller.ts
similarity index 97%
rename from apps/api/src/app/workflows/workflow.controller.ts
rename to apps/api/src/app/workflows-v1/workflow-v1.controller.ts
index f55528109c2..9f685a6084d 100644
--- a/apps/api/src/app/workflows/workflow.controller.ts
+++ b/apps/api/src/app/workflows-v1/workflow-v1.controller.ts
@@ -17,7 +17,7 @@ import {
UpdateWorkflow,
UpdateWorkflowCommand,
} from '@novu/application-generic';
-import { MemberRoleEnum, UserSessionData, WorkflowTypeEnum } from '@novu/shared';
+import { UserSessionData, WorkflowOriginEnum, WorkflowTypeEnum } from '@novu/shared';
import { ApiOperation, ApiTags } from '@nestjs/swagger';
import { UserSession } from '../shared/framework/user.decorator';
@@ -48,12 +48,15 @@ import { GetWorkflowVariablesCommand } from './usecases/get-workflow-variables/g
import { UserAuthentication } from '../shared/framework/swagger/api.key.security';
import { SdkGroupName } from '../shared/framework/swagger/sdk.decorators';
+/**
+ * @deprecated use controllers in /workflows directory
+ */
@ApiCommonResponses()
@Controller('/workflows')
@UseInterceptors(ClassSerializerInterceptor)
@UserAuthentication()
@ApiTags('Workflows')
-export class WorkflowController {
+export class WorkflowControllerV1 {
constructor(
private createWorkflowUsecase: CreateWorkflow,
private updateWorkflowByIdUsecase: UpdateWorkflow,
@@ -212,6 +215,7 @@ export class WorkflowController {
data: body.data,
__source: query?.__source,
type: WorkflowTypeEnum.REGULAR,
+ origin: WorkflowOriginEnum.NOVU_CLOUD,
})
);
}
diff --git a/apps/api/src/app/workflows/workflow.module.ts b/apps/api/src/app/workflows-v1/workflow-v1.module.ts
similarity index 87%
rename from apps/api/src/app/workflows/workflow.module.ts
rename to apps/api/src/app/workflows-v1/workflow-v1.module.ts
index 4eba215c234..8953599703d 100644
--- a/apps/api/src/app/workflows/workflow.module.ts
+++ b/apps/api/src/app/workflows-v1/workflow-v1.module.ts
@@ -6,15 +6,15 @@ import { MessageTemplateModule } from '../message-template/message-template.modu
import { SharedModule } from '../shared/shared.module';
import { NotificationTemplateController } from './notification-template.controller';
import { USE_CASES } from './usecases';
-import { WorkflowController } from './workflow.controller';
+import { WorkflowControllerV1 } from './workflow-v1.controller';
import { PreferencesModule } from '../preferences';
@Module({
imports: [SharedModule, MessageTemplateModule, ChangeModule, AuthModule, IntegrationModule, PreferencesModule],
- controllers: [NotificationTemplateController, WorkflowController],
+ controllers: [NotificationTemplateController, WorkflowControllerV1],
providers: [...USE_CASES],
exports: [...USE_CASES],
})
-export class WorkflowModule implements NestModule {
+export class WorkflowModuleV1 implements NestModule {
configure(consumer: MiddlewareConsumer): MiddlewareConsumer | void {}
}
diff --git a/apps/api/src/app/workflows-v2/dto/create-workflow-dto.ts b/apps/api/src/app/workflows-v2/dto/create-workflow-dto.ts
new file mode 100644
index 00000000000..f2067867202
--- /dev/null
+++ b/apps/api/src/app/workflows-v2/dto/create-workflow-dto.ts
@@ -0,0 +1,10 @@
+import { WorkflowCreationSourceEnum } from '@novu/shared';
+import { PreferencesRequestDto, StepCreateDto, WorkflowCommonsFields } from './workflow-commons-fields';
+
+export type CreateWorkflowDto = Omit & {
+ steps: StepCreateDto[];
+
+ __source: WorkflowCreationSourceEnum;
+
+ preferences?: PreferencesRequestDto;
+};
diff --git a/apps/api/src/app/workflows-v2/dto/update-workflow-dto.ts b/apps/api/src/app/workflows-v2/dto/update-workflow-dto.ts
new file mode 100644
index 00000000000..241ff85bf7a
--- /dev/null
+++ b/apps/api/src/app/workflows-v2/dto/update-workflow-dto.ts
@@ -0,0 +1,9 @@
+import { PreferencesRequestDto, StepCreateDto, StepUpdateDto, WorkflowCommonsFields } from './workflow-commons-fields';
+
+export type UpdateWorkflowDto = Omit & {
+ updatedAt: string;
+
+ steps: (StepCreateDto | StepUpdateDto)[];
+
+ preferences: PreferencesRequestDto;
+};
diff --git a/apps/api/src/app/workflows-v2/dto/workflow-commons-fields.ts b/apps/api/src/app/workflows-v2/dto/workflow-commons-fields.ts
new file mode 100644
index 00000000000..c43f16a6ae5
--- /dev/null
+++ b/apps/api/src/app/workflows-v2/dto/workflow-commons-fields.ts
@@ -0,0 +1,76 @@
+import { IsArray, IsBoolean, IsDefined, IsObject, IsOptional, IsString } from 'class-validator';
+
+import { JsonSchema } from '@novu/framework';
+import { StepTypeEnum, WorkflowPreferences } from '@novu/shared';
+import { WorkflowResponseDto } from './workflow-response-dto';
+
+export class ControlsSchema {
+ schema: JsonSchema;
+}
+
+export type StepResponseDto = StepDto & {
+ stepUuid: string;
+};
+
+export type StepUpdateDto = StepDto & {
+ stepUuid: string;
+};
+
+export type StepCreateDto = StepDto;
+
+export type ListWorkflowResponse = {
+ workflows: WorkflowListResponseDto[];
+ totalCount: number;
+};
+
+export type WorkflowListResponseDto = Pick & {
+ stepTypeOverviews: StepTypeEnum[];
+};
+
+export class StepDto {
+ @IsString()
+ @IsDefined()
+ name: string;
+
+ @IsString()
+ @IsDefined()
+ type: StepTypeEnum;
+
+ @IsOptional()
+ controls?: ControlsSchema;
+
+ @IsObject()
+ controlValues: Record;
+}
+
+export class WorkflowCommonsFields {
+ @IsString()
+ @IsDefined()
+ _id: string;
+
+ @IsOptional()
+ @IsArray()
+ @IsString({ each: true })
+ tags?: string[];
+
+ @IsOptional()
+ @IsBoolean()
+ active?: boolean;
+
+ @IsString()
+ @IsDefined()
+ name: string;
+
+ @IsString()
+ @IsOptional()
+ description?: string;
+}
+
+export type PreferencesResponseDto = {
+ user: WorkflowPreferences | null;
+ default: WorkflowPreferences;
+};
+
+export type PreferencesRequestDto = {
+ user: WorkflowPreferences | null;
+};
diff --git a/apps/api/src/app/workflows-v2/dto/workflow-response-dto.ts b/apps/api/src/app/workflows-v2/dto/workflow-response-dto.ts
new file mode 100644
index 00000000000..7363028585e
--- /dev/null
+++ b/apps/api/src/app/workflows-v2/dto/workflow-response-dto.ts
@@ -0,0 +1,25 @@
+import { WorkflowOriginEnum } from '@novu/shared';
+import { IsArray, IsDefined, IsEnum, IsObject, IsString } from 'class-validator';
+import { PreferencesResponseDto, StepResponseDto, WorkflowCommonsFields } from './workflow-commons-fields';
+
+export class WorkflowResponseDto extends WorkflowCommonsFields {
+ @IsString()
+ @IsDefined()
+ updatedAt: string;
+
+ @IsString()
+ @IsDefined()
+ createdAt: string;
+
+ @IsArray()
+ @IsDefined()
+ steps: StepResponseDto[];
+
+ @IsEnum(WorkflowOriginEnum)
+ @IsDefined()
+ origin: WorkflowOriginEnum;
+
+ @IsObject()
+ @IsDefined()
+ preferences: PreferencesResponseDto;
+}
diff --git a/apps/api/src/app/workflows-v2/exceptions/step-upsert-mechanism-failed-missing-id.exception.ts b/apps/api/src/app/workflows-v2/exceptions/step-upsert-mechanism-failed-missing-id.exception.ts
new file mode 100644
index 00000000000..e4a16833274
--- /dev/null
+++ b/apps/api/src/app/workflows-v2/exceptions/step-upsert-mechanism-failed-missing-id.exception.ts
@@ -0,0 +1,13 @@
+import { InternalServerErrorException } from '@nestjs/common';
+import { NotificationStepEntity } from '@novu/dal';
+
+export class StepUpsertMechanismFailedMissingIdException extends InternalServerErrorException {
+ constructor(stepDatabaseId: string, stepExternalID: string | undefined, persistedStep: NotificationStepEntity) {
+ super({
+ message: 'Failed to upsert step control values due to missing id',
+ stepDatabaseId,
+ stepExternalID,
+ persistedStep,
+ });
+ }
+}
diff --git a/apps/api/src/app/workflows-v2/exceptions/workflow-already-exist.ts b/apps/api/src/app/workflows-v2/exceptions/workflow-already-exist.ts
new file mode 100644
index 00000000000..25fbc67968c
--- /dev/null
+++ b/apps/api/src/app/workflows-v2/exceptions/workflow-already-exist.ts
@@ -0,0 +1,12 @@
+import { BadRequestException } from '@nestjs/common';
+import { UpsertWorkflowCommand } from '../usecases/upsert-workflow/upsert-workflow.command';
+
+export class WorkflowAlreadyExistException extends BadRequestException {
+ constructor(command: UpsertWorkflowCommand) {
+ super({
+ message: 'Workflow with the same name already exists',
+ workflowName: command.workflowDto.name,
+ environmentId: command.user.environmentId,
+ });
+ }
+}
diff --git a/apps/api/src/app/workflows-v2/exceptions/workflow-not-found-exception.ts b/apps/api/src/app/workflows-v2/exceptions/workflow-not-found-exception.ts
new file mode 100644
index 00000000000..ad9389bff96
--- /dev/null
+++ b/apps/api/src/app/workflows-v2/exceptions/workflow-not-found-exception.ts
@@ -0,0 +1,10 @@
+import { BadRequestException } from '@nestjs/common';
+
+export class WorkflowNotFoundException extends BadRequestException {
+ constructor(id: string) {
+ super({
+ message: 'Workflow cannot be found',
+ workflowId: id,
+ });
+ }
+}
diff --git a/apps/api/src/app/workflows-v2/mappers/notification-template-mapper.ts b/apps/api/src/app/workflows-v2/mappers/notification-template-mapper.ts
new file mode 100644
index 00000000000..9bbbab5acd1
--- /dev/null
+++ b/apps/api/src/app/workflows-v2/mappers/notification-template-mapper.ts
@@ -0,0 +1,86 @@
+import { DEFAULT_WORKFLOW_PREFERENCES, PreferencesTypeEnum, StepTypeEnum, WorkflowOriginEnum } from '@novu/shared';
+import { ControlValuesEntity, NotificationStepEntity, NotificationTemplateEntity } from '@novu/dal';
+import { GetPreferencesResponseDto } from '@novu/application-generic';
+
+import {
+ ControlsSchema,
+ PreferencesResponseDto,
+ StepResponseDto,
+ WorkflowListResponseDto,
+} from '../dto/workflow-commons-fields';
+import { WorkflowResponseDto } from '../dto/workflow-response-dto';
+
+export function toResponseWorkflowDto(
+ template: NotificationTemplateEntity,
+ preferences: GetPreferencesResponseDto | undefined,
+ stepIdToControlValuesMap: { [p: string]: ControlValuesEntity }
+): WorkflowResponseDto {
+ const preferencesDto: PreferencesResponseDto = {
+ user: preferences?.source[PreferencesTypeEnum.USER_WORKFLOW] || null,
+ default: preferences?.source[PreferencesTypeEnum.WORKFLOW_RESOURCE] || DEFAULT_WORKFLOW_PREFERENCES,
+ };
+
+ return {
+ _id: template._id,
+ tags: template.tags,
+ active: template.active,
+ preferences: preferencesDto,
+ steps: getSteps(template, stepIdToControlValuesMap),
+ name: template.name,
+ description: template.description,
+ origin: template.origin || WorkflowOriginEnum.NOVU_CLOUD,
+ updatedAt: template.updatedAt || 'Missing Updated At',
+ createdAt: template.createdAt || 'Missing Create At',
+ };
+}
+
+function getSteps(template: NotificationTemplateEntity, controlValuesMap: { [p: string]: ControlValuesEntity }) {
+ const steps: StepResponseDto[] = [];
+ for (const step of template.steps) {
+ const stepResponseDto = toStepResponseDto(step);
+ const controlValues = controlValuesMap[step._templateId];
+ if (controlValues?.controls && Object.entries(controlValues?.controls).length) {
+ stepResponseDto.controlValues = controlValues.controls;
+ }
+ steps.push(stepResponseDto);
+ }
+
+ return steps;
+}
+
+function toMinifiedWorkflowDto(template: NotificationTemplateEntity): WorkflowListResponseDto {
+ return {
+ _id: template._id,
+ name: template.name,
+ tags: template.tags,
+ updatedAt: template.updatedAt || 'Missing Updated At',
+ stepTypeOverviews: template.steps.map(buildStepTypeOverview).filter((stepTypeEnum) => !!stepTypeEnum),
+ createdAt: template.createdAt || 'Missing Create At',
+ };
+}
+
+export function toWorkflowsMinifiedDtos(templates: NotificationTemplateEntity[]): WorkflowListResponseDto[] {
+ return templates.map(toMinifiedWorkflowDto);
+}
+
+function toStepResponseDto(step: NotificationStepEntity): StepResponseDto {
+ return {
+ name: step.name || 'Missing Name',
+ stepUuid: step._templateId,
+ type: step.template?.type || StepTypeEnum.EMAIL,
+ controls: convertControls(step),
+ controlValues: step.controlVariables || {},
+ };
+}
+
+function convertControls(step: NotificationStepEntity): ControlsSchema | undefined {
+ if (step.template?.controls) {
+ return { schema: step.template.controls.schema };
+ } else {
+ return undefined;
+ }
+}
+
+function buildStepTypeOverview(step: NotificationStepEntity): StepTypeEnum | undefined {
+ return step.template?.type;
+}
diff --git a/apps/api/src/app/workflows-v2/params/get-list-query-params.ts b/apps/api/src/app/workflows-v2/params/get-list-query-params.ts
new file mode 100644
index 00000000000..bc26126c377
--- /dev/null
+++ b/apps/api/src/app/workflows-v2/params/get-list-query-params.ts
@@ -0,0 +1,7 @@
+import { LimitOffsetPaginationDto } from '@novu/shared';
+
+import { WorkflowResponseDto } from '../dto/workflow-response-dto';
+
+export class GetListQueryParams extends LimitOffsetPaginationDto {
+ query?: string;
+}
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
new file mode 100644
index 00000000000..07b6c2823b6
--- /dev/null
+++ b/apps/api/src/app/workflows-v2/usecases/delete-workflow/delete-workflow.command.ts
@@ -0,0 +1,8 @@
+import { EnvironmentWithUserObjectCommand } from '@novu/application-generic';
+import { IsDefined, IsString } from 'class-validator';
+
+export class DeleteWorkflowCommand extends EnvironmentWithUserObjectCommand {
+ @IsString()
+ @IsDefined()
+ workflowId: 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
new file mode 100644
index 00000000000..162b4efa28a
--- /dev/null
+++ b/apps/api/src/app/workflows-v2/usecases/delete-workflow/delete-workflow.usecase.ts
@@ -0,0 +1,59 @@
+import { Injectable } from '@nestjs/common';
+
+import {
+ ControlValuesRepository,
+ MessageTemplateRepository,
+ NotificationTemplateEntity,
+ NotificationTemplateRepository,
+} from '@novu/dal';
+
+import { DeleteWorkflowCommand } from './delete-workflow.command';
+import { WorkflowNotFoundException } from '../../exceptions/workflow-not-found-exception';
+
+@Injectable()
+export class DeleteWorkflowUseCase {
+ constructor(
+ private notificationTemplateRepository: NotificationTemplateRepository,
+ private messageTemplateRepository: MessageTemplateRepository,
+ private controlValuesRepository: ControlValuesRepository
+ ) {}
+
+ async execute(command: DeleteWorkflowCommand): Promise {
+ const workflow = await this.notificationTemplateRepository.findByIdQuery({
+ id: command.workflowId,
+ environmentId: command.user.environmentId,
+ });
+ if (!workflow) {
+ throw new WorkflowNotFoundException(command.workflowId);
+ }
+ await this.deleteRelatedEntities(command, workflow);
+ }
+
+ private async deleteRelatedEntities(command: DeleteWorkflowCommand, workflow) {
+ await this.controlValuesRepository.deleteMany({
+ _environmentId: command.user.environmentId,
+ _organizationId: command.user.organizationId,
+ _workflowId: command.workflowId,
+ });
+ await this.removeMessageTemplatesIfNeeded(workflow, command);
+ await this.notificationTemplateRepository.delete(buildDeleteQuery(command));
+ }
+
+ 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) {
+ return {
+ _id: command.workflowId,
+ _organizationId: command.user.organizationId,
+ _environmentId: command.user.environmentId,
+ };
+}
diff --git a/apps/api/src/app/workflows-v2/usecases/get-workflow/get-workflow.command.ts b/apps/api/src/app/workflows-v2/usecases/get-workflow/get-workflow.command.ts
new file mode 100644
index 00000000000..fb190e9a0e4
--- /dev/null
+++ b/apps/api/src/app/workflows-v2/usecases/get-workflow/get-workflow.command.ts
@@ -0,0 +1,8 @@
+import { EnvironmentWithUserObjectCommand } from '@novu/application-generic';
+import { IsDefined, IsString } from 'class-validator';
+
+export class GetWorkflowCommand extends EnvironmentWithUserObjectCommand {
+ @IsString()
+ @IsDefined()
+ _workflowId: string;
+}
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
new file mode 100644
index 00000000000..6729440b28b
--- /dev/null
+++ b/apps/api/src/app/workflows-v2/usecases/get-workflow/get-workflow.usecase.ts
@@ -0,0 +1,71 @@
+import { Injectable } from '@nestjs/common';
+
+import {
+ ControlValuesEntity,
+ ControlValuesRepository,
+ NotificationStepEntity,
+ NotificationTemplateRepository,
+} from '@novu/dal';
+import { ControlVariablesLevelEnum } from '@novu/shared';
+import { GetPreferences, GetPreferencesCommand } from '@novu/application-generic';
+
+import { GetWorkflowCommand } from './get-workflow.command';
+import { WorkflowNotFoundException } from '../../exceptions/workflow-not-found-exception';
+import { WorkflowResponseDto } from '../../dto/workflow-response-dto';
+import { toResponseWorkflowDto } from '../../mappers/notification-template-mapper';
+
+@Injectable()
+export class GetWorkflowUseCase {
+ constructor(
+ private notificationTemplateRepository: NotificationTemplateRepository,
+ private controlValuesRepository: ControlValuesRepository,
+ private getPreferencesUseCase: GetPreferences
+ ) {}
+ async execute(command: GetWorkflowCommand): Promise {
+ const notificationTemplateEntity = await this.notificationTemplateRepository.findByIdQuery({
+ id: command._workflowId,
+ environmentId: command.user.environmentId,
+ });
+
+ if (!notificationTemplateEntity) {
+ throw new WorkflowNotFoundException(command._workflowId);
+ }
+ const stepIdToControlValuesMap = await this.getControlsValuesMap(notificationTemplateEntity.steps, command);
+ const preferences = await this.getPreferencesUseCase.safeExecute(
+ GetPreferencesCommand.create({
+ environmentId: command.user.environmentId,
+ organizationId: command.user.organizationId,
+ })
+ );
+
+ return toResponseWorkflowDto(notificationTemplateEntity, preferences, stepIdToControlValuesMap);
+ }
+
+ private async getControlsValuesMap(
+ steps: NotificationStepEntity[],
+ command: GetWorkflowCommand
+ ): Promise<{ [key: string]: ControlValuesEntity }> {
+ const acc: { [key: string]: ControlValuesEntity } = {};
+
+ for (const step of steps) {
+ const controlValuesEntity = await this.buildControlValuesForStep(step, command);
+ if (controlValuesEntity) {
+ acc[step._templateId] = controlValuesEntity;
+ }
+ }
+
+ return acc;
+ }
+ private async buildControlValuesForStep(
+ step: NotificationStepEntity,
+ command: GetWorkflowCommand
+ ): Promise {
+ return await this.controlValuesRepository.findFirst({
+ _environmentId: command.user.environmentId,
+ _organizationId: command.user.organizationId,
+ _workflowId: command._workflowId,
+ _stepId: step._templateId,
+ level: ControlVariablesLevelEnum.STEP_CONTROLS,
+ });
+ }
+}
diff --git a/apps/api/src/app/workflows-v2/usecases/list-workflows/list-workflow.usecase.ts b/apps/api/src/app/workflows-v2/usecases/list-workflows/list-workflow.usecase.ts
new file mode 100644
index 00000000000..03c6425f98b
--- /dev/null
+++ b/apps/api/src/app/workflows-v2/usecases/list-workflows/list-workflow.usecase.ts
@@ -0,0 +1,28 @@
+import { Injectable } from '@nestjs/common';
+
+import { NotificationTemplateRepository } from '@novu/dal';
+import { ListWorkflowsCommand } from './list-workflows.command';
+import { ListWorkflowResponse } from '../../dto/workflow-commons-fields';
+import { toWorkflowsMinifiedDtos } from '../../mappers/notification-template-mapper';
+
+@Injectable()
+export class ListWorkflowsUseCase {
+ constructor(private notificationTemplateRepository: NotificationTemplateRepository) {}
+ async execute(command: ListWorkflowsCommand): Promise {
+ const res = await this.notificationTemplateRepository.getList(
+ command.user.organizationId,
+ command.user.environmentId,
+ command.offset,
+ command.limit,
+ command.searchQuery
+ );
+ if (res.data === null || res.data === undefined) {
+ return { workflows: [], totalCount: 0 };
+ }
+
+ return {
+ workflows: toWorkflowsMinifiedDtos(res.data),
+ totalCount: res.totalCount,
+ };
+ }
+}
diff --git a/apps/api/src/app/workflows-v2/usecases/list-workflows/list-workflows.command.ts b/apps/api/src/app/workflows-v2/usecases/list-workflows/list-workflows.command.ts
new file mode 100644
index 00000000000..1c68acf5add
--- /dev/null
+++ b/apps/api/src/app/workflows-v2/usecases/list-workflows/list-workflows.command.ts
@@ -0,0 +1,7 @@
+import { IsOptional } from 'class-validator';
+import { PaginatedListCommand } from '@novu/application-generic';
+
+export class ListWorkflowsCommand extends PaginatedListCommand {
+ @IsOptional()
+ searchQuery?: string;
+}
diff --git a/apps/api/src/app/workflows-v2/usecases/upsert-workflow/upsert-workflow.command.ts b/apps/api/src/app/workflows-v2/usecases/upsert-workflow/upsert-workflow.command.ts
new file mode 100644
index 00000000000..d86421c44cd
--- /dev/null
+++ b/apps/api/src/app/workflows-v2/usecases/upsert-workflow/upsert-workflow.command.ts
@@ -0,0 +1,9 @@
+import { EnvironmentWithUserObjectCommand } from '@novu/application-generic';
+import { CreateWorkflowDto } from '../../dto/create-workflow-dto';
+import { UpdateWorkflowDto } from '../../dto/update-workflow-dto';
+
+export class UpsertWorkflowCommand extends EnvironmentWithUserObjectCommand {
+ workflowDatabaseIdForUpdate?: string;
+
+ workflowDto: CreateWorkflowDto | UpdateWorkflowDto;
+}
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
new file mode 100644
index 00000000000..41d10353f71
--- /dev/null
+++ b/apps/api/src/app/workflows-v2/usecases/upsert-workflow/upsert-workflow.usecase.ts
@@ -0,0 +1,298 @@
+import { BadRequestException, Injectable } from '@nestjs/common';
+
+import {
+ ControlValuesEntity,
+ EnvironmentRepository,
+ NotificationGroupRepository,
+ NotificationStepEntity,
+ NotificationTemplateEntity,
+ NotificationTemplateRepository,
+ PreferencesEntity,
+} from '@novu/dal';
+import {
+ CreateWorkflow as CreateWorkflowGeneric,
+ CreateWorkflowCommand,
+ GetPreferences,
+ GetPreferencesCommand,
+ GetPreferencesResponseDto,
+ NotificationStep,
+ UpdateWorkflow,
+ UpdateWorkflowCommand,
+ UpsertControlValuesCommand,
+ UpsertControlValuesUseCase,
+ UpsertPreferences,
+ UpsertUserWorkflowPreferencesCommand,
+} from '@novu/application-generic';
+import { WorkflowCreationSourceEnum, WorkflowOriginEnum, WorkflowTypeEnum } from '@novu/shared';
+import { UpsertWorkflowCommand } from './upsert-workflow.command';
+import { WorkflowAlreadyExistException } from '../../exceptions/workflow-already-exist';
+import { StepCreateDto, StepDto, StepUpdateDto } from '../../dto/workflow-commons-fields';
+import { StepUpsertMechanismFailedMissingIdException } from '../../exceptions/step-upsert-mechanism-failed-missing-id.exception';
+import { CreateWorkflowDto } from '../../dto/create-workflow-dto';
+import { WorkflowResponseDto } from '../../dto/workflow-response-dto';
+import { toResponseWorkflowDto } from '../../mappers/notification-template-mapper';
+
+function buildUpsertControlValuesCommand(command: UpsertWorkflowCommand, persistedStep, persistedWorkflow, stepInDto) {
+ return UpsertControlValuesCommand.create({
+ organizationId: command.user.organizationId,
+ environmentId: command.user.environmentId,
+ notificationStepEntity: persistedStep,
+ workflowId: persistedWorkflow._id,
+ newControlValues: stepInDto.controlValues || {},
+ controlSchemas: stepInDto?.controls || { schema: {} },
+ });
+}
+
+@Injectable()
+export class UpsertWorkflowUseCase {
+ constructor(
+ private createWorkflowGenericUsecase: CreateWorkflowGeneric,
+ private updateWorkflowUsecase: UpdateWorkflow,
+ private notificationTemplateRepository: NotificationTemplateRepository,
+ private notificationGroupRepository: NotificationGroupRepository,
+ private upsertPreferencesUsecase: UpsertPreferences,
+ private upsertControlValuesUseCase: UpsertControlValuesUseCase,
+ private environmentRepository: EnvironmentRepository,
+ private getPreferencesUseCase: GetPreferences
+ ) {}
+ async execute(command: UpsertWorkflowCommand): Promise {
+ const workflowForUpdate = await this.getWorkflowIfUpdateAndExist(command);
+ if (!workflowForUpdate && (await this.workflowExistByExternalId(command))) {
+ throw new WorkflowAlreadyExistException(command);
+ }
+ const workflow = await this.createOrUpdateWorkflow(workflowForUpdate, command);
+ const stepIdToControlValuesMap = await this.upsertControlValues(workflow, command);
+ const preferences = await this.upsertPreference(command, workflow);
+
+ return toResponseWorkflowDto(workflow, preferences, stepIdToControlValuesMap);
+ }
+
+ private async upsertControlValues(workflow: NotificationTemplateEntity, command: UpsertWorkflowCommand) {
+ const stepIdToControlValuesMap: { [p: string]: ControlValuesEntity } = {};
+ for (const persistedStep of workflow.steps) {
+ const controlValuesEntity = await this.upsertControlValuesForSingleStep(persistedStep, command, workflow);
+ if (controlValuesEntity) {
+ stepIdToControlValuesMap[persistedStep._templateId] = controlValuesEntity;
+ }
+ }
+
+ return stepIdToControlValuesMap;
+ }
+
+ private async upsertControlValuesForSingleStep(
+ persistedStep: NotificationStepEntity,
+ command: UpsertWorkflowCommand,
+ persistedWorkflow: NotificationTemplateEntity
+ ): Promise {
+ const stepDatabaseId = persistedStep._templateId;
+ const stepExternalId = persistedStep.name;
+ if (!stepDatabaseId && !stepExternalId) {
+ throw new StepUpsertMechanismFailedMissingIdException(stepDatabaseId, stepExternalId, persistedStep);
+ }
+ const stepInDto = command.workflowDto?.steps.find((commandStepItem) => commandStepItem.name === persistedStep.name);
+ if (!stepInDto) {
+ // TODO: should delete the values from the database? or just ignore?
+ return;
+ }
+
+ const upsertControlValuesCommand = buildUpsertControlValuesCommand(
+ command,
+ persistedStep,
+ persistedWorkflow,
+ stepInDto
+ );
+
+ return await this.upsertControlValuesUseCase.execute(upsertControlValuesCommand);
+ }
+
+ private async upsertPreference(
+ command: UpsertWorkflowCommand,
+ workflow: NotificationTemplateEntity
+ ): Promise {
+ if (!command.workflowDto.preferences?.user) {
+ return undefined;
+ }
+ await this.upsertPreferences(workflow, command);
+
+ return await this.getPersistedPreferences(workflow);
+ }
+
+ private async getPersistedPreferences(workflow) {
+ return await this.getPreferencesUseCase.safeExecute(
+ GetPreferencesCommand.create({
+ environmentId: workflow._environmentId,
+ organizationId: workflow._organizationId,
+ templateId: workflow._id,
+ })
+ );
+ }
+
+ private async upsertPreferences(workflow, command: UpsertWorkflowCommand): Promise {
+ return await this.upsertPreferencesUsecase.upsertUserWorkflowPreferences(
+ UpsertUserWorkflowPreferencesCommand.create({
+ environmentId: workflow._environmentId,
+ organizationId: workflow._organizationId,
+ userId: command.user._id,
+ templateId: workflow._id,
+ preferences: command.workflowDto.preferences?.user,
+ })
+ );
+ }
+
+ private async createOrUpdateWorkflow(
+ existingWorkflow: NotificationTemplateEntity | null | undefined,
+ command: UpsertWorkflowCommand
+ ): Promise {
+ if (existingWorkflow) {
+ return await this.updateWorkflowUsecase.execute(
+ UpdateWorkflowCommand.create(this.convertCreateToUpdateCommand(command, existingWorkflow))
+ );
+ }
+
+ return await this.createWorkflowGenericUsecase.execute(
+ CreateWorkflowCommand.create(await this.buildCreateWorkflowGenericCommand(command))
+ );
+ }
+
+ private async buildCreateWorkflowGenericCommand(command: UpsertWorkflowCommand): Promise {
+ const { user } = command;
+ // It's safe to assume we're dealing with CreateWorkflowDto on the creation path
+ const workflowDto = command.workflowDto as CreateWorkflowDto;
+ const isWorkflowActive = workflowDto?.active ?? true;
+ const notificationGroupId = await this.getNotificationGroup(command.user.environmentId);
+
+ if (!notificationGroupId) {
+ throw new BadRequestException('Notification group not found');
+ }
+
+ return {
+ notificationGroupId,
+ environmentId: user.environmentId,
+ organizationId: user.organizationId,
+ userId: user._id,
+ name: workflowDto.name,
+ __source: workflowDto.__source || WorkflowCreationSourceEnum.DASHBOARD,
+ type: WorkflowTypeEnum.BRIDGE,
+ origin: WorkflowOriginEnum.NOVU_CLOUD,
+ steps: this.mapSteps(workflowDto.steps),
+ payloadSchema: {},
+ active: isWorkflowActive,
+ description: workflowDto.description || '',
+ tags: workflowDto.tags || [],
+ critical: false,
+ };
+ }
+
+ private async getWorkflowIfUpdateAndExist(upsertCommand: UpsertWorkflowCommand) {
+ if (upsertCommand.workflowDatabaseIdForUpdate) {
+ return await this.notificationTemplateRepository.findByIdQuery({
+ id: upsertCommand.workflowDatabaseIdForUpdate,
+ environmentId: upsertCommand.user.environmentId,
+ });
+ }
+ }
+
+ private async workflowExistByExternalId(upsertCommand: UpsertWorkflowCommand) {
+ const { environmentId } = upsertCommand.user;
+ const workflowByDbId = await this.notificationTemplateRepository.findByTriggerIdentifier(
+ environmentId,
+ upsertCommand.workflowDto.name
+ );
+
+ return !!workflowByDbId;
+ }
+
+ private convertCreateToUpdateCommand(
+ command: UpsertWorkflowCommand,
+ existingWorkflow: NotificationTemplateEntity
+ ): UpdateWorkflowCommand {
+ const { workflowDto } = command;
+ const { user } = command;
+
+ return {
+ id: existingWorkflow._id,
+ environmentId: user.environmentId,
+ organizationId: user.organizationId,
+ userId: user._id,
+ name: command.workflowDto.name,
+ steps: this.mapSteps(workflowDto.steps, existingWorkflow),
+ rawData: workflowDto,
+ type: WorkflowTypeEnum.BRIDGE,
+ description: workflowDto.description,
+ tags: workflowDto.tags,
+ active: workflowDto.active ?? true,
+ };
+ }
+
+ private mapSteps(
+ commandWorkflowSteps: Array,
+ persistedWorkflow?: NotificationTemplateEntity | undefined
+ ): NotificationStep[] {
+ const steps: NotificationStep[] = commandWorkflowSteps.map((step) => {
+ return this.mapSingleStep(persistedWorkflow, step);
+ });
+
+ return steps;
+ }
+
+ private mapSingleStep(
+ persistedWorkflow: NotificationTemplateEntity | undefined,
+ step: StepDto | (StepDto & { stepUuid: string })
+ ): NotificationStep {
+ const stepEntityToReturn = this.buildBaseStepEntity(step);
+ const foundPersistedStep = this.getPersistedStepIfFound(persistedWorkflow, step);
+ if (foundPersistedStep) {
+ return {
+ ...stepEntityToReturn,
+ _id: foundPersistedStep._templateId,
+ _templateId: foundPersistedStep._templateId,
+ template: { ...stepEntityToReturn.template, _id: foundPersistedStep._templateId },
+ };
+ }
+
+ return stepEntityToReturn;
+ }
+
+ private buildBaseStepEntity(step: StepDto | (StepDto & { stepUuid: string })) {
+ return {
+ template: {
+ type: step.type,
+ name: step.name,
+ controls: step.controls,
+ content: '',
+ },
+ name: step.name,
+ };
+ }
+
+ private getPersistedStepIfFound(
+ persistedWorkflow: NotificationTemplateEntity | undefined,
+ stepUpdateRequest: StepUpdateDto | StepCreateDto
+ ) {
+ if (!persistedWorkflow?.steps) {
+ return;
+ }
+
+ for (const persistedStep of persistedWorkflow.steps) {
+ if (this.isStepUpdateDto(stepUpdateRequest) && persistedStep._templateId === stepUpdateRequest.stepUuid) {
+ return persistedStep;
+ }
+ }
+ }
+
+ private isStepUpdateDto(obj: StepDto): obj is StepUpdateDto {
+ return typeof obj === 'object' && obj !== null && 'stepUuid' in obj;
+ }
+
+ private async getNotificationGroup(environmentId: string): Promise {
+ return (
+ await this.notificationGroupRepository.findOne(
+ {
+ name: 'General',
+ _environmentId: environmentId,
+ },
+ '_id'
+ )
+ )?._id;
+ }
+}
diff --git a/apps/api/src/app/workflows-v2/workflow.controller.e2e.ts b/apps/api/src/app/workflows-v2/workflow.controller.e2e.ts
new file mode 100644
index 00000000000..f9482963dcb
--- /dev/null
+++ b/apps/api/src/app/workflows-v2/workflow.controller.e2e.ts
@@ -0,0 +1,580 @@
+import { expect } from 'chai';
+import { UserSession } from '@novu/testing';
+import { DEFAULT_WORKFLOW_PREFERENCES, StepTypeEnum, WorkflowCreationSourceEnum } from '@novu/shared';
+import { randomBytes } from 'crypto';
+import { JsonSchema } from '@novu/framework';
+import {
+ ListWorkflowResponse,
+ StepCreateDto,
+ StepDto,
+ StepUpdateDto,
+ WorkflowCommonsFields,
+ WorkflowListResponseDto,
+} from './dto/workflow-commons-fields';
+import { WorkflowResponseDto } from './dto/workflow-response-dto';
+import { UpdateWorkflowDto } from './dto/update-workflow-dto';
+import { CreateWorkflowDto } from './dto/create-workflow-dto';
+
+const v2Prefix = '/v2';
+const PARTIAL_UPDATED_NAME = 'Updated';
+const TEST_WORKFLOW_UPDATED_NAME = `${PARTIAL_UPDATED_NAME} Workflow Name`;
+const TEST_WORKFLOW_NAME = 'Test Workflow Name';
+
+const TEST_TAGS = ['test'];
+let session: UserSession;
+
+const SCHEMA_WITH_TEXT: JsonSchema = {
+ type: 'object',
+ properties: {
+ text: {
+ type: 'string',
+ },
+ },
+ required: ['text'],
+};
+
+describe('Workflow Controller E2E API Testing', () => {
+ beforeEach(async () => {
+ // @ts-ignore
+ process.env.IS_WORKFLOW_PREFERENCES_ENABLED = 'true';
+ session = new UserSession();
+ await session.initialize();
+ });
+
+ it('Smoke Testing', async () => {
+ // @ts-ignore
+ process.env.IS_WORKFLOW_PREFERENCES_ENABLED = 'true';
+ const workflowCreated = await createWorkflowAndValidate();
+ await getWorkflowAndValidate(workflowCreated);
+ const updateRequest = buildUpdateRequest(workflowCreated);
+ await updateWorkflowAndValidate(workflowCreated._id, workflowCreated.updatedAt, updateRequest);
+ await updateWorkflowAndValidate(workflowCreated._id, workflowCreated.updatedAt, {
+ ...updateRequest,
+ description: 'Updated Description',
+ });
+ await getAllAndValidate({ searchQuery: PARTIAL_UPDATED_NAME, expectedTotalResults: 1, expectedArraySize: 1 });
+ await deleteWorkflowAndValidateDeletion(workflowCreated._id);
+ });
+
+ describe('Create Workflow Permutations', () => {
+ it('should not allow creating two workflows for the same user with the same name', async () => {
+ const nameSuffix = `Test Workflow${new Date().toString()}`;
+ await createWorkflowAndValidate(nameSuffix);
+ const createWorkflowDto: CreateWorkflowDto = buildCreateWorkflowDto(nameSuffix);
+ const res = await session.testAgent.post(`${v2Prefix}/workflows`).send(createWorkflowDto);
+ expect(res.status).to.be.equal(400);
+ expect(res.text).to.contain('Workflow with the same name already exists');
+ });
+ });
+
+ describe('Update Workflow Permutations', () => {
+ it('should update control values', async () => {
+ const nameSuffix = `Test Workflow${new Date().toString()}`;
+ const workflowCreated: WorkflowResponseDto = await createWorkflowAndValidate(nameSuffix);
+ const updateDtoWithValues = buildUpdateDtoWithValues(workflowCreated);
+ await updateWorkflowAndValidate(workflowCreated._id, workflowCreated.updatedAt, updateDtoWithValues);
+ });
+
+ it('should keep the step id on updated ', async () => {
+ const nameSuffix = `Test Workflow${new Date().toString()}`;
+ const workflowCreated: WorkflowResponseDto = await createWorkflowAndValidate(nameSuffix);
+ const updateDto = convertResponseToUpdateDto(workflowCreated);
+ const updatedWorkflow = await updateWorkflowRest(workflowCreated._id, updateDto);
+ const updatedStep = updatedWorkflow.steps[0];
+ const originalStep = workflowCreated.steps[0];
+ expect(updatedStep.stepUuid).to.be.ok;
+ expect(updatedStep.stepUuid).to.be.equal(originalStep.stepUuid);
+ });
+
+ it('adding user preferences', async () => {
+ const nameSuffix = `Test Workflow${new Date().toString()}`;
+ const workflowCreated: WorkflowResponseDto = await createWorkflowAndValidate(nameSuffix);
+ const updateDto = convertResponseToUpdateDto(workflowCreated);
+ const updatedWorkflow = await updateWorkflowRest(workflowCreated._id, {
+ ...updateDto,
+ preferences: {
+ user: { ...DEFAULT_WORKFLOW_PREFERENCES, all: { ...DEFAULT_WORKFLOW_PREFERENCES.all, enabled: false } },
+ },
+ });
+ expect(updatedWorkflow.preferences.user, JSON.stringify(updatedWorkflow, null, 2)).to.be.ok;
+ expect(updatedWorkflow.preferences?.user?.all.enabled, JSON.stringify(updatedWorkflow, null, 2)).to.be.false;
+
+ const updatedWorkflow2 = await updateWorkflowRest(workflowCreated._id, {
+ ...updateDto,
+ preferences: {
+ user: null,
+ },
+ });
+ expect(updatedWorkflow2.preferences.user).to.be.null;
+ expect(updatedWorkflow2.preferences.default).to.be.ok;
+ });
+ });
+
+ describe('List Workflow Permutations', () => {
+ it('should not return workflows with if not matching query', async () => {
+ await createWorkflowAndValidate('XYZ');
+ await createWorkflowAndValidate('XYZ2');
+ const workflowSummaries = await getAllAndValidate({
+ searchQuery: 'ABC',
+ expectedTotalResults: 0,
+ expectedArraySize: 0,
+ });
+ expect(workflowSummaries).to.be.empty;
+ });
+ it('should not return workflows if offset is bigger than the amount of available workflows', async () => {
+ const uuid = generateUUID();
+ await create10Workflows(uuid);
+ const listWorkflowResponse = await getAllAndValidate({
+ searchQuery: uuid,
+ offset: 11,
+ limit: 15,
+ expectedTotalResults: 10,
+ expectedArraySize: 0,
+ });
+ });
+ it('should return all results within range', async () => {
+ const uuid = generateUUID();
+
+ await create10Workflows(uuid);
+ const listWorkflowResponse = await getAllAndValidate({
+ searchQuery: uuid,
+ offset: 0,
+ limit: 15,
+ expectedTotalResults: 10,
+ expectedArraySize: 10,
+ });
+ });
+
+ it('should return results without query', async () => {
+ const uuid = generateUUID();
+ await create10Workflows(uuid);
+ const listWorkflowResponse = await getAllAndValidate({
+ searchQuery: uuid,
+ offset: 0,
+ limit: 15,
+ expectedTotalResults: 10,
+ expectedArraySize: 10,
+ });
+ });
+
+ it('page workflows without overlap', async () => {
+ const uuid = generateUUID();
+ await create10Workflows(uuid);
+ const listWorkflowResponse1 = await getAllAndValidate({
+ searchQuery: uuid,
+ offset: 0,
+ limit: 5,
+ expectedTotalResults: 10,
+ expectedArraySize: 5,
+ });
+ const listWorkflowResponse2 = await getAllAndValidate({
+ searchQuery: uuid,
+ offset: 5,
+ limit: 5,
+ expectedTotalResults: 10,
+ expectedArraySize: 5,
+ });
+ const idsDeduplicated = buildIdSet(listWorkflowResponse1, listWorkflowResponse2);
+ expect(idsDeduplicated.size).to.be.equal(10);
+ });
+ });
+});
+
+function buildErrorMsg(createWorkflowDto: Omit, createdWorkflowWithoutUpdateDate) {
+ return `created workflow does not match as expected
+ Original:
+ ${JSON.stringify(createWorkflowDto, null, 2)}
+ Returned:
+ ${JSON.stringify(createdWorkflowWithoutUpdateDate, null, 2)}
+
+ `;
+}
+
+async function createWorkflowAndValidate(nameSuffix: string = ''): Promise {
+ const createWorkflowDto: CreateWorkflowDto = buildCreateWorkflowDto(nameSuffix);
+ console.log('createWorkflowDto', JSON.stringify(createWorkflowDto, null, 2));
+ const res = await session.testAgent.post(`${v2Prefix}/workflows`).send(createWorkflowDto);
+ const workflowResponseDto: WorkflowResponseDto = res.body.data;
+ expect(workflowResponseDto, JSON.stringify(res, null, 2)).to.be.ok;
+ expect(workflowResponseDto._id, JSON.stringify(res, null, 2)).to.be.ok;
+ expect(workflowResponseDto.updatedAt, JSON.stringify(res, null, 2)).to.be.ok;
+ expect(workflowResponseDto.createdAt, JSON.stringify(res, null, 2)).to.be.ok;
+ expect(workflowResponseDto.preferences, JSON.stringify(res, null, 2)).to.be.ok;
+ const createdWorkflowWithoutUpdateDate = removeFields(
+ workflowResponseDto,
+ '_id',
+ 'origin',
+ 'preferences',
+ 'updatedAt',
+ 'createdAt'
+ );
+ createdWorkflowWithoutUpdateDate.steps = createdWorkflowWithoutUpdateDate.steps.map((step) =>
+ removeFields(step, 'stepUuid')
+ );
+ expect(createdWorkflowWithoutUpdateDate).to.deep.equal(
+ removeFields(createWorkflowDto, '__source'),
+ buildErrorMsg(createWorkflowDto, createdWorkflowWithoutUpdateDate)
+ );
+
+ return workflowResponseDto;
+}
+
+function buildEmailStep(): StepDto {
+ return {
+ controlValues: {},
+ name: 'Email Test Step',
+ type: StepTypeEnum.EMAIL,
+ };
+}
+
+function buildInAppStep(): StepDto {
+ return {
+ controlValues: {},
+ name: 'In-App Test Step',
+ type: StepTypeEnum.IN_APP,
+ };
+}
+
+function buildCreateWorkflowDto(nameSuffix: string): CreateWorkflowDto {
+ return {
+ __source: WorkflowCreationSourceEnum.EDITOR,
+ name: TEST_WORKFLOW_NAME + nameSuffix,
+ description: 'This is a test workflow',
+ active: true,
+ tags: TEST_TAGS,
+ steps: [buildEmailStep(), buildInAppStep()],
+ };
+}
+
+async function updateWorkflowRest(id: string, workflow: UpdateWorkflowDto): Promise {
+ console.log(`updateWorkflow- ${id}:
+ ${JSON.stringify(workflow, null, 2)}`);
+
+ return await safePut(`${v2Prefix}/workflows/${id}`, workflow);
+}
+
+function convertToDate(dateString: string) {
+ const timestamp = Date.parse(dateString);
+
+ return new Date(timestamp);
+}
+
+function isStepUpdateDto(obj: StepDto): obj is StepUpdateDto {
+ return typeof obj === 'object' && obj !== null && 'stepUuid' in obj;
+}
+
+function buildStepWithoutUUid(stepInResponse: StepDto & { stepUuid: string }) {
+ if (!stepInResponse.controls) {
+ return {
+ controlValues: stepInResponse.controlValues,
+ name: stepInResponse.name,
+ type: stepInResponse.type,
+ };
+ }
+
+ return {
+ controlValues: stepInResponse.controlValues,
+ controls: stepInResponse.controls,
+ name: stepInResponse.name,
+ type: stepInResponse.type,
+ };
+}
+
+function findStepOnRequestBasedOnId(workflowUpdateRequest: UpdateWorkflowDto, stepUuid: string) {
+ for (const stepInRequest of workflowUpdateRequest.steps) {
+ if (isStepUpdateDto(stepInRequest) && stepInRequest.stepUuid === stepUuid) {
+ return stepInRequest;
+ }
+ }
+
+ return undefined;
+}
+
+function validateUpdatedWorkflowAndRemoveResponseFields(
+ workflowResponse: WorkflowResponseDto,
+ workflowUpdateRequest: UpdateWorkflowDto
+): UpdateWorkflowDto {
+ const updatedWorkflowWoUpdated: UpdateWorkflowDto = removeFields(workflowResponse, 'updatedAt', 'origin', '_id');
+ const augmentedStep: (StepUpdateDto | StepCreateDto)[] = [];
+ for (const stepInResponse of workflowResponse.steps) {
+ expect(stepInResponse.stepUuid).to.be.ok;
+ const { stepUuid } = stepInResponse;
+ const stepOnRequestBasedOnId = findStepOnRequestBasedOnId(workflowUpdateRequest, stepUuid);
+ if (!stepOnRequestBasedOnId) {
+ augmentedStep.push(buildStepWithoutUUid(stepInResponse));
+ } else {
+ augmentedStep.push({ ...stepInResponse });
+ }
+ }
+ updatedWorkflowWoUpdated.steps = [...augmentedStep];
+
+ return updatedWorkflowWoUpdated;
+}
+
+async function updateWorkflowAndValidate(
+ id: string,
+ updatedAt: string,
+ updateRequest: UpdateWorkflowDto
+): Promise {
+ console.log('updateRequest:::'.toUpperCase(), JSON.stringify(updateRequest.steps, null, 2));
+ const updatedWorkflow: WorkflowResponseDto = await updateWorkflowRest(id, updateRequest);
+ const updatedWorkflowWithResponseFieldsRemoved = validateUpdatedWorkflowAndRemoveResponseFields(
+ updatedWorkflow,
+ updateRequest
+ );
+ expect(updatedWorkflowWithResponseFieldsRemoved, 'workflow after update does not match as expected').to.deep.equal(
+ updateRequest
+ );
+ expect(convertToDate(updatedWorkflow.updatedAt)).to.be.greaterThan(convertToDate(updatedAt));
+}
+
+function parseAndReturnJson(res: ApiResponse, url: string) {
+ let parse: any;
+ try {
+ parse = JSON.parse(res.text);
+ } catch (e) {
+ expect.fail(
+ '',
+ '',
+ `'Expected response to be JSON' text: ${res.text}, url: ${url}, method: ${res.req.method}, status: ${res.status}`
+ );
+ }
+ expect(parse).to.be.ok;
+
+ return parse.data;
+}
+
+async function safeRest(
+ url: string,
+ method: () => Promise,
+ expectedStatus: number = 200
+): Promise {
+ const res: ApiResponse = await method();
+ expect(res.status).to.eq(
+ expectedStatus,
+ `[${res.req.method}] Failed for URL: ${url}
+ with text:
+ ${res.text}
+ full response:
+ ${JSON.stringify(res, null, 2)}`
+ ); // Check if the status code is 200
+
+ if (res.status !== 200) {
+ return res.text;
+ }
+
+ return parseAndReturnJson(res, url);
+}
+
+async function getWorkflowRest(
+ workflowCreated: WorkflowCommonsFields & { updatedAt: string }
+): Promise {
+ return await safeGet(`${v2Prefix}/workflows/${workflowCreated._id}`);
+}
+
+async function validateWorkflowDeleted(workflowId: string): Promise {
+ await session.testAgent.get(`${v2Prefix}/workflows/${workflowId}`).expect(400);
+}
+
+async function getWorkflowAndValidate(workflowCreated: WorkflowResponseDto) {
+ const workflowRetrieved = await getWorkflowRest(workflowCreated);
+ expect(workflowRetrieved).to.deep.equal(workflowCreated);
+}
+
+async function getListWorkflows(query: string, offset: number, limit: number): Promise {
+ return await safeGet(`${v2Prefix}/workflows?query=${query}&offset=${offset}&limit=${limit}`);
+}
+
+// eslint-disable-next-line @typescript-eslint/naming-convention
+interface AllAndValidate {
+ msgPrefix?: string;
+ searchQuery: string;
+ offset?: number;
+ limit?: number;
+ expectedTotalResults: number;
+ expectedArraySize: number;
+}
+
+function buildLogMsg(
+ { msgPrefix = '', searchQuery = '', offset = 0, limit = 50, expectedTotalResults, expectedArraySize }: AllAndValidate,
+ listWorkflowResponse: ListWorkflowResponse
+): string {
+ return `Log - msgPrefix: ${msgPrefix},
+ searchQuery: ${searchQuery},
+ offset: ${offset},
+ limit: ${limit},
+ expectedTotalResults: ${expectedTotalResults ?? 'Not specified'},
+ expectedArraySize: ${expectedArraySize ?? 'Not specified'}
+ response:
+ ${JSON.stringify(listWorkflowResponse || 'Not specified', null, 2)}`;
+}
+
+async function getAllAndValidate({
+ msgPrefix = '',
+ searchQuery = '',
+ offset = 0,
+ limit = 50,
+ expectedTotalResults,
+ expectedArraySize,
+}: AllAndValidate): Promise {
+ const listWorkflowResponse: ListWorkflowResponse = await getListWorkflows(searchQuery, offset, limit);
+ const summery: string = buildLogMsg(
+ {
+ msgPrefix,
+ searchQuery,
+ offset,
+ limit,
+ expectedTotalResults,
+ expectedArraySize,
+ },
+ listWorkflowResponse
+ );
+ expect(listWorkflowResponse.workflows).to.be.an('array', summery);
+ expect(listWorkflowResponse.workflows).lengthOf(expectedArraySize, ` workflowSummaries length${summery}`);
+ expect(listWorkflowResponse.totalCount).to.be.equal(expectedTotalResults, `total Results don't match${summery}`);
+
+ return listWorkflowResponse.workflows;
+}
+
+async function deleteWorkflowRest(_id: string): Promise {
+ await safeDelete(`${v2Prefix}/workflows/${_id}`);
+}
+
+async function deleteWorkflowAndValidateDeletion(_id: string): Promise {
+ await deleteWorkflowRest(_id);
+ await validateWorkflowDeleted(_id);
+}
+
+function extractIDs(workflowSummaries: WorkflowListResponseDto[]) {
+ return workflowSummaries.map((workflow) => workflow._id);
+}
+
+function buildIdSet(
+ listWorkflowResponse1: WorkflowListResponseDto[],
+ listWorkflowResponse2: WorkflowListResponseDto[]
+) {
+ return new Set([...extractIDs(listWorkflowResponse1), ...extractIDs(listWorkflowResponse2)]);
+}
+
+async function create10Workflows(prefix: string) {
+ // eslint-disable-next-line no-plusplus
+ for (let i = 0; i < 10; i++) {
+ await createWorkflowAndValidate(`${prefix}-ABC${i}`);
+ }
+}
+function removeFields(obj: T, ...keysToRemove: (keyof T)[]): T {
+ const objCopy = JSON.parse(JSON.stringify(obj));
+ keysToRemove.forEach((key) => {
+ delete objCopy[key as keyof T];
+ });
+
+ return objCopy;
+}
+// eslint-disable-next-line @typescript-eslint/naming-convention
+interface ApiResponse {
+ req: {
+ method: string; // e.g., "GET"
+ url: string; // e.g., "http://127.0.0.1:1337/v1/v2/workflows/66e929c6667852862a1e5145"
+ headers: {
+ authorization: string; // e.g., "Bearer eyJhbGciOiJIUzI1NiIsInR5cCI6IkpX5cJ9..."
+ 'novu-environment-id': string; // e.g., "66e929c6667852862a1e50e4"
+ };
+ };
+ header: {
+ 'content-security-policy': string;
+ 'cross-origin-embedder-policy': string;
+ 'cross-origin-opener-policy': string;
+ 'cross-origin-resource-policy': string;
+ 'x-dns-prefetch-control': string;
+ 'x-frame-options': string;
+ 'strict-transport-security': string;
+ 'x-download-options': string;
+ 'x-content-type-options': string;
+ 'origin-agent-cluster': string;
+ 'x-permitted-cross-domain-policies': string;
+ 'referrer-policy': string;
+ 'x-xss-protection': string;
+ 'access-control-allow-origin': string;
+ 'content-type': string;
+ 'content-length': string;
+ etag: string;
+ vary: string;
+ date: string;
+ connection: string;
+ };
+ status: number; // e.g., 400
+ text: string; // e.g., "{\"message\":\"Workflow not found with id: 66e929c6667852862a1e5145\",\"error\":\"Bad Request\",\"statusCode\":400}"
+}
+async function safeGet(url: string): Promise {
+ return (await safeRest(url, () => session.testAgent.get(url) as unknown as Promise)) as T;
+}
+async function safePut(url: string, data: object): Promise {
+ return (await safeRest(url, () => session.testAgent.put(url).send(data) as unknown as Promise)) as T;
+}
+async function safePost(url: string, data: object): Promise {
+ return (await safeRest(url, () => session.testAgent.post(url).send(data) as unknown as Promise)) as T;
+}
+async function safeDelete(url: string): Promise {
+ await safeRest(url, () => session.testAgent.delete(url) as unknown as Promise, 204);
+}
+function generateUUID(): string {
+ // Generate a random 4-byte hex string
+ const randomHex = () => randomBytes(2).toString('hex');
+
+ // Construct the UUID using the random hex values
+ return `${randomHex()}${randomHex()}-${randomHex()}-${randomHex()}-${randomHex()}-${randomHex()}${randomHex()}${randomHex()}`;
+}
+function addValueToExistingStep(steps: (StepCreateDto | StepUpdateDto)[]): StepDto {
+ const stepToUpdate = steps[0];
+ stepToUpdate.name = `Updated Step Name- ${generateUUID()}`;
+ stepToUpdate.controlValues = { test: `test-${generateUUID()}` };
+
+ return stepToUpdate;
+}
+
+function buildInAppStepWithValues() {
+ const stepDto = buildInAppStep();
+ stepDto.controlValues = { testNew: [`testNew -${generateUUID()}`] };
+
+ return stepDto;
+}
+
+function convertResponseToUpdateDto(workflowCreated: WorkflowResponseDto): UpdateWorkflowDto {
+ return removeFields(workflowCreated, 'updatedAt', '_id', 'origin') as UpdateWorkflowDto;
+}
+
+function buildUpdateDtoWithValues(workflowCreated: WorkflowResponseDto): UpdateWorkflowDto {
+ const updateDto = convertResponseToUpdateDto(workflowCreated);
+ const updatedStep = addValueToExistingStep(updateDto.steps);
+ const newStep = buildInAppStepWithValues();
+ console.log('newStep:::', JSON.stringify(newStep, null, 2));
+
+ const stoWithValues: UpdateWorkflowDto = {
+ ...updateDto,
+ name: `${TEST_WORKFLOW_UPDATED_NAME}-${generateUUID()}`,
+ steps: [updatedStep, newStep],
+ };
+
+ console.log('updateDto:::', JSON.stringify(stoWithValues, null, 2));
+
+ return stoWithValues;
+}
+function createStep(): StepCreateDto {
+ return {
+ name: 'someStep',
+ type: StepTypeEnum.SMS,
+ controls: {
+ schema: SCHEMA_WITH_TEXT,
+ },
+ controlValues: {
+ text: '{SOME_TEXT_VARIABLE}',
+ },
+ };
+}
+
+function buildUpdateRequest(workflowCreated: WorkflowResponseDto): UpdateWorkflowDto {
+ const steps = [createStep()];
+ const updateRequest = removeFields(workflowCreated, 'updatedAt', '_id', 'origin') as UpdateWorkflowDto;
+
+ return { ...updateRequest, name: TEST_WORKFLOW_UPDATED_NAME, steps };
+}
diff --git a/apps/api/src/app/workflows-v2/workflow.controller.ts b/apps/api/src/app/workflows-v2/workflow.controller.ts
new file mode 100644
index 00000000000..01928afe5e2
--- /dev/null
+++ b/apps/api/src/app/workflows-v2/workflow.controller.ts
@@ -0,0 +1,112 @@
+import {
+ Body,
+ ClassSerializerInterceptor,
+ Controller,
+ Delete,
+ Get,
+ HttpCode,
+ HttpStatus,
+ Param,
+ Post,
+ Put,
+ Query,
+ UseGuards,
+ UseInterceptors,
+} from '@nestjs/common';
+
+import { ApiTags } from '@nestjs/swagger';
+import { DirectionEnum, UserSessionData } from '@novu/shared';
+import { ExternalApiAccessible, 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';
+import { UpsertWorkflowUseCase } from './usecases/upsert-workflow/upsert-workflow.usecase';
+import { UpsertWorkflowCommand } from './usecases/upsert-workflow/upsert-workflow.command';
+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 { ListWorkflowResponse } from './dto/workflow-commons-fields';
+import { DeleteWorkflowUseCase } from './usecases/delete-workflow/delete-workflow.usecase';
+import { DeleteWorkflowCommand } from './usecases/delete-workflow/delete-workflow.command';
+import { GetListQueryParams } from './params/get-list-query-params';
+import { CreateWorkflowDto } from './dto/create-workflow-dto';
+import { UpdateWorkflowDto } from './dto/update-workflow-dto';
+import { WorkflowResponseDto } from './dto/workflow-response-dto';
+
+@ApiCommonResponses()
+@Controller({ path: `/workflows`, version: '2' })
+@UseInterceptors(ClassSerializerInterceptor)
+@UserAuthentication()
+@ApiTags('Workflows')
+export class WorkflowController {
+ constructor(
+ private upsertWorkflowUseCase: UpsertWorkflowUseCase,
+ private getWorkflowUseCase: GetWorkflowUseCase,
+ private listWorkflowsUseCase: ListWorkflowsUseCase,
+ private deleteWorkflowUsecase: DeleteWorkflowUseCase
+ ) {}
+
+ @Post('')
+ @UseGuards(UserAuthGuard)
+ async create(
+ @UserSession() user: UserSessionData,
+ @Body() createWorkflowDto: CreateWorkflowDto
+ ): Promise {
+ return this.upsertWorkflowUseCase.execute(
+ UpsertWorkflowCommand.create({
+ workflowDto: createWorkflowDto,
+ user,
+ })
+ );
+ }
+
+ @Put(':workflowId')
+ @UseGuards(UserAuthGuard)
+ async update(
+ @UserSession() user: UserSessionData,
+ @Param('workflowId') workflowId: string,
+ @Body() updateWorkflowDto: UpdateWorkflowDto
+ ): Promise {
+ return await this.upsertWorkflowUseCase.execute(
+ UpsertWorkflowCommand.create({
+ workflowDto: updateWorkflowDto,
+ user,
+ workflowDatabaseIdForUpdate: workflowId,
+ })
+ );
+ }
+
+ @Get(':workflowId')
+ @UseGuards(UserAuthGuard)
+ async getWorkflow(
+ @UserSession() user: UserSessionData,
+ @Param('workflowId') workflowId: string
+ ): Promise {
+ return this.getWorkflowUseCase.execute(GetWorkflowCommand.create({ _workflowId: workflowId, user }));
+ }
+
+ @Delete(':workflowId')
+ @ExternalApiAccessible()
+ @HttpCode(HttpStatus.NO_CONTENT)
+ async removeWorkflow(@UserSession() user: UserSessionData, @Param('workflowId') workflowId: string) {
+ await this.deleteWorkflowUsecase.execute(DeleteWorkflowCommand.create({ workflowId, user }));
+ }
+
+ @Get('')
+ @UseGuards(UserAuthGuard)
+ async searchWorkflows(
+ @UserSession() user: UserSessionData,
+ @Query() query: GetListQueryParams
+ ): Promise {
+ return this.listWorkflowsUseCase.execute(
+ ListWorkflowsCommand.create({
+ offset: Number(query.offset || '0'),
+ limit: Number(query.limit || '50'),
+ orderDirection: query.orderDirection ?? DirectionEnum.DESC,
+ orderByField: query.orderByField ?? 'createdAt',
+ searchQuery: query.query,
+ user,
+ })
+ );
+ }
+}
diff --git a/apps/api/src/app/workflows-v2/workflow.module.ts b/apps/api/src/app/workflows-v2/workflow.module.ts
new file mode 100644
index 00000000000..a90d4a7370a
--- /dev/null
+++ b/apps/api/src/app/workflows-v2/workflow.module.ts
@@ -0,0 +1,37 @@
+import { MiddlewareConsumer, Module, NestModule } from '@nestjs/common';
+import {
+ CreateWorkflow,
+ GetPreferences,
+ UpdateWorkflow,
+ UpsertControlValuesUseCase,
+ UpsertPreferences,
+} from '@novu/application-generic';
+import { SharedModule } from '../shared/shared.module';
+import { MessageTemplateModule } from '../message-template/message-template.module';
+import { ChangeModule } from '../change/change.module';
+import { AuthModule } from '../auth/auth.module';
+import { IntegrationModule } from '../integrations/integrations.module';
+import { WorkflowController } from './workflow.controller';
+import { UpsertWorkflowUseCase } from './usecases/upsert-workflow/upsert-workflow.usecase';
+import { GetWorkflowUseCase } from './usecases/get-workflow/get-workflow.usecase';
+import { ListWorkflowsUseCase } from './usecases/list-workflows/list-workflow.usecase';
+import { DeleteWorkflowUseCase } from './usecases/delete-workflow/delete-workflow.usecase';
+
+@Module({
+ imports: [SharedModule, MessageTemplateModule, ChangeModule, AuthModule, IntegrationModule],
+ controllers: [WorkflowController],
+ providers: [
+ CreateWorkflow,
+ UpdateWorkflow,
+ UpsertWorkflowUseCase,
+ GetWorkflowUseCase,
+ ListWorkflowsUseCase,
+ DeleteWorkflowUseCase,
+ UpsertPreferences,
+ UpsertControlValuesUseCase,
+ GetPreferences,
+ ],
+})
+export class WorkflowModule implements NestModule {
+ configure(consumer: MiddlewareConsumer): MiddlewareConsumer | void {}
+}
diff --git a/apps/api/src/bootstrap.ts b/apps/api/src/bootstrap.ts
index 6a83edcc471..a4eb0e36ffd 100644
--- a/apps/api/src/bootstrap.ts
+++ b/apps/api/src/bootstrap.ts
@@ -3,20 +3,20 @@ import 'newrelic';
import '@sentry/tracing';
import helmet from 'helmet';
-import { INestApplication, Logger, ValidationPipe } from '@nestjs/common';
+import { INestApplication, Logger, ValidationPipe, VersioningType } from '@nestjs/common';
import { NestFactory, Reflector } from '@nestjs/core';
import bodyParser from 'body-parser';
-import { init, Integrations, Handlers } from '@sentry/node';
+import { Handlers, init, Integrations } from '@sentry/node';
import { BullMqService, getErrorInterceptor, Logger as PinoLogger } from '@novu/application-generic';
import { ExpressAdapter } from '@nestjs/platform-express';
-import { validateEnv, CONTEXT_PATH, corsOptionsDelegate } from './config';
+import { CONTEXT_PATH, corsOptionsDelegate, validateEnv } from './config';
import { AppModule } from './app.module';
-import { ResponseInterceptor } from './app/shared/framework/response.interceptor';
-import { SubscriberRouteGuard } from './app/auth/framework/subscriber-route.guard';
import packageJson from '../package.json';
import { setupSwagger } from './app/shared/framework/swagger/swagger.controller';
+import { SubscriberRouteGuard } from './app/auth/framework/subscriber-route.guard';
+import { ResponseInterceptor } from './app/shared/framework/response.interceptor';
const passport = require('passport');
const compression = require('compression');
@@ -72,6 +72,12 @@ export async function bootstrap(expressApp?): Promise {
app = await NestFactory.create(AppModule, { bufferLogs: true, ...nestOptions });
}
+ app.enableVersioning({
+ type: VersioningType.URI,
+ prefix: `${CONTEXT_PATH}v`,
+ defaultVersion: '1',
+ });
+
app.useLogger(app.get(PinoLogger));
app.flushLogs();
@@ -90,8 +96,6 @@ export async function bootstrap(expressApp?): Promise {
app.use(helmet());
app.enableCors(corsOptionsDelegate);
- app.setGlobalPrefix(`${CONTEXT_PATH}v1`);
-
app.use(passport.initialize());
app.useGlobalPipes(
diff --git a/apps/web/src/api/hooks/notification-templates/useCreateDigestDemoWorkflow.ts b/apps/web/src/api/hooks/notification-templates/useCreateDigestDemoWorkflow.ts
index b45c71105e2..e36cdf34168 100644
--- a/apps/web/src/api/hooks/notification-templates/useCreateDigestDemoWorkflow.ts
+++ b/apps/web/src/api/hooks/notification-templates/useCreateDigestDemoWorkflow.ts
@@ -1,7 +1,7 @@
import { useCallback } from 'react';
import { useMutation } from '@tanstack/react-query';
import { useNavigate } from 'react-router-dom';
-import { StepTypeEnum } from '@novu/shared';
+import { StepTypeEnum, WorkflowCreationSourceEnum } from '@novu/shared';
import type { IResponseError, ICreateNotificationTemplateDto, INotificationTemplate } from '@novu/shared';
import { v4 as uuid4 } from 'uuid';
@@ -10,7 +10,6 @@ import { parseUrl } from '../../../utils/routeUtils';
import { ROUTES } from '../../../constants/routes';
import { errorMessage } from '../../../utils/notifications';
import { useNotificationGroup, useTemplates } from '../../../hooks';
-import { TemplateCreationSourceEnum } from '../../../pages/templates/shared';
import { FIRST_100_WORKFLOWS } from '../../../constants/workflowConstants';
export const useCreateDigestDemoWorkflow = () => {
@@ -72,7 +71,7 @@ export const useCreateDigestDemoWorkflow = () => {
createNotificationTemplate({
template: payload as any,
- params: { __source: TemplateCreationSourceEnum.ONBOARDING_DIGEST_DEMO },
+ params: { __source: WorkflowCreationSourceEnum.ONBOARDING_DIGEST_DEMO },
});
}
}, [createNotificationTemplate, navigate, templatesLoading, groups, templates]);
diff --git a/apps/web/src/components/quick-start/in-app-onboarding/TriggerNode.tsx b/apps/web/src/components/quick-start/in-app-onboarding/TriggerNode.tsx
index 993e9bd9ffb..e910350b6cb 100644
--- a/apps/web/src/components/quick-start/in-app-onboarding/TriggerNode.tsx
+++ b/apps/web/src/components/quick-start/in-app-onboarding/TriggerNode.tsx
@@ -3,7 +3,13 @@ import { BoltOutlinedGradient, Button, colors, Playground, shadows, Text, Title
import styled from '@emotion/styled';
import { createStyles, Group, Popover, Stack, useMantineColorScheme } from '@mantine/core';
import type { INotificationTemplate, IResponseError } from '@novu/shared';
-import { ActorTypeEnum, ICreateNotificationTemplateDto, StepTypeEnum, SystemAvatarIconEnum } from '@novu/shared';
+import {
+ ActorTypeEnum,
+ ICreateNotificationTemplateDto,
+ StepTypeEnum,
+ SystemAvatarIconEnum,
+ WorkflowCreationSourceEnum,
+} from '@novu/shared';
import { useMutation } from '@tanstack/react-query';
import { useEffect, useState } from 'react';
import { useParams } from 'react-router-dom';
@@ -18,7 +24,6 @@ import {
import { NodeStep } from '../../workflow';
import { useSegment } from '../../providers/SegmentProvider';
import { errorMessage } from '../../../utils/notifications';
-import { TemplateCreationSourceEnum } from '../../../pages/templates/shared';
import { FIRST_100_WORKFLOWS } from '../../../constants/workflowConstants';
const useStyles = createStyles((theme) => ({
@@ -101,7 +106,7 @@ function TriggerButton({ setOpened }: { setOpened: (value: boolean) => void }) {
createNotificationTemplate({
template: payloadToCreate as unknown as ICreateNotificationTemplateDto,
- params: { __source: TemplateCreationSourceEnum.ONBOARDING_IN_APP },
+ params: { __source: WorkflowCreationSourceEnum.ONBOARDING_IN_APP },
});
}, hasToCreateOnboardingTemplate);
diff --git a/apps/web/src/hooks/useCreateWorkflowFromBlueprint.ts b/apps/web/src/hooks/useCreateWorkflowFromBlueprint.ts
index 72cea384109..7c26ed88ed4 100644
--- a/apps/web/src/hooks/useCreateWorkflowFromBlueprint.ts
+++ b/apps/web/src/hooks/useCreateWorkflowFromBlueprint.ts
@@ -1,10 +1,9 @@
import slugify from 'slugify';
import { useMutation, UseMutationOptions } from '@tanstack/react-query';
-import type { INotificationTemplate } from '@novu/shared';
+import { WorkflowCreationSourceEnum, type INotificationTemplate } from '@novu/shared';
import { useCreateTemplateFromBlueprint } from '../api/hooks';
import { getBlueprintTemplateById, getTemplateById } from '../api/notification-templates';
-import { TemplateCreationSourceEnum } from '../pages/templates/shared';
import { getWorkflowBlueprintDetails } from '../utils';
export const useCreateWorkflowFromBlueprint = (
@@ -27,7 +26,7 @@ export const useCreateWorkflowFromBlueprint = (
} catch (_error) {
return await createTemplateFromBlueprint({
blueprint: { ...blueprintData, name: blueprintName },
- params: { __source: TemplateCreationSourceEnum.ONBOARDING_GET_STARTED },
+ params: { __source: WorkflowCreationSourceEnum.ONBOARDING_GET_STARTED },
});
}
},
diff --git a/apps/web/src/pages/get-started/legacy-onboarding/components/OpenWorkflowButton.tsx b/apps/web/src/pages/get-started/legacy-onboarding/components/OpenWorkflowButton.tsx
index 1d159f1933c..3f3f109d964 100644
--- a/apps/web/src/pages/get-started/legacy-onboarding/components/OpenWorkflowButton.tsx
+++ b/apps/web/src/pages/get-started/legacy-onboarding/components/OpenWorkflowButton.tsx
@@ -1,12 +1,12 @@
import { errorMessage } from '@novu/design-system';
-import { TemplateCreationSourceEnum } from '../../../templates/shared/index';
import { useSegment } from '../../../../components/providers/SegmentProvider';
import { OnboardingWorkflowRouteEnum } from '../consts/types';
import { LinkButton } from '../consts/shared';
import { useCreateWorkflowFromBlueprint } from '../../../../hooks/index';
import { openInNewTab } from '../../../../utils/index';
import { buildWorkflowEditorUrl } from '../utils/workflowEditorUrl';
+import { WorkflowCreationSourceEnum } from '@novu/shared';
export function OpenWorkflowButton({
blueprintIdentifier,
@@ -30,7 +30,7 @@ export function OpenWorkflowButton({
const handleOpenWorkflowClick = () => {
segment.track('[Get Started] Click Create Notification Template', {
templateIdentifier: blueprintIdentifier,
- location: TemplateCreationSourceEnum.ONBOARDING_GET_STARTED,
+ location: WorkflowCreationSourceEnum.ONBOARDING_GET_STARTED,
});
createWorkflowFromBlueprint({ blueprintIdentifier });
};
diff --git a/apps/web/src/pages/templates/TemplatesListNoData.tsx b/apps/web/src/pages/templates/TemplatesListNoData.tsx
index 25f660202f1..3a6133d1847 100644
--- a/apps/web/src/pages/templates/TemplatesListNoData.tsx
+++ b/apps/web/src/pages/templates/TemplatesListNoData.tsx
@@ -3,11 +3,12 @@ import { FontAwesomeIcon } from '@fortawesome/react-fontawesome';
import { faDiagramNext } from '@fortawesome/free-solid-svg-icons';
import { faFile } from '@fortawesome/free-regular-svg-icons';
import { Skeleton } from '@mantine/core';
+
import { CardTile, colors, Popover } from '@novu/design-system';
+import { WorkflowCreationSourceEnum } from '@novu/shared';
import { useSegment } from '../../components/providers/SegmentProvider';
import { IBlueprintTemplate } from '../../api/types';
-import { TemplateCreationSourceEnum } from './shared';
import { useHoverOverItem } from '../../hooks';
import { FrameworkProjectCardTile } from './components/FrameworkProjectWaitList';
@@ -81,7 +82,7 @@ export const TemplatesListNoData = ({
onClick={(event) => {
segment.track('[Template Store] Click Create Notification Template', {
templateIdentifier: 'Blank Workflow',
- location: TemplateCreationSourceEnum.EMPTY_STATE,
+ location: WorkflowCreationSourceEnum.EMPTY_STATE,
});
onBlankWorkflowClick(event);
@@ -117,7 +118,7 @@ export const TemplatesListNoData = ({
onClick={() => {
segment.track('[Template Store] Click Create Notification Template', {
templateIdentifier: template?.triggers[0]?.identifier || '',
- location: TemplateCreationSourceEnum.EMPTY_STATE,
+ location: WorkflowCreationSourceEnum.EMPTY_STATE,
});
onTemplateClick(template);
@@ -138,7 +139,7 @@ export const TemplatesListNoData = ({
data-test-id="all-workflow-tile"
onClick={(event) => {
segment.track('[Template Store] Click Open Template Store', {
- location: TemplateCreationSourceEnum.EMPTY_STATE,
+ location: WorkflowCreationSourceEnum.EMPTY_STATE,
});
onAllTemplatesClick(event);
diff --git a/apps/web/src/pages/templates/WorkflowListPage.tsx b/apps/web/src/pages/templates/WorkflowListPage.tsx
index d707f0a1634..54a4c774b7e 100644
--- a/apps/web/src/pages/templates/WorkflowListPage.tsx
+++ b/apps/web/src/pages/templates/WorkflowListPage.tsx
@@ -19,7 +19,7 @@ import {
Tooltip,
SearchInput,
} from '@novu/design-system';
-import { FeatureFlagsKeysEnum } from '@novu/shared';
+import { FeatureFlagsKeysEnum, WorkflowCreationSourceEnum } from '@novu/shared';
import { css } from '@novu/novui/css';
import { Button } from '@novu/novui';
@@ -41,7 +41,6 @@ import { useFetchBlueprints, useCreateTemplateFromBlueprint } from '../../api/ho
import { CreateWorkflowDropdown } from './components/CreateWorkflowDropdown';
import { IBlueprintTemplate } from '../../api/types';
import { errorMessage } from '../../utils/notifications';
-import { TemplateCreationSourceEnum } from './shared';
import { When } from '../../components/utils/When';
import { ListPage } from '../../components/layout/components/ListPage';
import { WorkflowListNoMatches } from './WorkflowListNoMatches';
@@ -226,7 +225,7 @@ function WorkflowListPage() {
const handleOnBlueprintClick = (blueprint: IBlueprintTemplate) => {
createTemplateFromBlueprint({
blueprint: { ...blueprint },
- params: { __source: TemplateCreationSourceEnum.TEMPLATE_STORE },
+ params: { __source: WorkflowCreationSourceEnum.TEMPLATE_STORE },
});
};
diff --git a/apps/web/src/pages/templates/components/BlueprintModal.tsx b/apps/web/src/pages/templates/components/BlueprintModal.tsx
index 8f2f7f0ba7b..c621faca385 100644
--- a/apps/web/src/pages/templates/components/BlueprintModal.tsx
+++ b/apps/web/src/pages/templates/components/BlueprintModal.tsx
@@ -2,7 +2,8 @@ import { Modal, useMantineTheme, Center, Loader } from '@mantine/core';
import { useQuery, useMutation } from '@tanstack/react-query';
import { useNavigate } from 'react-router-dom';
import { useEffect, useState } from 'react';
-import type { IResponseError, IUserEntity } from '@novu/shared';
+
+import { IResponseError, IUserEntity, WorkflowCreationSourceEnum } from '@novu/shared';
import { colors, shadows, Title, Text, Button } from '@novu/design-system';
import { updateUserOnBoarding } from '../../../api/user';
@@ -11,7 +12,6 @@ import { errorMessage } from '../../../utils/notifications';
import { When } from '../../../components/utils/When';
import { useSegment } from '../../../components/providers/SegmentProvider';
import { useCreateTemplateFromBlueprint } from '../../../api/hooks';
-import { TemplateCreationSourceEnum } from '../shared';
import { ROUTES } from '../../../constants/routes';
export function BlueprintModal() {
@@ -121,7 +121,7 @@ export function BlueprintModal() {
if (blueprint) {
createTemplateFromBlueprint({
blueprint,
- params: { __source: TemplateCreationSourceEnum.NOTIFICATION_DIRECTORY },
+ params: { __source: WorkflowCreationSourceEnum.NOTIFICATION_DIRECTORY },
});
}
}}
diff --git a/apps/web/src/pages/templates/components/CreateWorkflowDropdown.tsx b/apps/web/src/pages/templates/components/CreateWorkflowDropdown.tsx
index 903710c09bd..727f98d4efa 100644
--- a/apps/web/src/pages/templates/components/CreateWorkflowDropdown.tsx
+++ b/apps/web/src/pages/templates/components/CreateWorkflowDropdown.tsx
@@ -1,3 +1,4 @@
+import { useEffect } from 'react';
import { Skeleton } from '@mantine/core';
import { FontAwesomeIcon } from '@fortawesome/react-fontawesome';
import { faFile } from '@fortawesome/free-regular-svg-icons';
@@ -5,18 +6,16 @@ import { faDiagramNext } from '@fortawesome/free-solid-svg-icons';
import styled from '@emotion/styled';
import { Dropdown, PlusButton, Popover } from '@novu/design-system';
-import { FeatureFlagsKeysEnum } from '@novu/shared';
+import { FeatureFlagsKeysEnum, WorkflowCreationSourceEnum } from '@novu/shared';
import { Button } from '@novu/novui';
import { IconOutlineAdd } from '@novu/novui/icons';
-import { useEffect } from 'react';
+
import { IBlueprintTemplate } from '../../../api/types';
import { useSegment } from '../../../components/providers/SegmentProvider';
-import { TemplateCreationSourceEnum } from '../shared';
import { useFeatureFlag, useHoverOverItem } from '../../../hooks';
import { FrameworkProjectDropDownItem } from './FrameworkProjectWaitList';
import { useDocsModal } from '../../../components/docs/useDocsModal';
import { PATHS } from '../../../components/docs/docs.const';
-import { ROUTES } from '../../../constants/routes';
const WIDTH = 172;
@@ -90,7 +89,7 @@ export const CreateWorkflowDropdown = ({
onClick={(event) => {
segment.track('[Template Store] Click Create Notification Template', {
templateIdentifier: 'Blank Workflow',
- location: TemplateCreationSourceEnum.DROPDOWN,
+ location: WorkflowCreationSourceEnum.DROPDOWN,
});
onBlankWorkflowClick(event);
@@ -121,7 +120,7 @@ export const CreateWorkflowDropdown = ({
onClick={() => {
segment.track('[Template Store] Click Create Notification Template', {
templateIdentifier: template?.triggers[0]?.identifier || '',
- location: TemplateCreationSourceEnum.DROPDOWN,
+ location: WorkflowCreationSourceEnum.DROPDOWN,
});
onTemplateClick(template);
@@ -143,7 +142,7 @@ export const CreateWorkflowDropdown = ({
icon={}
onClick={(event) => {
segment.track('[Template Store] Click Open Template Store', {
- location: TemplateCreationSourceEnum.DROPDOWN,
+ location: WorkflowCreationSourceEnum.DROPDOWN,
});
onAllTemplatesClick(event);
diff --git a/apps/web/src/pages/templates/components/TemplateEditorFormProvider.tsx b/apps/web/src/pages/templates/components/TemplateEditorFormProvider.tsx
index 5c59383bf85..61fef4597ca 100644
--- a/apps/web/src/pages/templates/components/TemplateEditorFormProvider.tsx
+++ b/apps/web/src/pages/templates/components/TemplateEditorFormProvider.tsx
@@ -1,21 +1,20 @@
-import { createContext, useEffect, useMemo, useCallback, useContext, useState } from 'react';
+import { createContext, useCallback, useContext, useEffect, useMemo, useState } from 'react';
import slugify from 'slugify';
-import { FormProvider, useForm, useFieldArray, FieldErrors } from 'react-hook-form';
+import { FieldErrors, FormProvider, useFieldArray, useForm } from 'react-hook-form';
import { zodResolver } from '@hookform/resolvers/zod';
import { useParams } from 'react-router-dom';
import cloneDeep from 'lodash.clonedeep';
import {
+ ActorTypeEnum,
DelayTypeEnum,
DigestTypeEnum,
DigestUnitEnum,
+ EmailBlockTypeEnum,
+ IEmailBlock,
INotificationTemplate,
INotificationTrigger,
isBridgeWorkflow,
- WorkflowTypeEnum,
StepTypeEnum,
- ActorTypeEnum,
- EmailBlockTypeEnum,
- IEmailBlock,
TextAlignEnum,
} from '@novu/shared';
import { captureException } from '@sentry/react';
@@ -23,7 +22,7 @@ import { captureException } from '@sentry/react';
import { v4 as uuid4 } from 'uuid';
import type { IForm, IFormStep, ITemplates } from './formTypes';
import { useTemplateController } from './useTemplateController';
-import { mapNotificationTemplateToForm, mapFormToCreateNotificationTemplate } from './templateToFormMappers';
+import { mapFormToCreateNotificationTemplate, mapNotificationTemplateToForm } from './templateToFormMappers';
import { errorMessage, successMessage } from '../../../utils/notifications';
import { schema } from './notificationTemplateSchema';
import { useEffectOnce, useNotificationGroup } from '../../../hooks';
@@ -155,28 +154,26 @@ const TemplateEditorFormContext = createContext({
deleteVariant: () => {},
});
-const defaultValues: IForm = {
- name: 'Untitled',
- notificationGroupId: '',
- description: '',
- identifier: '',
- tags: [],
- critical: true,
- steps: [],
- preferenceSettings: {
- email: true,
- sms: true,
- in_app: true,
- chat: true,
- push: true,
- },
-};
-
const TemplateEditorFormProvider = ({ children }) => {
const { templateId = '' } = useParams<{ templateId?: string }>();
const methods = useForm({
resolver: zodResolver(schema as any),
- defaultValues,
+ defaultValues: {
+ name: 'Untitled',
+ notificationGroupId: '',
+ description: '',
+ identifier: '',
+ tags: [],
+ critical: true,
+ steps: [],
+ preferenceSettings: {
+ email: true,
+ sms: true,
+ in_app: true,
+ chat: true,
+ push: true,
+ },
+ },
mode: 'onChange',
});
const [trigger, setTrigger] = useState();
diff --git a/apps/web/src/pages/templates/components/templates-store/TemplatesStoreModal.tsx b/apps/web/src/pages/templates/components/templates-store/TemplatesStoreModal.tsx
index c35dc21ee56..60882ddeb41 100644
--- a/apps/web/src/pages/templates/components/templates-store/TemplatesStoreModal.tsx
+++ b/apps/web/src/pages/templates/components/templates-store/TemplatesStoreModal.tsx
@@ -5,7 +5,8 @@ import { FontAwesomeIcon } from '@fortawesome/react-fontawesome';
import { useNavigate } from 'react-router-dom';
import { Button, colors, shadows, Close } from '@novu/design-system';
import { faFile } from '@fortawesome/free-regular-svg-icons';
-import { INotificationTemplateStep } from '@novu/shared';
+
+import { INotificationTemplateStep, WorkflowCreationSourceEnum } from '@novu/shared';
import {
CanvasHolder,
@@ -30,7 +31,6 @@ import { FlowEditor } from '../../../../components/workflow';
import { errorMessage } from '../../../../utils/notifications';
import { parseUrl } from '../../../../utils/routeUtils';
import { ROUTES } from '../../../../constants/routes';
-import { TemplateCreationSourceEnum } from '../../shared';
import { useSegment } from '../../../../components/providers/SegmentProvider';
import { IBlueprintTemplate } from '../../../../api/types';
import { TemplateAnalyticsEnum } from '../../constants';
@@ -68,7 +68,7 @@ export const TemplatesStoreModal = ({ general, popular, isOpened, onClose }: ITe
const handleTemplateClick = (template: IBlueprintTemplate) => {
segment.track('[Template Store] Click Notification Template', {
templateIdentifier: template.triggers[0]?.identifier,
- location: TemplateCreationSourceEnum.TEMPLATE_STORE,
+ location: WorkflowCreationSourceEnum.TEMPLATE_STORE,
});
setTemplate(template);
@@ -82,12 +82,12 @@ export const TemplatesStoreModal = ({ general, popular, isOpened, onClose }: ITe
const handleCreateTemplateClick = (blueprint: IBlueprintTemplate) => {
segment.track('[Template Store] Click Create Notification Template', {
templateIdentifier: blueprint.triggers[0]?.identifier,
- location: TemplateCreationSourceEnum.TEMPLATE_STORE,
+ location: WorkflowCreationSourceEnum.TEMPLATE_STORE,
});
createTemplateFromBlueprint({
blueprint,
- params: { __source: TemplateCreationSourceEnum.TEMPLATE_STORE },
+ params: { __source: WorkflowCreationSourceEnum.TEMPLATE_STORE },
});
};
@@ -117,7 +117,7 @@ export const TemplatesStoreModal = ({ general, popular, isOpened, onClose }: ITe
onClick={() => {
segment.track('[Template Store] Click Create Notification Template', {
templateIdentifier: 'Blank Workflow',
- location: TemplateCreationSourceEnum.DROPDOWN,
+ location: WorkflowCreationSourceEnum.DROPDOWN,
});
handleRedirectToCreateBlankTemplate(false);
}}
diff --git a/apps/web/src/pages/templates/hooks/useCreate.ts b/apps/web/src/pages/templates/hooks/useCreate.ts
index b6335e470db..86a151c64aa 100644
--- a/apps/web/src/pages/templates/hooks/useCreate.ts
+++ b/apps/web/src/pages/templates/hooks/useCreate.ts
@@ -1,10 +1,9 @@
import { useEffect } from 'react';
import { useNavigate } from 'react-router-dom';
-import { INotificationTrigger } from '@novu/shared';
+import { INotificationTrigger, WorkflowCreationSourceEnum } from '@novu/shared';
import { IForm } from '../components/formTypes';
import { mapFormToCreateNotificationTemplate } from '../components/templateToFormMappers';
import { useTemplateController } from '../components/useTemplateController';
-import { TemplateCreationSourceEnum } from '../shared';
export const useCreate = (
templateId: string,
@@ -35,7 +34,7 @@ export const useCreate = (
draft: false,
},
params: {
- __source: TemplateCreationSourceEnum.EDITOR,
+ __source: WorkflowCreationSourceEnum.EDITOR,
},
});
setTrigger(response.triggers[0]);
diff --git a/apps/worker/src/app/shared/shared.module.ts b/apps/worker/src/app/shared/shared.module.ts
index 75713884cf8..9b1197b691d 100644
--- a/apps/worker/src/app/shared/shared.module.ts
+++ b/apps/worker/src/app/shared/shared.module.ts
@@ -1,6 +1,6 @@
import { Module } from '@nestjs/common';
import {
- ControlVariablesRepository,
+ ControlValuesRepository,
DalService,
EnvironmentRepository,
ExecutionDetailsRepository,
@@ -88,7 +88,7 @@ const DAL_MODELS = [
TopicSubscribersRepository,
TenantRepository,
WorkflowOverrideRepository,
- ControlVariablesRepository,
+ ControlValuesRepository,
...getDynamicAuthProviders(),
];
diff --git a/apps/worker/src/app/workflow/usecases/execute-bridge-job/execute-bridge-job.usecase.ts b/apps/worker/src/app/workflow/usecases/execute-bridge-job/execute-bridge-job.usecase.ts
index 15fc383223b..74977407309 100644
--- a/apps/worker/src/app/workflow/usecases/execute-bridge-job/execute-bridge-job.usecase.ts
+++ b/apps/worker/src/app/workflow/usecases/execute-bridge-job/execute-bridge-job.usecase.ts
@@ -1,7 +1,7 @@
import { Injectable, Logger } from '@nestjs/common';
import {
- ControlVariablesRepository,
+ ControlValuesRepository,
NotificationTemplateEntity,
EnvironmentRepository,
JobRepository,
@@ -37,7 +37,7 @@ export class ExecuteBridgeJob {
private notificationTemplateRepository: NotificationTemplateRepository,
private messageRepository: MessageRepository,
private environmentRepository: EnvironmentRepository,
- private controlVariablesRepository: ControlVariablesRepository,
+ private controlValuesRepository: ControlValuesRepository,
private createExecutionDetails: CreateExecutionDetails,
private executeBridgeRequest: ExecuteBridgeRequest
) {}
@@ -135,7 +135,7 @@ export class ExecuteBridgeJob {
}
private async findControlVariables(command: ExecuteBridgeJobCommand, workflow: NotificationTemplateEntity) {
- const controls = await this.controlVariablesRepository.findOne({
+ const controls = await this.controlValuesRepository.findOne({
_organizationId: command.organizationId,
_workflowId: workflow._id,
_stepId: command.job.step._id,
diff --git a/apps/worker/src/app/workflow/workflow.module.ts b/apps/worker/src/app/workflow/workflow.module.ts
index 5584e8f994a..48d1df4793a 100644
--- a/apps/worker/src/app/workflow/workflow.module.ts
+++ b/apps/worker/src/app/workflow/workflow.module.ts
@@ -1,32 +1,32 @@
/* eslint-disable global-require */
-import { DynamicModule, Logger, Module, Provider, OnApplicationShutdown } from '@nestjs/common';
+import { DynamicModule, Logger, Module, OnApplicationShutdown, Provider } from '@nestjs/common';
import {
BulkCreateExecutionDetails,
CalculateLimitNovuIntegration,
CompileEmailTemplate,
+ CompileInAppTemplate,
CompileTemplate,
+ ConditionsFilter,
CreateExecutionDetails,
+ ExecutionLogRoute,
GetDecryptedIntegrations,
+ getFeatureFlag,
GetLayoutUseCase,
GetNovuLayout,
GetNovuProviderCredentials,
- GetSubscriberPreference,
+ GetPreferences,
GetSubscriberGlobalPreference,
+ GetSubscriberPreference,
GetSubscriberTemplatePreference,
+ GetTopicSubscribersUseCase,
+ NormalizeVariables,
ProcessTenant,
SelectIntegration,
- ConditionsFilter,
- NormalizeVariables,
- TriggerEvent,
SelectVariant,
- GetTopicSubscribersUseCase,
- getFeatureFlag,
TriggerBroadcast,
+ TriggerEvent,
TriggerMulticast,
- CompileInAppTemplate,
WorkflowInMemoryProviderService,
- ExecutionLogRoute,
- GetPreferences,
} from '@novu/application-generic';
import { CommunityOrganizationRepository, JobRepository, PreferencesRepository } from '@novu/dal';
@@ -34,6 +34,13 @@ import { Type } from '@nestjs/common/interfaces/type.interface';
import { ForwardReference } from '@nestjs/common/interfaces/modules/forward-reference.interface';
import { JobTopicNameEnum } from '@novu/shared';
import {
+ Digest,
+ ExecuteBridgeJob,
+ GetDigestEventsBackoff,
+ GetDigestEventsRegular,
+ HandleLastFailedJob,
+ QueueNextJob,
+ RunJob,
SendMessage,
SendMessageChat,
SendMessageDelay,
@@ -41,17 +48,10 @@ import {
SendMessageInApp,
SendMessagePush,
SendMessageSms,
- Digest,
- GetDigestEventsBackoff,
- GetDigestEventsRegular,
- HandleLastFailedJob,
- QueueNextJob,
- RunJob,
SetJobAsCompleted,
SetJobAsFailed,
UpdateJobStatus,
WebhookFilterBackoffStrategy,
- ExecuteBridgeJob,
} from './usecases';
import { SharedModule } from '../shared/shared.module';
diff --git a/libs/application-generic/src/commands/project.command.ts b/libs/application-generic/src/commands/project.command.ts
index f6399d385ef..9e18e7ba316 100644
--- a/libs/application-generic/src/commands/project.command.ts
+++ b/libs/application-generic/src/commands/project.command.ts
@@ -1,4 +1,13 @@
-import { IsNotEmpty } from 'class-validator';
+import {
+ IsDefined,
+ IsEnum,
+ IsNotEmpty,
+ IsNumber,
+ IsString,
+} from 'class-validator';
+
+import { DirectionEnum, UserSessionData } from '@novu/shared';
+
import { BaseCommand } from './base.command';
export abstract class EnvironmentLevelCommand extends BaseCommand {
@@ -46,6 +55,29 @@ export abstract class EnvironmentWithUserCommand extends BaseCommand {
readonly userId: string;
}
+export abstract class EnvironmentWithUserObjectCommand extends BaseCommand {
+ @IsNotEmpty()
+ user: UserSessionData;
+}
+
+export abstract class PaginatedListCommand extends EnvironmentWithUserObjectCommand {
+ @IsDefined()
+ @IsNumber()
+ offset: number;
+
+ @IsDefined()
+ @IsNumber()
+ limit: number;
+
+ @IsDefined()
+ @IsEnum(DirectionEnum)
+ orderDirection: DirectionEnum;
+
+ @IsDefined()
+ @IsString()
+ orderByField: string;
+}
+
export abstract class EnvironmentWithSubscriber extends BaseCommand {
@IsNotEmpty()
readonly environmentId: string;
diff --git a/libs/application-generic/src/logging/index.ts b/libs/application-generic/src/logging/index.ts
index 48fce2e4ce5..e99e8d85e46 100644
--- a/libs/application-generic/src/logging/index.ts
+++ b/libs/application-generic/src/logging/index.ts
@@ -1,11 +1,11 @@
import { NestInterceptor, RequestMethod } from '@nestjs/common';
import {
- LoggerErrorInterceptor,
+ getLoggerToken,
Logger,
+ LoggerErrorInterceptor,
LoggerModule,
- PinoLogger,
- getLoggerToken,
Params,
+ PinoLogger,
} from 'nestjs-pino';
import { storage, Store } from 'nestjs-pino/storage';
import { sensitiveFields } from './masking';
@@ -46,6 +46,8 @@ export function getLogLevel() {
logLevel = 'info';
}
+ // eslint-disable-next-line no-console
+ console.log(`Log Level Chosen: ${logLevel}`);
return logLevel;
}
@@ -78,7 +80,7 @@ function getLoggingVariables(): ILoggingVariables {
export function createNestLoggingModuleOptions(
settings: ILoggerSettings,
): Params {
- const values = getLoggingVariables();
+ const values: ILoggingVariables = getLoggingVariables();
let redactFields: string[] = sensitiveFields.map((val) => val);
@@ -100,6 +102,7 @@ export function createNestLoggingModuleOptions(
? { target: 'pino-pretty' }
: undefined;
+ // eslint-disable-next-line no-console
console.log(loggingLevelSet);
// eslint-disable-next-line no-console
@@ -115,7 +118,7 @@ export function createNestLoggingModuleOptions(
level: values.level,
redact: {
paths: redactFields,
- censor: '[REDACTED]',
+ censor: customRedaction,
},
base: {
pid: process.pid,
@@ -145,6 +148,17 @@ export function createNestLoggingModuleOptions(
};
}
+const customRedaction = (value: any, path: string[]) => {
+ /*
+ * Logger.
+ * if (obj.email && typeof obj.email === 'string') {
+ * obj.email = '[REDACTED]';
+ * }
+ *
+ * return JSON.parse(JSON.stringify(obj));
+ */
+};
+
interface ILoggerSettings {
serviceName: string;
version: string;
diff --git a/libs/application-generic/src/usecases/create-workflow/create-workflow.command.ts b/libs/application-generic/src/usecases/create-workflow/create-workflow.command.ts
index f801d2fa57f..a062ad6e4d5 100644
--- a/libs/application-generic/src/usecases/create-workflow/create-workflow.command.ts
+++ b/libs/application-generic/src/usecases/create-workflow/create-workflow.command.ts
@@ -13,12 +13,13 @@ import {
BuilderFieldType,
BuilderGroupValues,
ChannelCTATypeEnum,
+ FilterParts,
IMessageAction,
+ INotificationGroup,
IPreferenceChannels,
- FilterParts,
IWorkflowStepMetadata,
NotificationTemplateCustomData,
- INotificationGroup,
+ WorkflowOriginEnum,
WorkflowTypeEnum,
} from '@novu/shared';
@@ -28,14 +29,14 @@ import { EnvironmentWithUserCommand } from '../../commands';
export class CreateWorkflowCommand extends EnvironmentWithUserCommand {
@IsMongoId()
@IsDefined()
- notificationGroupId: string;
+ notificationGroupId?: string;
@IsOptional()
notificationGroup?: INotificationGroup;
@IsOptional()
@IsArray()
- tags: string[];
+ tags?: string[];
@IsDefined()
@IsString()
@@ -43,7 +44,7 @@ export class CreateWorkflowCommand extends EnvironmentWithUserCommand {
@IsString()
@IsOptional()
- description: string;
+ description?: string;
@IsDefined()
@IsArray()
@@ -54,7 +55,8 @@ export class CreateWorkflowCommand extends EnvironmentWithUserCommand {
active: boolean;
@IsBoolean()
- draft: boolean;
+ @IsOptional()
+ draft?: boolean;
@IsBoolean()
critical: boolean;
@@ -90,6 +92,8 @@ export class CreateWorkflowCommand extends EnvironmentWithUserCommand {
@IsEnum(WorkflowTypeEnum)
@IsDefined()
type: WorkflowTypeEnum;
+
+ origin: WorkflowOriginEnum;
}
export class ChannelCTACommand {
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 9b69a8a3a11..c2eb5ff363b 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
@@ -24,6 +24,7 @@ import {
isBridgeWorkflow,
IStepVariant,
TriggerTypeEnum,
+ WorkflowOriginEnum,
WorkflowTypeEnum,
} from '@novu/shared';
@@ -259,6 +260,7 @@ export class CreateWorkflow {
_notificationGroupId: command.notificationGroupId,
blueprintId: command.blueprintId,
type: command.type,
+ origin: command.origin,
...(command.rawData ? { rawData: command.rawData } : {}),
...(command.payloadSchema
? { payloadSchema: command.payloadSchema }
@@ -447,6 +449,7 @@ export class CreateWorkflow {
blueprintId: command.blueprintId,
__source: command.__source,
type: WorkflowTypeEnum.REGULAR,
+ origin: command.origin ?? WorkflowOriginEnum.NOVU_CLOUD,
});
}
diff --git a/libs/application-generic/src/usecases/get-preferences/get-preferences.dto.ts b/libs/application-generic/src/usecases/get-preferences/get-preferences.dto.ts
index 3251a15b88c..51dbf7dcdb8 100644
--- a/libs/application-generic/src/usecases/get-preferences/get-preferences.dto.ts
+++ b/libs/application-generic/src/usecases/get-preferences/get-preferences.dto.ts
@@ -2,6 +2,8 @@ import { PreferencesTypeEnum, WorkflowPreferences } from '@novu/shared';
export class GetPreferencesResponseDto {
preferences: WorkflowPreferences;
+
type: PreferencesTypeEnum;
+
source: Record;
}
diff --git a/libs/application-generic/src/usecases/get-preferences/get-preferences.usecase.ts b/libs/application-generic/src/usecases/get-preferences/get-preferences.usecase.ts
index 6b8c49aa83b..a4781cccdbe 100644
--- a/libs/application-generic/src/usecases/get-preferences/get-preferences.usecase.ts
+++ b/libs/application-generic/src/usecases/get-preferences/get-preferences.usecase.ts
@@ -1,17 +1,32 @@
-import { Injectable, NotFoundException } from '@nestjs/common';
+import { BadRequestException, Injectable } from '@nestjs/common';
import { PreferencesEntity, PreferencesRepository } from '@novu/dal';
import {
+ buildWorkflowPreferences,
FeatureFlagsKeysEnum,
IPreferenceChannels,
- WorkflowPreferences,
PreferencesTypeEnum,
- buildWorkflowPreferences,
+ WorkflowPreferences,
} from '@novu/shared';
import { deepMerge } from '../../utils';
import { GetFeatureFlag, GetFeatureFlagCommand } from '../get-feature-flag';
import { GetPreferencesCommand } from './get-preferences.command';
import { GetPreferencesResponseDto } from './get-preferences.dto';
+class PreferencesNotEnabledException extends BadRequestException {
+ constructor(featureFlagCommand: object) {
+ super({
+ message: 'Preferences Feature Flag are not enabled',
+ ...featureFlagCommand,
+ });
+ }
+}
+
+class PreferencesNotFoundException extends BadRequestException {
+ constructor(featureFlagCommand: GetPreferencesCommand) {
+ super({ message: 'Preferences not found', ...featureFlagCommand });
+ }
+}
+
@Injectable()
export class GetPreferences {
constructor(
@@ -22,34 +37,41 @@ export class GetPreferences {
async execute(
command: GetPreferencesCommand,
): Promise {
- const isEnabled = await this.getFeatureFlag.execute(
- GetFeatureFlagCommand.create({
- userId: 'system',
- environmentId: command.environmentId,
- organizationId: command.organizationId,
- key: FeatureFlagsKeysEnum.IS_WORKFLOW_PREFERENCES_ENABLED,
- }),
- );
-
- if (!isEnabled) {
- throw new NotFoundException();
- }
+ await this.validateFeatureFlag(command);
const items = await this.getPreferencesFromDb(command);
if (items.length === 0) {
- throw new NotFoundException('We could not find any preferences');
+ throw new PreferencesNotFoundException(command);
}
const mergedPreferences = this.mergePreferences(items, command.templateId);
if (!mergedPreferences.preferences) {
- throw new NotFoundException('We could not find any preferences');
+ throw new PreferencesNotFoundException(command);
}
return mergedPreferences;
}
+ private async validateFeatureFlag(command: GetPreferencesCommand) {
+ const featureFlagCommand = {
+ userId: 'system',
+ environmentId: command.environmentId,
+ organizationId: command.organizationId,
+ key: FeatureFlagsKeysEnum.IS_WORKFLOW_PREFERENCES_ENABLED,
+ };
+ const isEnabled = await this.getFeatureFlag.execute(
+ GetFeatureFlagCommand.create(featureFlagCommand),
+ );
+
+ if (!isEnabled) {
+ throw new PreferencesNotEnabledException(featureFlagCommand);
+ }
+
+ return featureFlagCommand;
+ }
+
/** Get only simple, channel-level enablement flags */
public async getPreferenceChannels(command: {
environmentId: string;
@@ -57,7 +79,7 @@ export class GetPreferences {
subscriberId: string;
templateId?: string;
}): Promise {
- const result = await this.getWorkflowPreferences(command);
+ const result = await this.safeExecute(command);
if (!result) {
return undefined;
@@ -68,15 +90,11 @@ export class GetPreferences {
);
}
- /** Safely get WorkflowPreferences by returning undefined if none are found */
- public async getWorkflowPreferences(command: {
- environmentId: string;
- organizationId: string;
- subscriberId: string;
- templateId?: string;
- }): Promise {
+ public async safeExecute(
+ command: GetPreferencesCommand,
+ ): Promise {
try {
- const result = await this.execute(
+ return await this.execute(
GetPreferencesCommand.create({
environmentId: command.environmentId,
organizationId: command.organizationId,
@@ -84,11 +102,9 @@ export class GetPreferences {
templateId: command.templateId,
}),
);
-
- return result;
} catch (e) {
// If we cant find preferences lets return undefined instead of throwing it up to caller to make it easier for caller to handle.
- if ((e as Error).name === NotFoundException.name) {
+ if ((e as Error).name === PreferencesNotFoundException.name) {
return undefined;
}
throw e;
diff --git a/libs/application-generic/src/usecases/get-preferences/index.ts b/libs/application-generic/src/usecases/get-preferences/index.ts
index 134e74b93c7..899497a55be 100644
--- a/libs/application-generic/src/usecases/get-preferences/index.ts
+++ b/libs/application-generic/src/usecases/get-preferences/index.ts
@@ -1,2 +1,3 @@
export * from './get-preferences.command';
export * from './get-preferences.usecase';
+export * from './get-preferences.dto';
diff --git a/libs/application-generic/src/usecases/get-subscriber-template-preference/get-subscriber-template-preference.usecase.ts b/libs/application-generic/src/usecases/get-subscriber-template-preference/get-subscriber-template-preference.usecase.ts
index 603b226cd7c..801c48f0ef0 100644
--- a/libs/application-generic/src/usecases/get-subscriber-template-preference/get-subscriber-template-preference.usecase.ts
+++ b/libs/application-generic/src/usecases/get-subscriber-template-preference/get-subscriber-template-preference.usecase.ts
@@ -23,7 +23,7 @@ import {
import { GetSubscriberTemplatePreferenceCommand } from './get-subscriber-template-preference.command';
import { ApiException } from '../../utils/exceptions';
-import { CachedEntity, buildSubscriberKey } from '../../services/cache';
+import { buildSubscriberKey, CachedEntity } from '../../services/cache';
import { GetPreferences } from '../get-preferences';
const PRIORITY_ORDER = [
@@ -79,13 +79,14 @@ export class GetSubscriberTemplatePreference {
/**
* V2 preference object.
*/
- const subscriberWorkflowPreferences =
- await this.getPreferences.getWorkflowPreferences({
+ const subscriberWorkflowPreferences = await this.getPreferences.safeExecute(
+ {
environmentId: command.environmentId,
organizationId: command.organizationId,
subscriberId: subscriber._id,
templateId: command.template._id,
- });
+ },
+ );
const subscriberPreferenceChannels = subscriberWorkflowPreferences
? GetPreferences.mapWorkflowPreferencesToChannelPreferences(
diff --git a/libs/application-generic/src/usecases/index.ts b/libs/application-generic/src/usecases/index.ts
index 3bfb8944312..ae434b0f813 100644
--- a/libs/application-generic/src/usecases/index.ts
+++ b/libs/application-generic/src/usecases/index.ts
@@ -43,4 +43,5 @@ export * from './message-template';
export * from './subscribers';
export * from './execute-bridge-request';
export * from './upsert-preferences';
+export * from './upsert-control-values';
export * from './get-preferences';
diff --git a/libs/application-generic/src/usecases/upsert-control-values/index.ts b/libs/application-generic/src/usecases/upsert-control-values/index.ts
new file mode 100644
index 00000000000..5e261f82ccc
--- /dev/null
+++ b/libs/application-generic/src/usecases/upsert-control-values/index.ts
@@ -0,0 +1,2 @@
+export * from './upsert-control-values-command';
+export * from './upsert-control-values-use-case';
diff --git a/libs/application-generic/src/usecases/upsert-control-values/upsert-control-values-command.ts b/libs/application-generic/src/usecases/upsert-control-values/upsert-control-values-command.ts
new file mode 100644
index 00000000000..162e52f36cd
--- /dev/null
+++ b/libs/application-generic/src/usecases/upsert-control-values/upsert-control-values-command.ts
@@ -0,0 +1,20 @@
+import { IsNotEmpty, IsObject, IsOptional, IsString } from 'class-validator';
+import { JsonSchema } from '@novu/framework';
+import { NotificationStepEntity } from '@novu/dal';
+import { OrganizationLevelCommand } from '../../commands';
+
+export class UpsertControlValuesCommand extends OrganizationLevelCommand {
+ @IsObject()
+ notificationStepEntity: NotificationStepEntity;
+
+ @IsString()
+ @IsNotEmpty()
+ workflowId: string;
+
+ @IsObject()
+ @IsOptional()
+ newControlValues?: Record;
+
+ @IsObject()
+ controlSchemas: { schema: JsonSchema };
+}
diff --git a/libs/application-generic/src/usecases/upsert-control-values/upsert-control-values-use-case.ts b/libs/application-generic/src/usecases/upsert-control-values/upsert-control-values-use-case.ts
new file mode 100644
index 00000000000..7a04905b581
--- /dev/null
+++ b/libs/application-generic/src/usecases/upsert-control-values/upsert-control-values-use-case.ts
@@ -0,0 +1,63 @@
+import { Injectable } from '@nestjs/common';
+
+import { ControlValuesEntity, ControlValuesRepository } from '@novu/dal';
+import { ControlVariablesLevelEnum } from '@novu/shared';
+import { UpsertControlValuesCommand } from './upsert-control-values-command';
+
+@Injectable()
+export class UpsertControlValuesUseCase {
+ constructor(private controlValuesRepository: ControlValuesRepository) {}
+
+ async execute(command: UpsertControlValuesCommand) {
+ const existingControlValues = await this.controlValuesRepository.findFirst({
+ _environmentId: command.environmentId || '',
+ _organizationId: command.organizationId,
+ _workflowId: command.workflowId,
+ _stepId: command.notificationStepEntity._templateId,
+ level: ControlVariablesLevelEnum.STEP_CONTROLS,
+ });
+
+ if (existingControlValues) {
+ return await this.updateControlVariables(
+ existingControlValues,
+ command,
+ command.newControlValues,
+ );
+ }
+
+ return await this.controlValuesRepository.create({
+ _organizationId: command.organizationId,
+ _environmentId: command.environmentId,
+ _workflowId: command.workflowId,
+ _stepId: command.notificationStepEntity._templateId,
+ level: ControlVariablesLevelEnum.STEP_CONTROLS,
+ priority: 0,
+ inputs: command.newControlValues,
+ controls: command.newControlValues,
+ });
+ }
+
+ private async updateControlVariables(
+ found: ControlValuesEntity,
+ command: UpsertControlValuesCommand,
+ controlValues: Record,
+ ) {
+ await this.controlValuesRepository.update(
+ {
+ _id: found._id,
+ _organizationId: command.organizationId,
+ },
+ {
+ priority: 0,
+ inputs: controlValues,
+ controls: controlValues,
+ },
+ );
+
+ return this.controlValuesRepository.findOne({
+ _id: found._id,
+ _organizationId: command.organizationId,
+ _environmentId: command.environmentId,
+ });
+ }
+}
diff --git a/libs/application-generic/src/usecases/workflow/update-workflow/update-workflow.command.ts b/libs/application-generic/src/usecases/workflow/update-workflow/update-workflow.command.ts
index 83f36244feb..bd4df9da898 100644
--- a/libs/application-generic/src/usecases/workflow/update-workflow/update-workflow.command.ts
+++ b/libs/application-generic/src/usecases/workflow/update-workflow/update-workflow.command.ts
@@ -74,6 +74,7 @@ export class UpdateWorkflowCommand extends EnvironmentWithUserCommand {
@IsOptional()
inputs?: IStepControl;
+
@IsOptional()
controls?: IStepControl;
diff --git a/libs/dal/src/repositories/base-repository.ts b/libs/dal/src/repositories/base-repository.ts
index 039a9ebe382..399be9ffa04 100644
--- a/libs/dal/src/repositories/base-repository.ts
+++ b/libs/dal/src/repositories/base-repository.ts
@@ -6,16 +6,7 @@ import {
DEFAULT_MESSAGE_IN_APP_RETENTION_DAYS,
DEFAULT_NOTIFICATION_RETENTION_DAYS,
} from '@novu/shared';
-import {
- Model,
- Types,
- ProjectionType,
- FilterQuery,
- UpdateQuery,
- QueryOptions,
- Query,
- QueryWithHelpers,
-} from 'mongoose';
+import { FilterQuery, Model, ProjectionType, QueryOptions, QueryWithHelpers, Types, UpdateQuery } from 'mongoose';
import { DalException } from '../shared';
export class BaseRepository {
@@ -269,8 +260,12 @@ export class BaseRepository {
let result;
try {
result = await this.MongooseModel.insertMany(data, { ordered });
- } catch (e) {
- throw new DalException(e.message);
+ } catch (e: unknown) {
+ if (e instanceof Error) {
+ throw new DalException(e.message);
+ } else {
+ throw new DalException('An unknown error occurred');
+ }
}
const insertedIds = result.map((inserted) => inserted._id);
diff --git a/libs/dal/src/repositories/control-variables/controlVariables.entity.ts b/libs/dal/src/repositories/control-variables/controlValuesEntity.ts
similarity index 90%
rename from libs/dal/src/repositories/control-variables/controlVariables.entity.ts
rename to libs/dal/src/repositories/control-variables/controlValuesEntity.ts
index 1d9e27f63e3..eb43fa0ec3c 100644
--- a/libs/dal/src/repositories/control-variables/controlVariables.entity.ts
+++ b/libs/dal/src/repositories/control-variables/controlValuesEntity.ts
@@ -1,6 +1,6 @@
import { ControlVariablesLevelEnum } from '@novu/shared';
-export class ControlVariablesEntity {
+export class ControlValuesEntity {
_id: string;
createdAt: string;
updatedAt: string;
diff --git a/libs/dal/src/repositories/control-variables/controlValuesRepository.ts b/libs/dal/src/repositories/control-variables/controlValuesRepository.ts
new file mode 100644
index 00000000000..6c716f3d740
--- /dev/null
+++ b/libs/dal/src/repositories/control-variables/controlValuesRepository.ts
@@ -0,0 +1,49 @@
+import { SoftDeleteModel } from 'mongoose-delete';
+import { ControlVariablesLevelEnum } from '@novu/shared';
+import { ControlValuesModel, ControlVariables } from './controlVariables.schema';
+import { ControlValuesEntity } from './controlValuesEntity';
+import { BaseRepository } from '../base-repository';
+import { EnforceEnvOrOrgIds } from '../../types';
+
+// eslint-disable-next-line @typescript-eslint/naming-convention
+export interface DeleteManyValuesQuery {
+ _environmentId: string;
+ _organizationId: string;
+ _workflowId: string;
+ _stepId?: string;
+ level?: ControlVariablesLevelEnum;
+}
+
+// eslint-disable-next-line @typescript-eslint/naming-convention
+export interface FindControlValuesQuery {
+ _environmentId: string;
+ _organizationId: string;
+ _workflowId: string;
+ _stepId: string;
+ level?: ControlVariablesLevelEnum;
+}
+
+export class ControlValuesRepository extends BaseRepository<
+ ControlValuesModel,
+ ControlValuesEntity,
+ EnforceEnvOrOrgIds
+> {
+ private controlVariables: SoftDeleteModel;
+
+ constructor() {
+ super(ControlVariables, ControlValuesEntity);
+ this.controlVariables = ControlVariables;
+ }
+
+ async deleteMany(query: DeleteManyValuesQuery) {
+ return await super.delete(query);
+ }
+
+ async findMany(query: FindControlValuesQuery): Promise {
+ return await super.find(query);
+ }
+
+ async findFirst(query: FindControlValuesQuery): Promise {
+ return await this.findOne(query);
+ }
+}
diff --git a/libs/dal/src/repositories/control-variables/controlVariables.repository.ts b/libs/dal/src/repositories/control-variables/controlVariables.repository.ts
deleted file mode 100644
index 58c14394906..00000000000
--- a/libs/dal/src/repositories/control-variables/controlVariables.repository.ts
+++ /dev/null
@@ -1,18 +0,0 @@
-import { SoftDeleteModel } from 'mongoose-delete';
-import { ControlVariables, ControlVariablesModel } from './controlVariables.schema';
-import { ControlVariablesEntity } from './controlVariables.entity';
-import { BaseRepository } from '../base-repository';
-import { EnforceEnvOrOrgIds } from '../../types';
-
-export class ControlVariablesRepository extends BaseRepository<
- ControlVariablesModel,
- ControlVariablesEntity,
- EnforceEnvOrOrgIds
-> {
- private controlVariables: SoftDeleteModel;
-
- constructor() {
- super(ControlVariables, ControlVariablesEntity);
- this.controlVariables = ControlVariables;
- }
-}
diff --git a/libs/dal/src/repositories/control-variables/controlVariables.schema.ts b/libs/dal/src/repositories/control-variables/controlVariables.schema.ts
index b3a1832bfbc..7356ccc8d22 100644
--- a/libs/dal/src/repositories/control-variables/controlVariables.schema.ts
+++ b/libs/dal/src/repositories/control-variables/controlVariables.schema.ts
@@ -1,16 +1,16 @@
import mongoose, { Schema } from 'mongoose';
import { ChangePropsValueType } from '../../types';
import { schemaOptions } from '../schema-default.options';
-import { ControlVariablesEntity } from './controlVariables.entity';
+import { ControlValuesEntity } from './controlValuesEntity';
const mongooseDelete = require('mongoose-delete');
-export type ControlVariablesModel = ChangePropsValueType<
- ControlVariablesEntity,
+export type ControlValuesModel = ChangePropsValueType<
+ ControlValuesEntity,
'_environmentId' | '_organizationId' | '_workflowId'
>;
-const controlVariablesSchema = new Schema(
+const controlVariablesSchema = new Schema(
{
_environmentId: {
type: Schema.Types.ObjectId,
@@ -42,6 +42,6 @@ const controlVariablesSchema = new Schema(
controlVariablesSchema.plugin(mongooseDelete, { deletedAt: true, deletedBy: true, overrideMethods: 'all' });
export const ControlVariables =
- (mongoose.models.ControlVariables as mongoose.Model) ||
- mongoose.model('controls', controlVariablesSchema) ||
- mongoose.model('inputs', controlVariablesSchema);
+ (mongoose.models.ControlVariables as mongoose.Model) ||
+ mongoose.model('controls', controlVariablesSchema) ||
+ mongoose.model('inputs', controlVariablesSchema);
diff --git a/libs/dal/src/repositories/control-variables/index.ts b/libs/dal/src/repositories/control-variables/index.ts
index 2b7267d633f..2d17e19095b 100644
--- a/libs/dal/src/repositories/control-variables/index.ts
+++ b/libs/dal/src/repositories/control-variables/index.ts
@@ -1,3 +1,3 @@
-export * from './controlVariables.entity';
+export * from './controlValuesEntity';
export * from './controlVariables.schema';
-export * from './controlVariables.repository';
+export * from './controlValuesRepository';
diff --git a/libs/dal/src/repositories/job/job.schema.ts b/libs/dal/src/repositories/job/job.schema.ts
index 72a48f69fb3..5d05d9703cc 100644
--- a/libs/dal/src/repositories/job/job.schema.ts
+++ b/libs/dal/src/repositories/job/job.schema.ts
@@ -40,7 +40,7 @@ const jobSchema = new Schema(
ref: 'Notification',
},
_mergedDigestId: {
- type: Schema.Types.ObjectId,
+ type: String,
ref: 'Job',
},
subscriberId: {
diff --git a/libs/dal/src/repositories/message-template/message-template.entity.ts b/libs/dal/src/repositories/message-template/message-template.entity.ts
index e0feb393622..6ef793750b7 100644
--- a/libs/dal/src/repositories/message-template/message-template.entity.ts
+++ b/libs/dal/src/repositories/message-template/message-template.entity.ts
@@ -59,6 +59,7 @@ export class MessageTemplateEntity implements IMessageTemplate {
inputs?: {
schema: JSONSchema;
};
+
controls?: {
schema: JSONSchema;
};
diff --git a/libs/dal/src/repositories/message-template/message-template.repository.ts b/libs/dal/src/repositories/message-template/message-template.repository.ts
index b7232d507f5..75569f41d8c 100644
--- a/libs/dal/src/repositories/message-template/message-template.repository.ts
+++ b/libs/dal/src/repositories/message-template/message-template.repository.ts
@@ -7,7 +7,11 @@ import { MessageTemplateDBModel, MessageTemplateEntity } from './message-templat
import { MessageTemplate } from './message-template.schema';
type MessageTemplateQuery = FilterQuery;
-
+// eslint-disable-next-line @typescript-eslint/naming-convention
+export interface DeleteMsgByIdQuery {
+ _id: string;
+ _environmentId: string;
+}
export class MessageTemplateRepository extends BaseRepository<
MessageTemplateDBModel,
MessageTemplateEntity,
@@ -53,6 +57,22 @@ export class MessageTemplateRepository extends BaseRepository<
});
}
+ async deleteById(query: DeleteMsgByIdQuery) {
+ const messageTemplate = await this.findOne({
+ _id: query._id,
+ _environmentId: query._environmentId,
+ });
+
+ if (!messageTemplate) {
+ throw new DalException(`Could not find a message template with id ${query._id}`);
+ }
+
+ return await this.messageTemplate.delete({
+ _id: messageTemplate._id,
+ _environmentId: messageTemplate._environmentId,
+ });
+ }
+
async findDeleted(query: MessageTemplateQuery): Promise {
const res: MessageTemplateEntity = await this.messageTemplate.findDeleted(query);
diff --git a/libs/dal/src/repositories/message/message.repository.ts b/libs/dal/src/repositories/message/message.repository.ts
index d6231cccd95..0eb9fd5a14b 100644
--- a/libs/dal/src/repositories/message/message.repository.ts
+++ b/libs/dal/src/repositories/message/message.repository.ts
@@ -1,15 +1,15 @@
import { SoftDeleteModel } from 'mongoose-delete';
import { FilterQuery, Types } from 'mongoose';
import {
- MessagesStatusEnum,
- ChannelTypeEnum,
ActorTypeEnum,
ButtonTypeEnum,
+ ChannelTypeEnum,
MessageActionStatusEnum,
+ MessagesStatusEnum,
} from '@novu/shared';
import { BaseRepository } from '../base-repository';
-import { MessageEntity, MessageDBModel } from './message.entity';
+import { MessageDBModel, MessageEntity } from './message.entity';
import { Message } from './message.schema';
import { FeedRepository } from '../feed';
import { DalException } from '../../shared';
@@ -653,8 +653,12 @@ export class MessageRepository extends BaseRepository & EnforceEnvOrOrgIds;
+// eslint-disable-next-line @typescript-eslint/naming-convention
+export interface FindByIdQuery {
+ id: string;
+ environmentId: string;
+}
export class NotificationTemplateRepository extends BaseRepository<
NotificationTemplateDBModel,
@@ -35,12 +40,13 @@ export class NotificationTemplateRepository extends BaseRepository<
}
async findById(id: string, environmentId: string) {
- const requestQuery: NotificationTemplateQuery = {
- _id: id,
- _environmentId: environmentId,
- };
-
- const item = await this.MongooseModel.findOne(requestQuery)
+ return this.findByIdQuery({ id, environmentId });
+ }
+ async findByIdQuery(query: FindByIdQuery) {
+ const item = await this.MongooseModel.findOne({
+ _id: query.id,
+ _environmentId: query.environmentId,
+ })
.populate('steps.template')
.populate('steps.variants.template');
diff --git a/libs/dal/src/repositories/notification-template/notification-template.schema.ts b/libs/dal/src/repositories/notification-template/notification-template.schema.ts
index 6a30ebd9336..2c39d2ad6e0 100644
--- a/libs/dal/src/repositories/notification-template/notification-template.schema.ts
+++ b/libs/dal/src/repositories/notification-template/notification-template.schema.ts
@@ -1,4 +1,4 @@
-import { WorkflowTypeEnum } from '@novu/shared';
+import { WorkflowOriginEnum, WorkflowTypeEnum } from '@novu/shared';
import mongoose, { Schema } from 'mongoose';
import { schemaOptions } from '../schema-default.options';
@@ -26,6 +26,10 @@ const variantSchemePart = {
type: Schema.Types.String,
default: WorkflowTypeEnum.REGULAR,
},
+ origin: {
+ type: Schema.Types.String,
+ default: WorkflowOriginEnum.NOVU_CLOUD,
+ },
filters: [
{
isNegated: Schema.Types.Boolean,
diff --git a/libs/dal/src/repositories/subscriber/subscriber.repository.ts b/libs/dal/src/repositories/subscriber/subscriber.repository.ts
index 0dffaff88cf..38b316e0d22 100644
--- a/libs/dal/src/repositories/subscriber/subscriber.repository.ts
+++ b/libs/dal/src/repositories/subscriber/subscriber.repository.ts
@@ -2,7 +2,7 @@ import { SoftDeleteModel } from 'mongoose-delete';
import { FilterQuery } from 'mongoose';
import { EnvironmentId, ISubscribersDefine, OrganizationId } from '@novu/shared';
-import { SubscriberEntity, SubscriberDBModel } from './subscriber.entity';
+import { SubscriberDBModel, SubscriberEntity } from './subscriber.entity';
import { Subscriber } from './subscriber.schema';
import { IExternalSubscribersEntity } from './types';
import { BaseRepository } from '../base-repository';
@@ -55,11 +55,15 @@ export class SubscriberRepository extends BaseRepository = keyof T;
+
+export class LimitOffsetPaginationDto> {
+ limit: string;
+ offset: string;
+ orderDirection?: DirectionEnum;
+ orderByField?: K;
+}
+export enum DirectionEnum {
+ ASC = 'ASC',
+ DESC = 'DESC',
+}