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
Merged
Show file tree
Hide file tree
Changes from 3 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
25 changes: 23 additions & 2 deletions apps/api/src/app/events/dtos/trigger-event-request.dto.ts
Original file line number Diff line number Diff line change
@@ -1,9 +1,17 @@
import { ArrayMaxSize, ArrayNotEmpty, IsArray, IsDefined, IsObject, IsOptional, IsString } from 'class-validator';
import { ApiExtraModels, ApiProperty, ApiPropertyOptional, getSchemaPath } from '@nestjs/swagger';
import { TopicKey, TriggerRecipientSubscriber, TriggerRecipients, TriggerRecipientsTypeEnum } from '@novu/shared';
import { CreateSubscriberRequestDto } from '../../subscribers/dtos/create-subscriber-request.dto';
import {
TopicKey,
TriggerRecipientSubscriber,
TriggerRecipients,
TriggerRecipientsTypeEnum,
TriggerTenantContext,
} from '@novu/shared';
import { CreateSubscriberRequestDto } from '../../subscribers/dtos';
import { CreateTenantRequestDto } from '../../tenant/dtos';

export class SubscriberPayloadDto extends CreateSubscriberRequestDto {}
export class TenantPayloadDto extends CreateTenantRequestDto {}

export class TopicPayloadDto {
@ApiProperty()
Expand All @@ -14,6 +22,7 @@ export class TopicPayloadDto {
}

@ApiExtraModels(SubscriberPayloadDto)
@ApiExtraModels(TenantPayloadDto)
@ApiExtraModels(TopicPayloadDto)
export class TriggerEventRequestDto {
@ApiProperty({
Expand Down Expand Up @@ -93,6 +102,18 @@ export class TriggerEventRequestDto {
})
@IsOptional()
actor?: TriggerRecipientSubscriber;

@ApiProperty({
description: `It is used to specify a tenant context during trigger event.
If a new tenant object is provided, we will create a new tenant in your system
`,
oneOf: [
{ type: 'string', description: 'Unique identifier of a tenant in your system' },
{ $ref: getSchemaPath(TenantPayloadDto) },
],
})
@IsOptional()
tenant?: TriggerTenantContext;
}

export class BulkTriggerEventDto {
Expand Down
16 changes: 14 additions & 2 deletions apps/api/src/app/events/dtos/trigger-event-to-all-request.dto.ts
Original file line number Diff line number Diff line change
@@ -1,8 +1,8 @@
import { IsDefined, IsObject, IsOptional, IsString } from 'class-validator';
import { ApiProperty, ApiPropertyOptional, getSchemaPath } from '@nestjs/swagger';
import { TriggerRecipientSubscriber } from '@novu/shared';
import { TriggerRecipientSubscriber, TriggerTenantContext } from '@novu/shared';

import { SubscriberPayloadDto } from './trigger-event-request.dto';
import { SubscriberPayloadDto, TenantPayloadDto } from './trigger-event-request.dto';

export class TriggerEventToAllRequestDto {
@ApiProperty({
Expand Down Expand Up @@ -58,4 +58,16 @@ export class TriggerEventToAllRequestDto {
})
@IsOptional()
actor?: TriggerRecipientSubscriber;

@ApiProperty({
description: `It is used to specify a tenant context during trigger event.
If a new tenant object is provided, we will create a new tenant in your system
`,
oneOf: [
{ type: 'string', description: 'Unique identifier of a tenant in your system' },
{ $ref: getSchemaPath(TenantPayloadDto) },
],
})
@IsOptional()
tenant?: TriggerTenantContext;
}
21 changes: 20 additions & 1 deletion apps/api/src/app/events/events.controller.ts
Original file line number Diff line number Diff line change
@@ -1,7 +1,13 @@
import { Body, Controller, Delete, Param, Post, Scope, UseGuards } from '@nestjs/common';
import { ApiOkResponse, ApiExcludeEndpoint, ApiOperation, ApiTags } from '@nestjs/swagger';
import { v4 as uuidv4 } from 'uuid';
import { IJwtPayload, ISubscribersDefine, TriggerRecipientSubscriber } from '@novu/shared';
import {
IJwtPayload,
ISubscribersDefine,
ITenantDefine,
TriggerRecipientSubscriber,
TriggerTenantContext,
} from '@novu/shared';
import { SendTestEmail, SendTestEmailCommand } from '@novu/application-generic';

import {
Expand Down Expand Up @@ -63,6 +69,7 @@ export class EventsController {
overrides: body.overrides || {},
to: body.to,
actor: body.actor,
tenant: body.tenant,
transactionId: body.transactionId,
})
);
Expand Down Expand Up @@ -110,6 +117,7 @@ export class EventsController {
): Promise<TriggerEventResponseDto> {
const transactionId = body.transactionId || uuidv4();
const mappedActor = body.actor ? this.mapActor(body.actor) : null;
const mappedTenant = body.tenant ? this.mapTenant(body.tenant) : null;

return this.triggerEventToAll.execute(
TriggerEventToAllCommand.create({
Expand All @@ -118,6 +126,7 @@ export class EventsController {
organizationId: user.organizationId,
identifier: body.name,
payload: body.payload,
tenant: mappedTenant,
transactionId,
overrides: body.overrides || {},
actor: mappedActor,
Expand Down Expand Up @@ -177,4 +186,14 @@ export class EventsController {

return this.mapTriggerRecipients.mapSubscriber(actor);
}

private mapTenant(tenant?: TriggerTenantContext | null): ITenantDefine | null {
if (!tenant) return null;

if (typeof tenant === 'string') {
return { identifier: tenant };
}

return tenant;
}
}
2 changes: 2 additions & 0 deletions apps/api/src/app/events/events.module.ts
Original file line number Diff line number Diff line change
Expand Up @@ -23,6 +23,7 @@ import { IntegrationModule } from '../integrations/integrations.module';
import { ExecutionDetailsModule } from '../execution-details/execution-details.module';
import { TopicsModule } from '../topics/topics.module';
import { LayoutsModule } from '../layouts/layouts.module';
import { TenantModule } from '../tenant/tenant.module';

@Module({
imports: [
Expand All @@ -37,6 +38,7 @@ import { LayoutsModule } from '../layouts/layouts.module';
ExecutionDetailsModule,
TopicsModule,
LayoutsModule,
TenantModule,
],
controllers: [EventsController],
providers: [
Expand Down
2 changes: 2 additions & 0 deletions apps/api/src/app/events/usecases/index.ts
Original file line number Diff line number Diff line change
Expand Up @@ -10,6 +10,7 @@ import {
ProcessSubscriber,
CreateNotificationJobs,
StoreSubscriberJobs,
ProcessTenant,
} from '@novu/application-generic';

import { CancelDelayed } from './cancel-delayed';
Expand Down Expand Up @@ -37,4 +38,5 @@ export const USE_CASES = [
ParseEventRequest,
ProcessBulkTrigger,
StoreSubscriberJobs,
ProcessTenant,
];
Original file line number Diff line number Diff line change
@@ -1,5 +1,5 @@
import { IsDefined, IsString, IsOptional } from 'class-validator';
import { ISubscribersDefine, TriggerRecipients, TriggerRecipientSubscriber } from '@novu/shared';
import { TriggerRecipients, TriggerRecipientSubscriber, TriggerTenantContext } from '@novu/shared';

import { EnvironmentWithUserCommand } from '../../../shared/commands/project.command';

Expand All @@ -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

}
Original file line number Diff line number Diff line change
@@ -1,5 +1,5 @@
import { IsDefined, IsObject, IsOptional, IsString } from 'class-validator';
import { ISubscribersDefine } from '@novu/shared';
import { ISubscribersDefine, ITenantDefine } from '@novu/shared';

import { EnvironmentWithUserCommand } from '../../../shared/commands/project.command';

Expand All @@ -21,4 +21,7 @@ export class TriggerEventToAllCommand extends EnvironmentWithUserCommand {

@IsOptional()
actor?: ISubscribersDefine | null;

@IsOptional()
tenant?: ITenantDefine | null;
}
40 changes: 34 additions & 6 deletions apps/api/src/app/shared/helpers/content.service.ts
Original file line number Diff line number Diff line change
Expand Up @@ -8,6 +8,10 @@ import {
DelayTypeEnum,
IFieldFilterPart,
FilterPartTypeEnum,
TriggerReservedVariables,
ReservedVariablesMap,
TriggerContextTypeEnum,
ITriggerSnippetVariable,
} from '@novu/shared';
import Handlebars from 'handlebars';
import { ApiException } from '../exceptions/api.exception';
Expand Down Expand Up @@ -37,21 +41,37 @@ export class ContentService {
}
}

extractMessageVariables(messages: INotificationTemplateStep[]): IMustacheVariable[] {
extractMessageVariables(messages: INotificationTemplateStep[]): {
variables: IMustacheVariable[];
snippetVariables: ITriggerSnippetVariable[];
} {
const variables: IMustacheVariable[] = [];
const snippetVariables: ITriggerSnippetVariable[] = [];

for (const text of this.messagesTextIterator(messages)) {
const extractedVariables = this.extractVariables(text);

const varArray = extractedVariables
.filter((item) => this.isReservedVariable(item.name))
.map((item) => this.getVariableNamePrefix(item.name));

const contextTypes = Array.from(new Set(varArray)) as TriggerContextTypeEnum[];
contextTypes.forEach((variable) => {
snippetVariables.push({ type: variable, variables: ReservedVariablesMap[variable] });
});
variables.push(...extractedVariables);
}

variables.push(...this.extractStepVariables(messages));

return [
...new Map(
variables.filter((item) => !this.isSystemVariable(item.name)).map((item) => [item.name, item])
).values(),
];
return {
variables: [
...new Map(
variables.filter((item) => !this.isSystemVariable(item.name)).map((item) => [item.name, item])
).values(),
],
snippetVariables,
};
}

extractStepVariables(messages: INotificationTemplateStep[]): IMustacheVariable[] {
Expand Down Expand Up @@ -134,6 +154,14 @@ export class ContentService {
return TemplateSystemVariables.includes(variableName.includes('.') ? variableName.split('.')[0] : variableName);
}

private getVariableNamePrefix(variableName: string): string {
return variableName.includes('.') ? variableName.split('.')[0] : variableName;
}

private isReservedVariable(variableName: string): boolean {
return TriggerReservedVariables.includes(this.getVariableNamePrefix(variableName));
}

private escapeForRegExp(content: string) {
return content.replace(/[.*+?^${}()|[\]\\]/g, '\\$&'); // $& means the whole matched string
}
Expand Down
14 changes: 8 additions & 6 deletions apps/api/src/app/tenant/tenant.controller.ts
Original file line number Diff line number Diff line change
Expand Up @@ -23,19 +23,21 @@ import {
} from '@nestjs/swagger';

import { IJwtPayload } from '@novu/shared';
import {
UpdateTenant,
UpdateTenantCommand,
GetTenant,
GetTenantCommand,
CreateTenant,
CreateTenantCommand,
} from '@novu/application-generic';

import { JwtAuthGuard } from '../auth/framework/auth.guard';
import { UserSession } from '../shared/framework/user.decorator';
import { CreateTenant } from './usecases/create-tenant/create-tenant.usecase';
import { CreateTenantCommand } from './usecases/create-tenant/create-tenant.command';
import { ExternalApiAccessible } from '../auth/framework/external-api.decorator';
import { ApiResponse } from '../shared/framework/response.decorator';
import { GetTenant } from './usecases/get-tenant/get-tenant.usecase';
import { GetTenantCommand } from './usecases/get-tenant/get-tenant.command';
import { UpdateTenant } from './usecases/update-tenant/update-tenant.usecase';
import { DeleteTenantCommand } from './usecases/delete-tenant/delete-tenant.command';
import { DeleteTenant } from './usecases/delete-tenant/delete-tenant.usecase';
import { UpdateTenantCommand } from './usecases/update-tenant/update-tenant.command';
import { ApiOkPaginatedResponse } from '../shared/framework/paginated-ok-response.decorator';
import { PaginatedResponseDto } from '../shared/dtos/pagination-response';
import { GetTenants } from './usecases/get-tenants/get-tenants.usecase';
Expand Down
Original file line number Diff line number Diff line change
@@ -1,11 +1,10 @@
import { Injectable, NotFoundException } from '@nestjs/common';
import { Injectable } from '@nestjs/common';

import { GetTenantCommand, GetTenant } from '@novu/application-generic';
import { TenantRepository, DalException } from '@novu/dal';

import { DeleteTenantCommand } from './delete-tenant.command';
import { ApiException } from '../../../shared/exceptions/api.exception';
import { GetTenantCommand } from '../get-tenant/get-tenant.command';
import { GetTenant } from '../get-tenant/get-tenant.usecase';

@Injectable()
export class DeleteTenant {
Expand Down

This file was deleted.

4 changes: 1 addition & 3 deletions apps/api/src/app/tenant/usecases/index.ts
Original file line number Diff line number Diff line change
@@ -1,6 +1,4 @@
import { CreateTenant } from './create-tenant/create-tenant.usecase';
import { GetTenant } from './get-tenant/get-tenant.usecase';
import { UpdateTenant } from './update-tenant/update-tenant.usecase';
import { GetTenant, UpdateTenant, CreateTenant } from '@novu/application-generic';
import { DeleteTenant } from './delete-tenant/delete-tenant.usecase';
import { GetTenants } from './get-tenants/get-tenants.usecase';

Expand Down
Original file line number Diff line number Diff line change
Expand Up @@ -39,7 +39,7 @@ export class CreateNotificationTemplate {
const command = blueprintCommand ?? usecaseCommand;

const contentService = new ContentService();
const variables = contentService.extractMessageVariables(command.steps);
const { variables, snippetVariables } = contentService.extractMessageVariables(command.steps);
const subscriberVariables = contentService.extractSubscriberMessageVariables(command.steps);

const triggerIdentifier = `${slugify(command.name, {
Expand All @@ -61,6 +61,17 @@ export class CreateNotificationTemplate {
type: i.type,
};
}),
snippetVariables: snippetVariables.map((i) => {
return {
type: i.type,
variables: i.variables.map((variable) => {
return {
name: variable.name,
type: variable.type,
};
}),
};
}),
subscriberVariables: subscriberVariables.map((i) => {
return {
name: i,
Expand Down
Original file line number Diff line number Diff line change
Expand Up @@ -125,15 +125,26 @@ export class UpdateNotificationTemplate {
const contentService = new ContentService();
const { steps } = command;

const variables = contentService.extractMessageVariables(command.steps);

const { variables, snippetVariables } = contentService.extractMessageVariables(command.steps);
updatePayload['triggers.0.variables'] = variables.map((i) => {
return {
name: i.name,
type: i.type,
};
});

updatePayload['triggers.0.snippetVariables'] = snippetVariables.map((i) => {
return {
type: i.type,
variables: i.variables.map((variable) => {
return {
name: variable.name,
type: variable.type,
};
}),
};
});

const subscribersVariables = contentService.extractSubscriberMessageVariables(command.steps);

updatePayload['triggers.0.subscriberVariables'] = subscribersVariables.map((i) => {
Expand Down
Loading