Skip to content

Commit

Permalink
Merge pull request #4102 from novuhq/conditions-for-integrations
Browse files Browse the repository at this point in the history
Conditions for integrations
  • Loading branch information
ainouzgali authored Sep 13, 2023
2 parents e5d5739 + 7877dd6 commit ab4e2e6
Show file tree
Hide file tree
Showing 77 changed files with 2,106 additions and 346 deletions.
128 changes: 127 additions & 1 deletion apps/api/src/app/events/e2e/trigger-event.e2e.ts
Original file line number Diff line number Diff line change
Expand Up @@ -34,6 +34,7 @@ import {
MESSAGE_GENERIC_RETENTION_DAYS,
} from '@novu/shared';
import { EmailEventStatusEnum } from '@novu/stateless';
import { createTenant } from '../../tenant/e2e/create-tenant.e2e';

const axiosInstance = axios.create();

Expand Down Expand Up @@ -65,6 +66,129 @@ describe(`Trigger event - ${eventTriggerPath} (POST)`, function () {
subscriber = await subscriberService.createSubscriber();
});

it('should use conditions to select integration', async function () {
const payload = {
providerId: EmailProviderIdEnum.Mailgun,
channel: 'email',
credentials: { apiKey: '123', secretKey: 'abc' },
_environmentId: session.environment._id,
conditions: [
{
children: [{ field: 'identifier', value: 'test', operator: 'EQUAL', on: 'tenant' }],
},
],
active: true,
check: false,
};

await session.testAgent.post('/v1/integrations').send(payload);

template = await createTemplate(session, ChannelTypeEnum.EMAIL);

await createTenant({ session, identifier: 'test', name: 'test' });

await sendTrigger(session, template, subscriber.subscriberId, {}, {}, 'test');

await session.awaitRunningJobs(template._id);

const createdSubscriber = await subscriberRepository.findBySubscriberId(
session.environment._id,
subscriber.subscriberId
);

const message = await messageRepository.findOne({
_environmentId: session.environment._id,
_subscriberId: createdSubscriber?._id,
channel: ChannelTypeEnum.EMAIL,
});

expect(message?.providerId).to.equal(payload.providerId);
});

it('should use or conditions to select integration', async function () {
const payload = {
providerId: EmailProviderIdEnum.Mailgun,
channel: 'email',
credentials: { apiKey: '123', secretKey: 'abc' },
_environmentId: session.environment._id,
conditions: [
{
value: 'OR',
children: [
{ field: 'identifier', value: 'test3', operator: 'EQUAL', on: 'tenant' },
{ field: 'identifier', value: 'test2', operator: 'EQUAL', on: 'tenant' },
],
},
],
active: true,
check: false,
};

await session.testAgent.post('/v1/integrations').send(payload);

template = await createTemplate(session, ChannelTypeEnum.EMAIL);

await createTenant({ session, identifier: 'test3', name: 'test3' });
await createTenant({ session, identifier: 'test2', name: 'test2' });

await sendTrigger(session, template, subscriber.subscriberId, {}, {}, 'test3');

await session.awaitRunningJobs(template._id);

const createdSubscriber = await subscriberRepository.findBySubscriberId(
session.environment._id,
subscriber.subscriberId
);

const firstMessage = await messageRepository.findOne({
_environmentId: session.environment._id,
_subscriberId: createdSubscriber?._id,
channel: ChannelTypeEnum.EMAIL,
});

expect(firstMessage?.providerId).to.equal(payload.providerId);

await sendTrigger(session, template, subscriber.subscriberId, {}, {}, 'test2');

await session.awaitRunningJobs(template._id);

const secondMessage = await messageRepository.findOne({
_environmentId: session.environment._id,
_subscriberId: createdSubscriber?._id,
channel: ChannelTypeEnum.EMAIL,
_id: {
$ne: firstMessage?._id,
},
});

expect(secondMessage?.providerId).to.equal(payload.providerId);
expect(firstMessage?._id).to.not.equal(secondMessage?._id);
});

it('should return correct status when using a non existing tenant', async function () {
const payload = {
providerId: EmailProviderIdEnum.Mailgun,
channel: 'email',
credentials: { apiKey: '123', secretKey: 'abc' },
_environmentId: session.environment._id,
conditions: [
{
children: [{ field: 'identifier', value: 'test1', operator: 'EQUAL', on: 'tenant' }],
},
],
active: true,
check: false,
};

await session.testAgent.post('/v1/integrations').send(payload);

template = await createTemplate(session, ChannelTypeEnum.EMAIL);

const result = await sendTrigger(session, template, subscriber.subscriberId, {}, {}, 'test1');

expect(result.data.data.status).to.equal('no_tenant_found');
});

it('should trigger an event successfully', async function () {
const response = await axiosInstance.post(
`${session.serverUrl}${eventTriggerPath}`,
Expand Down Expand Up @@ -1848,7 +1972,8 @@ export async function sendTrigger(
template,
newSubscriberIdInAppNotification: string,
payload: Record<string, unknown> = {},
overrides: Record<string, unknown> = {}
overrides: Record<string, unknown> = {},
tenant?: string
): Promise<AxiosResponse> {
return await axiosInstance.post(
`${session.serverUrl}${eventTriggerPath}`,
Expand All @@ -1861,6 +1986,7 @@ export async function sendTrigger(
...payload,
},
overrides,
tenant,
},
{
headers: {
Expand Down
Original file line number Diff line number Diff line change
Expand Up @@ -11,7 +11,7 @@ import {
StorageHelperService,
WorkflowQueueService,
} from '@novu/application-generic';
import { NotificationTemplateRepository, NotificationTemplateEntity } from '@novu/dal';
import { NotificationTemplateRepository, NotificationTemplateEntity, TenantRepository } from '@novu/dal';
import {
ISubscribersDefine,
ITenantDefine,
Expand All @@ -35,7 +35,8 @@ export class ParseEventRequest {
private verifyPayload: VerifyPayload,
private storageHelperService: StorageHelperService,
private workflowQueueService: WorkflowQueueService,
private mapTriggerRecipients: MapTriggerRecipients
private mapTriggerRecipients: MapTriggerRecipients,
private tenantRepository: TenantRepository
) {}

@InstrumentUsecase()
Expand Down Expand Up @@ -90,6 +91,17 @@ export class ParseEventRequest {
};
}

if (command.tenant) {
try {
await this.validateTenant(typeof command.tenant === 'string' ? command.tenant : command.tenant.identifier);
} catch (e) {
return {
acknowledged: true,
status: 'no_tenant_found',
};
}
}

Sentry.addBreadcrumb({
message: 'Sending trigger',
data: {
Expand Down Expand Up @@ -146,6 +158,15 @@ export class ParseEventRequest {
);
}

private async validateTenant(identifier: string) {
const found = await this.tenantRepository.findOne({
identifier,
});
if (!found) {
throw new ApiException(`Tenant with identifier ${identifier} cound not be found`);
}
}

@Instrument()
private async validateSubscriberIdProperty(to: ISubscribersDefine[]): Promise<boolean> {
for (const subscriber of to) {
Expand Down
Original file line number Diff line number Diff line change
@@ -1,9 +1,19 @@
import { IsBoolean, IsDefined, IsEnum, IsMongoId, IsOptional, IsString, ValidateNested } from 'class-validator';
import {
IsArray,
IsBoolean,
IsDefined,
IsEnum,
IsMongoId,
IsOptional,
IsString,
ValidateNested,
} from 'class-validator';
import { ApiProperty, ApiPropertyOptional } from '@nestjs/swagger';
import { Type } from 'class-transformer';
import { ChannelTypeEnum, ICreateIntegrationBodyDto } from '@novu/shared';

import { CredentialsDto } from './credentials.dto';
import { StepFilter } from '../../shared/dtos/step-filter';

export class CreateIntegrationRequestDto implements ICreateIntegrationBodyDto {
@ApiPropertyOptional({ type: String })
Expand Down Expand Up @@ -53,4 +63,12 @@ export class CreateIntegrationRequestDto implements ICreateIntegrationBodyDto {
@IsOptional()
@IsBoolean()
check?: boolean;

@ApiPropertyOptional({
type: [StepFilter],
})
@IsArray()
@IsOptional()
@ValidateNested({ each: true })
conditions?: StepFilter[];
}
Original file line number Diff line number Diff line change
@@ -1,5 +1,6 @@
import { ApiProperty, ApiPropertyOptional } from '@nestjs/swagger';
import { ChannelTypeEnum } from '@novu/shared';
import { StepFilter } from '../../shared/dtos/step-filter';
import { CredentialsDto } from './credentials.dto';

export class IntegrationResponseDto {
Expand Down Expand Up @@ -45,4 +46,9 @@ export class IntegrationResponseDto {

@ApiProperty()
primary: boolean;

@ApiPropertyOptional({
type: [StepFilter],
})
conditions?: StepFilter[];
}
11 changes: 10 additions & 1 deletion apps/api/src/app/integrations/dtos/update-integration.dto.ts
Original file line number Diff line number Diff line change
@@ -1,8 +1,9 @@
import { ApiPropertyOptional } from '@nestjs/swagger';
import { IUpdateIntegrationBodyDto } from '@novu/shared';
import { IsBoolean, IsMongoId, IsOptional, IsString, ValidateNested } from 'class-validator';
import { IsArray, IsBoolean, IsMongoId, IsOptional, IsString, ValidateNested } from 'class-validator';
import { CredentialsDto } from './credentials.dto';
import { Type } from 'class-transformer';
import { StepFilter } from '../../shared/dtos/step-filter';

export class UpdateIntegrationRequestDto implements IUpdateIntegrationBodyDto {
@ApiPropertyOptional({ type: String })
Expand Down Expand Up @@ -40,4 +41,12 @@ export class UpdateIntegrationRequestDto implements IUpdateIntegrationBodyDto {
@IsOptional()
@IsBoolean()
check?: boolean;

@ApiPropertyOptional({
type: [StepFilter],
})
@IsArray()
@IsOptional()
@ValidateNested({ each: true })
conditions?: StepFilter[];
}
44 changes: 44 additions & 0 deletions apps/api/src/app/integrations/e2e/create-integration.e2e.ts
Original file line number Diff line number Diff line change
Expand Up @@ -104,6 +104,50 @@ describe('Create Integration - /integration (POST)', function () {
}
});

it('should create integration with conditions', async function () {
const payload = {
providerId: EmailProviderIdEnum.SendGrid,
channel: ChannelTypeEnum.EMAIL,
identifier: 'identifier-conditions',
active: false,
check: false,
conditions: [
{
children: [{ field: 'identifier', value: 'test', operator: 'EQUAL', on: 'tenant' }],
},
],
};

const { body } = await session.testAgent.post('/v1/integrations').send(payload);

expect(body.data.conditions.length).to.equal(1);
expect(body.data.conditions[0].children.length).to.equal(1);
expect(body.data.conditions[0].children[0].on).to.equal('tenant');
expect(body.data.conditions[0].children[0].field).to.equal('identifier');
expect(body.data.conditions[0].children[0].value).to.equal('test');
expect(body.data.conditions[0].children[0].operator).to.equal('EQUAL');
});

it('should return error with malformed conditions', async function () {
const payload = {
providerId: EmailProviderIdEnum.SendGrid,
channel: ChannelTypeEnum.EMAIL,
identifier: 'identifier-conditions',
active: false,
check: false,
conditions: [
{
children: 'test',
},
],
};

const { body } = await session.testAgent.post('/v1/integrations').send(payload);

expect(body.statusCode).to.equal(400);
expect(body.error).to.equal('Bad Request');
});

it('should not allow to create integration with same identifier', async function () {
const payload = {
providerId: EmailProviderIdEnum.SendGrid,
Expand Down
28 changes: 28 additions & 0 deletions apps/api/src/app/integrations/e2e/set-itegration-as-primary.e2e.ts
Original file line number Diff line number Diff line change
Expand Up @@ -65,6 +65,34 @@ describe('Set Integration As Primary - /integrations/:integrationId/set-primary
expect(body.message).to.equal(`Channel ${inAppIntegration.channel} does not support primary`);
});

it('clears conditions when set as primary', async () => {
await integrationRepository.deleteMany({
_organizationId: session.organization._id,
_environmentId: session.environment._id,
});

const integration = await integrationRepository.create({
name: 'Email with conditions',
identifier: 'identifier1',
providerId: EmailProviderIdEnum.SendGrid,
channel: ChannelTypeEnum.EMAIL,
active: false,
_organizationId: session.organization._id,
_environmentId: session.environment._id,
conditions: [{}],
});

await session.testAgent.post(`/v1/integrations/${integration._id}/set-primary`).send({});

const found = await integrationRepository.findOne({
_id: integration._id,
_organizationId: session.organization._id,
});

expect(found?.conditions).to.deep.equal([]);
expect(found?.primary).to.equal(true);
});

it('push channel does not support primary flag, then for integration it should throw bad request exception', async () => {
await integrationRepository.deleteMany({
_organizationId: session.organization._id,
Expand Down
Loading

1 comment on commit ab4e2e6

@github-actions
Copy link

Choose a reason for hiding this comment

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

Please sign in to comment.