-
Notifications
You must be signed in to change notification settings - Fork 3.4k
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
feat(api): refactor of trigger event #2825
Conversation
NV-1724 Feature: refactor trigger event
Why? (Context)Trigger event calls ProcessSubscriber that inside was hosting the creation of the notification and the building of the job steps for the jobs of the notification. Felt like a confusing implementation so decided to isolate both use cases into ProcessSubscriber and CreateNotification. What?Refactor. Definition of DoneWe have a clear separation of logic for the subscriber domain operations and the notification domain operations. The code is easier to understand and any developer can understand the flow of what's happening inside of the trigger of an event. Trigger event is left unmodified functionality wise. Existing test suites pass. |
f0d1e05
to
0334c3e
Compare
@@ -5,11 +5,12 @@ import { | |||
NotificationTemplateEntity, | |||
SubscriberEntity, | |||
} from '@novu/dal'; | |||
import { ISubscribersDefine, ITopic, TriggerRecipients } from '@novu/node'; |
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.
Didn't make sense to me an interface that is shared in the monorepo to be exported from the Node.js client package.
overrides: Record<string, Record<string, unknown>>; | ||
|
||
@IsDefined() | ||
payload: any; // eslint-disable-line @typescript-eslint/no-explicit-any |
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.
I think here we would need to also make shareable the interface ITriggerPayload
so we can remove all any set for payload property. But not 100% sure if it will be right interface.
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.
yeah, I saw this couple of times when we use any
and add the eslint-disable comment, I agree we should have the type for the payload and we might assign it to Record<string, unknown>
to get rid of the eslint comment...
type ITriggerPayload = Record<string, unknown>;
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.
Soon to be fixed.
); | ||
|
||
const jobs: Omit<JobEntity, '_id'>[] = []; | ||
public async execute(command: ProcessSubscriberCommand): Promise<SubscriberEntity | undefined> { |
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.
Now this use case does just one thing, process the subscriber.
const template = await this.notificationTemplateRepository.findByTriggerIdentifier( | ||
command.environmentId, | ||
command.identifier | ||
); |
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.
Potentially we could send the template from the queued data as during ParseEventRequest we already check the template exists, but I find sensible to retrieve it again in case during the queued time for the event, the template is being somehow deleted.
if (!template) { | ||
const message = 'Notification template could not be found'; | ||
Logger.error(message, LOG_CONTEXT); | ||
throw new ApiException(message); | ||
} |
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.
Saving execution time and DB calls if we reach this scenario.
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.
The use-case TriggerEvent
is used by the TriggerHandlerQueueService
and throwing that error won't work for the case when the trigger queue events are processed, because there is no REST context anymore... so I feel like it should throw some generic runtime error and we should catch and wrap it in other places... wdyt?
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.
Not totally understand the point you explain. ApiException
is a generic runtime error. We already throw something similar in the method validateTransactionIdProperty
that validates the entry data. And the worker at the end it has its own context, though mostly of the information provided will come from the database. I am happy to discuss this privately if it is easier because I couldn't understand what you mean, sorry. 😓
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.
APIException
extends BadRequestException
which is HttpException
, but we are not in that HTTP context anymore, so I think that this is not an appropriate exception here.
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.
🤦🏻 We have a bigger problem then. We would need to change the definition of ApiException and the use in the monorepo. The are more places where it is used under the queue context. Adding to technical debt.
let actorProcessed; | ||
if (actor) { | ||
actorProcessed = await this.processSubscriber.execute( | ||
ProcessSubscriberCommand.create({ | ||
environmentId, | ||
organizationId, | ||
userId, | ||
subscriber: actor, | ||
}) | ||
); | ||
} |
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.
Before, when sending the actor, we were searching for it in the DB inside the loop when executing ProcessSubscriber
. Therefore we would be searching for the actor number of subscribers times. Not efficient. 😅
Also its processing is the same as any subscriber recipient, so that helped to simplify ProcessSubscriber
code.
); | ||
|
||
// If no subscriber makes no sense to try to create notification | ||
if (subscriberProcessed) { |
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.
Comment says it all. Before we would try to create a notification even if the subscriber didn't exist in the database.
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.
should we then return even before we enter this for loop?
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.
Do you mean instead to do:
if (!subscriberProcessed) {
continue;
}
?
RIght now I am surrounding the loop code to execute with the condition so nothing is executed if no subscriber. And the query we have to do it, to double check in case data stored in the queue and database data is not consistent.
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.
nvm, I feel like I didn't notice that the code is surrounded within the same if...
@Cached(CacheKeyPrefixEnum.NOTIFICATION_TEMPLATE) | ||
private async getNotificationTemplate({ _id, environmentId }: { _id: string; environmentId: string }) { | ||
return await this.notificationTemplateRepository.findById(_id, environmentId); |
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.
I didn't move this code at all and removed it because we are passing the template all the time to the notification creation so not needed to cache its retrieval as we only need to look for it once.
Before it made sense as we were checking in the loop for every subscriber if the template existed.
CC: @djabarovgeorge
478f534
to
d5c03b8
Compare
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.
awesome! 🚀 left some small suggestions
overrides: Record<string, Record<string, unknown>>; | ||
|
||
@IsDefined() | ||
payload: any; // eslint-disable-line @typescript-eslint/no-explicit-any |
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.
yeah, I saw this couple of times when we use any
and add the eslint-disable comment, I agree we should have the type for the payload and we might assign it to Record<string, unknown>
to get rid of the eslint comment...
type ITriggerPayload = Record<string, unknown>;
type NotificationJob = Omit<JobEntity, '_id'>; | ||
|
||
@Injectable() | ||
export class CreateNotification { |
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.
this one should be called probably CreateNotificationJobs
because it's what it returns
subscriberPayload, | ||
subscriber: SubscriberEntity | null | ||
) { | ||
// TODO: Getting rid of this null would be amazing |
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.
after we do a findBySubscriberId
we should have a check and throw if null
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.
If we throw we would break the loop. That would mean that some notifications will be processed from the trigger but not all, leading to inconsistency. Unless we set up a rollback for the full trigger, for now I think it is better to leave the current behaviour of sending to the existing subscribers the notifications, and just skip it for non existent subscribers or failed subscribers.
Something worth to bring to tech summit to see what's the best direction we want to have for best fault tolerance. 👍🏻
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.
ahhh ok sorry I was confused about this TODO comment, I think it's not possible to get rid of that null type in this function then because the subscriber is created in the createSubscriberUsecase.execute
call when it's null...
if (!template) { | ||
const message = 'Notification template could not be found'; | ||
Logger.error(message, LOG_CONTEXT); | ||
throw new ApiException(message); | ||
} |
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.
The use-case TriggerEvent
is used by the TriggerHandlerQueueService
and throwing that error won't work for the case when the trigger queue events are processed, because there is no REST context anymore... so I feel like it should throw some generic runtime error and we should catch and wrap it in other places... wdyt?
); | ||
|
||
// If no subscriber makes no sense to try to create notification | ||
if (subscriberProcessed) { |
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.
should we then return even before we enter this for loop?
6a0e315
to
3fd3df4
Compare
ba733bc
to
228b08c
Compare
What change does this PR introduce?
Refactor of trigger event.
Also moves
ISubscribersDefine
interface to@novu/shared
as it was being exported from@novu/node
. Felt weird our API had that dependency from a client package.Why was this change needed?
Trigger event calls ProcessSubscriber that inside was hosting the creation of the notification and the building of the job steps for the jobs of the notification. Felt like a confusing implementation so decided to isolate both use cases into ProcessSubscriber and CreateNotification.
Other information (Screenshots)