Skip to content

Commit 742c781

Browse files
authored
Merge branch 'next' into nv-5616-do-not-create-pill-for-payload-or-stepsdigesteventspayload-pill
2 parents 8496662 + 1c2df1d commit 742c781

File tree

126 files changed

+1097
-1017
lines changed

Some content is hidden

Large Commits have some content hidden by default. Use the searchbox below for content that may be hidden.

126 files changed

+1097
-1017
lines changed
Original file line numberDiff line numberDiff line change
@@ -0,0 +1,61 @@
1+
name: Validate Submodule Sync Post-Merge
2+
3+
# This workflow validates submodule synchronization specifically after merges to main branches
4+
# It runs separately from the PR/push validation to provide dedicated post-merge verification
5+
# Logic:
6+
# 1. Triggers only on successful merges (push events with merged PRs)
7+
# 2. Checks if SUBMODULES_TOKEN secret exists
8+
# 3. If token exists, validates that submodules are properly synchronized
9+
# 4. Can provide notifications or take corrective actions if issues are found
10+
11+
on:
12+
push:
13+
branches:
14+
- next
15+
- main
16+
- prod
17+
18+
jobs:
19+
check_submodule_token:
20+
name: Check if submodule token exists
21+
runs-on: ubuntu-latest
22+
outputs:
23+
has_token: ${{ steps.secret-check.outputs.has_token }}
24+
steps:
25+
- name: Check if secret exists
26+
id: secret-check
27+
run: |
28+
if [[ -n "${{ secrets.SUBMODULES_TOKEN }}" ]]; then
29+
echo "has_token=true" >> $GITHUB_OUTPUT
30+
else
31+
echo "has_token=false" >> $GITHUB_OUTPUT
32+
fi
33+
34+
validate-submodule-sync:
35+
runs-on: ubuntu-latest
36+
needs: [check_submodule_token]
37+
if: needs.check_submodule_token.outputs.has_token == 'true'
38+
39+
steps:
40+
- name: Checkout repository
41+
uses: actions/checkout@v4
42+
with:
43+
fetch-depth: 0
44+
submodules: true
45+
token: ${{ secrets.SUBMODULES_TOKEN }}
46+
47+
- name: Run validation script
48+
run: |
49+
# Ensure the script is executable
50+
chmod +x .github/workflows/scripts/validate-submodule-sync.sh
51+
52+
# Run the script with the current branch as reference
53+
.github/workflows/scripts/validate-submodule-sync.sh ${GITHUB_REF#refs/heads/}
54+
env:
55+
SUBMODULES_TOKEN: ${{ secrets.SUBMODULES_TOKEN }}
56+
57+
- name: Send Slack notification on failure
58+
if: failure()
59+
uses: ./.github/actions/slack-notify-on-failure
60+
with:
61+
slackWebhookURL: ${{ secrets.SLACK_WEBHOOK_URL_ENG_FEED_GITHUB }}

.github/workflows/validate-submodule-sync.yaml renamed to .github/workflows/check-submodule-sync-pr.yaml

+1-1
Original file line numberDiff line numberDiff line change
@@ -24,7 +24,7 @@ on:
2424

2525
jobs:
2626
check_submodule_token:
27-
name: Check if the secret exists or not.
27+
name: Check if submodule token exists
2828
runs-on: ubuntu-latest
2929
outputs:
3030
has_token: ${{ steps.secret-check.outputs.has_token }}

apps/api/package.json

+2-1
Original file line numberDiff line numberDiff line change
@@ -27,7 +27,7 @@
2727
"generate:sdk": " (cd ../../libs/internal-sdk && speakeasy run --skip-compile --minimal --skip-versioning) && (cd ../../libs/internal-sdk && pnpm build) ",
2828
"test": "cross-env TS_NODE_COMPILER_OPTIONS='{\"strictNullChecks\": false}' NODE_ENV=test mocha --timeout 5000 --require ts-node/register --exit 'src/**/*.spec.ts'",
2929
"test:e2e:novu-v0": "cross-env TS_NODE_COMPILER_OPTIONS='{\"strictNullChecks\": false}' NODE_ENV=test mocha --timeout 5000 --retries 3 --grep '#novu-v0' --require ts-node/register --exit --file e2e/setup.ts src/**/*.e2e{,-ee}.ts",
30-
"test:e2e:novu-v2": "cross-env TS_NODE_COMPILER_OPTIONS='{\"strictNullChecks\": false}' NODE_ENV=test CI_EE_TEST=true CLERK_ENABLED=true NODE_OPTIONS=--max_old_space_size=8192 mocha --bail --timeout 5000 --retries 3 --grep '#novu-v2' --require ts-node/register --exit --file e2e/setup.ts src/**/*.e2e{,-ee}.ts",
30+
"test:e2e:novu-v2": "cross-env TS_NODE_COMPILER_OPTIONS='{\"strictNullChecks\": false}' NODE_ENV=test CI_EE_TEST=true CLERK_ENABLED=true NODE_OPTIONS=--max_old_space_size=8192 mocha --bail --timeout 10000 --retries 3 --grep '#novu-v2' --require ts-node/register --exit --file e2e/setup.ts src/**/*.e2e{,-ee}.ts",
3131
"migration": "cross-env NODE_ENV=local MIGRATION=true ts-node --transpileOnly",
3232
"link:submodules": "pnpm link ../../enterprise/packages/auth && pnpm link ../../enterprise/packages/translation && pnpm link ../../enterprise/packages/billing",
3333
"admin:remove-user-account": "cross-env NODE_ENV=local MIGRATION=true ts-node --transpileOnly ./admin/remove-user-account.ts",
@@ -128,6 +128,7 @@
128128
"@types/supertest": "^2.0.8",
129129
"async": "^3.2.0",
130130
"chai": "^4.2.0",
131+
"chai-subset": "^1.6.0",
131132
"express": "^5.0.1",
132133
"get-port": "^5.1.1",
133134
"mocha": "^10.2.0",

apps/api/src/app/auth/auth.controller.ts

+1-2
Original file line numberDiff line numberDiff line change
@@ -33,7 +33,6 @@ import { PasswordResetRequestCommand } from './usecases/password-reset-request/p
3333
import { PasswordResetRequest } from './usecases/password-reset-request/password-reset-request.usecase';
3434
import { PasswordResetCommand } from './usecases/password-reset/password-reset.command';
3535
import { PasswordReset } from './usecases/password-reset/password-reset.usecase';
36-
import { ApiException } from '../shared/exceptions/api.exception';
3736
import { PasswordResetBodyDto, PasswordResetRequestBodyDto } from './dtos/password-reset.dto';
3837
import { ApiCommonResponses } from '../shared/framework/response.decorator';
3938
import { UpdatePasswordBodyDto } from './dtos/update-password.dto';
@@ -70,7 +69,7 @@ export class AuthController {
7069
Logger.verbose('Checking Github Auth');
7170

7271
if (!process.env.GITHUB_OAUTH_CLIENT_ID || !process.env.GITHUB_OAUTH_CLIENT_SECRET) {
73-
throw new ApiException(
72+
throw new BadRequestException(
7473
'GitHub auth is not configured, please provide GITHUB_OAUTH_CLIENT_ID and GITHUB_OAUTH_CLIENT_SECRET as env variables'
7574
);
7675
}

apps/api/src/app/auth/services/community.auth.service.ts

+9-3
Original file line numberDiff line numberDiff line change
@@ -1,5 +1,12 @@
11
import { createHash } from 'crypto';
2-
import { forwardRef, Inject, Injectable, NotFoundException, UnauthorizedException } from '@nestjs/common';
2+
import {
3+
forwardRef,
4+
Inject,
5+
Injectable,
6+
NotFoundException,
7+
UnauthorizedException,
8+
BadRequestException,
9+
} from '@nestjs/common';
310
import { JwtService } from '@nestjs/jwt';
411

512
import {
@@ -25,7 +32,6 @@ import {
2532

2633
import {
2734
AnalyticsService,
28-
ApiException,
2935
buildSubscriberKey,
3036
buildUserKey,
3137
CachedResponse,
@@ -147,7 +153,7 @@ export class CommunityAuthService implements IAuthService {
147153
);
148154

149155
const dbUser = await this.userRepository.findById(user._id);
150-
if (!dbUser) throw new ApiException('User not found');
156+
if (!dbUser) throw new BadRequestException('User not found');
151157
// eslint-disable-next-line no-param-reassign
152158
user = dbUser;
153159
}

apps/api/src/app/auth/usecases/login/login.usecase.ts

+2-3
Original file line numberDiff line numberDiff line change
@@ -1,12 +1,11 @@
11
import bcrypt from 'bcrypt';
2-
import { Injectable, UnauthorizedException } from '@nestjs/common';
2+
import { Injectable, UnauthorizedException, BadRequestException } from '@nestjs/common';
33
import { differenceInMinutes, parseISO } from 'date-fns';
44
import { UserRepository, UserEntity, OrganizationRepository } from '@novu/dal';
55
import { AnalyticsService, createHash } from '@novu/application-generic';
66
import { normalizeEmail } from '@novu/shared';
77
import { AuthService } from '../../services/auth.service';
88
import { LoginCommand } from './login.command';
9-
import { ApiException } from '../../../shared/exceptions/api.exception';
109

1110
@Injectable()
1211
export class Login {
@@ -44,7 +43,7 @@ export class Login {
4443
}
4544

4645
// TODO: Trigger a password reset flow automatically for existing OAuth users instead of throwing an error
47-
if (!user.password) throw new ApiException('Please sign in using Github.');
46+
if (!user.password) throw new BadRequestException('Please sign in using Github.');
4847

4948
const isMatching = await bcrypt.compare(command.password, user.password);
5049
if (!isMatching) {

apps/api/src/app/auth/usecases/password-reset/password-reset.usecase.ts

+3-4
Original file line numberDiff line numberDiff line change
@@ -1,11 +1,10 @@
1-
import { Injectable } from '@nestjs/common';
1+
import { Injectable, BadRequestException } from '@nestjs/common';
22
import { hash } from 'bcrypt';
33
import { isBefore, subDays } from 'date-fns';
44
import { UserRepository } from '@novu/dal';
55
import { InvalidateCacheService, buildUserKey } from '@novu/application-generic';
66
import { AuthService } from '../../services/auth.service';
77
import { PasswordResetCommand } from './password-reset.command';
8-
import { ApiException } from '../../../shared/exceptions/api.exception';
98

109
@Injectable()
1110
export class PasswordReset {
@@ -18,11 +17,11 @@ export class PasswordReset {
1817
async execute(command: PasswordResetCommand): Promise<{ token: string }> {
1918
const user = await this.userRepository.findUserByToken(command.token);
2019
if (!user) {
21-
throw new ApiException('Bad token provided');
20+
throw new BadRequestException('Bad token provided');
2221
}
2322

2423
if (user.resetTokenDate && isBefore(new Date(user.resetTokenDate), subDays(new Date(), 7))) {
25-
throw new ApiException('Token has expired');
24+
throw new BadRequestException('Token has expired');
2625
}
2726

2827
const passwordHash = await hash(command.password, 10);

apps/api/src/app/auth/usecases/register/user-register.usecase.ts

+3-4
Original file line numberDiff line numberDiff line change
@@ -1,11 +1,10 @@
1-
import { Injectable } from '@nestjs/common';
1+
import { Injectable, BadRequestException } from '@nestjs/common';
22
import { OrganizationEntity, UserRepository } from '@novu/dal';
33
import { hash } from 'bcrypt';
44
import { SignUpOriginEnum, normalizeEmail } from '@novu/shared';
55
import { AnalyticsService, createHash } from '@novu/application-generic';
66
import { AuthService } from '../../services/auth.service';
77
import { UserRegisterCommand } from './user-register.command';
8-
import { ApiException } from '../../../shared/exceptions/api.exception';
98
import { CreateOrganization } from '../../../organization/usecases/create-organization/create-organization.usecase';
109
import { CreateOrganizationCommand } from '../../../organization/usecases/create-organization/create-organization.command';
1110

@@ -19,11 +18,11 @@ export class UserRegister {
1918
) {}
2019

2120
async execute(command: UserRegisterCommand) {
22-
if (process.env.DISABLE_USER_REGISTRATION === 'true') throw new ApiException('Account creation is disabled');
21+
if (process.env.DISABLE_USER_REGISTRATION === 'true') throw new BadRequestException('Account creation is disabled');
2322

2423
const email = normalizeEmail(command.email);
2524
const existingUser = await this.userRepository.findByEmail(email);
26-
if (existingUser) throw new ApiException('User already exists');
25+
if (existingUser) throw new BadRequestException('User already exists');
2726

2827
const passwordHash = await hash(command.password, 10);
2928
const user = await this.userRepository.create({

apps/api/src/app/auth/usecases/switch-organization/switch-organization.usecase.ts

+3-4
Original file line numberDiff line numberDiff line change
@@ -1,6 +1,5 @@
1-
import { Injectable, UnauthorizedException } from '@nestjs/common';
1+
import { Injectable, UnauthorizedException, BadRequestException } from '@nestjs/common';
22
import { MemberRepository, UserRepository } from '@novu/dal';
3-
import { ApiException } from '../../../shared/exceptions/api.exception';
43
import { AuthService } from '../../services/auth.service';
54
import { SwitchOrganizationCommand } from './switch-organization.command';
65

@@ -22,10 +21,10 @@ export class SwitchOrganization {
2221
}
2322

2423
const member = await this.memberRepository.findMemberByUserId(command.newOrganizationId, command.userId);
25-
if (!member) throw new ApiException('Member not found');
24+
if (!member) throw new BadRequestException('Member not found');
2625

2726
const user = await this.userRepository.findById(command.userId);
28-
if (!user) throw new ApiException(`User ${command.userId} not found`);
27+
if (!user) throw new BadRequestException(`User ${command.userId} not found`);
2928

3029
const token = await this.authService.getSignedToken(user, command.newOrganizationId, member);
3130

apps/api/src/app/auth/usecases/update-password/update-password.usecase.ts

+3-4
Original file line numberDiff line numberDiff line change
@@ -1,9 +1,8 @@
1-
import { Injectable, UnauthorizedException } from '@nestjs/common';
1+
import { Injectable, UnauthorizedException, BadRequestException } from '@nestjs/common';
22
import { buildUserKey, InvalidateCacheService } from '@novu/application-generic';
33
import { UserRepository } from '@novu/dal';
44
import { hash, compare } from 'bcrypt';
55

6-
import { ApiException } from '../../../shared/exceptions/api.exception';
76
import { UpdatePasswordCommand } from './update-password.command';
87

98
@Injectable()
@@ -15,15 +14,15 @@ export class UpdatePassword {
1514

1615
async execute(command: UpdatePasswordCommand) {
1716
if (command.newPassword !== command.confirmPassword) {
18-
throw new ApiException('Passwords do not match.');
17+
throw new BadRequestException('Passwords do not match.');
1918
}
2019

2120
const user = await this.userRepository.findById(command.userId);
2221
if (!user) {
2322
throw new UnauthorizedException();
2423
}
2524
if (!user.password) {
26-
throw new ApiException('OAuth user cannot change password.');
25+
throw new BadRequestException('OAuth user cannot change password.');
2726
}
2827

2928
const isAuthorized = await compare(command.currentPassword, user.password);

apps/api/src/app/change/usecases/get-changes/get-changes.usecase.ts

+3-4
Original file line numberDiff line numberDiff line change
@@ -1,5 +1,5 @@
11
/* eslint-disable global-require */
2-
import { Injectable, Logger } from '@nestjs/common';
2+
import { Injectable, Logger, BadRequestException } from '@nestjs/common';
33
import {
44
ChangeEntity,
55
ChangeRepository,
@@ -13,7 +13,6 @@ import { ChangeEntityTypeEnum } from '@novu/shared';
1313
import { ModuleRef } from '@nestjs/core';
1414
import { ChangesResponseDto } from '../../dtos/change-response.dto';
1515
import { GetChangesCommand } from './get-changes.command';
16-
import { ApiException } from '../../../shared/exceptions/api.exception';
1716

1817
interface IViewEntity {
1918
templateName: string;
@@ -151,7 +150,7 @@ export class GetChanges {
151150
try {
152151
if (process.env.NOVU_ENTERPRISE === 'true' || process.env.CI_EE_TEST === 'true') {
153152
if (!require('@novu/ee-shared-services')?.TranslationsService) {
154-
throw new ApiException('Translation module is not loaded');
153+
throw new BadRequestException('Translation module is not loaded');
155154
}
156155
const service = this.moduleRef.get(require('@novu/ee-shared-services')?.TranslationsService, { strict: false });
157156
const { name, identifier } = await service.getTranslationGroupData(environmentId, entityId);
@@ -175,7 +174,7 @@ export class GetChanges {
175174
try {
176175
if (process.env.NOVU_ENTERPRISE === 'true' || process.env.CI_EE_TEST === 'true') {
177176
if (!require('@novu/ee-shared-services')?.TranslationsService) {
178-
throw new ApiException('Translation module is not loaded');
177+
throw new BadRequestException('Translation module is not loaded');
179178
}
180179
const service = this.moduleRef.get(require('@novu/ee-shared-services')?.TranslationsService, { strict: false });
181180
const { name, group } = await service.getTranslationData(environmentId, entityId);

apps/api/src/app/content-templates/content-templates.controller.ts

+2-3
Original file line numberDiff line numberDiff line change
@@ -1,11 +1,10 @@
11
/* eslint-disable global-require */
2-
import { Body, Controller, Logger, Post } from '@nestjs/common';
2+
import { Body, Controller, Logger, Post, BadRequestException } from '@nestjs/common';
33
import { ApiExcludeController } from '@nestjs/swagger';
44
import { format } from 'date-fns';
55
import i18next from 'i18next';
66
import { ModuleRef } from '@nestjs/core';
77
import {
8-
ApiException,
98
CompileEmailTemplate,
109
CompileEmailTemplateCommand,
1110
CompileInAppTemplate,
@@ -152,7 +151,7 @@ export class ContentTemplatesController {
152151
try {
153152
if (process.env.NOVU_ENTERPRISE === 'true' || process.env.CI_EE_TEST === 'true') {
154153
if (!require('@novu/ee-shared-services')?.TranslationsService) {
155-
throw new ApiException('Translation module is not loaded');
154+
throw new BadRequestException('Translation module is not loaded');
156155
}
157156
const service = this.moduleRef.get(require('@novu/ee-shared-services')?.TranslationsService, { strict: false });
158157
const { namespaces, resources, defaultLocale } = await service.getTranslationsList(

apps/api/src/app/environments-v1/usecases/construct-framework-workflow/construct-framework-workflow.usecase.ts

+1-1
Original file line numberDiff line numberDiff line change
@@ -2,7 +2,7 @@ import { Injectable, InternalServerErrorException } from '@nestjs/common';
22
import { workflow } from '@novu/framework/express';
33
import { ActionStep, ChannelStep, JsonSchema, Step, StepOptions, StepOutput, Workflow } from '@novu/framework/internal';
44
import { NotificationStepEntity, NotificationTemplateEntity, NotificationTemplateRepository } from '@novu/dal';
5-
import { JSONSchemaDefinition, StepTypeEnum, WorkflowOriginEnum } from '@novu/shared';
5+
import { JSONSchemaDefinition, StepTypeEnum } from '@novu/shared';
66
import { Instrument, InstrumentUsecase, PinoLogger } from '@novu/application-generic';
77
import { AdditionalOperation, RulesLogic } from 'json-logic-js';
88
import _ from 'lodash';

apps/api/src/app/environments-v1/usecases/regenerate-api-keys/regenerate-api-keys.usecase.ts

+2-3
Original file line numberDiff line numberDiff line change
@@ -1,10 +1,9 @@
11
import { createHash } from 'crypto';
2-
import { Injectable } from '@nestjs/common';
2+
import { Injectable, BadRequestException } from '@nestjs/common';
33

44
import { EnvironmentRepository } from '@novu/dal';
55
import { decryptApiKey, encryptApiKey } from '@novu/application-generic';
66

7-
import { ApiException } from '../../../shared/exceptions/api.exception';
87
import { GenerateUniqueApiKey } from '../generate-unique-api-key/generate-unique-api-key.usecase';
98
import { GetApiKeysCommand } from '../get-api-keys/get-api-keys.command';
109

@@ -21,7 +20,7 @@ export class RegenerateApiKeys {
2120
const environment = await this.environmentRepository.findOne({ _id: command.environmentId });
2221

2322
if (!environment) {
24-
throw new ApiException(`Environment id: ${command.environmentId} not found`);
23+
throw new BadRequestException(`Environment id: ${command.environmentId} not found`);
2524
}
2625

2726
const key = await this.generateUniqueApiKey.execute();

apps/api/src/app/events/usecases/parse-event-request/parse-event-request.usecase.ts

+1-2
Original file line numberDiff line numberDiff line change
@@ -38,7 +38,6 @@ import { addBreadcrumb } from '@sentry/node';
3838
import { randomBytes } from 'crypto';
3939
import { merge } from 'lodash';
4040
import { v4 as uuidv4 } from 'uuid';
41-
import { ApiException } from '../../../shared/exceptions/api.exception';
4241
import { RecipientSchema, RecipientsSchema } from '../../utils/trigger-recipient-validation';
4342
import { VerifyPayload, VerifyPayloadCommand } from '../verify-payload';
4443
import {
@@ -327,7 +326,7 @@ export class ParseEventRequest {
327326
}
328327

329328
if (invalidKeys.length) {
330-
throw new ApiException(`Trigger is missing: ${invalidKeys.join(', ')}`);
329+
throw new BadRequestException(`Trigger is missing: ${invalidKeys.join(', ')}`);
331330
}
332331
}
333332

apps/api/src/app/events/usecases/send-test-email/send-test-email.usecase.ts

+3-4
Original file line numberDiff line numberDiff line change
@@ -5,7 +5,6 @@ import { ChannelTypeEnum, EmailProviderIdEnum, IEmailOptions, WorkflowOriginEnum
55

66
import {
77
AnalyticsService,
8-
ApiException,
98
CompileEmailTemplate,
109
CompileEmailTemplateCommand,
1110
GetNovuProviderCredentials,
@@ -51,7 +50,7 @@ export class SendTestEmail {
5150
);
5251

5352
if (!integration) {
54-
throw new ApiException(`Missing an active email integration`);
53+
throw new BadRequestException(`Missing an active email integration`);
5554
}
5655

5756
if (integration.providerId === EmailProviderIdEnum.Novu) {
@@ -107,7 +106,7 @@ export class SendTestEmail {
107106
);
108107

109108
if (!data.outputs) {
110-
throw new ApiException('Could not retrieve content from edge');
109+
throw new BadRequestException('Could not retrieve content from edge');
111110
}
112111

113112
html = data.outputs.body as string;
@@ -149,7 +148,7 @@ export class SendTestEmail {
149148
providerId,
150149
});
151150
} catch (error) {
152-
throw new ApiException(`Unexpected provider error`);
151+
throw new BadRequestException(`Unexpected provider error`);
153152
}
154153
}
155154

0 commit comments

Comments
 (0)