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

feat(root): include local tunnel work #5698

Merged
merged 22 commits into from
Jun 11, 2024
Merged
Show file tree
Hide file tree
Changes from 1 commit
Commits
Show all changes
22 commits
Select commit Hold shift + click to select a range
10dfc49
feat: include local tunnel work
denis-kralj-novu Jun 7, 2024
b20a0ed
feat: include local tunnel work
denis-kralj-novu Jun 7, 2024
0420589
feat: include local tunnel work
denis-kralj-novu Jun 9, 2024
7bad5cd
feat: include local tunnel work
denis-kralj-novu Jun 9, 2024
0244ab6
feat: include local tunnel work
denis-kralj-novu Jun 9, 2024
d503dab
feat: include local tunnel work
denis-kralj-novu Jun 10, 2024
655c795
feat: include local tunnel work
denis-kralj-novu Jun 10, 2024
677377c
feat: include local tunnel work
denis-kralj-novu Jun 10, 2024
3f62384
feat: include local tunnel work
denis-kralj-novu Jun 10, 2024
90c774c
feat: include local tunnel work
denis-kralj-novu Jun 10, 2024
71ad0f2
feat: include local tunnel work
denis-kralj-novu Jun 11, 2024
f1b951b
Merge branch 'next' into include-local-tunnel
denis-kralj-novu Jun 11, 2024
3fdff04
feat: include local tunnel work
denis-kralj-novu Jun 11, 2024
45a3d2d
feat: include local tunnel work
denis-kralj-novu Jun 11, 2024
4f42449
feat: include local tunnel work
denis-kralj-novu Jun 11, 2024
9ec063d
Merge branch 'next' into include-local-tunnel
denis-kralj-novu Jun 11, 2024
cd0d119
feat: include local tunnel work
denis-kralj-novu Jun 11, 2024
7e8600b
Merge branch 'next' into include-local-tunnel
denis-kralj-novu Jun 11, 2024
4792379
feat: include local tunnel work
denis-kralj-novu Jun 11, 2024
afe74ea
Merge branch 'next' into include-local-tunnel
denis-kralj-novu Jun 11, 2024
3285b63
feat: include local tunnel work
denis-kralj-novu Jun 11, 2024
f8c1d84
feat: include local tunnel work
denis-kralj-novu Jun 11, 2024
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
2 changes: 1 addition & 1 deletion .source
2 changes: 2 additions & 0 deletions apps/api/src/.example.env
Original file line number Diff line number Diff line change
Expand Up @@ -78,3 +78,5 @@ API_RATE_LIMIT_MAXIMUM_UNLIMITED_GLOBAL=

HUBSPOT_INVITE_NUDGE_EMAIL_USER_LIST_ID=
HUBSPOT_PRIVATE_APP_ACCESS_TOKEN=

TUNNEL_BASE_ADDRESS=
Original file line number Diff line number Diff line change
Expand Up @@ -6,6 +6,11 @@ export class InBoundParseDomainDto {
inboundParseDomain?: string;
}

export class BridgeConfigurationDto {
@ApiPropertyOptional({ type: String })
url?: string;
}

export class UpdateEnvironmentRequestDto {
@ApiProperty()
@IsOptional()
Expand All @@ -26,4 +31,9 @@ export class UpdateEnvironmentRequestDto {
type: InBoundParseDomainDto,
})
dns?: InBoundParseDomainDto;

@ApiPropertyOptional({
type: BridgeConfigurationDto,
})
bridge?: BridgeConfigurationDto;
}
1 change: 1 addition & 0 deletions apps/api/src/app/environments/environments.controller.ts
Original file line number Diff line number Diff line change
Expand Up @@ -116,6 +116,7 @@ export class EnvironmentsController {
identifier: payload.identifier,
_parentId: payload.parentId,
dns: payload.dns,
bridge: payload.bridge,
})
);
}
Expand Down
Original file line number Diff line number Diff line change
@@ -1,9 +1,10 @@
import { nanoid } from 'nanoid';
import { Injectable } from '@nestjs/common';
import { Injectable, InternalServerErrorException, Logger } from '@nestjs/common';
import { ModuleRef } from '@nestjs/core';
import { createHash } from 'crypto';

import { EnvironmentRepository } from '@novu/dal';
import { encryptApiKey } from '@novu/application-generic';
import { ApiException, encryptApiKey, buildBridgeEndpointUrl } from '@novu/application-generic';

import { CreateEnvironmentCommand } from './create-environment.command';
import { GenerateUniqueApiKey } from '../generate-unique-api-key/generate-unique-api-key.usecase';
Expand All @@ -18,7 +19,8 @@ export class CreateEnvironment {
private environmentRepository: EnvironmentRepository,
private createNotificationGroup: CreateNotificationGroup,
private generateUniqueApiKey: GenerateUniqueApiKey,
private createDefaultLayoutUsecase: CreateDefaultLayout
private createDefaultLayoutUsecase: CreateDefaultLayout,
protected moduleRef: ModuleRef
) {}

async execute(command: CreateEnvironmentCommand) {
Expand All @@ -40,6 +42,10 @@ export class CreateEnvironment {
],
});

if (command.name === 'Development') {
await this.storeDefaultTunnelUrl(command.userId, command.organizationId, environment._id, key);
}

if (!command.parentEnvironmentId) {
await this.createNotificationGroup.execute(
CreateNotificationGroupCommand.create({
Expand All @@ -61,4 +67,35 @@ export class CreateEnvironment {

return environment;
}

private async storeDefaultTunnelUrl(userId: string, organizationId: string, environmentId: string, apiKey: string) {
try {
if (process.env.NOVU_ENTERPRISE === 'true' || process.env.CI_EE_TEST === 'true') {
if (!require('@novu/ee-echo-api')?.StoreBridgeConfiguration) {
throw new ApiException('Echo api module is not loaded');
}

const baseUrl = process.env.TUNNEL_BASE_ADDRESS;

if (baseUrl === undefined || baseUrl === '') {
throw new InternalServerErrorException('Base tunnel url not configured');
}

const bridgeUrl = buildBridgeEndpointUrl(apiKey, baseUrl);

const usecase = this.moduleRef.get(require('@novu/ee-echo-api')?.StoreBridgeConfiguration, {
strict: false,
});

await usecase.execute({
userId,
organizationId,
environmentId,
bridgeUrl,
});
}
} catch (e) {
Logger.error(e, `Unexpected error while importing enterprise modules`, 'StoreBridgeConfiguration');
}
}
}
Original file line number Diff line number Diff line change
Expand Up @@ -22,4 +22,7 @@ export class UpdateEnvironmentCommand extends OrganizationCommand {

@IsOptional()
dns?: { inboundParseDomain?: string };

@IsOptional()
bridge?: { url?: string };
}
Original file line number Diff line number Diff line change
@@ -0,0 +1,25 @@
import { UserSession } from '@novu/testing';
import { expect } from 'chai';
import { UpdateEnvironmentRequestDto } from '../../dtos/update-environment-request.dto';

describe('Update Environment - /environments (PUT)', async () => {
let session: UserSession;

before(async () => {
session = new UserSession();
await session.initialize();
});

it('should update bridge data correctly', async () => {
const updatePayload: UpdateEnvironmentRequestDto = {
name: 'Development',
bridge: { url: 'http://example.com' },
};

await session.testAgent.put(`/v1/environments/${session.environment._id}`).send(updatePayload).expect(200);
const { body } = await session.testAgent.get('/v1/environments/me');

expect(body.data.name).to.eq(updatePayload.name);
expect(body.data.echo.url).to.equal(updatePayload.bridge?.url);
});
});
Original file line number Diff line number Diff line change
@@ -1,4 +1,3 @@
import { EnvironmentEntity, EnvironmentRepository } from '@novu/dal';
import { UserSession } from '@novu/testing';
import { expect } from 'chai';
import { UpdateEnvironmentRequestDto } from '../../dtos/update-environment-request.dto';
Expand Down
Original file line number Diff line number Diff line change
Expand Up @@ -20,10 +20,19 @@ export class UpdateEnvironment {
updatePayload.identifier = command.identifier;
}

if (command.dns && command.dns.inboundParseDomain !== '') {
if (command.dns && command.dns.inboundParseDomain && command.dns.inboundParseDomain !== '') {
rifont marked this conversation as resolved.
Show resolved Hide resolved
updatePayload[`dns.inboundParseDomain`] = command.dns.inboundParseDomain;
}

if (
(await this.shouldUpdateEchoConfiguration(command)) &&
command.bridge &&
command.bridge.url &&
command.bridge.url !== ''
) {
updatePayload['echo.url'] = command.bridge.url;
}

return await this.environmentRepository.update(
{
_id: command.environmentId,
Expand All @@ -32,4 +41,24 @@ export class UpdateEnvironment {
{ $set: updatePayload }
);
}
async shouldUpdateEchoConfiguration(command: UpdateEnvironmentCommand): Promise<boolean> {
if (process.env.NOVU_ENTERPRISE === 'true' || process.env.CI_EE_TEST === 'true') {
Copy link
Contributor Author

Choose a reason for hiding this comment

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

I initially thought that I could use the command that I defined in the ee package for updating the endpoint. However, this would then lead to a situation where that command would return the results of the update, wheres the previous set of adjustments in the caller would have its own result. I could do consolidation logic since it is aiming via environmentId, but that seemed a bit messy. Let me know if that would still be preferable to this way of updating the data.

Copy link
Contributor Author

Choose a reason for hiding this comment

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

@rifont let me hear your thoughts on this as well

Copy link
Collaborator

Choose a reason for hiding this comment

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

I think this is a suitable approach for the time being 👍

let name: string;
if (command.name && command.name !== '') {
name = command.name;
} else {
const env = await this.environmentRepository.findOne({ _id: command.environmentId });

if (!env) {
return false;
}

name = env.name;
}

return name === 'Development';
} else {
return false;
}
}
}
2 changes: 1 addition & 1 deletion apps/web/package.json
Original file line number Diff line number Diff line change
Expand Up @@ -5,7 +5,7 @@
"scripts": {
"start": "pnpm panda --watch & cross-env NODE_OPTIONS=--max_old_space_size=8192 PORT=4200 react-app-rewired start",
"prebuild": "rimraf build",
"build": "pnpm panda && cross-env NODE_OPTIONS=--max_old_space_size=4096 GENERATE_SOURCEMAP=false react-app-rewired --max_old_space_size=4096 build",
"build": "pnpm panda && cross-env NODE_OPTIONS=--max_old_space_size=8192 GENERATE_SOURCEMAP=false react-app-rewired --max_old_space_size=8192 build",
denis-kralj-novu marked this conversation as resolved.
Show resolved Hide resolved
"build:test": "pnpm panda && cross-env REACT_APP_HUBSPOT_EMBED=44416662 REACT_APP_API_URL=http://127.0.0.1:1336 REACT_APP_LAUNCH_DARKLY_CLIENT_SIDE_ID=some_fake_id NODE_OPTIONS=--max_old_space_size=4096 GENERATE_SOURCEMAP=false react-app-rewired --max_old_space_size=4096 build",
"precommit": "lint-staged",
"docker:build": "docker buildx build --load -f ./Dockerfile -t novu-web ./../.. $DOCKER_BUILD_ARGUMENTS",
Expand Down
1 change: 1 addition & 0 deletions libs/application-generic/src/index.ts
Original file line number Diff line number Diff line change
Expand Up @@ -21,6 +21,7 @@ export * from './utils/hmac';
export * from './utils/novu-integrations';
export * from './utils/require-inject';
export * from './utils/variants';
export * from './utils/buildBridgeEndpointUrl';
export * from './decorators';
export * from './tracing';
export * from './dtos';
Expand Down
19 changes: 19 additions & 0 deletions libs/application-generic/src/utils/buildBridgeEndpointUrl.ts
Original file line number Diff line number Diff line change
@@ -0,0 +1,19 @@
import { createHash } from 'crypto';

/*
* Creates a bridge endpoint url to be used for request from novu cloud to the local
* workflow definition
*/
export const buildBridgeEndpointUrl = (
apiKey: string,
baseAddress: string
): string => {
return `${buildBridgeSubdomain(apiKey)}.${baseAddress}`;
};

/*
* Creates a bridge subdomain based on the apiKey provided
*/
export const buildBridgeSubdomain = (apiKey: string): string => {
return createHash('md5').update(apiKey).digest('hex');
};
denis-kralj-novu marked this conversation as resolved.
Show resolved Hide resolved
2 changes: 1 addition & 1 deletion package.json
Original file line number Diff line number Diff line change
Expand Up @@ -13,7 +13,7 @@
"clean": "lerna clean --yes && rimraf node_modules",
"commit": "cz",
"nx": "nx",
"lint-staged": "lint-staged",
"lint-staged": "NODE_OPTIONS=--max_old_space_size=8192 lint-staged",
denis-kralj-novu marked this conversation as resolved.
Show resolved Hide resolved
"generate:provider": "cd libs/automation && npm run generate:provider",
"lint": "nx run-many --target=lint --all",
"test": "cross-env CI=true lerna run test:watch --parallel",
Expand Down
8 changes: 7 additions & 1 deletion packages/create-novu-app/create-app.ts
Original file line number Diff line number Diff line change
Expand Up @@ -33,6 +33,8 @@ export async function createApp({
eslint,
srcDir,
importAlias,
apiKey,
tunnelHost,
}: {
appPath: string;
packageManager: PackageManager;
Expand All @@ -43,6 +45,8 @@ export async function createApp({
eslint: boolean;
srcDir: boolean;
importAlias: string;
apiKey: string;
tunnelHost: string;
}): Promise<void> {
let repoInfo: RepoInfo | undefined;
const mode: TemplateMode = typescript ? 'ts' : 'js';
Expand Down Expand Up @@ -121,7 +125,7 @@ export async function createApp({
const isOnline = !useYarn || (await getOnline());
const originalDirectory = process.cwd();

console.log(`Creating a new Echo app in ${green(root)}.`);
console.log(`Creating a new Novu app in ${green(root)}.`);
console.log();

process.chdir(root);
Expand Down Expand Up @@ -193,6 +197,8 @@ export async function createApp({
eslint,
srcDir,
importAlias,
apiKey,
tunnelHost,
});
}

Expand Down
31 changes: 29 additions & 2 deletions packages/create-novu-app/index.ts
Original file line number Diff line number Diff line change
Expand Up @@ -19,6 +19,8 @@ let projectPath = '';

const handleSigTerm = () => process.exit(0);

const defaultTunnelHost = 'https://localtunnel.me';
denis-kralj-novu marked this conversation as resolved.
Show resolved Hide resolved

process.on('SIGINT', handleSigTerm);
process.on('SIGTERM', handleSigTerm);

Expand All @@ -41,6 +43,22 @@ const program = new Commander.Command(packageJson.name)
.action((name) => {
projectPath = name;
})
.option(
'-k, --api-key [apiKey]',
`

Your Novu Development environment apiKey. Note that your novu app won't
work with a non development environment apiKey.
`
)
.option(
'-th, --tunnel-host',
denis-kralj-novu marked this conversation as resolved.
Show resolved Hide resolved
`

Set's the tunnel host url that will be used to request local tunnels,
defaults to ${defaultTunnelHost}
`
)
.option(
'--ts, --typescript',
`
Expand Down Expand Up @@ -113,7 +131,7 @@ const packageManager = !!program.useNpm
: getPkgManager();

async function run(): Promise<void> {
const conf = new Conf({ projectName: 'create-echo-app' });
const conf = new Conf({ projectName: 'create-novu-app' });

if (program.resetPreferences) {
conf.clear();
Expand All @@ -132,7 +150,7 @@ async function run(): Promise<void> {
type: 'text',
name: 'path',
message: 'What is your project named?',
initial: 'my-echo-app',
initial: 'my-novu-app',
validate: (name) => {
const validation = validateNpmName(path.basename(path.resolve(name)));
if (validation.valid) {
Expand Down Expand Up @@ -175,6 +193,11 @@ async function run(): Promise<void> {
process.exit(1);
}

if (program.apiKey === true || !program.apiKey) {
console.error('Please provide a valid apiKey value.');
process.exit(1);
}

/**
* Verify the project dir is empty or doesn't exist
*/
Expand Down Expand Up @@ -260,6 +283,8 @@ async function run(): Promise<void> {
eslint: program.eslint,
srcDir: program.srcDir,
importAlias: program.importAlias,
apiKey: program.apiKey,
tunnelHost: program.tunnelHost ? program.tunnelHost : defaultTunnelHost,
});
} catch (reason) {
if (!(reason instanceof DownloadError)) {
Expand Down Expand Up @@ -287,6 +312,8 @@ async function run(): Promise<void> {
reactEmail: program.reactEmail,
srcDir: program.srcDir,
importAlias: program.importAlias,
apiKey: program.apiKey,
tunnelHost: program.tunnelHost ? program.tunnelHost : defaultTunnelHost,
});
}
conf.set('preferences', preferences);
Expand Down
1 change: 1 addition & 0 deletions packages/create-novu-app/package.json
Original file line number Diff line number Diff line change
Expand Up @@ -27,6 +27,7 @@
"release:alpha": "npm i && npm run build && npm version prerelease --preid=alpha && npm publish"
},
"devDependencies": {
"@novu/application-generic": "workspace:*",
"@types/async-retry": "1.4.2",
"@types/ci-info": "2.0.0",
"@types/cross-spawn": "6.0.0",
Expand Down
denis-kralj-novu marked this conversation as resolved.
Show resolved Hide resolved
Original file line number Diff line number Diff line change
@@ -0,0 +1,34 @@
import LocalTunnelWrapper from "./tunnelWrapper.mjs";
import dotenv from "dotenv";

dotenv.config();

(async () => {
if (
process.env.PORT === undefined ||
Number.isNaN(parseInt(process.env.PORT))
) {
throw new Error(
"POST environment variable value is required and should be a positive integer",
);
}

if (
process.env.TUNNEL_SUBDOMAIN === undefined ||
process.env.TUNNEL_SUBDOMAIN === ""
) {
throw new Error("TUNNEL_SUBDOMAIN environment variable value is required");
}

if (process.env.TUNNEL_HOST === undefined || process.env.TUNNEL_HOST === "") {
throw new Error("TUNNEL_HOST environment variable value is required");
}
const port = parseInt(process.env.PORT);
const wrapper = new LocalTunnelWrapper(
port,
process.env.TUNNEL_SUBDOMAIN,
process.env.TUNNEL_HOST,
);
await wrapper.connect();
console.log(wrapper.getUrl());
})();
Loading
Loading