diff --git a/apps/api/package.json b/apps/api/package.json index 97aaf593c05..81c211f9834 100644 --- a/apps/api/package.json +++ b/apps/api/package.json @@ -1,5 +1,5 @@ { - "name": "@novu/api", + "name": "@novu/api-service", "version": "2.1.1", "description": "description", "author": "", @@ -42,7 +42,7 @@ "@nestjs/swagger": "7.4.0", "@nestjs/terminus": "10.2.3", "@nestjs/throttler": "6.2.1", - "@novu/api": "0.0.1-alpha.109", + "@novu/api": "0.0.1-alpha.119", "@novu/application-generic": "workspace:*", "@novu/dal": "workspace:*", "@novu/framework": "workspace:*", diff --git a/apps/api/src/app/messages/e2e/remove-messages-by-transactionId.e2e.ts b/apps/api/src/app/messages/e2e/remove-messages-by-transactionId.e2e.ts index f34b7708f59..e982f9be7b6 100644 --- a/apps/api/src/app/messages/e2e/remove-messages-by-transactionId.e2e.ts +++ b/apps/api/src/app/messages/e2e/remove-messages-by-transactionId.e2e.ts @@ -3,7 +3,7 @@ import { ChannelTypeEnum } from '@novu/shared'; import { SubscribersService, UserSession } from '@novu/testing'; import { expect } from 'chai'; import { Novu } from '@novu/api'; -import { initNovuClassSdk } from '../../shared/helpers/e2e/sdk/e2e-sdk.helper'; +import { expectSdkExceptionGeneric, initNovuClassSdk } from '../../shared/helpers/e2e/sdk/e2e-sdk.helper'; describe('Delete Messages By TransactionId - /messages/?transactionId= (DELETE)', function () { let session: UserSession; @@ -23,10 +23,9 @@ describe('Delete Messages By TransactionId - /messages/?transactionId= (DELETE)' }); it('should fail to delete non existing message', async function () { - const response = await session.testAgent.delete(`/v1/messages/transaction/abc-1234`); - - expect(response.statusCode).to.equal(404); - expect(response.body.error).to.equal('Not Found'); + const { error } = await expectSdkExceptionGeneric(() => novuClient.messages.deleteByTransactionId('abc-1234')); + expect(error?.statusCode).to.equal(404); + expect(error?.name, JSON.stringify(error)).to.equal('Not Found'); }); it('should delete messages by transactionId', async function () { 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 c6778d19950..721fa65a198 100644 --- a/apps/api/src/app/notifications/dtos/activities-request.dto.ts +++ b/apps/api/src/app/notifications/dtos/activities-request.dto.ts @@ -4,57 +4,71 @@ import { IsOptional } from 'class-validator'; export class ActivitiesRequestDto { @ApiPropertyOptional({ - enum: ChannelTypeEnum, + enum: [...Object.values(ChannelTypeEnum)], + enumName: 'ChannelTypeEnum', isArray: true, + description: 'Array of channel types', }) + @IsOptional() channels?: ChannelTypeEnum[] | ChannelTypeEnum; @ApiPropertyOptional({ type: String, isArray: true, + description: 'Array of template IDs or a single template ID', }) + @IsOptional() templates?: string[] | string; @ApiPropertyOptional({ type: String, isArray: true, + description: 'Array of email addresses or a single email address', }) + @IsOptional() emails?: string | string[]; @ApiPropertyOptional({ type: String, deprecated: true, + description: 'Search term (deprecated)', }) + @IsOptional() search?: string; @ApiPropertyOptional({ type: String, isArray: true, + description: 'Array of subscriber IDs or a single subscriber ID', }) + @IsOptional() subscriberIds?: string | string[]; @ApiPropertyOptional({ type: Number, - required: false, + default: 0, + description: 'Page number for pagination', }) - page?: number = 0; + @IsOptional() + page?: number; @ApiPropertyOptional({ type: String, - required: false, + description: 'Transaction ID for filtering', }) + @IsOptional() transactionId?: string; @ApiPropertyOptional({ type: String, - required: false, + description: 'Date filter for records after this timestamp', }) @IsOptional() after?: string; @ApiPropertyOptional({ type: String, - required: false, + description: 'Date filter for records before this timestamp', }) @IsOptional() before?: 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 1adad68d087..e8a2c1e5d9e 100644 --- a/apps/api/src/app/notifications/dtos/activities-response.dto.ts +++ b/apps/api/src/app/notifications/dtos/activities-response.dto.ts @@ -4,177 +4,214 @@ import { ExecutionDetailsStatusEnum, MessageTemplateDto, ProvidersIdEnum, + ProvidersIdEnumConst, StepTypeEnum, TriggerTypeEnum, } from '@novu/shared'; import { StepFilter } from '@novu/dal'; export class ActivityNotificationStepResponseDto { - @ApiProperty() + @ApiProperty({ description: 'Unique identifier of the step' }) _id: string; - @ApiProperty() + @ApiProperty({ description: 'Whether the step is active or not' }) active: boolean; - @ApiProperty() + @ApiProperty({ description: 'Filter criteria for the step' }) filters: StepFilter; - @ApiPropertyOptional() + @ApiPropertyOptional({ description: 'Optional template for the step' }) template?: MessageTemplateDto; } export class ActivityNotificationExecutionDetailResponseDto { - @ApiProperty() + @ApiProperty({ description: 'Unique identifier of the execution detail' }) _id: string; - @ApiProperty() + @ApiProperty({ description: 'Unique identifier of the job' }) _jobId: string; @ApiProperty({ enum: ExecutionDetailsStatusEnum, + description: 'Status of the execution detail', }) status: ExecutionDetailsStatusEnum; - @ApiProperty() + @ApiProperty({ description: 'Detailed information about the execution' }) detail: string; - @ApiProperty() + @ApiProperty({ description: 'Whether the execution is a retry or not' }) isRetry: boolean; - @ApiProperty() + @ApiProperty({ description: 'Whether the execution is a test or not' }) isTest: boolean; - @ApiProperty() + @ApiProperty({ + enum: [...Object.values(ProvidersIdEnumConst)], + enumName: 'ProvidersIdEnum', + description: 'Provider ID of the execution', + }) providerId: ProvidersIdEnum; - @ApiPropertyOptional() + @ApiPropertyOptional({ description: 'Raw data of the execution' }) raw?: string; @ApiProperty({ enum: ExecutionDetailsSourceEnum, + description: 'Source of the execution detail', }) source: ExecutionDetailsSourceEnum; } export class ActivityNotificationJobResponseDto { - @ApiProperty() + @ApiProperty({ description: 'Unique identifier of the job' }) _id: string; - @ApiProperty() + @ApiProperty({ description: 'Type of the job' }) type: string; - @ApiPropertyOptional() + @ApiPropertyOptional({ description: 'Optional digest for the job' }) digest?: Record<string, unknown>; - @ApiProperty() + @ApiProperty({ + description: 'Execution details of the job', + type: [ActivityNotificationExecutionDetailResponseDto], + }) executionDetails: ActivityNotificationExecutionDetailResponseDto[]; - @ApiProperty() + @ApiProperty({ + description: 'Step details of the job', + type: ActivityNotificationStepResponseDto, + }) step: ActivityNotificationStepResponseDto; - @ApiPropertyOptional() + @ApiPropertyOptional({ description: 'Optional payload for the job' }) payload?: Record<string, unknown>; - @ApiProperty() + @ApiProperty({ + enum: [...Object.values(ProvidersIdEnumConst)], + enumName: 'ProvidersIdEnum', + description: 'Provider ID of the job', + }) providerId: ProvidersIdEnum; - @ApiProperty() + @ApiProperty({ description: 'Status of the job' }) status: string; } export class ActivityNotificationSubscriberResponseDto { - @ApiPropertyOptional() + @ApiPropertyOptional({ description: 'First name of the subscriber' }) firstName?: string; - @ApiProperty() + @ApiProperty({ description: 'Unique identifier of the subscriber' }) _id: string; - @ApiPropertyOptional() + @ApiPropertyOptional({ description: 'Last name of the subscriber' }) lastName?: string; - @ApiPropertyOptional() + @ApiPropertyOptional({ description: 'Email of the subscriber' }) email?: string; - @ApiPropertyOptional() + @ApiPropertyOptional({ description: 'Phone number of the subscriber' }) phone?: string; } export class NotificationTriggerVariable { + @ApiProperty({ description: 'Name of the variable' }) name: string; } export class NotificationTrigger { @ApiProperty({ enum: TriggerTypeEnum, + description: 'Type of the trigger', }) type: TriggerTypeEnum; - @ApiProperty() + @ApiProperty({ description: 'Identifier of the trigger' }) identifier: string; @ApiProperty({ + description: 'Variables of the trigger', type: [NotificationTriggerVariable], }) variables: NotificationTriggerVariable[]; - @ApiProperty({ + @ApiPropertyOptional({ + description: 'Subscriber variables of the trigger', type: [NotificationTriggerVariable], }) subscriberVariables?: NotificationTriggerVariable[]; } class ActivityNotificationTemplateResponseDto { - @ApiPropertyOptional() + @ApiPropertyOptional({ description: 'Unique identifier of the template' }) _id?: string; - @ApiProperty() + @ApiProperty({ description: 'Name of the template' }) name: string; - @ApiProperty() + @ApiProperty({ + description: 'Triggers of the template', + type: [NotificationTrigger], + }) triggers: NotificationTrigger[]; } export class ActivityNotificationResponseDto { - @ApiPropertyOptional() + @ApiPropertyOptional({ description: 'Unique identifier of the notification' }) _id?: string; - @ApiProperty() + @ApiProperty({ description: 'Environment ID of the notification' }) _environmentId: string; - @ApiProperty() + @ApiProperty({ description: 'Organization ID of the notification' }) _organizationId: string; - @ApiProperty() + @ApiProperty({ description: 'Transaction ID of the notification' }) transactionId: string; - @ApiPropertyOptional() + @ApiPropertyOptional({ description: 'Creation time of the notification' }) createdAt?: string; @ApiPropertyOptional({ + description: 'Channels of the notification', enum: StepTypeEnum, }) channels?: StepTypeEnum[]; - @ApiPropertyOptional() + @ApiPropertyOptional({ + description: 'Subscriber of the notification', + type: ActivityNotificationSubscriberResponseDto, + }) subscriber?: ActivityNotificationSubscriberResponseDto; - @ApiPropertyOptional() + @ApiPropertyOptional({ + description: 'Template of the notification', + type: ActivityNotificationTemplateResponseDto, + }) template?: ActivityNotificationTemplateResponseDto; - @ApiPropertyOptional() + @ApiPropertyOptional({ + description: 'Jobs of the notification', + type: [ActivityNotificationJobResponseDto], + }) jobs?: ActivityNotificationJobResponseDto[]; } export class ActivitiesResponseDto { - @ApiProperty() + @ApiProperty({ description: 'Whether there are more activities' }) hasMore: boolean; - @ApiProperty({ type: [ActivityNotificationResponseDto], description: 'Array of Activity notifications' }) + @ApiProperty({ + description: 'Array of activity notifications', + type: [ActivityNotificationResponseDto], + }) data: ActivityNotificationResponseDto[]; - @ApiProperty() + @ApiProperty({ description: 'Page size of the activities' }) pageSize: number; - @ApiProperty() + @ApiProperty({ description: 'Current page of the activities' }) page: number; } diff --git a/apps/api/src/app/notifications/e2e/get-activity-feed.e2e.ts b/apps/api/src/app/notifications/e2e/get-activity-feed.e2e.ts index aaf4fdb23ea..e87dd4fea48 100644 --- a/apps/api/src/app/notifications/e2e/get-activity-feed.e2e.ts +++ b/apps/api/src/app/notifications/e2e/get-activity-feed.e2e.ts @@ -1,20 +1,25 @@ -import { NotificationEntity, NotificationTemplateEntity, SubscriberRepository } from '@novu/dal'; +import { NotificationTemplateEntity, SubscriberRepository } from '@novu/dal'; import { UserSession } from '@novu/testing'; import { expect } from 'chai'; -import { ChannelTypeEnum, StepTypeEnum, IMessage } from '@novu/shared'; +import { IMessage, StepTypeEnum } from '@novu/shared'; +import { Novu } from '@novu/api'; +import { ActivityNotificationResponseDto, ChannelTypeEnum } from '@novu/api/models/components'; +import { initNovuClassSdk } from '../../shared/helpers/e2e/sdk/e2e-sdk.helper'; describe('Get activity feed - /notifications (GET)', async () => { let session: UserSession; let template: NotificationTemplateEntity; let smsOnlyTemplate: NotificationTemplateEntity; let subscriberId: string; - + let novuClient: Novu; beforeEach(async () => { session = new UserSession(); await session.initialize(); template = await session.createTemplate(); smsOnlyTemplate = await session.createChannelTemplate(StepTypeEnum.SMS); subscriberId = SubscriberRepository.createObjectId(); + novuClient = initNovuClassSdk(session); + await session.testAgent .post('/v1/widgets/session/initialize') .send({ @@ -28,159 +33,204 @@ describe('Get activity feed - /notifications (GET)', async () => { }); it('should get the current activity feed of user', async function () { - await session.triggerEvent(template.triggers[0].identifier, subscriberId, { - firstName: 'Test', + await novuClient.trigger({ + name: template.triggers[0].identifier, + to: subscriberId, + payload: { firstName: 'Test' }, }); - await session.triggerEvent(template.triggers[0].identifier, subscriberId, { - firstName: 'Test', + await novuClient.trigger({ + name: template.triggers[0].identifier, + to: subscriberId, + payload: { firstName: 'Test' }, }); await session.awaitRunningJobs(template._id); - const { body } = await session.testAgent.get('/v1/notifications?page=0'); - - const activities = body.data; - - expect(body.hasMore).to.equal(false); - expect(activities.length).to.equal(2); - expect(activities[0].template.name).to.equal(template.name); - expect(activities[0].template._id).to.equal(template._id); - expect(activities[0].subscriber.firstName).to.equal('Test'); - expect(activities[0].channels).to.include.oneOf(Object.keys(ChannelTypeEnum).map((i) => ChannelTypeEnum[i])); + const body = await novuClient.notifications.list({ page: 0 }); + const activities = body.result; + + expect(activities.hasMore).to.equal(false); + expect(activities.data.length).to.equal(2); + const activity = activities.data[0]; + if (!activity || !activity.template || !activity.subscriber) { + throw new Error('must have activity'); + } + expect(activity.template.name).to.equal(template.name); + expect(activity.template.id).to.equal(template._id); + expect(activity.subscriber.firstName).to.equal('Test'); + expect(activity.channels).to.include.oneOf(Object.keys(ChannelTypeEnum).map((i) => ChannelTypeEnum[i])); }); it('should filter by channel', async function () { - await session.triggerEvent(template.triggers[0].identifier, subscriberId, { - firstName: 'Test', + await novuClient.trigger({ + name: template.triggers[0].identifier, + to: subscriberId, + payload: { firstName: 'Test' }, }); - await session.triggerEvent(smsOnlyTemplate.triggers[0].identifier, subscriberId, { - firstName: 'Test', + await novuClient.trigger({ + name: smsOnlyTemplate.triggers[0].identifier, + to: subscriberId, + payload: { + firstName: 'Test', + }, }); - await session.triggerEvent(smsOnlyTemplate.triggers[0].identifier, subscriberId, { - firstName: 'Test', + await novuClient.trigger({ + name: smsOnlyTemplate.triggers[0].identifier, + to: subscriberId, + payload: { + firstName: 'Test', + }, }); await session.awaitRunningJobs([template._id, smsOnlyTemplate._id]); + novuClient.notifications.list({ page: 0, transactionId: ChannelTypeEnum.Sms }); - const { body } = await session.testAgent.get(`/v1/notifications?page=0&channels=${ChannelTypeEnum.SMS}`); - const activities: NotificationEntity[] = body.data; - - expect(activities.length).to.equal(2); + const body = await novuClient.notifications.list({ page: 0, channels: [ChannelTypeEnum.Sms] }); + const activities = body.result; - const activity = activities[0]; + expect(activities.hasMore).to.equal(false); + expect(activities.data.length).to.equal(2); + const activity = activities.data[0]; + if (!activity || !activity.template || !activity.subscriber) { + throw new Error('must have activity'); + } expect(activity.template?.name).to.equal(smsOnlyTemplate.name); - expect(activity.channels).to.include(ChannelTypeEnum.SMS); + expect(activity.channels).to.include(ChannelTypeEnum.Sms); }); it('should filter by templateId', async function () { - await session.triggerEvent(smsOnlyTemplate.triggers[0].identifier, subscriberId, { + await novuClient.trigger({ + name: smsOnlyTemplate.triggers[0].identifier, + to: subscriberId, payload: { firstName: 'Test', }, }); - await session.triggerEvent(template.triggers[0].identifier, subscriberId, { - firstName: 'Test', + await novuClient.trigger({ + name: template.triggers[0].identifier, + to: subscriberId, + payload: { firstName: 'Test' }, }); - await session.triggerEvent(template.triggers[0].identifier, subscriberId, { - firstName: 'Test', + await novuClient.trigger({ + name: template.triggers[0].identifier, + to: subscriberId, + payload: { firstName: 'Test' }, }); - await session.awaitRunningJobs(template._id); - const { body } = await session.testAgent.get(`/v1/notifications?page=0&templates=${template._id}`); - const activities: IMessage[] = body.data; + const body = await novuClient.notifications.list({ page: 0, templates: [template._id] }); + const activities = body.result; - expect(activities.length).to.equal(2); - expect(activities[0]._templateId).to.equal(template._id); - expect(activities[1]._templateId).to.equal(template._id); + expect(activities.hasMore).to.equal(false); + expect(activities.data.length).to.equal(2); + + expect(getActivity(activities.data, 0).template?.id).to.equal(template._id); + expect(getActivity(activities.data, 1).template?.id).to.equal(template._id); }); + function getActivity( + activities: Array<ActivityNotificationResponseDto>, + index: number + ): ActivityNotificationResponseDto { + const activity = activities[index]; + if (!activity || !activity.template || !activity.subscriber) { + throw new Error('must have activity'); + } + + return activity; + } it('should filter by email', async function () { - await session.triggerEvent( - template.triggers[0].identifier, - { + await novuClient.trigger({ + name: template.triggers[0].identifier, + to: { subscriberId: SubscriberRepository.createObjectId(), email: 'test@email.coms', }, - { + payload: { firstName: 'Test', - } - ); - - await session.triggerEvent(template.triggers[0].identifier, SubscriberRepository.createObjectId(), { - firstName: 'Test', + }, }); - - await session.triggerEvent( - template.triggers[0].identifier, - { + await novuClient.trigger({ + name: template.triggers[0].identifier, + to: { subscriberId: SubscriberRepository.createObjectId(), + email: 'test@email.coms', }, - { + payload: { firstName: 'Test', - } - ); + }, + }); - await session.triggerEvent( - template.triggers[0].identifier, - { - subscriberId, + await novuClient.trigger({ + name: template.triggers[0].identifier, + to: SubscriberRepository.createObjectId(), + payload: { + firstName: 'Test', }, - { + }); + + await novuClient.trigger({ + name: template.triggers[0].identifier, + to: SubscriberRepository.createObjectId(), + payload: { firstName: 'Test', - } - ); + }, + }); - await session.awaitRunningJobs(template._id); + await novuClient.trigger({ + name: template.triggers[0].identifier, + to: subscriberId, + payload: { + firstName: 'Test', + }, + }); - const { body } = await session.testAgent.get(`/v1/notifications?page=0&emails=test@email.coms`); - const activities: IMessage[] = body.data; + await session.awaitRunningJobs(template._id); + const activities = (await novuClient.notifications.list({ page: 0, emails: ['test@email.coms'] })).result.data; expect(activities.length).to.equal(1); - expect(activities[0]._templateId).to.equal(template._id); + expect(getActivity(activities, 0).template?.id).to.equal(template._id); }); it('should filter by subscriberId', async function () { const subscriberIdToCreate = `${SubscriberRepository.createObjectId()}some-test`; - await session.triggerEvent( - template.triggers[0].identifier, - { + await novuClient.trigger({ + name: template.triggers[0].identifier, + to: { subscriberId: subscriberIdToCreate, email: 'test@email.coms', }, - { + payload: { firstName: 'Test', - } - ); - - await session.triggerEvent(template.triggers[0].identifier, SubscriberRepository.createObjectId(), { - firstName: 'Test', - }); - - await session.triggerEvent( - template.triggers[0].identifier, - { - subscriberId: SubscriberRepository.createObjectId(), }, - { + }); + await novuClient.trigger({ + name: template.triggers[0].identifier, + to: SubscriberRepository.createObjectId(), + payload: { firstName: 'Test', - } - ); + }, + }); - await session.triggerEvent( - template.triggers[0].identifier, - { - subscriberId, + await novuClient.trigger({ + name: template.triggers[0].identifier, + to: SubscriberRepository.createObjectId(), + payload: { + firstName: 'Test', }, - { + }); + await novuClient.trigger({ + name: template.triggers[0].identifier, + to: subscriberId, + payload: { firstName: 'Test', - } - ); + }, + }); await session.awaitRunningJobs(template._id); diff --git a/apps/api/src/app/notifications/e2e/get-activity-graph-states.e2e.ts b/apps/api/src/app/notifications/e2e/get-activity-graph-states.e2e.ts index eb5c12d3216..36b8fe154ac 100644 --- a/apps/api/src/app/notifications/e2e/get-activity-graph-states.e2e.ts +++ b/apps/api/src/app/notifications/e2e/get-activity-graph-states.e2e.ts @@ -3,17 +3,20 @@ import { format } from 'date-fns'; import { UserSession } from '@novu/testing'; import { NotificationTemplateEntity, SubscriberRepository } from '@novu/dal'; import { ChannelTypeEnum } from '@novu/shared'; +import { Novu } from '@novu/api'; +import { initNovuClassSdk } from '../../shared/helpers/e2e/sdk/e2e-sdk.helper'; describe('Get activity feed graph stats - /notifications/graph/stats (GET)', async () => { let session: UserSession; let template: NotificationTemplateEntity; let subscriberId: string; - + let novuClient: Novu; beforeEach(async () => { session = new UserSession(); await session.initialize(); template = await session.createTemplate(); subscriberId = SubscriberRepository.createObjectId(); + novuClient = initNovuClassSdk(session); await session.testAgent .post('/v1/widgets/session/initialize') .send({ @@ -27,29 +30,33 @@ describe('Get activity feed graph stats - /notifications/graph/stats (GET)', asy }); it('should return the empty stats if there were no triggers', async function () { - const { body } = await session.testAgent.get('/v1/notifications/graph/stats'); + const body = await novuClient.notifications.stats.graph(); - const stats = body.data; + const stats = body.result; expect(stats.length).to.equal(0); }); it('should get the current activity feed graph stats', async function () { - await session.triggerEvent(template.triggers[0].identifier, subscriberId, { - firstName: 'Test', + await novuClient.trigger({ + name: template.triggers[0].identifier, + to: subscriberId, + payload: { firstName: 'Test' }, }); - await session.triggerEvent(template.triggers[0].identifier, subscriberId, { - firstName: 'Test', + await novuClient.trigger({ + name: template.triggers[0].identifier, + to: subscriberId, + payload: { firstName: 'Test' }, }); await session.awaitRunningJobs(template._id); - const { body } = await session.testAgent.get('/v1/notifications/graph/stats'); + const body = await novuClient.notifications.stats.graph(); - const stats = body.data; + const stats = body.result; expect(stats.length).to.equal(1); - expect(stats[0]._id).to.equal(format(new Date(), 'yyyy-MM-dd')); + expect(stats[0].id).to.equal(format(new Date(), 'yyyy-MM-dd')); expect(stats[0].count).to.equal(4); expect(stats[0].channels).to.include.oneOf(Object.keys(ChannelTypeEnum).map((i) => ChannelTypeEnum[i])); expect(stats[0].templates).to.include(template._id); diff --git a/apps/api/src/app/notifications/e2e/get-activity-stats.e2e.ts b/apps/api/src/app/notifications/e2e/get-activity-stats.e2e.ts index f0ec0e491b2..1ae3ab310ec 100644 --- a/apps/api/src/app/notifications/e2e/get-activity-stats.e2e.ts +++ b/apps/api/src/app/notifications/e2e/get-activity-stats.e2e.ts @@ -2,15 +2,17 @@ import { JobRepository, MessageRepository, NotificationEntity, - NotificationTemplateEntity, NotificationRepository, + NotificationTemplateEntity, SubscriberRepository, } from '@novu/dal'; import { StepTypeEnum } from '@novu/shared'; import { UserSession } from '@novu/testing'; import { expect } from 'chai'; -import { formatISO, subDays, subWeeks, subMonths } from 'date-fns'; +import { formatISO, subDays, subMonths } from 'date-fns'; import { v4 as uuid } from 'uuid'; +import { Novu } from '@novu/api'; +import { initNovuClassSdk } from '../../shared/helpers/e2e/sdk/e2e-sdk.helper'; describe('Get activity stats - /notifications/stats (GET)', async () => { let session: UserSession; @@ -19,13 +21,13 @@ describe('Get activity stats - /notifications/stats (GET)', async () => { const jobRepository = new JobRepository(); const notificationRepository = new NotificationRepository(); let subscriberId: string; - + let novuClient: Novu; beforeEach(async () => { session = new UserSession(); await session.initialize(); template = await session.createTemplate(); subscriberId = SubscriberRepository.createObjectId(); - + novuClient = initNovuClassSdk(session); await session.testAgent .post('/v1/widgets/session/initialize') .send({ @@ -48,12 +50,16 @@ describe('Get activity stats - /notifications/stats (GET)', async () => { }); it('should retrieve last month and last week activity', async function () { - await session.triggerEvent(template.triggers[0].identifier, subscriberId, { - firstName: 'Test', + await novuClient.trigger({ + name: template.triggers[0].identifier, + to: subscriberId, + payload: { firstName: 'Test' }, }); - await session.triggerEvent(template.triggers[0].identifier, subscriberId, { - firstName: 'Test', + await novuClient.trigger({ + name: template.triggers[0].identifier, + to: subscriberId, + payload: { firstName: 'Test' }, }); await session.awaitRunningJobs(template._id); diff --git a/apps/api/src/app/notifications/usecases/get-activity-feed/get-activity-feed.usecase.ts b/apps/api/src/app/notifications/usecases/get-activity-feed/get-activity-feed.usecase.ts index e0cbe7f059b..4b75e5a4b8f 100644 --- a/apps/api/src/app/notifications/usecases/get-activity-feed/get-activity-feed.usecase.ts +++ b/apps/api/src/app/notifications/usecases/get-activity-feed/get-activity-feed.usecase.ts @@ -1,9 +1,16 @@ import { Injectable } from '@nestjs/common'; -import { SubscriberRepository, NotificationRepository } from '@novu/dal'; +import { NotificationEntity, NotificationRepository, SubscriberEntity, SubscriberRepository } from '@novu/dal'; import { Instrument } from '@novu/application-generic'; -import { ActivitiesResponseDto } from '../../dtos/activities-response.dto'; +import { ActivitiesResponseDto, ActivityNotificationResponseDto } from '../../dtos/activities-response.dto'; import { GetActivityFeedCommand } from './get-activity-feed.command'; +const LIMIT = 10; +const EMPTY_RESPONSE = { + page: 0, + hasMore: false, + pageSize: LIMIT, + data: [], +}; @Injectable() export class GetActivityFeed { constructor( @@ -12,63 +19,102 @@ export class GetActivityFeed { ) {} async execute(command: GetActivityFeedCommand): Promise<ActivitiesResponseDto> { - const LIMIT = 10; - - let subscriberIds: string[] = []; - - if (command.search || command.emails?.length || command.subscriberIds?.length) { - const foundSubscribers = await this.findSubscribers(command); - - subscriberIds = foundSubscribers.map((subscriber) => subscriber._id); + if (!(command.search || command.emails?.length || command.subscriberIds?.length)) { + return EMPTY_RESPONSE; + } + const foundSubscribers: SubscriberEntity[] = await this.findSubscribers(command); - if (subscriberIds.length === 0) { - return { - page: 0, - hasMore: false, - pageSize: LIMIT, - data: [], - }; - } + if (foundSubscribers.length === 0) { + return EMPTY_RESPONSE; } - const { notifications } = await this.getFeedNotifications(command, subscriberIds, LIMIT); + const notifications: NotificationEntity[] = await this.getFeedNotifications(command, foundSubscribers, LIMIT); + const subscriberIdsToSubscriberMap = this.getSubscriberIdToSubscriberMap(foundSubscribers); return { page: command.page, hasMore: notifications?.length === LIMIT, pageSize: LIMIT, - data: notifications, + data: notifications.map((notification) => + this.mapNotificationEntityToActivityNotificationResponseDto(notification, subscriberIdsToSubscriberMap) + ), }; } + private getSubscriberIdToSubscriberMap(foundSubscribers: SubscriberEntity[]): { + [p: string]: SubscriberEntity; + } { + return foundSubscribers.reduce<{ [key: string]: SubscriberEntity }>((acc, item) => { + acc[item.subscriberId] = item; + + return acc; + }, {}); + } + private mapNotificationEntityToActivityNotificationResponseDto( + notificationEntity: NotificationEntity, + subscriberIdsToSubscriberMap: { [p: string]: SubscriberEntity } + ): ActivityNotificationResponseDto { + const activityNotificationResponseDto: ActivityNotificationResponseDto = { + _id: notificationEntity._id, + _environmentId: notificationEntity._environmentId.toString(), + _organizationId: notificationEntity._organizationId.toString(), + transactionId: notificationEntity.transactionId, + createdAt: notificationEntity.createdAt, + channels: notificationEntity.channels, + }; + + if (notificationEntity.template) { + activityNotificationResponseDto.template = { + _id: notificationEntity.template._id, + name: notificationEntity.template.name, + triggers: notificationEntity.template.triggers, + }; + } + + const subscriberEntity = subscriberIdsToSubscriberMap[notificationEntity._subscriberId]; + if (notificationEntity.to && subscriberEntity) { + activityNotificationResponseDto.subscriber = { + _id: subscriberEntity._id, + email: subscriberEntity.email, + firstName: subscriberEntity.firstName, + lastName: subscriberEntity.lastName, + phone: subscriberEntity.phone, + }; + } + + return activityNotificationResponseDto; + } @Instrument() private async findSubscribers(command: GetActivityFeedCommand) { - const foundSubscribers = await this.subscribersRepository.searchSubscribers( + return await this.subscribersRepository.searchSubscribers( command.environmentId, command.subscriberIds, command.emails, command.search ); - - return foundSubscribers; } @Instrument() - private async getFeedNotifications(command: GetActivityFeedCommand, subscriberIds: string[], LIMIT: number) { + private async getFeedNotifications( + command: GetActivityFeedCommand, + subscriberEntities: SubscriberEntity[], + limit: number + ): Promise<NotificationEntity[]> { + const subscriberDatabaseIds = subscriberEntities.map((subscriber) => subscriber._id); const { data: notifications } = await this.notificationRepository.getFeed( command.environmentId, { channels: command.channels, templates: command.templates, - subscriberIds, + subscriberIds: subscriberDatabaseIds, transactionId: command.transactionId, after: command.after, before: command.before, }, - command.page * LIMIT, - LIMIT + command.page * limit, + limit ); - return { notifications }; + return notifications; } } diff --git a/apps/api/src/app/shared/framework/swagger/sdk.decorators.ts b/apps/api/src/app/shared/framework/swagger/sdk.decorators.ts index 026e6fc7971..f95589f2d60 100644 --- a/apps/api/src/app/shared/framework/swagger/sdk.decorators.ts +++ b/apps/api/src/app/shared/framework/swagger/sdk.decorators.ts @@ -1,5 +1,6 @@ import { applyDecorators } from '@nestjs/common'; -import { ApiExtension } from '@nestjs/swagger'; +import { ApiExtension, ApiParam } from '@nestjs/swagger'; +import { ApiParamOptions } from '@nestjs/swagger/dist/decorators/api-param.decorator'; /** * Sets the method name for the SDK. @@ -53,6 +54,29 @@ export function SdkMethodMaxParamsOverride(maxParamsBeforeCollapseToObject?: num return applyDecorators(ApiExtension('x-speakeasy-max-method-params', maxParamsBeforeCollapseToObject)); } +class SDKOverrideOptions { + // 'x-speakeasy-name-override': 'workflowId', + nameOverride?: string; +} + +function overloadOptions(options: ApiParamOptions, sdkOverrideOptions: SDKOverrideOptions) { + let finalOptions = options; + if (sdkOverrideOptions.nameOverride) { + finalOptions = { + ...finalOptions, + 'x-speakeasy-name-override': sdkOverrideOptions.nameOverride, + } as unknown as ApiParamOptions; + } + + return finalOptions as ApiParamOptions; +} + +export function SdkApiParam(options: ApiParamOptions, sdkOverrideOptions?: SDKOverrideOptions) { + const finalOptions = sdkOverrideOptions ? overloadOptions(options, sdkOverrideOptions) : options; + + return applyDecorators(ApiParam(finalOptions)); +} + /** * Sets the pagination for the SDK. * @param {string} override - The override for the limit parameter. 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 index b94fa8082a6..7738999c151 100644 --- 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 @@ -4,9 +4,9 @@ import { UserSession } from '@novu/testing'; import { expect } from 'chai'; import { ErrorDto, ValidationErrorDto } from '@novu/api/models/errors'; -export function initNovuClassSdk(session: UserSession): Novu { +export function initNovuClassSdk(session: UserSession, overrideApiKey?: string): Novu { // return new Novu({ apiKey: session.apiKey, serverURL: session.serverUrl, debugLogger: console }); if needed debugging - return new Novu({ apiKey: session.apiKey, serverURL: session.serverUrl }); + return new Novu({ apiKey: overrideApiKey || session.apiKey, serverURL: session.serverUrl }); } export function initNovuFunctionSdk(session: UserSession): NovuCore { return new NovuCore({ apiKey: session.apiKey, serverURL: session.serverUrl, debugLogger: console }); diff --git a/apps/api/src/app/subscribers/e2e/get-notifications-feed.e2e.ts b/apps/api/src/app/subscribers/e2e/get-notifications-feed.e2e.ts index 40d0e35c3d0..f041737e2c1 100644 --- a/apps/api/src/app/subscribers/e2e/get-notifications-feed.e2e.ts +++ b/apps/api/src/app/subscribers/e2e/get-notifications-feed.e2e.ts @@ -1,16 +1,18 @@ import { UserSession } from '@novu/testing'; import { expect } from 'chai'; -import axios from 'axios'; import { NotificationTemplateEntity, SubscriberRepository } from '@novu/dal'; +import { Novu } from '@novu/api'; +import { expectSdkExceptionGeneric, initNovuClassSdk } from '../../shared/helpers/e2e/sdk/e2e-sdk.helper'; describe('Get Notifications feed - /:subscriberId/notifications/feed (GET)', function () { let session: UserSession; let template: NotificationTemplateEntity; let subscriberId: string; - + let novuClient: Novu; beforeEach(async () => { session = new UserSession(); await session.initialize(); + novuClient = initNovuClassSdk(session); template = await session.createTemplate({ noFeedId: false, @@ -20,52 +22,57 @@ describe('Get Notifications feed - /:subscriberId/notifications/feed (GET)', fun }); it('should throw exception on invalid subscriber id', async function () { - await session.triggerEvent(template.triggers[0].identifier, subscriberId); + await novuClient.trigger({ name: template.triggers[0].identifier, to: subscriberId }); await session.awaitRunningJobs(template._id); - const notificationsFeedResponse = await getNotificationsFeed(subscriberId, session.apiKey, { limit: 5 }); + const notificationsFeedResponse = (await novuClient.subscribers.notifications.feed({ limit: 5, subscriberId })) + .result; expect(notificationsFeedResponse.pageSize).to.equal(5); - - try { - await getNotificationsFeed(`${subscriberId}111`, session.apiKey, { seen: false, limit: 5 }); - } catch (err) { - expect(err.response.status).to.equals(400); - expect(err.response.data.message).to.contain( - `Subscriber not found for this environment with the id: ${`${subscriberId}111`}. Make sure to create a subscriber before fetching the feed.` - ); - } + const { error } = await expectSdkExceptionGeneric(() => + novuClient.subscribers.notifications.feed({ + subscriberId, + seen: false, + limit: 5, + }) + ); + expect(error?.statusCode).to.equals(400); + expect(error?.message).to.contain( + `Subscriber not found for this environment with the id: ${`${subscriberId}111`}. + Make sure to create a subscriber before fetching the feed.` + ); }); it('should throw exception when invalid payload query param is passed', async function () { - await session.triggerEvent(template.triggers[0].identifier, subscriberId); + await novuClient.trigger({ name: template.triggers[0].identifier, to: subscriberId }); await session.awaitRunningJobs(template._id); - try { - await getNotificationsFeed(subscriberId, session.apiKey, { limit: 5, payload: 'invalid' }); - } catch (err) { - expect(err.response.status).to.equals(400); - expect(err.response.data.message).to.eq(`Invalid payload, the JSON object should be encoded to base64 string.`); - - return; - } - - expect.fail('Should have thrown an bad request exception'); + const { error: err } = await expectSdkExceptionGeneric(() => + novuClient.subscribers.notifications.feed({ + limit: 5, + payload: 'invalid', + subscriberId, + }) + ); + expect(err?.statusCode).to.equals(400); + expect(err?.message).to.eq(`Invalid payload, the JSON object should be encoded to base64 string.`); }); it('should allow filtering by custom data from the payload', async function () { const partialPayload = { foo: 123 }; const payload = { ...partialPayload, bar: 'bar' }; - await session.triggerEvent(template.triggers[0].identifier, subscriberId); + await novuClient.trigger({ name: template.triggers[0].identifier, to: subscriberId }); await session.awaitRunningJobs(template._id); - await session.triggerEvent(template.triggers[0].identifier, subscriberId, payload); + await novuClient.trigger({ name: template.triggers[0].identifier, to: subscriberId, payload }); await session.awaitRunningJobs(template._id); const payloadQueryValue = Buffer.from(JSON.stringify(partialPayload)).toString('base64'); - const { data } = await getNotificationsFeed(subscriberId, session.apiKey, { limit: 5, payload: payloadQueryValue }); + const { data } = ( + await novuClient.subscribers.notifications.feed({ limit: 5, payload: payloadQueryValue, subscriberId }) + ).result; expect(data.length).to.equal(1); expect(data[0].payload).to.deep.equal(payload); @@ -75,32 +82,22 @@ describe('Get Notifications feed - /:subscriberId/notifications/feed (GET)', fun const partialPayload = { foo: { bar: 123 } }; const payload = { ...partialPayload, baz: 'baz' }; - await session.triggerEvent(template.triggers[0].identifier, subscriberId); + await novuClient.trigger({ name: template.triggers[0].identifier, to: subscriberId }); await session.awaitRunningJobs(template._id); - await session.triggerEvent(template.triggers[0].identifier, subscriberId, payload); + await novuClient.trigger({ name: template.triggers[0].identifier, to: subscriberId, payload }); await session.awaitRunningJobs(template._id); const payloadQueryValue = Buffer.from(JSON.stringify(partialPayload)).toString('base64'); - const { data } = await getNotificationsFeed(subscriberId, session.apiKey, { limit: 5, payload: payloadQueryValue }); + const { data } = ( + await novuClient.subscribers.notifications.feed({ + limit: 5, + payload: payloadQueryValue, + subscriberId, + }) + ).result; expect(data.length).to.equal(1); expect(data[0].payload).to.deep.equal(payload); }); }); - -async function getNotificationsFeed(subscriberId: string, apiKey: string, query = {}) { - const response = await axios.get( - `http://127.0.0.1:${process.env.PORT}/v1/subscribers/${subscriberId}/notifications/feed`, - { - params: { - ...query, - }, - headers: { - authorization: `ApiKey ${apiKey}`, - }, - } - ); - - return response.data; -} diff --git a/apps/api/src/app/subscribers/e2e/get-preferences.e2e.ts b/apps/api/src/app/subscribers/e2e/get-preferences.e2e.ts index cc44c1e6536..50f2b7f8037 100644 --- a/apps/api/src/app/subscribers/e2e/get-preferences.e2e.ts +++ b/apps/api/src/app/subscribers/e2e/get-preferences.e2e.ts @@ -3,7 +3,7 @@ import { expect } from 'chai'; import { NotificationTemplateEntity } from '@novu/dal'; import { PreferenceLevelEnum } from '@novu/shared'; -import { getPreference, getPreferenceByLevel } from './helpers'; +import { initNovuClassSdk } from '../../shared/helpers/e2e/sdk/e2e-sdk.helper'; describe('Get Subscribers workflow preferences - /subscribers/:subscriberId/preferences (GET)', function () { let session: UserSession; @@ -18,7 +18,10 @@ describe('Get Subscribers workflow preferences - /subscribers/:subscriberId/pref }); it('should get subscriber workflow preferences with inactive channels by default', async function () { - const response = await getPreference(session, session.subscriberId); + const response = await initNovuClassSdk(session).subscribers.preferences.list( + session.subscriberId, + {}.includeInactiveChannels + ); const data = response.data.data[0]; expect(data.preference.channels).to.deep.equal({ @@ -31,9 +34,12 @@ describe('Get Subscribers workflow preferences - /subscribers/:subscriberId/pref }); it('should get subscriber workflow preferences with inactive channels when includeInactiveChannels is true', async function () { - const response = await getPreference(session, session.subscriberId, { - includeInactiveChannels: true, - }); + const response = await initNovuClassSdk(session).subscribers.preferences.list( + session.subscriberId, + { + includeInactiveChannels: true, + }.includeInactiveChannels + ); const data = response.data.data[0]; expect(data.preference.channels).to.deep.equal({ @@ -46,9 +52,12 @@ describe('Get Subscribers workflow preferences - /subscribers/:subscriberId/pref }); it('should get subscriber workflow preferences with active channels when includeInactiveChannels is false', async function () { - const response = await getPreference(session, session.subscriberId, { - includeInactiveChannels: false, - }); + const response = await initNovuClassSdk(session).subscribers.preferences.list( + session.subscriberId, + { + includeInactiveChannels: false, + }.includeInactiveChannels + ); const data = response.data.data[0]; expect(data.preference.channels).to.deep.equal({ @@ -60,7 +69,10 @@ describe('Get Subscribers workflow preferences - /subscribers/:subscriberId/pref it('should handle un existing subscriberId', async function () { let error; try { - await getPreference(session, 'unexisting-subscriber-id'); + await initNovuClassSdk(session).subscribers.preferences.list( + 'unexisting-subscriber-id', + {}.includeInactiveChannels + ); } catch (e) { error = e; } @@ -86,7 +98,11 @@ describe('Get Subscribers preferences by level - /subscribers/:subscriberId/pref levels.forEach((level) => { it(`should get subscriber ${level} preferences with inactive channels by default`, async function () { - const response = await getPreferenceByLevel(session, session.subscriberId, level); + const response = await initNovuClassSdk(session).subscribers.preferences.retrieveByLevel({ + preferenceLevel: level, + subscriberId: session.subscriberId, + includeInactiveChannels: {}.includeInactiveChannels, + }); const data = response.data.data[0]; expect(data.preference.channels).to.deep.equal({ @@ -99,8 +115,12 @@ describe('Get Subscribers preferences by level - /subscribers/:subscriberId/pref }); it(`should get subscriber ${level} preferences with inactive channels when includeInactiveChannels is true`, async function () { - const response = await getPreferenceByLevel(session, session.subscriberId, level, { - includeInactiveChannels: true, + const response = await initNovuClassSdk(session).subscribers.preferences.retrieveByLevel({ + preferenceLevel: level, + subscriberId: session.subscriberId, + includeInactiveChannels: { + includeInactiveChannels: true, + }.includeInactiveChannels, }); const data = response.data.data[0]; @@ -114,8 +134,12 @@ describe('Get Subscribers preferences by level - /subscribers/:subscriberId/pref }); it(`should get subscriber ${level} preferences with active channels when includeInactiveChannels is false`, async function () { - const response = await getPreferenceByLevel(session, session.subscriberId, level, { - includeInactiveChannels: false, + const response = await initNovuClassSdk(session).subscribers.preferences.retrieveByLevel({ + preferenceLevel: level, + subscriberId: session.subscriberId, + includeInactiveChannels: { + includeInactiveChannels: false, + }.includeInactiveChannels, }); const data = response.data.data[0]; diff --git a/apps/api/src/app/subscribers/e2e/get-unseen-count.e2e.ts b/apps/api/src/app/subscribers/e2e/get-unseen-count.e2e.ts index d6882d5c772..83d21723a58 100644 --- a/apps/api/src/app/subscribers/e2e/get-unseen-count.e2e.ts +++ b/apps/api/src/app/subscribers/e2e/get-unseen-count.e2e.ts @@ -1,16 +1,19 @@ import { UserSession } from '@novu/testing'; import { expect } from 'chai'; -import axios from 'axios'; import { NotificationTemplateEntity, SubscriberRepository } from '@novu/dal'; +import { Novu } from '@novu/api'; +import { SubscribersControllerGetUnseenCountRequest } from '@novu/api/models/operations'; +import { expectSdkExceptionGeneric, initNovuClassSdk } from '../../shared/helpers/e2e/sdk/e2e-sdk.helper'; describe('Get Unseen Count - /:subscriberId/notifications/unseen (GET)', function () { let session: UserSession; let template: NotificationTemplateEntity; let subscriberId: string; - + let novuClient: Novu; beforeEach(async () => { session = new UserSession(); await session.initialize(); + novuClient = initNovuClassSdk(session); template = await session.createTemplate({ noFeedId: true, @@ -20,34 +23,20 @@ describe('Get Unseen Count - /:subscriberId/notifications/unseen (GET)', functio }); it('should throw exception on invalid subscriber id', async function () { - await session.triggerEvent(template.triggers[0].identifier, subscriberId); + await novuClient.trigger({ name: template.triggers[0].identifier, to: subscriberId }); await session.awaitRunningJobs(template._id); - const seenCount = (await getUnSeenCount(subscriberId, session.apiKey, { seen: false })).data.count; + const seenCount = await getUnSeenCount({ limit: 0, seen: false, subscriberId }); expect(seenCount).to.equal(1); - try { - await getUnSeenCount(`${subscriberId}111`, session.apiKey, { seen: false }); - } catch (err) { - expect(err.response.status).to.equals(400); - expect(err.response.data.message).to.contain(`Subscriber ${`${subscriberId}111`} is not exist in environment`); - } + const { error } = await expectSdkExceptionGeneric(() => getUnSeenCount({ seen: false, subscriberId })); + expect(error?.statusCode).to.equals(400); + expect(error?.message).to.contain(`Subscriber ${`${subscriberId}111`} is not exist in environment`); }); -}); + async function getUnSeenCount(query: SubscribersControllerGetUnseenCountRequest) { + const response = await novuClient.subscribers.notifications.unseenCount(query); -async function getUnSeenCount(subscriberId: string, apiKey: string, query = {}) { - const response = await axios.get( - `http://127.0.0.1:${process.env.PORT}/v1/subscribers/${subscriberId}/notifications/unseen`, - { - params: { - ...query, - }, - headers: { - authorization: `ApiKey ${apiKey}`, - }, - } - ); - - return response.data; -} + return response.result.count; + } +}); diff --git a/apps/api/src/app/subscribers/e2e/helpers/index.ts b/apps/api/src/app/subscribers/e2e/helpers/index.ts index 7d402ad4b3d..1d156b84730 100644 --- a/apps/api/src/app/subscribers/e2e/helpers/index.ts +++ b/apps/api/src/app/subscribers/e2e/helpers/index.ts @@ -1,11 +1,7 @@ import { UserSession } from '@novu/testing'; -import { IUpdateNotificationTemplateDto, PreferenceLevelEnum } from '@novu/shared'; +import { IUpdateNotificationTemplateDto } from '@novu/shared'; import axios from 'axios'; -import { UpdateSubscriberOnlineFlagRequestDto } from '../../dtos/update-subscriber-online-flag-request.dto'; -import { UpdateSubscriberPreferenceRequestDto } from '../../../widgets/dtos/update-subscriber-preference-request.dto'; -import { UpdateSubscriberGlobalPreferencesRequestDto } from '../../dtos/update-subscriber-global-preferences-request.dto'; - const axiosInstance = axios.create(); export async function getNotificationTemplate(session: UserSession, id: string) { @@ -27,82 +23,3 @@ export async function updateNotificationTemplate( }, }); } - -export async function getPreference( - session: UserSession, - subscriberId = session.subscriberId, - queryParams: { includeInactiveChannels?: boolean } = {} -) { - const url = new URL(`${session.serverUrl}/v1/subscribers/${subscriberId}/preferences`); - Object.entries(queryParams).forEach(([key, value]) => { - url.searchParams.append(key, String(value)); - }); - - return await axiosInstance.get(url.toString(), { - headers: { - authorization: `ApiKey ${session.apiKey}`, - }, - }); -} - -export async function getPreferenceByLevel( - session: UserSession, - subscriberId = session.subscriberId, - level: PreferenceLevelEnum, - queryParams: { includeInactiveChannels?: boolean } = {} -) { - const url = new URL(`${session.serverUrl}/v1/subscribers/${subscriberId}/preferences/${level}`); - Object.entries(queryParams).forEach(([key, value]) => { - url.searchParams.append(key, String(value)); - }); - - return await axiosInstance.get(url.toString(), { - headers: { - authorization: `ApiKey ${session.apiKey}`, - }, - }); -} - -export async function updateSubscriberOnlineFlag( - data: UpdateSubscriberOnlineFlagRequestDto, - session: UserSession, - subscriberId: string -) { - return await axiosInstance.patch(`${session.serverUrl}/v1/subscribers/${subscriberId}/online-status`, data, { - headers: { - authorization: `ApiKey ${session.apiKey}`, - }, - }); -} - -export async function updatePreference( - data: UpdateSubscriberPreferenceRequestDto, - session: UserSession, - templateId: string -) { - return await axiosInstance.patch( - `${session.serverUrl}/v1/subscribers/${session.subscriberId}/preferences/${templateId}`, - data, - { - headers: { - authorization: `ApiKey ${session.apiKey}`, - }, - } - ); -} - -export async function updatePreferences(data: UpdateSubscriberPreferenceRequestDto, session: UserSession) { - return await axiosInstance.patch(`${session.serverUrl}/v1/subscribers/${session.subscriberId}/preferences`, data, { - headers: { - authorization: `ApiKey ${session.apiKey}`, - }, - }); -} - -export async function updateGlobalPreferences(data: UpdateSubscriberGlobalPreferencesRequestDto, session: UserSession) { - return await axiosInstance.patch(`${session.serverUrl}/v1/subscribers/${session.subscriberId}/preferences`, data, { - headers: { - authorization: `ApiKey ${session.apiKey}`, - }, - }); -} diff --git a/apps/api/src/app/subscribers/e2e/mark-all-subscriber-messages.e2e.ts b/apps/api/src/app/subscribers/e2e/mark-all-subscriber-messages.e2e.ts index 79021c90cc6..3a555cf6f08 100644 --- a/apps/api/src/app/subscribers/e2e/mark-all-subscriber-messages.e2e.ts +++ b/apps/api/src/app/subscribers/e2e/mark-all-subscriber-messages.e2e.ts @@ -15,11 +15,11 @@ describe('Mark All Subscriber Messages - /subscribers/:subscriberId/messages/mar session = new UserSession(); await session.initialize(); template = await session.createTemplate(); + novuClient = initNovuClassSdk(session); await messageRepository.deleteMany({ _environmentId: session.environment._id, _subscriberId: session.subscriberId, }); - novuClient = initNovuClassSdk(session); }); it("should throw not found when subscriberId doesn't exist", async function () { @@ -39,11 +39,11 @@ describe('Mark All Subscriber Messages - /subscribers/:subscriberId/messages/mar it('should mark all the subscriber messages as read', async function () { const { subscriberId } = session; - await session.triggerEvent(template.triggers[0].identifier, subscriberId); - await session.triggerEvent(template.triggers[0].identifier, subscriberId); - await session.triggerEvent(template.triggers[0].identifier, subscriberId); - await session.triggerEvent(template.triggers[0].identifier, subscriberId); - await session.triggerEvent(template.triggers[0].identifier, subscriberId); + await novuClient.trigger({ name: template.triggers[0].identifier, to: subscriberId }); + await novuClient.trigger({ name: template.triggers[0].identifier, to: subscriberId }); + await novuClient.trigger({ name: template.triggers[0].identifier, to: subscriberId }); + await novuClient.trigger({ name: template.triggers[0].identifier, to: subscriberId }); + await novuClient.trigger({ name: template.triggers[0].identifier, to: subscriberId }); await session.awaitRunningJobs(template._id); @@ -71,11 +71,11 @@ describe('Mark All Subscriber Messages - /subscribers/:subscriberId/messages/mar it('should not mark all the messages as read if they are already read', async function () { const { subscriberId } = session; - await session.triggerEvent(template.triggers[0].identifier, subscriberId); - await session.triggerEvent(template.triggers[0].identifier, subscriberId); - await session.triggerEvent(template.triggers[0].identifier, subscriberId); - await session.triggerEvent(template.triggers[0].identifier, subscriberId); - await session.triggerEvent(template.triggers[0].identifier, subscriberId); + await novuClient.trigger({ name: template.triggers[0].identifier, to: subscriberId }); + await novuClient.trigger({ name: template.triggers[0].identifier, to: subscriberId }); + await novuClient.trigger({ name: template.triggers[0].identifier, to: subscriberId }); + await novuClient.trigger({ name: template.triggers[0].identifier, to: subscriberId }); + await novuClient.trigger({ name: template.triggers[0].identifier, to: subscriberId }); await session.awaitRunningJobs(template._id); @@ -114,11 +114,11 @@ describe('Mark All Subscriber Messages - /subscribers/:subscriberId/messages/mar it('should mark all the subscriber messages as unread', async function () { const { subscriberId } = session; - await session.triggerEvent(template.triggers[0].identifier, subscriberId); - await session.triggerEvent(template.triggers[0].identifier, subscriberId); - await session.triggerEvent(template.triggers[0].identifier, subscriberId); - await session.triggerEvent(template.triggers[0].identifier, subscriberId); - await session.triggerEvent(template.triggers[0].identifier, subscriberId); + await novuClient.trigger({ name: template.triggers[0].identifier, to: subscriberId }); + await novuClient.trigger({ name: template.triggers[0].identifier, to: subscriberId }); + await novuClient.trigger({ name: template.triggers[0].identifier, to: subscriberId }); + await novuClient.trigger({ name: template.triggers[0].identifier, to: subscriberId }); + await novuClient.trigger({ name: template.triggers[0].identifier, to: subscriberId }); await session.awaitRunningJobs(template._id); @@ -157,11 +157,11 @@ describe('Mark All Subscriber Messages - /subscribers/:subscriberId/messages/mar it('should mark all the subscriber messages as seen', async function () { const { subscriberId } = session; - await session.triggerEvent(template.triggers[0].identifier, subscriberId); - await session.triggerEvent(template.triggers[0].identifier, subscriberId); - await session.triggerEvent(template.triggers[0].identifier, subscriberId); - await session.triggerEvent(template.triggers[0].identifier, subscriberId); - await session.triggerEvent(template.triggers[0].identifier, subscriberId); + await novuClient.trigger({ name: template.triggers[0].identifier, to: subscriberId }); + await novuClient.trigger({ name: template.triggers[0].identifier, to: subscriberId }); + await novuClient.trigger({ name: template.triggers[0].identifier, to: subscriberId }); + await novuClient.trigger({ name: template.triggers[0].identifier, to: subscriberId }); + await novuClient.trigger({ name: template.triggers[0].identifier, to: subscriberId }); await session.awaitRunningJobs(template._id); @@ -189,11 +189,11 @@ describe('Mark All Subscriber Messages - /subscribers/:subscriberId/messages/mar it('should mark all the subscriber messages as unseen', async function () { const { subscriberId } = session; - await session.triggerEvent(template.triggers[0].identifier, subscriberId); - await session.triggerEvent(template.triggers[0].identifier, subscriberId); - await session.triggerEvent(template.triggers[0].identifier, subscriberId); - await session.triggerEvent(template.triggers[0].identifier, subscriberId); - await session.triggerEvent(template.triggers[0].identifier, subscriberId); + await novuClient.trigger({ name: template.triggers[0].identifier, to: subscriberId }); + await novuClient.trigger({ name: template.triggers[0].identifier, to: subscriberId }); + await novuClient.trigger({ name: template.triggers[0].identifier, to: subscriberId }); + await novuClient.trigger({ name: template.triggers[0].identifier, to: subscriberId }); + await novuClient.trigger({ name: template.triggers[0].identifier, to: subscriberId }); await session.awaitRunningJobs(template._id); diff --git a/apps/api/src/app/subscribers/e2e/mark-as-by-mark.e2e.ts b/apps/api/src/app/subscribers/e2e/mark-as-by-mark.e2e.ts index 8c71edc1352..e9dc38e4049 100644 --- a/apps/api/src/app/subscribers/e2e/mark-as-by-mark.e2e.ts +++ b/apps/api/src/app/subscribers/e2e/mark-as-by-mark.e2e.ts @@ -10,6 +10,8 @@ import { SubscriberRepository, } from '@novu/dal'; import { ChannelTypeEnum, MessagesStatusEnum } from '@novu/shared'; +import { Novu } from '@novu/api'; +import { initNovuClassSdk } from '../../shared/helpers/e2e/sdk/e2e-sdk.helper'; const axiosInstance = axios.create(); @@ -21,17 +23,18 @@ describe('Mark as Seen - /widgets/messages/mark-as (POST)', async () => { let subscriberId; let subscriber: SubscriberEntity; let message: MessageEntity; - + let novuClient: Novu; before(async () => { session = new UserSession(); await session.initialize(); subscriberId = SubscriberRepository.createObjectId(); template = await session.createTemplate(); + novuClient = initNovuClassSdk(session); }); beforeEach(async () => { - await session.triggerEvent(template.triggers[0].identifier, subscriberId); + await novuClient.trigger({ name: template.triggers[0].identifier, to: subscriberId }); await session.awaitRunningJobs(template._id); subscriber = await getSubscriber(session, subscriberRepository, subscriberId); diff --git a/apps/api/src/app/subscribers/e2e/update-global-preference.e2e.ts b/apps/api/src/app/subscribers/e2e/update-global-preference.e2e.ts index a41789803b0..42ee514496b 100644 --- a/apps/api/src/app/subscribers/e2e/update-global-preference.e2e.ts +++ b/apps/api/src/app/subscribers/e2e/update-global-preference.e2e.ts @@ -1,15 +1,19 @@ import { ChannelTypeEnum } from '@novu/shared'; import { UserSession } from '@novu/testing'; import { expect } from 'chai'; - -import { updateGlobalPreferences } from './helpers'; +import { Novu } from '@novu/api'; +import { UpdateSubscriberGlobalPreferencesRequestDto } from '@novu/api/models/components'; +import axios from 'axios'; +import { initNovuClassSdk } from '../../shared/helpers/e2e/sdk/e2e-sdk.helper'; describe('Update Subscribers global preferences - /subscribers/:subscriberId/preferences (PATCH)', function () { let session: UserSession; + let novuClient: Novu; beforeEach(async () => { session = new UserSession(); await session.initialize(); + novuClient = initNovuClassSdk(session); }); it('should validate the payload', async function () { @@ -52,13 +56,13 @@ describe('Update Subscribers global preferences - /subscribers/:subscriberId/pre preferences: [{ type: ChannelTypeEnum.EMAIL, enabled: true }], }; - const response = await updateGlobalPreferences(payload, session); + const response = await novuClient.subscribers.preferences.updateGlobal(payload, session.subscriberId); - expect(response.data.data.preference.enabled).to.eql(true); - expect(response.data.data.preference.channels).to.not.eql({ + expect(response.result.preference.enabled).to.eql(true); + expect(response.result.preference.channels).to.not.eql({ [ChannelTypeEnum.IN_APP]: true, }); - expect(response.data.data.preference.channels).to.eql({ + expect(response.result.preference.channels).to.eql({ [ChannelTypeEnum.EMAIL]: true, [ChannelTypeEnum.PUSH]: true, [ChannelTypeEnum.CHAT]: true, @@ -77,10 +81,10 @@ describe('Update Subscribers global preferences - /subscribers/:subscriberId/pre ], }; - const response = await updateGlobalPreferences(payload, session); + const response = await novuClient.subscribers.preferences.updateGlobal(payload, session.subscriberId); - expect(response.data.data.preference.enabled).to.eql(true); - expect(response.data.data.preference.channels).to.eql({ + expect(response.result.preference.enabled).to.eql(true); + expect(response.result.preference.channels).to.eql({ [ChannelTypeEnum.EMAIL]: true, [ChannelTypeEnum.PUSH]: false, [ChannelTypeEnum.CHAT]: true, @@ -88,23 +92,15 @@ describe('Update Subscribers global preferences - /subscribers/:subscriberId/pre [ChannelTypeEnum.IN_APP]: false, }); }); +}); - // `enabled` flag is not used anymore. The presence of a preference object means that the subscriber has enabled notifications. - it.skip('should update user global preference and disable the flag for the future channels update', async function () { - const disablePreferenceData = { - enabled: false, - }; - - const response = await updateGlobalPreferences(disablePreferenceData, session); - - expect(response.data.data.preference.enabled).to.eql(false); - - const preferenceChannel = { - preferences: [{ type: ChannelTypeEnum.EMAIL, enabled: true }], - }; - - const res = await updateGlobalPreferences(preferenceChannel, session); +// This is kept in order to validate the server controller behavior as the SDK will not allow problematic payloads in compilation +export async function updateGlobalPreferences(data: UpdateSubscriberGlobalPreferencesRequestDto, session: UserSession) { + const axiosInstance = axios.create(); - expect(res.data.data.preference.channels).to.eql({}); + return await axiosInstance.patch(`${session.serverUrl}/v1/subscribers/${session.subscriberId}/preferences`, data, { + headers: { + authorization: `ApiKey ${session.apiKey}`, + }, }); -}); +} diff --git a/apps/api/src/app/subscribers/e2e/update-online-flag.e2e.ts b/apps/api/src/app/subscribers/e2e/update-online-flag.e2e.ts index 6be55fef4fc..8124b1fc2f0 100644 --- a/apps/api/src/app/subscribers/e2e/update-online-flag.e2e.ts +++ b/apps/api/src/app/subscribers/e2e/update-online-flag.e2e.ts @@ -2,8 +2,7 @@ import { SubscribersService, UserSession } from '@novu/testing'; import { expect } from 'chai'; import { sub } from 'date-fns'; import { SubscriberEntity } from '@novu/dal'; - -import { updateSubscriberOnlineFlag } from './helpers'; +import { initNovuClassSdk } from '../../shared/helpers/e2e/sdk/e2e-sdk.helper'; describe('Update Subscriber online flag - /subscribers/:subscriberId/online-status (PATCH)', function () { let session: UserSession; @@ -30,10 +29,13 @@ describe('Update Subscriber online flag - /subscribers/:subscriberId/online-stat isOnline: false, }; - const { data } = await updateSubscriberOnlineFlag(body, session, onlineSubscriber.subscriberId); + const { result: data } = await initNovuClassSdk(session).subscribers.properties.updateOnlineFlag( + body, + onlineSubscriber.subscriberId + ); - expect(data.data.isOnline).to.equal(false); - expect(data.data.lastOnlineAt).to.be.a('string'); + expect(data.isOnline).to.equal(false); + expect(data.lastOnlineAt).to.be.a('string'); }); it('should set the online status to true', async function () { @@ -41,8 +43,11 @@ describe('Update Subscriber online flag - /subscribers/:subscriberId/online-stat isOnline: true, }; - const { data } = await updateSubscriberOnlineFlag(body, session, offlineSubscriber.subscriberId); + const { result: data } = await initNovuClassSdk(session).subscribers.properties.updateOnlineFlag( + body, + offlineSubscriber.subscriberId + ); - expect(data.data.isOnline).to.equal(true); + expect(data.isOnline).to.equal(true); }); }); diff --git a/apps/api/src/app/subscribers/e2e/update-preference.e2e.ts b/apps/api/src/app/subscribers/e2e/update-preference.e2e.ts index 9975522a21b..8d6662648a0 100644 --- a/apps/api/src/app/subscribers/e2e/update-preference.e2e.ts +++ b/apps/api/src/app/subscribers/e2e/update-preference.e2e.ts @@ -9,13 +9,8 @@ import { StepTypeEnum, } from '@novu/shared'; -import { - getNotificationTemplate, - updateNotificationTemplate, - getPreference, - updatePreference, - updatePreferences, -} from './helpers'; +import { getNotificationTemplate, updateNotificationTemplate } from './helpers'; +import { initNovuClassSdk } from '../../shared/helpers/e2e/sdk/e2e-sdk.helper'; describe('Update Subscribers preferences - /subscribers/:subscriberId/preferences/:templateId (PATCH)', function () { let session: UserSession; @@ -36,7 +31,11 @@ describe('Update Subscribers preferences - /subscribers/:subscriberId/preference }; try { - const response = await updatePreference(updateDataEmailFalse as any, session, template._id); + const response = await initNovuClassSdk(session).subscribers.preferences.update({ + workflowId: template._id, + subscriberId: session.subscriberId, + updateSubscriberPreferenceRequestDto: updateDataEmailFalse as any, + }); expect(response).to.not.be.ok; } catch (error) { expect(error.toJSON()).to.have.include({ @@ -56,7 +55,11 @@ describe('Update Subscribers preferences - /subscribers/:subscriberId/preference }; try { - const response = await updatePreference(updateDataEmailFalse as any, session, template._id); + const response = await initNovuClassSdk(session).subscribers.preferences.update({ + workflowId: template._id, + subscriberId: session.subscriberId, + updateSubscriberPreferenceRequestDto: updateDataEmailFalse as any, + }); expect(response).to.not.be.ok; } catch (error) { expect(error.toJSON()).to.have.include({ @@ -76,7 +79,11 @@ describe('Update Subscribers preferences - /subscribers/:subscriberId/preference }; try { - const response = await updatePreference(updateDataEmailFalse as any, session, '63cc6e0b561e0a609f223e27'); + const response = await initNovuClassSdk(session).subscribers.preferences.update({ + workflowId: '63cc6e0b561e0a609f223e27', + subscriberId: session.subscriberId, + updateSubscriberPreferenceRequestDto: updateDataEmailFalse as any, + }); expect(response).to.not.be.ok; } catch (error) { const { response } = error; @@ -98,7 +105,11 @@ describe('Update Subscribers preferences - /subscribers/:subscriberId/preference }; try { - const response = await updatePreference(updatePreferenceDataEmailFalse as any, session, template._id); + const response = await initNovuClassSdk(session).subscribers.preferences.update({ + workflowId: template._id, + subscriberId: session.subscriberId, + updateSubscriberPreferenceRequestDto: updatePreferenceDataEmailFalse as any, + }); expect(response).to.not.be.ok; } catch (error) { const { response } = error; @@ -116,7 +127,10 @@ describe('Update Subscribers preferences - /subscribers/:subscriberId/preference }; try { - const response = await updatePreferences(updatePreferencesDataEmailFalse as any, session); + const response = await initNovuClassSdk(session).subscribers.preferences.updateGlobal( + updatePreferencesDataEmailFalse as any, + session.subscriberId + ); expect(response).to.not.be.ok; } catch (error) { const { response } = error; @@ -126,7 +140,9 @@ describe('Update Subscribers preferences - /subscribers/:subscriberId/preference }); it('should not do any action or error when sending an empty channels property', async function () { - const initialPreferences = (await getPreference(session)).data.data[0]; + const initialPreferences = ( + await initNovuClassSdk(session).subscribers.preferences.list(session.subscriberId, {}.includeInactiveChannels) + ).data.data[0]; expect(initialPreferences.preference.enabled).to.eql(true); expect(initialPreferences.preference.channels).to.eql({ [ChannelTypeEnum.EMAIL]: true, @@ -140,9 +156,15 @@ describe('Update Subscribers preferences - /subscribers/:subscriberId/preference channels: {}, }; - await updatePreference(emptyPreferenceData as any, session, template._id); + await initNovuClassSdk(session).subscribers.preferences.update({ + workflowId: template._id, + subscriberId: session.subscriberId, + updateSubscriberPreferenceRequestDto: emptyPreferenceData as any, + }); - const preferences = (await getPreference(session)).data.data[0]; + const preferences = ( + await initNovuClassSdk(session).subscribers.preferences.list(session.subscriberId, {}.includeInactiveChannels) + ).data.data[0]; expect(preferences.preference.enabled).to.eql(true); expect(preferences.preference.channels).to.eql({ @@ -156,7 +178,9 @@ describe('Update Subscribers preferences - /subscribers/:subscriberId/preference // `enabled` flag is not used anymore. The presence of a preference object means that the subscriber has enabled notifications. it.skip('should update user preference and disable the flag for the future general notification template preference', async function () { - const initialPreferences = (await getPreference(session)).data.data[0]; + const initialPreferences = ( + await initNovuClassSdk(session).subscribers.preferences.list(session.subscriberId, {}.includeInactiveChannels) + ).data.data[0]; expect(initialPreferences.preference.enabled).to.eql(true); expect(initialPreferences.preference.channels).to.eql({ [ChannelTypeEnum.EMAIL]: true, @@ -170,9 +194,15 @@ describe('Update Subscribers preferences - /subscribers/:subscriberId/preference enabled: false, }; - await updatePreference(disablePreferenceData, session, template._id); + await initNovuClassSdk(session).subscribers.preferences.update({ + workflowId: template._id, + subscriberId: session.subscriberId, + updateSubscriberPreferenceRequestDto: disablePreferenceData, + }); - const midwayPreferences = (await getPreference(session)).data.data[0]; + const midwayPreferences = ( + await initNovuClassSdk(session).subscribers.preferences.list(session.subscriberId, {}.includeInactiveChannels) + ).data.data[0]; expect(midwayPreferences.preference.enabled).to.eql(false); expect(midwayPreferences.preference.channels).to.eql({ [ChannelTypeEnum.EMAIL]: true, @@ -189,9 +219,15 @@ describe('Update Subscribers preferences - /subscribers/:subscriberId/preference }, }; - await updatePreference(updateEmailPreferenceData, session, template._id); + await initNovuClassSdk(session).subscribers.preferences.update({ + workflowId: template._id, + subscriberId: session.subscriberId, + updateSubscriberPreferenceRequestDto: updateEmailPreferenceData, + }); - const finalPreferences = (await getPreference(session)).data.data[0]; + const finalPreferences = ( + await initNovuClassSdk(session).subscribers.preferences.list(session.subscriberId, {}.includeInactiveChannels) + ).data.data[0]; expect(finalPreferences.preference.enabled).to.eql(false); expect(finalPreferences.preference.channels).to.eql({ [ChannelTypeEnum.EMAIL]: false, @@ -204,7 +240,9 @@ describe('Update Subscribers preferences - /subscribers/:subscriberId/preference // `enabled` flag is not used anymore. The presence of a preference object means that the subscriber has enabled notifications. it.skip('should update user preference and enable the flag for the future general notification template preference', async function () { - const initialPreferences = (await getPreference(session)).data.data[0]; + const initialPreferences = ( + await initNovuClassSdk(session).subscribers.preferences.list(session.subscriberId, {}.includeInactiveChannels) + ).data.data[0]; expect(initialPreferences.preference.enabled).to.eql(true); expect(initialPreferences.preference.channels).to.eql({ [ChannelTypeEnum.EMAIL]: true, @@ -218,9 +256,15 @@ describe('Update Subscribers preferences - /subscribers/:subscriberId/preference enabled: false, }; - await updatePreference(disablePreferenceData, session, template._id); + await initNovuClassSdk(session).subscribers.preferences.update({ + workflowId: template._id, + subscriberId: session.subscriberId, + updateSubscriberPreferenceRequestDto: disablePreferenceData, + }); - const midwayPreferences = (await getPreference(session)).data.data[0]; + const midwayPreferences = ( + await initNovuClassSdk(session).subscribers.preferences.list(session.subscriberId, {}.includeInactiveChannels) + ).data.data[0]; expect(midwayPreferences.preference.enabled).to.eql(false); expect(midwayPreferences.preference.channels).to.eql({ [ChannelTypeEnum.EMAIL]: true, @@ -234,9 +278,15 @@ describe('Update Subscribers preferences - /subscribers/:subscriberId/preference enabled: true, }; - await updatePreference(enablePreferenceData, session, template._id); + await initNovuClassSdk(session).subscribers.preferences.update({ + workflowId: template._id, + subscriberId: session.subscriberId, + updateSubscriberPreferenceRequestDto: enablePreferenceData, + }); - const finalPreferences = (await getPreference(session)).data.data[0]; + const finalPreferences = ( + await initNovuClassSdk(session).subscribers.preferences.list(session.subscriberId, {}.includeInactiveChannels) + ).data.data[0]; expect(finalPreferences.preference.enabled).to.eql(true); expect(finalPreferences.preference.channels).to.eql({ [ChannelTypeEnum.EMAIL]: true, @@ -248,7 +298,9 @@ describe('Update Subscribers preferences - /subscribers/:subscriberId/preference }); it('should be able to update the subscriber preference for an active channel of the template', async function () { - const initialPreferences = (await getPreference(session)).data.data[0]; + const initialPreferences = ( + await initNovuClassSdk(session).subscribers.preferences.list(session.subscriberId, {}.includeInactiveChannels) + ).data.data[0]; expect(initialPreferences.preference.enabled).to.eql(true); expect(initialPreferences.preference.channels).to.eql({ [ChannelTypeEnum.EMAIL]: true, @@ -265,9 +317,15 @@ describe('Update Subscribers preferences - /subscribers/:subscriberId/preference }, }; - await updatePreference(disableEmailPreferenceData, session, template._id); + await initNovuClassSdk(session).subscribers.preferences.update({ + workflowId: template._id, + subscriberId: session.subscriberId, + updateSubscriberPreferenceRequestDto: disableEmailPreferenceData, + }); - const updatedPreferences = (await getPreference(session)).data.data[0]; + const updatedPreferences = ( + await initNovuClassSdk(session).subscribers.preferences.list(session.subscriberId, {}.includeInactiveChannels) + ).data.data[0]; expect(updatedPreferences.preference.enabled).to.eql(true); expect(updatedPreferences.preference.channels).to.eql({ [ChannelTypeEnum.EMAIL]: false, @@ -284,9 +342,15 @@ describe('Update Subscribers preferences - /subscribers/:subscriberId/preference }, }; - await updatePreference(enableEmailPreferenceData, session, template._id); + await initNovuClassSdk(session).subscribers.preferences.update({ + workflowId: template._id, + subscriberId: session.subscriberId, + updateSubscriberPreferenceRequestDto: enableEmailPreferenceData, + }); - const finalPreferences = (await getPreference(session)).data.data[0]; + const finalPreferences = ( + await initNovuClassSdk(session).subscribers.preferences.list(session.subscriberId, {}.includeInactiveChannels) + ).data.data[0]; expect(finalPreferences.preference.enabled).to.eql(true); expect(finalPreferences.preference.channels).to.eql({ [ChannelTypeEnum.EMAIL]: true, @@ -298,7 +362,9 @@ describe('Update Subscribers preferences - /subscribers/:subscriberId/preference }); it('should ignore the channel update if channel not being used in the notification template', async function () { - const initialPreferences = (await getPreference(session)).data.data[0]; + const initialPreferences = ( + await initNovuClassSdk(session).subscribers.preferences.list(session.subscriberId, {}.includeInactiveChannels) + ).data.data[0]; expect(initialPreferences.preference.enabled).to.eql(true); expect(initialPreferences.preference.channels).to.eql({ [ChannelTypeEnum.EMAIL]: true, @@ -315,9 +381,15 @@ describe('Update Subscribers preferences - /subscribers/:subscriberId/preference }, }; - await updatePreference(updateSmsPreferenceData, session, template._id); + await initNovuClassSdk(session).subscribers.preferences.update({ + workflowId: template._id, + subscriberId: session.subscriberId, + updateSubscriberPreferenceRequestDto: updateSmsPreferenceData, + }); - const finalPreferences = (await getPreference(session)).data.data[0]; + const finalPreferences = ( + await initNovuClassSdk(session).subscribers.preferences.list(session.subscriberId, {}.includeInactiveChannels) + ).data.data[0]; expect(finalPreferences.preference.enabled).to.eql(true); expect(finalPreferences.preference.channels).to.eql({ [ChannelTypeEnum.EMAIL]: true, @@ -350,7 +422,9 @@ describe('Update Subscribers preferences - /subscribers/:subscriberId/preference const updatedNotificationTemplate = (await getNotificationTemplate(session, template._id)).data.data; expect(updatedNotificationTemplate.steps.length).to.eql(3); - const initialPreferences = (await getPreference(session)).data.data[0]; + const initialPreferences = ( + await initNovuClassSdk(session).subscribers.preferences.list(session.subscriberId, {}.includeInactiveChannels) + ).data.data[0]; expect(initialPreferences.preference.enabled).to.eql(true); expect(initialPreferences.preference.channels).to.eql({ [ChannelTypeEnum.EMAIL]: true, @@ -367,9 +441,15 @@ describe('Update Subscribers preferences - /subscribers/:subscriberId/preference }, }; - await updatePreference(updateSmsPreferenceData, session, template._id); + await initNovuClassSdk(session).subscribers.preferences.update({ + workflowId: template._id, + subscriberId: session.subscriberId, + updateSubscriberPreferenceRequestDto: updateSmsPreferenceData, + }); - const finalPreferences = (await getPreference(session)).data.data[0]; + const finalPreferences = ( + await initNovuClassSdk(session).subscribers.preferences.list(session.subscriberId, {}.includeInactiveChannels) + ).data.data[0]; expect(finalPreferences.preference.enabled).to.eql(true); expect(finalPreferences.preference.channels).to.eql({ [ChannelTypeEnum.EMAIL]: true, @@ -408,7 +488,9 @@ describe('Update Subscribers preferences - /subscribers/:subscriberId/preference const updatedNotificationTemplate = (await getNotificationTemplate(session, template._id)).data.data; expect(updatedNotificationTemplate.steps.length).to.eql(3); - const initialPreferences = (await getPreference(session)).data.data[0]; + const initialPreferences = ( + await initNovuClassSdk(session).subscribers.preferences.list(session.subscriberId, {}.includeInactiveChannels) + ).data.data[0]; expect(initialPreferences.preference.enabled).to.eql(true); expect(initialPreferences.preference.channels).to.eql({ [ChannelTypeEnum.EMAIL]: true, @@ -425,9 +507,15 @@ describe('Update Subscribers preferences - /subscribers/:subscriberId/preference }, }; - await updatePreference(updateSmsPreferenceData, session, template._id); + await initNovuClassSdk(session).subscribers.preferences.update({ + workflowId: template._id, + subscriberId: session.subscriberId, + updateSubscriberPreferenceRequestDto: updateSmsPreferenceData, + }); - const finalPreferences = (await getPreference(session)).data.data[0]; + const finalPreferences = ( + await initNovuClassSdk(session).subscribers.preferences.list(session.subscriberId, {}.includeInactiveChannels) + ).data.data[0]; expect(finalPreferences.preference.enabled).to.eql(true); expect(finalPreferences.preference.channels).to.eql({ [ChannelTypeEnum.EMAIL]: false, diff --git a/apps/api/src/app/subscribers/e2e/update-subscriber.e2e.ts b/apps/api/src/app/subscribers/e2e/update-subscriber.e2e.ts index 2571fc26336..8a2c45afe2f 100644 --- a/apps/api/src/app/subscribers/e2e/update-subscriber.e2e.ts +++ b/apps/api/src/app/subscribers/e2e/update-subscriber.e2e.ts @@ -1,37 +1,29 @@ import { UserSession } from '@novu/testing'; import { SubscriberRepository } from '@novu/dal'; import { expect } from 'chai'; -import axios from 'axios'; - -const axiosInstance = axios.create(); +import { Novu } from '@novu/api'; +import { initNovuClassSdk } from '../../shared/helpers/e2e/sdk/e2e-sdk.helper'; +const subscriberId = '123'; describe('Update Subscriber - /subscribers/:subscriberId (PUT)', function () { let session: UserSession; const subscriberRepository = new SubscriberRepository(); - + let novuClient: Novu; beforeEach(async () => { session = new UserSession(); await session.initialize(); + novuClient = initNovuClassSdk(session); }); it('should update an existing subscriber', async function () { - await axiosInstance.post( - `${session.serverUrl}/v1/subscribers`, - { - subscriberId: '123', - firstName: 'John', - lastName: 'Doe', - email: 'john@doe.com', - }, - { - headers: { - authorization: `ApiKey ${session.apiKey}`, - }, - } - ); - - const response = await axiosInstance.put( - `${session.serverUrl}/v1/subscribers/123`, + await novuClient.subscribers.create({ + subscriberId, + firstName: 'John', + lastName: 'Doe', + email: 'john@doe.com', + }); + + const response = await novuClient.subscribers.update( { lastName: 'Test Changed', email: 'changed@mail.com', @@ -39,17 +31,13 @@ describe('Update Subscriber - /subscribers/:subscriberId (PUT)', function () { locale: 'sv', data: { test: 'test value' }, }, - { - headers: { - authorization: `ApiKey ${session.apiKey}`, - }, - } + subscriberId ); - const { data: body } = response; + const { result: body } = response; - expect(body.data).to.be.ok; - const createdSubscriber = await subscriberRepository.findBySubscriberId(session.environment._id, '123'); + expect(body).to.be.ok; + const createdSubscriber = await subscriberRepository.findBySubscriberId(session.environment._id, subscriberId); expect(createdSubscriber?.firstName).to.equal('John'); expect(createdSubscriber?.lastName).to.equal('Test Changed'); @@ -60,41 +48,27 @@ describe('Update Subscriber - /subscribers/:subscriberId (PUT)', function () { }); it('should allow unsetting the email', async function () { - await axiosInstance.post( - `${session.serverUrl}/v1/subscribers`, - { - subscriberId: '123', - firstName: 'John', - lastName: 'Doe', - email: 'john@doe.com', - }, - { - headers: { - authorization: `ApiKey ${session.apiKey}`, - }, - } - ); - - const response = await axiosInstance.put( - `${session.serverUrl}/v1/subscribers/123`, + await novuClient.subscribers.create({ + subscriberId, + firstName: 'John', + lastName: 'Doe', + email: 'john@doe.com', + }); + + const response = await novuClient.subscribers.update( { lastName: 'Test Changed', - email: null, phone: '+972523333333', locale: 'sv', data: { test: 'test value' }, }, - { - headers: { - authorization: `ApiKey ${session.apiKey}`, - }, - } + subscriberId ); - const { data: body } = response; + const { result: body } = response; - expect(body.data).to.be.ok; - const createdSubscriber = await subscriberRepository.findBySubscriberId(session.environment._id, '123'); + expect(body).to.be.ok; + const createdSubscriber = await subscriberRepository.findBySubscriberId(session.environment._id, subscriberId); expect(createdSubscriber?.firstName).to.equal('John'); expect(createdSubscriber?.lastName).to.equal('Test Changed'); @@ -105,39 +79,24 @@ describe('Update Subscriber - /subscribers/:subscriberId (PUT)', function () { }); it('should update an existing subscriber credentials', async function () { - await axiosInstance.post( - `${session.serverUrl}/v1/subscribers`, - { - subscriberId: '123', - firstName: 'John', - lastName: 'Doe', - email: 'john@doe.com', - }, + await novuClient.subscribers.create({ + subscriberId, + firstName: 'John', + lastName: 'Doe', + email: 'john@doe.com', + }); + const response = await novuClient.subscribers.credentials.update( { - headers: { - authorization: `ApiKey ${session.apiKey}`, - }, - } - ); - - const response = await axiosInstance.put( - `${session.serverUrl}/v1/subscribers/123/credentials`, - { - subscriberId: '123', providerId: 'slack', credentials: { webhookUrl: 'webhookUrlNew' }, }, - { - headers: { - authorization: `ApiKey ${session.apiKey}`, - }, - } + subscriberId ); - const { data: body } = response; + const { result: body } = response; - expect(body.data).to.be.ok; - const createdSubscriber = await subscriberRepository.findBySubscriberId(session.environment._id, '123'); + expect(body).to.be.ok; + const createdSubscriber = await subscriberRepository.findBySubscriberId(session.environment._id, subscriberId); const subscriberChannel = createdSubscriber?.channels?.find((channel) => channel.providerId === 'slack'); diff --git a/apps/api/src/app/subscribers/query-objects/unseen-count.query.ts b/apps/api/src/app/subscribers/query-objects/unseen-count.query.ts new file mode 100644 index 00000000000..55da8f595dc --- /dev/null +++ b/apps/api/src/app/subscribers/query-objects/unseen-count.query.ts @@ -0,0 +1,26 @@ +import { ApiProperty } from '@nestjs/swagger'; + +export class UnseenCountQueryDto { + @ApiProperty({ + description: 'Identifier for the feed. Can be a single string or an array of strings.', + oneOf: [{ type: 'string' }, { type: 'array', items: { type: 'string' } }], + required: false, + }) + feedId?: string | string[]; + + @ApiProperty({ + description: 'Indicates whether to count seen notifications.', + required: false, + default: false, + type: Boolean, + }) + seen?: boolean; + + @ApiProperty({ + description: 'The maximum number of notifications to return.', + required: false, + default: 100, + type: Number, + }) + limit?: number; +} diff --git a/apps/api/src/app/subscribers/subscribers.controller.ts b/apps/api/src/app/subscribers/subscribers.controller.ts index 5aca32dc7bb..7f5dfb124f9 100644 --- a/apps/api/src/app/subscribers/subscribers.controller.ts +++ b/apps/api/src/app/subscribers/subscribers.controller.ts @@ -2,7 +2,6 @@ import { BadRequestException, Body, Controller, - DefaultValuePipe, Delete, Get, HttpCode, @@ -36,7 +35,6 @@ import { UserSessionData, } from '@novu/shared'; import { MessageEntity } from '@novu/dal'; - import { RemoveSubscriber, RemoveSubscriberCommand } from './usecases/remove-subscriber'; import { ExternalApiAccessible } from '../auth/framework/external-api.decorator'; import { UserSession } from '../shared/framework/user.decorator'; @@ -104,9 +102,10 @@ import { MarkMessageAsByMarkCommand } from '../widgets/usecases/mark-message-as- import { MarkMessageAsByMark } from '../widgets/usecases/mark-message-as-by-mark/mark-message-as-by-mark.usecase'; import { FeedResponseDto } from '../widgets/dtos/feeds-response.dto'; import { UserAuthentication } from '../shared/framework/swagger/api.key.security'; -import { SdkGroupName, SdkMethodName, SdkUsePagination } from '../shared/framework/swagger/sdk.decorators'; +import { SdkApiParam, SdkGroupName, SdkMethodName, SdkUsePagination } from '../shared/framework/swagger/sdk.decorators'; import { UpdatePreferences } from '../inbox/usecases/update-preferences/update-preferences.usecase'; import { UpdatePreferencesCommand } from '../inbox/usecases/update-preferences/update-preferences.command'; +import { UnseenCountQueryDto } from './query-objects/unseen-count.query'; @ThrottlerCategory(ApiRateLimitCategoryEnum.CONFIGURATION) @ApiCommonResponses() @@ -415,6 +414,7 @@ export class SubscribersController { 'A flag which specifies if the inactive workflow channels should be included in the retrieved preferences. Default is true', }) @SdkGroupName('Subscribers.Preferences') + @SdkMethodName('list') async listSubscriberPreferences( @UserSession() user: UserSessionData, @Param('subscriberId') subscriberId: string, @@ -439,13 +439,16 @@ export class SubscribersController { summary: 'Get subscriber preferences by level', }) @ApiParam({ name: 'subscriberId', type: String, required: true }) - @ApiParam({ - name: 'parameter', - type: String, - enum: PreferenceLevelEnum, - required: true, - description: 'the preferences level to be retrieved (template / global) ', - }) + @SdkApiParam( + { + name: 'parameter', + type: String, + enum: PreferenceLevelEnum, + required: true, + description: 'the preferences level to be retrieved (template / global) ', + }, + { nameOverride: 'preferenceLevel' } + ) @ApiQuery({ name: 'includeInactiveChannels', type: Boolean, @@ -471,12 +474,20 @@ export class SubscribersController { return await this.getPreferenceUsecase.execute(command); } + // @ts-ignore @Patch('/:subscriberId/preferences/:parameter') @ExternalApiAccessible() @UserAuthentication() @ApiResponse(UpdateSubscriberPreferenceResponseDto) @ApiParam({ name: 'subscriberId', type: String, required: true }) - @ApiParam({ name: 'parameter', type: String, required: true }) + @SdkApiParam( + { + name: 'parameter', + type: String, + required: true, + }, + { nameOverride: 'workflowId' } + ) @ApiOperation({ summary: 'Update subscriber preference', }) @@ -484,7 +495,7 @@ export class SubscribersController { async updateSubscriberPreference( @UserSession() user: UserSessionData, @Param('subscriberId') subscriberId: string, - @Param('parameter') templateId: string, + @Param('parameter') workflowId: string, @Body() body: UpdateSubscriberPreferenceRequestDto ): Promise<UpdateSubscriberPreferenceResponseDto> { const result = await this.updatePreferencesUsecase.execute( @@ -492,7 +503,7 @@ export class SubscribersController { environmentId: user.environmentId, organizationId: user.organizationId, subscriberId, - workflowId: templateId, + workflowId, level: PreferenceLevelEnum.TEMPLATE, includeInactiveChannels: true, ...(body.channel && { [body.channel.type]: body.channel.enabled }), @@ -604,20 +615,18 @@ export class SubscribersController { @SdkMethodName('unseenCount') async getUnseenCount( @UserSession() user: UserSessionData, - @Query('feedIdentifier') feedId: string[] | string, - @Query('seen') seen: boolean, @Param('subscriberId') subscriberId: string, - @Query('limit', new DefaultValuePipe(100)) limit: number + @Query() query: UnseenCountQueryDto ): Promise<UnseenCountResponse> { let feedsQuery: string[] | undefined; - if (feedId) { - feedsQuery = Array.isArray(feedId) ? feedId : [feedId]; + if (query.feedId) { + feedsQuery = Array.isArray(query.feedId) ? query.feedId : [query.feedId]; } - if (seen === undefined) { + if (query.seen === undefined) { // eslint-disable-next-line no-param-reassign - seen = false; + query.seen = false; } const command = GetFeedCountCommand.create({ @@ -625,8 +634,8 @@ export class SubscribersController { subscriberId, environmentId: user.environmentId, feedId: feedsQuery, - seen, - limit, + seen: query.seen, + limit: query.limit || 100, }); return await this.getFeedCountUsecase.execute(command); diff --git a/apps/api/src/app/topics/dtos/filter-topics.dto.ts b/apps/api/src/app/topics/dtos/filter-topics.dto.ts index c45291177d4..4b1fa1ca858 100644 --- a/apps/api/src/app/topics/dtos/filter-topics.dto.ts +++ b/apps/api/src/app/topics/dtos/filter-topics.dto.ts @@ -1,49 +1,70 @@ -import { ApiProperty, ApiPropertyOptional } from '@nestjs/swagger'; +import { ApiProperty } from '@nestjs/swagger'; import { IsInt, IsOptional, IsString, Min } from 'class-validator'; -import { Transform } from 'class-transformer'; import { TopicDto } from './topic.dto'; export class FilterTopicsRequestDto { - @Transform(({ value }) => Number(value)) + @ApiProperty({ + example: 0, + required: false, + type: 'integer', + format: 'int64', + description: 'The page number to retrieve (starts from 0)', + }) @IsOptional() @IsInt() @Min(0) - @ApiPropertyOptional({ type: Number }) public page?: number = 0; - @Transform(({ value }) => Number(value)) + @ApiProperty({ + example: 10, + required: false, + type: 'integer', + format: 'int64', + description: 'The number of items to return per page (default: 10)', + }) @IsOptional() @IsInt() @Min(0) - @ApiPropertyOptional({ type: Number }) public pageSize?: number = 10; + @ApiProperty({ + example: 'exampleKey', + required: false, + type: 'string', + description: 'A filter key to apply to the results', + }) @IsString() @IsOptional() - @ApiPropertyOptional({ type: String }) public key?: string; } export class FilterTopicsResponseDto { @ApiProperty({ - type: TopicDto, - isArray: true, + example: [], + type: [TopicDto], + description: 'The list of topics', }) data: TopicDto[]; @ApiProperty({ + example: 1, type: Number, + description: 'The current page number', }) page: number; @ApiProperty({ + example: 10, type: Number, + description: 'The number of items per page', }) pageSize: number; @ApiProperty({ + example: 10, type: Number, + description: 'The total number of items', }) totalCount: number; } diff --git a/apps/api/src/app/topics/e2e/add-subscribers.e2e.ts b/apps/api/src/app/topics/e2e/add-subscribers.e2e.ts index 303433f61ed..fe9028dcec5 100644 --- a/apps/api/src/app/topics/e2e/add-subscribers.e2e.ts +++ b/apps/api/src/app/topics/e2e/add-subscribers.e2e.ts @@ -39,8 +39,8 @@ describe('Add subscribers to topic - /topics/:topicKey/subscribers (POST)', asyn topicUrl = `${URL}/${topicKey}`; addSubscribersUrl = `${topicUrl}/subscribers`; }); - it('should throw validation error for missing request payload information', async () => { + // Not changing to use SDK since the SDK would block wrong usage const { body } = await session.testAgent.post(addSubscribersUrl).send({}); expect(body.statusCode).to.eql(400); diff --git a/apps/api/src/app/topics/e2e/delete-topic.e2e.ts b/apps/api/src/app/topics/e2e/delete-topic.e2e.ts index 2c8a7a1f544..d7ae5405c19 100644 --- a/apps/api/src/app/topics/e2e/delete-topic.e2e.ts +++ b/apps/api/src/app/topics/e2e/delete-topic.e2e.ts @@ -5,8 +5,6 @@ import { Novu } from '@novu/api'; import { expectSdkExceptionGeneric, initNovuClassSdk } from '../../shared/helpers/e2e/sdk/e2e-sdk.helper'; import { addSubscribers, createTopic, getTopic } from './helpers/topic-e2e-helper'; -const BASE_PATH = '/v1/topics'; - describe('Delete a topic - /topics/:topicKey (DELETE)', async () => { let session: UserSession; let novuClient: Novu; @@ -59,12 +57,11 @@ describe('Delete a topic - /topics/:topicKey (DELETE)', async () => { await addSubscribers(session, topicKey, [subscriber.subscriberId]); - const { body } = await session.testAgent.delete(`${BASE_PATH}/${topicKey}`); - - expect(body.statusCode).to.equal(409); - expect(body.message).to.eql( + const { error } = await expectSdkExceptionGeneric(() => novuClient.topics.delete(topicKey)); + expect(error?.statusCode).to.equal(409); + expect(error?.message).to.eql( `Topic with key ${topicKey} in the environment ${session.environment._id} can't be deleted as it still has subscribers assigned` ); - expect(body.error).to.eql('Conflict'); + expect(error?.ctx?.error, JSON.stringify(error)).to.eql('Conflict'); }); }); diff --git a/apps/api/src/app/topics/e2e/filter-topics.e2e.ts b/apps/api/src/app/topics/e2e/filter-topics.e2e.ts index ee59a9ae2ce..f4d870f838b 100644 --- a/apps/api/src/app/topics/e2e/filter-topics.e2e.ts +++ b/apps/api/src/app/topics/e2e/filter-topics.e2e.ts @@ -8,7 +8,6 @@ import { CreateTopicResponseDto } from '@novu/api/models/components'; import { TopicsControllerAssignResponse } from '@novu/api/models/operations'; import { initNovuClassSdk } from '../../shared/helpers/e2e/sdk/e2e-sdk.helper'; -const BASE_PATH = '/v1/topics'; describe('Filter topics - /topics (GET)', async () => { let firstSubscriber: SubscriberEntity; let secondSubscriber: SubscriberEntity; @@ -43,12 +42,10 @@ describe('Filter topics - /topics (GET)', async () => { }); it('should return a validation error if the params provided are not in the right type', async () => { - const url = `${BASE_PATH}?page=first&pageSize=big`; - const response = await session.testAgent.get(url); - + const response = await session.testAgent.get(`/v1/topics?page=first&pageSize=big`); expect(response.statusCode).to.eql(400); - expect(response.body.error).to.eql('Bad Request'); - expect(response.body.message).to.eql([ + expect(response.body.ctx.error, JSON.stringify(response.body)).to.eql('Bad Request'); + expect(response.body.message, JSON.stringify(response.body)).to.eql([ 'page must not be less than 0', 'page must be an integer number', 'pageSize must not be less than 0', @@ -57,7 +54,7 @@ describe('Filter topics - /topics (GET)', async () => { }); it('should return a validation error if the expected params provided are not integers', async () => { - const url = `${BASE_PATH}?page=1.5&pageSize=1.5`; + const url = `/v1/topics?page=1.5&pageSize=1.5`; const response = await session.testAgent.get(url); expect(response.statusCode).to.eql(400); @@ -66,7 +63,7 @@ describe('Filter topics - /topics (GET)', async () => { }); it('should return a validation error if the expected params provided are negative integers', async () => { - const url = `${BASE_PATH}?page=-1&pageSize=-1`; + const url = `/v1/topics?page=-1&pageSize=-1`; const response = await session.testAgent.get(url); expect(response.statusCode).to.eql(400); @@ -75,7 +72,7 @@ describe('Filter topics - /topics (GET)', async () => { }); it('should return a Bad Request error if the page size requested is bigger than the default one (10)', async () => { - const url = `${BASE_PATH}?page=1&pageSize=101`; + const url = `/v1/topics?page=1&pageSize=101`; const response = await session.testAgent.get(url); expect(response.statusCode).to.eql(400); @@ -122,7 +119,7 @@ describe('Filter topics - /topics (GET)', async () => { }); it('should ignore other query params and return all the topics that belong the user', async () => { - const url = `${BASE_PATH}?unsupportedParam=whatever`; + const url = `/v1/topics?unsupportedParam=whatever`; const response = await session.testAgent.get(url); expect(response.statusCode).to.eql(200); diff --git a/apps/api/src/app/topics/e2e/helpers/topic-e2e-helper.ts b/apps/api/src/app/topics/e2e/helpers/topic-e2e-helper.ts index 44a69cd61c3..84ffbb29b70 100644 --- a/apps/api/src/app/topics/e2e/helpers/topic-e2e-helper.ts +++ b/apps/api/src/app/topics/e2e/helpers/topic-e2e-helper.ts @@ -6,8 +6,6 @@ import { GetTopicResponseDto } from '@novu/api/models/components'; import { TopicId, TopicKey, TopicName } from '../../types'; import { initNovuClassSdk } from '../../../shared/helpers/e2e/sdk/e2e-sdk.helper'; -const BASE_PATH = '/v1/topics'; - export const addSubscribers = async ( session: UserSession, topicKey: TopicKey, diff --git a/apps/api/src/app/topics/topics.controller.ts b/apps/api/src/app/topics/topics.controller.ts index 09c29ca0144..3726170ed5a 100644 --- a/apps/api/src/app/topics/topics.controller.ts +++ b/apps/api/src/app/topics/topics.controller.ts @@ -1,5 +1,5 @@ import { Body, Controller, Delete, Get, HttpCode, HttpStatus, Param, Patch, Post, Query } from '@nestjs/common'; -import { ApiOperation, ApiParam, ApiQuery, ApiTags } from '@nestjs/swagger'; +import { ApiOperation, ApiParam, ApiTags } from '@nestjs/swagger'; import { ApiRateLimitCategoryEnum, ExternalSubscriberId, TopicKey, UserSessionData } from '@novu/shared'; import { @@ -166,29 +166,11 @@ export class TopicsController { @Get('') @ExternalApiAccessible() - @ApiQuery({ - name: 'key', - type: String, - description: 'Topic key', - required: false, - }) - @ApiQuery({ - name: 'page', - type: Number, - description: 'Number of page for the pagination', - required: false, - }) - @ApiQuery({ - name: 'pageSize', - type: Number, - description: 'Size of page for the pagination', - required: false, - }) @ApiOkResponse({ type: FilterTopicsResponseDto, }) @ApiOperation({ - summary: 'Filter topics', + summary: 'Get topic list filtered ', description: 'Returns a list of topics that can be paginated using the `page` query ' + 'parameter and filtered by the topic key with the `key` query parameter', diff --git a/apps/api/src/app/widgets/e2e/get-count.e2e.ts b/apps/api/src/app/widgets/e2e/get-count.e2e.ts index 5ece2cbdd32..107b715c2fe 100644 --- a/apps/api/src/app/widgets/e2e/get-count.e2e.ts +++ b/apps/api/src/app/widgets/e2e/get-count.e2e.ts @@ -10,6 +10,8 @@ import { CacheService, InvalidateCacheService, } from '@novu/application-generic'; +import { Novu } from '@novu/api'; +import { initNovuClassSdk } from '../../shared/helpers/e2e/sdk/e2e-sdk.helper'; describe('Count - GET /widget/notifications/count', function () { const messageRepository = new MessageRepository(); @@ -23,7 +25,7 @@ describe('Count - GET /widget/notifications/count', function () { let invalidateCache: InvalidateCacheService; let cacheInMemoryProviderService: CacheInMemoryProviderService; - + let novuClient: Novu; before(async () => { cacheInMemoryProviderService = new CacheInMemoryProviderService(); const cacheService = new CacheService(cacheInMemoryProviderService); @@ -34,6 +36,7 @@ describe('Count - GET /widget/notifications/count', function () { beforeEach(async () => { session = new UserSession(); await session.initialize(); + novuClient = initNovuClassSdk(session); subscriberId = SubscriberRepository.createObjectId(); @@ -59,9 +62,9 @@ describe('Count - GET /widget/notifications/count', function () { }); it('should return unseen count', async function () { - await session.triggerEvent(template.triggers[0].identifier, subscriberId); - await session.triggerEvent(template.triggers[0].identifier, subscriberId); - await session.triggerEvent(template.triggers[0].identifier, subscriberId); + await novuClient.trigger({ name: template.triggers[0].identifier, to: subscriberId }); + await novuClient.trigger({ name: template.triggers[0].identifier, to: subscriberId }); + await novuClient.trigger({ name: template.triggers[0].identifier, to: subscriberId }); await session.awaitRunningJobs(template._id); @@ -75,9 +78,9 @@ describe('Count - GET /widget/notifications/count', function () { }); it('should return unseen count after on message was seen', async function () { - await session.triggerEvent(template.triggers[0].identifier, subscriberId); - await session.triggerEvent(template.triggers[0].identifier, subscriberId); - await session.triggerEvent(template.triggers[0].identifier, subscriberId); + await novuClient.trigger({ name: template.triggers[0].identifier, to: subscriberId }); + await novuClient.trigger({ name: template.triggers[0].identifier, to: subscriberId }); + await novuClient.trigger({ name: template.triggers[0].identifier, to: subscriberId }); await session.awaitRunningJobs(template._id); @@ -105,9 +108,9 @@ describe('Count - GET /widget/notifications/count', function () { }); it('should return unseen count after on message was read', async function () { - await session.triggerEvent(template.triggers[0].identifier, subscriberId); - await session.triggerEvent(template.triggers[0].identifier, subscriberId); - await session.triggerEvent(template.triggers[0].identifier, subscriberId); + await novuClient.trigger({ name: template.triggers[0].identifier, to: subscriberId }); + await novuClient.trigger({ name: template.triggers[0].identifier, to: subscriberId }); + await novuClient.trigger({ name: template.triggers[0].identifier, to: subscriberId }); await session.awaitRunningJobs(template._id); @@ -138,9 +141,9 @@ describe('Count - GET /widget/notifications/count', function () { }); it('should return unseen count by limit', async function () { - await session.triggerEvent(template.triggers[0].identifier, subscriberId); - await session.triggerEvent(template.triggers[0].identifier, subscriberId); - await session.triggerEvent(template.triggers[0].identifier, subscriberId); + await novuClient.trigger({ name: template.triggers[0].identifier, to: subscriberId }); + await novuClient.trigger({ name: template.triggers[0].identifier, to: subscriberId }); + await novuClient.trigger({ name: template.triggers[0].identifier, to: subscriberId }); await session.awaitRunningJobs(template._id); @@ -201,8 +204,8 @@ describe('Count - GET /widget/notifications/count', function () { }); it('should return default on string non numeric(NaN) value', async function () { - await session.triggerEvent(template.triggers[0].identifier, subscriberId); - await session.triggerEvent(template.triggers[0].identifier, subscriberId); + await novuClient.trigger({ name: template.triggers[0].identifier, to: subscriberId }); + await novuClient.trigger({ name: template.triggers[0].identifier, to: subscriberId }); await session.awaitRunningJobs(template._id); @@ -211,8 +214,8 @@ describe('Count - GET /widget/notifications/count', function () { }); it('should return parse numeric string to number', async function () { - await session.triggerEvent(template.triggers[0].identifier, subscriberId); - await session.triggerEvent(template.triggers[0].identifier, subscriberId); + await novuClient.trigger({ name: template.triggers[0].identifier, to: subscriberId }); + await novuClient.trigger({ name: template.triggers[0].identifier, to: subscriberId }); await session.awaitRunningJobs(template._id); @@ -246,9 +249,9 @@ describe('Count - GET /widget/notifications/count', function () { }); it('should return unseen count with a seen filter', async function () { - await session.triggerEvent(template.triggers[0].identifier, subscriberId); - await session.triggerEvent(template.triggers[0].identifier, subscriberId); - await session.triggerEvent(template.triggers[0].identifier, subscriberId); + await novuClient.trigger({ name: template.triggers[0].identifier, to: subscriberId }); + await novuClient.trigger({ name: template.triggers[0].identifier, to: subscriberId }); + await novuClient.trigger({ name: template.triggers[0].identifier, to: subscriberId }); await session.awaitRunningJobs(template._id); @@ -275,9 +278,9 @@ describe('Count - GET /widget/notifications/count', function () { }); it('should return unread count with a read filter', async function () { - await session.triggerEvent(template.triggers[0].identifier, subscriberId); - await session.triggerEvent(template.triggers[0].identifier, subscriberId); - await session.triggerEvent(template.triggers[0].identifier, subscriberId); + await novuClient.trigger({ name: template.triggers[0].identifier, to: subscriberId }); + await novuClient.trigger({ name: template.triggers[0].identifier, to: subscriberId }); + await novuClient.trigger({ name: template.triggers[0].identifier, to: subscriberId }); await session.awaitRunningJobs(template._id); if (!subscriberProfile) throw new Error('Subscriber profile is null'); @@ -309,9 +312,9 @@ describe('Count - GET /widget/notifications/count', function () { }); it('should return unseen count after mark as request', async function () { - await session.triggerEvent(template.triggers[0].identifier, subscriberId); - await session.triggerEvent(template.triggers[0].identifier, subscriberId); - await session.triggerEvent(template.triggers[0].identifier, subscriberId); + await novuClient.trigger({ name: template.triggers[0].identifier, to: subscriberId }); + await novuClient.trigger({ name: template.triggers[0].identifier, to: subscriberId }); + await novuClient.trigger({ name: template.triggers[0].identifier, to: subscriberId }); await session.awaitRunningJobs(template._id); diff --git a/apps/api/src/app/widgets/e2e/get-notification-feed.e2e.ts b/apps/api/src/app/widgets/e2e/get-notification-feed.e2e.ts index da86850f1b4..1fca57fdba3 100644 --- a/apps/api/src/app/widgets/e2e/get-notification-feed.e2e.ts +++ b/apps/api/src/app/widgets/e2e/get-notification-feed.e2e.ts @@ -3,6 +3,8 @@ import { MessageRepository, NotificationTemplateEntity, SubscriberRepository } f import { UserSession } from '@novu/testing'; import { expect } from 'chai'; import { ChannelTypeEnum } from '@novu/shared'; +import { Novu } from '@novu/api'; +import { initNovuClassSdk } from '../../shared/helpers/e2e/sdk/e2e-sdk.helper'; describe('GET /widget/notifications/feed', function () { const messageRepository = new MessageRepository(); @@ -13,7 +15,7 @@ describe('GET /widget/notifications/feed', function () { let subscriberProfile: { _id: string; } | null = null; - + let novuClient: Novu; beforeEach(async () => { session = new UserSession(); await session.initialize(); @@ -38,6 +40,7 @@ describe('GET /widget/notifications/feed', function () { subscriberToken = token; subscriberProfile = profile; + novuClient = initNovuClassSdk(session); }); it('should fetch a feed without filters and with feed id', async function () { @@ -50,8 +53,8 @@ describe('GET /widget/notifications/feed', function () { */ template = await session.createTemplate(); - await session.triggerEvent(template.triggers[0].identifier, subscriberId); - await session.triggerEvent(template.triggers[0].identifier, subscriberId); + await novuClient.trigger({ name: template.triggers[0].identifier, to: subscriberId }); + await novuClient.trigger({ name: template.triggers[0].identifier, to: subscriberId }); await session.awaitRunningJobs(template._id); @@ -60,8 +63,8 @@ describe('GET /widget/notifications/feed', function () { }); it('should fetch a feed without filters', async function () { - await session.triggerEvent(template.triggers[0].identifier, subscriberId); - await session.triggerEvent(template.triggers[0].identifier, subscriberId); + await novuClient.trigger({ name: template.triggers[0].identifier, to: subscriberId }); + await novuClient.trigger({ name: template.triggers[0].identifier, to: subscriberId }); await session.awaitRunningJobs(template._id); @@ -70,8 +73,8 @@ describe('GET /widget/notifications/feed', function () { }); it('should filter only unseen messages', async function () { - await session.triggerEvent(template.triggers[0].identifier, subscriberId); - await session.triggerEvent(template.triggers[0].identifier, subscriberId); + await novuClient.trigger({ name: template.triggers[0].identifier, to: subscriberId }); + await novuClient.trigger({ name: template.triggers[0].identifier, to: subscriberId }); await session.awaitRunningJobs(template._id); @@ -95,8 +98,8 @@ describe('GET /widget/notifications/feed', function () { }); it('should return seen and unseen', async function () { - await session.triggerEvent(template.triggers[0].identifier, subscriberId); - await session.triggerEvent(template.triggers[0].identifier, subscriberId); + await novuClient.trigger({ name: template.triggers[0].identifier, to: subscriberId }); + await novuClient.trigger({ name: template.triggers[0].identifier, to: subscriberId }); await session.awaitRunningJobs(template._id); @@ -123,8 +126,8 @@ describe('GET /widget/notifications/feed', function () { }); it('should include subscriber object', async function () { - await session.triggerEvent(template.triggers[0].identifier, subscriberId); - await session.triggerEvent(template.triggers[0].identifier, subscriberId); + await novuClient.trigger({ name: template.triggers[0].identifier, to: subscriberId }); + await novuClient.trigger({ name: template.triggers[0].identifier, to: subscriberId }); await session.awaitRunningJobs(template._id); @@ -134,7 +137,7 @@ describe('GET /widget/notifications/feed', function () { }); it('should include hasMore when there is more notification', async function () { - await session.triggerEvent(template.triggers[0].identifier, subscriberId); + await novuClient.trigger({ name: template.triggers[0].identifier, to: subscriberId }); await session.awaitRunningJobs(template._id); @@ -145,7 +148,7 @@ describe('GET /widget/notifications/feed', function () { expect(feed.hasMore).to.be.equal(false); for (let i = 0; i < 10; i += 1) { - await session.triggerEvent(template.triggers[0].identifier, subscriberId); + await novuClient.trigger({ name: template.triggers[0].identifier, to: subscriberId }); } await session.awaitRunningJobs(template._id); @@ -158,7 +161,7 @@ describe('GET /widget/notifications/feed', function () { }); it('should throw exception when invalid payload query param is passed', async function () { - await session.triggerEvent(template.triggers[0].identifier, subscriberId); + await novuClient.trigger({ name: template.triggers[0].identifier, to: subscriberId }); await session.awaitRunningJobs(template._id); @@ -178,10 +181,10 @@ describe('GET /widget/notifications/feed', function () { const partialPayload = { foo: 123 }; const payload = { ...partialPayload, bar: 'bar' }; - await session.triggerEvent(template.triggers[0].identifier, subscriberId); + await novuClient.trigger({ name: template.triggers[0].identifier, to: subscriberId }); await session.awaitRunningJobs(template._id); - await session.triggerEvent(template.triggers[0].identifier, subscriberId, payload); + await novuClient.trigger({ name: template.triggers[0].identifier, to: subscriberId, payload }); await session.awaitRunningJobs(template._id); const payloadQueryValue = Buffer.from(JSON.stringify(partialPayload)).toString('base64'); @@ -195,10 +198,10 @@ describe('GET /widget/notifications/feed', function () { const partialPayload = { foo: { bar: 123 } }; const payload = { ...partialPayload, baz: 'baz' }; - await session.triggerEvent(template.triggers[0].identifier, subscriberId); + await novuClient.trigger({ name: template.triggers[0].identifier, to: subscriberId }); await session.awaitRunningJobs(template._id); - await session.triggerEvent(template.triggers[0].identifier, subscriberId, payload); + await novuClient.trigger({ name: template.triggers[0].identifier, to: subscriberId, payload }); await session.awaitRunningJobs(template._id); const payloadQueryValue = Buffer.from(JSON.stringify(partialPayload)).toString('base64'); diff --git a/apps/api/src/app/widgets/e2e/get-unread-count.e2e.ts b/apps/api/src/app/widgets/e2e/get-unread-count.e2e.ts index bc7512da204..764c7b30000 100644 --- a/apps/api/src/app/widgets/e2e/get-unread-count.e2e.ts +++ b/apps/api/src/app/widgets/e2e/get-unread-count.e2e.ts @@ -3,6 +3,8 @@ import { expect } from 'chai'; import { MessageRepository, NotificationTemplateEntity, SubscriberRepository } from '@novu/dal'; import { UserSession } from '@novu/testing'; import { ChannelTypeEnum } from '@novu/shared'; +import { Novu } from '@novu/api'; +import { initNovuClassSdk } from '../../shared/helpers/e2e/sdk/e2e-sdk.helper'; describe('Unread Count - GET /widget/notifications/unread', function () { const messageRepository = new MessageRepository(); @@ -13,11 +15,12 @@ describe('Unread Count - GET /widget/notifications/unread', function () { let subscriberProfile: { _id: string; } | null = null; - + let novuClient: Novu; beforeEach(async () => { session = new UserSession(); await session.initialize(); subscriberId = SubscriberRepository.createObjectId(); + novuClient = initNovuClassSdk(session); template = await session.createTemplate({ noFeedId: true, @@ -41,9 +44,9 @@ describe('Unread Count - GET /widget/notifications/unread', function () { }); it('should return unread count with no query', async function () { - await session.triggerEvent(template.triggers[0].identifier, subscriberId); - await session.triggerEvent(template.triggers[0].identifier, subscriberId); - await session.triggerEvent(template.triggers[0].identifier, subscriberId); + await novuClient.trigger({ name: template.triggers[0].identifier, to: subscriberId }); + await novuClient.trigger({ name: template.triggers[0].identifier, to: subscriberId }); + await novuClient.trigger({ name: template.triggers[0].identifier, to: subscriberId }); await session.awaitRunningJobs(template._id); @@ -70,9 +73,9 @@ describe('Unread Count - GET /widget/notifications/unread', function () { }); it('should return unread count with query read false', async function () { - await session.triggerEvent(template.triggers[0].identifier, subscriberId); - await session.triggerEvent(template.triggers[0].identifier, subscriberId); - await session.triggerEvent(template.triggers[0].identifier, subscriberId); + await novuClient.trigger({ name: template.triggers[0].identifier, to: subscriberId }); + await novuClient.trigger({ name: template.triggers[0].identifier, to: subscriberId }); + await novuClient.trigger({ name: template.triggers[0].identifier, to: subscriberId }); await session.awaitRunningJobs(template._id); @@ -99,9 +102,9 @@ describe('Unread Count - GET /widget/notifications/unread', function () { }); it('should return unread count with query read true', async function () { - await session.triggerEvent(template.triggers[0].identifier, subscriberId); - await session.triggerEvent(template.triggers[0].identifier, subscriberId); - await session.triggerEvent(template.triggers[0].identifier, subscriberId); + await novuClient.trigger({ name: template.triggers[0].identifier, to: subscriberId }); + await novuClient.trigger({ name: template.triggers[0].identifier, to: subscriberId }); + await novuClient.trigger({ name: template.triggers[0].identifier, to: subscriberId }); await session.awaitRunningJobs(template._id); diff --git a/apps/api/src/app/widgets/e2e/get-unseen-count.e2e.ts b/apps/api/src/app/widgets/e2e/get-unseen-count.e2e.ts index a254198d06d..ccabc5caa5b 100644 --- a/apps/api/src/app/widgets/e2e/get-unseen-count.e2e.ts +++ b/apps/api/src/app/widgets/e2e/get-unseen-count.e2e.ts @@ -10,6 +10,8 @@ import { CacheService, InvalidateCacheService, } from '@novu/application-generic'; +import { Novu } from '@novu/api'; +import { initNovuClassSdk } from '../../shared/helpers/e2e/sdk/e2e-sdk.helper'; describe('Unseen Count - GET /widget/notifications/unseen', function () { const messageRepository = new MessageRepository(); @@ -23,12 +25,13 @@ describe('Unseen Count - GET /widget/notifications/unseen', function () { let cacheInMemoryProviderService: CacheInMemoryProviderService; let invalidateCache: InvalidateCacheService; - + let novuClient: Novu; before(async () => { cacheInMemoryProviderService = new CacheInMemoryProviderService(); const cacheService = new CacheService(cacheInMemoryProviderService); await cacheService.initialize(); invalidateCache = new InvalidateCacheService(cacheService); + novuClient = initNovuClassSdk(session); }); beforeEach(async () => { @@ -59,9 +62,9 @@ describe('Unseen Count - GET /widget/notifications/unseen', function () { }); it('should return unseen count with no query', async function () { - await session.triggerEvent(template.triggers[0].identifier, subscriberId); - await session.triggerEvent(template.triggers[0].identifier, subscriberId); - await session.triggerEvent(template.triggers[0].identifier, subscriberId); + await novuClient.trigger({ name: template.triggers[0].identifier, to: subscriberId }); + await novuClient.trigger({ name: template.triggers[0].identifier, to: subscriberId }); + await novuClient.trigger({ name: template.triggers[0].identifier, to: subscriberId }); await session.awaitRunningJobs(template._id); @@ -88,9 +91,9 @@ describe('Unseen Count - GET /widget/notifications/unseen', function () { }); it('should return unseen count with query seen false', async function () { - await session.triggerEvent(template.triggers[0].identifier, subscriberId); - await session.triggerEvent(template.triggers[0].identifier, subscriberId); - await session.triggerEvent(template.triggers[0].identifier, subscriberId); + await novuClient.trigger({ name: template.triggers[0].identifier, to: subscriberId }); + await novuClient.trigger({ name: template.triggers[0].identifier, to: subscriberId }); + await novuClient.trigger({ name: template.triggers[0].identifier, to: subscriberId }); await session.awaitRunningJobs(template._id); @@ -117,9 +120,9 @@ describe('Unseen Count - GET /widget/notifications/unseen', function () { }); it('should return unseen count with query seen true', async function () { - await session.triggerEvent(template.triggers[0].identifier, subscriberId); - await session.triggerEvent(template.triggers[0].identifier, subscriberId); - await session.triggerEvent(template.triggers[0].identifier, subscriberId); + await novuClient.trigger({ name: template.triggers[0].identifier, to: subscriberId }); + await novuClient.trigger({ name: template.triggers[0].identifier, to: subscriberId }); + await novuClient.trigger({ name: template.triggers[0].identifier, to: subscriberId }); await session.awaitRunningJobs(template._id); @@ -146,9 +149,9 @@ describe('Unseen Count - GET /widget/notifications/unseen', function () { }); it('should return unseen count after mark as request', async function () { - await session.triggerEvent(template.triggers[0].identifier, subscriberId); - await session.triggerEvent(template.triggers[0].identifier, subscriberId); - await session.triggerEvent(template.triggers[0].identifier, subscriberId); + await novuClient.trigger({ name: template.triggers[0].identifier, to: subscriberId }); + await novuClient.trigger({ name: template.triggers[0].identifier, to: subscriberId }); + await novuClient.trigger({ name: template.triggers[0].identifier, to: subscriberId }); await session.awaitRunningJobs(template._id); diff --git a/apps/api/src/app/widgets/e2e/mark-all-as-read.e2e.ts b/apps/api/src/app/widgets/e2e/mark-all-as-read.e2e.ts index 935ba5a993b..e7e8dab0e00 100644 --- a/apps/api/src/app/widgets/e2e/mark-all-as-read.e2e.ts +++ b/apps/api/src/app/widgets/e2e/mark-all-as-read.e2e.ts @@ -2,7 +2,8 @@ import axios from 'axios'; import { MessageRepository, NotificationTemplateEntity, SubscriberRepository } from '@novu/dal'; import { UserSession } from '@novu/testing'; import { expect } from 'chai'; -import { ChannelTypeEnum } from '@novu/shared'; +import { Novu } from '@novu/api'; +import { initNovuClassSdk } from '../../shared/helpers/e2e/sdk/e2e-sdk.helper'; describe('Mark all as read - /widgets/messages/seen (POST)', function () { const messageRepository = new MessageRepository(); @@ -13,11 +14,12 @@ describe('Mark all as read - /widgets/messages/seen (POST)', function () { let subscriberProfile: { _id: string; } | null = null; - + let novuClient: Novu; beforeEach(async () => { session = new UserSession(); await session.initialize(); subscriberId = SubscriberRepository.createObjectId(); + novuClient = initNovuClassSdk(session); template = await session.createTemplate({ noFeedId: true, @@ -41,9 +43,9 @@ describe('Mark all as read - /widgets/messages/seen (POST)', function () { }); it('should mark all as seen', async function () { - await session.triggerEvent(template.triggers[0].identifier, subscriberId); - await session.triggerEvent(template.triggers[0].identifier, subscriberId); - await session.triggerEvent(template.triggers[0].identifier, subscriberId); + await novuClient.trigger({ name: template.triggers[0].identifier, to: subscriberId }); + await novuClient.trigger({ name: template.triggers[0].identifier, to: subscriberId }); + await novuClient.trigger({ name: template.triggers[0].identifier, to: subscriberId }); await session.awaitRunningJobs(template._id); @@ -65,9 +67,9 @@ describe('Mark all as read - /widgets/messages/seen (POST)', function () { }); it('should mark all as read', async function () { - await session.triggerEvent(template.triggers[0].identifier, subscriberId); - await session.triggerEvent(template.triggers[0].identifier, subscriberId); - await session.triggerEvent(template.triggers[0].identifier, subscriberId); + await novuClient.trigger({ name: template.triggers[0].identifier, to: subscriberId }); + await novuClient.trigger({ name: template.triggers[0].identifier, to: subscriberId }); + await novuClient.trigger({ name: template.triggers[0].identifier, to: subscriberId }); await session.awaitRunningJobs(template._id); diff --git a/apps/api/src/app/widgets/e2e/mark-as-by-mark.e2e.ts b/apps/api/src/app/widgets/e2e/mark-as-by-mark.e2e.ts index 465e5598263..3f81b0a108b 100644 --- a/apps/api/src/app/widgets/e2e/mark-as-by-mark.e2e.ts +++ b/apps/api/src/app/widgets/e2e/mark-as-by-mark.e2e.ts @@ -10,6 +10,8 @@ import { SubscriberRepository, } from '@novu/dal'; import { ChannelTypeEnum, MessagesStatusEnum } from '@novu/shared'; +import { Novu } from '@novu/api'; +import { initNovuClassSdk } from '../../shared/helpers/e2e/sdk/e2e-sdk.helper'; describe('Mark as Seen - /widgets/messages/mark-as (POST)', async () => { const messageRepository = new MessageRepository(); @@ -20,12 +22,13 @@ describe('Mark as Seen - /widgets/messages/mark-as (POST)', async () => { let subscriberToken: string; let subscriber: SubscriberEntity; let message: MessageEntity; - + let novuClient: Novu; before(async () => { session = new UserSession(); await session.initialize(); subscriberId = SubscriberRepository.createObjectId(); template = await session.createTemplate(); + novuClient = initNovuClassSdk(session); const { body } = await session.testAgent .post('/v1/widgets/session/initialize') @@ -42,7 +45,7 @@ describe('Mark as Seen - /widgets/messages/mark-as (POST)', async () => { }); beforeEach(async () => { - await session.triggerEvent(template.triggers[0].identifier, subscriberId); + await novuClient.trigger({ name: template.triggers[0].identifier, to: subscriberId }); await session.awaitRunningJobs(template._id); message = await getMessage(session, messageRepository, subscriber); diff --git a/apps/api/src/app/widgets/e2e/mark-as.e2e.ts b/apps/api/src/app/widgets/e2e/mark-as.e2e.ts index fdb91c40335..2ad838143e0 100644 --- a/apps/api/src/app/widgets/e2e/mark-as.e2e.ts +++ b/apps/api/src/app/widgets/e2e/mark-as.e2e.ts @@ -1,20 +1,23 @@ -import { MessageRepository, NotificationTemplateEntity, SubscriberRepository, MessageEntity } from '@novu/dal'; +import { MessageEntity, MessageRepository, NotificationTemplateEntity, SubscriberRepository } from '@novu/dal'; import { UserSession } from '@novu/testing'; import axios from 'axios'; import { ChannelTypeEnum } from '@novu/shared'; import { expect } from 'chai'; +import { Novu } from '@novu/api'; +import { initNovuClassSdk } from '../../shared/helpers/e2e/sdk/e2e-sdk.helper'; describe('Mark as Seen - /widgets/messages/markAs (POST)', async () => { const messageRepository = new MessageRepository(); let session: UserSession; let template: NotificationTemplateEntity; let subscriberId; - + let novuClient: Novu; before(async () => { session = new UserSession(); await session.initialize(); subscriberId = SubscriberRepository.createObjectId(); template = await session.createTemplate(); + novuClient = initNovuClassSdk(session); }); it('should change the seen status', async function () { @@ -29,9 +32,9 @@ describe('Mark as Seen - /widgets/messages/markAs (POST)', async () => { }) .expect(201); - await session.triggerEvent(template.triggers[0].identifier, subscriberId); + await novuClient.trigger({ name: template.triggers[0].identifier, to: subscriberId }); - await session.triggerEvent(template.triggers[0].identifier, subscriberId); + await novuClient.trigger({ name: template.triggers[0].identifier, to: subscriberId }); await session.awaitRunningJobs(template._id); const { token } = body.data; const messages = await messageRepository.findBySubscriberChannel( diff --git a/apps/api/src/app/widgets/e2e/remove-all-messages.e2e.ts b/apps/api/src/app/widgets/e2e/remove-all-messages.e2e.ts index 3c984801c5b..ca3770c1f34 100644 --- a/apps/api/src/app/widgets/e2e/remove-all-messages.e2e.ts +++ b/apps/api/src/app/widgets/e2e/remove-all-messages.e2e.ts @@ -3,6 +3,8 @@ import { MessageRepository, NotificationTemplateEntity, SubscriberRepository } f import { UserSession } from '@novu/testing'; import { expect } from 'chai'; import { ChannelTypeEnum } from '@novu/shared'; +import { Novu } from '@novu/api'; +import { initNovuClassSdk } from '../../shared/helpers/e2e/sdk/e2e-sdk.helper'; describe('Remove all messages - /widgets/messages (DELETE)', function () { const messageRepository = new MessageRepository(); @@ -13,11 +15,12 @@ describe('Remove all messages - /widgets/messages (DELETE)', function () { let subscriberProfile: { _id: string; } | null = null; - + let novuClient: Novu; beforeEach(async () => { session = new UserSession(); await session.initialize(); subscriberId = SubscriberRepository.createObjectId(); + novuClient = initNovuClassSdk(session); template = await session.createTemplate({ noFeedId: true, @@ -41,9 +44,9 @@ describe('Remove all messages - /widgets/messages (DELETE)', function () { }); it('should remove all messages', async function () { - await session.triggerEvent(template.triggers[0].identifier, subscriberId); - await session.triggerEvent(template.triggers[0].identifier, subscriberId); - await session.triggerEvent(template.triggers[0].identifier, subscriberId); + await novuClient.trigger({ name: template.triggers[0].identifier, to: subscriberId }); + await novuClient.trigger({ name: template.triggers[0].identifier, to: subscriberId }); + await novuClient.trigger({ name: template.triggers[0].identifier, to: subscriberId }); await session.awaitRunningJobs(template._id); @@ -74,11 +77,11 @@ describe('Remove all messages - /widgets/messages (DELETE)', function () { const _feedId = templateWithFeed?.steps[0]?.template?._feedId; - await session.triggerEvent(template.triggers[0].identifier, subscriberId); - await session.triggerEvent(template.triggers[0].identifier, subscriberId); - await session.triggerEvent(template.triggers[0].identifier, subscriberId); - await session.triggerEvent(templateWithFeed.triggers[0].identifier, subscriberId); - await session.triggerEvent(templateWithFeed.triggers[0].identifier, subscriberId); + await novuClient.trigger({ name: template.triggers[0].identifier, to: subscriberId }); + await novuClient.trigger({ name: template.triggers[0].identifier, to: subscriberId }); + await novuClient.trigger({ name: template.triggers[0].identifier, to: subscriberId }); + await novuClient.trigger({ name: templateWithFeed.triggers[0].identifier, to: subscriberId }); + await novuClient.trigger({ name: templateWithFeed.triggers[0].identifier, to: subscriberId }); await session.awaitRunningJobs(templateWithFeed._id); await session.awaitRunningJobs(template._id); diff --git a/apps/api/src/app/widgets/e2e/remove-messages-bulk.e2e.ts b/apps/api/src/app/widgets/e2e/remove-messages-bulk.e2e.ts index 2003d67f65d..762ddd5f70d 100644 --- a/apps/api/src/app/widgets/e2e/remove-messages-bulk.e2e.ts +++ b/apps/api/src/app/widgets/e2e/remove-messages-bulk.e2e.ts @@ -4,6 +4,8 @@ import { expect } from 'chai'; import { UserSession } from '@novu/testing'; import { MessageRepository, NotificationTemplateEntity, SubscriberRepository } from '@novu/dal'; import { ChannelTypeEnum } from '@novu/shared'; +import { Novu } from '@novu/api'; +import { initNovuClassSdk } from '../../shared/helpers/e2e/sdk/e2e-sdk.helper'; describe('Remove messages by bulk - /widgets/messages/bulk/delete (POST)', function () { const messageRepository = new MessageRepository(); @@ -14,11 +16,12 @@ describe('Remove messages by bulk - /widgets/messages/bulk/delete (POST)', funct let subscriberProfile: { _id: string; } | null = null; - + let novuClient: Novu; beforeEach(async () => { session = new UserSession(); await session.initialize(); subscriberId = SubscriberRepository.createObjectId(); + novuClient = initNovuClassSdk(session); template = await session.createTemplate({ noFeedId: true, @@ -42,9 +45,9 @@ describe('Remove messages by bulk - /widgets/messages/bulk/delete (POST)', funct }); it('should remove messages by bulk', async function () { - await session.triggerEvent(template.triggers[0].identifier, subscriberId); - await session.triggerEvent(template.triggers[0].identifier, subscriberId); - await session.triggerEvent(template.triggers[0].identifier, subscriberId); + await novuClient.trigger({ name: template.triggers[0].identifier, to: subscriberId }); + await novuClient.trigger({ name: template.triggers[0].identifier, to: subscriberId }); + await novuClient.trigger({ name: template.triggers[0].identifier, to: subscriberId }); await session.awaitRunningJobs(template._id); @@ -79,9 +82,9 @@ describe('Remove messages by bulk - /widgets/messages/bulk/delete (POST)', funct }); it('should throw an exception when message ids were not provided', async function () { - await session.triggerEvent(template.triggers[0].identifier, subscriberId); - await session.triggerEvent(template.triggers[0].identifier, subscriberId); - await session.triggerEvent(template.triggers[0].identifier, subscriberId); + await novuClient.trigger({ name: template.triggers[0].identifier, to: subscriberId }); + await novuClient.trigger({ name: template.triggers[0].identifier, to: subscriberId }); + await novuClient.trigger({ name: template.triggers[0].identifier, to: subscriberId }); await session.awaitRunningJobs(template._id); diff --git a/libs/testing/src/user.session.ts b/libs/testing/src/user.session.ts index 96dd8418180..fb776c8f496 100644 --- a/libs/testing/src/user.session.ts +++ b/libs/testing/src/user.session.ts @@ -8,22 +8,21 @@ import { EmailBlockTypeEnum, IApiRateLimitMaximum, IEmailBlock, + isClerkEnabled, JobTopicNameEnum, StepTypeEnum, - TriggerRecipientsPayload, - isClerkEnabled, } from '@novu/shared'; import { - UserEntity, + ChangeEntity, + ChangeRepository, EnvironmentEntity, - OrganizationEntity, + FeedRepository, + LayoutRepository, NotificationGroupEntity, NotificationGroupRepository, - FeedRepository, - ChangeRepository, - ChangeEntity, + OrganizationEntity, SubscriberRepository, - LayoutRepository, + UserEntity, } from '@novu/dal'; import { NotificationTemplateService } from './notification-template.service'; @@ -431,14 +430,6 @@ export class UserSession { return feed; } - async triggerEvent(triggerName: string, to: TriggerRecipientsPayload, payload = {}) { - await this.testAgent.post('/v1/events/trigger').send({ - name: triggerName, - to, - payload, - }); - } - public async awaitRunningJobs( templateId?: string | string[], delay?: boolean, diff --git a/packages/shared/src/types/providers.ts b/packages/shared/src/types/providers.ts index b0acdbe18c4..5554e4335bc 100644 --- a/packages/shared/src/types/providers.ts +++ b/packages/shared/src/types/providers.ts @@ -140,3 +140,11 @@ export type ProvidersIdEnum = | PushProviderIdEnum | InAppProviderIdEnum | ChatProviderIdEnum; + +export const ProvidersIdEnumConst = { + EmailProviderIdEnum, + SmsProviderIdEnum, + PushProviderIdEnum, + InAppProviderIdEnum, + ChatProviderIdEnum, +}; diff --git a/pnpm-lock.yaml b/pnpm-lock.yaml index 192656f3944..960e5a2fdf0 100644 --- a/pnpm-lock.yaml +++ b/pnpm-lock.yaml @@ -394,8 +394,8 @@ importers: specifier: 6.2.1 version: 6.2.1(@nestjs/common@10.4.1(class-transformer@0.5.1)(class-validator@0.14.1)(reflect-metadata@0.2.2)(rxjs@7.8.1))(@nestjs/core@10.4.1(@nestjs/common@10.4.1(class-transformer@0.5.1)(class-validator@0.14.1)(reflect-metadata@0.2.2)(rxjs@7.8.1))(@nestjs/platform-express@10.4.1)(@nestjs/websockets@10.4.1)(encoding@0.1.13)(reflect-metadata@0.2.2)(rxjs@7.8.1))(reflect-metadata@0.2.2) '@novu/api': - specifier: 0.0.1-alpha.109 - version: 0.0.1-alpha.109(zod@3.23.8) + specifier: 0.0.1-alpha.119 + version: 0.0.1-alpha.119(zod@3.23.8) '@novu/application-generic': specifier: workspace:* version: link:../../libs/application-generic @@ -10595,8 +10595,8 @@ packages: '@nothing-but/utils@0.12.1': resolution: {integrity: sha512-1qZU1Q5El0IjE7JT/ucvJNzdr2hL3W8Rm27xNf1p6gb3Nw8pGnZmxp6/GEW9h+I1k1cICxXNq25hBwknTQ7yhg==} - '@novu/api@0.0.1-alpha.109': - resolution: {integrity: sha512-+fbdpvrhgzzRmdo6wh/gQG8whqSreTDGdkoW8N44sevTcMqg2LwQQs7E2hXkOcmBWUkyuZ7bbCPzMrf5uoKgfw==} + '@novu/api@0.0.1-alpha.119': + resolution: {integrity: sha512-3w8CZZNKR+88oaPyAIbyveQS2H4bAKyKfJqfSW4WngCauGnsGoVQuqRhNoTfmLHL7SHlXQQzzdIWsvg/1yX1RA==} peerDependencies: zod: '>= 3' @@ -46985,7 +46985,7 @@ snapshots: '@nothing-but/utils@0.12.1': {} - '@novu/api@0.0.1-alpha.109(zod@3.23.8)': + '@novu/api@0.0.1-alpha.119(zod@3.23.8)': dependencies: zod: 3.23.8