Skip to content

Commit

Permalink
chore(root): Release 2024-11-01 12:40 (#6826)
Browse files Browse the repository at this point in the history
  • Loading branch information
github-actions[bot] authored Nov 1, 2024
2 parents f92f5a4 + 8ea9057 commit 22f1bc1
Show file tree
Hide file tree
Showing 66 changed files with 612 additions and 369 deletions.
7 changes: 3 additions & 4 deletions .github/workflows/prod-deploy-inbound-mail.yml
Original file line number Diff line number Diff line change
Expand Up @@ -9,7 +9,7 @@ jobs:
test_inbound_mail:
strategy:
matrix:
name: [ 'novu/inbound-mail-ee', 'novu/inbound-mail' ]
name: ['novu/inbound-mail-ee', 'novu/inbound-mail']
uses: ./.github/workflows/reusable-inbound-mail-e2e.yml
with:
ee: ${{ contains (matrix.name,'-ee') }}
Expand All @@ -28,7 +28,7 @@ jobs:
id-token: write
strategy:
matrix:
name: [ 'novu/inbound-mail-ee', 'novu/inbound-mail' ]
name: ['novu/inbound-mail-ee', 'novu/inbound-mail']
steps:
- uses: actions/checkout@v4
- uses: ./.github/actions/setup-project
Expand Down Expand Up @@ -89,7 +89,7 @@ jobs:
docker tag novu-inbound-mail ghcr.io/$REGISTRY_OWNER/$DOCKER_NAME:$IMAGE_TAG
docker run --network=host --name inbound-mail -dit --env NODE_ENV=test ghcr.io/$REGISTRY_OWNER/$DOCKER_NAME:$IMAGE_TAG
docker push ghcr.io/$REGISTRY_OWNER/$DOCKER_NAME:prod
docker push ghcr.io/$REGISTRY_OWNER/$DOCKER_NAME:latest
docker push ghcr.io/$REGISTRY_OWNER/$DOCKER_NAME:$IMAGE_TAG
Expand All @@ -108,7 +108,6 @@ jobs:

deploy_prod_inbound_mail_us:
needs:
- deploy_prod_inbound_mail_eu
- build_prod_image
uses: ./.github/workflows/reusable-app-service-deploy.yml
secrets: inherit
Expand Down
1 change: 0 additions & 1 deletion .github/workflows/prod-deploy-webhook.yml
Original file line number Diff line number Diff line change
Expand Up @@ -33,7 +33,6 @@ jobs:

deploy_prod_webhook_us:
needs:
- deploy_prod_webhook_eu
- publish_docker_image_webhook
uses: ./.github/workflows/reusable-app-service-deploy.yml
secrets: inherit
Expand Down
5 changes: 2 additions & 3 deletions .github/workflows/prod-deploy-ws.yml
Original file line number Diff line number Diff line change
Expand Up @@ -41,14 +41,14 @@ jobs:
run: |
service="novu/ws-ee"
echo "SERVICE_NAME=$(basename "${service//-/-}")" >> $GITHUB_ENV
- name: Configure AWS credentials
uses: aws-actions/configure-aws-credentials@v4
with:
aws-access-key-id: ${{ secrets.AWS_ACCESS_KEY_ID}}
aws-secret-access-key: ${{ secrets.AWS_SECRET_ACCESS_KEY }}
aws-region: us-east-1

- name: Login to Amazon ECR
id: login-ecr
uses: aws-actions/amazon-ecr-login@v2
Expand Down Expand Up @@ -88,7 +88,6 @@ jobs:

deploy_prod_ws_us:
needs:
- deploy_prod_ws_eu
- build_prod_image
uses: ./.github/workflows/reusable-app-service-deploy.yml
secrets: inherit
Expand Down
28 changes: 21 additions & 7 deletions .vscode/tasks.json
Original file line number Diff line number Diff line change
Expand Up @@ -20,13 +20,8 @@
"beginsPattern": "Successfully compiled",
"endsPattern": "Started application in NODE_ENV"
}
},
"dependsOn": [
"SHARED",
"APPLICATION GENERIC",
"DAL"
]
},
}
},
{
"type": "npm",
"script": "start",
Expand Down Expand Up @@ -56,6 +51,25 @@
]
},
{
"type": "npm",
"script": "start",
"isBackground": true,
"label": "DASHBOARD",
"path": "/apps/dashboard",
"icon": {
"id": "browser",
"color": "terminal.ansiGreen"
},
"problemMatcher": {
"base": "$tsc-watch",
"owner": "typescript",
"background": {
"activeOnStart": true,
"beginsPattern": "Compiling...",
"endsPattern": "webpack compiled successfully"
}
}
}, {
"type": "npm",
"script": "start",
"isBackground": true,
Expand Down
2 changes: 1 addition & 1 deletion LICENSE
Original file line number Diff line number Diff line change
@@ -1,6 +1,6 @@
MIT License

Copyright (c) 2019 Dima Grossman
Copyright (c) 2019 Noti-fire apps ltd.

Permission is hereby granted, free of charge, to any person obtaining a copy
of this software and associated documentation files (the "Software"), to deal
Expand Down
10 changes: 6 additions & 4 deletions README.md
Original file line number Diff line number Diff line change
Expand Up @@ -16,9 +16,6 @@
<a href="https://www.npmjs.com/package/@novu/node">
<img src="https://img.shields.io/npm/dm/@novu/node" alt="npm downloads">
</a>
<a href="https://github.com/novuhq/novu/blob/main/LICENSE">
<img src="https://img.shields.io/github/license/novuhq/novu" alt="MIT">
</a>
</p>

<h1 align="center">
Expand Down Expand Up @@ -217,7 +214,12 @@ We are more than happy to help you. If you are getting any errors or facing prob

## 🛡️ License

Novu is licensed under the MIT License - see the [LICENSE](https://github.com/novuhq/novu/blob/main/LICENSE) file for details.
Novu is a commercial open source company, which means some parts of this open source repository require a commercial license. The concept is called "Open Core," where the core technology is fully open source, licensed under MIT license, and the enterprise code is covered under a commercial license ("/enterprise" Enterprise Edition). Enterprise features are built by the core engineering team of Novu which is hired in full-time.

The following modules and folders are licensed under the enterprise license:
- `enterprise` folder at the root of the project and all of their subfolders and modules
- `apps/web/src/ee` folder and all of their subfolders and modules
- `apps/dashboard/src/ee` folder and all of their subfolders and modules

## 💪 Thanks to all of our contributors

Expand Down
7 changes: 4 additions & 3 deletions apps/api/src/app/analytics/analytics.controller.ts
Original file line number Diff line number Diff line change
@@ -1,4 +1,4 @@
import { Body, Controller, Post, Headers } from '@nestjs/common';
import { Body, Controller, Post, HttpCode, HttpStatus } from '@nestjs/common';
import { SkipThrottle } from '@nestjs/throttler';
import { AnalyticsService, ExternalApiAccessible, UserSession } from '@novu/application-generic';
import { UserSessionData } from '@novu/shared';
Expand Down Expand Up @@ -35,8 +35,9 @@ export class AnalyticsController {
@Post('/identify')
@ExternalApiAccessible()
@UserAuthentication()
async identifyUser(@Body() body: any, @UserSession() user: UserSessionData): Promise<any> {
return this.hubspotIdentifyFormUsecase.execute(
@HttpCode(HttpStatus.NO_CONTENT)
async identifyUser(@Body() body: any, @UserSession() user: UserSessionData) {
await this.hubspotIdentifyFormUsecase.execute(
HubspotIdentifyFormCommand.create({
email: user.email as string,
lastName: user.lastName,
Expand Down
Original file line number Diff line number Diff line change
@@ -1,37 +1,49 @@
import { Injectable } from '@nestjs/common';
import { HttpService } from '@nestjs/axios';
import { firstValueFrom } from 'rxjs';
import { Injectable, Logger } from '@nestjs/common';
import { AxiosError } from 'axios';
import { HubspotIdentifyFormCommand } from './hubspot-identify-form.command';

const LOG_CONTEXT = 'HubspotIdentifyFormUsecase';

@Injectable()
export class HubspotIdentifyFormUsecase {
private readonly hubspotPortalId = '44416662';
private readonly hubspotFormId = 'fc39aa98-4285-4322-9514-52da978baae8';

constructor(private httpService: HttpService) {}

async execute(command: HubspotIdentifyFormCommand): Promise<{ success: boolean; hubspotResponse?: any }> {
const hubspotSubmitUrl = `https://api.hsforms.com/submissions/v3/integration/submit/${this.hubspotPortalId}/${this.hubspotFormId}`;

const hubspotData = {
fields: [
{ name: 'email', value: command.email },
{ name: 'lastname', value: command.lastName || 'Unknown' },
{ name: 'firstname', value: command.firstName || 'Unknown' },
{ name: 'app_organizationid', value: command.organizationId },
],
context: {
hutk: command.hubspotContext,
pageUri: command.pageUri,
pageName: command.pageName,
},
};
async execute(command: HubspotIdentifyFormCommand) {
try {
const hubspotSubmitUrl = `https://api.hsforms.com/submissions/v3/integration/submit/${this.hubspotPortalId}/${this.hubspotFormId}`;

const response = await firstValueFrom(this.httpService.post(hubspotSubmitUrl, hubspotData));
const hubspotData = {
fields: [
{ name: 'email', value: command.email },
{ name: 'lastname', value: command.lastName || 'Unknown' },
{ name: 'firstname', value: command.firstName || 'Unknown' },
{ name: 'app_organizationid', value: command.organizationId },
],
context: {
hutk: command.hubspotContext,
pageUri: command.pageUri,
pageName: command.pageName,
},
};

return {
success: true,
hubspotResponse: response.data,
};
await this.httpService.post(hubspotSubmitUrl, hubspotData);
} catch (error) {
if (error instanceof AxiosError) {
Logger.error(
`Failed to submit to Hubspot message=${error.message}, status=${error.status}`,
{
organizationId: command.organizationId,
response: error.response?.data,
},
LOG_CONTEXT
);
} else {
throw error;
}
}
}
}
5 changes: 2 additions & 3 deletions apps/api/src/app/bridge/usecases/sync/sync.usecase.ts
Original file line number Diff line number Diff line change
Expand Up @@ -255,7 +255,7 @@ export class Sync {
commandWorkflowSteps: DiscoverStepOutput[],
workflow?: NotificationTemplateEntity | undefined
): NotificationStep[] {
const steps: NotificationStep[] = commandWorkflowSteps.map((step) => {
return commandWorkflowSteps.map((step) => {
const foundStep = workflow?.steps?.find((workflowStep) => workflowStep.stepId === step.stepId);

const template = {
Expand All @@ -273,11 +273,10 @@ export class Sync {
name: step.stepId,
stepId: step.stepId,
uuid: step.stepId,
_templateId: foundStep?._templateId,
shouldStopOnFail: this.castToAnyNotSupportedParam(step.options)?.failOnErrorEnabled ?? false,
};
});

return steps;
}

private async getNotificationGroup(
Expand Down
101 changes: 99 additions & 2 deletions apps/api/src/app/events/e2e/bridge-sync.e2e.ts
Original file line number Diff line number Diff line change
@@ -1,7 +1,12 @@
import { UserSession } from '@novu/testing';
import { expect } from 'chai';
import { EnvironmentRepository, NotificationTemplateRepository, MessageTemplateRepository } from '@novu/dal';
import { FeatureFlagsKeysEnum, WorkflowTypeEnum } from '@novu/shared';
import {
EnvironmentRepository,
NotificationTemplateRepository,
MessageTemplateRepository,
ControlValuesRepository,
} from '@novu/dal';
import { WorkflowTypeEnum } from '@novu/shared';
import { workflow } from '@novu/framework';
import { BridgeServer } from '../../../../e2e/bridge.server';

Expand All @@ -10,6 +15,7 @@ describe('Bridge Sync - /bridge/sync (POST)', async () => {
const environmentRepository = new EnvironmentRepository();
const workflowsRepository = new NotificationTemplateRepository();
const messageTemplateRepository = new MessageTemplateRepository();
const controlValuesRepository = new ControlValuesRepository();

const inputPostPayload = {
schema: {
Expand Down Expand Up @@ -553,4 +559,95 @@ describe('Bridge Sync - /bridge/sync (POST)', async () => {
const workflowDataWithName = workflowsWithDescription[0];
expect(workflowDataWithName.description).to.equal('');
});

it('should preserve control values across workflow syncs', async () => {
const workflowId = 'My Workflow';
const spaceSeparatedIdWorkflow = workflow(workflowId, async ({ step }) => {
await step.email('send-email', () => ({
subject: 'Welcome!',
body: 'Hello there',
}));
});
await bridgeServer.start({ workflows: [spaceSeparatedIdWorkflow] });

const firstSyncResponse = await session.testAgent.post(`/v1/bridge/sync`).send({
bridgeUrl: bridgeServer.serverPath,
});
expect(firstSyncResponse.body.data?.length).to.equal(1);

const firstWorkflowCountResponse = await workflowsRepository.count({ _environmentId: session.environment._id });
expect(firstWorkflowCountResponse).to.equal(1);

const firstWorkflowResponse = await workflowsRepository.findById(
firstSyncResponse.body.data[0]._id,
session.environment._id
);

expect(firstWorkflowResponse).to.be.ok;
if (!firstWorkflowResponse) {
throw new Error('Workflow not found');
}

expect(firstWorkflowResponse.name).to.equal(workflowId);
expect(firstWorkflowResponse.type).to.equal(WorkflowTypeEnum.BRIDGE);
expect(firstWorkflowResponse.rawData.workflowId).to.equal(workflowId);
expect(firstWorkflowResponse.triggers[0].identifier).to.equal(workflowId);

expect(firstWorkflowResponse.steps.length).to.equal(1);
expect(firstWorkflowResponse.steps[0].stepId).to.equal('send-email');
expect(firstWorkflowResponse.steps[0]._templateId).to.exist;

await session.testAgent.put(`/v1/bridge/controls/${workflowId}/send-email`).send({
variables: { subject: 'Hello World again' },
});

const firstControlValueResponse = await controlValuesRepository.find({
_environmentId: session.environment._id,
_workflowId: firstWorkflowResponse._id,
});

expect(firstControlValueResponse.length).to.equal(1);
expect(firstControlValueResponse[0].controls.subject).to.equal('Hello World again');

const firstStepResponse = await session.testAgent.get(`/v1/bridge/controls/${workflowId}/send-email`);
expect(firstStepResponse.body.data.controls.subject).to.equal('Hello World again');

const secondSyncResponse = await session.testAgent.post(`/v1/bridge/sync`).send({
bridgeUrl: bridgeServer.serverPath,
});
expect(secondSyncResponse.body.data?.length).to.equal(1);

const secondWorkflowCountResponse = await workflowsRepository.count({ _environmentId: session.environment._id });
expect(secondWorkflowCountResponse).to.equal(1);

const secondWorkflowResponse = await workflowsRepository.findById(
firstSyncResponse.body.data[0]._id,
session.environment._id
);

expect(secondWorkflowResponse).to.be.ok;
if (!secondWorkflowResponse) {
throw new Error('Workflow not found');
}

expect(secondWorkflowResponse.name).to.equal(workflowId);
expect(secondWorkflowResponse.type).to.equal(WorkflowTypeEnum.BRIDGE);
expect(secondWorkflowResponse.rawData.workflowId).to.equal(workflowId);
expect(secondWorkflowResponse.triggers[0].identifier).to.equal(workflowId);

expect(secondWorkflowResponse.steps.length).to.equal(1);
expect(secondWorkflowResponse.steps[0].stepId).to.equal('send-email');
expect(secondWorkflowResponse.steps[0]._templateId).to.exist;

const secondControlValueResponse = await controlValuesRepository.find({
_environmentId: session.environment._id,
_workflowId: secondWorkflowResponse._id,
});

expect(secondControlValueResponse.length).to.equal(1);
expect(secondControlValueResponse[0].controls.subject).to.equal('Hello World again');

const secondStepResponse = await session.testAgent.get(`/v1/bridge/controls/${workflowId}/send-email`);
expect(secondStepResponse.body.data.controls.subject).to.equal('Hello World again');
});
});
Original file line number Diff line number Diff line change
Expand Up @@ -5,6 +5,7 @@ import {
GetSubscriberPreferenceCommand,
GetSubscriberGlobalPreference,
GetSubscriberGlobalPreferenceCommand,
InstrumentUsecase,
} from '@novu/application-generic';
import { PreferenceLevelEnum } from '@novu/shared';
import { AnalyticsEventsEnum } from '../../utils';
Expand All @@ -19,6 +20,7 @@ export class GetInboxPreferences {
private getSubscriberPreference: GetSubscriberPreference
) {}

@InstrumentUsecase()
async execute(command: GetInboxPreferencesCommand): Promise<InboxPreference[]> {
const globalPreference = await this.getSubscriberGlobalPreference.execute(
GetSubscriberGlobalPreferenceCommand.create({
Expand Down
Loading

0 comments on commit 22f1bc1

Please sign in to comment.