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

Nv 2403 tenant id trigger engine integration #3994

Merged
merged 15 commits into from
Aug 23, 2023

Conversation

ainouzgali
Copy link
Contributor

@ainouzgali ainouzgali commented Aug 17, 2023

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):
Screen Shot 2023-08-17 at 16 35 53

Trigger snippet will recognize that a tenant object needs to be included in the trigger event:
Screen Shot 2023-08-21 at 13 32 37
Screen Shot 2023-08-21 at 13 32 46

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:
Screen Shot 2023-08-21 at 13 33 09
Screen Shot 2023-08-21 at 13 33 22
Screen Shot 2023-08-21 at 13 33 46

Why was this change needed?

Other information (Screenshots)

@linear
Copy link

linear bot commented Aug 17, 2023

NV-2403 Tenant Id Trigger Engine Integration

What?

Allow our API users to pass a tenantIdentifier during the trigger (the custom "Identifier/ID" our users have provided during tenant creation.)

Once a trigger is generated with the tenantIdentifier property, the tenant entity should be added to the template generation context of all channels, meaning it should be possible to use it as the variable for ex. in the content editor.

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

  • User is able to pass the tenantId on the trigger level
  • Tenant entity information is accessible and could be rendered inside an e-mail/in-app/etc… templates
  • E2E tests for template compilation

…to nv-2403-tenant-id-trigger-engine-integration
@ainouzgali ainouzgali changed the base branch from next to feat-add-tenants-to-node-sdk August 17, 2023 13:34
Comment on lines 42 to 45
export enum TriggerContextTypeEnum {
TENANT = 'tenant',
ACTOR = 'actor',
}
Copy link
Contributor Author

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

Comment on lines 73 to 75
export const ReservedVariablesMap = {
[TriggerContextTypeEnum.TENANT]: [{ name: 'identifier', type: TemplateVariableTypeEnum.STRING }],
[TriggerContextTypeEnum.ACTOR]: [{ name: 'subscriberId', type: TemplateVariableTypeEnum.STRING }],
Copy link
Contributor Author

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.

Comment on lines 119 to 125
_tenantId: {
type: Schema.Types.ObjectId,
ref: 'Tenant',
},
tenantIdentifier: {
type: Schema.Types.String,
},
Copy link
Contributor Author

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;
Copy link
Contributor Author

Choose a reason for hiding this comment

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

Name help? ;)

Comment on lines 76 to 103
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);
}

Copy link
Contributor Author

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

Comment on lines 172 to 184
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}`);
}
Copy link
Contributor Author

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

Comment on lines +133 to +137
const [tenant, overrideLayoutId] = await Promise.all([
this.handleTenantExecution(command.job),
this.getOverrideLayoutId(command),
this.sendSelectedIntegrationExecution(command.job, integration),
]);
Copy link
Contributor Author

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;
Copy link
Contributor Author

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

Comment on lines +120 to +129
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
);
}
Copy link
Contributor Author

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.

@ainouzgali ainouzgali marked this pull request as ready for review August 21, 2023 10:30
@@ -98,6 +107,28 @@ export class TriggerEvent {
template
);

if (tenant) {
Copy link
Contributor

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;
Copy link
Contributor

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) {
Copy link
Contributor

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) => {
Copy link
Contributor

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?

Copy link
Contributor

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.

@@ -23,4 +23,7 @@ export class ParseEventRequestCommand extends EnvironmentWithUserCommand {

@IsOptional()
actor?: TriggerRecipientSubscriber | null;

@IsOptional()
tenant?: TriggerTenantContext | null;
Copy link
Contributor

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

@@ -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) => {
Copy link
Contributor

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.

@ainouzgali ainouzgali requested a review from LetItRock August 22, 2023 12:37
Base automatically changed from feat-add-tenants-to-node-sdk to next August 22, 2023 12:39
Copy link
Contributor

@LetItRock LetItRock left a comment

Choose a reason for hiding this comment

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

🚀

@ainouzgali ainouzgali added this pull request to the merge queue Aug 23, 2023
Merged via the queue into next with commit 16603e3 Aug 23, 2023
@ainouzgali ainouzgali deleted the nv-2403-tenant-id-trigger-engine-integration branch August 23, 2023 07:00
@@ -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';
Copy link
Contributor

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? 🙏🏻

Sign up for free to join this conversation on GitHub. Already have an account? Sign in to comment
Projects
None yet
Development

Successfully merging this pull request may close these issues.

4 participants