diff --git a/apps/api/src/app/events/dtos/trigger-event-request.dto.ts b/apps/api/src/app/events/dtos/trigger-event-request.dto.ts index 1ee27c7a8db..4488d4fd22a 100644 --- a/apps/api/src/app/events/dtos/trigger-event-request.dto.ts +++ b/apps/api/src/app/events/dtos/trigger-event-request.dto.ts @@ -1,9 +1,28 @@ -import { ArrayMaxSize, ArrayNotEmpty, IsArray, IsDefined, IsObject, IsOptional, IsString } from 'class-validator'; +import { + ArrayMaxSize, + ArrayNotEmpty, + IsArray, + IsDefined, + IsObject, + IsOptional, + IsString, + ValidateIf, + ValidateNested, +} from 'class-validator'; +import { Type } from 'class-transformer'; 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 { UpdateTenantRequestDto } from '../../tenant/dtos'; export class SubscriberPayloadDto extends CreateSubscriberRequestDto {} +export class TenantPayloadDto extends UpdateTenantRequestDto {} export class TopicPayloadDto { @ApiProperty() @@ -14,6 +33,7 @@ export class TopicPayloadDto { } @ApiExtraModels(SubscriberPayloadDto) +@ApiExtraModels(TenantPayloadDto) @ApiExtraModels(TopicPayloadDto) export class TriggerEventRequestDto { @ApiProperty({ @@ -93,6 +113,21 @@ 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. + `, + oneOf: [ + { type: 'string', description: 'Unique identifier of a tenant in your system' }, + { $ref: getSchemaPath(TenantPayloadDto) }, + ], + }) + @IsOptional() + @ValidateIf((_, value) => typeof value !== 'string') + @ValidateNested() + @Type(() => TenantPayloadDto) + tenant?: TriggerTenantContext; } export class BulkTriggerEventDto { diff --git a/apps/api/src/app/events/dtos/trigger-event-to-all-request.dto.ts b/apps/api/src/app/events/dtos/trigger-event-to-all-request.dto.ts index 3fb713866bc..ab518c77753 100644 --- a/apps/api/src/app/events/dtos/trigger-event-to-all-request.dto.ts +++ b/apps/api/src/app/events/dtos/trigger-event-to-all-request.dto.ts @@ -1,8 +1,9 @@ -import { IsDefined, IsObject, IsOptional, IsString } from 'class-validator'; +import { IsDefined, IsObject, IsOptional, IsString, ValidateIf, ValidateNested } from 'class-validator'; +import { Type } from 'class-transformer'; 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({ @@ -58,4 +59,19 @@ 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. + `, + oneOf: [ + { type: 'string', description: 'Unique identifier of a tenant in your system' }, + { $ref: getSchemaPath(TenantPayloadDto) }, + ], + }) + @IsOptional() + @ValidateIf((_, value) => typeof value !== 'string') + @ValidateNested() + @Type(() => TenantPayloadDto) + tenant?: TriggerTenantContext; } diff --git a/apps/api/src/app/events/events.controller.ts b/apps/api/src/app/events/events.controller.ts index 2424d005039..9e60084e8a9 100644 --- a/apps/api/src/app/events/events.controller.ts +++ b/apps/api/src/app/events/events.controller.ts @@ -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 { @@ -53,6 +59,8 @@ export class EventsController { @UserSession() user: IJwtPayload, @Body() body: TriggerEventRequestDto ): Promise { + const mappedTenant = body.tenant ? this.mapTenant(body.tenant) : null; + const result = await this.parseEventRequest.execute( ParseEventRequestCommand.create({ userId: user._id, @@ -63,6 +71,7 @@ export class EventsController { overrides: body.overrides || {}, to: body.to, actor: body.actor, + tenant: mappedTenant, transactionId: body.transactionId, }) ); @@ -110,6 +119,7 @@ export class EventsController { ): Promise { 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({ @@ -118,6 +128,7 @@ export class EventsController { organizationId: user.organizationId, identifier: body.name, payload: body.payload, + tenant: mappedTenant, transactionId, overrides: body.overrides || {}, actor: mappedActor, @@ -177,4 +188,10 @@ export class EventsController { return this.mapTriggerRecipients.mapSubscriber(actor); } + + private mapTenant(tenant?: TriggerTenantContext | null): ITenantDefine | null { + if (!tenant) return null; + + return this.parseEventRequest.mapTenant(tenant); + } } diff --git a/apps/api/src/app/events/events.module.ts b/apps/api/src/app/events/events.module.ts index 364bc19326f..662c1e9109c 100644 --- a/apps/api/src/app/events/events.module.ts +++ b/apps/api/src/app/events/events.module.ts @@ -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: [ @@ -37,6 +38,7 @@ import { LayoutsModule } from '../layouts/layouts.module'; ExecutionDetailsModule, TopicsModule, LayoutsModule, + TenantModule, ], controllers: [EventsController], providers: [ diff --git a/apps/api/src/app/events/usecases/index.ts b/apps/api/src/app/events/usecases/index.ts index 22dd852e4cb..736db4f62b5 100644 --- a/apps/api/src/app/events/usecases/index.ts +++ b/apps/api/src/app/events/usecases/index.ts @@ -10,6 +10,7 @@ import { ProcessSubscriber, CreateNotificationJobs, StoreSubscriberJobs, + ProcessTenant, } from '@novu/application-generic'; import { CancelDelayed } from './cancel-delayed'; @@ -37,4 +38,5 @@ export const USE_CASES = [ ParseEventRequest, ProcessBulkTrigger, StoreSubscriberJobs, + ProcessTenant, ]; diff --git a/apps/api/src/app/events/usecases/parse-event-request/parse-event-request.command.ts b/apps/api/src/app/events/usecases/parse-event-request/parse-event-request.command.ts index 007adb0acaf..1ce91ea52e1 100644 --- a/apps/api/src/app/events/usecases/parse-event-request/parse-event-request.command.ts +++ b/apps/api/src/app/events/usecases/parse-event-request/parse-event-request.command.ts @@ -1,5 +1,5 @@ -import { IsDefined, IsString, IsOptional } from 'class-validator'; -import { ISubscribersDefine, TriggerRecipients, TriggerRecipientSubscriber } from '@novu/shared'; +import { IsDefined, IsString, IsOptional, ValidateNested } from 'class-validator'; +import { TriggerRecipients, TriggerRecipientSubscriber, TriggerTenantContext } from '@novu/shared'; import { EnvironmentWithUserCommand } from '../../../shared/commands/project.command'; @@ -23,4 +23,8 @@ export class ParseEventRequestCommand extends EnvironmentWithUserCommand { @IsOptional() actor?: TriggerRecipientSubscriber | null; + + @IsOptional() + @ValidateNested() + tenant?: TriggerTenantContext | null; } diff --git a/apps/api/src/app/events/usecases/parse-event-request/parse-event-request.usecase.ts b/apps/api/src/app/events/usecases/parse-event-request/parse-event-request.usecase.ts index 77998a51e06..63729368cc2 100644 --- a/apps/api/src/app/events/usecases/parse-event-request/parse-event-request.usecase.ts +++ b/apps/api/src/app/events/usecases/parse-event-request/parse-event-request.usecase.ts @@ -4,8 +4,14 @@ import * as hat from 'hat'; import { merge } from 'lodash'; import { v4 as uuidv4 } from 'uuid'; import { Instrument, InstrumentUsecase } from '@novu/application-generic'; -import { NotificationTemplateRepository } from '@novu/dal'; -import { ISubscribersDefine } from '@novu/shared'; +import { NotificationTemplateRepository, NotificationTemplateEntity } from '@novu/dal'; +import { + ISubscribersDefine, + ITenantDefine, + ReservedVariablesMap, + TriggerContextTypeEnum, + TriggerTenantContext, +} from '@novu/shared'; import { StorageHelperService, CachedEntity, buildNotificationTemplateIdentifierKey } from '@novu/application-generic'; import { ApiException } from '../../../shared/exceptions/api.exception'; @@ -55,6 +61,9 @@ export class ParseEventRequest { throw new UnprocessableEntityException('workflow_not_found'); } + const reservedVariablesTypes = this.getReservedVariablesTypes(template); + this.validateTriggerContext(command, reservedVariablesTypes); + if (!template.active) { return { acknowledged: true, @@ -142,7 +151,7 @@ export class ParseEventRequest { if (!subscriberIdExists) { throw new ApiException( - 'subscriberId under property to is not configured, please make sure all the subscriber contains subscriberId property' + 'subscriberId under property to is not configured, please make sure all subscribers contains subscriberId property' ); } } @@ -150,6 +159,34 @@ export class ParseEventRequest { return true; } + @Instrument() + private validateTriggerContext( + command: ParseEventRequestCommand, + reservedVariablesTypes: TriggerContextTypeEnum[] + ): void { + const invalidKeys: string[] = []; + + for (const reservedVariableType of reservedVariablesTypes) { + const payload = command[reservedVariableType]; + if (!payload) { + invalidKeys.push(`${reservedVariableType} object`); + continue; + } + const reservedVariableFields = ReservedVariablesMap[reservedVariableType].map((variable) => variable.name); + for (const variableName of reservedVariableFields) { + const variableNameExists = payload[variableName]; + + if (!variableNameExists) { + invalidKeys.push(`${variableName} property of ${reservedVariableType}`); + } + } + } + + if (invalidKeys.length) { + throw new ApiException(`Trigger is missing: ${invalidKeys.join(', ')}`); + } + } + private modifyAttachments(command: ParseEventRequestCommand) { command.payload.attachments = command.payload.attachments.map((attachment) => ({ ...attachment, @@ -158,4 +195,18 @@ export class ParseEventRequest { storagePath: `${command.organizationId}/${command.environmentId}/${hat()}/${attachment.name}`, })); } + + public mapTenant(tenant: TriggerTenantContext): ITenantDefine { + if (typeof tenant === 'string') { + return { identifier: tenant }; + } + + return tenant; + } + + public getReservedVariablesTypes(template: NotificationTemplateEntity): TriggerContextTypeEnum[] { + const reservedVariables = template.triggers[0].reservedVariables; + + return reservedVariables?.map((reservedVariable) => reservedVariable.type) || []; + } } diff --git a/apps/api/src/app/events/usecases/process-bulk-trigger/process-bulk-trigger.usecase.ts b/apps/api/src/app/events/usecases/process-bulk-trigger/process-bulk-trigger.usecase.ts index 3609a3e4c16..4ee9560454f 100644 --- a/apps/api/src/app/events/usecases/process-bulk-trigger/process-bulk-trigger.usecase.ts +++ b/apps/api/src/app/events/usecases/process-bulk-trigger/process-bulk-trigger.usecase.ts @@ -14,6 +14,7 @@ export class ProcessBulkTrigger { for (const event of command.events) { let result: TriggerEventResponseDto; + const mappedTenant = event.tenant ? this.parseEventRequest.mapTenant(event.tenant) : null; try { result = (await this.parseEventRequest.execute( @@ -26,6 +27,7 @@ export class ProcessBulkTrigger { overrides: event.overrides || {}, to: event.to, actor: event.actor, + tenant: mappedTenant, transactionId: event.transactionId, }) )) as unknown as TriggerEventResponseDto; diff --git a/apps/api/src/app/events/usecases/trigger-event-to-all/trigger-event-to-all.command.ts b/apps/api/src/app/events/usecases/trigger-event-to-all/trigger-event-to-all.command.ts index 5b4d08ba31c..4f6127424cf 100644 --- a/apps/api/src/app/events/usecases/trigger-event-to-all/trigger-event-to-all.command.ts +++ b/apps/api/src/app/events/usecases/trigger-event-to-all/trigger-event-to-all.command.ts @@ -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'; @@ -21,4 +21,7 @@ export class TriggerEventToAllCommand extends EnvironmentWithUserCommand { @IsOptional() actor?: ISubscribersDefine | null; + + @IsOptional() + tenant?: ITenantDefine | null; } diff --git a/apps/api/src/app/events/usecases/trigger-event-to-all/trigger-event-to-all.usecase.ts b/apps/api/src/app/events/usecases/trigger-event-to-all/trigger-event-to-all.usecase.ts index d7668498977..cf20170e8b4 100644 --- a/apps/api/src/app/events/usecases/trigger-event-to-all/trigger-event-to-all.usecase.ts +++ b/apps/api/src/app/events/usecases/trigger-event-to-all/trigger-event-to-all.usecase.ts @@ -54,6 +54,7 @@ export class TriggerEventToAll { transactionId: command.transactionId, overrides: command.overrides, actor: command.actor, + tenant: command.tenant, }) ); } diff --git a/apps/api/src/app/shared/helpers/content.service.spec.ts b/apps/api/src/app/shared/helpers/content.service.spec.ts index 938473822d3..1dd0833410d 100644 --- a/apps/api/src/app/shared/helpers/content.service.spec.ts +++ b/apps/api/src/app/shared/helpers/content.service.spec.ts @@ -1,5 +1,12 @@ import { expect } from 'chai'; -import { DelayTypeEnum, DigestTypeEnum, DigestUnitEnum, FilterPartTypeEnum, StepTypeEnum } from '@novu/shared'; +import { + DelayTypeEnum, + DigestTypeEnum, + DigestUnitEnum, + FilterPartTypeEnum, + StepTypeEnum, + TriggerContextTypeEnum, +} from '@novu/shared'; import { ContentService } from './content.service'; import { INotificationTemplateStep } from '@novu/shared'; @@ -94,7 +101,7 @@ describe('ContentService', function () { describe('extractMessageVariables', function () { it('should not extract variables', function () { const contentService = new ContentService(); - const variables = contentService.extractMessageVariables([ + const { variables } = contentService.extractMessageVariables([ { template: { type: StepTypeEnum.IN_APP, @@ -108,7 +115,7 @@ describe('ContentService', function () { it('should extract subject variables', function () { const contentService = new ContentService(); - const variables = contentService.extractMessageVariables([ + const { variables } = contentService.extractMessageVariables([ { template: { type: StepTypeEnum.EMAIL, @@ -121,6 +128,24 @@ describe('ContentService', function () { expect(variables[0].name).to.include('firstName'); }); + it('should extract reserved variables', function () { + const contentService = new ContentService(); + const { variables, reservedVariables } = contentService.extractMessageVariables([ + { + template: { + type: StepTypeEnum.EMAIL, + subject: 'Test {{firstName}} {{tenant.name}}', + content: [], + }, + }, + ]); + expect(variables.length).to.equal(1); + expect(variables[0].name).to.include('firstName'); + expect(reservedVariables.length).to.equal(1); + expect(reservedVariables[0].type).to.eq(TriggerContextTypeEnum.TENANT); + expect(reservedVariables[0].variables[0].name).to.include('identifier'); + }); + it('should add phone when SMS channel Exists', function () { const contentService = new ContentService(); const variables = contentService.extractSubscriberMessageVariables([ @@ -202,7 +227,7 @@ describe('ContentService', function () { }, ] as INotificationTemplateStep[]; - const variables = contentService.extractMessageVariables(messages); + const { variables } = contentService.extractMessageVariables(messages); const subscriberVariables = contentService.extractSubscriberMessageVariables(messages); const variablesNames = variables.map((variable) => variable.name); @@ -216,7 +241,7 @@ describe('ContentService', function () { it('should extract in-app content variables', function () { const contentService = new ContentService(); - const variables = contentService.extractMessageVariables([ + const { variables } = contentService.extractMessageVariables([ { template: { type: StepTypeEnum.IN_APP, @@ -231,7 +256,7 @@ describe('ContentService', function () { it('should extract action steps variables', function () { const contentService = new ContentService(); - const variables = contentService.extractMessageVariables([ + const { variables } = contentService.extractMessageVariables([ { template: { type: StepTypeEnum.DELAY, @@ -257,7 +282,7 @@ describe('ContentService', function () { it('should extract filter variables on payload', function () { const contentService = new ContentService(); - const variables = contentService.extractMessageVariables([ + const { variables } = contentService.extractMessageVariables([ { template: { type: StepTypeEnum.EMAIL, @@ -304,7 +329,7 @@ describe('ContentService', function () { }, }, ] as INotificationTemplateStep[]; - const extractVariables = contentService.extractMessageVariables(messages); + const { variables: extractVariables } = contentService.extractMessageVariables(messages); expect(extractVariables.length).to.equal(1); expect(extractVariables[0].name).to.include('lastName'); diff --git a/apps/api/src/app/shared/helpers/content.service.ts b/apps/api/src/app/shared/helpers/content.service.ts index 95946a1b31e..6dfcff38f89 100644 --- a/apps/api/src/app/shared/helpers/content.service.ts +++ b/apps/api/src/app/shared/helpers/content.service.ts @@ -8,6 +8,10 @@ import { DelayTypeEnum, IFieldFilterPart, FilterPartTypeEnum, + TriggerReservedVariables, + ReservedVariablesMap, + TriggerContextTypeEnum, + ITriggerReservedVariable, } from '@novu/shared'; import Handlebars from 'handlebars'; import { ApiException } from '../exceptions/api.exception'; @@ -37,21 +41,31 @@ export class ContentService { } } - extractMessageVariables(messages: INotificationTemplateStep[]): IMustacheVariable[] { + extractMessageVariables(messages: INotificationTemplateStep[]): { + variables: IMustacheVariable[]; + reservedVariables: ITriggerReservedVariable[]; + } { const variables: IMustacheVariable[] = []; + const reservedVariables: ITriggerReservedVariable[] = []; for (const text of this.messagesTextIterator(messages)) { const extractedVariables = this.extractVariables(text); + const extractedReservedVariables = this.extractReservedVariables(extractedVariables); + + reservedVariables.push(...extractedReservedVariables); 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(), + ], + reservedVariables: [...new Map(reservedVariables.map((item) => [item.type, item])).values()], + }; } extractStepVariables(messages: INotificationTemplateStep[]): IMustacheVariable[] { @@ -86,6 +100,22 @@ export class ContentService { return variables; } + extractReservedVariables(variables: IMustacheVariable[]): ITriggerReservedVariable[] { + const reservedVariables: ITriggerReservedVariable[] = []; + + const reservedVariableTypes = variables + .filter((item) => this.isReservedVariable(item.name)) + .map((item) => this.getVariableNamePrefix(item.name)); + + const triggerContextTypes = Array.from(new Set(reservedVariableTypes)) as TriggerContextTypeEnum[]; + + triggerContextTypes.forEach((variable) => { + reservedVariables.push({ type: variable, variables: ReservedVariablesMap[variable] }); + }); + + return reservedVariables; + } + extractSubscriberMessageVariables(messages: INotificationTemplateStep[]): string[] { const variables: string[] = []; @@ -131,7 +161,15 @@ export class ContentService { } private isSystemVariable(variableName: string): boolean { - return TemplateSystemVariables.includes(variableName.includes('.') ? variableName.split('.')[0] : variableName); + return TemplateSystemVariables.includes(this.getVariableNamePrefix(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) { diff --git a/apps/api/src/app/tenant/e2e/create-tenant.e2e.ts b/apps/api/src/app/tenant/e2e/create-tenant.e2e.ts index dbee741d4eb..82061ee61c0 100644 --- a/apps/api/src/app/tenant/e2e/create-tenant.e2e.ts +++ b/apps/api/src/app/tenant/e2e/create-tenant.e2e.ts @@ -39,12 +39,14 @@ describe('Create Tenant - /tenants (POST)', function () { await createTenant({ session, identifier: 'identifier_123', + name: 'name_123', }); try { await createTenant({ session, identifier: 'identifier_123', + name: 'name_123', }); throw new Error(''); diff --git a/apps/api/src/app/tenant/tenant.controller.ts b/apps/api/src/app/tenant/tenant.controller.ts index 5faca47756b..444e78a129f 100644 --- a/apps/api/src/app/tenant/tenant.controller.ts +++ b/apps/api/src/app/tenant/tenant.controller.ts @@ -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'; diff --git a/apps/api/src/app/tenant/usecases/delete-tenant/delete-tenant.usecase.ts b/apps/api/src/app/tenant/usecases/delete-tenant/delete-tenant.usecase.ts index 46b2e669ab4..323c5871b59 100644 --- a/apps/api/src/app/tenant/usecases/delete-tenant/delete-tenant.usecase.ts +++ b/apps/api/src/app/tenant/usecases/delete-tenant/delete-tenant.usecase.ts @@ -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 { diff --git a/apps/api/src/app/tenant/usecases/get-tenant/get-tenant.command.ts b/apps/api/src/app/tenant/usecases/get-tenant/get-tenant.command.ts deleted file mode 100644 index 6ee919e706e..00000000000 --- a/apps/api/src/app/tenant/usecases/get-tenant/get-tenant.command.ts +++ /dev/null @@ -1,8 +0,0 @@ -import { EnvironmentCommand } from '@novu/application-generic'; -import { IsMongoId, IsNotEmpty, IsString } from 'class-validator'; - -export class GetTenantCommand extends EnvironmentCommand { - @IsString() - @IsNotEmpty() - identifier: string; -} diff --git a/apps/api/src/app/tenant/usecases/index.ts b/apps/api/src/app/tenant/usecases/index.ts index 042ace15d63..313c576b7f5 100644 --- a/apps/api/src/app/tenant/usecases/index.ts +++ b/apps/api/src/app/tenant/usecases/index.ts @@ -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'; diff --git a/apps/api/src/app/workflows/usecases/create-notification-template/create-notification-template.usecase.ts b/apps/api/src/app/workflows/usecases/create-notification-template/create-notification-template.usecase.ts index f89bdbfb5bd..9a3ac6e45e3 100644 --- a/apps/api/src/app/workflows/usecases/create-notification-template/create-notification-template.usecase.ts +++ b/apps/api/src/app/workflows/usecases/create-notification-template/create-notification-template.usecase.ts @@ -39,7 +39,7 @@ export class CreateNotificationTemplate { const command = blueprintCommand ?? usecaseCommand; const contentService = new ContentService(); - const variables = contentService.extractMessageVariables(command.steps); + const { variables, reservedVariables } = contentService.extractMessageVariables(command.steps); const subscriberVariables = contentService.extractSubscriberMessageVariables(command.steps); const triggerIdentifier = `${slugify(command.name, { @@ -61,6 +61,17 @@ export class CreateNotificationTemplate { type: i.type, }; }), + reservedVariables: reservedVariables.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, diff --git a/apps/api/src/app/workflows/usecases/update-notification-template/update-notification-template.usecase.ts b/apps/api/src/app/workflows/usecases/update-notification-template/update-notification-template.usecase.ts index 10e292f3eb5..0569d38d91e 100644 --- a/apps/api/src/app/workflows/usecases/update-notification-template/update-notification-template.usecase.ts +++ b/apps/api/src/app/workflows/usecases/update-notification-template/update-notification-template.usecase.ts @@ -125,8 +125,7 @@ export class UpdateNotificationTemplate { const contentService = new ContentService(); const { steps } = command; - const variables = contentService.extractMessageVariables(command.steps); - + const { variables, reservedVariables } = contentService.extractMessageVariables(command.steps); updatePayload['triggers.0.variables'] = variables.map((i) => { return { name: i.name, @@ -134,6 +133,18 @@ export class UpdateNotificationTemplate { }; }); + updatePayload['triggers.0.reservedVariables'] = reservedVariables.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) => { diff --git a/apps/web/src/components/execution-detail/ExecutionDetailTrigger.tsx b/apps/web/src/components/execution-detail/ExecutionDetailTrigger.tsx index 232c0a74367..8e9b0864051 100644 --- a/apps/web/src/components/execution-detail/ExecutionDetailTrigger.tsx +++ b/apps/web/src/components/execution-detail/ExecutionDetailTrigger.tsx @@ -11,9 +11,9 @@ const TriggerTitle = styled(Text)` `; export const ExecutionDetailTrigger = ({ identifier, step, subscriberVariables }) => { - const { payload, overrides } = step || {}; + const { payload, overrides, tenant } = step || {}; - const curlSnippet = getCurlTriggerSnippet(identifier, subscriberVariables, payload, overrides); + const curlSnippet = getCurlTriggerSnippet(identifier, subscriberVariables, payload, overrides, { tenant }); return ( <> diff --git a/apps/web/src/pages/templates/components/TestWorkflow.tsx b/apps/web/src/pages/templates/components/TestWorkflow.tsx index 39ffe5ab91b..481034ead89 100644 --- a/apps/web/src/pages/templates/components/TestWorkflow.tsx +++ b/apps/web/src/pages/templates/components/TestWorkflow.tsx @@ -3,6 +3,8 @@ import { Group, JsonInput, Text } from '@mantine/core'; import { useForm } from '@mantine/form'; import { useMutation } from '@tanstack/react-query'; import * as Sentry from '@sentry/react'; +import * as capitalize from 'lodash.capitalize'; + import { IUserEntity, INotificationTriggerVariable } from '@novu/shared'; import { Button, colors } from '../../../design-system'; import { inputStyles } from '../../../design-system/config/inputs.styles'; @@ -47,6 +49,7 @@ export function TestWorkflow({ trigger }) { return [{ name: 'subscriberId' }, ...(trigger?.subscriberVariables || [])]; }, [trigger]); const variables = useMemo(() => [...(trigger?.variables || [])], [trigger]); + const reservedVariables = useMemo(() => [...(trigger?.reservedVariables || [])], [trigger]); const overridesTrigger = '{\n\n}'; @@ -62,6 +65,9 @@ export function TestWorkflow({ trigger }) { initialValues: { toValue: makeToValue(subscriberVariables, currentUser), payloadValue: makePayloadValue(variables) === '{}' ? '{\n\n}' : makePayloadValue(variables), + snippetValue: reservedVariables.map((variable) => { + return { ...variable, variables: makePayloadValue(variable.variables) }; + }), overridesValue: overridesTrigger, }, validate: { @@ -75,10 +81,15 @@ export function TestWorkflow({ trigger }) { form.setValues({ toValue: makeToValue(subscriberVariables, currentUser) }); }, [subscriberVariables, currentUser]); - const onTrigger = async ({ toValue, payloadValue, overridesValue }) => { + const onTrigger = async ({ toValue, payloadValue, overridesValue, snippetValue }) => { const to = JSON.parse(toValue); const payload = JSON.parse(payloadValue); const overrides = JSON.parse(overridesValue); + const snippet = snippetValue.reduce((acc, variable) => { + acc[variable.type] = JSON.parse(variable.variables); + + return acc; + }, {}); try { const response = await triggerTestEvent({ @@ -88,6 +99,7 @@ export function TestWorkflow({ trigger }) { ...payload, __source: 'test-workflow', }, + ...snippet, overrides, }); @@ -139,6 +151,19 @@ export function TestWorkflow({ trigger }) { minRows={3} validationError="Invalid JSON" /> + {form.values.snippetValue.map((variable, index) => ( + + ))}