-
Notifications
You must be signed in to change notification settings - Fork 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
Nv 2403 tenant id trigger engine integration #3994
Nv 2403 tenant id trigger engine integration #3994
Conversation
NV-2403 Tenant Id Trigger Engine Integration
What?Allow our API users to pass a Once a trigger is generated with the Why? (Context)The user should be able to utilize common tenant information provided in the creation phase of the tenant (things like tenant name, address, or any other custom data) as part of trigger process. Definition of Done
|
…to nv-2403-tenant-id-trigger-engine-integration
export enum TriggerContextTypeEnum { | ||
TENANT = 'tenant', | ||
ACTOR = '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.
I will remove the actor behviour from this PR, but just wanted to show the direction I intend to take here. It will make adding new sections of trigger objects easier and more immediate
export const ReservedVariablesMap = { | ||
[TriggerContextTypeEnum.TENANT]: [{ name: 'identifier', type: TemplateVariableTypeEnum.STRING }], | ||
[TriggerContextTypeEnum.ACTOR]: [{ name: 'subscriberId', type: TemplateVariableTypeEnum.STRING }], |
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 variables we need to be included for our 'reserved' variables to work in the context of a trigger.
_tenantId: { | ||
type: Schema.Types.ObjectId, | ||
ref: 'Tenant', | ||
}, | ||
tenantIdentifier: { | ||
type: Schema.Types.String, | ||
}, |
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 will remove the _tenantId and leave only the tenantIdentifier.
I feel it does make sense to have the tenantIdentifier as part of the job entity, also for future use when we incorporate more of the tenants logic in the future.
@@ -7,3 +7,5 @@ export type TriggerRecipient = TriggerRecipientSubscriber | ITopic; | |||
export type TriggerRecipients = TriggerRecipient[]; | |||
|
|||
export type TriggerRecipientsPayload = TriggerRecipientSubscriber | TriggerRecipients; | |||
|
|||
export type TriggerTenantContext = string | ITenantDefine; |
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.
Name help? ;)
const tenantIdentifier = command.job.tenantIdentifier; | ||
let tenant: TenantEntity | null = null; | ||
|
||
if (tenantIdentifier) { | ||
tenant = await this.tenantRepository.findOne({ | ||
_environmentId: command.environmentId, | ||
identifier: tenantIdentifier, | ||
}); | ||
if (!tenant) { | ||
await this.createExecutionDetails.execute( | ||
CreateExecutionDetailsCommand.create({ | ||
...CreateExecutionDetailsCommand.getDetailsFromJob(command.job), | ||
detail: DetailEnum.TENANT_NOT_FOUND, | ||
source: ExecutionDetailsSourceEnum.INTERNAL, | ||
status: ExecutionDetailsStatusEnum.FAILED, | ||
isTest: false, | ||
isRetry: false, | ||
raw: JSON.stringify({ | ||
tenantIdentifier: tenantIdentifier, | ||
}), | ||
}) | ||
); | ||
|
||
return; | ||
} | ||
await this.sendSelectedTenantExecution(command.job, tenant); | ||
} | ||
|
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 will extract this duplication and add to all the other channels
…to nv-2403-tenant-id-trigger-engine-integration
for (const reservedVariableType of reservedVariablesTypes) { | ||
const payload = command[reservedVariableType]; | ||
if (!payload) { | ||
invalidKeys.push(`${reservedVariableType} object`); | ||
} else { | ||
const requiredVariablesContextNames = ReservedVariablesMap[reservedVariableType].map( | ||
(variable) => variable.name | ||
); | ||
for (const variableName of requiredVariablesContextNames) { | ||
const variableNameExists = typeof payload === 'string' ? payload : payload[variableName]; | ||
if (!variableNameExists) { | ||
invalidKeys.push(`${variableName} property of ${reservedVariableType}`); | ||
} |
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.
On trigger, if missing the tenant object in trigger or missing the mandatory property 'identifier' will throw error
const [tenant, overrideLayoutId] = await Promise.all([ | ||
this.handleTenantExecution(command.job), | ||
this.getOverrideLayoutId(command), | ||
this.sendSelectedIntegrationExecution(command.job, integration), | ||
]); |
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 felt the order it will show in activity feed is not crucial
@@ -16,6 +16,7 @@ export class JobEntity { | |||
// eslint-disable-next-line @typescript-eslint/no-explicit-any | |||
overrides: Record<string, Record<string, unknown>>; | |||
step: NotificationStepEntity; | |||
tenant?: ITenantDefine; |
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.
Added so we can show it in the snippet in activity feed - so we can show the user a correct and accurate presentation of the snippet he used + from here we take the identifier for the messages compilation
if (!tenantProcessed) { | ||
Logger.warn( | ||
`Tenant with identifier ${JSON.stringify( | ||
tenant.identifier | ||
)} of organization ${command.organizationId} in transaction ${ | ||
command.transactionId | ||
} could not be processed.`, | ||
LOG_CONTEXT | ||
); | ||
} |
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 there was a problem creating/updating the tenant, we can't add to execution details at this stage. It will continue execution and in send message, if he can't find it -> will stop execution.
…igger-engine-integration
@@ -98,6 +107,28 @@ export class TriggerEvent { | |||
template | |||
); | |||
|
|||
if (tenant) { |
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.
Could this be a function?
tenant | ||
); | ||
} catch (e) { | ||
tenantEntity = 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.
Why do we do null here? will getTenant in some cases return null otherwise we could reduce this code with just do the catch with no code and skip the null check on line 38?
_environmentId: job._environmentId, | ||
identifier: tenantIdentifier, | ||
}); | ||
if (!tenant) { |
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.
Could this be a part of sendSelectedTenantExecution?
@@ -19,14 +20,20 @@ export function TriggerSnippetTabs({ trigger }: { trigger: INotificationTrigger | |||
const toValue = getSubscriberValue(subscriberVariables, (variable) => variable.value || '<REPLACE_WITH_DATA>'); | |||
const payloadValue = getPayloadValue(trigger.variables); | |||
|
|||
const reservedValue = triggerSnippetVariables.reduce((acc, variable) => { |
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 will not create a lot of rerenders?
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 Tabs
component is not memoized, so it will re-render it and all the children when TriggerSnippetTabs
will render, like it is right now. 😉
The thing that we can improve is memoize the calculations for this variable, so React won't need to do it every time on a new re-render. Actually same for tabs.
apps/api/src/app/events/usecases/parse-event-request/parse-event-request.usecase.ts
Outdated
Show resolved
Hide resolved
…igger-engine-integration
@@ -23,4 +23,7 @@ export class ParseEventRequestCommand extends EnvironmentWithUserCommand { | |||
|
|||
@IsOptional() | |||
actor?: TriggerRecipientSubscriber | null; | |||
|
|||
@IsOptional() | |||
tenant?: TriggerTenantContext | 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.
I think here we should have a class that extends this interface and do the nested validation over the fields
apps/api/src/app/events/usecases/parse-event-request/parse-event-request.usecase.ts
Outdated
Show resolved
Hide resolved
apps/api/src/app/events/usecases/parse-event-request/parse-event-request.usecase.ts
Outdated
Show resolved
Hide resolved
@@ -19,14 +20,20 @@ export function TriggerSnippetTabs({ trigger }: { trigger: INotificationTrigger | |||
const toValue = getSubscriberValue(subscriberVariables, (variable) => variable.value || '<REPLACE_WITH_DATA>'); | |||
const payloadValue = getPayloadValue(trigger.variables); | |||
|
|||
const reservedValue = triggerSnippetVariables.reduce((acc, variable) => { |
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 Tabs
component is not memoized, so it will re-render it and all the children when TriggerSnippetTabs
will render, like it is right now. 😉
The thing that we can improve is memoize the calculations for this variable, so React won't need to do it every time on a new re-render. Actually same for tabs.
libs/dal/src/repositories/notification-template/notification-template.entity.ts
Outdated
Show resolved
Hide resolved
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.
🚀
@@ -59,6 +62,7 @@ import { | |||
WebhookFilterBackoffStrategy, | |||
} from './usecases'; | |||
import { MetricQueueService } from './services/metric-queue.service'; | |||
import { ProcessTenant } from '@novu/application-generic/build/main/usecases/process-tenant'; |
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.
@ainouzgali this import slipped through the cracks. Could you please fix it in next
? 🙏🏻
What change does this PR introduce?
Allow to override tenant identifier in the context of a trigger.
I still have a lot of refactoring to do - opened this PR to allow discussion about this direction.
When using tenant in an editor(will also have autocomplete for tenant):

Trigger snippet will recognize that a tenant object needs to be included in the trigger event:


The tenant object in the trigger will add the tenant to the context of this run. Also allows to create/update the tenant, similar to what we do for subscribers.
Execution detail example:



Why was this change needed?
Other information (Screenshots)