diff --git a/.idea/runConfigurations/_template__of_mocha_javascript_test_runner.xml b/.idea/runConfigurations/_template__of_mocha_javascript_test_runner.xml
index 3feb4656ed2..2ebef3abd61 100644
--- a/.idea/runConfigurations/_template__of_mocha_javascript_test_runner.xml
+++ b/.idea/runConfigurations/_template__of_mocha_javascript_test_runner.xml
@@ -8,10 +8,10 @@
-
+
- --timeout 30000 --require ts-node/register --exit --file e2e/setup.ts
+ --require ts-node/register --exit --file e2e/setup.ts
DIRECTORY
false
diff --git a/apps/api/src/app/events/e2e/trigger-event.e2e.ts b/apps/api/src/app/events/e2e/trigger-event.e2e.ts
index cf0789ceee9..7edd39ca0b0 100644
--- a/apps/api/src/app/events/e2e/trigger-event.e2e.ts
+++ b/apps/api/src/app/events/e2e/trigger-event.e2e.ts
@@ -4,40 +4,41 @@ import axios, { AxiosResponse } from 'axios';
import { v4 as uuid } from 'uuid';
import { differenceInMilliseconds, subDays } from 'date-fns';
import {
+ EnvironmentRepository,
+ ExecutionDetailsRepository,
+ IntegrationRepository,
+ JobRepository,
+ JobStatusEnum,
MessageRepository,
NotificationRepository,
NotificationTemplateEntity,
+ NotificationTemplateRepository,
SubscriberEntity,
SubscriberRepository,
- JobRepository,
- JobStatusEnum,
- IntegrationRepository,
- ExecutionDetailsRepository,
- EnvironmentRepository,
TenantRepository,
- NotificationTemplateRepository,
} from '@novu/dal';
-import { UserSession, SubscribersService, WorkflowOverrideService } from '@novu/testing';
+import { SubscribersService, UserSession, WorkflowOverrideService } from '@novu/testing';
import {
+ ActorTypeEnum,
ChannelTypeEnum,
+ ChatProviderIdEnum,
+ DEFAULT_MESSAGE_GENERIC_RETENTION_DAYS,
+ DEFAULT_MESSAGE_IN_APP_RETENTION_DAYS,
+ DelayTypeEnum,
+ DigestUnitEnum,
EmailBlockTypeEnum,
+ EmailProviderIdEnum,
FieldLogicalOperatorEnum,
FieldOperatorEnum,
FilterPartTypeEnum,
- StepTypeEnum,
IEmailBlock,
+ InAppProviderIdEnum,
ISubscribersDefine,
- TemplateVariableTypeEnum,
- EmailProviderIdEnum,
- SmsProviderIdEnum,
- DigestUnitEnum,
- DelayTypeEnum,
PreviousStepTypeEnum,
- InAppProviderIdEnum,
- DEFAULT_MESSAGE_IN_APP_RETENTION_DAYS,
- DEFAULT_MESSAGE_GENERIC_RETENTION_DAYS,
- ActorTypeEnum,
+ SmsProviderIdEnum,
+ StepTypeEnum,
SystemAvatarIconEnum,
+ TemplateVariableTypeEnum,
} from '@novu/shared';
import { EmailEventStatusEnum } from '@novu/stateless';
import { createTenant } from '../../tenant/e2e/create-tenant.e2e';
@@ -1068,6 +1069,132 @@ describe(`Trigger event - ${eventTriggerPath} (POST)`, function () {
expect(updatedSubscriber?.locale).to.equal(payload.locale);
});
+ describe('Subscriber channels', function () {
+ it('should set a new subscriber with channels array', async function () {
+ const subscriberId = SubscriberRepository.createObjectId();
+ const payload: ISubscribersDefine = {
+ subscriberId,
+ firstName: 'Test Name',
+ lastName: 'Last of name',
+ email: undefined,
+ locale: 'en',
+ channels: [
+ {
+ providerId: ChatProviderIdEnum.Slack,
+ credentials: {
+ webhookUrl: 'https://slack.com/webhook/test',
+ deviceTokens: ['1', '2'],
+ },
+ },
+ ],
+ };
+
+ await axiosInstance.post(
+ `${session.serverUrl}${eventTriggerPath}`,
+ {
+ name: template.triggers[0].identifier,
+ to: {
+ ...payload,
+ },
+ payload: {
+ urlVar: '/test/url/path',
+ },
+ },
+ {
+ headers: {
+ authorization: `ApiKey ${session.apiKey}`,
+ },
+ }
+ );
+
+ await session.awaitRunningJobs();
+
+ const createdSubscriber = await subscriberRepository.findBySubscriberId(session.environment._id, subscriberId);
+
+ expect(createdSubscriber?.channels?.length).to.equal(1);
+ expect(createdSubscriber?.channels[0]?.providerId).to.equal(ChatProviderIdEnum.Slack);
+ expect(createdSubscriber?.channels[0]?.credentials?.webhookUrl).to.equal('https://slack.com/webhook/test');
+ expect(createdSubscriber?.channels[0]?.credentials?.deviceTokens.length).to.equal(2);
+ });
+
+ it('should update a subscribers channels array', async function () {
+ const subscriberId = SubscriberRepository.createObjectId();
+ const payload: ISubscribersDefine = {
+ subscriberId,
+ firstName: 'Test Name',
+ lastName: 'Last of name',
+ email: undefined,
+ locale: 'en',
+ channels: [
+ {
+ providerId: ChatProviderIdEnum.Slack,
+ credentials: {
+ webhookUrl: 'https://slack.com/webhook/test',
+ },
+ },
+ ],
+ };
+
+ await axiosInstance.post(
+ `${session.serverUrl}${eventTriggerPath}`,
+ {
+ name: template.triggers[0].identifier,
+ to: {
+ ...payload,
+ },
+ payload: {
+ urlVar: '/test/url/path',
+ },
+ },
+ {
+ headers: {
+ authorization: `ApiKey ${session.apiKey}`,
+ },
+ }
+ );
+
+ await session.awaitRunningJobs();
+ const createdSubscriber = await subscriberRepository.findBySubscriberId(session.environment._id, subscriberId);
+
+ expect(createdSubscriber?.subscriberId).to.equal(subscriberId);
+ expect(createdSubscriber?.channels?.length).to.equal(1);
+
+ await axiosInstance.post(
+ `${session.serverUrl}${eventTriggerPath}`,
+ {
+ name: template.triggers[0].identifier,
+ to: {
+ ...payload,
+ channels: [
+ {
+ providerId: ChatProviderIdEnum.Slack,
+ credentials: {
+ webhookUrl: 'https://slack.com/webhook/test2',
+ },
+ },
+ ],
+ },
+ payload: {
+ urlVar: '/test/url/path',
+ },
+ },
+ {
+ headers: {
+ authorization: `ApiKey ${session.apiKey}`,
+ },
+ }
+ );
+
+ await session.awaitRunningJobs();
+
+ const updatedSubscriber = await subscriberRepository.findBySubscriberId(session.environment._id, subscriberId);
+
+ expect(updatedSubscriber?.channels?.length).to.equal(1);
+ expect(updatedSubscriber?.channels[0]?.providerId).to.equal(ChatProviderIdEnum.Slack);
+ expect(updatedSubscriber?.channels[0]?.credentials?.webhookUrl).to.equal('https://slack.com/webhook/test2');
+ });
+ });
+
it('should not unset a subscriber email', async function () {
const subscriberId = SubscriberRepository.createObjectId();
const payload = {
diff --git a/apps/api/src/app/subscribers/dtos/update-subscriber-channel-request.dto.ts b/apps/api/src/app/subscribers/dtos/update-subscriber-channel-request.dto.ts
index cd0cd51b9ff..d99a034511f 100644
--- a/apps/api/src/app/subscribers/dtos/update-subscriber-channel-request.dto.ts
+++ b/apps/api/src/app/subscribers/dtos/update-subscriber-channel-request.dto.ts
@@ -1,10 +1,10 @@
import { ApiProperty } from '@nestjs/swagger';
import { IsDefined, IsObject, IsOptional, IsString } from 'class-validator';
-import { ChatProviderIdEnum, PushProviderIdEnum } from '@novu/shared';
+import { ChatProviderIdEnum, ISubscriberChannel, PushProviderIdEnum } from '@novu/shared';
import { ChannelCredentials } from '../../shared/dtos/subscriber-channel';
-export class UpdateSubscriberChannelRequestDto {
+export class UpdateSubscriberChannelRequestDto implements ISubscriberChannel {
@ApiProperty({
enum: { ...ChatProviderIdEnum, ...PushProviderIdEnum },
description: 'The provider identifier for the credentials',
diff --git a/apps/api/src/app/subscribers/subscribers.controller.ts b/apps/api/src/app/subscribers/subscribers.controller.ts
index fd9716c5ef2..820ded0cf2f 100644
--- a/apps/api/src/app/subscribers/subscribers.controller.ts
+++ b/apps/api/src/app/subscribers/subscribers.controller.ts
@@ -20,6 +20,9 @@ import {
CreateSubscriberCommand,
UpdateSubscriber,
UpdateSubscriberCommand,
+ OAuthHandlerEnum,
+ UpdateSubscriberChannel,
+ UpdateSubscriberChannelCommand,
} from '@novu/application-generic';
import { ApiOperation, ApiTags, ApiParam } from '@nestjs/swagger';
import {
@@ -45,7 +48,6 @@ import {
UpdateSubscriberGlobalPreferencesRequestDto,
UpdateSubscriberRequestDto,
} from './dtos';
-import { UpdateSubscriberChannel, UpdateSubscriberChannelCommand } from './usecases/update-subscriber-channel';
import { GetSubscribers, GetSubscribersCommand } from './usecases/get-subscribers';
import { GetSubscriber, GetSubscriberCommand } from './usecases/get-subscriber';
import { GetPreferencesByLevelCommand } from './usecases/get-preferences-by-level/get-preferences-by-level.command';
@@ -77,7 +79,6 @@ import { GetSubscribersDto } from './dtos/get-subscribers.dto';
import { GetInAppNotificationsFeedForSubscriberDto } from './dtos/get-in-app-notification-feed-for-subscriber.dto';
import { ApiCommonResponses, ApiResponse, ApiNoContentResponse } from '../shared/framework/response.decorator';
import { ChatOauthCallbackRequestDto, ChatOauthRequestDto } from './dtos/chat-oauth-request.dto';
-import { OAuthHandlerEnum } from './types';
import { ChatOauthCallback } from './usecases/chat-oauth-callback/chat-oauth-callback.usecase';
import { ChatOauthCallbackCommand } from './usecases/chat-oauth-callback/chat-oauth-callback.command';
import { ChatOauth } from './usecases/chat-oauth/chat-oauth.usecase';
diff --git a/apps/api/src/app/subscribers/usecases/update-subscriber-channel/update-subscriber-channel.spec.ts b/apps/api/src/app/subscribers/unit/update-subscriber-channel.spec.ts
similarity index 98%
rename from apps/api/src/app/subscribers/usecases/update-subscriber-channel/update-subscriber-channel.spec.ts
rename to apps/api/src/app/subscribers/unit/update-subscriber-channel.spec.ts
index 73bd7604e2c..fd6a04426ba 100644
--- a/apps/api/src/app/subscribers/usecases/update-subscriber-channel/update-subscriber-channel.spec.ts
+++ b/apps/api/src/app/subscribers/unit/update-subscriber-channel.spec.ts
@@ -2,12 +2,10 @@ import { IntegrationRepository, SubscriberRepository } from '@novu/dal';
import { SubscribersService, UserSession } from '@novu/testing';
import { Test } from '@nestjs/testing';
import { expect } from 'chai';
-import { SharedModule } from '../../../shared/shared.module';
import { ChannelTypeEnum, ChatProviderIdEnum, PushProviderIdEnum } from '@novu/shared';
-import { UpdateSubscriberChannel } from './update-subscriber-channel.usecase';
-import { UpdateSubscriberChannelCommand } from './update-subscriber-channel.command';
-import { OAuthHandlerEnum } from '../../types';
import { faker } from '@faker-js/faker';
+import { OAuthHandlerEnum, UpdateSubscriberChannel, UpdateSubscriberChannelCommand } from '@novu/application-generic';
+import { SharedModule } from '../../shared/shared.module';
describe('Update Subscriber channel credentials', function () {
let updateSubscriberChannelUsecase: UpdateSubscriberChannel;
diff --git a/apps/api/src/app/subscribers/usecases/chat-oauth-callback/chat-oauth-callback.usecase.ts b/apps/api/src/app/subscribers/usecases/chat-oauth-callback/chat-oauth-callback.usecase.ts
index 6f3fa5caf53..410c075432a 100644
--- a/apps/api/src/app/subscribers/usecases/chat-oauth-callback/chat-oauth-callback.usecase.ts
+++ b/apps/api/src/app/subscribers/usecases/chat-oauth-callback/chat-oauth-callback.usecase.ts
@@ -1,7 +1,15 @@
import { Injectable, NotFoundException } from '@nestjs/common';
import axios from 'axios';
-import { CreateSubscriber, CreateSubscriberCommand, decryptCredentials } from '@novu/application-generic';
+import {
+ CreateSubscriber,
+ CreateSubscriberCommand,
+ decryptCredentials,
+ OAuthHandlerEnum,
+ IChannelCredentialsCommand,
+ UpdateSubscriberChannel,
+ UpdateSubscriberChannelCommand,
+} from '@novu/application-generic';
import { ICredentialsDto } from '@novu/shared';
import {
ChannelTypeEnum,
@@ -12,13 +20,7 @@ import {
} from '@novu/dal';
import { ChatOauthCallbackCommand } from './chat-oauth-callback.command';
-import {
- IChannelCredentialsCommand,
- UpdateSubscriberChannel,
- UpdateSubscriberChannelCommand,
-} from '../update-subscriber-channel';
import { ApiException } from '../../../shared/exceptions/api.exception';
-import { OAuthHandlerEnum } from '../../types';
import { validateEncryption } from '../chat-oauth/chat-oauth.usecase';
@Injectable()
diff --git a/apps/api/src/app/subscribers/usecases/delete-subscriber-credentials/delete-subscriber-credentials.spec.ts b/apps/api/src/app/subscribers/usecases/delete-subscriber-credentials/delete-subscriber-credentials.spec.ts
index ef808b462b7..d2c3ee1a937 100644
--- a/apps/api/src/app/subscribers/usecases/delete-subscriber-credentials/delete-subscriber-credentials.spec.ts
+++ b/apps/api/src/app/subscribers/usecases/delete-subscriber-credentials/delete-subscriber-credentials.spec.ts
@@ -6,10 +6,8 @@ import { SharedModule } from '../../../shared/shared.module';
import { ChatProviderIdEnum, PushProviderIdEnum } from '@novu/shared';
import { DeleteSubscriberCredentials } from './delete-subscriber-credentials.usecase';
import { DeleteSubscriberCredentialsCommand } from './delete-subscriber-credentials.command';
-import { UpdateSubscriberChannel } from '../update-subscriber-channel/update-subscriber-channel.usecase';
-import { UpdateSubscriberChannelCommand } from '../update-subscriber-channel/update-subscriber-channel.command';
-import { OAuthHandlerEnum } from '../../types';
import { GetSubscriber } from '../get-subscriber/get-subscriber.usecase';
+import { OAuthHandlerEnum, UpdateSubscriberChannel, UpdateSubscriberChannelCommand } from '@novu/application-generic';
describe('Delete subscriber provider credentials', function () {
let updateSubscriberChannelUsecase: UpdateSubscriberChannel;
diff --git a/apps/api/src/app/subscribers/usecases/index.ts b/apps/api/src/app/subscribers/usecases/index.ts
index 7569c1263fc..3d8aa6097d5 100644
--- a/apps/api/src/app/subscribers/usecases/index.ts
+++ b/apps/api/src/app/subscribers/usecases/index.ts
@@ -4,6 +4,7 @@ import {
UpdateSubscriber,
CreateSubscriber,
GetSubscriberGlobalPreference,
+ UpdateSubscriberChannel,
} from '@novu/application-generic';
import { GetSubscribers } from './get-subscribers';
@@ -12,7 +13,6 @@ import { GetPreferencesByLevel } from './get-preferences-by-level/get-preference
import { RemoveSubscriber } from './remove-subscriber';
import { SearchByExternalSubscriberIds } from './search-by-external-subscriber-ids';
import { UpdatePreference } from './update-preference/update-preference.usecase';
-import { UpdateSubscriberChannel } from './update-subscriber-channel';
import { UpdateSubscriberPreference } from './update-subscriber-preference';
import { UpdateSubscriberOnlineFlag } from './update-subscriber-online-flag';
import { ChatOauth } from './chat-oauth/chat-oauth.usecase';
diff --git a/apps/api/src/app/subscribers/usecases/update-subscriber-channel/update-subscriber-channel.command.ts b/apps/api/src/app/subscribers/usecases/update-subscriber-channel/update-subscriber-channel.command.ts
deleted file mode 100644
index 73dbb2ed04f..00000000000
--- a/apps/api/src/app/subscribers/usecases/update-subscriber-channel/update-subscriber-channel.command.ts
+++ /dev/null
@@ -1,39 +0,0 @@
-import { IsBoolean, IsNotEmpty, IsOptional, IsString, ValidateNested } from 'class-validator';
-import { EnvironmentCommand } from '../../../shared/commands/project.command';
-import { ChatProviderIdEnum, PushProviderIdEnum } from '@novu/shared';
-import { ChannelCredentials, SubscriberChannel } from '../../../shared/dtos/subscriber-channel';
-import { OAuthHandlerEnum } from '../../types';
-
-export class IChannelCredentialsCommand implements ChannelCredentials {
- @IsString()
- @IsOptional()
- webhookUrl?: string;
-
- @IsString()
- @IsOptional()
- channel?: string;
-
- @IsString({ each: true })
- @IsOptional()
- deviceTokens?: string[];
-}
-
-export class UpdateSubscriberChannelCommand extends EnvironmentCommand implements SubscriberChannel {
- @IsString()
- subscriberId: string;
-
- providerId: ChatProviderIdEnum | PushProviderIdEnum;
-
- @ValidateNested()
- credentials: IChannelCredentialsCommand;
-
- @IsNotEmpty()
- oauthHandler: OAuthHandlerEnum;
-
- @IsOptional()
- @IsString()
- integrationIdentifier?: string;
-
- @IsBoolean()
- isIdempotentOperation: boolean;
-}
diff --git a/apps/worker/src/app/shared/shared.module.ts b/apps/worker/src/app/shared/shared.module.ts
index 8229158fe4f..7905076ec05 100644
--- a/apps/worker/src/app/shared/shared.module.ts
+++ b/apps/worker/src/app/shared/shared.module.ts
@@ -49,6 +49,7 @@ import {
StorageHelperService,
storageService,
UpdateSubscriber,
+ UpdateSubscriberChannel,
UpdateTenant,
} from '@novu/application-generic';
@@ -114,6 +115,7 @@ const PROVIDERS = [
StorageHelperService,
storageService,
UpdateSubscriber,
+ UpdateSubscriberChannel,
UpdateTenant,
GetTenant,
CreateTenant,
diff --git a/libs/shared/src/types/subscriber/index.ts b/libs/shared/src/types/subscriber/index.ts
index 3cfe79ceed4..530b77faa21 100644
--- a/libs/shared/src/types/subscriber/index.ts
+++ b/libs/shared/src/types/subscriber/index.ts
@@ -1,10 +1,18 @@
import { CustomDataType } from '../shared';
+import { ChatProviderIdEnum, PushProviderIdEnum } from '../../consts';
+import { IChannelCredentials } from '../../entities/subscriber';
export type ExternalSubscriberId = string;
export type SubscriberId = string;
export type SubscriberCustomData = CustomDataType;
+export interface ISubscriberChannel {
+ providerId: ChatProviderIdEnum | PushProviderIdEnum;
+ integrationIdentifier?: string;
+ credentials: IChannelCredentials;
+}
+
export interface ISubscriberPayload {
firstName?: string;
lastName?: string;
@@ -13,6 +21,7 @@ export interface ISubscriberPayload {
avatar?: string;
locale?: string;
data?: SubscriberCustomData;
+ channels?: ISubscriberChannel[];
}
export interface ISubscribersDefine extends ISubscriberPayload {
diff --git a/packages/application-generic/src/usecases/create-subscriber/create-subscriber.command.ts b/packages/application-generic/src/usecases/create-subscriber/create-subscriber.command.ts
index e635dbff911..9bbbd0950c6 100644
--- a/packages/application-generic/src/usecases/create-subscriber/create-subscriber.command.ts
+++ b/packages/application-generic/src/usecases/create-subscriber/create-subscriber.command.ts
@@ -7,7 +7,7 @@ import {
} from 'class-validator';
import { Transform } from 'class-transformer';
import { SubscriberEntity } from '@novu/dal';
-import { SubscriberCustomData } from '@novu/shared';
+import { ISubscriberChannel, SubscriberCustomData } from '@novu/shared';
import { EnvironmentCommand } from '../../commands/project.command';
@@ -46,4 +46,7 @@ export class CreateSubscriberCommand extends EnvironmentCommand {
@IsOptional()
subscriber?: SubscriberEntity;
+
+ @IsOptional()
+ channels?: ISubscriberChannel[];
}
diff --git a/packages/application-generic/src/usecases/create-subscriber/create-subscriber.usecase.ts b/packages/application-generic/src/usecases/create-subscriber/create-subscriber.usecase.ts
index a49078fd0ac..513cfbd1385 100644
--- a/packages/application-generic/src/usecases/create-subscriber/create-subscriber.usecase.ts
+++ b/packages/application-generic/src/usecases/create-subscriber/create-subscriber.usecase.ts
@@ -1,4 +1,4 @@
-import { Injectable, Logger } from '@nestjs/common';
+import { forwardRef, Inject, Injectable, Logger } from '@nestjs/common';
import { SubscriberRepository } from '@novu/dal';
import { SubscriberEntity, ErrorCodesEnum } from '@novu/dal';
@@ -12,13 +12,19 @@ import {
UpdateSubscriber,
UpdateSubscriberCommand,
} from '../update-subscriber';
+import {
+ OAuthHandlerEnum,
+ UpdateSubscriberChannel,
+ UpdateSubscriberChannelCommand,
+} from '../subscribers';
@Injectable()
export class CreateSubscriber {
constructor(
private invalidateCache: InvalidateCacheService,
private subscriberRepository: SubscriberRepository,
- private updateSubscriber: UpdateSubscriber
+ private updateSubscriber: UpdateSubscriber,
+ private updateSubscriberChannel: UpdateSubscriberChannel
) {}
async execute(command: CreateSubscriberCommand) {
@@ -31,6 +37,10 @@ export class CreateSubscriber {
if (!subscriber) {
subscriber = await this.createSubscriber(command);
+
+ if (command.channels?.length) {
+ await this.updateCredentials(command);
+ }
} else {
subscriber = await this.updateSubscriber.execute(
UpdateSubscriberCommand.create({
@@ -45,6 +55,7 @@ export class CreateSubscriber {
locale: command.locale,
data: command.data,
subscriber,
+ channels: command.channels,
})
);
}
@@ -52,6 +63,23 @@ export class CreateSubscriber {
return subscriber;
}
+ private async updateCredentials(command: CreateSubscriberCommand) {
+ for (const channel of command.channels) {
+ await this.updateSubscriberChannel.execute(
+ UpdateSubscriberChannelCommand.create({
+ organizationId: command.organizationId,
+ environmentId: command.environmentId,
+ subscriberId: command.subscriberId,
+ providerId: channel.providerId,
+ credentials: channel.credentials,
+ integrationIdentifier: channel.integrationIdentifier,
+ oauthHandler: OAuthHandlerEnum.EXTERNAL,
+ isIdempotentOperation: false,
+ })
+ );
+ }
+ }
+
private async createSubscriber(command: CreateSubscriberCommand) {
try {
await this.invalidateCache.invalidateByKey({
diff --git a/packages/application-generic/src/usecases/index.ts b/packages/application-generic/src/usecases/index.ts
index 9866e217fb7..6e0adabfa09 100644
--- a/packages/application-generic/src/usecases/index.ts
+++ b/packages/application-generic/src/usecases/index.ts
@@ -39,3 +39,4 @@ export * from './compile-step-template';
export * from './create-workflow';
export * from './workflow';
export * from './message-template';
+export * from './subscribers';
diff --git a/packages/application-generic/src/usecases/process-subscriber/process-subscriber.usecase.ts b/packages/application-generic/src/usecases/process-subscriber/process-subscriber.usecase.ts
index e9148501f7a..825b63a6df6 100644
--- a/packages/application-generic/src/usecases/process-subscriber/process-subscriber.usecase.ts
+++ b/packages/application-generic/src/usecases/process-subscriber/process-subscriber.usecase.ts
@@ -7,7 +7,6 @@ import {
CreateSubscriberCommand,
} from '../create-subscriber';
import { InstrumentUsecase } from '../../instrumentation';
-import { subscriberNeedUpdate } from '../../utils/subscriber';
import { ProcessSubscriberCommand } from './process-subscriber.command';
import { buildSubscriberKey, CachedEntity } from '../../services/cache';
@@ -47,10 +46,6 @@ export class ProcessSubscriber {
subscriberId: subscriberPayload.subscriberId,
});
- if (subscriber && !subscriberNeedUpdate(subscriber, subscriberPayload)) {
- return subscriber;
- }
-
return await this.createOrUpdateSubscriber(
environmentId,
organizationId,
@@ -79,6 +74,7 @@ export class ProcessSubscriber {
locale: subscriberPayload?.locale,
subscriber: subscriber ?? undefined,
data: subscriberPayload?.data,
+ channels: subscriberPayload?.channels,
})
);
}
diff --git a/packages/application-generic/src/usecases/subscribers/index.ts b/packages/application-generic/src/usecases/subscribers/index.ts
new file mode 100644
index 00000000000..b0c8098d56b
--- /dev/null
+++ b/packages/application-generic/src/usecases/subscribers/index.ts
@@ -0,0 +1,2 @@
+export * from './update-subscriber-channel';
+export * from './types';
diff --git a/apps/api/src/app/subscribers/types/index.ts b/packages/application-generic/src/usecases/subscribers/types/index.ts
similarity index 100%
rename from apps/api/src/app/subscribers/types/index.ts
rename to packages/application-generic/src/usecases/subscribers/types/index.ts
diff --git a/apps/api/src/app/subscribers/usecases/update-subscriber-channel/index.ts b/packages/application-generic/src/usecases/subscribers/update-subscriber-channel/index.ts
similarity index 100%
rename from apps/api/src/app/subscribers/usecases/update-subscriber-channel/index.ts
rename to packages/application-generic/src/usecases/subscribers/update-subscriber-channel/index.ts
diff --git a/packages/application-generic/src/usecases/subscribers/update-subscriber-channel/update-subscriber-channel.command.ts b/packages/application-generic/src/usecases/subscribers/update-subscriber-channel/update-subscriber-channel.command.ts
new file mode 100644
index 00000000000..fca398028b4
--- /dev/null
+++ b/packages/application-generic/src/usecases/subscribers/update-subscriber-channel/update-subscriber-channel.command.ts
@@ -0,0 +1,75 @@
+import {
+ IsBoolean,
+ IsNotEmpty,
+ IsOptional,
+ IsString,
+ ValidateNested,
+} from 'class-validator';
+import {
+ ChatProviderIdEnum,
+ IChannelCredentials,
+ PushProviderIdEnum,
+} from '@novu/shared';
+import { EnvironmentCommand } from '../../../commands';
+import { OAuthHandlerEnum } from '../types';
+import { SubscriberEntity } from '@novu/dal';
+
+export class SubscriberChannel {
+ providerId: ChatProviderIdEnum | PushProviderIdEnum;
+
+ credentials: IChannelCredentials;
+}
+
+export class IChannelCredentialsCommand implements IChannelCredentials {
+ @IsString()
+ @IsOptional()
+ webhookUrl?: string;
+
+ @IsString()
+ @IsOptional()
+ channel?: string;
+
+ @IsString({ each: true })
+ @IsOptional()
+ deviceTokens?: string[];
+
+ @IsOptional()
+ alertUid?: string;
+
+ @IsOptional()
+ title?: string;
+
+ @IsOptional()
+ imageUrl?: string;
+
+ @IsOptional()
+ state?: string;
+
+ @IsOptional()
+ externalUrl?: string;
+}
+
+export class UpdateSubscriberChannelCommand
+ extends EnvironmentCommand
+ implements SubscriberChannel
+{
+ @IsString()
+ subscriberId: string;
+
+ providerId: ChatProviderIdEnum | PushProviderIdEnum;
+
+ subscriber?: SubscriberEntity;
+
+ @ValidateNested()
+ credentials: IChannelCredentialsCommand;
+
+ @IsNotEmpty()
+ oauthHandler: OAuthHandlerEnum;
+
+ @IsOptional()
+ @IsString()
+ integrationIdentifier?: string;
+
+ @IsBoolean()
+ isIdempotentOperation: boolean;
+}
diff --git a/apps/api/src/app/subscribers/usecases/update-subscriber-channel/update-subscriber-channel.usecase.ts b/packages/application-generic/src/usecases/subscribers/update-subscriber-channel/update-subscriber-channel.usecase.ts
similarity index 68%
rename from apps/api/src/app/subscribers/usecases/update-subscriber-channel/update-subscriber-channel.usecase.ts
rename to packages/application-generic/src/usecases/subscribers/update-subscriber-channel/update-subscriber-channel.usecase.ts
index d5b767931e3..a754dd23c9b 100644
--- a/apps/api/src/app/subscribers/usecases/update-subscriber-channel/update-subscriber-channel.usecase.ts
+++ b/packages/application-generic/src/usecases/subscribers/update-subscriber-channel/update-subscriber-channel.usecase.ts
@@ -1,4 +1,4 @@
-import { Injectable } from '@nestjs/common';
+import { forwardRef, Inject, Injectable } from '@nestjs/common';
import { isEqual } from 'lodash';
import {
IChannelSettings,
@@ -7,25 +7,33 @@ import {
SubscriberEntity,
IntegrationEntity,
} from '@novu/dal';
-import { AnalyticsService, buildSubscriberKey, InvalidateCacheService } from '@novu/application-generic';
-import { ApiException } from '../../../shared/exceptions/api.exception';
import { UpdateSubscriberChannelCommand } from './update-subscriber-channel.command';
+import { ApiException } from '../../../utils/exceptions';
+import {
+ AnalyticsService,
+ buildSubscriberKey,
+ InvalidateCacheService,
+} from '../../../services';
@Injectable()
export class UpdateSubscriberChannel {
constructor(
+ @Inject(forwardRef(() => InvalidateCacheService))
private invalidateCache: InvalidateCacheService,
private subscriberRepository: SubscriberRepository,
private integrationRepository: IntegrationRepository,
+ @Inject(forwardRef(() => AnalyticsService))
private analyticsService: AnalyticsService
) {}
async execute(command: UpdateSubscriberChannelCommand) {
- const foundSubscriber = await this.subscriberRepository.findBySubscriberId(
- command.environmentId,
- command.subscriberId
- );
+ const foundSubscriber =
+ command.subscriber ??
+ (await this.subscriberRepository.findBySubscriberId(
+ command.environmentId,
+ command.subscriberId
+ ));
if (!foundSubscriber) {
throw new ApiException(`SubscriberId: ${command.subscriberId} not found`);
@@ -40,9 +48,13 @@ export class UpdateSubscriberChannel {
query.identifier = command.integrationIdentifier;
}
- const foundIntegration = await this.integrationRepository.findOne(query, undefined, {
- query: { sort: { createdAt: -1 } },
- });
+ const foundIntegration = await this.integrationRepository.findOne(
+ query,
+ undefined,
+ {
+ query: { sort: { createdAt: -1 } },
+ }
+ );
if (!foundIntegration) {
throw new ApiException(
@@ -53,7 +65,8 @@ export class UpdateSubscriberChannel {
const existingChannel = foundSubscriber?.channels?.find(
(subscriberChannel) =>
- subscriberChannel.providerId === command.providerId && subscriberChannel._integrationId === foundIntegration._id
+ subscriberChannel.providerId === command.providerId &&
+ subscriberChannel._integrationId === foundIntegration._id
);
if (existingChannel) {
@@ -65,15 +78,24 @@ export class UpdateSubscriberChannel {
command.isIdempotentOperation
);
} else {
- await this.addChannelToSubscriber(updatePayload, foundIntegration, command, foundSubscriber);
+ await this.addChannelToSubscriber(
+ updatePayload,
+ foundIntegration,
+ command,
+ foundSubscriber
+ );
}
- this.analyticsService.mixpanelTrack('Set Subscriber Credentials - [Subscribers]', '', {
- providerId: command.providerId,
- _organization: command.organizationId,
- oauthHandler: command.oauthHandler,
- _subscriberId: foundSubscriber._id,
- });
+ this.analyticsService.mixpanelTrack(
+ 'Set Subscriber Credentials - [Subscribers]',
+ '',
+ {
+ providerId: command.providerId,
+ _organization: command.organizationId,
+ oauthHandler: command.oauthHandler,
+ _subscriberId: foundSubscriber._id,
+ }
+ );
return (await this.subscriberRepository.findBySubscriberId(
command.environmentId,
@@ -114,7 +136,10 @@ export class UpdateSubscriberChannel {
foundSubscriber: SubscriberEntity,
isIdempotentOperation: boolean
) {
- const equal = isEqual(existingChannel.credentials, updatePayload.credentials);
+ const equal = isEqual(
+ existingChannel.credentials,
+ updatePayload.credentials
+ );
if (equal) {
return;
@@ -124,7 +149,10 @@ export class UpdateSubscriberChannel {
if (updatePayload.credentials?.deviceTokens) {
if (isIdempotentOperation) {
- deviceTokens = this.unionDeviceTokens([], updatePayload.credentials.deviceTokens);
+ deviceTokens = this.unionDeviceTokens(
+ [],
+ updatePayload.credentials.deviceTokens
+ );
} else {
deviceTokens = this.unionDeviceTokens(
existingChannel.credentials.deviceTokens ?? [],
@@ -140,7 +168,11 @@ export class UpdateSubscriberChannel {
}),
});
- const mappedChannel: IChannelSettings = this.mapChannel(updatePayload, existingChannel, deviceTokens);
+ const mappedChannel: IChannelSettings = this.mapChannel(
+ updatePayload,
+ existingChannel,
+ deviceTokens
+ );
await this.subscriberRepository.update(
{
@@ -158,13 +190,21 @@ export class UpdateSubscriberChannel {
deviceTokens: string[]
): IChannelSettings {
return {
- _integrationId: updatePayload._integrationId || existingChannel._integrationId,
+ _integrationId:
+ updatePayload._integrationId || existingChannel._integrationId,
providerId: updatePayload.providerId || existingChannel.providerId,
- credentials: { ...existingChannel.credentials, ...updatePayload.credentials, deviceTokens },
+ credentials: {
+ ...existingChannel.credentials,
+ ...updatePayload.credentials,
+ deviceTokens,
+ },
};
}
- private unionDeviceTokens(existingDeviceTokens: string[], updateDeviceTokens: string[]): string[] {
+ private unionDeviceTokens(
+ existingDeviceTokens: string[],
+ updateDeviceTokens: string[]
+ ): string[] {
// in order to not have breaking change we will support [] update
if (updateDeviceTokens?.length === 0) return [];
@@ -180,8 +220,13 @@ export class UpdateSubscriberChannel {
if (command.credentials.webhookUrl != null && updatePayload.credentials) {
updatePayload.credentials.webhookUrl = command.credentials.webhookUrl;
}
- if (command.credentials.deviceTokens != null && updatePayload.credentials) {
- updatePayload.credentials.deviceTokens = [...new Set([...command.credentials.deviceTokens])];
+ if (
+ command.credentials.deviceTokens != null &&
+ updatePayload.credentials
+ ) {
+ updatePayload.credentials.deviceTokens = [
+ ...new Set([...command.credentials.deviceTokens]),
+ ];
}
if (command.credentials.channel != null && updatePayload.credentials) {
updatePayload.credentials.channel = command.credentials.channel;
diff --git a/packages/application-generic/src/usecases/update-subscriber/update-subscriber.command.ts b/packages/application-generic/src/usecases/update-subscriber/update-subscriber.command.ts
index 413c05e18e6..4faa17d37f3 100644
--- a/packages/application-generic/src/usecases/update-subscriber/update-subscriber.command.ts
+++ b/packages/application-generic/src/usecases/update-subscriber/update-subscriber.command.ts
@@ -1,6 +1,6 @@
import { IsEmail, IsLocale, IsOptional, IsString } from 'class-validator';
import { SubscriberEntity } from '@novu/dal';
-import { SubscriberCustomData } from '@novu/shared';
+import { ISubscriberChannel, SubscriberCustomData } from '@novu/shared';
import { Transform } from 'class-transformer';
import { EnvironmentCommand } from '../../commands';
@@ -37,4 +37,7 @@ export class UpdateSubscriberCommand extends EnvironmentCommand {
@IsOptional()
subscriber?: SubscriberEntity;
+
+ @IsOptional()
+ channels?: ISubscriberChannel[];
}
diff --git a/packages/application-generic/src/usecases/update-subscriber/update-subscriber.usecase.ts b/packages/application-generic/src/usecases/update-subscriber/update-subscriber.usecase.ts
index b55717495dc..fabb5c67879 100644
--- a/packages/application-generic/src/usecases/update-subscriber/update-subscriber.usecase.ts
+++ b/packages/application-generic/src/usecases/update-subscriber/update-subscriber.usecase.ts
@@ -1,4 +1,4 @@
-import { Injectable } from '@nestjs/common';
+import { forwardRef, Inject, Injectable } from '@nestjs/common';
import { SubscriberEntity, SubscriberRepository } from '@novu/dal';
import {
@@ -9,12 +9,18 @@ import { subscriberNeedUpdate } from '../../utils/subscriber';
import { UpdateSubscriberCommand } from './update-subscriber.command';
import { ApiException } from '../../utils/exceptions';
+import {
+ OAuthHandlerEnum,
+ UpdateSubscriberChannel,
+ UpdateSubscriberChannelCommand,
+} from '../subscribers';
@Injectable()
export class UpdateSubscriber {
constructor(
private invalidateCache: InvalidateCacheService,
- private subscriberRepository: SubscriberRepository
+ private subscriberRepository: SubscriberRepository,
+ private updateSubscriberChannel: UpdateSubscriberChannel
) {}
public async execute(
@@ -61,6 +67,10 @@ export class UpdateSubscriber {
updatePayload.data = command.data;
}
+ if (command.channels?.length) {
+ await this.updateSubscriberChannels(command, foundSubscriber);
+ }
+
if (!subscriberNeedUpdate(foundSubscriber, updatePayload)) {
return {
...foundSubscriber,
@@ -89,4 +99,25 @@ export class UpdateSubscriber {
...updatePayload,
};
}
+
+ private async updateSubscriberChannels(
+ command: UpdateSubscriberCommand,
+ foundSubscriber: SubscriberEntity
+ ) {
+ for (const channel of command.channels) {
+ await this.updateSubscriberChannel.execute(
+ UpdateSubscriberChannelCommand.create({
+ subscriber: foundSubscriber,
+ organizationId: command.organizationId,
+ environmentId: command.environmentId,
+ subscriberId: command.subscriberId,
+ providerId: channel.providerId,
+ credentials: channel.credentials,
+ integrationIdentifier: channel.integrationIdentifier,
+ oauthHandler: OAuthHandlerEnum.EXTERNAL,
+ isIdempotentOperation: false,
+ })
+ );
+ }
+ }
}
diff --git a/packages/application-generic/src/utils/subscriber.ts b/packages/application-generic/src/utils/subscriber.ts
index 947e5707335..27319b7249d 100644
--- a/packages/application-generic/src/utils/subscriber.ts
+++ b/packages/application-generic/src/utils/subscriber.ts
@@ -3,7 +3,7 @@ import { isEqual } from 'lodash';
export function subscriberNeedUpdate(
subscriber: SubscriberEntity,
- subscriberPayload: Partial
+ subscriberPayload: Partial>
): boolean {
return (
!!(