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

Conditions for integrations #4102

Merged
merged 78 commits into from
Sep 13, 2023
Merged
Show file tree
Hide file tree
Changes from all commits
Commits
Show all changes
78 commits
Select commit Hold shift + click to select a range
a414dcd
refactor: so filter logic from message matcher to filter abstract class
davidsoderberg Aug 28, 2023
5ac009c
feat: add mongoose schema and usecase for conditions
davidsoderberg Aug 29, 2023
399547c
feat: add conditions filter for integrations
davidsoderberg Aug 29, 2023
bfe7d67
Merge pull request #4038 from novuhq/nv-2734-refactor-message-matcher…
davidsoderberg Aug 29, 2023
06fe8ff
fix: so conditions filter usecase is injected
davidsoderberg Aug 29, 2023
736c525
Merge branch 'next' into conditions-for-integrations
davidsoderberg Aug 29, 2023
cec6aab
Merge branch 'conditions-for-integrations' into nv-2702-selectintegra…
davidsoderberg Aug 29, 2023
e75fd7a
fix: after pr comments
davidsoderberg Aug 30, 2023
74a13ab
feat: add api to add conditions on create and update integration
davidsoderberg Aug 30, 2023
e06e6ef
fix: after pr comments
davidsoderberg Aug 30, 2023
2688d6f
Merge branch 'nv-2702-selectintegration-use-case-update-the-logic-for…
davidsoderberg Aug 30, 2023
280c2ad
feat: fix so conditions are emptied when integration are set as primary
davidsoderberg Aug 30, 2023
d7f4337
Merge pull request #4044 from novuhq/nv-2702-selectintegration-use-ca…
davidsoderberg Aug 30, 2023
3423af0
Merge pull request #4050 from novuhq/nv-2686-setintegrationasprimary-…
davidsoderberg Aug 30, 2023
2e25e86
fix: after pr comments
davidsoderberg Aug 30, 2023
64dcdda
fix: after pr comments
davidsoderberg Aug 31, 2023
93124b8
fix: add logs for debugging
davidsoderberg Aug 31, 2023
c95b29c
Merge branch 'next' into conditions-for-integrations
davidsoderberg Aug 31, 2023
bf0dfe1
Merge branch 'conditions-for-integrations' into nv-2682-apply-conditi…
davidsoderberg Aug 31, 2023
36c0b53
feat: add condition icon in integration table
davidsoderberg Aug 31, 2023
3ce8743
fix: condition cell
davidsoderberg Aug 31, 2023
9b05594
Merge pull request #4048 from novuhq/nv-2682-apply-conditions-when-th…
davidsoderberg Sep 1, 2023
d7db633
feat: add logic for select primary integration modal
davidsoderberg Sep 1, 2023
b89d10f
fix: cspell errors
davidsoderberg Sep 1, 2023
5fbf165
fix: after pr comment
davidsoderberg Sep 2, 2023
558fd8c
Merge pull request #4066 from novuhq/nv-2681-show-the-number-of-appli…
davidsoderberg Sep 2, 2023
6bc1abc
feat: wip conditions component
ainouzgali Sep 4, 2023
c259fbd
fix: after pr comments
davidsoderberg Sep 4, 2023
1de9293
fix: after pr comment
davidsoderberg Sep 4, 2023
7d8d098
feat(wip): conditions component
ainouzgali Sep 4, 2023
b8fea49
fix: after pr comment
davidsoderberg Sep 5, 2023
0618ea9
Merge pull request #4095 from novuhq/nv-2679-reusable-conditions-comp…
ainouzgali Sep 5, 2023
6dc61c6
Merge pull request #4072 from novuhq/nv-2687-update-the-logic-of-disp…
davidsoderberg Sep 5, 2023
7cc4c4e
feat: add condition and primary icon buttons
davidsoderberg Sep 4, 2023
121601a
feat: Add conditions button for create integration
davidsoderberg Sep 4, 2023
1a11fb0
feat: add some nodes missing provider modal
davidsoderberg Sep 5, 2023
33d70ce
fix: after merging feature branch to this branch
davidsoderberg Sep 5, 2023
5cc60d1
fix: after pr comments
davidsoderberg Sep 5, 2023
f88bca2
feat: add condition icon button modal
davidsoderberg Sep 5, 2023
00199f3
fix: so modal are not always shown
davidsoderberg Sep 5, 2023
51b0507
fix: after pr comments
davidsoderberg Sep 5, 2023
a2a4d59
Merge pull request #4098 from novuhq/nv-2704-apply-conditions-on-the-…
davidsoderberg Sep 5, 2023
c9d6106
feat: add interface for conditions in sdk
davidsoderberg Sep 5, 2023
92a658d
fix: after pr comments
davidsoderberg Sep 5, 2023
7e418ed
Merge branch 'next' into conditions-for-integrations
davidsoderberg Sep 5, 2023
b04c297
fix: after fixing merge conflicts
davidsoderberg Sep 5, 2023
942b37f
fix: after merge conflicts
davidsoderberg Sep 5, 2023
fe2c8e3
refactor: conditions component
ainouzgali Sep 5, 2023
620ea34
Merge pull request #4101 from novuhq/nv-2778-update-node-sdk-with-the…
davidsoderberg Sep 5, 2023
8df4d3c
Merge branch 'conditions-for-integrations' into feat-refactor-conditi…
ainouzgali Sep 5, 2023
bbc314c
feat: add tests for create and update integration with conditions
davidsoderberg Sep 6, 2023
f689ba3
feat: add test for set as primary integration with conditions
davidsoderberg Sep 6, 2023
3f0f960
Merge branch 'next' into conditions-for-integrations
ainouzgali Sep 6, 2023
6d3da19
feat: add more update integration tests
davidsoderberg Sep 6, 2023
5f8b219
fix: select integration with condtions
davidsoderberg Sep 6, 2023
1bda159
fix: so conditions can be undefined
davidsoderberg Sep 6, 2023
ea62be5
Merge pull request #4115 from novuhq/fix-select-integration-with-cond…
davidsoderberg Sep 6, 2023
3f6edae
Merge branch 'conditions-for-integrations' into nv-2771-api-e2e-tests…
davidsoderberg Sep 6, 2023
6ea7338
Merge remote-tracking branch 'origin/conditions-for-integrations' int…
ainouzgali Sep 6, 2023
136f5c5
fix: fix small notes and refactor
ainouzgali Sep 6, 2023
1e4bdc0
Merge pull request #4105 from novuhq/feat-refactor-conditions-component
ainouzgali Sep 6, 2023
35fae69
fix: so remove primary on condition modal is not shown as often as be…
davidsoderberg Sep 7, 2023
8b84a11
Merge pull request #4118 from novuhq/fix-so-primary-and-conditions-wo…
davidsoderberg Sep 7, 2023
043f78b
fix: copy for primary flag will be removed modal
davidsoderberg Sep 7, 2023
3e8cf76
Merge pull request #4119 from novuhq/fix-condition-icon-button-copy
davidsoderberg Sep 7, 2023
3c1fade
feat: add e2e test for trigger event with condition
davidsoderberg Sep 7, 2023
f74afb7
fix: csspell error
davidsoderberg Sep 7, 2023
c5952cb
feat: add more tests and fixes
davidsoderberg Sep 8, 2023
331b741
fix: test description
davidsoderberg Sep 8, 2023
b22e39f
fix: test
davidsoderberg Sep 8, 2023
f399b56
Merge pull request #4120 from novuhq/nv-2771-api-e2e-tests-for-the-in…
davidsoderberg Sep 8, 2023
f905c54
feat: add test for or condtions
davidsoderberg Sep 10, 2023
6de3cf8
fix: tenant identifier in test
davidsoderberg Sep 10, 2023
7c2a313
Merge pull request #4132 from novuhq/add-test-for-conditions
davidsoderberg Sep 10, 2023
ed118da
Merge branch 'next' into conditions-for-integrations
davidsoderberg Sep 11, 2023
b9a8787
test: conditions for integration tests
ainouzgali Sep 11, 2023
1aaa712
Merge pull request #4138 from novuhq/nv-2689-cypress-e2e-tests-for-th…
ainouzgali Sep 11, 2023
7877dd6
Merge remote-tracking branch 'origin/next' into conditions-for-integr…
ainouzgali Sep 13, 2023
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
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