Skip to content

Commit

Permalink
Release 2024-07-02 13:55 (#5927)
Browse files Browse the repository at this point in the history
* fix(web): local to dev sync modal improvement (#5912)

* fix: wip

* fix: update copy

* fix: remove not reccomended

* feat: manual sync

* fix(api): Nv 4021 syncing to production environment fails for new org (#5916)

* fix(web): set cookie as secure

* fix(web): set cookie as secure

* fix(api): create notification group for both envs

* refactor(root): Rename from `web.novu.co` to `dashboard.novu.co` (#5876)

* refactor(env): update URLs to use dashboard subdomain

* chore(netlify): reorder redirects in netlify.toml

* refactor(onboarding): remove unused imports

* fix(novu): Apply the correct authorization type

* feat(framework): Add trigger capability to defined workflows (#5877)

* test(env.utils): add tests for env utility functions

* refactor(client): Update workflow and error handling

* docs(env.utils): Add comments for getBridgeUrl environments

* refactor: Remove generic type from Workflow definition

* test(env.utils): update bridge URL in development env

* fix(env.utils): correct URL construction in dev environment

* test(workflow): add payload schema to test workflow

* refactor(types): define specific types for event params

* refactor(event.types): update import to use type keyword

* refactor(types): update Workflow and EventTriggerParams

* refactor: rename novu sh tunnel domain

* fix(api): Ensure both web and dashboard subdomains work in parallel (#5919)

* fix(api): Ensure both web and dashboard subdomains work in parallel

This is required for a smooth migration process and to avoid signing out existing users.

* fixup! fix(api): Ensure both web and dashboard subdomains work in parallel

* fixup! fixup! fix(api): Ensure both web and dashboard subdomains work in parallel

* fixup! fixup! fixup! fix(api): Ensure both web and dashboard subdomains work in parallel

* fix(worker): store null in cache after stateless trigger (#5922)

* ci: Remove redundant checkout step in deploy job (#5924)

* refactor(cli): update tunnel URL and submodule commit (#5920)

* feat: add tracking event for sync button clicked (#5925)

* chore(root): Update .source

---------

Co-authored-by: Dima Grossman <dima@grossman.io>
Co-authored-by: Gali Ainouz Baum <ainouzgali@gmail.com>
Co-authored-by: Richard Fontein <32132657+rifont@users.noreply.github.com>
Co-authored-by: Sokratis Vidros <sokratis.vidros@gmail.com>
Co-authored-by: Sokratis Vidros <SokratisVidros@users.noreply.github.com>
Co-authored-by: George Djabarov <39195835+djabarovgeorge@users.noreply.github.com>
Co-authored-by: David Söderberg <2233092+davidsoderberg@users.noreply.github.com>
  • Loading branch information
8 people authored Jul 2, 2024
1 parent 484b35e commit d533d5f
Show file tree
Hide file tree
Showing 25 changed files with 524 additions and 74 deletions.
2 changes: 1 addition & 1 deletion .source
4 changes: 2 additions & 2 deletions apps/api/e2e/echo.server.ts
Original file line number Diff line number Diff line change
Expand Up @@ -2,10 +2,10 @@ import * as http from 'http';
import * as express from 'express';
// FIXME: subpath import not working with `workspace:` protocol. Currently we need to drill into the module instead of using the ES export.
import { serve } from '../../../packages/framework/dist/express';
import { Client, DiscoverWorkflowOutput } from '@novu/framework';
import { Client, Workflow } from '@novu/framework';

export type ServerStartOptions = {
workflows: Array<DiscoverWorkflowOutput>;
workflows: Array<Workflow>;
};

export class EchoServer {
Expand Down
6 changes: 4 additions & 2 deletions apps/api/src/config/cors.spec.ts
Original file line number Diff line number Diff line change
Expand Up @@ -30,6 +30,7 @@ describe('CORS Configuration', () => {
process.env.NODE_ENV = environment;

process.env.FRONT_BASE_URL = 'https://test.com';
process.env.LEGACY_V1_FRONT_BASE_URL = 'https://test-legacy.com';
process.env.WIDGET_BASE_URL = 'https://widget.com';
process.env.PR_PREVIEW_ROOT_URL = 'https://pr-preview.com';
});
Expand All @@ -46,9 +47,10 @@ describe('CORS Configuration', () => {

expect(callbackSpy.calledOnce).to.be.ok;
expect(callbackSpy.firstCall.firstArg).to.be.null;
expect(callbackSpy.firstCall.lastArg.origin.length).to.equal(2);
expect(callbackSpy.firstCall.lastArg.origin.length).to.equal(3);
expect(callbackSpy.firstCall.lastArg.origin[0]).to.equal('https://test.com');
expect(callbackSpy.firstCall.lastArg.origin[1]).to.equal('https://widget.com');
expect(callbackSpy.firstCall.lastArg.origin[1]).to.equal('https://test-legacy.com');
expect(callbackSpy.firstCall.lastArg.origin[2]).to.equal('https://widget.com');
});

it('widget routes should be wildcarded', () => {
Expand Down
9 changes: 8 additions & 1 deletion apps/api/src/config/cors.ts
Original file line number Diff line number Diff line change
Expand Up @@ -15,7 +15,14 @@ export const corsOptionsDelegate: Parameters<INestApplication['enableCors']>[0]
if (enableWildcard(req)) {
corsOptions.origin = '*';
} else {
corsOptions.origin = [process.env.FRONT_BASE_URL];
corsOptions.origin = [];

if (process.env.FRONT_BASE_URL) {
corsOptions.origin.push(process.env.FRONT_BASE_URL);
}
if (process.env.LEGACY_V1_FRONT_BASE_URL) {
corsOptions.origin.push(process.env.LEGACY_V1_FRONT_BASE_URL);
}
if (process.env.WIDGET_BASE_URL) {
corsOptions.origin.push(process.env.WIDGET_BASE_URL);
}
Expand Down
2 changes: 2 additions & 0 deletions apps/api/src/types/env.d.ts
Original file line number Diff line number Diff line change
Expand Up @@ -21,6 +21,8 @@ declare global {
DISABLE_USER_REGISTRATION: `${boolean}`;
IS_API_IDEMPOTENCY_ENABLED: `${boolean}`;
FRONT_BASE_URL: string;
// @deprecated use FRONT_BASE_URL
LEGACY_V1_FRONT_BASE_URL: string;
API_ROOT_URL: string;
SENTRY_DSN: string;
STRIPE_API_KEY: string;
Expand Down
Original file line number Diff line number Diff line change
Expand Up @@ -135,9 +135,6 @@ jobs:
deploy:
runs-on: ubuntu-latest
steps:
- name: Checkout code
uses: actions/checkout@v4
- name: Sync State to Novu
uses: novuhq/actions-novu-sync@v2
with:
Expand Down
Original file line number Diff line number Diff line change
@@ -1,13 +1,16 @@
import { Button } from '@novu/novui';
import { IconOutlineCloudUpload } from '@novu/novui/icons';
import { useState } from 'react';
import { useSegment } from '../../../providers/SegmentProvider';
import { SyncInfoModal } from './SyncInfoModal';

export function SyncInfoModalTrigger() {
const [showSyncInfoModal, setShowSyncInfoModal] = useState(false);
const segment = useSegment();

const toggleSyncInfoModalShow = () => {
setShowSyncInfoModal((previous) => !previous);
segment.track('Workflow sync button clicked - [Studio]');
};

return (
Expand Down
Original file line number Diff line number Diff line change
Expand Up @@ -96,16 +96,15 @@ export class TriggerEvent {
organizationId: mappedCommand.organizationId,
});

const template = await this.getNotificationTemplateByTriggerIdentifier({
environmentId: mappedCommand.environmentId,
triggerIdentifier: mappedCommand.identifier,
});
let storedWorkflow: NotificationTemplateEntity | null = null;
if (!command.bridgeWorkflow) {
storedWorkflow = await this.getNotificationTemplateByTriggerIdentifier({
environmentId: mappedCommand.environmentId,
triggerIdentifier: mappedCommand.identifier,
});
}

/*
* Makes no sense to execute anything if template doesn't exist
* TODO: Send a 404?
*/
if (!template && !command.bridgeWorkflow) {
if (!storedWorkflow && !command.bridgeWorkflow) {
throw new ApiException('Notification template could not be found');
}

Expand Down Expand Up @@ -151,7 +150,7 @@ export class TriggerEvent {
...mappedCommand,
actor: actorProcessed,
template:
template ||
storedWorkflow ||
(command.bridgeWorkflow as unknown as NotificationTemplateEntity),
})
);
Expand All @@ -163,7 +162,7 @@ export class TriggerEvent {
...mappedCommand,
actor: actorProcessed,
template:
template ||
storedWorkflow ||
(command.bridgeWorkflow as unknown as NotificationTemplateEntity),
})
);
Expand All @@ -176,7 +175,7 @@ export class TriggerEvent {
...(mappedCommand as TriggerMulticastCommand),
actor: actorProcessed,
template:
template ||
storedWorkflow ||
(command.bridgeWorkflow as unknown as NotificationTemplateEntity),
})
);
Expand Down
2 changes: 1 addition & 1 deletion packages/cli/src/commands/dev.ts
Original file line number Diff line number Diff line change
Expand Up @@ -23,7 +23,7 @@ export enum DashboardUrlEnum {
STAGING = 'https://dev.dashboard.novu.co',
}

const TUNNEL_URL = 'https://ntfr.dev/api/tunnels';
const TUNNEL_URL = 'https://novu.sh/api/tunnels';

export type DevCommandOptions = {
port: string;
Expand Down
2 changes: 1 addition & 1 deletion packages/framework/src/client.test.ts
Original file line number Diff line number Diff line change
Expand Up @@ -47,7 +47,7 @@ describe('Novu Client', () => {

it('should throw an error when secretKey is not provided', () => {
expect(() => new Client({ secretKey: undefined })).toThrow(
'Missing secret key. Set the NOVU_SECRET_KEY environment variable or pass `secretKey` to the client options.'
'Missing secret key. Set the `NOVU_SECRET_KEY` environment variable or pass `secretKey` to the client options.'
);
});

Expand Down
14 changes: 7 additions & 7 deletions packages/framework/src/client.ts
Original file line number Diff line number Diff line change
Expand Up @@ -10,6 +10,7 @@ import {
ExecutionStateCorruptError,
ExecutionStateOutputInvalidError,
ExecutionStateResultInvalidError,
MissingSecretKeyError,
ProviderExecutionFailedError,
ProviderNotFoundError,
StepNotFoundError,
Expand All @@ -30,6 +31,7 @@ import type {
Schema,
Skip,
ValidationError,
Workflow,
} from './types';
import { EMOJI, log } from './utils';
import { transformSchema, validateData } from './validators';
Expand Down Expand Up @@ -78,9 +80,7 @@ export class Client {
providedOptions?.secretKey || process.env.NOVU_SECRET_KEY || process.env.NOVU_API_KEY;

if (!isRuntimeInDevelopment() && !builtConfiguration.secretKey) {
throw new Error(
'Missing secret key. Set the NOVU_SECRET_KEY environment variable or pass `secretKey` to the client options.'
);
throw new MissingSecretKeyError();
}

if (providedOptions?.strictAuthentication !== undefined) {
Expand All @@ -92,12 +92,12 @@ export class Client {
return builtConfiguration;
}

public addWorkflows(workflows: Array<DiscoverWorkflowOutput>) {
public addWorkflows(workflows: Array<Workflow>) {
for (const workflow of workflows) {
if (this.discoveredWorkflows.some((existing) => existing.workflowId === workflow.workflowId)) {
throw new WorkflowAlreadyExistsError(workflow.workflowId);
if (this.discoveredWorkflows.some((existing) => existing.workflowId === workflow.definition.workflowId)) {
throw new WorkflowAlreadyExistsError(workflow.definition.workflowId);
} else {
this.discoveredWorkflows.push(workflow);
this.discoveredWorkflows.push(workflow.definition);
}
}
}
Expand Down
1 change: 1 addition & 0 deletions packages/framework/src/constants/error.constants.ts
Original file line number Diff line number Diff line change
Expand Up @@ -26,6 +26,7 @@ export enum ErrorCodeEnum {
SIGNING_KEY_NOT_FOUND_ERROR = 'SigningKeyNotFoundError',
PLATFORM_ERROR = 'PlatformError',
SIGNATURE_VERSION_INVALID_ERROR = 'SignatureVersionInvalidError',
WORKFLOW_PAYLOAD_INVALID_ERROR = 'WorkflowPayloadInvalidError',
}

testErrorCodeEnumValidity(ErrorCodeEnum);
9 changes: 9 additions & 0 deletions packages/framework/src/errors/execution.errors.ts
Original file line number Diff line number Diff line change
Expand Up @@ -75,6 +75,15 @@ export class ExecutionProviderOutputInvalidError extends BadRequestError {
}
}

export class WorkflowPayloadInvalidError extends BadRequestError {
code = ErrorCodeEnum.WORKFLOW_PAYLOAD_INVALID_ERROR;

constructor(workflowId: string, data: any) {
super(`Workflow with id: \`${workflowId}\` has invalid \`payload\`. Please provide the correct payload.`);
this.data = data;
}
}

export class UnknownError extends Error {
/**
* HTTP status code.
Expand Down
4 changes: 3 additions & 1 deletion packages/framework/src/errors/handler.errors.ts
Original file line number Diff line number Diff line change
Expand Up @@ -23,6 +23,8 @@ export class MissingSecretKeyError extends BadRequestError {
code = ErrorCodeEnum.MISSING_SECRET_KEY_ERROR;

constructor() {
super(`API Key is missing. Please add the API Key during Client initialization.`);
super(
'Missing secret key. Set the `NOVU_SECRET_KEY` environment variable or pass `secretKey` to the client options.'
);
}
}
19 changes: 6 additions & 13 deletions packages/framework/src/handler.ts
Original file line number Diff line number Diff line change
Expand Up @@ -23,19 +23,19 @@ import {
SigningKeyNotFoundError,
} from './errors';
import { FRAMEWORK_VERSION, SDK_VERSION } from './version';
import { Awaitable, DiscoverWorkflowOutput, TriggerEvent } from './types';
import { Awaitable, EventTriggerParams, Workflow } from './types';
import { initApiClient } from './utils';

// eslint-disable-next-line @typescript-eslint/naming-convention
export interface ServeHandlerOptions {
client?: Client;
workflows: Array<DiscoverWorkflowOutput>;
workflows: Array<Workflow>;
}

interface INovuRequestHandlerOptions<Input extends any[] = any[], Output = any> extends ServeHandlerOptions {
frameworkName: string;
client?: Client;
workflows: Array<DiscoverWorkflowOutput>;
workflows: Array<Workflow>;
handler: Handler<Input, Output>;
}

Expand Down Expand Up @@ -133,11 +133,6 @@ export class NovuRequestHandler<Input extends any[] = any[], Output = any> {
(await actions.headers(HttpHeaderKeysEnum.NOVU_SIGNATURE)) ||
(await actions.headers(HttpHeaderKeysEnum.NOVU_SIGNATURE_DEPRECATED)) ||
'';
const anonymousHeader =
(await actions.headers(HttpHeaderKeysEnum.NOVU_ANONYMOUS)) ||
(await actions.headers(HttpHeaderKeysEnum.NOVU_ANONYMOUS_DEPRECATED)) ||
'';
const source = url.searchParams.get(HttpQueryKeysEnum.SOURCE) || '';

let body: Record<string, unknown> = {};
try {
Expand All @@ -153,7 +148,7 @@ export class NovuRequestHandler<Input extends any[] = any[], Output = any> {
this.validateHmac(body, signatureHeader);
}

const postActionMap = this.getPostActionMap(body, workflowId, stepId, action, anonymousHeader, source);
const postActionMap = this.getPostActionMap(body, workflowId, stepId, action);
const getActionMap = this.getGetActionMap(workflowId, stepId);

if (method === HttpMethodEnum.POST) {
Expand All @@ -178,9 +173,7 @@ export class NovuRequestHandler<Input extends any[] = any[], Output = any> {
body: any,
workflowId: string,
stepId: string,
action: string,
anonymousHeader: string,
source: string
action: string
): Record<PostActionEnum, () => Promise<IActionResponse>> {
return {
[PostActionEnum.TRIGGER]: this.triggerAction({ workflowId, ...body }),
Expand All @@ -207,7 +200,7 @@ export class NovuRequestHandler<Input extends any[] = any[], Output = any> {
};
}

public triggerAction(triggerEvent: TriggerEvent) {
public triggerAction(triggerEvent: EventTriggerParams) {
return async () => {
const requestPayload = {
name: triggerEvent.workflowId,
Expand Down
10 changes: 9 additions & 1 deletion packages/framework/src/types/discover.types.ts
Original file line number Diff line number Diff line change
Expand Up @@ -2,7 +2,8 @@ import { ActionStepEnum, ChannelStepEnum } from '../constants';
import { JsonSchema, Schema } from './schema.types';
import { StepOptions } from './step.types';
import { Execute, WorkflowOptions } from './workflow.types';
import { Awaitable } from './util.types';
import { Awaitable, Prettify } from './util.types';
import { EventTriggerParams, EventTriggerResult } from './event.types';

export type StepType = `${ChannelStepEnum | ActionStepEnum}`;

Expand Down Expand Up @@ -67,6 +68,13 @@ export type DiscoverWorkflowOutput = {
};
};

export type Workflow<T_Payload = any> = {
trigger: (
event: Prettify<Omit<EventTriggerParams<T_Payload>, 'workflowId' | 'bridgeUrl' | 'controls'>>
) => Promise<EventTriggerResult>;
definition: DiscoverWorkflowOutput;
};

export type DiscoverOutput = {
workflows: Array<DiscoverWorkflowOutput>;
};
Loading

0 comments on commit d533d5f

Please sign in to comment.