[go: up one dir, main page]

Skip to content
New issue

Have a question about this project? Sign up for a free GitHub account to open an issue and contact its maintainers and the community.

By clicking “Sign up for GitHub”, you agree to our terms of service and privacy statement. We’ll occasionally send you account related emails.

Already on GitHub? Sign in to your account

refactor(api): Use UpdatePreference use-case for all Subscriber Preference updates #6889

Merged
Merged
Show file tree
Hide file tree
Changes from 8 commits
Commits
File filter

Filter by extension

Filter by extension

Conversations
Failed to load comments.
Loading
Jump to
Jump to file
Failed to load files.
Loading
Diff view
Diff view
Copy link
Contributor Author
@rifont rifont Nov 8, 2024

Choose a reason for hiding this comment

The reason will be displayed to describe this comment to others. Learn more.

In a follow-up PR (to keep this diff clean), I will rename this usecase from UpdatePreferences to UpsertSubscriberPreferences as it functions as such now.

Original file line number Diff line number Diff line change
Expand Up @@ -8,8 +8,9 @@
UpsertPreferences,
UpsertSubscriberWorkflowPreferencesCommand,
UpsertSubscriberGlobalPreferencesCommand,
InstrumentUsecase,

Check warning on line 11 in apps/api/src/app/inbox/usecases/update-preferences/update-preferences.usecase.ts

View workflow job for this annotation

GitHub Actions / Spell check

Unknown word (Usecase)
Instrument,
} from '@novu/application-generic';

Check warning on line 13 in apps/api/src/app/inbox/usecases/update-preferences/update-preferences.usecase.ts

View workflow job for this annotation

GitHub Actions / Spell check

Unknown word (novu)
import {
NotificationTemplateEntity,
NotificationTemplateRepository,
Expand All @@ -18,8 +19,8 @@
SubscriberPreferenceEntity,
SubscriberPreferenceRepository,
SubscriberRepository,
} from '@novu/dal';

Check warning on line 22 in apps/api/src/app/inbox/usecases/update-preferences/update-preferences.usecase.ts

View workflow job for this annotation

GitHub Actions / Spell check

Unknown word (novu)
import { IPreferenceChannels, WorkflowPreferences, WorkflowPreferencesPartial } from '@novu/shared';

Check warning on line 23 in apps/api/src/app/inbox/usecases/update-preferences/update-preferences.usecase.ts

View workflow job for this annotation

GitHub Actions / Spell check

Unknown word (novu)
import { ApiException } from '../../../shared/exceptions/api.exception';
import { AnalyticsEventsEnum } from '../../utils';
import { InboxPreference } from '../../utils/types';
Expand All @@ -33,11 +34,11 @@
private subscriberRepository: SubscriberRepository,
private analyticsService: AnalyticsService,
private getSubscriberGlobalPreference: GetSubscriberGlobalPreference,
private getSubscriberTemplatePreferenceUsecase: GetSubscriberTemplatePreference,

Check warning on line 37 in apps/api/src/app/inbox/usecases/update-preferences/update-preferences.usecase.ts

View workflow job for this annotation

GitHub Actions / Spell check

Unknown word (Usecase)
private upsertPreferences: UpsertPreferences
) {}

@InstrumentUsecase()

Check warning on line 41 in apps/api/src/app/inbox/usecases/update-preferences/update-preferences.usecase.ts

View workflow job for this annotation

GitHub Actions / Spell check

Unknown word (Usecase)
async execute(command: UpdatePreferencesCommand): Promise<InboxPreference> {
const subscriber = await this.subscriberRepository.findBySubscriberId(command.environmentId, command.subscriberId);
if (!subscriber) throw new NotFoundException(`Subscriber with id: ${command.subscriberId} is not found`);
Expand All @@ -55,10 +56,8 @@
}
}

const userPreference: SubscriberPreferenceEntity | null = await this.subscriberPreferenceRepository.findOne(
this.commonQuery(command, subscriber)
);
if (!userPreference) {
const subscriberPreference = await this.findPreference(command, subscriber);
if (!subscriberPreference) {
await this.createUserPreference(command, subscriber);
} else {
await this.updateUserPreference(command, subscriber);
Expand All @@ -67,6 +66,7 @@
return await this.findPreference(command, subscriber);
}

@Instrument()
Copy link
Contributor Author

Choose a reason for hiding this comment

The reason will be displayed to describe this comment to others. Learn more.

Adding some more instrumentation to the update preference flow.

private async createUserPreference(command: UpdatePreferencesCommand, subscriber: SubscriberEntity): Promise<void> {
const channelPreferences: IPreferenceChannels = this.buildPreferenceChannels(command);

Expand Down Expand Up @@ -94,6 +94,7 @@
});
}

@Instrument()
private async updateUserPreference(command: UpdatePreferencesCommand, subscriber: SubscriberEntity): Promise<void> {
const channelPreferences: IPreferenceChannels = this.buildPreferenceChannels(command);

Expand Down Expand Up @@ -136,6 +137,7 @@
};
}

@Instrument()
private async findPreference(
command: UpdatePreferencesCommand,
subscriber: SubscriberEntity
Expand All @@ -146,7 +148,7 @@
throw new NotFoundException(`Workflow with id: ${command.workflowId} is not found`);
}

const { preference } = await this.getSubscriberTemplatePreferenceUsecase.execute(

Check warning on line 151 in apps/api/src/app/inbox/usecases/update-preferences/update-preferences.usecase.ts

View workflow job for this annotation

GitHub Actions / Spell check

Unknown word (Usecase)
GetSubscriberTemplatePreferenceCommand.create({
organizationId: command.organizationId,
subscriberId: command.subscriberId,
Expand Down Expand Up @@ -198,6 +200,7 @@
/**
* Strangler pattern to migrate to V2 preferences.
*/
@Instrument()
private async storePreferencesV2(item: {
channels: IPreferenceChannels;
organizationId: string;
Expand Down
Original file line number Diff line number Diff line change
@@ -1,5 +1,5 @@
import { ChannelTypeEnum } from '@novu/shared';

Check warning on line 1 in apps/api/src/app/subscribers/e2e/update-global-preference.e2e.ts

View workflow job for this annotation

GitHub Actions / Spell check

Unknown word (novu)
import { UserSession } from '@novu/testing';

Check warning on line 2 in apps/api/src/app/subscribers/e2e/update-global-preference.e2e.ts

View workflow job for this annotation

GitHub Actions / Spell check

Unknown word (novu)
import { expect } from 'chai';

import { updateGlobalPreferences } from './helpers';
Expand Down Expand Up @@ -77,7 +77,14 @@
expect(response.data.data.preference.channels).to.eql({});
});

it('should update user global preference and disable the flag for the future channels update', async function () {
it('should unset all preferences when the preferences object is empty', async function () {
const response = await updateGlobalPreferences({}, session);

expect(response.data.data.preference.channels).to.eql({});
});
Copy link
Contributor Author

Choose a reason for hiding this comment

The reason will be displayed to describe this comment to others. Learn more.

This test case asserts that empty objects can be supplied when updating global preferences in relation to this UpsertPreferences change


// `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,
};
Expand Down
8 changes: 5 additions & 3 deletions apps/api/src/app/subscribers/e2e/update-preference.e2e.ts
Original file line number Diff line number Diff line change
@@ -1,4 +1,4 @@
import { UserSession } from '@novu/testing';

Check warning on line 1 in apps/api/src/app/subscribers/e2e/update-preference.e2e.ts

View workflow job for this annotation

GitHub Actions / Spell check

Unknown word (novu)
import { expect } from 'chai';
import { NotificationTemplateEntity } from '@novu/dal';
import {
Expand Down Expand Up @@ -83,7 +83,7 @@
expect(response.status).to.eql(404);
expect(response.data).to.have.include({
statusCode: 404,
message: 'Template with id 63cc6e0b561e0a609f223e27 is not found',
message: 'Workflow with id: 63cc6e0b561e0a609f223e27 is not found',
error: 'Not Found',
});
}
Expand Down Expand Up @@ -148,7 +148,8 @@
});
});

it('should update user preference and disable the flag for the future general notification template preference', async function () {
// `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];
expect(initialPreferences.preference.enabled).to.eql(true);
expect(initialPreferences.preference.channels).to.eql({
Expand Down Expand Up @@ -186,7 +187,8 @@
});
});

it('should update user preference and enable the flag for the future general notification template preference', async function () {
// `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];
expect(initialPreferences.preference.enabled).to.eql(true);
expect(initialPreferences.preference.channels).to.eql({
Expand Down
84 changes: 58 additions & 26 deletions apps/api/src/app/subscribers/subscribers.controller.ts
Copy link
Contributor Author
@rifont rifont Nov 8, 2024

Choose a reason for hiding this comment

The reason will be displayed to describe this comment to others. Learn more.

As you will see further down, it's not ideal that we have some mapping logic in the controller level. I decided that's a suitable approach for now as we'll be performing a Subscriber API overhaul soon.

Original file line number Diff line number Diff line change
Expand Up @@ -7,6 +7,7 @@ import {
Get,
HttpCode,
HttpStatus,
NotFoundException,
Param,
Patch,
Post,
Expand All @@ -29,6 +30,8 @@ import {
ApiRateLimitCostEnum,
ButtonTypeEnum,
ChatProviderIdEnum,
IPreferenceChannels,
TriggerTypeEnum,
UserSessionData,
} from '@novu/shared';
import { MessageEntity, PreferenceLevelEnum } from '@novu/dal';
Expand All @@ -50,8 +53,6 @@ import { GetSubscribers, GetSubscribersCommand } from './usecases/get-subscriber
import { GetSubscriber, GetSubscriberCommand } from './usecases/get-subscriber';
import { GetPreferencesByLevelCommand } from './usecases/get-preferences-by-level/get-preferences-by-level.command';
import { GetPreferencesByLevel } from './usecases/get-preferences-by-level/get-preferences-by-level.usecase';
import { UpdatePreference } from './usecases/update-preference/update-preference.usecase';
import { UpdateSubscriberPreferenceCommand } from './usecases/update-subscriber-preference';
import { UpdateSubscriberPreferenceResponseDto } from '../widgets/dtos/update-subscriber-preference-response.dto';
import { UpdateSubscriberPreferenceRequestDto } from '../widgets/dtos/update-subscriber-preference-request.dto';
import { MessageResponseDto } from '../widgets/dtos/message-response.dto';
Expand Down Expand Up @@ -90,10 +91,6 @@ import { MarkAllMessagesAs } from '../widgets/usecases/mark-all-messages-as/mark
import { MarkAllMessageAsRequestDto } from './dtos/mark-all-messages-as-request.dto';
import { BulkCreateSubscribers } from './usecases/bulk-create-subscribers/bulk-create-subscribers.usecase';
import { BulkCreateSubscribersCommand } from './usecases/bulk-create-subscribers';
import {
UpdateSubscriberGlobalPreferences,
UpdateSubscriberGlobalPreferencesCommand,
} from './usecases/update-subscriber-global-preferences';
import { GetSubscriberPreferencesByLevelParams } from './params';
import { ThrottlerCategory, ThrottlerCost } from '../rate-limiting/guards';
import { MessageMarkAsRequestDto } from '../widgets/dtos/mark-as-request.dto';
Expand All @@ -102,6 +99,8 @@ import { MarkMessageAsByMark } from '../widgets/usecases/mark-message-as-by-mark
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 { UpdatePreferences } from '../inbox/usecases/update-preferences/update-preferences.usecase';
import { UpdatePreferencesCommand } from '../inbox/usecases/update-preferences/update-preferences.command';

@ThrottlerCategory(ApiRateLimitCategoryEnum.CONFIGURATION)
@ApiCommonResponses()
Expand All @@ -117,8 +116,7 @@ export class SubscribersController {
private getSubscriberUseCase: GetSubscriber,
private getSubscribersUsecase: GetSubscribers,
private getPreferenceUsecase: GetPreferencesByLevel,
private updatePreferenceUsecase: UpdatePreference,
private updateGlobalPreferenceUsecase: UpdateSubscriberGlobalPreferences,
private updatePreferencesUsecase: UpdatePreferences,
private getNotificationsFeedUsecase: GetNotificationsFeed,
private getFeedCountUsecase: GetFeedCount,
private markMessageAsUsecase: MarkMessageAs,
Expand Down Expand Up @@ -465,16 +463,37 @@ export class SubscribersController {
@Param('parameter') templateId: string,
@Body() body: UpdateSubscriberPreferenceRequestDto
): Promise<UpdateSubscriberPreferenceResponseDto> {
const command = UpdateSubscriberPreferenceCommand.create({
organizationId: user.organizationId,
subscriberId,
environmentId: user.environmentId,
templateId,
...(typeof body.enabled === 'boolean' && { enabled: body.enabled }),
...(body.channel && { channel: body.channel }),
});
const result = await this.updatePreferencesUsecase.execute(
UpdatePreferencesCommand.create({
environmentId: user.environmentId,
organizationId: user.organizationId,
subscriberId,
workflowId: templateId,
level: PreferenceLevelEnum.TEMPLATE,
...(body.channel && { [body.channel.type]: body.channel.enabled }),
})
);

return await this.updatePreferenceUsecase.execute(command);
if (!result.workflow) throw new NotFoundException('Workflow not found');

return {
preference: {
channels: result.channels,
enabled: result.enabled,
},
template: {
_id: result.workflow.id,
name: result.workflow.name,
critical: result.workflow.critical,
triggers: [
{
identifier: result.workflow.identifier,
type: TriggerTypeEnum.EVENT,
variables: [],
},
],
},
};
}

@Patch('/:subscriberId/preferences')
Expand All @@ -490,16 +509,29 @@ export class SubscribersController {
@UserSession() user: UserSessionData,
@Param('subscriberId') subscriberId: string,
@Body() body: UpdateSubscriberGlobalPreferencesRequestDto
) {
const command = UpdateSubscriberGlobalPreferencesCommand.create({
organizationId: user.organizationId,
subscriberId,
environmentId: user.environmentId,
enabled: body.enabled,
preferences: body.preferences,
});
): Promise<Omit<UpdateSubscriberPreferenceResponseDto, 'template'>> {
const channels = body.preferences?.reduce((acc, curr) => {
acc[curr.type] = curr.enabled;
Copy link
Contributor

Choose a reason for hiding this comment

The reason will be displayed to describe this comment to others. Learn more.

Is this enabled flag the same as the one we talked about in skipped tests?

Copy link
Contributor Author

Choose a reason for hiding this comment

The reason will be displayed to describe this comment to others. Learn more.

No, this is the channel-level flag. There are 2 enabled flags:

  • enabled at top level of V1 Preferences, which controls whether to use the subscriber preference during preference merging
  • enabled flag for each channel, which controls delivery for the channel


return acc;
}, {} as IPreferenceChannels);

const result = await this.updatePreferencesUsecase.execute(
UpdatePreferencesCommand.create({
environmentId: user.environmentId,
organizationId: user.organizationId,
subscriberId,
level: PreferenceLevelEnum.GLOBAL,
...channels,
})
);

return await this.updateGlobalPreferenceUsecase.execute(command);
return {
preference: {
channels: result.channels,
enabled: result.enabled,
},
};
}

@ExternalApiAccessible()
Expand Down
2 changes: 0 additions & 2 deletions apps/api/src/app/subscribers/subscribers.module.ts
Original file line number Diff line number Diff line change
@@ -1,7 +1,5 @@
import { forwardRef, Module } from '@nestjs/common';
import { TerminusModule } from '@nestjs/terminus';
import { GetPreferences, UpsertPreferences } from '@novu/application-generic';
import { PreferencesRepository } from '@novu/dal';
import { AuthModule } from '../auth/auth.module';
import { SharedModule } from '../shared/shared.module';
import { WidgetsModule } from '../widgets/widgets.module';
Expand Down
8 changes: 2 additions & 6 deletions apps/api/src/app/subscribers/usecases/index.ts
Original file line number Diff line number Diff line change
Expand Up @@ -12,17 +12,15 @@ import { GetSubscriber } from './get-subscriber';
import { GetPreferencesByLevel } from './get-preferences-by-level/get-preferences-by-level.usecase';
import { RemoveSubscriber } from './remove-subscriber';
import { SearchByExternalSubscriberIds } from './search-by-external-subscriber-ids';
import { UpdatePreference } from './update-preference/update-preference.usecase';
import { UpdateSubscriberPreference } from './update-subscriber-preference';
import { UpdateSubscriberOnlineFlag } from './update-subscriber-online-flag';
import { ChatOauth } from './chat-oauth/chat-oauth.usecase';
import { ChatOauthCallback } from './chat-oauth-callback/chat-oauth-callback.usecase';
import { DeleteSubscriberCredentials } from './delete-subscriber-credentials/delete-subscriber-credentials.usecase';
import { BulkCreateSubscribers } from './bulk-create-subscribers/bulk-create-subscribers.usecase';
import { UpdateSubscriberGlobalPreferences } from './update-subscriber-global-preferences';
import { CreateIntegration } from '../../integrations/usecases/create-integration/create-integration.usecase';
import { CheckIntegration } from '../../integrations/usecases/check-integration/check-integration.usecase';
import { CheckIntegrationEMail } from '../../integrations/usecases/check-integration/check-integration-email.usecase';
import { UpdatePreferences } from '../../inbox/usecases/update-preferences/update-preferences.usecase';

export {
SearchByExternalSubscriberIds,
Expand All @@ -38,18 +36,16 @@ export const USE_CASES = [
GetPreferencesByLevel,
RemoveSubscriber,
SearchByExternalSubscriberIds,
UpdatePreference,
UpdateSubscriber,
UpdateSubscriberChannel,
UpdateSubscriberPreference,
UpdateSubscriberOnlineFlag,
ChatOauthCallback,
ChatOauth,
DeleteSubscriberCredentials,
BulkCreateSubscribers,
UpdateSubscriberGlobalPreferences,
GetSubscriberGlobalPreference,
CreateIntegration,
CheckIntegration,
CheckIntegrationEMail,
UpdatePreferences,
];
Copy link
Contributor Author

Choose a reason for hiding this comment

The reason will be displayed to describe this comment to others. Learn more.

A bunch of deletions for the old use-cases that only updated V1 Preferences 🧹

This file was deleted.

This file was deleted.

This file was deleted.

This file was deleted.

Loading
Loading