diff --git a/.cspell.json b/.cspell.json index 5b103437191..93c9b7b81ff 100644 --- a/.cspell.json +++ b/.cspell.json @@ -6,6 +6,7 @@ "ABNF", "addrs", "adresses", + "sdkerror", "africas", "africastalking", "Aland", diff --git a/apps/api/package.json b/apps/api/package.json index f46d5d0655d..ad6acc52d24 100644 --- a/apps/api/package.json +++ b/apps/api/package.json @@ -59,6 +59,7 @@ "@sentry/tracing": "^7.40.0", "@types/newrelic": "^9.14.0", "@upstash/ratelimit": "^0.4.4", + "@novu/api": "^0.0.1-alpha.39", "axios": "^1.6.8", "liquidjs": "^10.13.1", "bcrypt": "^5.0.0", diff --git a/apps/api/src/app/bridge/bridge.module.ts b/apps/api/src/app/bridge/bridge.module.ts index bd9161cfebe..1e1ab224cb1 100644 --- a/apps/api/src/app/bridge/bridge.module.ts +++ b/apps/api/src/app/bridge/bridge.module.ts @@ -13,11 +13,23 @@ import { UpsertControlValuesUseCase, UpsertPreferences, DeletePreferencesUseCase, + TierRestrictionsValidateUsecase, } from '@novu/application-generic'; -import { PreferencesRepository } from '@novu/dal'; +import { CommunityOrganizationRepository, PreferencesRepository } from '@novu/dal'; import { SharedModule } from '../shared/shared.module'; import { BridgeController } from './bridge.controller'; import { USECASES } from './usecases'; +import { PostProcessWorkflowUpdate } from '../workflows-v2/usecases/post-process-workflow-update'; +import { OverloadContentDataOnWorkflowUseCase } from '../workflows-v2/usecases/overload-content-data'; +import { + BuildDefaultPayloadUsecase, + CollectPlaceholderWithDefaultsUsecase, + PrepareAndValidateContentUsecase, + ValidatePlaceholderUsecase, +} from '../workflows-v2/usecases/validate-content'; +import { BuildAvailableVariableSchemaUsecase } from '../workflows-v2/usecases/build-variable-schema'; +import { ExtractDefaultValuesFromSchemaUsecase } from '../workflows-v2/usecases/extract-default-values-from-schema'; +import { HydrateEmailSchemaUseCase } from '../environments-v1/usecases/output-renderers/hydrate-email-schema.usecase'; const PROVIDERS = [ CreateWorkflow, @@ -35,6 +47,17 @@ const PROVIDERS = [ UpsertPreferences, DeletePreferencesUseCase, UpsertControlValuesUseCase, + PostProcessWorkflowUpdate, + OverloadContentDataOnWorkflowUseCase, + PrepareAndValidateContentUsecase, + BuildAvailableVariableSchemaUsecase, + BuildDefaultPayloadUsecase, + ValidatePlaceholderUsecase, + CollectPlaceholderWithDefaultsUsecase, + ExtractDefaultValuesFromSchemaUsecase, + TierRestrictionsValidateUsecase, + HydrateEmailSchemaUseCase, + CommunityOrganizationRepository, ]; @Module({ 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 c6c47af0382..5d11e2d8931 100644 --- a/apps/api/src/app/bridge/usecases/sync/sync.usecase.ts +++ b/apps/api/src/app/bridge/usecases/sync/sync.usecase.ts @@ -1,6 +1,7 @@ import { BadRequestException, HttpException, Injectable } from '@nestjs/common'; import { + EnvironmentEntity, EnvironmentRepository, NotificationGroupRepository, NotificationTemplateEntity, @@ -20,6 +21,7 @@ import { import { buildWorkflowPreferences, JSONSchemaDto, + UserSessionData, WorkflowCreationSourceEnum, WorkflowOriginEnum, WorkflowPreferences, @@ -29,6 +31,7 @@ import { DiscoverOutput, DiscoverStepOutput, DiscoverWorkflowOutput, GetActionEn import { SyncCommand } from './sync.command'; import { CreateBridgeResponseDto } from '../../dtos/create-bridge-response.dto'; +import { PostProcessWorkflowUpdate } from '../../../workflows-v2/usecases/post-process-workflow-update'; @Injectable() export class Sync { @@ -40,15 +43,35 @@ export class Sync { private notificationGroupRepository: NotificationGroupRepository, private environmentRepository: EnvironmentRepository, private executeBridgeRequest: ExecuteBridgeRequest, + private workflowUpdatePostProcess: PostProcessWorkflowUpdate, private analyticsService: AnalyticsService ) {} async execute(command: SyncCommand): Promise { - const environment = await this.environmentRepository.findOne({ _id: command.environmentId }); + const environment = await this.findEnvironment(command); + const discover = await this.executeDiscover(command); + this.sendAnalytics(command, environment, discover); + const persistedWorkflowsInBridge = await this.processWorkflows(command, discover.workflows); - if (!environment) { - throw new BadRequestException('Environment not found'); + await this.disposeOldWorkflows(command, persistedWorkflowsInBridge); + await this.updateBridgeUrl(command); + + return persistedWorkflowsInBridge; + } + + private sendAnalytics(command: SyncCommand, environment: EnvironmentEntity, discover: DiscoverOutput) { + if (command.source !== 'sample-workspace') { + this.analyticsService.track('Sync Request - [Bridge API]', command.userId, { + _organization: command.organizationId, + _environment: command.environmentId, + environmentName: environment.name, + workflowsCount: discover.workflows?.length || 0, + localEnvironment: !!command.bridgeUrl?.includes('novu.sh'), + source: command.source, + }); } + } + private async executeDiscover(command: SyncCommand): Promise { let discover: DiscoverOutput | undefined; try { discover = (await this.executeBridgeRequest.execute({ @@ -70,24 +93,17 @@ export class Sync { throw new BadRequestException('Invalid Bridge URL Response'); } - if (command.source !== 'sample-workspace') { - this.analyticsService.track('Sync Request - [Bridge API]', command.userId, { - _organization: command.organizationId, - _environment: command.environmentId, - environmentName: environment.name, - workflowsCount: discover.workflows?.length || 0, - localEnvironment: !!command.bridgeUrl?.includes('novu.sh'), - source: command.source, - }); - } - - const persistedWorkflowsInBridge = await this.createWorkflows(command, discover.workflows); + return discover; + } - await this.disposeOldWorkflows(command, persistedWorkflowsInBridge); + private async findEnvironment(command: SyncCommand): Promise { + const environment = await this.environmentRepository.findOne({ _id: command.environmentId }); - await this.updateBridgeUrl(command); + if (!environment) { + throw new BadRequestException('Environment not found'); + } - return persistedWorkflowsInBridge; + return environment; } private async updateBridgeUrl(command: SyncCommand): Promise { @@ -142,46 +158,79 @@ export class Sync { }); } - private async createWorkflows( + private async processWorkflows( command: SyncCommand, workflowsFromBridge: DiscoverWorkflowOutput[] ): Promise { return Promise.all( workflowsFromBridge.map(async (workflow) => { - const workflowExist = await this.notificationTemplateRepository.findByTriggerIdentifier( - command.environmentId, - workflow.workflowId + let savedWorkflow = await this.upsertWorkflow(command, workflow); + + const validatedWorkflowWithIssues = await this.workflowUpdatePostProcess.execute({ + user: { + _id: command.userId, + environmentId: command.environmentId, + organizationId: command.organizationId, + } as UserSessionData, + workflow: { + ...savedWorkflow, + userPreferences: null, + defaultPreferences: this.getWorkflowPreferences(workflow), + }, + }); + + savedWorkflow = await this.updateWorkflowUsecase.execute( + UpdateWorkflowCommand.create({ + ...validatedWorkflowWithIssues, + id: validatedWorkflowWithIssues._id, + type: WorkflowTypeEnum.BRIDGE, + environmentId: command.environmentId, + organizationId: command.organizationId, + userId: command.userId, + }) ); - let savedWorkflow: NotificationTemplateEntity | undefined; + return savedWorkflow; + }) + ); + } - if (workflowExist) { - savedWorkflow = await this.updateWorkflow(workflowExist, command, workflow); - } else { - const notificationGroupId = await this.getNotificationGroup( - this.castToAnyNotSupportedParam(workflow)?.notificationGroupId, - command.environmentId - ); + private async upsertWorkflow( + command: SyncCommand, + workflow: DiscoverWorkflowOutput + ): Promise { + const workflowExist = await this.notificationTemplateRepository.findByTriggerIdentifier( + command.environmentId, + workflow.workflowId + ); - if (!notificationGroupId) { - throw new BadRequestException('Notification group not found'); - } - const isWorkflowActive = this.castToAnyNotSupportedParam(workflow)?.active ?? true; + let savedWorkflow: NotificationTemplateEntity | undefined; - savedWorkflow = await this.createWorkflow(notificationGroupId, isWorkflowActive, command, workflow); - } + if (workflowExist) { + savedWorkflow = await this.updateWorkflowUsecase.execute( + UpdateWorkflowCommand.create(this.mapDiscoverWorkflowToUpdateWorkflowCommand(workflowExist, command, workflow)) + ); + } else { + savedWorkflow = await this.createWorkflow(command, workflow); + } - return savedWorkflow; - }) - ); + return savedWorkflow; } private async createWorkflow( - notificationGroupId: string, - isWorkflowActive: boolean, command: SyncCommand, workflow: DiscoverWorkflowOutput ): Promise { + const notificationGroupId = await this.getNotificationGroup( + this.castToAnyNotSupportedParam(workflow)?.notificationGroupId, + command.environmentId + ); + + if (!notificationGroupId) { + throw new BadRequestException('Notification group not found'); + } + const isWorkflowActive = this.castToAnyNotSupportedParam(workflow)?.active ?? true; + return await this.createWorkflowUsecase.execute( CreateWorkflowCommand.create({ origin: WorkflowOriginEnum.EXTERNAL, @@ -209,33 +258,31 @@ export class Sync { ); } - private async updateWorkflow( + private mapDiscoverWorkflowToUpdateWorkflowCommand( workflowExist: NotificationTemplateEntity, command: SyncCommand, workflow: DiscoverWorkflowOutput - ): Promise { - return await this.updateWorkflowUsecase.execute( - UpdateWorkflowCommand.create({ - id: workflowExist._id, - environmentId: command.environmentId, - organizationId: command.organizationId, - userId: command.userId, - name: this.getWorkflowName(workflow), - workflowId: workflow.workflowId, - steps: this.mapSteps(workflow.steps, workflowExist), - controls: { - schema: workflow.controls?.schema as JSONSchemaDto, - }, - rawData: workflow, - payloadSchema: workflow.payload?.schema as unknown as JSONSchemaDto, - type: WorkflowTypeEnum.BRIDGE, - description: this.getWorkflowDescription(workflow), - data: this.castToAnyNotSupportedParam(workflow)?.data, - tags: this.getWorkflowTags(workflow), - active: this.castToAnyNotSupportedParam(workflow)?.active ?? true, - defaultPreferences: this.getWorkflowPreferences(workflow), - }) - ); + ): UpdateWorkflowCommand { + return { + id: workflowExist._id, + environmentId: command.environmentId, + organizationId: command.organizationId, + userId: command.userId, + name: this.getWorkflowName(workflow), + workflowId: workflow.workflowId, + steps: this.mapSteps(workflow.steps, workflowExist), + controls: { + schema: workflow.controls?.schema as JSONSchemaDto, + }, + rawData: workflow, + payloadSchema: workflow.payload?.schema as unknown as JSONSchemaDto, + type: WorkflowTypeEnum.BRIDGE, + description: this.getWorkflowDescription(workflow), + data: this.castToAnyNotSupportedParam(workflow)?.data, + tags: this.getWorkflowTags(workflow), + active: this.castToAnyNotSupportedParam(workflow)?.active ?? true, + defaultPreferences: this.getWorkflowPreferences(workflow), + }; } private mapSteps( diff --git a/apps/api/src/app/events/dtos/trigger-event-request.dto.ts b/apps/api/src/app/events/dtos/trigger-event-request.dto.ts index c405ad086d0..4ea6fecd5b9 100644 --- a/apps/api/src/app/events/dtos/trigger-event-request.dto.ts +++ b/apps/api/src/app/events/dtos/trigger-event-request.dto.ts @@ -57,6 +57,12 @@ export class TriggerEventRequestDto { }, }, }) + @ApiProperty({ + type: 'object', + description: 'An optional payload object that can contain any properties', + required: false, + additionalProperties: true, + }) @IsObject() @IsOptional() payload?: Record; @@ -87,14 +93,14 @@ export class TriggerEventRequestDto { { $ref: getSchemaPath(SubscriberPayloadDto), }, + { + $ref: getSchemaPath(TopicPayloadDto), + }, { type: 'string', description: 'Unique identifier of a subscriber in your systems', example: 'SUBSCRIBER_ID', }, - { - $ref: getSchemaPath(TopicPayloadDto), - }, ], }, }) diff --git a/apps/api/src/app/events/e2e/bulk-trigger.e2e.ts b/apps/api/src/app/events/e2e/bulk-trigger.e2e.ts index 905e81d50f2..6b72a4cb2a7 100644 --- a/apps/api/src/app/events/e2e/bulk-trigger.e2e.ts +++ b/apps/api/src/app/events/e2e/bulk-trigger.e2e.ts @@ -1,13 +1,15 @@ import { expect } from 'chai'; -import axios from 'axios'; import { MessageRepository, NotificationRepository, NotificationTemplateEntity, SubscriberEntity } from '@novu/dal'; -import { UserSession, SubscribersService } from '@novu/testing'; +import { SubscribersService, UserSession } from '@novu/testing'; import { ChannelTypeEnum, StepTypeEnum } from '@novu/shared'; - -const axiosInstance = axios.create(); +import { Novu } from '@novu/api'; +import { triggerBulk } from '@novu/api/funcs/triggerBulk'; +import { TriggerEventRequestDto } from '@novu/api/models/components'; +import { z } from 'zod'; +import { NovuCore } from '@novu/api/core'; +import { handleSdkError, initNovuClassSdk, initNovuFunctionSdk } from '../../shared/helpers/e2e/sdk/e2e-sdk.helper'; describe('Trigger bulk events - /v1/events/trigger/bulk (POST)', function () { - const BULK_API_ENDPOINT = '/v1/events/trigger/bulk'; let session: UserSession; let template: NotificationTemplateEntity; let secondTemplate: NotificationTemplateEntity; @@ -16,6 +18,8 @@ describe('Trigger bulk events - /v1/events/trigger/bulk (POST)', function () { let subscriberService: SubscribersService; const notificationRepository = new NotificationRepository(); const messageRepository = new MessageRepository(); + let novuClient: Novu; + let novuCore: NovuCore; beforeEach(async () => { session = new UserSession(); @@ -32,101 +36,93 @@ describe('Trigger bulk events - /v1/events/trigger/bulk (POST)', function () { subscriberService = new SubscribersService(session.organization._id, session.environment._id); subscriber = await subscriberService.createSubscriber(); secondSubscriber = await subscriberService.createSubscriber(); + novuClient = initNovuClassSdk(session); + novuCore = initNovuFunctionSdk(session); }); it('should return the response array in correct order', async function () { - const response = await axiosInstance.post( - `${session.serverUrl}${BULK_API_ENDPOINT}`, - { - events: [ - { - transactionId: '1111', - name: template.triggers[0].identifier, - to: subscriber.subscriberId, - payload: { - firstName: 'Testing of User Name', - urlVariable: '/test/url/path', - }, + const bulkTriggerResponse = await triggerBulk(novuCore, { + events: [ + { + transactionId: '1111', + name: template.triggers[0].identifier, + to: [subscriber.subscriberId], + payload: { + firstName: 'Testing of User Name', + urlVariable: '/test/url/path', }, - { - transactionId: '2222', - name: template.triggers[0].identifier, - to: subscriber.subscriberId, - payload: { - firstName: 'Testing of User Name', - urlVariable: '/test/url/path', - }, + }, + { + transactionId: '2222', + name: template.triggers[0].identifier, + to: [subscriber.subscriberId], + payload: { + firstName: 'Testing of User Name', + urlVariable: '/test/url/path', }, - { - transactionId: '3333', - name: template.triggers[0].identifier, - to: subscriber.subscriberId, - payload: { - firstName: 'Testing of User Name', - urlVariable: '/test/url/path', - }, + }, + { + transactionId: '3333', + name: template.triggers[0].identifier, + to: [subscriber.subscriberId], + payload: { + firstName: 'Testing of User Name', + urlVariable: '/test/url/path', }, - ], - }, - { - headers: { - authorization: `ApiKey ${session.apiKey}`, }, - } - ); - - const { data: body } = response; - expect(body.data).to.be.ok; - expect(body.data.length).to.equal(3); + ], + }); + if (!bulkTriggerResponse.ok) { + throw new Error(`Failed to make bulkTriggerResponse\n${JSON.stringify(bulkTriggerResponse.error, null, 2)}`); + } + const value = bulkTriggerResponse.value.result; + expect(bulkTriggerResponse).to.be.ok; + expect(bulkTriggerResponse.value.result.length).to.equal(3); - const firstEvent = body.data[0]; + const firstEvent = bulkTriggerResponse.value.result[0]; expect(firstEvent.status).to.equal('processed'); expect(firstEvent.acknowledged).to.equal(true); expect(firstEvent.transactionId).to.equal('1111'); - const secondEvent = body.data[1]; + const secondEvent = bulkTriggerResponse.value.result[1]; expect(secondEvent.status).to.equal('processed'); expect(secondEvent.acknowledged).to.equal(true); expect(secondEvent.transactionId).to.equal('2222'); - const thirdEvent = body.data[2]; + const thirdEvent = bulkTriggerResponse.value.result[2]; expect(thirdEvent.status).to.equal('processed'); expect(thirdEvent.acknowledged).to.equal(true); expect(thirdEvent.transactionId).to.equal('3333'); }); - it('should generate message and notification based on a bulk event', async function () { - const { data: body } = await axiosInstance.post( - `${session.serverUrl}${BULK_API_ENDPOINT}`, - { - events: [ - { - name: template.triggers[0].identifier, - to: { + it('should gene?rate message and notification based on a bulk event', async function () { + await novuClient.triggerBulk({ + events: [ + { + name: template.triggers[0].identifier, + to: [ + { subscriberId: subscriber.subscriberId, }, - payload: { - firstName: 'Testing of User Name', - urlVar: '/test/url/path', - }, + ], + payload: { + firstName: 'Testing of User Name', + urlVar: '/test/url/path', }, - { - name: secondTemplate.triggers[0].identifier, - to: { + }, + { + name: secondTemplate.triggers[0].identifier, + to: [ + { subscriberId: secondSubscriber.subscriberId, }, - payload: { - firstName: 'This is a second template', - }, + ], + payload: { + firstName: 'This is a second template', }, - ], - }, - { - headers: { - authorization: `ApiKey ${session.apiKey}`, }, - } - ); + ], + }); await session.awaitRunningJobs(template._id); await session.awaitRunningJobs(secondTemplate._id); @@ -200,10 +196,10 @@ describe('Trigger bulk events - /v1/events/trigger/bulk (POST)', function () { }); it('should throw an error when sending more than 100 events', async function () { - const event = { + const event: TriggerEventRequestDto = { transactionId: '2222', name: template.triggers[0].identifier, - to: subscriber.subscriberId, + to: [subscriber.subscriberId], payload: { firstName: 'Testing of User Name', urlVariable: '/test/url/path', @@ -212,73 +208,67 @@ describe('Trigger bulk events - /v1/events/trigger/bulk (POST)', function () { let error; try { - await axiosInstance.post( - `${session.serverUrl}${BULK_API_ENDPOINT}`, - { - events: Array.from({ length: 101 }, () => event), - }, - { - headers: { - authorization: `ApiKey ${session.apiKey}`, - }, - } - ); + await novuClient.triggerBulk({ + events: Array.from({ length: 101 }, () => event), + }); } catch (e) { error = e; } + const { error: sdkError, parsedBody } = handleSdkError(error); - expect(error).to.be.ok; - expect(error.response.status).to.equal(400); - expect(error.response.data.message[0]).to.equal('events must contain no more than 100 elements'); + expect(sdkError.statusCode).to.equal(400); + expect(parsedBody.statusCode).to.equal(400); + expect(parsedBody.message[0]).to.equal('events must contain no more than 100 elements'); }); it('should handle bulk if one of the events returns errors', async function () { - const response = await axiosInstance.post( - `${session.serverUrl}${BULK_API_ENDPOINT}`, - { - events: [ - { - transactionId: '1111', - name: 'non-existing-trigger', - to: subscriber.subscriberId, - payload: { - firstName: 'Testing of User Name', - urlVariable: '/test/url/path', - }, + const bulkTriggerResponse = await triggerBulk(novuCore, { + events: [ + { + transactionId: '1111', + name: 'non-existing-trigger', + to: [subscriber.subscriberId], + payload: { + firstName: 'Testing of User Name', + urlVariable: '/test/url/path', }, - { - transactionId: '2222', - name: template.triggers[0].identifier, - to: subscriber.subscriberId, - payload: { - firstName: 'Testing of User Name', - urlVariable: '/test/url/path', - }, + }, + { + transactionId: '2222', + name: template.triggers[0].identifier, + to: [subscriber.subscriberId], + payload: { + firstName: 'Testing of User Name', + urlVariable: '/test/url/path', }, - { - transactionId: '1111', - payload: { - firstName: 'Testing of User Name', - urlVariable: '/test/url/path', - }, + }, + { + transactionId: '1111', + payload: { + firstName: 'Testing of User Name', + name: '', }, - ], - }, - { - headers: { - authorization: `ApiKey ${session.apiKey}`, + name: '', + to: [], }, - } - ); + ], + }); + if (!bulkTriggerResponse.ok) { + throw new Error(`failed to bulk trigger:${JSON.stringify(bulkTriggerResponse.error)}`); + } - const { data: body } = response; - expect(body.data).to.be.ok; - expect(body.data.length).to.equal(3); + const dtoList = bulkTriggerResponse.value.result; + expect(dtoList).to.be.ok; + expect(dtoList.length).to.equal(3); - const errorEvent = body.data[0]; + const errorEvent = dtoList[0]; + z; + if (!errorEvent.error) { + throw new Error('should have been an error'); + } expect(errorEvent.error[0]).to.equal('workflow_not_found'); expect(errorEvent.status).to.equal('error'); - expect(body.data[1].status).to.equal('processed'); + expect(dtoList[1].status).to.equal('processed'); }); }); diff --git a/apps/api/src/app/events/usecases/process-bulk-trigger/process-bulk-trigger.usecase.ts b/apps/api/src/app/events/usecases/process-bulk-trigger/process-bulk-trigger.usecase.ts index bc75cf03201..c02d22ba8e8 100644 --- a/apps/api/src/app/events/usecases/process-bulk-trigger/process-bulk-trigger.usecase.ts +++ b/apps/api/src/app/events/usecases/process-bulk-trigger/process-bulk-trigger.usecase.ts @@ -13,10 +13,12 @@ export class ProcessBulkTrigger { async execute(command: ProcessBulkTriggerCommand) { const results: TriggerEventResponseDto[] = []; + console.log('event.to', JSON.stringify(command)); for (const event of command.events) { let result: TriggerEventResponseDto; - + console.log('event.to', event.to); + console.log('event.to', event.payload); try { result = (await this.parseEventRequest.execute( ParseEventRequestMulticastCommand.create({ diff --git a/apps/api/src/app/notifications/dtos/activities-request.dto.ts b/apps/api/src/app/notifications/dtos/activities-request.dto.ts index 4c452c1e727..a9c7f1b5b71 100644 --- a/apps/api/src/app/notifications/dtos/activities-request.dto.ts +++ b/apps/api/src/app/notifications/dtos/activities-request.dto.ts @@ -1,36 +1,36 @@ -import { ApiProperty, ApiPropertyOptional } from '@nestjs/swagger'; +import { ApiPropertyOptional } from '@nestjs/swagger'; import { ChannelTypeEnum } from '@novu/shared'; export class ActivitiesRequestDto { - @ApiProperty({ + @ApiPropertyOptional({ enum: ChannelTypeEnum, isArray: true, }) - channels: ChannelTypeEnum[] | ChannelTypeEnum; + channels?: ChannelTypeEnum[] | ChannelTypeEnum; - @ApiProperty({ + @ApiPropertyOptional({ type: String, isArray: true, }) - templates: string[] | string; + templates?: string[] | string; - @ApiProperty({ + @ApiPropertyOptional({ type: String, isArray: true, }) - emails: string | string[]; + emails?: string | string[]; - @ApiProperty({ + @ApiPropertyOptional({ type: String, deprecated: true, }) - search: string; + search?: string; - @ApiProperty({ + @ApiPropertyOptional({ type: String, isArray: true, }) - subscriberIds: string | string[]; + subscriberIds?: string | string[]; @ApiPropertyOptional({ type: Number, @@ -42,5 +42,5 @@ export class ActivitiesRequestDto { type: String, required: false, }) - transactionId: string; + transactionId?: string; } diff --git a/apps/api/src/app/notifications/dtos/activities-response.dto.ts b/apps/api/src/app/notifications/dtos/activities-response.dto.ts index 8f237ac62f4..1adad68d087 100644 --- a/apps/api/src/app/notifications/dtos/activities-response.dto.ts +++ b/apps/api/src/app/notifications/dtos/activities-response.dto.ts @@ -169,7 +169,7 @@ export class ActivitiesResponseDto { @ApiProperty() hasMore: boolean; - @ApiProperty() + @ApiProperty({ type: [ActivityNotificationResponseDto], description: 'Array of Activity notifications' }) data: ActivityNotificationResponseDto[]; @ApiProperty() diff --git a/apps/api/src/app/shared/helpers/e2e/sdk/e2e-sdk.helper.ts b/apps/api/src/app/shared/helpers/e2e/sdk/e2e-sdk.helper.ts new file mode 100644 index 00000000000..64a1dafb6e1 --- /dev/null +++ b/apps/api/src/app/shared/helpers/e2e/sdk/e2e-sdk.helper.ts @@ -0,0 +1,29 @@ +import { Novu } from '@novu/api'; +import { NovuCore } from '@novu/api/core'; +import { UserSession } from '@novu/testing'; +import { SDKError } from '@novu/api/models/errors/sdkerror'; +import { expect } from 'chai'; + +export function initNovuClassSdk(session: UserSession): Novu { + return new Novu({ apiKey: session.apiKey, serverURL: session.serverUrl, debugLogger: console }); +} +export function initNovuFunctionSdk(session: UserSession): NovuCore { + return new NovuCore({ apiKey: session.apiKey, serverURL: session.serverUrl, debugLogger: console }); +} + +function isSDKError(error: unknown): error is SDKError { + return typeof error === 'object' && error !== null && 'name' in error && 'body' in error; +} + +export function handleSdkError(error: unknown): { error: SDKError; parsedBody: any } { + if (!isSDKError(error)) { + throw new Error('Provided error is not an SDKError'); + } + + expect(error.name).to.equal('SDKError'); + expect(error.body).to.be.ok; + expect(typeof error.body).to.be.eq('string'); + const errorBody = JSON.parse(error.body); + + return { error, parsedBody: errorBody }; +} diff --git a/apps/api/src/app/subscribers/subscribers.controller.ts b/apps/api/src/app/subscribers/subscribers.controller.ts index f9bdb53a3c9..cc68b281b5d 100644 --- a/apps/api/src/app/subscribers/subscribers.controller.ts +++ b/apps/api/src/app/subscribers/subscribers.controller.ts @@ -24,7 +24,7 @@ import { UpdateSubscriberChannelCommand, UpdateSubscriberCommand, } from '@novu/application-generic'; -import { ApiExcludeEndpoint, ApiOperation, ApiParam, ApiTags, ApiQuery } from '@nestjs/swagger'; +import { ApiExcludeEndpoint, ApiOperation, ApiParam, ApiQuery, ApiTags } from '@nestjs/swagger'; import { ApiRateLimitCategoryEnum, ApiRateLimitCostEnum, diff --git a/apps/api/src/app/workflows-v2/generate-preview.e2e.ts b/apps/api/src/app/workflows-v2/generate-preview.e2e.ts index 86ffa3b8fd3..4e267cb70e1 100644 --- a/apps/api/src/app/workflows-v2/generate-preview.e2e.ts +++ b/apps/api/src/app/workflows-v2/generate-preview.e2e.ts @@ -13,6 +13,7 @@ import { GeneratePreviewResponseDto, HttpError, NovuRestResult, + PreviewIssueEnum, RedirectTargetEnum, StepTypeEnum, WorkflowCreationSourceEnum, diff --git a/apps/api/src/app/workflows-v2/usecases/extract-default-values-from-schema/extract-default-values-from-schema.usecase.ts b/apps/api/src/app/workflows-v2/usecases/extract-default-values-from-schema/extract-default-values-from-schema.usecase.ts index 1ab76f78103..c745d8e622e 100644 --- a/apps/api/src/app/workflows-v2/usecases/extract-default-values-from-schema/extract-default-values-from-schema.usecase.ts +++ b/apps/api/src/app/workflows-v2/usecases/extract-default-values-from-schema/extract-default-values-from-schema.usecase.ts @@ -1,4 +1,4 @@ -import { JSONSchemaDto } from '@novu/shared'; +import { JSONSchemaDto, PreviewIssueEnum } from '@novu/shared'; import { Injectable } from '@nestjs/common'; import { ExtractDefaultValuesFromSchemaCommand } from './extract-default-values-from-schema.command'; @@ -35,7 +35,7 @@ export class ExtractDefaultValuesFromSchemaUsecase { if (key.toLowerCase().trim().includes('url')) { result[key] = 'https://www.example.com/search?query=placeholder'; } - result[key] = 'PREVIEW_ISSUE:REQUIRED_CONTROL_VALUE_IS_MISSING'; + result[key] = PreviewIssueEnum.PREVIEW_ISSUE_REQUIRED_CONTROL_VALUE_IS_MISSING; } else { result[key] = value.default; } diff --git a/apps/dashboard/src/components/confirmation-modal.tsx b/apps/dashboard/src/components/confirmation-modal.tsx index b3c5a34a490..7ad1120a058 100644 --- a/apps/dashboard/src/components/confirmation-modal.tsx +++ b/apps/dashboard/src/components/confirmation-modal.tsx @@ -53,7 +53,7 @@ export const ConfirmationModal = ({ - diff --git a/apps/dashboard/src/components/create-workflow-button.tsx b/apps/dashboard/src/components/create-workflow-button.tsx index 1fedb2ce178..5ca61fe9681 100644 --- a/apps/dashboard/src/components/create-workflow-button.tsx +++ b/apps/dashboard/src/components/create-workflow-button.tsx @@ -188,7 +188,7 @@ export const CreateWorkflowButton = (props: CreateWorkflowButtonProps) => { - diff --git a/apps/dashboard/src/components/header-navigation/edit-bridge-url-button.tsx b/apps/dashboard/src/components/header-navigation/edit-bridge-url-button.tsx index e2ec8072884..8715026d995 100644 --- a/apps/dashboard/src/components/header-navigation/edit-bridge-url-button.tsx +++ b/apps/dashboard/src/components/header-navigation/edit-bridge-url-button.tsx @@ -109,6 +109,7 @@ export const EditBridgeUrlButton = () => { type="submit" variant="primary" size="xs" + isLoading={isUpdatingBridgeUrl} disabled={!isDirty || isValidatingBridgeUrl || isUpdatingBridgeUrl} > Update endpoint diff --git a/apps/dashboard/src/components/primitives/button.tsx b/apps/dashboard/src/components/primitives/button.tsx index e5bf280f616..632308afbf3 100644 --- a/apps/dashboard/src/components/primitives/button.tsx +++ b/apps/dashboard/src/components/primitives/button.tsx @@ -2,6 +2,7 @@ import * as React from 'react'; import { Slot } from '@radix-ui/react-slot'; import { cva, type VariantProps } from 'class-variance-authority'; import { cn } from '@/utils/ui'; +import { RiLoader4Line } from 'react-icons/ri'; export const buttonVariants = cva( `relative isolate inline-flex items-center justify-center whitespace-nowrap rounded-lg gap-1 text-sm font-medium transition-colors focus-visible:outline-none focus-visible:ring-2 focus-visible:ring-ring disabled:pointer-events-none disabled:opacity-50`, @@ -43,12 +44,34 @@ export interface ButtonProps extends React.ButtonHTMLAttributes, VariantProps { asChild?: boolean; + isLoading?: boolean; } const Button = React.forwardRef( - ({ className, variant, size, asChild = false, ...props }, ref) => { + ({ className, variant, size, asChild = false, isLoading = false, children, disabled, ...props }, ref) => { const Comp = asChild ? Slot : 'button'; - return ; + return ( + + + {children} + + {isLoading && ( +
+ +
+ )} +
+ ); } ); Button.displayName = 'Button'; diff --git a/apps/dashboard/src/components/workflow-editor/test-workflow/test-workflow-tabs.tsx b/apps/dashboard/src/components/workflow-editor/test-workflow/test-workflow-tabs.tsx index 3ac1458f725..df8b490e2c9 100644 --- a/apps/dashboard/src/components/workflow-editor/test-workflow/test-workflow-tabs.tsx +++ b/apps/dashboard/src/components/workflow-editor/test-workflow/test-workflow-tabs.tsx @@ -1,6 +1,6 @@ import { useMemo } from 'react'; import { Link, useParams } from 'react-router-dom'; -import { RiPlayCircleLine, RiProgress1Fill } from 'react-icons/ri'; +import { RiPlayCircleLine } from 'react-icons/ri'; import { useForm } from 'react-hook-form'; // eslint-disable-next-line // @ts-ignore @@ -127,8 +127,8 @@ export const TestWorkflowTabs = ({ testData }: { testData: WorkflowTestDataRespo
-
diff --git a/apps/dashboard/src/pages/usecase-select-page.tsx b/apps/dashboard/src/pages/usecase-select-page.tsx index 30f44a99ef9..89119615a52 100644 --- a/apps/dashboard/src/pages/usecase-select-page.tsx +++ b/apps/dashboard/src/pages/usecase-select-page.tsx @@ -8,7 +8,6 @@ import { useNavigate } from 'react-router-dom'; import { OnboardingArrowLeft } from '../components/icons/onboarding-arrow-left'; import { updateClerkOrgMetadata } from '../api/organization'; import { ChannelTypeEnum } from '@novu/shared'; -import { RiLoader2Line } from 'react-icons/ri'; import { PageMeta } from '../components/page-meta'; import { useTelemetry } from '../hooks'; import { TelemetryEvent } from '../utils/telemetry'; @@ -79,9 +78,8 @@ export function UsecaseSelectPage() { />
-