-
Notifications
You must be signed in to change notification settings - Fork 3.9k
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
Changes from 8 commits
123046e
ecfac24
27493b2
b61109d
e638dc0
af7821e
fd87c40
f5d76bb
ce4b0ba
4e7c973
b2f9974
dcc98e1
b96fc0f
File filter
Filter by extension
Conversations
Jump to
Diff view
Diff view
There are no files selected for viewing
Original file line number | Diff line number | Diff line change |
---|---|---|
|
@@ -8,8 +8,9 @@ | |
UpsertPreferences, | ||
UpsertSubscriberWorkflowPreferencesCommand, | ||
UpsertSubscriberGlobalPreferencesCommand, | ||
InstrumentUsecase, | ||
Instrument, | ||
} from '@novu/application-generic'; | ||
import { | ||
NotificationTemplateEntity, | ||
NotificationTemplateRepository, | ||
|
@@ -18,8 +19,8 @@ | |
SubscriberPreferenceEntity, | ||
SubscriberPreferenceRepository, | ||
SubscriberRepository, | ||
} from '@novu/dal'; | ||
import { IPreferenceChannels, WorkflowPreferences, WorkflowPreferencesPartial } from '@novu/shared'; | ||
import { ApiException } from '../../../shared/exceptions/api.exception'; | ||
import { AnalyticsEventsEnum } from '../../utils'; | ||
import { InboxPreference } from '../../utils/types'; | ||
|
@@ -33,11 +34,11 @@ | |
private subscriberRepository: SubscriberRepository, | ||
private analyticsService: AnalyticsService, | ||
private getSubscriberGlobalPreference: GetSubscriberGlobalPreference, | ||
private getSubscriberTemplatePreferenceUsecase: GetSubscriberTemplatePreference, | ||
private upsertPreferences: UpsertPreferences | ||
) {} | ||
|
||
@InstrumentUsecase() | ||
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`); | ||
|
@@ -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); | ||
|
@@ -67,6 +66,7 @@ | |
return await this.findPreference(command, subscriber); | ||
} | ||
|
||
@Instrument() | ||
There was a problem hiding this comment. Choose a reason for hiding this commentThe 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); | ||
|
||
|
@@ -94,6 +94,7 @@ | |
}); | ||
} | ||
|
||
@Instrument() | ||
private async updateUserPreference(command: UpdatePreferencesCommand, subscriber: SubscriberEntity): Promise<void> { | ||
const channelPreferences: IPreferenceChannels = this.buildPreferenceChannels(command); | ||
|
||
|
@@ -136,6 +137,7 @@ | |
}; | ||
} | ||
|
||
@Instrument() | ||
private async findPreference( | ||
command: UpdatePreferencesCommand, | ||
subscriber: SubscriberEntity | ||
|
@@ -146,7 +148,7 @@ | |
throw new NotFoundException(`Workflow with id: ${command.workflowId} is not found`); | ||
} | ||
|
||
const { preference } = await this.getSubscriberTemplatePreferenceUsecase.execute( | ||
GetSubscriberTemplatePreferenceCommand.create({ | ||
organizationId: command.organizationId, | ||
subscriberId: command.subscriberId, | ||
|
@@ -198,6 +200,7 @@ | |
/** | ||
* Strangler pattern to migrate to V2 preferences. | ||
*/ | ||
@Instrument() | ||
private async storePreferencesV2(item: { | ||
channels: IPreferenceChannels; | ||
organizationId: string; | ||
|
Original file line number | Diff line number | Diff line change |
---|---|---|
@@ -1,5 +1,5 @@ | ||
import { ChannelTypeEnum } from '@novu/shared'; | ||
import { UserSession } from '@novu/testing'; | ||
import { expect } from 'chai'; | ||
|
||
import { updateGlobalPreferences } from './helpers'; | ||
|
@@ -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({}); | ||
}); | ||
There was a problem hiding this comment. Choose a reason for hiding this commentThe 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 |
||
|
||
// `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, | ||
}; | ||
|
There was a problem hiding this comment. Choose a reason for hiding this commentThe 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 |
Original file line number | Diff line number | Diff line change |
---|---|---|
|
@@ -7,6 +7,7 @@ import { | |
Get, | ||
HttpCode, | ||
HttpStatus, | ||
NotFoundException, | ||
Param, | ||
Patch, | ||
Post, | ||
|
@@ -29,6 +30,8 @@ import { | |
ApiRateLimitCostEnum, | ||
ButtonTypeEnum, | ||
ChatProviderIdEnum, | ||
IPreferenceChannels, | ||
TriggerTypeEnum, | ||
UserSessionData, | ||
} from '@novu/shared'; | ||
import { MessageEntity, PreferenceLevelEnum } from '@novu/dal'; | ||
|
@@ -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'; | ||
|
@@ -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'; | ||
|
@@ -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() | ||
|
@@ -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, | ||
|
@@ -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') | ||
|
@@ -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; | ||
There was a problem hiding this comment. Choose a reason for hiding this commentThe 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? There was a problem hiding this comment. Choose a reason for hiding this commentThe reason will be displayed to describe this comment to others. Learn more. No, this is the channel-level flag. There are 2
|
||
|
||
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() | ||
|
There was a problem hiding this comment. Choose a reason for hiding this commentThe 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.
There was a problem hiding this comment.
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
toUpsertSubscriberPreferences
as it functions as such now.