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

prod deploy #5910

Merged
merged 18 commits into from
Jul 1, 2024
Merged
Show file tree
Hide file tree
Changes from all commits
Commits
Show all changes
18 commits
Select commit Hold shift + click to select a range
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
3 changes: 3 additions & 0 deletions .cspell.json
Original file line number Diff line number Diff line change
Expand Up @@ -140,6 +140,7 @@
"domainname",
"domainsuffix",
"donefunc",
"Dotan",
"dotenv",
"doublecolon",
"dtos",
Expand All @@ -151,6 +152,7 @@
"elif",
"emailjs",
"Embeddable",
"Emek",
"EMSA",
"endgroup",
"enroute",
Expand Down Expand Up @@ -308,6 +310,7 @@
"mkdocs",
"mlen",
"moby",
"Modiin",
"modlen",
"mongod",
"mongosh",
Expand Down
2 changes: 1 addition & 1 deletion .source
5 changes: 0 additions & 5 deletions apps/api/package.json
Original file line number Diff line number Diff line change
Expand Up @@ -125,10 +125,5 @@
"@novu/ee-echo-api": "workspace:*",
"@novu/ee-shared-services": "workspace:*",
"@novu/ee-translation": "workspace:*"
},
"lint-staged": {
"*.{js,jsx,ts,tsx}": [
"eslint"
]
}
}
3 changes: 3 additions & 0 deletions apps/api/src/app/events/dtos/trigger-event-request.dto.ts
Original file line number Diff line number Diff line change
Expand Up @@ -12,6 +12,7 @@ import {
import { Type } from 'class-transformer';
import { ApiExtraModels, ApiProperty, ApiPropertyOptional, getSchemaPath } from '@nestjs/swagger';
import {
ControlsDto,
TopicKey,
TriggerRecipients,
TriggerRecipientsTypeEnum,
Expand Down Expand Up @@ -137,6 +138,8 @@ export class TriggerEventRequestDto {
@ValidateNested()
@Type(() => TenantPayloadDto)
tenant?: TriggerTenantContext;

controls?: ControlsDto;
}

export class BulkTriggerEventDto {
Expand Down
89 changes: 82 additions & 7 deletions apps/api/src/app/events/e2e/bridge-trigger.e2e-ee.ts
Original file line number Diff line number Diff line change
Expand Up @@ -2,13 +2,13 @@ import axios from 'axios';
import { expect } from 'chai';
import { v4 as uuidv4 } from 'uuid';

import { UserSession, SubscribersService } from '@novu/testing';
import { SubscribersService, UserSession } from '@novu/testing';
import {
ExecutionDetailsRepository,
JobRepository,
MessageRepository,
SubscriberEntity,
NotificationTemplateRepository,
JobRepository,
ExecutionDetailsRepository,
SubscriberEntity,
} from '@novu/dal';
import {
ChannelTypeEnum,
Expand Down Expand Up @@ -566,8 +566,8 @@ contexts.forEach((context: Context) => {
expect(messagesAfter[0].content).to.match(/people waited for \d+ seconds/);
});

it(`should trigger the echo workflow with control variables [${context.name}]`, async () => {
const workflowId = `control-variables-workflow-${context.name + '-' + uuidv4()}`;
it(`should trigger the echo workflow with control default and payload data [${context.name}]`, async () => {
const workflowId = `default-payload-params-workflow-${context.name + '-' + uuidv4()}`;
const newWorkflow = workflow(
workflowId,
async ({ step, payload }) => {
Expand Down Expand Up @@ -622,6 +622,64 @@ contexts.forEach((context: Context) => {
expect(sentMessage[1].subject).to.include('prefix Hello default_name');
expect(sentMessage[0].subject).to.include('prefix Hello payload_name');
});

it(`should trigger the echo workflow with control variables [${context.name}]`, async () => {
const workflowId = `control-variables-workflow-${context.name + '-' + uuidv4()}`;
const stepId = 'send-email';
const newWorkflow = workflow(
workflowId,
async ({ step, payload }) => {
await step.email(
stepId,
async (controls) => {
return {
subject: 'email subject ' + controls.name,
body: 'Body result',
};
},
{
controlSchema: {
type: 'object',
properties: {
name: { type: 'string', default: 'control default' },
},
} as const,
}
);
},
{
// todo delete
payloadSchema: {
type: 'object',
properties: {
name: { type: 'string', default: 'default_name' },
},
required: [],
additionalProperties: false,
} as const,
}
);

await echoServer.start({ workflows: [newWorkflow] });

if (context.isStateful) {
await discoverAndSyncEcho(session, workflowsRepository, workflowId, echoServer);
await saveControlVariables(session, workflowId, stepId, { variables: { name: 'stored_control_name' } });
}

const controls = { steps: { [stepId]: { name: 'stored_control_name' } } };
await triggerEvent(session, workflowId, subscriber, undefined, bridge, controls);
await session.awaitRunningJobs();

const sentMessage = await messageRepository.find({
_environmentId: session.environment._id,
_subscriberId: subscriber._id,
channel: StepTypeEnum.EMAIL,
});

expect(sentMessage.length).to.be.eq(1);
expect(sentMessage[0].subject).to.equal('email subject stored_control_name');
});
});
});

Expand All @@ -641,7 +699,14 @@ async function syncWorkflow(
if (!foundWorkflow) throw new Error('Workflow not found');
}

async function triggerEvent(session, workflowId: string, subscriber, payload?: any, bridge?: { url: string }) {
async function triggerEvent(
session,
workflowId: string,
subscriber,
payload?: any,
bridge?: { url: string },
controls?: Record<string, unknown>
) {
const defaultPayload = {
name: 'test_name',
};
Expand All @@ -655,6 +720,7 @@ async function triggerEvent(session, workflowId: string, subscriber, payload?: a
email: '[email protected]',
},
payload: payload ?? defaultPayload,
controls: controls ?? undefined,
bridgeUrl: bridge?.url ?? undefined,
},
{
Expand Down Expand Up @@ -689,6 +755,15 @@ async function discoverAndSyncEcho(
return discoverResponse;
}

async function saveControlVariables(
session: UserSession,
workflowIdentifier?: string,
stepIdentifier?: string,
payloadBody?: any
) {
return await session.testAgent.put(`/v1/bridge/controls/${workflowIdentifier}/${stepIdentifier}`).send(payloadBody);
}

async function markAllSubscriberMessagesAs(session: UserSession, subscriberId: string, markAs: MessagesStatusEnum) {
const response = await axios.post(
`${session.serverUrl}/v1/subscribers/${subscriberId}/messages/mark-all`,
Expand Down
Empty file.
1 change: 1 addition & 0 deletions apps/api/src/app/events/events.controller.ts
Original file line number Diff line number Diff line change
Expand Up @@ -83,6 +83,7 @@ export class EventsController {
addressingType: AddressingTypeEnum.MULTICAST,
requestCategory: TriggerRequestCategoryEnum.SINGLE,
bridgeUrl: body.bridgeUrl,
controls: body.controls,
})
);

Expand Down
Original file line number Diff line number Diff line change
@@ -1,6 +1,7 @@
import { IsDefined, IsString, IsOptional, ValidateNested, ValidateIf, IsEnum, IsObject } from 'class-validator';
import {
AddressingTypeEnum,
ControlsDto,
TriggerRecipients,
TriggerRecipientSubscriber,
TriggerRequestCategoryEnum,
Expand Down Expand Up @@ -41,6 +42,8 @@ export class ParseEventRequestBaseCommand extends EnvironmentWithUserCommand {
@IsString()
@IsOptional()
bridgeUrl?: string;

controls?: ControlsDto;
}

export class ParseEventRequestMulticastCommand extends ParseEventRequestBaseCommand {
Expand Down
5 changes: 0 additions & 5 deletions apps/web/package.json
Original file line number Diff line number Diff line change
Expand Up @@ -184,11 +184,6 @@
"last 1 safari version"
]
},
"lint-staged": {
"*.{js,jsx,ts,tsx}": [
"eslint"
]
},
"eslintConfig": {
"overrides": [
{
Expand Down
14 changes: 9 additions & 5 deletions apps/web/src/bridgeApi/bridgeApi.client.ts
Original file line number Diff line number Diff line change
Expand Up @@ -12,6 +12,9 @@ export type TriggerParams = {
bridgeUrl?: string;
to: { subscriberId: string; email: string };
payload: Record<string, unknown>;
controls?: {
steps?: Record<string, unknown>;
};
};

export type BridgeStatus = {
Expand All @@ -27,6 +30,8 @@ export function buildBridgeHTTPClient(baseURL: string) {
baseURL,
headers: {
'Content-Type': 'application/json',
// Required if a custom tunnel is used by developers such as localtunnel.it
'Bypass-Tunnel-Reminder': true,
},
});

Expand Down Expand Up @@ -83,24 +88,23 @@ export function buildBridgeHTTPClient(baseURL: string) {
*/
async getStepPreview({ workflowId, stepId, controls, payload }: StepPreviewParams): Promise<any> {
return post(`${baseURL}?action=preview&workflowId=${workflowId}&stepId=${stepId}`, {
// TODO: Rename to controls
inputs: controls || {},
// TODO: Rename to payload
data: payload || {},
controls: controls || {},
payload: payload || {},
});
},

/**
* TODO: Use framework shared types
*/
async trigger({ workflowId, bridgeUrl, to, payload }: TriggerParams): Promise<any> {
async trigger({ workflowId, bridgeUrl, to, payload, controls }: TriggerParams): Promise<any> {
payload = payload || {};
payload.__source = 'studio-test-workflow';

return post(`${baseURL}?action=trigger&workflowId=${workflowId}`, {
bridgeUrl,
to,
payload,
controls,
});
},
};
Expand Down
Original file line number Diff line number Diff line change
@@ -1,10 +1,9 @@
import { Header } from '@mantine/core';
import { IconButton } from '@novu/novui';
import { css } from '@novu/novui/css';
import { IconHelpOutline, IconOutlineMenuBook } from '@novu/novui/icons';
import { IconOutlineMenuBook } from '@novu/novui/icons';
import { HStack } from '@novu/novui/jsx';
import { FC } from 'react';
import { discordInviteUrl } from '../../../../pages/quick-start/consts';
import { useStudioWorkflowsNavigation } from '../../../../studio/hooks';
import { HEADER_NAV_HEIGHT } from '../../constants';
import { BridgeMenuItems } from '../v2/BridgeMenuItems';
Expand Down Expand Up @@ -35,10 +34,6 @@ export const LocalStudioHeader: FC = () => {
target="_blank"
rel="noopener noreferrer"
/>

{/* This doesn't work because of Discord's popup blocker via the response header:
Cross-Origin-Opener-Policy: same-origin-allow-popups. We will likely need a Javascript workaround for Discord's popup blocker. */}
<IconButton Icon={IconHelpOutline} as="a" href={discordInviteUrl} target="_blank" rel="noopener noreferrer" />
</HStack>
</HStack>
</Header>
Expand Down
Original file line number Diff line number Diff line change
@@ -1,11 +1,17 @@
import { useStudioState } from '../../../../studio/StudioStateProvider';
import { When } from '../../../utils/When';
import { BridgeUpdateModalTrigger } from './BridgeUpdateModalTrigger';
import { SyncInfoModalTrigger } from './SyncInfoModalTrigger';

export function BridgeMenuItems() {
const { isLocalStudio } = useStudioState();

return (
<>
<BridgeUpdateModalTrigger />
<SyncInfoModalTrigger />
<When truthy={isLocalStudio}>
<SyncInfoModalTrigger />
</When>
</>
);
}
31 changes: 28 additions & 3 deletions apps/web/src/components/layout/components/v2/BridgeUpdateModal.tsx
Original file line number Diff line number Diff line change
Expand Up @@ -12,6 +12,7 @@ import { hstack } from '@novu/novui/patterns';
import { useSegment } from '../../../providers/SegmentProvider';
import { validateURL } from '../../../../utils/url';
import { useStudioState } from '../../../../studio/StudioStateProvider';
import { buildBridgeHTTPClient } from '../../../../bridgeApi/bridgeApi.client';

export type BridgeUpdateModalProps = {
isOpen: boolean;
Expand All @@ -20,7 +21,7 @@ export type BridgeUpdateModalProps = {

export const BridgeUpdateModal: FC<BridgeUpdateModalProps> = ({ isOpen, toggleOpen }) => {
const segment = useSegment();
const { local, bridgeURL, setBridgeURL } = useStudioState();
const { isLocalStudio, bridgeURL, setBridgeURL } = useStudioState();
const [urlError, setUrlError] = useState<string>('');
const [url, setUrl] = useState(bridgeURL);
const [isUpdating, setIsUpdating] = useState(false);
Expand All @@ -33,14 +34,38 @@ export const BridgeUpdateModal: FC<BridgeUpdateModalProps> = ({ isOpen, toggleOp
setUrl(event.target.value);
};

const validateFromLocal = async (bridgeUrl: string): Promise<{ isValid: boolean }> => {
try {
const client = buildBridgeHTTPClient(bridgeUrl);
const response = await client.healthCheck();

const result = { isValid: response.status === 'ok' };

return result;
} catch {}

return { isValid: false };
};
const localDomains = ['localhost', '127.0.0.1'];
const isLocalAddress = () => {
return localDomains.includes(location.hostname);
};

const onUpdateClick = async () => {
setUrlError('');
setIsUpdating(true);
try {
if (url) {
validateURL(url);

const result = await validateBridgeUrl({ bridgeUrl: url });
let result =
isLocalStudio && isLocalAddress()
? await validateFromLocal(url)
: await validateBridgeUrl({ bridgeUrl: url });

if (!result.isValid && isLocalStudio) {
result = await validateBridgeUrl({ bridgeUrl: url });
}
if (!result.isValid) {
throw new Error('The provided URL is not the Novu Endpoint URL');
}
Expand All @@ -60,7 +85,7 @@ export const BridgeUpdateModal: FC<BridgeUpdateModalProps> = ({ isOpen, toggleOp

const storeInProperLocation = async (newUrl: string) => {
setBridgeURL(newUrl);
if (!local) {
if (!isLocalStudio) {
await updateBridgeUrl({ url: newUrl }, environment?._id ?? '');
}
};
Expand Down
Loading
Loading