diff --git a/.cspell.json b/.cspell.json index 25aedeab6db..009c807cbee 100644 --- a/.cspell.json +++ b/.cspell.json @@ -724,7 +724,8 @@ "navigatable", "facated", "dotenvcreate", - "querybuilder" + "querybuilder", + "liquified" ], "flagWords": [], "patterns": [ @@ -827,7 +828,7 @@ "tsconfig.json", "unreadRead", "websockets", - "apps/dashboard/src/components/header-navigation/customer-support-button.tsx", + "apps/dashboard/src/hooks/use-plain-chat.ts", "apps/dashboard/src/components/primitives/control-input/variable-popover/constants.ts" ] } diff --git a/.github/workflows/prepare-self-hosted-release.yml b/.github/workflows/prepare-self-hosted-release.yml index 4eed3dbec2b..5bf69ab96c1 100644 --- a/.github/workflows/prepare-self-hosted-release.yml +++ b/.github/workflows/prepare-self-hosted-release.yml @@ -92,6 +92,7 @@ jobs: --platform=linux/amd64,linux/arm64 --provenance=false --output=type=image,name=ghcr.io/${{ env.REGISTRY_OWNER }}/${{ env.SERVICE_NAME }},push-by-digest=true,name-canonical=true run: | + cp scripts/dotenvcreate.mjs apps/$SERVICE_COMMON_NAME/src/dotenvcreate.mjs cd apps/$SERVICE_COMMON_NAME if [ "${{ env.SERVICE_NAME }}" == "worker" ]; then diff --git a/.github/workflows/reusable-api-e2e.yml b/.github/workflows/reusable-api-e2e.yml index 45f06209162..ed4a065610e 100644 --- a/.github/workflows/reusable-api-e2e.yml +++ b/.github/workflows/reusable-api-e2e.yml @@ -52,7 +52,7 @@ jobs: # This workflow contains a single job called "build" e2e_api: name: Test E2E - runs-on: ubuntu-latest + runs-on: blacksmith-2vcpu-ubuntu-2204 timeout-minutes: 80 permissions: contents: read diff --git a/.source b/.source index af65e6881b8..d034791c4e3 160000 --- a/.source +++ b/.source @@ -1 +1 @@ -Subproject commit af65e6881b8917e4030e1873443faf1cf12a82f2 +Subproject commit d034791c4e39516d4a9e4161e632fa7dcef5bb39 diff --git a/apps/api/src/app/bridge/bridge.module.ts b/apps/api/src/app/bridge/bridge.module.ts index 6a4c9940120..1bc46fbb3d8 100644 --- a/apps/api/src/app/bridge/bridge.module.ts +++ b/apps/api/src/app/bridge/bridge.module.ts @@ -20,8 +20,7 @@ import { SharedModule } from '../shared/shared.module'; import { BridgeController } from './bridge.controller'; import { USECASES } from './usecases'; import { BuildVariableSchemaUsecase } from '../workflows-v2/usecases/build-variable-schema'; -import { HydrateEmailSchemaUseCase } from '../environments-v1/usecases/output-renderers/hydrate-email-schema.usecase'; -import { BuildPayloadSchema } from '../workflows-v2/usecases/build-payload-schema/build-payload-schema.usecase'; +import { ExtractVariables } from '../workflows-v2/usecases/extract-variables/extract-variables.usecase'; import { BuildStepIssuesUsecase } from '../workflows-v2/usecases/build-step-issues/build-step-issues.usecase'; const PROVIDERS = [ @@ -42,9 +41,8 @@ const PROVIDERS = [ UpsertControlValuesUseCase, BuildVariableSchemaUsecase, TierRestrictionsValidateUsecase, - HydrateEmailSchemaUseCase, CommunityOrganizationRepository, - BuildPayloadSchema, + ExtractVariables, BuildStepIssuesUsecase, ]; diff --git a/apps/api/src/app/environments-v1/dtos/create-environment-request.dto.ts b/apps/api/src/app/environments-v1/dtos/create-environment-request.dto.ts index 1383919bdf4..d581a19ce1a 100644 --- a/apps/api/src/app/environments-v1/dtos/create-environment-request.dto.ts +++ b/apps/api/src/app/environments-v1/dtos/create-environment-request.dto.ts @@ -1,5 +1,5 @@ import { ApiProperty, ApiPropertyOptional } from '@nestjs/swagger'; -import { IsDefined, IsMongoId, IsOptional, IsString } from 'class-validator'; +import { IsDefined, IsHexColor, IsMongoId, IsOptional, IsString } from 'class-validator'; export class CreateEnvironmentRequestDto { @ApiProperty() @@ -11,4 +11,9 @@ export class CreateEnvironmentRequestDto { @IsOptional() @IsMongoId() parentId?: string; + + @ApiProperty() + @IsDefined() + @IsHexColor() + color: string; } diff --git a/apps/api/src/app/environments-v1/dtos/update-environment-request.dto.ts b/apps/api/src/app/environments-v1/dtos/update-environment-request.dto.ts index 4502a82e5d7..18862f31578 100644 --- a/apps/api/src/app/environments-v1/dtos/update-environment-request.dto.ts +++ b/apps/api/src/app/environments-v1/dtos/update-environment-request.dto.ts @@ -1,5 +1,5 @@ import { ApiProperty, ApiPropertyOptional } from '@nestjs/swagger'; -import { IsMongoId, IsOptional, IsString } from 'class-validator'; +import { IsHexColor, IsMongoId, IsOptional, IsString } from 'class-validator'; export class InBoundParseDomainDto { @ApiPropertyOptional({ type: String }) @@ -27,6 +27,11 @@ export class UpdateEnvironmentRequestDto { @IsMongoId() parentId?: string; + @ApiPropertyOptional() + @IsOptional() + @IsHexColor() + color?: string; + @ApiPropertyOptional({ type: InBoundParseDomainDto, }) diff --git a/apps/api/src/app/environments-v1/e2e/get-api-keys.e2e.ts b/apps/api/src/app/environments-v1/e2e/get-api-keys.e2e.ts index 688e32d0ea1..2ecf4a9250c 100644 --- a/apps/api/src/app/environments-v1/e2e/get-api-keys.e2e.ts +++ b/apps/api/src/app/environments-v1/e2e/get-api-keys.e2e.ts @@ -1,6 +1,6 @@ +import { NOVU_ENCRYPTION_SUB_MASK } from '@novu/shared'; import { UserSession } from '@novu/testing'; import { expect } from 'chai'; -import { NOVU_ENCRYPTION_SUB_MASK } from '@novu/shared'; describe('Get Environment API Keys - /environments/api-keys (GET) #novu-v2', async () => { let session: UserSession; @@ -10,11 +10,6 @@ describe('Get Environment API Keys - /environments/api-keys (GET) #novu-v2', asy }); it('should get environment api keys correctly', async () => { - const demoEnvironment = { - name: 'Hello App', - }; - await session.testAgent.post('/v1/environments').send(demoEnvironment).expect(201); - const { body } = await session.testAgent.get('/v1/environments/api-keys').send(); expect(body.data[0].key).to.not.contains(NOVU_ENCRYPTION_SUB_MASK); diff --git a/apps/api/src/app/environments-v1/environments-v1.controller.ts b/apps/api/src/app/environments-v1/environments-v1.controller.ts index ab435c71f7d..6f47c703799 100644 --- a/apps/api/src/app/environments-v1/environments-v1.controller.ts +++ b/apps/api/src/app/environments-v1/environments-v1.controller.ts @@ -2,6 +2,7 @@ import { Body, ClassSerializerInterceptor, Controller, + Delete, Get, Param, Post, @@ -9,28 +10,31 @@ import { UseGuards, UseInterceptors, } from '@nestjs/common'; -import { ApiAuthSchemeEnum, MemberRoleEnum, UserSessionData } from '@novu/shared'; -import { ApiExcludeController, ApiExcludeEndpoint, ApiOperation, ApiTags } from '@nestjs/swagger'; +import { ApiExcludeController, ApiExcludeEndpoint, ApiOperation, ApiParam, ApiTags } from '@nestjs/swagger'; import { Roles, RolesGuard } from '@novu/application-generic'; +import { ApiAuthSchemeEnum, MemberRoleEnum, ProductFeatureKeyEnum, UserSessionData } from '@novu/shared'; +import { ExternalApiAccessible } from '../auth/framework/external-api.decorator'; +import { ProductFeature } from '../shared/decorators/product-feature.decorator'; +import { ApiKey } from '../shared/dtos/api-key'; +import { ApiCommonResponses, ApiResponse } from '../shared/framework/response.decorator'; +import { UserAuthentication } from '../shared/framework/swagger/api.key.security'; +import { SdkGroupName } from '../shared/framework/swagger/sdk.decorators'; import { UserSession } from '../shared/framework/user.decorator'; -import { CreateEnvironment } from './usecases/create-environment/create-environment.usecase'; -import { CreateEnvironmentCommand } from './usecases/create-environment/create-environment.command'; import { CreateEnvironmentRequestDto } from './dtos/create-environment-request.dto'; +import { EnvironmentResponseDto } from './dtos/environment-response.dto'; +import { UpdateEnvironmentRequestDto } from './dtos/update-environment-request.dto'; +import { CreateEnvironmentCommand } from './usecases/create-environment/create-environment.command'; +import { CreateEnvironment } from './usecases/create-environment/create-environment.usecase'; +import { DeleteEnvironmentCommand } from './usecases/delete-environment/delete-environment.command'; +import { DeleteEnvironment } from './usecases/delete-environment/delete-environment.usecase'; import { GetApiKeysCommand } from './usecases/get-api-keys/get-api-keys.command'; import { GetApiKeys } from './usecases/get-api-keys/get-api-keys.usecase'; import { GetEnvironment, GetEnvironmentCommand } from './usecases/get-environment'; -import { GetMyEnvironments } from './usecases/get-my-environments/get-my-environments.usecase'; import { GetMyEnvironmentsCommand } from './usecases/get-my-environments/get-my-environments.command'; -import { ApiKey } from '../shared/dtos/api-key'; -import { EnvironmentResponseDto } from './dtos/environment-response.dto'; -import { ExternalApiAccessible } from '../auth/framework/external-api.decorator'; +import { GetMyEnvironments } from './usecases/get-my-environments/get-my-environments.usecase'; import { RegenerateApiKeys } from './usecases/regenerate-api-keys/regenerate-api-keys.usecase'; import { UpdateEnvironmentCommand } from './usecases/update-environment/update-environment.command'; import { UpdateEnvironment } from './usecases/update-environment/update-environment.usecase'; -import { UpdateEnvironmentRequestDto } from './dtos/update-environment-request.dto'; -import { ApiCommonResponses, ApiResponse } from '../shared/framework/response.decorator'; -import { UserAuthentication } from '../shared/framework/swagger/api.key.security'; -import { SdkGroupName } from '../shared/framework/swagger/sdk.decorators'; /** * @deprecated use EnvironmentsControllerV2 @@ -48,7 +52,8 @@ export class EnvironmentsControllerV1 { private getApiKeysUsecase: GetApiKeys, private regenerateApiKeysUsecase: RegenerateApiKeys, private getEnvironmentUsecase: GetEnvironment, - private getMyEnvironmentsUsecase: GetMyEnvironments + private getMyEnvironmentsUsecase: GetMyEnvironments, + private deleteEnvironmentUsecase: DeleteEnvironment ) {} @Get('/me') @@ -73,6 +78,9 @@ export class EnvironmentsControllerV1 { }) @ApiExcludeEndpoint() @ApiResponse(EnvironmentResponseDto, 201) + @ProductFeature(ProductFeatureKeyEnum.MANAGE_ENVIRONMENTS) + @UseGuards(RolesGuard) + @Roles(MemberRoleEnum.ADMIN) async createEnvironment( @UserSession() user: UserSessionData, @Body() body: CreateEnvironmentRequestDto @@ -82,6 +90,8 @@ export class EnvironmentsControllerV1 { name: body.name, userId: user._id, organizationId: user.organizationId, + color: body.color, + system: false, }) ); } @@ -121,6 +131,7 @@ export class EnvironmentsControllerV1 { name: payload.name, identifier: payload.identifier, _parentId: payload.parentId, + color: payload.color, dns: payload.dns, bridge: payload.bridge, }) @@ -157,4 +168,22 @@ export class EnvironmentsControllerV1 { return await this.regenerateApiKeysUsecase.execute(command); } + + @Delete('/:environmentId') + @ApiOperation({ + summary: 'Delete environment', + }) + @ApiParam({ name: 'environmentId', type: String, required: true }) + @ProductFeature(ProductFeatureKeyEnum.MANAGE_ENVIRONMENTS) + @UseGuards(RolesGuard) + @Roles(MemberRoleEnum.ADMIN) + async deleteEnvironment(@UserSession() user: UserSessionData, @Param('environmentId') environmentId: string) { + return await this.deleteEnvironmentUsecase.execute( + DeleteEnvironmentCommand.create({ + userId: user._id, + organizationId: user.organizationId, + environmentId, + }) + ); + } } diff --git a/apps/api/src/app/environments-v1/environments-v1.module.ts b/apps/api/src/app/environments-v1/environments-v1.module.ts index 436bd4a14a1..93ba2f38b36 100644 --- a/apps/api/src/app/environments-v1/environments-v1.module.ts +++ b/apps/api/src/app/environments-v1/environments-v1.module.ts @@ -1,12 +1,13 @@ import { forwardRef, Module } from '@nestjs/common'; -import { SharedModule } from '../shared/shared.module'; -import { USE_CASES } from './usecases'; -import { EnvironmentsControllerV1 } from './environments-v1.controller'; -import { NotificationGroupsModule } from '../notification-groups/notification-groups.module'; import { AuthModule } from '../auth/auth.module'; +import { IntegrationModule } from '../integrations/integrations.module'; import { LayoutsModule } from '../layouts/layouts.module'; +import { NotificationGroupsModule } from '../notification-groups/notification-groups.module'; +import { SharedModule } from '../shared/shared.module'; +import { EnvironmentsControllerV1 } from './environments-v1.controller'; import { NovuBridgeModule } from './novu-bridge.module'; +import { USE_CASES } from './usecases'; @Module({ imports: [ @@ -14,6 +15,7 @@ import { NovuBridgeModule } from './novu-bridge.module'; NotificationGroupsModule, forwardRef(() => AuthModule), forwardRef(() => LayoutsModule), + forwardRef(() => IntegrationModule), NovuBridgeModule, ], controllers: [EnvironmentsControllerV1], diff --git a/apps/api/src/app/environments-v1/novu-bridge.module.ts b/apps/api/src/app/environments-v1/novu-bridge.module.ts index 9339893706e..dbd97b3804b 100644 --- a/apps/api/src/app/environments-v1/novu-bridge.module.ts +++ b/apps/api/src/app/environments-v1/novu-bridge.module.ts @@ -8,8 +8,6 @@ import { ConstructFrameworkWorkflow } from './usecases/construct-framework-workf import { NovuBridgeController } from './novu-bridge.controller'; import { ChatOutputRendererUsecase, - ExpandEmailEditorSchemaUsecase, - HydrateEmailSchemaUseCase, InAppOutputRendererUsecase, PushOutputRendererUsecase, EmailOutputRendererUsecase, @@ -17,6 +15,7 @@ import { } from './usecases/output-renderers'; import { DelayOutputRendererUsecase } from './usecases/output-renderers/delay-output-renderer.usecase'; import { DigestOutputRendererUsecase } from './usecases/output-renderers/digest-output-renderer.usecase'; +import { WrapMailyInLiquidUseCase } from './usecases/output-renderers/maily-to-liquid/wrap-maily-in-liquid.usecase'; @Module({ controllers: [NovuBridgeController], @@ -36,8 +35,7 @@ import { DigestOutputRendererUsecase } from './usecases/output-renderers/digest- ChatOutputRendererUsecase, PushOutputRendererUsecase, EmailOutputRendererUsecase, - ExpandEmailEditorSchemaUsecase, - HydrateEmailSchemaUseCase, + WrapMailyInLiquidUseCase, DelayOutputRendererUsecase, DigestOutputRendererUsecase, ], diff --git a/apps/api/src/app/environments-v1/usecases/construct-framework-workflow/construct-framework-workflow.usecase.ts b/apps/api/src/app/environments-v1/usecases/construct-framework-workflow/construct-framework-workflow.usecase.ts index 95f1c9d4dc9..20519ca276d 100644 --- a/apps/api/src/app/environments-v1/usecases/construct-framework-workflow/construct-framework-workflow.usecase.ts +++ b/apps/api/src/app/environments-v1/usecases/construct-framework-workflow/construct-framework-workflow.usecase.ts @@ -230,7 +230,10 @@ export class ConstructFrameworkWorkflow { return foundWorkflow; } - private processSkipOption(controlValues: { [x: string]: unknown }, variables: FullPayloadForRender) { + private async processSkipOption( + controlValues: { [x: string]: unknown }, + variables: FullPayloadForRender + ): Promise { const skipRules = controlValues.skip as RulesLogic; if (_.isEmpty(skipRules)) { diff --git a/apps/api/src/app/environments-v1/usecases/create-environment/create-environment.command.ts b/apps/api/src/app/environments-v1/usecases/create-environment/create-environment.command.ts index b4444e1bd8d..d39d1b78241 100644 --- a/apps/api/src/app/environments-v1/usecases/create-environment/create-environment.command.ts +++ b/apps/api/src/app/environments-v1/usecases/create-environment/create-environment.command.ts @@ -1,4 +1,4 @@ -import { IsDefined, IsMongoId, IsOptional, IsString } from 'class-validator'; +import { IsBoolean, IsDefined, IsHexColor, IsMongoId, IsOptional, IsString } from 'class-validator'; import { OrganizationCommand } from '../../../shared/commands/organization.command'; export class CreateEnvironmentCommand extends OrganizationCommand { @@ -9,4 +9,12 @@ export class CreateEnvironmentCommand extends OrganizationCommand { @IsOptional() @IsMongoId() parentEnvironmentId?: string; + + @IsOptional() + @IsHexColor() + color?: string; + + @IsBoolean() + @IsDefined() + system: boolean; } diff --git a/apps/api/src/app/environments-v1/usecases/create-environment/create-environment.usecase.ts b/apps/api/src/app/environments-v1/usecases/create-environment/create-environment.usecase.ts index 6d2d5488845..f9693fa2e2b 100644 --- a/apps/api/src/app/environments-v1/usecases/create-environment/create-environment.usecase.ts +++ b/apps/api/src/app/environments-v1/usecases/create-environment/create-environment.usecase.ts @@ -1,13 +1,16 @@ -import { nanoid } from 'nanoid'; -import { Injectable } from '@nestjs/common'; +import { BadRequestException, Injectable, UnprocessableEntityException } from '@nestjs/common'; import { createHash } from 'crypto'; +import { nanoid } from 'nanoid'; -import { EnvironmentRepository, NotificationGroupRepository } from '@novu/dal'; import { encryptApiKey } from '@novu/application-generic'; +import { EnvironmentRepository, NotificationGroupRepository } from '@novu/dal'; -import { CreateEnvironmentCommand } from './create-environment.command'; -import { GenerateUniqueApiKey } from '../generate-unique-api-key/generate-unique-api-key.usecase'; +import { EnvironmentEnum, PROTECTED_ENVIRONMENTS } from '@novu/shared'; +import { CreateNovuIntegrationsCommand } from '../../../integrations/usecases/create-novu-integrations/create-novu-integrations.command'; +import { CreateNovuIntegrations } from '../../../integrations/usecases/create-novu-integrations/create-novu-integrations.usecase'; import { CreateDefaultLayout, CreateDefaultLayoutCommand } from '../../../layouts/usecases'; +import { GenerateUniqueApiKey } from '../generate-unique-api-key/generate-unique-api-key.usecase'; +import { CreateEnvironmentCommand } from './create-environment.command'; @Injectable() export class CreateEnvironment { @@ -15,19 +18,52 @@ export class CreateEnvironment { private environmentRepository: EnvironmentRepository, private notificationGroupRepository: NotificationGroupRepository, private generateUniqueApiKey: GenerateUniqueApiKey, - private createDefaultLayoutUsecase: CreateDefaultLayout + private createDefaultLayoutUsecase: CreateDefaultLayout, + private createNovuIntegrationsUsecase: CreateNovuIntegrations ) {} async execute(command: CreateEnvironmentCommand) { + const environmentCount = await this.environmentRepository.count({ + _organizationId: command.organizationId, + }); + + if (environmentCount >= 10) { + throw new BadRequestException('Organization cannot have more than 10 environments'); + } + const normalizedName = command.name.trim(); + + if (!command.system) { + const { name } = command; + + if (PROTECTED_ENVIRONMENTS?.map((env) => env.toLowerCase()).includes(normalizedName.toLowerCase())) { + throw new UnprocessableEntityException('Environment name cannot be Development or Production'); + } + + const environment = await this.environmentRepository.findOne({ + _organizationId: command.organizationId, + name, + }); + + if (environment) { + throw new BadRequestException('Environment name must be unique'); + } + } + const key = await this.generateUniqueApiKey.execute(); const encryptedApiKey = encryptApiKey(key); const hashedApiKey = createHash('sha256').update(key).digest('hex'); + const color = this.getEnvironmentColor(command.name, command.color); + + if (!color) { + throw new BadRequestException('Color property is required'); + } const environment = await this.environmentRepository.create({ _organizationId: command.organizationId, - name: command.name, + name: normalizedName, identifier: nanoid(12), _parentId: command.parentEnvironmentId, + color, apiKeys: [ { key: encryptedApiKey, @@ -67,6 +103,21 @@ export class CreateEnvironment { }); } + await this.createNovuIntegrationsUsecase.execute( + CreateNovuIntegrationsCommand.create({ + environmentId: environment._id, + organizationId: environment._organizationId, + userId: command.userId, + }) + ); + return environment; } + + private getEnvironmentColor(name: string, commandColor?: string): string | undefined { + if (name === EnvironmentEnum.DEVELOPMENT) return '#ff8547'; + if (name === EnvironmentEnum.PRODUCTION) return '#7e52f4'; + + return commandColor; + } } diff --git a/apps/api/src/app/environments-v1/usecases/delete-environment/delete-environment.command.ts b/apps/api/src/app/environments-v1/usecases/delete-environment/delete-environment.command.ts new file mode 100644 index 00000000000..189c61a2fc6 --- /dev/null +++ b/apps/api/src/app/environments-v1/usecases/delete-environment/delete-environment.command.ts @@ -0,0 +1,3 @@ +import { EnvironmentWithUserCommand } from '../../../shared/commands/project.command'; + +export class DeleteEnvironmentCommand extends EnvironmentWithUserCommand {} diff --git a/apps/api/src/app/environments-v1/usecases/delete-environment/delete-environment.usecase.ts b/apps/api/src/app/environments-v1/usecases/delete-environment/delete-environment.usecase.ts new file mode 100644 index 00000000000..2476ae33d3d --- /dev/null +++ b/apps/api/src/app/environments-v1/usecases/delete-environment/delete-environment.usecase.ts @@ -0,0 +1,53 @@ +import { BadRequestException, Injectable, NotFoundException } from '@nestjs/common'; +import { EnvironmentRepository, IntegrationRepository } from '@novu/dal'; +import { EnvironmentEnum, PROTECTED_ENVIRONMENTS } from '@novu/shared'; +import { RemoveIntegrationCommand } from '../../../integrations/usecases/remove-integration/remove-integration.command'; +import { RemoveIntegration } from '../../../integrations/usecases/remove-integration/remove-integration.usecase'; +import { DeleteEnvironmentCommand } from './delete-environment.command'; + +@Injectable() +export class DeleteEnvironment { + constructor( + private environmentRepository: EnvironmentRepository, + private removeIntegration: RemoveIntegration, + private integrationRepository: IntegrationRepository + ) {} + + async execute(command: DeleteEnvironmentCommand): Promise { + const environment = await this.environmentRepository.findOne({ + _id: command.environmentId, + _organizationId: command.organizationId, + }); + + if (!environment) { + throw new NotFoundException(`Environment ${command.environmentId} not found`); + } + + if (PROTECTED_ENVIRONMENTS.includes(environment.name as EnvironmentEnum)) { + throw new BadRequestException( + `The ${environment.name} environment is protected and cannot be deleted. Only custom environments can be deleted.` + ); + } + + await this.environmentRepository.delete({ + _id: command.environmentId, + _organizationId: command.organizationId, + }); + + const integrations = await this.integrationRepository.find({ + _organizationId: command.organizationId, + _environmentId: command.environmentId, + }); + + for (const integration of integrations) { + await this.removeIntegration.execute( + RemoveIntegrationCommand.create({ + organizationId: command.organizationId, + integrationId: integration._id, + userId: command.userId, + environmentId: command.environmentId, + }) + ); + } + } +} diff --git a/apps/api/src/app/environments-v1/usecases/delete-environment/index.ts b/apps/api/src/app/environments-v1/usecases/delete-environment/index.ts new file mode 100644 index 00000000000..77fbc92d6e8 --- /dev/null +++ b/apps/api/src/app/environments-v1/usecases/delete-environment/index.ts @@ -0,0 +1,2 @@ +export * from './delete-environment.command'; +export * from './delete-environment.usecase'; diff --git a/apps/api/src/app/environments-v1/usecases/index.ts b/apps/api/src/app/environments-v1/usecases/index.ts index 98658a41aa8..8fb987543d8 100644 --- a/apps/api/src/app/environments-v1/usecases/index.ts +++ b/apps/api/src/app/environments-v1/usecases/index.ts @@ -1,11 +1,12 @@ +import { GetMxRecord } from '../../inbound-parse/usecases/get-mx-record/get-mx-record.usecase'; import { CreateEnvironment } from './create-environment/create-environment.usecase'; +import { DeleteEnvironment } from './delete-environment/delete-environment.usecase'; import { GenerateUniqueApiKey } from './generate-unique-api-key/generate-unique-api-key.usecase'; import { GetApiKeys } from './get-api-keys/get-api-keys.usecase'; -import { RegenerateApiKeys } from './regenerate-api-keys/regenerate-api-keys.usecase'; import { GetEnvironment } from './get-environment'; import { GetMyEnvironments } from './get-my-environments/get-my-environments.usecase'; +import { RegenerateApiKeys } from './regenerate-api-keys/regenerate-api-keys.usecase'; import { UpdateEnvironment } from './update-environment/update-environment.usecase'; -import { GetMxRecord } from '../../inbound-parse/usecases/get-mx-record/get-mx-record.usecase'; export const USE_CASES = [ GetMxRecord, @@ -16,4 +17,5 @@ export const USE_CASES = [ RegenerateApiKeys, GetEnvironment, GetMyEnvironments, + DeleteEnvironment, ]; diff --git a/apps/api/src/app/environments-v1/usecases/output-renderers/email-output-renderer.spec.ts b/apps/api/src/app/environments-v1/usecases/output-renderers/email-output-renderer.spec.ts index 805431ce692..a087fb6336f 100644 --- a/apps/api/src/app/environments-v1/usecases/output-renderers/email-output-renderer.spec.ts +++ b/apps/api/src/app/environments-v1/usecases/output-renderers/email-output-renderer.spec.ts @@ -1,24 +1,19 @@ import { Test } from '@nestjs/testing'; import { expect } from 'chai'; -import { TipTapNode } from '@novu/shared'; +import { JSONContent as MailyJSONContent } from '@maily-to/render'; import { EmailOutputRendererUsecase } from './email-output-renderer.usecase'; -import { ExpandEmailEditorSchemaUsecase } from './expand-email-editor-schema.usecase'; -import { HydrateEmailSchemaUseCase } from './hydrate-email-schema.usecase'; import { FullPayloadForRender } from './render-command'; +import { WrapMailyInLiquidUseCase } from './maily-to-liquid/wrap-maily-in-liquid.usecase'; describe('EmailOutputRendererUsecase', () => { let emailOutputRendererUsecase: EmailOutputRendererUsecase; - let expandEmailEditorSchemaUseCase: ExpandEmailEditorSchemaUsecase; - let hydrateEmailSchemaUseCase: HydrateEmailSchemaUseCase; beforeEach(async () => { const moduleRef = await Test.createTestingModule({ - providers: [EmailOutputRendererUsecase, ExpandEmailEditorSchemaUsecase, HydrateEmailSchemaUseCase], + providers: [EmailOutputRendererUsecase, WrapMailyInLiquidUseCase], }).compile(); emailOutputRendererUsecase = moduleRef.get(EmailOutputRendererUsecase); - expandEmailEditorSchemaUseCase = moduleRef.get(ExpandEmailEditorSchemaUsecase); - hydrateEmailSchemaUseCase = moduleRef.get(HydrateEmailSchemaUseCase); }); const mockFullPayload: FullPayloadForRender = { @@ -61,7 +56,7 @@ describe('EmailOutputRendererUsecase', () => { }); it('should process simple text with liquid variables', async () => { - const mockTipTapNode: TipTapNode = { + const mockTipTapNode: MailyJSONContent = { type: 'doc', content: [ { @@ -94,7 +89,7 @@ describe('EmailOutputRendererUsecase', () => { }); it('should handle nested object variables with liquid syntax', async () => { - const mockTipTapNode: TipTapNode = { + const mockTipTapNode: MailyJSONContent = { type: 'doc', content: [ { @@ -132,7 +127,7 @@ describe('EmailOutputRendererUsecase', () => { }); it('should handle liquid variables with default values', async () => { - const mockTipTapNode: TipTapNode = { + const mockTipTapNode: MailyJSONContent = { type: 'doc', content: [ { @@ -167,7 +162,7 @@ describe('EmailOutputRendererUsecase', () => { describe('variable node transformation to text', () => { it('should handle maily variables', async () => { - const mockTipTapNode: TipTapNode = { + const mockTipTapNode: MailyJSONContent = { type: 'doc', content: [ { @@ -237,7 +232,7 @@ describe('EmailOutputRendererUsecase', () => { }); it('should handle maily variables with fallback values', async () => { - const mockTipTapNode: TipTapNode = { + const mockTipTapNode: MailyJSONContent = { type: 'doc', content: [ { @@ -340,7 +335,7 @@ describe('EmailOutputRendererUsecase', () => { describe('conditional block transformation (showIfKey)', () => { it('should render content when showIfKey condition is true', async () => { - const mockTipTapNode: TipTapNode = { + const mockTipTapNode: MailyJSONContent = { type: 'doc', content: [ { @@ -397,7 +392,7 @@ describe('EmailOutputRendererUsecase', () => { }); it('should not render content when showIfKey condition is false', async () => { - const mockTipTapNode: TipTapNode = { + const mockTipTapNode: MailyJSONContent = { type: 'doc', content: [ { @@ -459,7 +454,7 @@ describe('EmailOutputRendererUsecase', () => { }); it('should handle nested conditional blocks correctly', async () => { - const mockTipTapNode: TipTapNode = { + const mockTipTapNode: MailyJSONContent = { type: 'doc', content: [ { @@ -543,12 +538,127 @@ describe('EmailOutputRendererUsecase', () => { }); describe('for block transformation and expansion', () => { - // Tests for for loop block transformation will be added here + it('should handle for loop block transformation with array of objects', async () => { + const mockTipTapNode: MailyJSONContent = { + type: 'doc', + content: [ + { + type: 'for', + attrs: { + each: 'payload.comments', + isUpdatingKey: false, + showIfKey: null, + }, + content: [ + { + type: 'paragraph', + attrs: { + textAlign: 'left', + }, + content: [ + { + type: 'text', + text: 'This is an author: ', + }, + { + type: 'variable', + attrs: { + id: 'payload.comments.author', + label: null, + fallback: null, + required: false, + }, + }, + { + type: 'variable', + attrs: { + // variable not belonging to the loop + id: 'payload.postTitle', + label: null, + fallback: null, + required: false, + }, + }, + ], + }, + ], + }, + ], + }; + + const renderCommand = { + controlValues: { + subject: 'For Loop Test', + body: JSON.stringify(mockTipTapNode), + }, + fullPayloadForRender: { + ...mockFullPayload, + payload: { + postTitle: 'Post Title', + comments: [{ author: 'John' }, { author: 'Jane' }], + }, + }, + }; + const result = await emailOutputRendererUsecase.execute(renderCommand); + expect(result.body).to.include('This is an author: JohnPost Title'); + expect(result.body).to.include('This is an author: JanePost Title'); + }); + + it('should handle for loop block transformation with array of primitives', async () => { + const mockTipTapNode: MailyJSONContent = { + type: 'doc', + content: [ + { + type: 'for', + attrs: { + each: 'payload.names', + isUpdatingKey: false, + showIfKey: null, + }, + content: [ + { + type: 'paragraph', + attrs: { + textAlign: 'left', + }, + content: [ + { + type: 'variable', + attrs: { + id: 'payload.names', + label: null, + fallback: null, + required: false, + }, + }, + ], + }, + ], + }, + ], + }; + + const renderCommand = { + controlValues: { + subject: 'For Loop Test', + body: JSON.stringify(mockTipTapNode), + }, + fullPayloadForRender: { + ...mockFullPayload, + payload: { + names: ['John', 'Jane'], + }, + }, + }; + const result = await emailOutputRendererUsecase.execute(renderCommand); + expect(result.body).to.include('John'); + expect(result.body).to.include('Jane'); + }); }); describe('node attrs and marks attrs hydration', () => { it('should handle links with href attributes', async () => { - const mockTipTapNode: TipTapNode = { + const mockTipTapNode: MailyJSONContent = { type: 'doc', content: [ { @@ -601,7 +711,7 @@ describe('EmailOutputRendererUsecase', () => { }); it('should handle image nodes with variable attributes', async () => { - const mockTipTapNode: TipTapNode = { + const mockTipTapNode: MailyJSONContent = { type: 'doc', content: [ { @@ -637,7 +747,7 @@ describe('EmailOutputRendererUsecase', () => { }); it('should handle marks attrs href', async () => { - const mockTipTapNode: TipTapNode = { + const mockTipTapNode: MailyJSONContent = { type: 'doc', content: [ { diff --git a/apps/api/src/app/environments-v1/usecases/output-renderers/email-output-renderer.usecase.ts b/apps/api/src/app/environments-v1/usecases/output-renderers/email-output-renderer.usecase.ts index 2ea73395be7..70ddc167e0c 100644 --- a/apps/api/src/app/environments-v1/usecases/output-renderers/email-output-renderer.usecase.ts +++ b/apps/api/src/app/environments-v1/usecases/output-renderers/email-output-renderer.usecase.ts @@ -1,16 +1,19 @@ -import { render as mailyRender } from '@maily-to/render'; +/* eslint-disable no-param-reassign */ +import { render as mailyRender, JSONContent as MailyJSONContent } from '@maily-to/render'; import { Injectable } from '@nestjs/common'; import { Liquid } from 'liquidjs'; -import { EmailRenderOutput, TipTapNode } from '@novu/shared'; +import { EmailRenderOutput } from '@novu/shared'; import { InstrumentUsecase } from '@novu/application-generic'; import { FullPayloadForRender, RenderCommand } from './render-command'; -import { ExpandEmailEditorSchemaUsecase } from './expand-email-editor-schema.usecase'; +import { WrapMailyInLiquidUseCase } from './maily-to-liquid/wrap-maily-in-liquid.usecase'; +import { MAILY_ITERABLE_MARK, MailyAttrsEnum, MailyContentTypeEnum } from './maily-to-liquid/maily.types'; +import { parseLiquid } from '../../../shared/helpers/liquid'; export class EmailOutputRendererCommand extends RenderCommand {} @Injectable() export class EmailOutputRendererUsecase { - constructor(private expandEmailEditorSchemaUseCase: ExpandEmailEditorSchemaUsecase) {} + constructor(private wrapMailyInLiquidUsecase: WrapMailyInLiquidUseCase) {} @InstrumentUsecase() async execute(renderCommand: EmailOutputRendererCommand): Promise { @@ -28,12 +31,11 @@ export class EmailOutputRendererUsecase { }; } - const expandedMailyContent = await this.expandEmailEditorSchemaUseCase.execute({ - emailEditorJson: body, - fullPayloadForRender: renderCommand.fullPayloadForRender, - }); - const parsedTipTap = await this.parseTipTapNodeByLiquid(expandedMailyContent, renderCommand); - const renderedHtml = await mailyRender(parsedTipTap); + const liquifiedMaily = this.wrapMailyInLiquidUsecase.execute({ emailEditor: body }); + const transformedMaily = await this.transformMailyContent(liquifiedMaily, renderCommand.fullPayloadForRender); + const parsedMaily = await this.parseMailyContentByLiquid(transformedMaily, renderCommand.fullPayloadForRender); + const strippedMaily = this.removeTrailingEmptyLines(parsedMaily); + const renderedHtml = await mailyRender(strippedMaily); /** * Force type mapping in case undefined control. @@ -43,36 +45,223 @@ export class EmailOutputRendererUsecase { return { subject: subject as string, body: renderedHtml }; } - private async parseTipTapNodeByLiquid( - tiptapNode: TipTapNode, - renderCommand: EmailOutputRendererCommand - ): Promise { - const parsedString = await parseLiquid(JSON.stringify(tiptapNode), renderCommand.fullPayloadForRender); + private removeTrailingEmptyLines(node: MailyJSONContent): MailyJSONContent { + if (!node.content || node.content.length === 0) return node; + + // Iterate from the end of the content and find the first non-empty node + let lastIndex = node.content.length; + // eslint-disable-next-line no-plusplus + for (let i = node.content.length - 1; i >= 0; i--) { + const childNode = node.content[i]; + + const isEmptyParagraph = + childNode.type === 'paragraph' && !childNode.text && (!childNode.content || childNode.content.length === 0); + + if (!isEmptyParagraph) { + lastIndex = i + 1; // Include this node in the result + break; + } + } + + // Slice the content to remove trailing empty nodes + const filteredContent = node.content.slice(0, lastIndex); + + return { ...node, content: filteredContent }; + } + + private async parseMailyContentByLiquid( + mailyContent: MailyJSONContent, + variables: FullPayloadForRender + ): Promise { + const parsedString = await parseLiquid(JSON.stringify(mailyContent), variables); return JSON.parse(parsedString); } -} -export const parseLiquid = async (value: string, variables: FullPayloadForRender): Promise => { - const client = new Liquid({ - outputEscape: (output) => { - return stringifyDataStructureWithSingleQuotes(output); - }, - }); + private async transformMailyContent( + node: MailyJSONContent, + variables: FullPayloadForRender, + parent?: MailyJSONContent + ) { + const queue: Array<{ node: MailyJSONContent; parent?: MailyJSONContent }> = [{ node, parent }]; + + while (queue.length > 0) { + const current = queue.shift()!; + + if (this.hasShow(current.node)) { + await this.handleShowNode(current.node, variables, current.parent); + } + + if (this.isForNode(current.node)) { + await this.handleEachNode(current.node, variables, current.parent); + } + + if (this.isVariableNode(current.node)) { + this.processVariableNodeTypes(current.node); + } - const template = client.parse(value); + if (current.node.content) { + for (const childNode of current.node.content) { + queue.push({ node: childNode, parent: current.node }); + } + } + } + + return node; + } + + private async handleShowNode( + node: MailyJSONContent & { attrs: { [MailyAttrsEnum.SHOW_IF_KEY]: string } }, + variables: FullPayloadForRender, + parent?: MailyJSONContent + ): Promise { + const shouldShow = await this.evaluateShowCondition(variables, node); + if (!shouldShow && parent?.content) { + parent.content = parent.content.filter((pNode) => pNode !== node); + + return; + } + + // @ts-ignore + delete node.attrs[MailyAttrsEnum.SHOW_IF_KEY]; + } + + private async handleEachNode( + node: MailyJSONContent & { attrs: { [MailyAttrsEnum.EACH_KEY]: string } }, + variables: FullPayloadForRender, + parent?: MailyJSONContent + ): Promise { + const newContent = await this.multiplyForEachNode(node, variables); - return await client.render(template, variables); -}; + if (parent?.content) { + const nodeIndex = parent.content.indexOf(node); + parent.content = [...parent.content.slice(0, nodeIndex), ...newContent, ...parent.content.slice(nodeIndex + 1)]; + } else { + node.content = newContent; + } + } -const stringifyDataStructureWithSingleQuotes = (value: unknown, spaces: number = 0): string => { - if (Array.isArray(value) || (typeof value === 'object' && value !== null)) { - const valueStringified = JSON.stringify(value, null, spaces); - const valueSingleQuotes = valueStringified.replace(/"/g, "'"); - const valueEscapedNewLines = valueSingleQuotes.replace(/\n/g, '\\n'); + private async evaluateShowCondition( + variables: FullPayloadForRender, + node: MailyJSONContent & { attrs: { [MailyAttrsEnum.SHOW_IF_KEY]: string } } + ): Promise { + const { [MailyAttrsEnum.SHOW_IF_KEY]: showIfKey } = node.attrs; + const parsedShowIfValue = await parseLiquid(showIfKey, variables); - return valueEscapedNewLines; - } else { - return String(value); + return this.stringToBoolean(parsedShowIfValue); } -}; + + private processVariableNodeTypes(node: MailyJSONContent) { + node.type = 'text'; // set 'variable' to 'text' to for Liquid to recognize it + node.text = node.attrs?.id || ''; + } + + private isForNode( + node: MailyJSONContent + ): node is MailyJSONContent & { attrs: { [MailyAttrsEnum.EACH_KEY]: string } } { + return !!( + node.type === MailyContentTypeEnum.FOR && + node.attrs && + node.attrs[MailyAttrsEnum.EACH_KEY] !== undefined && + typeof node.attrs[MailyAttrsEnum.EACH_KEY] === 'string' + ); + } + + private hasShow( + node: MailyJSONContent + ): node is MailyJSONContent & { attrs: { [MailyAttrsEnum.SHOW_IF_KEY]: string } } { + return node.attrs?.[MailyAttrsEnum.SHOW_IF_KEY] !== undefined && node.attrs?.[MailyAttrsEnum.SHOW_IF_KEY] !== null; + } + + /** + * For 'each' node, multiply the content by the number of items in the iterable array + * and add indexes to the placeholders. + * + * @example + * node: + * { + * type: 'each', + * attrs: { each: '{{ payload.comments }}' }, + * content: [ + * { type: 'variable', text: '{{ payload.comments[0].author }}' } + * ] + * } + * + * variables: + * { payload: { comments: [{ author: 'John Doe' }, { author: 'Jane Doe' }] } } + * + * result: + * [ + * { type: 'text', text: '{{ payload.comments[0].author }}' }, + * { type: 'text', text: '{{ payload.comments[1].author }}' } + * ] + * + */ + private async multiplyForEachNode( + node: MailyJSONContent & { attrs: { [MailyAttrsEnum.EACH_KEY]: string } }, + variables: FullPayloadForRender + ): Promise { + const iterablePath = node.attrs[MailyAttrsEnum.EACH_KEY]; + const forEachNodes = node.content || []; + const iterableArray = await this.getIterableArray(iterablePath, variables); + + return iterableArray.flatMap((_, index) => this.processForEachNodes(forEachNodes, iterablePath, index)); + } + + private async getIterableArray(iterablePath: string, variables: FullPayloadForRender): Promise { + const iterableArrayString = await parseLiquid(iterablePath, variables); + + try { + const parsedArray = JSON.parse(iterableArrayString.replace(/'/g, '"')); + + if (!Array.isArray(parsedArray)) { + throw new Error(`Iterable "${iterablePath}" is not an array`); + } + + return parsedArray; + } catch (error) { + throw new Error(`Failed to parse iterable value for "${iterablePath}": ${error.message}`); + } + } + + private processForEachNodes(nodes: MailyJSONContent[], iterablePath: string, index: number): MailyJSONContent[] { + return nodes.map((node) => { + const processedNode = { ...node }; + + if (this.isVariableNode(processedNode)) { + this.processVariableNodeTypes(processedNode); + + if (processedNode.text) { + processedNode.text = processedNode.text.replace(MAILY_ITERABLE_MARK, index.toString()); + } + + return processedNode; + } + + if (processedNode.content?.length) { + processedNode.content = this.processForEachNodes(processedNode.content, iterablePath, index); + } + + return processedNode; + }); + } + + private stringToBoolean(value: unknown): boolean { + if (typeof value === 'string') { + return value.toLowerCase() === 'true'; + } + + return false; + } + + private isVariableNode( + node: MailyJSONContent + ): node is MailyJSONContent & { attrs: { [MailyAttrsEnum.ID]: string } } { + return !!( + node.type === MailyContentTypeEnum.VARIABLE && + node.attrs && + node.attrs[MailyAttrsEnum.ID] !== undefined && + typeof node.attrs[MailyAttrsEnum.ID] === 'string' + ); + } +} diff --git a/apps/api/src/app/environments-v1/usecases/output-renderers/expand-email-editor-schema-command.ts b/apps/api/src/app/environments-v1/usecases/output-renderers/expand-email-editor-schema-command.ts deleted file mode 100644 index 9a4dbe23a6c..00000000000 --- a/apps/api/src/app/environments-v1/usecases/output-renderers/expand-email-editor-schema-command.ts +++ /dev/null @@ -1,7 +0,0 @@ -import { BaseCommand } from '@novu/application-generic'; -import { FullPayloadForRender } from './render-command'; - -export class ExpandEmailEditorSchemaCommand extends BaseCommand { - emailEditorJson: string; - fullPayloadForRender: FullPayloadForRender; -} diff --git a/apps/api/src/app/environments-v1/usecases/output-renderers/expand-email-editor-schema.usecase.ts b/apps/api/src/app/environments-v1/usecases/output-renderers/expand-email-editor-schema.usecase.ts deleted file mode 100644 index ed887eed248..00000000000 --- a/apps/api/src/app/environments-v1/usecases/output-renderers/expand-email-editor-schema.usecase.ts +++ /dev/null @@ -1,213 +0,0 @@ -/* eslint-disable no-param-reassign */ -import { TipTapNode } from '@novu/shared'; -import { Injectable } from '@nestjs/common'; -import { MAILY_ITERABLE_MARK, MailyAttrsEnum } from '@novu/application-generic'; -import { ExpandEmailEditorSchemaCommand } from './expand-email-editor-schema-command'; -import { HydrateEmailSchemaUseCase } from './hydrate-email-schema.usecase'; -import { parseLiquid } from './email-output-renderer.usecase'; -import { FullPayloadForRender } from './render-command'; - -@Injectable() -export class ExpandEmailEditorSchemaUsecase { - constructor(private hydrateEmailSchemaUseCase: HydrateEmailSchemaUseCase) {} - - async execute(command: ExpandEmailEditorSchemaCommand): Promise { - const hydratedEmailSchema = this.hydrateEmailSchemaUseCase.execute({ - emailEditor: command.emailEditorJson, - }); - - const processed = await this.processSpecialNodeTypes(command.fullPayloadForRender, hydratedEmailSchema); - - // needs to be done after the special node types are processed - this.processVariableNodeTypes(processed, command.fullPayloadForRender); - - return processed; - } - - private async processSpecialNodeTypes(variables: FullPayloadForRender, rootNode: TipTapNode): Promise { - const processedNode = structuredClone(rootNode); - await this.traverseAndProcessNodes(processedNode, variables); - - return processedNode; - } - - private async traverseAndProcessNodes( - node: TipTapNode, - variables: FullPayloadForRender, - parent?: TipTapNode - ): Promise { - const queue: Array<{ node: TipTapNode; parent?: TipTapNode }> = [{ node, parent }]; - - while (queue.length > 0) { - const current = queue.shift()!; - await this.processNode(current.node, variables, current.parent); - - if (current.node.content) { - for (const childNode of current.node.content) { - queue.push({ node: childNode, parent: current.node }); - } - } - } - } - - private async processNode(node: TipTapNode, variables: FullPayloadForRender, parent?: TipTapNode): Promise { - if (this.hasShow(node)) { - await this.handleShowNode(node, variables, parent); - } - - if (this.hasEach(node)) { - await this.handleEachNode(node, variables, parent); - } - } - - private async handleShowNode( - node: TipTapNode & { attrs: { [MailyAttrsEnum.SHOW_IF_KEY]: string } }, - variables: FullPayloadForRender, - parent?: TipTapNode - ): Promise { - const shouldShow = await this.evaluateShowCondition(variables, node); - if (!shouldShow && parent?.content) { - parent.content = parent.content.filter((pNode) => pNode !== node); - - return; - } - - // @ts-ignore - delete node.attrs[MailyAttrsEnum.SHOW_IF_KEY]; - } - - private async handleEachNode( - node: TipTapNode & { attrs: { [MailyAttrsEnum.EACH_KEY]: string } }, - variables: FullPayloadForRender, - parent?: TipTapNode - ): Promise { - const newContent = await this.multiplyForEachNode(node, variables); - if (parent?.content) { - const nodeIndex = parent.content.indexOf(node); - parent.content = [...parent.content.slice(0, nodeIndex), ...newContent, ...parent.content.slice(nodeIndex + 1)]; - } else { - node.content = newContent; - } - } - - private async evaluateShowCondition( - variables: FullPayloadForRender, - node: TipTapNode & { attrs: { [MailyAttrsEnum.SHOW_IF_KEY]: string } } - ): Promise { - const { [MailyAttrsEnum.SHOW_IF_KEY]: showIfKey } = node.attrs; - const parsedShowIfValue = await parseLiquid(showIfKey, variables); - - return this.stringToBoolean(parsedShowIfValue); - } - - private processVariableNodeTypes(node: TipTapNode, variables: FullPayloadForRender) { - if (this.isAVariableNode(node)) { - node.type = 'text'; // set 'variable' to 'text' to for Liquid to recognize it - } - - node.content?.forEach((innerNode) => this.processVariableNodeTypes(innerNode, variables)); - } - - private hasEach(node: TipTapNode): node is TipTapNode & { attrs: { [MailyAttrsEnum.EACH_KEY]: string } } { - return node.attrs?.[MailyAttrsEnum.EACH_KEY] !== undefined && node.attrs?.[MailyAttrsEnum.EACH_KEY] !== null; - } - - private hasShow(node: TipTapNode): node is TipTapNode & { attrs: { [MailyAttrsEnum.SHOW_IF_KEY]: string } } { - return node.attrs?.[MailyAttrsEnum.SHOW_IF_KEY] !== undefined && node.attrs?.[MailyAttrsEnum.SHOW_IF_KEY] !== null; - } - - private isOrderedList(templateContent: TipTapNode[]) { - return templateContent.length === 1 && templateContent[0].type === 'orderedList'; - } - - private isBulletList(templateContent: TipTapNode[]) { - return templateContent.length === 1 && templateContent[0].type === 'bulletList'; - } - - /** - * For 'each' node, multiply the content by the number of items in the iterable array - * and add indexes to the placeholders. - * - * @example - * node: - * { - * type: 'each', - * attrs: { each: '{{ payload.comments }}' }, - * content: [ - * { type: 'variable', text: '{{ payload.comments[0].author }}' } - * ] - * } - * - * variables: - * { payload: { comments: [{ author: 'John Doe' }, { author: 'Jane Doe' }] } } - * - * result: - * [ - * { type: 'text', text: '{{ payload.comments[0].author }}' }, - * { type: 'text', text: '{{ payload.comments[1].author }}' } - * ] - * - */ - private async multiplyForEachNode( - node: TipTapNode & { attrs: { [MailyAttrsEnum.EACH_KEY]: string } }, - variables: FullPayloadForRender - ): Promise { - const iterablePath = node.attrs[MailyAttrsEnum.EACH_KEY]; - const nodeContent = node.content || []; - const expandedContent: TipTapNode[] = []; - - const iterableArrayString = await parseLiquid(iterablePath, variables); - - let iterableArray: unknown; - try { - iterableArray = JSON.parse(iterableArrayString.replace(/'/g, '"')); - } catch (error) { - throw new Error(`Failed to parse iterable value for "${iterablePath}": ${error.message}`); - } - - if (!Array.isArray(iterableArray)) { - throw new Error(`Iterable "${iterablePath}" is not an array`); - } - - for (const [index] of iterableArray.entries()) { - const contentToExpand = - (this.isOrderedList(nodeContent) || this.isBulletList(nodeContent)) && nodeContent[0].content - ? nodeContent[0].content - : nodeContent; - - const hydratedContent = this.addIndexesToPlaceholders(contentToExpand, iterablePath, index); - expandedContent.push(...hydratedContent); - } - - return expandedContent; - } - - private addIndexesToPlaceholders(nodes: TipTapNode[], iterablePath: string, index: number): TipTapNode[] { - return nodes.map((node) => { - const newNode: TipTapNode = { ...node }; - - if (this.isAVariableNode(newNode)) { - const nodePlaceholder = newNode.text as string; - - newNode.text = nodePlaceholder.replace(MAILY_ITERABLE_MARK, index.toString()); - newNode.type = 'text'; // set 'variable' to 'text' to for Liquid to recognize it - } else if (newNode.content) { - newNode.content = this.addIndexesToPlaceholders(newNode.content, iterablePath, index); - } - - return newNode; - }); - } - - private stringToBoolean(value: unknown): boolean { - if (typeof value === 'string') { - return value.toLowerCase() === 'true'; - } - - return false; - } - - private isAVariableNode(newNode: TipTapNode): newNode is TipTapNode & { attrs: { [MailyAttrsEnum.ID]: string } } { - return newNode.type === 'variable'; - } -} diff --git a/apps/api/src/app/environments-v1/usecases/output-renderers/hydrate-email-schema.command.ts b/apps/api/src/app/environments-v1/usecases/output-renderers/hydrate-email-schema.command.ts deleted file mode 100644 index 35b5ca9b6b7..00000000000 --- a/apps/api/src/app/environments-v1/usecases/output-renderers/hydrate-email-schema.command.ts +++ /dev/null @@ -1,7 +0,0 @@ -import { BaseCommand } from '@novu/application-generic'; -import { IsNotEmpty, IsString } from 'class-validator'; - -export class HydrateEmailSchemaCommand extends BaseCommand { - @IsString() - emailEditor: string; -} diff --git a/apps/api/src/app/environments-v1/usecases/output-renderers/hydrate-email-schema.usecase.ts b/apps/api/src/app/environments-v1/usecases/output-renderers/hydrate-email-schema.usecase.ts deleted file mode 100644 index 3e3eda861d0..00000000000 --- a/apps/api/src/app/environments-v1/usecases/output-renderers/hydrate-email-schema.usecase.ts +++ /dev/null @@ -1,142 +0,0 @@ -/* eslint-disable no-param-reassign */ -import { Injectable } from '@nestjs/common'; -import { z } from 'zod'; - -import { TipTapNode } from '@novu/shared'; -import { MailyAttrsEnum, processNodeAttrs, processNodeMarks } from '@novu/application-generic'; - -import { HydrateEmailSchemaCommand } from './hydrate-email-schema.command'; - -/** - * Transforms the content in place by processing nodes and replacing variables with Liquid.js syntax. - * Handles both array iterations and simple variables. - * - * @example - * Input: - * { - * type: "for", - * attrs: { each: "payload.comments" }, - * content: [{ - * type: "variable", - * attrs: { id: "payload.comments.name" } - * }] - * }, - * { - * type: "variable", - * attrs: { id: "payload.test" } - * } - * - * Output: - * { - * type: "paragraph", - * attrs: { each: "{{ payload.comments }}" }, - * content: [{ - * type: "variable", - * text: "{{ payload.comments[0].name }}" - * }] - * }, - * { - * type: "variable", - * text: "{{ payload.test }}" - * } - */ -@Injectable() -export class HydrateEmailSchemaUseCase { - execute(command: HydrateEmailSchemaCommand): TipTapNode { - // TODO: Aligned Zod inferred type and TipTapNode to remove the need of a type assertion - const emailBody: TipTapNode = TipTapSchema.parse(JSON.parse(command.emailEditor)) as TipTapNode; - if (emailBody) { - this.prepareForLiquidParsing([emailBody]); - } - - return emailBody; - } - - private variableLogic( - node: TipTapNode & { - attrs: { id: string }; - }, - content: TipTapNode[], - index: number - ) { - content[index] = { - type: 'variable', - text: node.attrs.id, - attrs: { - ...node.attrs, - }, - }; - } - - private async forNodeLogic( - node: TipTapNode & { - attrs: { each: string }; - }, - content: TipTapNode[], - index: number - ) { - content[index] = { - type: 'paragraph', - content: node.content, - attrs: { - ...node.attrs, - }, - }; - } - - private prepareForLiquidParsing(content: TipTapNode[], forLoopVariable?: string) { - content.forEach((node, index) => { - processNodeAttrs(node, forLoopVariable); - processNodeMarks(node); - - if (this.isForNode(node)) { - this.forNodeLogic(node, content, index); - - if (node.content) { - this.prepareForLiquidParsing(node.content, node.attrs.each); - } - } else if (this.isVariableNode(node)) { - this.variableLogic(node, content, index); - } else if (node.content) { - this.prepareForLiquidParsing(node.content, forLoopVariable); - } - }); - } - - private isForNode(node: TipTapNode): node is TipTapNode & { attrs: { each: string } } { - return !!( - node.type === 'for' && - node.attrs && - node.attrs[MailyAttrsEnum.EACH_KEY] !== undefined && - typeof node.attrs.each === 'string' - ); - } - - private isVariableNode(node: TipTapNode): node is TipTapNode & { attrs: { id: string } } { - return !!( - node.type === 'variable' && - node.attrs && - node.attrs[MailyAttrsEnum.ID] !== undefined && - typeof node.attrs.id === 'string' - ); - } -} - -export const TipTapSchema = z - .object({ - type: z.string().optional(), - content: z.array(z.lazy(() => TipTapSchema)).optional(), - text: z.string().optional(), - marks: z - .array( - z - .object({ - type: z.string(), - attrs: z.record(z.any()).optional(), - }) - .passthrough() - ) - .optional(), - attrs: z.record(z.unknown()).optional(), - }) - .passthrough(); diff --git a/apps/api/src/app/environments-v1/usecases/output-renderers/index.ts b/apps/api/src/app/environments-v1/usecases/output-renderers/index.ts index 64cdbeeed83..fac72149694 100644 --- a/apps/api/src/app/environments-v1/usecases/output-renderers/index.ts +++ b/apps/api/src/app/environments-v1/usecases/output-renderers/index.ts @@ -4,6 +4,3 @@ export * from './push-output-renderer.usecase'; export * from './sms-output-renderer.usecase'; export * from './in-app-output-renderer.usecase'; export * from './email-output-renderer.usecase'; -export * from './hydrate-email-schema.usecase'; -export * from './hydrate-email-schema.command'; -export * from './expand-email-editor-schema.usecase'; diff --git a/apps/api/src/app/environments-v1/usecases/output-renderers/maily-to-liquid/maily.types.ts b/apps/api/src/app/environments-v1/usecases/output-renderers/maily-to-liquid/maily.types.ts new file mode 100644 index 00000000000..582be895f24 --- /dev/null +++ b/apps/api/src/app/environments-v1/usecases/output-renderers/maily-to-liquid/maily.types.ts @@ -0,0 +1,31 @@ +export enum MailyContentTypeEnum { + VARIABLE = 'variable', + FOR = 'for', + BUTTON = 'button', + IMAGE = 'image', + LINK = 'link', +} + +export enum MailyAttrsEnum { + ID = 'id', + SHOW_IF_KEY = 'showIfKey', + EACH_KEY = 'each', + FALLBACK = 'fallback', + IS_SRC_VARIABLE = 'isSrcVariable', + IS_EXTERNAL_LINK_VARIABLE = 'isExternalLinkVariable', + IS_TEXT_VARIABLE = 'isTextVariable', + IS_URL_VARIABLE = 'isUrlVariable', + TEXT = 'text', + URL = 'url', + SRC = 'src', + EXTERNAL_LINK = 'externalLink', + HREF = 'href', +} + +export const MAILY_ITERABLE_MARK = '0'; + +export const MAILY_FIRST_CITIZEN_VARIABLE_KEY = [ + MailyAttrsEnum.ID, + MailyAttrsEnum.SHOW_IF_KEY, + MailyAttrsEnum.EACH_KEY, +]; diff --git a/apps/api/src/app/environments-v1/usecases/output-renderers/maily-to-liquid/wrap-maily-in-liquid.command.ts b/apps/api/src/app/environments-v1/usecases/output-renderers/maily-to-liquid/wrap-maily-in-liquid.command.ts new file mode 100644 index 00000000000..07190fa5693 --- /dev/null +++ b/apps/api/src/app/environments-v1/usecases/output-renderers/maily-to-liquid/wrap-maily-in-liquid.command.ts @@ -0,0 +1,45 @@ +import { BaseCommand } from '@novu/application-generic'; +import { ValidateBy, ValidationOptions } from 'class-validator'; +import { JSONContent as MailyJSONContent } from '@maily-to/render'; +import { z } from 'zod'; + +export const MailyJSONContentSchema = z.custom(); + +export function isStringifiedMailyJSONContent(value: unknown): value is string { + if (typeof value !== 'string') return false; + + try { + const parsed = JSON.parse(value); + + return isObjectMailyJSONContent(parsed); + } catch { + return false; + } +} + +export function isObjectMailyJSONContent(value: unknown): value is MailyJSONContent { + if (!value || typeof value !== 'object') return false; + + const doc = value as MailyJSONContent; + if (doc.type !== 'doc' || !Array.isArray(doc.content)) return false; + + return true; +} + +function IsStringifiedMailyJSONContent(validationOptions?: ValidationOptions) { + return ValidateBy( + { + name: 'isStringifiedMailyJSONContent', + validator: { + validate: (value): boolean => isStringifiedMailyJSONContent(value), + defaultMessage: () => 'Input must be a valid stringified Maily JSON content', + }, + }, + validationOptions + ); +} + +export class WrapMailyInLiquidCommand extends BaseCommand { + @IsStringifiedMailyJSONContent() + emailEditor: string; +} diff --git a/apps/api/src/app/environments-v1/usecases/output-renderers/maily-to-liquid/wrap-maily-in-liquid.usecase.ts b/apps/api/src/app/environments-v1/usecases/output-renderers/maily-to-liquid/wrap-maily-in-liquid.usecase.ts new file mode 100644 index 00000000000..61d9edde972 --- /dev/null +++ b/apps/api/src/app/environments-v1/usecases/output-renderers/maily-to-liquid/wrap-maily-in-liquid.usecase.ts @@ -0,0 +1,208 @@ +/* eslint-disable no-param-reassign */ +import { Injectable } from '@nestjs/common'; +import { z } from 'zod'; +import { JSONContent as MailyJSONContent } from '@maily-to/render'; +import { WrapMailyInLiquidCommand } from './wrap-maily-in-liquid.command'; +import { + MailyContentTypeEnum, + MailyAttrsEnum, + MAILY_ITERABLE_MARK, + MAILY_FIRST_CITIZEN_VARIABLE_KEY, +} from './maily.types'; + +/** + * Enriches Maily JSON content with Liquid syntax for variables. + * + * @example + * Input: + * { + * type: "for", + * attrs: { each: "payload.comments" }, + * content: [{ + * type: "variable", + * attrs: { id: "payload.comments.name" } + * }] + * }, + * { + * type: "variable", + * attrs: { id: "payload.test" } + * } + * + * Output: + * { + * type: "paragraph", + * attrs: { each: "{{ payload.comments }}" }, + * content: [{ + * type: "variable", + * text: "{{ payload.comments[0].name }}" + * }] + * }, + * { + * type: "variable", + * text: "{{ payload.test }}" + * } + */ +@Injectable() +export class WrapMailyInLiquidUseCase { + execute(command: WrapMailyInLiquidCommand): MailyJSONContent { + const mailyJSONContent: MailyJSONContent = JSON.parse(command.emailEditor); + + return this.wrapVariablesInLiquid(mailyJSONContent); + } + + private wrapVariablesInLiquid(node: MailyJSONContent, parentForLoopKey?: string): MailyJSONContent { + const newNode = { ...node } as MailyJSONContent & { attrs: Record }; + + // if this is a for loop node, track its variable + if (this.isForNode(node)) { + parentForLoopKey = node.attrs[MailyAttrsEnum.EACH_KEY]; + } + + if (node.content) { + newNode.content = node.content.map((child) => this.wrapVariablesInLiquid(child, parentForLoopKey)); + } + + if (this.hasAttrs(node)) { + newNode.attrs = this.processVariableNodeAttributes(node, parentForLoopKey); + } + + if (this.hasMarks(node)) { + newNode.marks = this.processNodeMarks(node); + } + + return newNode; + } + + private processVariableNodeAttributes( + node: MailyJSONContent & { attrs: Record }, + parentForLoopKey?: string + ) { + const { attrs, type } = node; + const config = variableAttributeConfig(type as MailyContentTypeEnum); + const processedAttrs = { ...attrs }; + + config.forEach(({ attr, flag }) => { + const attrValue = attrs[attr]; + const flagValue = attrs[flag]; + + if (!flagValue || !attrValue) { + return; + } + + let processedValue = attrValue; + + // add array index for attributes that belong to the for loop + if (parentForLoopKey && processedValue.startsWith(`${parentForLoopKey}`) && type !== MailyContentTypeEnum.FOR) { + processedValue = processedValue.replace(`${parentForLoopKey}`, `${parentForLoopKey}[${MAILY_ITERABLE_MARK}]`); + } + + processedAttrs[attr] = this.wrapInLiquidOutput(processedValue, attrs.fallback); + + if (!MAILY_FIRST_CITIZEN_VARIABLE_KEY.includes(flag)) { + processedAttrs[flag] = false; + } + }); + + return processedAttrs; + } + + private processNodeMarks(node: MailyJSONContent & { marks: Record[] }) { + return node.marks.map((mark) => { + if (!mark.attrs) { + return mark; + } + + const { attrs } = mark; + const processedMark = { + ...mark, + attrs: { ...attrs }, + }; + + const config = variableAttributeConfig(mark.type as MailyContentTypeEnum); + + config.forEach(({ attr, flag }) => { + const attrValue = attrs[attr]; + const flagValue = attrs[flag]; + const { fallback } = attrs; + + if (!flagValue || !attrValue || typeof attrValue !== 'string') { + return; + } + + processedMark.attrs[attr] = this.wrapInLiquidOutput(attrValue, fallback); + + if (!MAILY_FIRST_CITIZEN_VARIABLE_KEY.includes(flag)) { + processedMark.attrs[flag] = false; + } + }); + + return processedMark; + }); + } + + private wrapInLiquidOutput(variableName: string, fallback?: string): string { + const fallbackSuffix = fallback ? ` | default: '${fallback}'` : ''; + + return `{{ ${variableName}${fallbackSuffix} }}`; + } + + private hasAttrs(node: MailyJSONContent): node is MailyJSONContent & { attrs: Record } { + return !!node.attrs; + } + + private hasMarks(node: MailyJSONContent): node is MailyJSONContent & { marks: Record[] } { + return !!node.marks; + } + + private isForNode( + node: MailyJSONContent + ): node is MailyJSONContent & { attrs: { [MailyAttrsEnum.EACH_KEY]: string } } { + return !!( + node.type === MailyContentTypeEnum.FOR && + node.attrs && + node.attrs[MailyAttrsEnum.EACH_KEY] !== undefined && + typeof node.attrs[MailyAttrsEnum.EACH_KEY] === 'string' + ); + } +} + +const variableAttributeConfig = (type: MailyContentTypeEnum) => { + const commonConfig = [ + /* + * Maily Variable Map + * * maily_id equals to maily_variable + * * https://github.com/arikchakma/maily.to/blob/ebcf233eb1d4b16fb568fb702bf0756678db38d0/packages/render/src/maily.tsx#L787 + */ + { attr: MailyAttrsEnum.ID, flag: MailyAttrsEnum.ID }, + /* + * showIfKey is always a maily_variable + */ + { attr: MailyAttrsEnum.SHOW_IF_KEY, flag: MailyAttrsEnum.SHOW_IF_KEY }, + { attr: MailyAttrsEnum.EACH_KEY, flag: MailyAttrsEnum.EACH_KEY }, + ]; + + if (type === MailyContentTypeEnum.BUTTON) { + return [ + { attr: MailyAttrsEnum.TEXT, flag: MailyAttrsEnum.IS_TEXT_VARIABLE }, + { attr: MailyAttrsEnum.URL, flag: MailyAttrsEnum.IS_URL_VARIABLE }, + ...commonConfig, + ]; + } + + if (type === MailyContentTypeEnum.IMAGE) { + return [ + { attr: MailyAttrsEnum.SRC, flag: MailyAttrsEnum.IS_SRC_VARIABLE }, + { + attr: MailyAttrsEnum.EXTERNAL_LINK, + flag: MailyAttrsEnum.IS_EXTERNAL_LINK_VARIABLE, + }, + ...commonConfig, + ]; + } + + if (type === MailyContentTypeEnum.LINK) { + return [{ attr: MailyAttrsEnum.HREF, flag: MailyAttrsEnum.IS_URL_VARIABLE }, ...commonConfig]; + } + + return commonConfig; +}; diff --git a/apps/api/src/app/environments-v1/usecases/update-environment/update-environment.command.ts b/apps/api/src/app/environments-v1/usecases/update-environment/update-environment.command.ts index cdc16866e1f..39e17fa58b5 100644 --- a/apps/api/src/app/environments-v1/usecases/update-environment/update-environment.command.ts +++ b/apps/api/src/app/environments-v1/usecases/update-environment/update-environment.command.ts @@ -19,6 +19,10 @@ export class UpdateEnvironmentCommand extends OrganizationCommand { @IsMongoId() _parentId?: string; + @IsOptional() + @IsString() + color?: string; + @IsOptional() dns?: { inboundParseDomain?: string }; diff --git a/apps/api/src/app/environments-v1/usecases/update-environment/update-environment.usecase.ts b/apps/api/src/app/environments-v1/usecases/update-environment/update-environment.usecase.ts index 03756dd5b5a..417b353983d 100644 --- a/apps/api/src/app/environments-v1/usecases/update-environment/update-environment.usecase.ts +++ b/apps/api/src/app/environments-v1/usecases/update-environment/update-environment.usecase.ts @@ -1,5 +1,6 @@ -import { Injectable } from '@nestjs/common'; +import { Injectable, UnauthorizedException, UnprocessableEntityException } from '@nestjs/common'; import { EnvironmentEntity, EnvironmentRepository } from '@novu/dal'; +import { PROTECTED_ENVIRONMENTS } from '@novu/shared'; import { UpdateEnvironmentCommand } from './update-environment.command'; @Injectable() @@ -7,10 +8,24 @@ export class UpdateEnvironment { constructor(private environmentRepository: EnvironmentRepository) {} async execute(command: UpdateEnvironmentCommand) { + const environment = await this.environmentRepository.findOne({ + _id: command.environmentId, + _organizationId: command.organizationId, + }); + + if (!environment) { + throw new UnauthorizedException('Environment not found'); + } + const updatePayload: Partial = {}; if (command.name && command.name !== '') { - updatePayload.name = command.name; + const normalizedName = command.name.trim(); + if (PROTECTED_ENVIRONMENTS?.map((env) => env.toLowerCase()).includes(normalizedName.toLowerCase())) { + throw new UnprocessableEntityException('Environment name cannot be Development or Production'); + } + + updatePayload.name = normalizedName; } if (command._parentId && command.name !== '') { updatePayload._parentId = command._parentId; @@ -20,6 +35,10 @@ export class UpdateEnvironment { updatePayload.identifier = command.identifier; } + if (command.color) { + updatePayload.color = command.color; + } + if (command.dns && command.dns.inboundParseDomain && command.dns.inboundParseDomain !== '') { updatePayload[`dns.inboundParseDomain`] = command.dns.inboundParseDomain; } diff --git a/apps/api/src/app/events/e2e/bridge-trigger.e2e.ts b/apps/api/src/app/events/e2e/bridge-trigger.e2e.ts index d5d4893c0f0..1435c37b772 100644 --- a/apps/api/src/app/events/e2e/bridge-trigger.e2e.ts +++ b/apps/api/src/app/events/e2e/bridge-trigger.e2e.ts @@ -569,6 +569,42 @@ contexts.forEach((context: Context) => { expect(messagesAfter.length).to.be.eq(1); expect(messagesAfter[0].content).to.match(/people waited for \d+ seconds/); + + const exceedMaxTierDurationWorkflowId = `exceed-max-tier-duration-workflow-${`${context.name}`}`; + const exceedMaxTierDurationWorkflow = workflow(exceedMaxTierDurationWorkflowId, async ({ step }) => { + await step.delay('delay-id', async (controls) => { + return { + type: 'regular', + amount: 100, + unit: 'days', + }; + }); + + await step.inApp('send-in-app', async () => { + return { + body: `people want to wait for 100 days`, + }; + }); + }); + + await bridgeServer.stop(); + await bridgeServer.start({ workflows: [exceedMaxTierDurationWorkflow] }); + + if (context.isStateful) { + await discoverAndSyncBridge(session, workflowsRepository, workflowId, bridgeServer); + } + + const result = await triggerEvent(session, exceedMaxTierDurationWorkflowId, subscriber.subscriberId, {}, bridge); + await session.awaitRunningJobs(); + + const executionDetails = await executionDetailsRepository.find({ + _environmentId: session.environment._id, + _subscriberId: subscriber._id, + transactionId: result?.data?.data?.transactionId, + }); + + const delayExecutionDetails = executionDetails.filter((executionDetail) => executionDetail.channel === 'delay'); + expect(delayExecutionDetails.some((detail) => detail.detail === 'Defer duration limit exceeded')).to.be.true; }); it(`should trigger the bridge workflow with control default and payload data [${context.name}]`, async () => { @@ -1662,7 +1698,7 @@ async function triggerEvent( name: 'test_name', }; - await axios.post( + return await axios.post( `${session.serverUrl}${eventTriggerPath}`, { name: workflowId, diff --git a/apps/api/src/app/organization/usecases/create-organization/create-organization.usecase.ts b/apps/api/src/app/organization/usecases/create-organization/create-organization.usecase.ts index c98be3c217f..2414f323a9d 100644 --- a/apps/api/src/app/organization/usecases/create-organization/create-organization.usecase.ts +++ b/apps/api/src/app/organization/usecases/create-organization/create-organization.usecase.ts @@ -1,8 +1,8 @@ /* eslint-disable global-require */ -import { BadRequestException, Inject, Injectable, Logger, Scope } from '@nestjs/common'; -import { OrganizationEntity, OrganizationRepository, UserRepository } from '@novu/dal'; -import { ApiServiceLevelEnum, JobTitleEnum, MemberRoleEnum, EnvironmentEnum } from '@novu/shared'; +import { BadRequestException, Injectable, Logger, Scope } from '@nestjs/common'; import { AnalyticsService } from '@novu/application-generic'; +import { OrganizationEntity, OrganizationRepository, UserRepository } from '@novu/dal'; +import { ApiServiceLevelEnum, EnvironmentEnum, JobTitleEnum, MemberRoleEnum } from '@novu/shared'; import { ModuleRef } from '@nestjs/core'; import { CreateEnvironmentCommand } from '../../../environments-v1/usecases/create-environment/create-environment.command'; @@ -13,9 +13,8 @@ import { AddMemberCommand } from '../membership/add-member/add-member.command'; import { AddMember } from '../membership/add-member/add-member.usecase'; import { CreateOrganizationCommand } from './create-organization.command'; -import { ApiException } from '../../../shared/exceptions/api.exception'; import { CreateNovuIntegrations } from '../../../integrations/usecases/create-novu-integrations/create-novu-integrations.usecase'; -import { CreateNovuIntegrationsCommand } from '../../../integrations/usecases/create-novu-integrations/create-novu-integrations.command'; +import { ApiException } from '../../../shared/exceptions/api.exception'; @Injectable({ scope: Scope.REQUEST, @@ -61,31 +60,17 @@ export class CreateOrganization { userId: user._id, name: EnvironmentEnum.DEVELOPMENT, organizationId: createdOrganization._id, + system: true, }) ); - await this.createNovuIntegrations.execute( - CreateNovuIntegrationsCommand.create({ - environmentId: devEnv._id, - organizationId: devEnv._organizationId, - userId: user._id, - }) - ); - - const prodEnv = await this.createEnvironmentUsecase.execute( + await this.createEnvironmentUsecase.execute( CreateEnvironmentCommand.create({ userId: user._id, name: EnvironmentEnum.PRODUCTION, organizationId: createdOrganization._id, parentEnvironmentId: devEnv._id, - }) - ); - - await this.createNovuIntegrations.execute( - CreateNovuIntegrationsCommand.create({ - environmentId: prodEnv._id, - organizationId: prodEnv._organizationId, - userId: user._id, + system: true, }) ); @@ -132,9 +117,11 @@ export class CreateOrganization { if (!require('@novu/ee-billing')?.StartReverseFreeTrial) { throw new BadRequestException('Billing module is not loaded'); } + const usecase = this.moduleRef.get(require('@novu/ee-billing')?.StartReverseFreeTrial, { strict: false, }); + await usecase.execute({ userId, organizationId, diff --git a/apps/api/src/app/organization/usecases/create-organization/sync-external-organization/sync-external-organization.usecase.ts b/apps/api/src/app/organization/usecases/create-organization/sync-external-organization/sync-external-organization.usecase.ts index 6f10aeb5569..a14b3fe1733 100644 --- a/apps/api/src/app/organization/usecases/create-organization/sync-external-organization/sync-external-organization.usecase.ts +++ b/apps/api/src/app/organization/usecases/create-organization/sync-external-organization/sync-external-organization.usecase.ts @@ -1,7 +1,7 @@ /* eslint-disable global-require */ -import { BadRequestException, Injectable, Logger, Scope } from '@nestjs/common'; -import { OrganizationEntity, OrganizationRepository, UserRepository } from '@novu/dal'; +import { BadRequestException, Injectable, Logger } from '@nestjs/common'; import { AnalyticsService } from '@novu/application-generic'; +import { OrganizationEntity, OrganizationRepository, UserRepository } from '@novu/dal'; import { ModuleRef } from '@nestjs/core'; import { CreateEnvironmentCommand } from '../../../../environments-v1/usecases/create-environment/create-environment.command'; @@ -9,9 +9,9 @@ import { CreateEnvironment } from '../../../../environments-v1/usecases/create-e import { GetOrganizationCommand } from '../../get-organization/get-organization.command'; import { GetOrganization } from '../../get-organization/get-organization.usecase'; -import { ApiException } from '../../../../shared/exceptions/api.exception'; -import { CreateNovuIntegrations } from '../../../../integrations/usecases/create-novu-integrations/create-novu-integrations.usecase'; import { CreateNovuIntegrationsCommand } from '../../../../integrations/usecases/create-novu-integrations/create-novu-integrations.command'; +import { CreateNovuIntegrations } from '../../../../integrations/usecases/create-novu-integrations/create-novu-integrations.usecase'; +import { ApiException } from '../../../../shared/exceptions/api.exception'; import { SyncExternalOrganizationCommand } from './sync-external-organization.command'; // TODO: eventually move to @novu/ee-auth @@ -49,6 +49,7 @@ export class SyncExternalOrganization { userId: user._id, name: 'Development', organizationId: organization._id, + system: true, }) ); @@ -66,6 +67,7 @@ export class SyncExternalOrganization { name: 'Production', organizationId: organization._id, parentEnvironmentId: devEnv._id, + system: true, }) ); diff --git a/apps/api/src/app/shared/helpers/liquid.ts b/apps/api/src/app/shared/helpers/liquid.ts new file mode 100644 index 00000000000..959869897e2 --- /dev/null +++ b/apps/api/src/app/shared/helpers/liquid.ts @@ -0,0 +1,25 @@ +import { Liquid } from 'liquidjs'; + +export const parseLiquid = async (value: string, variables: object): Promise => { + const client = new Liquid({ + outputEscape: (output) => { + return stringifyDataStructureWithSingleQuotes(output); + }, + }); + + const template = client.parse(value); + + return await client.render(template, variables); +}; + +const stringifyDataStructureWithSingleQuotes = (value: unknown, spaces: number = 0): string => { + if (Array.isArray(value) || (typeof value === 'object' && value !== null)) { + const valueStringified = JSON.stringify(value, null, spaces); + const valueSingleQuotes = valueStringified.replace(/"/g, "'"); + const valueEscapedNewLines = valueSingleQuotes.replace(/\n/g, '\\n'); + + return valueEscapedNewLines; + } else { + return String(value); + } +}; diff --git a/apps/api/src/app/shared/services/query-parser/query-parser.service.ts b/apps/api/src/app/shared/services/query-parser/query-parser.service.ts index 35ac695bb2d..2f63a2967d4 100644 --- a/apps/api/src/app/shared/services/query-parser/query-parser.service.ts +++ b/apps/api/src/app/shared/services/query-parser/query-parser.service.ts @@ -144,3 +144,39 @@ export function isValidRule(rule: RulesLogic): boolean { return false; } } + +export function extractFieldsFromRules(rules: RulesLogic): string[] { + const variables = new Set(); + + const collectVariables = (node: RulesLogic) => { + if (!node || typeof node !== 'object') { + return; + } + + const entries = Object.entries(node); + + for (const [key, value] of entries) { + if (key === 'var' && typeof value === 'string') { + variables.add(value); + continue; + } + + if (Array.isArray(value)) { + value.forEach((item) => { + if (typeof item === 'object') { + collectVariables(item); + } + }); + continue; + } + + if (typeof value === 'object') { + collectVariables(value as RulesLogic); + } + } + }; + + collectVariables(rules); + + return Array.from(variables); +} diff --git a/apps/api/src/app/shared/services/query-parser/query-validator.service.spec.ts b/apps/api/src/app/shared/services/query-parser/query-validator.service.spec.ts new file mode 100644 index 00000000000..b7111d010d1 --- /dev/null +++ b/apps/api/src/app/shared/services/query-parser/query-validator.service.spec.ts @@ -0,0 +1,480 @@ +import { RulesLogic, AdditionalOperation } from 'json-logic-js'; +import { expect } from 'chai'; + +import { QueryIssueTypeEnum, QueryValidatorService } from './query-validator.service'; +import { COMPARISON_OPERATORS, JsonLogicOperatorEnum } from './types'; + +describe('QueryValidatorService', () => { + let queryValidatorService: QueryValidatorService; + + beforeEach(() => { + queryValidatorService = new QueryValidatorService(); + }); + + describe('validateQueryRules', () => { + it('should validate a invalid node structure', () => { + const rule: RulesLogic = null; + + const issues = queryValidatorService.validateQueryRules(rule); + + expect(issues).to.have.lengthOf(1); + expect(issues[0].message).to.include('Invalid node structure'); + expect(issues[0].path).to.deep.equal([]); + }); + + describe('logical operators', () => { + [JsonLogicOperatorEnum.AND, JsonLogicOperatorEnum.OR].forEach((operator) => { + it(`should validate valid ${operator} operation`, () => { + const rule: RulesLogic = { + [operator]: [{ '==': [{ var: 'field1' }, 'value1'] }, { '==': [{ var: 'field2' }, 'value2'] }], + }; + + const issues = queryValidatorService.validateQueryRules(rule); + + expect(issues).to.be.empty; + }); + + it(`should detect invalid ${operator} structure`, () => { + const rule: any = { + [operator]: { '==': [{ var: 'field' }, 'value'] }, // Invalid: and should be an array + }; + + const issues = queryValidatorService.validateQueryRules(rule); + + expect(issues).to.have.lengthOf(1); + expect(issues[0].message).to.include(`Invalid logical operator "${operator}"`); + expect(issues[0].path).to.deep.equal([]); + expect(issues[0].type).to.equal(QueryIssueTypeEnum.INVALID_STRUCTURE); + }); + }); + + it('should validate NOT operation', () => { + const rule: RulesLogic = { + '!': { '==': [{ var: 'field' }, 'value'] }, + }; + + const issues = queryValidatorService.validateQueryRules(rule); + + expect(issues).to.be.empty; + }); + + it('should detect invalid NOT operation', () => { + const rule: RulesLogic = { + '!': { '==': [{ var: 'field' }, ''] }, + }; + + const issues = queryValidatorService.validateQueryRules(rule); + + expect(issues).to.have.lengthOf(1); + expect(issues[0].message).to.include('Value is required'); + expect(issues[0].path).to.deep.equal([]); + expect(issues[0].type).to.equal(QueryIssueTypeEnum.MISSING_VALUE); + }); + }); + + describe('in operation', () => { + it('should detect invalid array in operation', () => { + const rule: any = { + in: [], + }; + + const issues = queryValidatorService.validateQueryRules(rule); + + expect(issues).to.have.lengthOf(1); + expect(issues[0].message).to.include('Invalid operation structure'); + expect(issues[0].path).to.deep.equal([]); + expect(issues[0].type).to.equal(QueryIssueTypeEnum.INVALID_STRUCTURE); + }); + + describe('"in" operation', () => { + it('should validate valid "in" operation', () => { + const rule: RulesLogic = { + in: [{ var: 'field' }, ['value1', 'value2']], + }; + + const issues = queryValidatorService.validateQueryRules(rule); + + expect(issues).to.be.empty; + }); + + it('should detect invalid field reference in "in" operation', () => { + const rule: RulesLogic = { + in: [{}, [1, 2]], + }; + + const issues = queryValidatorService.validateQueryRules(rule); + + expect(issues).to.have.lengthOf(1); + expect(issues[0].message).to.include('Invalid field reference in comparison'); + expect(issues[0].path).to.deep.equal([]); + expect(issues[0].type).to.equal(QueryIssueTypeEnum.INVALID_STRUCTURE); + }); + + it('should detect empty array in "in" operation', () => { + const rule: RulesLogic = { + in: [{ var: 'field' }, []], + }; + + const issues = queryValidatorService.validateQueryRules(rule); + + expect(issues).to.have.lengthOf(1); + expect(issues[0].message).to.include('Value is required'); + expect(issues[0].path).to.deep.equal([]); + expect(issues[0].type).to.equal(QueryIssueTypeEnum.MISSING_VALUE); + }); + }); + + describe('"contains" operation', () => { + it('should validate valid "contains" operation', () => { + const rule: RulesLogic = { + in: ['search', { var: 'field' }], + }; + + const issues = queryValidatorService.validateQueryRules(rule); + + expect(issues).to.be.empty; + }); + + it('should detect invalid field reference in "contains" operation', () => { + const rule: RulesLogic = { + in: ['search', {}], + }; + + const issues = queryValidatorService.validateQueryRules(rule); + + expect(issues).to.have.lengthOf(1); + expect(issues[0].message).to.include('Invalid field reference in comparison'); + expect(issues[0].path).to.deep.equal([]); + expect(issues[0].type).to.equal(QueryIssueTypeEnum.INVALID_STRUCTURE); + }); + + it('should detect invalid value in "contains" operation', () => { + const rule: RulesLogic = { + in: ['', { var: 'field' }], + }; + + const issues = queryValidatorService.validateQueryRules(rule); + + expect(issues).to.have.lengthOf(1); + expect(issues[0].message).to.include('Value is required'); + expect(issues[0].path).to.deep.equal([]); + expect(issues[0].type).to.equal(QueryIssueTypeEnum.MISSING_VALUE); + }); + }); + }); + + describe('between operation', () => { + it('should validate valid between operation', () => { + const rule: RulesLogic = { + '<=': [1, { var: 'field' }, 10], + }; + + const issues = queryValidatorService.validateQueryRules(rule); + + expect(issues).to.be.empty; + }); + + it('should detect invalid between structure from lower bound', () => { + const rule: any = { + '<=': [undefined, { var: 'field' }, 10], // Missing lower bound + }; + + const issues = queryValidatorService.validateQueryRules(rule); + + expect(issues).to.have.lengthOf(1); + expect(issues[0].message).to.include('Value is required'); + expect(issues[0].path).to.deep.equal([]); + expect(issues[0].type).to.equal(QueryIssueTypeEnum.MISSING_VALUE); + }); + + it('should detect invalid between structure from upper bound', () => { + const rule: any = { + '<=': [1, { var: 'field' }, undefined], // Missing upper bound + }; + + const issues = queryValidatorService.validateQueryRules(rule); + + expect(issues).to.have.lengthOf(1); + expect(issues[0].message).to.include('Value is required'); + expect(issues[0].path).to.deep.equal([]); + expect(issues[0].type).to.equal(QueryIssueTypeEnum.MISSING_VALUE); + }); + + it('should detect invalid field reference in "contains" operation', () => { + const rule: any = { + '<=': [1, {}, 1], // invalid field reference + }; + + const issues = queryValidatorService.validateQueryRules(rule); + + expect(issues).to.have.lengthOf(1); + expect(issues[0].message).to.include('Invalid field reference in comparison'); + expect(issues[0].path).to.deep.equal([]); + expect(issues[0].type).to.equal(QueryIssueTypeEnum.INVALID_STRUCTURE); + }); + }); + + describe('comparison operators', () => { + COMPARISON_OPERATORS.forEach((operator) => { + it(`should validate a valid simple ${operator} rule`, () => { + const rule: RulesLogic = { + [operator]: [{ var: 'field' }, 'value'], + }; + + const issues = queryValidatorService.validateQueryRules(rule); + + expect(issues).to.be.empty; + }); + + it(`should detect invalid ${operator} structure`, () => { + const rule: any = { + [operator]: [{ var: 'field' }], // Missing second operand + }; + + const issues = queryValidatorService.validateQueryRules(rule); + + expect(issues).to.have.lengthOf(1); + expect(issues[0].message).to.include('Invalid operation structure'); + expect(issues[0].type).to.equal(QueryIssueTypeEnum.INVALID_STRUCTURE); + }); + + it(`should detect invalid field reference in "${operator}" operation`, () => { + const rule: RulesLogic = { + [operator]: [{}, 'value'], + }; + + const issues = queryValidatorService.validateQueryRules(rule); + + expect(issues).to.have.lengthOf(1); + expect(issues[0].message).to.include('Invalid field reference in comparison'); + expect(issues[0].path).to.deep.equal([]); + expect(issues[0].type).to.equal(QueryIssueTypeEnum.INVALID_STRUCTURE); + }); + }); + + it('should validate valid comparison operations', () => { + const validOperations: RulesLogic[] = [ + { '<': [{ var: 'field' }, 5] }, + { '>': [{ var: 'field' }, 5] }, + { '<=': [{ var: 'field' }, 5] }, + { '>=': [{ var: 'field' }, 5] }, + { '==': [{ var: 'field' }, 'value'] }, + { '!=': [{ var: 'field' }, 'value'] }, + ]; + + validOperations.forEach((operation) => { + const issues = queryValidatorService.validateQueryRules(operation); + expect(issues).to.be.empty; + }); + }); + + it('should handle null values correctly for isNull', () => { + const rule: RulesLogic = { + '==': [{ var: 'field' }, null], + }; + + const issues = queryValidatorService.validateQueryRules(rule); + + expect(issues).to.be.empty; + }); + + it('should handle null values correctly for !isNull', () => { + const rule: RulesLogic = { + '!=': [{ var: 'field' }, null], + }; + + const issues = queryValidatorService.validateQueryRules(rule); + + expect(issues).to.be.empty; + }); + + it('should detect null values for non-equality operators', () => { + const rule: RulesLogic = { + '>': [{ var: 'field' }, null], + }; + + const issues = queryValidatorService.validateQueryRules(rule); + + expect(issues).to.have.lengthOf(1); + expect(issues[0].message).to.include('Value is required'); + expect(issues[0].type).to.equal(QueryIssueTypeEnum.MISSING_VALUE); + }); + }); + + describe('path calculation', () => { + const tests = [ + { + name: 'single rule', + rule: { + and: [ + { + '==': [ + { + var: 'subscriber.email', + }, + '', + ], + }, + ], + }, + path: [0], + }, + { + name: 'second rule', + rule: { + and: [ + { + '==': [ + { + var: 'subscriber.email', + }, + 'asdf', + ], + }, + { + '==': [ + { + var: 'subscriber.email', + }, + '', + ], + }, + ], + }, + path: [1], + }, + { + name: 'nested rule', + rule: { + and: [ + { + and: [ + { + '==': [ + { + var: 'subscriber.email', + }, + '', + ], + }, + ], + }, + ], + }, + path: [0, 0], + }, + { + name: 'nested second rule', + rule: { + and: [ + { + and: [ + { + '==': [ + { + var: 'subscriber.email', + }, + 'asdf', + ], + }, + { + '!=': [ + { + var: 'subscriber.email', + }, + undefined, + ], + }, + ], + }, + ], + }, + path: [0, 1], + }, + { + name: 'second or operator first rule', + rule: { + or: [ + { + and: [ + { + '==': [ + { + var: 'subscriber.email', + }, + 'asdf', + ], + }, + { + '!=': [ + { + var: 'subscriber.email', + }, + '22', + ], + }, + ], + }, + { + or: [ + { + '==': [ + { + var: 'subscriber.email', + }, + '', + ], + }, + ], + }, + ], + }, + path: [1, 0], + }, + { + name: 'nested not in operation', + rule: { + or: [ + { + and: [ + { + '==': [ + { + var: 'subscriber.email', + }, + 'asdf', + ], + }, + { + '!': { + in: [ + '', + { + var: 'subscriber.firstName', + }, + ], + }, + }, + ], + }, + ], + }, + path: [0, 1], + }, + ]; + + tests.forEach((test) => { + it(`should return the correct path for ${test.name}`, () => { + const { rule, path } = test; + + const issues = queryValidatorService.validateQueryRules(rule as any); + + expect(issues).to.have.lengthOf(1); + expect(issues[0].message).to.include('Value is required'); + expect(issues[0].path).to.deep.equal(path); + expect(issues[0].type).to.equal(QueryIssueTypeEnum.MISSING_VALUE); + }); + }); + }); + }); +}); diff --git a/apps/api/src/app/shared/services/query-parser/query-validator.service.ts b/apps/api/src/app/shared/services/query-parser/query-validator.service.ts new file mode 100644 index 00000000000..6d731884fd3 --- /dev/null +++ b/apps/api/src/app/shared/services/query-parser/query-validator.service.ts @@ -0,0 +1,251 @@ +import { AdditionalOperation, RulesLogic } from 'json-logic-js'; + +import { COMPARISON_OPERATORS, JsonComparisonOperatorEnum, JsonLogicOperatorEnum } from './types'; + +type QueryIssue = { + message: string; + path: number[]; + type: QueryIssueTypeEnum; +}; + +export enum QueryIssueTypeEnum { + INVALID_STRUCTURE = 'INVALID_STRUCTURE', + MISSING_VALUE = 'MISSING_VALUE', +} + +export class QueryValidatorService { + private isInvalidFieldReference(field: unknown) { + return !field || typeof field !== 'object' || !('var' in field); + } + + private getLogicalOperatorIssue(operator: string, path: number[]): QueryIssue { + return { + message: `Invalid logical operator "${operator}" structure`, + path, + type: QueryIssueTypeEnum.INVALID_STRUCTURE, + }; + } + + private getFieldReferenceIssue(path: number[]): QueryIssue { + return { + message: 'Invalid field reference in comparison', + path, + type: QueryIssueTypeEnum.INVALID_STRUCTURE, + }; + } + + private getOperationIssue(operator: string, path: number[]): QueryIssue { + return { + message: `Invalid operation structure for operator "${operator}"`, + path, + type: QueryIssueTypeEnum.INVALID_STRUCTURE, + }; + } + + private getValueIssue(path: number[]): QueryIssue { + return { + message: 'Value is required', + path, + type: QueryIssueTypeEnum.MISSING_VALUE, + }; + } + + private validateNode({ + node, + issues, + path = [], + }: { + node: RulesLogic; + issues: QueryIssue[]; + path?: number[]; + }) { + if (!node || typeof node !== 'object') { + issues.push({ + message: 'Invalid node structure', + path, + type: QueryIssueTypeEnum.INVALID_STRUCTURE, + }); + + return; + } + + const entries = Object.entries(node); + + for (const [key, value] of entries) { + // handle logical operators "and" and "or" + if ([JsonLogicOperatorEnum.AND, JsonLogicOperatorEnum.OR].includes(key as JsonLogicOperatorEnum)) { + if (!Array.isArray(value)) { + issues.push(this.getLogicalOperatorIssue(key, path)); + continue; + } + + value.forEach((item, index) => this.validateNode({ node: item, issues, path: [...path, index] })); + continue; + } + + // handle negation '!' operator + if (key === JsonLogicOperatorEnum.NOT) { + this.validateNode({ node: value as RulesLogic, issues, path }); + continue; + } + + // handle 'in' and 'contains' operators + if (key === JsonComparisonOperatorEnum.IN) { + this.validateInOperation({ value, issues, path }); + continue; + } + + const isBetween = + key === JsonComparisonOperatorEnum.LESS_THAN_OR_EQUAL && Array.isArray(value) && value.length === 3; + if (isBetween) { + this.validateBetweenOperation({ value: value as [unknown, unknown, unknown], issues, path }); + continue; + } + + // handle the rest of the comparison operators + if (COMPARISON_OPERATORS.includes(key as JsonComparisonOperatorEnum)) { + this.validateComparisonOperation({ operator: key as JsonComparisonOperatorEnum, value, issues, path }); + continue; + } + + // handle field variable + if (key === 'var') { + if (!value) { + issues.push({ + message: 'Field variable is required', + path, + type: QueryIssueTypeEnum.MISSING_VALUE, + }); + } + + continue; + } + } + } + + private validateBetweenOperation({ + value, + issues, + path, + }: { + value: [unknown, unknown, unknown]; + issues: QueryIssue[]; + path: number[]; + }) { + const [lowerBound, field, upperBound] = value; + + if (this.isInvalidFieldReference(field)) { + issues.push(this.getFieldReferenceIssue(path)); + } + + const lowerBoundIsUndefined = lowerBound === undefined || lowerBound === null; + const upperBoundIsUndefined = upperBound === undefined || upperBound === null; + if (lowerBoundIsUndefined || upperBoundIsUndefined) { + issues.push(this.getValueIssue(path)); + } + } + + private validateComparisonOperation({ + operator, + value, + issues, + path, + }: { + operator: JsonComparisonOperatorEnum; + value: unknown; + issues: QueryIssue[]; + path: number[]; + }) { + if (!Array.isArray(value) || value.length !== 2) { + issues.push(this.getOperationIssue(operator, path)); + + return; + } + + const [field, comparisonValue] = value; + + // Validate field reference + if (this.isInvalidFieldReference(field)) { + issues.push(this.getFieldReferenceIssue(path)); + } + + // Validate comparison value exists + const valueIsUndefinedOrEmptyCase = + (comparisonValue === undefined || comparisonValue === '') && + [ + JsonComparisonOperatorEnum.EQUAL, + JsonComparisonOperatorEnum.NOT_EQUAL, + JsonComparisonOperatorEnum.LESS_THAN, + JsonComparisonOperatorEnum.GREATER_THAN, + JsonComparisonOperatorEnum.LESS_THAN_OR_EQUAL, + JsonComparisonOperatorEnum.GREATER_THAN_OR_EQUAL, + JsonComparisonOperatorEnum.STARTS_WITH, + JsonComparisonOperatorEnum.ENDS_WITH, + ].includes(operator); + const valueIsNullCase = + comparisonValue === null && + ![JsonComparisonOperatorEnum.EQUAL, JsonComparisonOperatorEnum.NOT_EQUAL].includes(operator); + + if (valueIsUndefinedOrEmptyCase || valueIsNullCase) { + issues.push(this.getValueIssue(path)); + } + + // Validate array for 'in' operations + const invalidComparisonValue = operator === 'in' && !Array.isArray(comparisonValue); + if (invalidComparisonValue) { + issues.push(this.getOperationIssue(operator, path)); + } + } + + /* + * in operator has field and the array as operands + * but as contains it has the search value and the field as operands + */ + private validateInOperation({ value, issues, path }: { value: unknown; issues: QueryIssue[]; path: number[] }) { + if (!Array.isArray(value) || value.length !== 2) { + issues.push(this.getOperationIssue('in', path)); + + return; + } + + const [firstOperand, secondOperand] = value; + const isContains = typeof firstOperand === 'string'; + + if (isContains) { + // Validate search value exists + const searchValueExists = firstOperand === undefined || firstOperand === ''; + if (searchValueExists) { + issues.push(this.getValueIssue(path)); + } + + // Validate field reference + const secondOperandInvalid = !secondOperand || typeof secondOperand !== 'object' || !('var' in secondOperand); + if (secondOperandInvalid) { + issues.push(this.getFieldReferenceIssue(path)); + } + } else { + // Validate field reference + const firstOperandInvalid = !firstOperand || typeof firstOperand !== 'object' || !('var' in firstOperand); + if (firstOperandInvalid) { + issues.push(this.getFieldReferenceIssue(path)); + } + + // Validate the in array is not empty + const secondOperandEmpty = + secondOperand === undefined || + secondOperand === null || + (Array.isArray(secondOperand) && secondOperand.length === 0); + if (secondOperandEmpty) { + issues.push(this.getValueIssue(path)); + } + } + } + + public validateQueryRules(node: RulesLogic): QueryIssue[] { + const issues: QueryIssue[] = []; + + this.validateNode({ node, issues }); + + return issues; + } +} diff --git a/apps/api/src/app/shared/services/query-parser/types.ts b/apps/api/src/app/shared/services/query-parser/types.ts new file mode 100644 index 00000000000..c9df38d780b --- /dev/null +++ b/apps/api/src/app/shared/services/query-parser/types.ts @@ -0,0 +1,35 @@ +export enum JsonComparisonOperatorEnum { + EQUAL = '==', + NOT_EQUAL = '!=', + GREATER_THAN = '>', + LESS_THAN = '<', + GREATER_THAN_OR_EQUAL = '>=', + LESS_THAN_OR_EQUAL = '<=', + IN = 'in', + STARTS_WITH = 'startsWith', + ENDS_WITH = 'endsWith', +} + +export enum JsonLogicOperatorEnum { + NOT = '!', + AND = 'and', + OR = 'or', +} + +export const COMPARISON_OPERATORS = [ + JsonComparisonOperatorEnum.EQUAL, + JsonComparisonOperatorEnum.NOT_EQUAL, + JsonComparisonOperatorEnum.GREATER_THAN, + JsonComparisonOperatorEnum.LESS_THAN, + JsonComparisonOperatorEnum.GREATER_THAN_OR_EQUAL, + JsonComparisonOperatorEnum.LESS_THAN_OR_EQUAL, + JsonComparisonOperatorEnum.IN, + JsonComparisonOperatorEnum.STARTS_WITH, + JsonComparisonOperatorEnum.ENDS_WITH, +] as const; + +export const LOGICAL_OPERATORS = [ + JsonLogicOperatorEnum.AND, + JsonLogicOperatorEnum.OR, + JsonLogicOperatorEnum.NOT, +] as const; diff --git a/apps/api/src/app/support/support.controller.ts b/apps/api/src/app/support/support.controller.ts index 9ab9d13a014..f1a30dbd4c1 100644 --- a/apps/api/src/app/support/support.controller.ts +++ b/apps/api/src/app/support/support.controller.ts @@ -16,9 +16,9 @@ export class SupportController { ) {} @UseGuards(PlainCardsGuard) - @Post('user-organizations') + @Post('customer-details') async fetchUserOrganizations(@Body() body: PlainCardRequestDto) { - return this.plainCardsUsecase.fetchUserOrganizations(PlainCardsCommand.create({ ...body })); + return this.plainCardsUsecase.fetchCustomerDetails(PlainCardsCommand.create({ ...body })); } @UseGuards(UserAuthGuard) diff --git a/apps/api/src/app/support/support.module.ts b/apps/api/src/app/support/support.module.ts index 1007966f39a..bae29169f09 100644 --- a/apps/api/src/app/support/support.module.ts +++ b/apps/api/src/app/support/support.module.ts @@ -1,6 +1,6 @@ import { Module } from '@nestjs/common'; import { SupportService } from '@novu/application-generic'; -import { OrganizationRepository } from '@novu/dal'; +import { OrganizationRepository, UserRepository } from '@novu/dal'; import { SupportController } from './support.controller'; import { SharedModule } from '../shared/shared.module'; import { CreateSupportThreadUsecase, PlainCardsUsecase } from './usecases'; @@ -9,6 +9,13 @@ import { PlainCardsGuard } from './guards/plain-cards.guard'; @Module({ imports: [SharedModule], controllers: [SupportController], - providers: [CreateSupportThreadUsecase, PlainCardsUsecase, SupportService, OrganizationRepository, PlainCardsGuard], + providers: [ + CreateSupportThreadUsecase, + PlainCardsUsecase, + SupportService, + OrganizationRepository, + UserRepository, + PlainCardsGuard, + ], }) export class SupportModule {} diff --git a/apps/api/src/app/support/usecases/plain-cards.usecase.ts b/apps/api/src/app/support/usecases/plain-cards.usecase.ts index c81be8d3c67..4592ba09f99 100644 --- a/apps/api/src/app/support/usecases/plain-cards.usecase.ts +++ b/apps/api/src/app/support/usecases/plain-cards.usecase.ts @@ -1,17 +1,48 @@ import { Injectable } from '@nestjs/common'; -import { OrganizationRepository } from '@novu/dal'; +import { OrganizationRepository, UserRepository } from '@novu/dal'; import { PlainCardsCommand } from './plain-cards.command'; +const divider = [ + { + componentDivider: { + dividerSpacingSize: 'M', + }, + }, +]; + +const organizationDetailsHeading = [ + { + componentText: { + text: `User's Organizations`, + textSize: 'L', + }, + }, +]; + +const sessionsDetailsHeading = [ + { + componentText: { + text: `User's Sessions`, + textSize: 'L', + }, + }, +]; + @Injectable() export class PlainCardsUsecase { - constructor(private organizationRepository: OrganizationRepository) {} - async fetchUserOrganizations(command: PlainCardsCommand) { + constructor( + private organizationRepository: OrganizationRepository, + private userRepository: UserRepository + ) {} + async fetchCustomerDetails(command: PlainCardsCommand) { + const key = process.env.NOVU_REGION === 'eu-west-2' ? 'customer-details-eu' : 'customer-details-us'; + if (!command?.customer?.externalId) { return { data: {}, cards: [ { - key: 'plain-customer-details', + key, components: [ { componentSpacer: { @@ -28,9 +59,30 @@ export class PlainCardsUsecase { ], }; } + const organizations = await this.organizationRepository.findUserActiveOrganizations(command?.customer?.externalId); + const sessions = await this.userRepository.findUserSessions(command?.customer?.externalId); - const organizationsComponent = organizations?.map((organization) => { + return { + data: {}, + cards: [ + { + key, + components: [ + ...organizationDetailsHeading, + ...divider, + ...this.organizationsComponent(organizations), + ...divider, + ...sessionsDetailsHeading, + ...this.sessionsComponent(sessions), + ], + }, + ], + }; + } + + private organizationsComponent = (organizations) => { + const activeOrganizations = organizations?.map((organization) => { return { componentContainer: { containerContent: [ @@ -199,14 +251,133 @@ export class PlainCardsUsecase { }; }); - return { - data: {}, - cards: [ - { - key: 'plain-customer-details', - components: organizationsComponent, + return activeOrganizations; + }; + + private sessionsComponent = (sessions) => { + const allSessions = sessions.map((session) => { + return { + componentContainer: { + containerContent: [ + { + componentRow: { + rowMainContent: [ + { + componentText: { + text: 'Status', + textSize: 'S', + }, + }, + ], + rowAsideContent: [ + { + componentText: { + text: session?.status || 'NA', + }, + }, + ], + }, + }, + { + componentRow: { + rowMainContent: [ + { + componentText: { + text: 'City', + textSize: 'S', + }, + }, + ], + rowAsideContent: [ + { + componentText: { + text: session?.latestActivity?.city || 'NA', + }, + }, + ], + }, + }, + { + componentRow: { + rowMainContent: [ + { + componentText: { + text: 'Country', + textSize: 'S', + }, + }, + ], + rowAsideContent: [ + { + componentText: { + text: session?.latestActivity?.country || 'NA', + }, + }, + ], + }, + }, + { + componentRow: { + rowMainContent: [ + { + componentText: { + text: 'Device Type', + textSize: 'S', + }, + }, + ], + rowAsideContent: [ + { + componentText: { + text: session?.latestActivity?.deviceType || 'NA', + }, + }, + ], + }, + }, + { + componentRow: { + rowMainContent: [ + { + componentText: { + text: 'Browser Name', + textSize: 'S', + }, + }, + ], + rowAsideContent: [ + { + componentText: { + text: session?.latestActivity?.browserName || 'NA', + }, + }, + ], + }, + }, + { + componentRow: { + rowMainContent: [ + { + componentText: { + text: 'Browser Version', + textSize: 'S', + }, + }, + ], + rowAsideContent: [ + { + componentText: { + text: session?.latestActivity?.browserVersion || 'NA', + }, + }, + ], + }, + }, + ], }, - ], - }; - } + }; + }); + + return allSessions; + }; } diff --git a/apps/api/src/app/workflows-v2/e2e/generate-preview.e2e.ts b/apps/api/src/app/workflows-v2/e2e/generate-preview.e2e.ts index fd12586c389..37e793772cc 100644 --- a/apps/api/src/app/workflows-v2/e2e/generate-preview.e2e.ts +++ b/apps/api/src/app/workflows-v2/e2e/generate-preview.e2e.ts @@ -520,150 +520,6 @@ describe('Workflow Step Preview - POST /:workflowId/step/:stepId/preview #novu-v }); }); - it('should transform tip tap node to liquid variables', async () => { - const workflow = await createWorkflow(); - - const stepId = workflow.steps[1]._id; // Using the email step (second step) - const bodyControlValue = { - type: 'doc', - content: [ - { - type: 'heading', - attrs: { textAlign: 'left', level: 1 }, - content: [ - { type: 'text', text: 'New Maily Email Editor ' }, - { type: 'variable', attrs: { id: 'payload.foo', label: null, fallback: null, showIfKey: null } }, - { type: 'text', text: ' ' }, - ], - }, - { - type: 'paragraph', - attrs: { textAlign: 'left' }, - content: [ - { type: 'text', text: 'free text last name is: ' }, - { - type: 'variable', - attrs: { id: 'subscriber.lastName', label: null, fallback: null, showIfKey: `payload.show` }, - }, - { type: 'text', text: ' ' }, - { type: 'hardBreak' }, - { type: 'text', text: 'extra data : ' }, - { type: 'variable', attrs: { id: 'payload.extraData', label: null, fallback: null, showIfKey: null } }, - { type: 'text', text: ' ' }, - ], - }, - ], - }; - const controlValues = { - subject: 'Hello {{subscriber.firstName}} World!', - body: JSON.stringify(bodyControlValue), - }; - - const { status, body } = await session.testAgent.post(`/v2/workflows/${workflow._id}/step/${stepId}/preview`).send({ - controlValues, - previewPayload: {}, - }); - - expect(status).to.equal(201); - expect(body.data.result.type).to.equal('email'); - expect(body.data.result.preview.subject).to.equal('Hello {{subscriber.firstName}} World!'); - expect(body.data.result.preview.body).to.not.include('{{subscriber.lastName}}'); - expect(body.data.result.preview.body).to.include('{{payload.foo}}'); - expect(body.data.result.preview.body).to.include('{{payload.extraData}}'); - expect(body.data.previewPayloadExample).to.deep.equal({ - subscriber: { - firstName: '{{subscriber.firstName}}', - lastName: '{{subscriber.lastName}}', - }, - payload: { - foo: '{{payload.foo}}', - show: '{{payload.show}}', - extraData: '{{payload.extraData}}', - }, - }); - }); - - it('should render tip tap node with api client variables example', async () => { - const workflow = await createWorkflow(); - - const stepId = workflow.steps[1]._id; // Using the email step (second step) - const bodyControlValue = { - type: 'doc', - content: [ - { - type: 'heading', - attrs: { textAlign: 'left', level: 1 }, - content: [ - { type: 'text', text: 'New Maily Email Editor ' }, - { type: 'variable', attrs: { id: 'payload.foo', label: null, fallback: null, showIfKey: null } }, - { type: 'text', text: ' ' }, - ], - }, - { - type: 'paragraph', - attrs: { textAlign: 'left' }, - content: [ - { type: 'text', text: 'free text last name is: ' }, - { - type: 'variable', - attrs: { id: 'subscriber.lastName', label: null, fallback: null, showIfKey: `payload.show` }, - }, - { type: 'text', text: ' ' }, - { type: 'hardBreak' }, - { type: 'text', text: 'extra data : ' }, - { - type: 'variable', - attrs: { - id: 'payload.extraData', - label: null, - fallback: 'fallback extra data is awesome', - showIfKey: null, - }, - }, - { type: 'text', text: ' ' }, - ], - }, - ], - }; - const controlValues = { - subject: 'Hello {{subscriber.firstName}} World!', - body: JSON.stringify(bodyControlValue), - }; - - const { status, body } = await session.testAgent.post(`/v2/workflows/${workflow._id}/step/${stepId}/preview`).send({ - controlValues, - previewPayload: { - subscriber: { - firstName: 'John', - // lastName: 'Doe', - }, - payload: { - foo: 'foo from client', - show: true, - extraData: '', - }, - }, - }); - - expect(status).to.equal(201); - expect(body.data.result.type).to.equal('email'); - expect(body.data.result.preview.subject).to.equal('Hello John World!'); - expect(body.data.result.preview.body).to.include('{{subscriber.lastName}}'); - expect(body.data.result.preview.body).to.include('foo from client'); - expect(body.data.result.preview.body).to.include('fallback extra data is awesome'); - expect(body.data.previewPayloadExample).to.deep.equal({ - subscriber: { - firstName: 'John', - lastName: '{{subscriber.lastName}}', - }, - payload: { - foo: 'foo from client', - show: true, - extraData: '', - }, - }); - }); - async function createWorkflow(overrides: Partial = {}): Promise { const createWorkflowDto: CreateWorkflowDto = { __source: WorkflowCreationSourceEnum.EDITOR, diff --git a/apps/api/src/app/workflows-v2/e2e/list-workflows.e2e.ts b/apps/api/src/app/workflows-v2/e2e/list-workflows.e2e.ts new file mode 100644 index 00000000000..ad8a956422b --- /dev/null +++ b/apps/api/src/app/workflows-v2/e2e/list-workflows.e2e.ts @@ -0,0 +1,133 @@ +import { + CreateWorkflowDto, + DirectionEnum, + WorkflowCreationSourceEnum, + WorkflowResponseDto, + WorkflowStatusEnum, +} from '@novu/shared'; +import { UserSession } from '@novu/testing'; +import { expect } from 'chai'; + +describe('List Workflows - /workflows (GET) #novu-v2', function () { + let session: UserSession; + + beforeEach(async () => { + session = new UserSession(); + await session.initialize(); + }); + + describe('Pagination and Search', () => { + it('should correctly paginate workflows', async () => { + const workflowIds: string[] = []; + for (let i = 0; i < 15; i += 1) { + const workflow = await createWorkflow(`Test Workflow ${i}`); + workflowIds.push(workflow._id); + } + + const firstPage = await session.testAgent.get('/v2/workflows').query({ + limit: 10, + offset: 0, + }); + + expect(firstPage.body.data.workflows).to.have.length(10); + expect(firstPage.body.data.totalCount).to.equal(15); + + const secondPage = await session.testAgent.get('/v2/workflows').query({ + limit: 10, + offset: 10, + }); + + expect(secondPage.body.data.workflows).to.have.length(5); + expect(secondPage.body.data.totalCount).to.equal(15); + + const firstPageIds = firstPage.body.data.workflows.map((workflow: WorkflowResponseDto) => workflow._id); + const secondPageIds = secondPage.body.data.workflows.map((workflow: WorkflowResponseDto) => workflow._id); + const uniqueIds = new Set([...firstPageIds, ...secondPageIds]); + + expect(uniqueIds.size).to.equal(15); + }); + + it('should correctly search workflows by name', async () => { + const searchTerm = 'SEARCHABLE_WORKFLOW'; + + // Create workflows with different names + await createWorkflow(`${searchTerm}_1`); + await createWorkflow(`${searchTerm}_2`); + await createWorkflow('Different Workflow'); + + const { body } = await session.testAgent.get('/v2/workflows').query({ + query: searchTerm, + }); + + expect(body.data.workflows).to.have.length(2); + expect(body.data.workflows[0].name).to.include(searchTerm); + expect(body.data.workflows[1].name).to.include(searchTerm); + }); + }); + + describe('Sorting', () => { + it('should sort workflows by creation date in descending order by default', async () => { + await createWorkflow('First Workflow'); + await delay(100); // Ensure different creation times + await createWorkflow('Second Workflow'); + + const { body } = await session.testAgent.get('/v2/workflows'); + + expect(body.data.workflows[0].name).to.equal('Second Workflow'); + expect(body.data.workflows[1].name).to.equal('First Workflow'); + }); + + it('should sort workflows by creation date in ascending order when specified', async () => { + await createWorkflow('First Workflow'); + await delay(100); // Ensure different creation times + await createWorkflow('Second Workflow'); + + const { body } = await session.testAgent.get('/v2/workflows').query({ + orderDirection: DirectionEnum.ASC, + orderBy: 'createdAt', + }); + + expect(body.data.workflows[0].name).to.equal('First Workflow'); + expect(body.data.workflows[1].name).to.equal('Second Workflow'); + }); + }); + + describe('Response Structure', () => { + it('should return correct workflow fields in response', async () => { + const workflowName = 'Test Workflow Structure'; + const createdWorkflow = await createWorkflow(workflowName); + + const { body } = await session.testAgent.get('/v2/workflows'); + const returnedWorkflow = body.data.workflows[0]; + + expect(returnedWorkflow).to.include({ + _id: createdWorkflow._id, + name: workflowName, + workflowId: createdWorkflow.workflowId, + status: WorkflowStatusEnum.ACTIVE, + }); + expect(returnedWorkflow.createdAt).to.be.a('string'); + expect(returnedWorkflow.updatedAt).to.be.a('string'); + }); + }); + + async function createWorkflow(name: string): Promise { + const createWorkflowDto: CreateWorkflowDto = { + name, + workflowId: name.toLowerCase().replace(/\s+/g, '-'), + __source: WorkflowCreationSourceEnum.EDITOR, + active: true, + steps: [], + }; + + const { body } = await session.testAgent.post('/v2/workflows').send(createWorkflowDto); + + return body.data; + } + + function delay(ms: number): Promise { + return new Promise((resolve) => { + setTimeout(resolve, ms); + }); + } +}); diff --git a/apps/api/src/app/workflows-v2/generate-preview.e2e.ts b/apps/api/src/app/workflows-v2/generate-preview.e2e.ts index fc70ab3a40e..7d564eae5a6 100644 --- a/apps/api/src/app/workflows-v2/generate-preview.e2e.ts +++ b/apps/api/src/app/workflows-v2/generate-preview.e2e.ts @@ -8,6 +8,7 @@ import { createWorkflowClient, CreateWorkflowDto, CronExpressionEnum, + EmailRenderOutput, GeneratePreviewRequestDto, GeneratePreviewResponseDto, HttpError, @@ -18,7 +19,7 @@ import { } from '@novu/shared'; import { EmailControlType, InAppControlType } from '@novu/application-generic'; import { buildCreateWorkflowDto } from './workflow.controller.e2e'; -import { forSnippet, fullCodeSnippet } from './maily-test-data'; +import { fullCodeSnippet, previewPayloadExample } from './maily-test-data'; const SUBJECT_TEST_PAYLOAD = '{{payload.subject.test.payload}}'; const PLACEHOLDER_SUBJECT_INAPP = '{{payload.subject}}'; @@ -170,138 +171,29 @@ describe('Generate Preview #novu-v2', () => { expect(previewResponseDto.result!.preview).to.deep.equal(getTestControlValues()[StepTypeEnum.CHAT]); }); - it.skip('email: should match the body in the preview response', async () => { + it('email: should match the body in the preview response', async () => { const previewResponseDto = await createWorkflowAndPreview(StepTypeEnum.EMAIL, 'Email'); + const preview = previewResponseDto.result.preview as EmailRenderOutput; - expect(previewResponseDto.result!.preview).to.exist; - expect(previewResponseDto.previewPayloadExample).to.exist; - expect(previewResponseDto.previewPayloadExample.subscriber, 'Expecting to find subscriber in the payload').to - .exist; + expect(previewResponseDto.result.type).to.equal(StepTypeEnum.EMAIL); - assertEmail(previewResponseDto); + expect(preview).to.exist; + expect(preview.body).to.exist; + expect(preview.subject).to.exist; + expect(preview.body).to.contain(previewPayloadExample().payload.body); + expect(preview.subject).to.contain(`Hello, World! ${SUBJECT_TEST_PAYLOAD}`); + expect(previewResponseDto.previewPayloadExample).to.exist; + expect(previewResponseDto.previewPayloadExample).to.deep.equal(previewPayloadExample()); }); async function createWorkflowAndPreview(type: StepTypeEnum, description: string) { - const { stepDatabaseId, workflowId, stepId } = await createWorkflowAndReturnId(workflowsClient, type); + const { stepDatabaseId, workflowId } = await createWorkflowAndReturnId(workflowsClient, type); const requestDto = buildDtoNoPayload(type); return await generatePreview(workflowsClient, workflowId, stepDatabaseId, requestDto, description); } }); - describe('email specific features', () => { - describe('show', () => { - it('show -> should hide element based on payload', async () => { - const { stepDatabaseId, workflowId } = await createWorkflowAndReturnId(workflowsClient, StepTypeEnum.EMAIL); - const previewResponseDto = await generatePreview( - workflowsClient, - workflowId, - stepDatabaseId, - { - controlValues: getTestControlValues()[StepTypeEnum.EMAIL], - previewPayload: { payload: { params: { isPayedUser: 'false' } } }, - }, - 'email' - ); - expect(previewResponseDto.result!.preview).to.exist; - if (previewResponseDto.result!.type !== ChannelTypeEnum.EMAIL) { - throw new Error('Expected email'); - } - const preview = previewResponseDto.result!.preview.body; - expect(preview).to.not.contain('should be the fallback value'); - }); - it('show -> should show element based on payload - string', async () => { - const { stepDatabaseId, workflowId } = await createWorkflowAndReturnId(workflowsClient, StepTypeEnum.EMAIL); - const previewResponseDto = await generatePreview( - workflowsClient, - workflowId, - stepDatabaseId, - { - controlValues: getTestControlValues()[StepTypeEnum.EMAIL], - previewPayload: { payload: { params: { isPayedUser: 'true' } } }, - }, - 'email' - ); - expect(previewResponseDto.result!.preview).to.exist; - if (previewResponseDto.result!.type !== ChannelTypeEnum.EMAIL) { - throw new Error('Expected email'); - } - const preview = previewResponseDto.result!.preview.body; - expect(preview).to.contain('should be the fallback value'); - }); - it('show -> should show element based on payload - boolean', async () => { - const { stepDatabaseId, workflowId, stepId } = await createWorkflowAndReturnId( - workflowsClient, - StepTypeEnum.EMAIL - ); - const previewResponseDto = await generatePreview( - workflowsClient, - workflowId, - stepDatabaseId, - { - controlValues: getTestControlValues(stepId)[StepTypeEnum.EMAIL], - previewPayload: { payload: { params: { isPayedUser: true } } }, - }, - 'email' - ); - if (previewResponseDto.result!.type !== ChannelTypeEnum.EMAIL) { - throw new Error('Expected email'); - } - const preview = previewResponseDto.result!.preview.body; - expect(preview).to.contain('should be the fallback value'); - }); - it('show -> should show element if payload is missing', async () => { - const { stepDatabaseId, workflowId, stepId } = await createWorkflowAndReturnId( - workflowsClient, - StepTypeEnum.EMAIL - ); - const previewResponseDto = await generatePreview( - workflowsClient, - workflowId, - stepDatabaseId, - { - controlValues: getTestControlValues(stepId)[StepTypeEnum.EMAIL], - previewPayload: { payload: { params: { isPayedUser: 'true' } } }, - }, - 'email' - ); - expect(previewResponseDto.result!.preview).to.exist; - if (previewResponseDto.result!.type !== ChannelTypeEnum.EMAIL) { - throw new Error('Expected email'); - } - const preview = previewResponseDto.result!.preview.body; - expect(preview).to.contain('should be the fallback value'); - }); - }); - describe('for', () => { - it('should populate for if payload exist with actual values', async () => { - const { stepDatabaseId, workflowId } = await createWorkflowAndReturnId(workflowsClient, StepTypeEnum.EMAIL); - const name1 = 'ball is round'; - const name2 = 'square is square'; - const previewResponseDto = await generatePreview( - workflowsClient, - workflowId, - stepDatabaseId, - { - controlValues: buildSimpleForEmail() as unknown as Record, - previewPayload: { payload: { food: { items: [{ name: name1 }, { name: name2 }] } } }, - }, - 'email' - ); - expect(previewResponseDto.result!.preview).to.exist; - if (previewResponseDto.result!.type !== ChannelTypeEnum.EMAIL) { - throw new Error('Expected email'); - } - const preview = previewResponseDto.result!.preview.body; - expect(preview).to.not.contain('should be the fallback value'); - expect(preview).not.to.contain('{{item.name}}1'); - expect(preview).not.to.contain('{{item.name}}2'); - expect(preview).to.contain(name1); - expect(preview).to.contain(name2); - }); - }); - }); - describe('payload sanitation', () => { it('Should produce a correct payload when pipe is used etc {{payload.variable | upper}}', async () => { const { stepDatabaseId, workflowId } = await createWorkflowAndReturnId(workflowsClient, StepTypeEnum.SMS); @@ -507,18 +399,13 @@ function buildDtoNoPayload(stepTypeEnum: StepTypeEnum, stepId?: string): Generat }; } -function buildEmailControlValuesPayload(stepId?: string): EmailControlType { +function buildEmailControlValuesPayload(): EmailControlType { return { subject: `Hello, World! ${SUBJECT_TEST_PAYLOAD}`, - body: JSON.stringify(fullCodeSnippet(stepId)), - }; -} -function buildSimpleForEmail(): EmailControlType { - return { - subject: `Hello, World! ${SUBJECT_TEST_PAYLOAD}`, - body: JSON.stringify(forSnippet), + body: JSON.stringify(fullCodeSnippet()), }; } + function buildInAppControlValues() { return { subject: `{{subscriber.firstName}} Hello, World! ${PLACEHOLDER_SUBJECT_INAPP}`, @@ -598,7 +485,7 @@ function buildDigestControlValuesPayload() { export const getTestControlValues = (stepId?: string) => ({ [StepTypeEnum.SMS]: buildSmsControlValuesPayload(stepId), - [StepTypeEnum.EMAIL]: buildEmailControlValuesPayload(stepId) as unknown as Record, + [StepTypeEnum.EMAIL]: buildEmailControlValuesPayload(), [StepTypeEnum.PUSH]: buildPushControlValuesPayload(), [StepTypeEnum.CHAT]: buildChatControlValuesPayload(), [StepTypeEnum.IN_APP]: buildInAppControlValues(), @@ -619,23 +506,6 @@ async function assertHttpError( return new Error(`${description}: Failed to generate preview, bug in response error mapping `); } -function assertEmail(dto: GeneratePreviewResponseDto) { - if (dto.result!.type === ChannelTypeEnum.EMAIL) { - const preview = dto.result!.preview.body; - expect(preview).to.exist; - expect(preview).to.contain('{{item.header}}-1'); - expect(preview).to.contain('{{item.header}}-2'); - expect(preview).to.contain('{{item.name}}-1'); - expect(preview).to.contain('{{item.name}}-2'); - expect(preview).to.contain('{{item.id}}-1'); - expect(preview).to.contain('{{item.id}}-2'); - expect(preview).to.contain('{{item.origin.country}}-1'); - expect(preview).to.contain('{{item.origin.country}}-2'); - expect(preview).to.contain('{{payload.body}}'); - expect(preview).to.contain('should be the fallback value'); - } -} - export async function createWorkflowAndReturnId( workflowsClient: ReturnType, type: StepTypeEnum diff --git a/apps/api/src/app/workflows-v2/maily-test-data.ts b/apps/api/src/app/workflows-v2/maily-test-data.ts index ec88cc877f5..6f3f98f8512 100644 --- a/apps/api/src/app/workflows-v2/maily-test-data.ts +++ b/apps/api/src/app/workflows-v2/maily-test-data.ts @@ -1,42 +1,4 @@ -export const forSnippet = { - type: 'doc', - content: [ - { - type: 'for', - attrs: { - each: 'payload.food.items', - isUpdatingKey: false, - }, - content: [ - { - type: 'paragraph', - attrs: { - textAlign: 'left', - }, - content: [ - { - type: 'text', - text: 'this is a food item with name ', - }, - { - type: 'variable', - attrs: { - id: 'payload.food.items.name', - label: null, - }, - }, - { - type: 'text', - text: ' ', - }, - ], - }, - ], - }, - ], -}; - -export function fullCodeSnippet(stepId?: string) { +export function fullCodeSnippet() { return { type: 'doc', content: [ @@ -502,3 +464,66 @@ export function fullCodeSnippet(stepId?: string) { ], }; } + +export function previewPayloadExample() { + return { + payload: { + subject: { + test: { + payload: '{{payload.subject.test.payload}}', + }, + }, + params: { + isPayedUser: true, + }, + hidden: { + section: '{{payload.hidden.section}}', + }, + body: '{{payload.body}}', + origins: [ + { + country: '{{payload.origins.country}}', + id: '{{payload.origins.id}}', + time: '{{payload.origins.time}}', + }, + { + country: '{{payload.origins.country}}', + id: '{{payload.origins.id}}', + time: '{{payload.origins.time}}', + }, + { + country: '{{payload.origins.country}}', + id: '{{payload.origins.id}}', + time: '{{payload.origins.time}}', + }, + ], + students: [ + { + id: '{{payload.students.id}}', + name: '{{payload.students.name}}', + }, + { + id: '{{payload.students.id}}', + name: '{{payload.students.name}}', + }, + { + id: '{{payload.students.id}}', + name: '{{payload.students.name}}', + }, + ], + food: { + items: [ + { + name: '{{payload.food.items.name}}', + }, + { + name: '{{payload.food.items.name}}', + }, + { + name: '{{payload.food.items.name}}', + }, + ], + }, + }, + }; +} diff --git a/apps/api/src/app/workflows-v2/usecases/build-payload-schema/build-payload-schema.usecase.ts b/apps/api/src/app/workflows-v2/usecases/build-payload-schema/build-payload-schema.usecase.ts deleted file mode 100644 index ca33080eee5..00000000000 --- a/apps/api/src/app/workflows-v2/usecases/build-payload-schema/build-payload-schema.usecase.ts +++ /dev/null @@ -1,138 +0,0 @@ -import { Injectable } from '@nestjs/common'; -import { ControlValuesRepository } from '@novu/dal'; -import { ControlValuesLevelEnum, JSONSchemaDto } from '@novu/shared'; -import { Instrument, InstrumentUsecase } from '@novu/application-generic'; -import { flattenObjectValues, keysToObject } from '../../util/utils'; -import { extractLiquidTemplateVariables } from '../../util/template-parser/liquid-parser'; -import { BuildPayloadSchemaCommand } from './build-payload-schema.command'; -import { isStringTipTapNode } from '../../util/tip-tap.util'; -import { HydrateEmailSchemaUseCase } from '../../../environments-v1/usecases/output-renderers/hydrate-email-schema.usecase'; - -@Injectable() -export class BuildPayloadSchema { - constructor( - private readonly controlValuesRepository: ControlValuesRepository, - private readonly hydrateEmailSchemaUseCase: HydrateEmailSchemaUseCase - ) {} - - @InstrumentUsecase() - async execute(command: BuildPayloadSchemaCommand): Promise { - const controlValues = await this.getControlValues(command); - const extractedVariables = await this.extractAllVariables(controlValues); - - return this.buildVariablesSchema(extractedVariables); - } - - private async getControlValues(command: BuildPayloadSchemaCommand) { - let controlValues = command.controlValues ? [command.controlValues] : []; - - if (!controlValues.length && command.workflowId) { - controlValues = ( - await this.controlValuesRepository.find( - { - _environmentId: command.environmentId, - _organizationId: command.organizationId, - _workflowId: command.workflowId, - level: ControlValuesLevelEnum.STEP_CONTROLS, - controls: { $ne: null }, - }, - { - controls: 1, - _id: 0, - } - ) - ).map((item) => item.controls); - } - - return controlValues.flat(); - } - - @Instrument() - private async extractAllVariables(controlValues: Record[]): Promise { - const allVariables: string[] = []; - - for (const controlValue of controlValues) { - const processedControlValue = await this.extractVariables(controlValue); - const controlValuesString = flattenObjectValues(processedControlValue).join(' '); - const templateVariables = extractLiquidTemplateVariables(controlValuesString); - allVariables.push(...templateVariables.validVariables.map((variable) => variable.name)); - } - - return [...new Set(allVariables)]; - } - - @Instrument() - private async extractVariables(controlValue: Record): Promise> { - const processedValue: Record = {}; - - for (const [key, value] of Object.entries(controlValue)) { - if (isStringTipTapNode(value)) { - processedValue[key] = this.hydrateEmailSchemaUseCase.execute({ emailEditor: value }); - } else { - processedValue[key] = value; - } - } - - return processedValue; - } - - private async buildVariablesSchema(variables: string[]) { - const { payload } = keysToObject(variables); - - const schema: JSONSchemaDto = { - type: 'object', - properties: {}, - required: [], - additionalProperties: true, - }; - - if (payload) { - for (const [key, value] of Object.entries(payload)) { - if (schema.properties && schema.required) { - schema.properties[key] = determineSchemaType(value); - schema.required.push(key); - } - } - } - - return schema; - } -} - -function determineSchemaType(value: unknown): JSONSchemaDto { - if (value === null) { - return { type: 'null' }; - } - - if (Array.isArray(value)) { - return { - type: 'array', - items: value.length > 0 ? determineSchemaType(value[0]) : { type: 'null' }, - }; - } - - switch (typeof value) { - case 'string': - return { type: 'string', default: value }; - case 'number': - return { type: 'number', default: value }; - case 'boolean': - return { type: 'boolean', default: value }; - case 'object': - return { - type: 'object', - properties: Object.entries(value).reduce( - (acc, [key, val]) => { - acc[key] = determineSchemaType(val); - - return acc; - }, - {} as { [key: string]: JSONSchemaDto } - ), - required: Object.keys(value), - }; - - default: - return { type: 'null' }; - } -} diff --git a/apps/api/src/app/workflows-v2/usecases/build-step-issues/build-step-issues.usecase.ts b/apps/api/src/app/workflows-v2/usecases/build-step-issues/build-step-issues.usecase.ts index 16cfa6a249d..7ff98a4f61f 100644 --- a/apps/api/src/app/workflows-v2/usecases/build-step-issues/build-step-issues.usecase.ts +++ b/apps/api/src/app/workflows-v2/usecases/build-step-issues/build-step-issues.usecase.ts @@ -1,3 +1,9 @@ +import merge from 'lodash/merge'; +import capitalize from 'lodash/capitalize'; +import isEmpty from 'lodash/isEmpty'; +import Ajv, { ErrorObject } from 'ajv'; +import addFormats from 'ajv-formats'; +import { AdditionalOperation, RulesLogic } from 'json-logic-js'; import { Injectable } from '@nestjs/common'; import { ControlValuesRepository } from '@novu/dal'; import { @@ -16,16 +22,16 @@ import { TierRestrictionsValidateCommand, dashboardSanitizeControlValues, PinoLogger, + Instrument, } from '@novu/application-generic'; -import merge from 'lodash/merge'; -import capitalize from 'lodash/capitalize'; -import isEmpty from 'lodash/isEmpty'; -import Ajv, { ErrorObject } from 'ajv'; -import addFormats from 'ajv-formats'; import { buildVariables } from '../../util/build-variables'; import { BuildVariableSchemaCommand, BuildVariableSchemaUsecase } from '../build-variable-schema'; import { BuildStepIssuesCommand } from './build-step-issues.command'; +import { + QueryIssueTypeEnum, + QueryValidatorService, +} from '../../../shared/services/query-parser/query-validator.service'; @Injectable() export class BuildStepIssuesUsecase { @@ -73,18 +79,29 @@ export class BuildStepIssuesUsecase { )?.controls; } - const sanitizedControlValues = - newControlValues && workflowOrigin === WorkflowOriginEnum.NOVU_CLOUD - ? dashboardSanitizeControlValues(this.logger, newControlValues, stepTypeDto) || {} - : this.frameworkSanitizeEmptyStringsToNull(newControlValues) || {}; - + const sanitizedControlValues = this.sanitizeControlValues(newControlValues, workflowOrigin, stepTypeDto); const schemaIssues = this.processControlValuesBySchema(controlSchema, sanitizedControlValues || {}); const liquidIssues = this.processControlValuesByLiquid(variableSchema, newControlValues || {}); const customIssues = await this.processControlValuesByCustomeRules(user, stepTypeDto, sanitizedControlValues || {}); + const skipLogicIssues = sanitizedControlValues?.skip + ? this.validateSkipField(sanitizedControlValues.skip as RulesLogic) + : {}; + + return merge(schemaIssues, liquidIssues, customIssues, skipLogicIssues); + } - return merge(schemaIssues, liquidIssues, customIssues); + @Instrument() + private sanitizeControlValues( + newControlValues: Record | undefined, + workflowOrigin: WorkflowOriginEnum, + stepTypeDto: StepTypeEnum + ) { + return newControlValues && workflowOrigin === WorkflowOriginEnum.NOVU_CLOUD + ? dashboardSanitizeControlValues(this.logger, newControlValues, stepTypeDto) || {} + : this.frameworkSanitizeEmptyStringsToNull(newControlValues) || {}; } + @Instrument() private processControlValuesByLiquid( variableSchema: JSONSchemaDto | undefined, controlValues: Record | null @@ -125,6 +142,7 @@ export class BuildStepIssuesUsecase { return issues; } + @Instrument() private processControlValuesBySchema( controlSchema: JSONSchemaDto | undefined, controlValues: Record | null @@ -167,6 +185,7 @@ export class BuildStepIssuesUsecase { return issues; } + @Instrument() private async processControlValuesByCustomeRules( user: UserSessionData, stepType: StepTypeEnum, @@ -257,4 +276,27 @@ export class BuildStepIssuesUsecase { return error.message || 'Invalid value'; } + + @Instrument() + private validateSkipField(skipLogic: RulesLogic): StepIssuesDto { + const issues: StepIssuesDto = {}; + + const queryValidatorService = new QueryValidatorService(); + const skipRulesIssues = queryValidatorService.validateQueryRules(skipLogic); + + if (skipRulesIssues.length > 0) { + issues.controls = { + skip: skipRulesIssues.map((issue) => ({ + issueType: + issue.type === QueryIssueTypeEnum.MISSING_VALUE + ? StepContentIssueEnum.MISSING_VALUE + : StepContentIssueEnum.ILLEGAL_VARIABLE_IN_CONTROL_VALUE, + message: issue.message, + variableName: issue.path.join('.'), + })), + }; + } + + return issues.controls?.skip.length ? issues : {}; + } } diff --git a/apps/api/src/app/workflows-v2/usecases/build-test-data/build-workflow-test-data.usecase.ts b/apps/api/src/app/workflows-v2/usecases/build-test-data/build-workflow-test-data.usecase.ts index 799c001197f..acf788abd81 100644 --- a/apps/api/src/app/workflows-v2/usecases/build-test-data/build-workflow-test-data.usecase.ts +++ b/apps/api/src/app/workflows-v2/usecases/build-test-data/build-workflow-test-data.usecase.ts @@ -16,14 +16,15 @@ import { import { WorkflowTestDataCommand } from './build-workflow-test-data.command'; import { parsePayloadSchema } from '../../shared/parse-payload-schema'; import { mockSchemaDefaults } from '../../util/utils'; -import { BuildPayloadSchema } from '../build-payload-schema/build-payload-schema.usecase'; -import { BuildPayloadSchemaCommand } from '../build-payload-schema/build-payload-schema.command'; +import { ExtractVariables } from '../extract-variables/extract-variables.usecase'; +import { ExtractVariablesCommand } from '../extract-variables/extract-variables.command'; +import { buildVariablesSchema } from '../../util/create-schema'; @Injectable() export class BuildWorkflowTestDataUseCase { constructor( private readonly getWorkflowByIdsUseCase: GetWorkflowByIdsUseCase, - private readonly buildPayloadSchema: BuildPayloadSchema + private readonly extractVariables: ExtractVariables ) {} @InstrumentUsecase() @@ -48,14 +49,16 @@ export class BuildWorkflowTestDataUseCase { return parsePayloadSchema(workflow.payloadSchema, { safe: true }) || {}; } - return this.buildPayloadSchema.execute( - BuildPayloadSchemaCommand.create({ + const { payload } = await this.extractVariables.execute( + ExtractVariablesCommand.create({ environmentId: command.user.environmentId, organizationId: command.user.organizationId, userId: command.user._id, workflowId: workflow._id, }) ); + + return buildVariablesSchema(payload); } private generatePayloadMock(schema: JSONSchemaDto): Record { diff --git a/apps/api/src/app/workflows-v2/usecases/build-variable-schema/build-available-variable-schema.usecase.ts b/apps/api/src/app/workflows-v2/usecases/build-variable-schema/build-available-variable-schema.usecase.ts index e8625d0f3db..2015de825f9 100644 --- a/apps/api/src/app/workflows-v2/usecases/build-variable-schema/build-available-variable-schema.usecase.ts +++ b/apps/api/src/app/workflows-v2/usecases/build-variable-schema/build-available-variable-schema.usecase.ts @@ -5,13 +5,14 @@ import { Instrument } from '@novu/application-generic'; import { computeResultSchema } from '../../shared'; import { BuildVariableSchemaCommand } from './build-available-variable-schema.command'; import { parsePayloadSchema } from '../../shared/parse-payload-schema'; -import { BuildPayloadSchemaCommand } from '../build-payload-schema/build-payload-schema.command'; -import { BuildPayloadSchema } from '../build-payload-schema/build-payload-schema.usecase'; +import { ExtractVariablesCommand } from '../extract-variables/extract-variables.command'; +import { ExtractVariables } from '../extract-variables/extract-variables.usecase'; import { emptyJsonSchema } from '../../util/jsonToSchema'; +import { buildVariablesSchema } from '../../util/create-schema'; @Injectable() export class BuildVariableSchemaUsecase { - constructor(private readonly buildPayloadSchema: BuildPayloadSchema) {} + constructor(private readonly extractVariables: ExtractVariables) {} async execute(command: BuildVariableSchemaCommand): Promise { const { workflow, stepInternalId } = command; @@ -19,6 +20,15 @@ export class BuildVariableSchemaUsecase { 0, workflow?.steps.findIndex((stepItem) => stepItem._id === stepInternalId) ); + const { payload, subscriber } = await this.extractVariables.execute( + ExtractVariablesCommand.create({ + environmentId: command.environmentId, + organizationId: command.organizationId, + userId: command.userId, + workflowId: workflow?._id, + ...(command.optimisticControlValues ? { controlValues: command.optimisticControlValues } : {}), + }) + ); return { type: 'object', @@ -40,18 +50,15 @@ export class BuildVariableSchemaUsecase { format: 'date-time', description: 'The last time the subscriber was online (optional)', }, - data: { - type: 'object', - properties: {}, - description: 'Additional data about the subscriber', - additionalProperties: true, - }, + data: buildVariablesSchema( + subscriber && typeof subscriber === 'object' && 'data' in subscriber ? subscriber.data : {} + ), }, required: ['firstName', 'lastName', 'email', 'subscriberId'], additionalProperties: false, }, steps: buildPreviousStepsSchema(previousSteps, workflow?.payloadSchema), - payload: await this.resolvePayloadSchema(workflow, command), + payload: await this.resolvePayloadSchema(workflow, payload), }, additionalProperties: false, } as const satisfies JSONSchemaDto; @@ -60,7 +67,7 @@ export class BuildVariableSchemaUsecase { @Instrument() private async resolvePayloadSchema( workflow: NotificationTemplateEntity | undefined, - command: BuildVariableSchemaCommand + payload: unknown ): Promise { if (workflow && workflow.steps.length === 0) { return { @@ -74,15 +81,7 @@ export class BuildVariableSchemaUsecase { return parsePayloadSchema(workflow.payloadSchema, { safe: true }) || emptyJsonSchema(); } - return this.buildPayloadSchema.execute( - BuildPayloadSchemaCommand.create({ - environmentId: command.environmentId, - organizationId: command.organizationId, - userId: command.userId, - workflowId: workflow?._id, - ...(command.optimisticControlValues ? { controlValues: command.optimisticControlValues } : {}), - }) - ); + return buildVariablesSchema(payload); } } diff --git a/apps/api/src/app/workflows-v2/usecases/build-payload-schema/build-payload-schema.command.ts b/apps/api/src/app/workflows-v2/usecases/extract-variables/extract-variables.command.ts similarity index 84% rename from apps/api/src/app/workflows-v2/usecases/build-payload-schema/build-payload-schema.command.ts rename to apps/api/src/app/workflows-v2/usecases/extract-variables/extract-variables.command.ts index f18e6313fc8..831c8b8106a 100644 --- a/apps/api/src/app/workflows-v2/usecases/build-payload-schema/build-payload-schema.command.ts +++ b/apps/api/src/app/workflows-v2/usecases/extract-variables/extract-variables.command.ts @@ -1,7 +1,7 @@ import { EnvironmentWithUserCommand } from '@novu/application-generic'; import { IsString, IsObject, IsOptional } from 'class-validator'; -export class BuildPayloadSchemaCommand extends EnvironmentWithUserCommand { +export class ExtractVariablesCommand extends EnvironmentWithUserCommand { @IsString() @IsOptional() workflowId?: string; diff --git a/apps/api/src/app/workflows-v2/usecases/extract-variables/extract-variables.usecase.ts b/apps/api/src/app/workflows-v2/usecases/extract-variables/extract-variables.usecase.ts new file mode 100644 index 00000000000..f29652faf1b --- /dev/null +++ b/apps/api/src/app/workflows-v2/usecases/extract-variables/extract-variables.usecase.ts @@ -0,0 +1,63 @@ +import { Injectable } from '@nestjs/common'; +import { ControlValuesRepository } from '@novu/dal'; +import { ControlValuesLevelEnum } from '@novu/shared'; +import { Instrument, InstrumentUsecase } from '@novu/application-generic'; + +import { keysToObject } from '../../util/utils'; +import { buildVariables } from '../../util/build-variables'; +import { ExtractVariablesCommand } from './extract-variables.command'; + +@Injectable() +export class ExtractVariables { + constructor(private readonly controlValuesRepository: ControlValuesRepository) {} + + @InstrumentUsecase() + async execute(command: ExtractVariablesCommand): Promise> { + const controlValues = await this.getControlValues(command); + const extractedVariables = await this.extractAllVariables(controlValues); + + return keysToObject(extractedVariables); + } + + private async getControlValues(command: ExtractVariablesCommand) { + let controlValues = command.controlValues ? [command.controlValues] : []; + + if (!controlValues.length && command.workflowId) { + controlValues = ( + await this.controlValuesRepository.find( + { + _environmentId: command.environmentId, + _organizationId: command.organizationId, + _workflowId: command.workflowId, + level: ControlValuesLevelEnum.STEP_CONTROLS, + controls: { $ne: null }, + }, + { + controls: 1, + _id: 0, + } + ) + ).map((item) => item.controls); + } + + // get just the actual control "values", not entire objects + return controlValues.flat().flatMap((obj) => Object.values(obj)); + } + + /** + * @example + * controlValues = [ "John {{name}}", "Address {{address}} {{address}}", "nothing", 123, true ] + * returns = [ "name", "address" ] + */ + @Instrument() + private async extractAllVariables(controlValues: unknown[]): Promise { + const allVariables: string[] = []; + + for (const controlValue of controlValues) { + const templateVariables = buildVariables(undefined, controlValue); + allVariables.push(...templateVariables.validVariables.map((variable) => variable.name)); + } + + return [...new Set(allVariables)]; + } +} diff --git a/apps/api/src/app/workflows-v2/usecases/generate-preview/generate-preview.usecase.ts b/apps/api/src/app/workflows-v2/usecases/generate-preview/generate-preview.usecase.ts index 8f63622b640..4f635db5748 100644 --- a/apps/api/src/app/workflows-v2/usecases/generate-preview/generate-preview.usecase.ts +++ b/apps/api/src/app/workflows-v2/usecases/generate-preview/generate-preview.usecase.ts @@ -12,7 +12,6 @@ import { PreviewPayload, StepResponseDto, WorkflowOriginEnum, - TipTapNode, StepTypeEnum, } from '@novu/shared'; import { @@ -25,17 +24,18 @@ import { dashboardSanitizeControlValues, } from '@novu/application-generic'; import { channelStepSchemas, actionStepSchemas } from '@novu/framework/internal'; - +import { JSONContent as MailyJSONContent } from '@maily-to/render'; import { PreviewStep, PreviewStepCommand } from '../../../bridge/usecases/preview-step'; import { FrameworkPreviousStepsOutputState } from '../../../bridge/usecases/preview-step/preview-step.command'; import { BuildStepDataUsecase } from '../build-step-data'; import { GeneratePreviewCommand } from './generate-preview.command'; -import { BuildPayloadSchemaCommand } from '../build-payload-schema/build-payload-schema.command'; -import { BuildPayloadSchema } from '../build-payload-schema/build-payload-schema.usecase'; +import { ExtractVariablesCommand } from '../extract-variables/extract-variables.command'; +import { ExtractVariables } from '../extract-variables/extract-variables.usecase'; import { Variable } from '../../util/template-parser/liquid-parser'; -import { isObjectTipTapNode } from '../../util/tip-tap.util'; import { buildVariables } from '../../util/build-variables'; import { keysToObject, mergeCommonObjectKeys, multiplyArrayItems } from '../../util/utils'; +import { buildVariablesSchema } from '../../util/create-schema'; +import { isObjectMailyJSONContent } from '../../../environments-v1/usecases/output-renderers/maily-to-liquid/wrap-maily-in-liquid.command'; const LOG_CONTEXT = 'GeneratePreviewUsecase'; @@ -45,7 +45,7 @@ export class GeneratePreviewUsecase { private previewStepUsecase: PreviewStep, private buildStepDataUsecase: BuildStepDataUsecase, private getWorkflowByIdsUseCase: GetWorkflowByIdsUseCase, - private buildPayloadSchema: BuildPayloadSchema, + private extractVariables: ExtractVariables, private readonly logger: PinoLogger ) {} @@ -84,9 +84,10 @@ export class GeneratePreviewUsecase { for (const [controlKey, controlValue] of Object.entries(sanitizedValidatedControls || {})) { const variables = buildVariables(variableSchema, controlValue, this.logger); const processedControlValues = this.fixControlValueInvalidVariables(controlValue, variables.invalidVariables); - + const showIfVariables: string[] = this.findShowIfVariables(processedControlValues); const validVariableNames = variables.validVariables.map((variable) => variable.name); - const variablesExampleResult = keysToObject(validVariableNames); + const variablesExampleResult = keysToObject(validVariableNames, showIfVariables); + // multiply array items by 3 for preview example purposes const multipliedVariablesExampleResult = multiplyArrayItems(variablesExampleResult, 3); @@ -94,7 +95,7 @@ export class GeneratePreviewUsecase { variablesExample: _.merge(previewTemplateData.variablesExample, multipliedVariablesExampleResult), controlValues: { ...previewTemplateData.controlValues, - [controlKey]: isObjectTipTapNode(processedControlValues) + [controlKey]: isObjectMailyJSONContent(processedControlValues) ? JSON.stringify(processedControlValues) : processedControlValues, }, @@ -141,6 +142,33 @@ export class GeneratePreviewUsecase { } } + /** + * Extracts showIf variables from TipTap nodes to transform template variables + * (e.g. {{payload.foo}}) into true - for preview purposes + */ + private findShowIfVariables(processedControlValues: Record) { + const showIfVariables: string[] = []; + if (typeof processedControlValues === 'string') { + try { + const parsed = JSON.parse(processedControlValues); + const extractShowIfKeys = (node: any) => { + if (node?.attrs?.showIfKey) { + const key = node.attrs.showIfKey; + showIfVariables.push(key); + } + if (node.content) { + node.content.forEach((child: any) => extractShowIfKeys(child)); + } + }; + extractShowIfKeys(parsed); + } catch (e) { + // If parsing fails, continue with empty showIfVariables + } + } + + return showIfVariables; + } + private sanitizeControlsForPreview(initialControlValues: Record, stepData: StepResponseDto) { const sanitizedValues = dashboardSanitizeControlValues(this.logger, initialControlValues, stepData.type); @@ -189,8 +217,8 @@ export class GeneratePreviewUsecase { command: GeneratePreviewCommand, controlValues: Record ) { - const payloadSchema = await this.buildPayloadSchema.execute( - BuildPayloadSchemaCommand.create({ + const { payload } = await this.extractVariables.execute( + ExtractVariablesCommand.create({ environmentId: command.user.environmentId, organizationId: command.user.organizationId, userId: command.user._id, @@ -198,6 +226,7 @@ export class GeneratePreviewUsecase { controlValues, }) ); + const payloadSchema = buildVariablesSchema(payload); if (Object.keys(payloadSchema).length === 0) { return variables; @@ -403,7 +432,7 @@ const EMPTY_STRING = ''; const WHITESPACE = ' '; const DEFAULT_URL_TARGET = '_blank'; const DEFAULT_URL_PATH = 'https://www.redirect-example.com'; -const DEFAULT_TIP_TAP_EMPTY_PREVIEW: TipTapNode = { +const DEFAULT_TIP_TAP_EMPTY_PREVIEW: MailyJSONContent = { type: 'doc', content: [ { diff --git a/apps/api/src/app/workflows-v2/usecases/list-workflows/list-workflow.usecase.ts b/apps/api/src/app/workflows-v2/usecases/list-workflows/list-workflow.usecase.ts index dd0c2a3fdcd..7dde7614a3d 100644 --- a/apps/api/src/app/workflows-v2/usecases/list-workflows/list-workflow.usecase.ts +++ b/apps/api/src/app/workflows-v2/usecases/list-workflows/list-workflow.usecase.ts @@ -1,10 +1,10 @@ import { Injectable } from '@nestjs/common'; +import { InstrumentUsecase } from '@novu/application-generic'; import { NotificationTemplateRepository } from '@novu/dal'; import { ListWorkflowResponse } from '@novu/shared'; -import { InstrumentUsecase } from '@novu/application-generic'; -import { ListWorkflowsCommand } from './list-workflows.command'; import { toWorkflowsMinifiedDtos } from '../../mappers/notification-template-mapper'; +import { ListWorkflowsCommand } from './list-workflows.command'; @Injectable() export class ListWorkflowsUseCase { @@ -17,7 +17,10 @@ export class ListWorkflowsUseCase { command.user.environmentId, command.offset, command.limit, - command.searchQuery + command.searchQuery, + false, + command.orderBy, + command.orderDirection ); if (res.data === null || res.data === undefined) { return { workflows: [], totalCount: 0 }; diff --git a/apps/api/src/app/workflows-v2/util/build-variables.ts b/apps/api/src/app/workflows-v2/util/build-variables.ts index d17206e30c0..fd42a7eb838 100644 --- a/apps/api/src/app/workflows-v2/util/build-variables.ts +++ b/apps/api/src/app/workflows-v2/util/build-variables.ts @@ -1,10 +1,12 @@ import _ from 'lodash'; - -import { MAILY_ITERABLE_MARK, PinoLogger } from '@novu/application-generic'; +import { AdditionalOperation, RulesLogic } from 'json-logic-js'; +import { PinoLogger } from '@novu/application-generic'; import { Variable, extractLiquidTemplateVariables, TemplateVariables } from './template-parser/liquid-parser'; -import { isStringTipTapNode } from './tip-tap.util'; -import { HydrateEmailSchemaUseCase } from '../../environments-v1/usecases/output-renderers/hydrate-email-schema.usecase'; +import { WrapMailyInLiquidUseCase } from '../../environments-v1/usecases/output-renderers/maily-to-liquid/wrap-maily-in-liquid.usecase'; +import { MAILY_ITERABLE_MARK } from '../../environments-v1/usecases/output-renderers/maily-to-liquid/maily.types'; +import { isStringifiedMailyJSONContent } from '../../environments-v1/usecases/output-renderers/maily-to-liquid/wrap-maily-in-liquid.command'; +import { extractFieldsFromRules, isValidRule } from '../../shared/services/query-parser/query-parser.service'; export function buildVariables( variableSchema: Record | undefined, @@ -13,9 +15,11 @@ export function buildVariables( ): TemplateVariables { let variableControlValue = controlValue; - if (isStringTipTapNode(variableControlValue)) { + if (isStringifiedMailyJSONContent(variableControlValue)) { try { - variableControlValue = new HydrateEmailSchemaUseCase().execute({ emailEditor: variableControlValue }); + variableControlValue = new WrapMailyInLiquidUseCase().execute({ + emailEditor: variableControlValue, + }); } catch (error) { logger?.error( { @@ -26,10 +30,24 @@ export function buildVariables( 'BuildVariables' ); } + } else if (isValidRule(variableControlValue as RulesLogic)) { + const fields = extractFieldsFromRules(variableControlValue as RulesLogic) + .filter((field) => field.startsWith('payload.') || field.startsWith('subscriber.data.')) + .map((field) => `{{${field}}}`); + + variableControlValue = { + rules: variableControlValue, + fields, + }; } const { validVariables, invalidVariables } = extractLiquidTemplateVariables(JSON.stringify(variableControlValue)); + // don't compare against schema if it's not provided + if (!variableSchema) { + return { validVariables, invalidVariables }; + } + const { validVariables: validSchemaVariables, invalidVariables: invalidSchemaVariables } = identifyUnknownVariables( variableSchema || {}, validVariables diff --git a/apps/api/src/app/workflows-v2/util/create-schema.ts b/apps/api/src/app/workflows-v2/util/create-schema.ts new file mode 100644 index 00000000000..3fa28045978 --- /dev/null +++ b/apps/api/src/app/workflows-v2/util/create-schema.ts @@ -0,0 +1,59 @@ +import { JSONSchemaDto } from '@novu/shared'; + +function determineSchemaType(value: unknown): JSONSchemaDto { + if (value === null) { + return { type: 'null' }; + } + + if (Array.isArray(value)) { + return { + type: 'array', + items: value.length > 0 ? determineSchemaType(value[0]) : { type: 'null' }, + }; + } + + switch (typeof value) { + case 'string': + return { type: 'string', default: value }; + case 'number': + return { type: 'number', default: value }; + case 'boolean': + return { type: 'boolean', default: value }; + case 'object': + return { + type: 'object', + properties: Object.entries(value).reduce( + (acc, [key, val]) => { + acc[key] = determineSchemaType(val); + + return acc; + }, + {} as { [key: string]: JSONSchemaDto } + ), + required: Object.keys(value), + }; + + default: + return { type: 'null' }; + } +} + +export function buildVariablesSchema(object: unknown) { + const schema: JSONSchemaDto = { + type: 'object', + properties: {}, + required: [], + additionalProperties: true, + }; + + if (object) { + for (const [key, value] of Object.entries(object)) { + if (schema.properties && schema.required) { + schema.properties[key] = determineSchemaType(value); + schema.required.push(key); + } + } + } + + return schema; +} diff --git a/apps/api/src/app/workflows-v2/util/tip-tap.util.ts b/apps/api/src/app/workflows-v2/util/tip-tap.util.ts deleted file mode 100644 index e577e01268a..00000000000 --- a/apps/api/src/app/workflows-v2/util/tip-tap.util.ts +++ /dev/null @@ -1,62 +0,0 @@ -import { TipTapNode } from '@novu/shared'; - -/** - * - * @param value minimal tiptap object from the client is - * { - * "type": "doc", - * "content": [ - * { - * "type": "paragraph", - * "attrs": { - * "textAlign": "left" - * }, - * "content": [ - * { - * "type": "text", - * "text": " " - * } - * ] - * } - *] - *} - */ -export function isStringTipTapNode(value: unknown): value is string { - if (typeof value !== 'string') return false; - - try { - const parsed = JSON.parse(value); - - return isObjectTipTapNode(parsed); - } catch { - return false; - } -} - -export function isObjectTipTapNode(value: unknown): value is TipTapNode { - if (!value || typeof value !== 'object') return false; - - const doc = value as TipTapNode; - if (doc.type !== 'doc' || !Array.isArray(doc.content)) return false; - - return true; -} - -function isValidTipTapContent(node: unknown): boolean { - if (!node || typeof node !== 'object') return false; - const content = node as TipTapNode; - if (typeof content.type !== 'string') return false; - if (content.attrs !== undefined && (typeof content.attrs !== 'object' || content.attrs === null)) { - return false; - } - if (content.text !== undefined && typeof content.text !== 'string') { - return false; - } - if (content.content !== undefined) { - if (!Array.isArray(content.content)) return false; - - return content.content.every((child) => isValidTipTapContent(child)); - } - - return true; -} diff --git a/apps/api/src/app/workflows-v2/util/utils.ts b/apps/api/src/app/workflows-v2/util/utils.ts index 96ada9a55cd..67dd673ec05 100644 --- a/apps/api/src/app/workflows-v2/util/utils.ts +++ b/apps/api/src/app/workflows-v2/util/utils.ts @@ -1,13 +1,10 @@ /* eslint-disable no-param-reassign */ import difference from 'lodash/difference'; -import flatMap from 'lodash/flatMap'; import isArray from 'lodash/isArray'; import isObject from 'lodash/isObject'; import reduce from 'lodash/reduce'; -import values from 'lodash/values'; - import { JSONSchemaDto } from '@novu/shared'; -import { MAILY_ITERABLE_MARK } from '@novu/application-generic'; +import { MAILY_ITERABLE_MARK } from '../../environments-v1/usecases/output-renderers/maily-to-liquid/maily.types'; export function findMissingKeys(requiredRecord: object, actualRecord: object) { const requiredKeys = collectKeys(requiredRecord); @@ -33,51 +30,6 @@ export function collectKeys(obj, prefix = '') { ); } -/** - * Recursively flattens an object's values into an array of strings. - * Handles nested objects, arrays, and converts primitive values to strings. - * - * @param obj - The object to flatten - * @returns An array of strings containing all primitive values found in the object - * - * @example - * ```typescript - * const input = { - * subject: "Hello {{name}}", - * body: "Welcome!", - * actions: { - * primary: { - * label: "Click {{here}}", - * url: "https://example.com" - * } - * }, - * data: { count: 42 } - * }; - * - * flattenObjectValues(input); - * Returns: - * [ - * "Hello {{name}}", - * "Welcome!", - * "Click {{here}}", - * "https://example.com", - * "42" - * ] - * ``` - */ -export function flattenObjectValues(obj: Record): string[] { - return flatMap(values(obj), (value) => { - if (typeof value === 'string' || typeof value === 'number' || typeof value === 'boolean') { - return String(value); - } - if (value && typeof value === 'object') { - return flattenObjectValues(value as Record); - } - - return []; - }); -} - /** * Recursively adds missing defaults for properties in a JSON schema object. * For properties without defaults, adds interpolated path as the default value. @@ -159,7 +111,7 @@ export function mockSchemaDefaults(schema: JSONSchemaDto, parentPath = 'payload' * } * } */ -export function keysToObject(paths: string[]): Record { +export function keysToObject(paths: string[], showIfVariablesPath?: string[]): Record { const result = {}; const validPaths = paths @@ -167,7 +119,7 @@ export function keysToObject(paths: string[]): Record { // remove paths that are a prefix of another path .filter((path) => !paths.some((otherPath) => otherPath !== path && otherPath.startsWith(`${path}.`))); - validPaths.filter(hasNamespace).forEach((path) => buildPathInObject(path, result)); + validPaths.filter(hasNamespace).forEach((path) => buildPathInObject(path, result, showIfVariablesPath)); return result; } @@ -176,7 +128,7 @@ function hasNamespace(path: string): boolean { return path.includes('.'); } -function buildPathInObject(path: string, result: Record): void { +function buildPathInObject(path: string, result: Record, showIfVariablesPath?: string[]): void { const parts = path.split('.'); let current = result; @@ -192,7 +144,7 @@ function buildPathInObject(path: string, result: Record): void { current = handleObjectPath(current, key); } - setFinalLeafValue(current, parts[parts.length - 1], path); + setFinalLeafValue(current, parts[parts.length - 1], path, showIfVariablesPath); } function isArrayNotation(part: string): boolean { @@ -213,9 +165,16 @@ function handleObjectPath(current: Record, key: string): Record, lastPart: string, fullPath: string): void { +function setFinalLeafValue( + current: Record, + lastPart: string, + fullPath: string, + showIfVariablesPath?: string[] +): void { if (lastPart !== '0') { - current[lastPart] = `{{${fullPath.replace('.0.', '.')}}}`; + const currentPath = fullPath.replace('.0.', '.'); + const showIfPath = showIfVariablesPath?.find((path) => path.includes(currentPath)); + current[lastPart] = showIfPath ? true : `{{${currentPath}}}`; } } diff --git a/apps/api/src/app/workflows-v2/workflow.controller.ts b/apps/api/src/app/workflows-v2/workflow.controller.ts index 4d05d99ec2f..d79b06fa931 100644 --- a/apps/api/src/app/workflows-v2/workflow.controller.ts +++ b/apps/api/src/app/workflows-v2/workflow.controller.ts @@ -165,7 +165,7 @@ export class WorkflowController { offset: Number(query.offset || '0'), limit: Number(query.limit || '50'), orderDirection: query.orderDirection ?? DirectionEnum.DESC, - orderByField: query.orderByField ?? 'createdAt', + orderBy: query.orderBy ?? 'createdAt', searchQuery: query.query, user, }) diff --git a/apps/api/src/app/workflows-v2/workflow.module.ts b/apps/api/src/app/workflows-v2/workflow.module.ts index 28a04687111..e30d8c3d1ba 100644 --- a/apps/api/src/app/workflows-v2/workflow.module.ts +++ b/apps/api/src/app/workflows-v2/workflow.module.ts @@ -15,7 +15,6 @@ import { CommunityOrganizationRepository } from '@novu/dal'; import { AuthModule } from '../auth/auth.module'; import { BridgeModule } from '../bridge'; import { ChangeModule } from '../change/change.module'; -import { HydrateEmailSchemaUseCase } from '../environments-v1/usecases/output-renderers'; import { IntegrationModule } from '../integrations/integrations.module'; import { MessageTemplateModule } from '../message-template/message-template.module'; import { SharedModule } from '../shared/shared.module'; @@ -31,7 +30,7 @@ import { } from './usecases'; import { PatchWorkflowUsecase } from './usecases/patch-workflow'; import { PatchStepUsecase } from './usecases/patch-step-data/patch-step.usecase'; -import { BuildPayloadSchema } from './usecases/build-payload-schema/build-payload-schema.usecase'; +import { ExtractVariables } from './usecases/extract-variables/extract-variables.usecase'; import { BuildStepIssuesUsecase } from './usecases/build-step-issues/build-step-issues.usecase'; import { WorkflowController } from './workflow.controller'; @@ -57,12 +56,11 @@ const DAL_REPOSITORIES = [CommunityOrganizationRepository]; GeneratePreviewUsecase, BuildWorkflowTestDataUseCase, GetWorkflowUseCase, - HydrateEmailSchemaUseCase, BuildVariableSchemaUsecase, PatchStepUsecase, PatchWorkflowUsecase, TierRestrictionsValidateUsecase, - BuildPayloadSchema, + ExtractVariables, BuildStepIssuesUsecase, ], }) diff --git a/apps/dashboard/package.json b/apps/dashboard/package.json index 52cf9811ff5..085323e88ea 100644 --- a/apps/dashboard/package.json +++ b/apps/dashboard/package.json @@ -1,7 +1,7 @@ { "name": "@novu/dashboard", "private": true, - "version": "2.1.0", + "version": "2.1.1", "type": "module", "scripts": { "start": "vite", @@ -89,8 +89,8 @@ "react-helmet-async": "^1.3.0", "react-hook-form": "7.53.2", "react-icons": "^5.3.0", - "react-resizable-panels": "^2.1.7", "react-querybuilder": "^8.0.0", + "react-resizable-panels": "^2.1.7", "react-router-dom": "6.26.2", "react-use-intercom": "^2.0.0", "sonner": "^1.7.0", diff --git a/apps/dashboard/src/api/environments.ts b/apps/dashboard/src/api/environments.ts index 2c0ab54221f..8117ef762cb 100644 --- a/apps/dashboard/src/api/environments.ts +++ b/apps/dashboard/src/api/environments.ts @@ -1,12 +1,23 @@ -import type { IApiKey, IEnvironment, ITagsResponse } from '@novu/shared'; -import { get, getV2, put } from './api.client'; +import { IApiKey, IEnvironment, ITagsResponse } from '@novu/shared'; +import { del, get, getV2, post, put } from './api.client'; export async function getEnvironments() { const { data } = await get<{ data: IEnvironment[] }>('/environments'); return data; } -// TODO: Reuse the BridgeRequestDto type +export async function updateEnvironment({ + environment, + name, + color, +}: { + environment: IEnvironment; + name: string; + color?: string; +}) { + return put<{ data: IEnvironment }>(`/environments/${environment._id}`, { body: { name, color } }); +} + export async function updateBridgeUrl({ environment, url }: { environment: IEnvironment; url?: string }) { return put(`/environments/${environment._id}`, { body: { bridge: { url } } }); } @@ -21,3 +32,13 @@ export async function getTags({ environment }: { environment: IEnvironment }): P const { data } = await getV2<{ data: ITagsResponse }>(`/environments/${environment._id}/tags`); return data; } + +export async function createEnvironment(payload: { name: string; color: string }): Promise { + const response = await post<{ data: IEnvironment }>('/environments', { body: payload }); + + return response.data; +} + +export async function deleteEnvironment({ environment }: { environment: IEnvironment }): Promise { + return del(`/environments/${environment._id}`); +} diff --git a/apps/dashboard/src/api/workflows.ts b/apps/dashboard/src/api/workflows.ts index 2c751b354ef..2f1cf4b06ee 100644 --- a/apps/dashboard/src/api/workflows.ts +++ b/apps/dashboard/src/api/workflows.ts @@ -27,16 +27,31 @@ export const getWorkflows = async ({ limit, query, offset, + orderBy, + orderDirection, }: { environment: IEnvironment; limit: number; offset: number; query: string; + orderBy?: string; + orderDirection?: string; }): Promise => { - const { data } = await getV2<{ data: ListWorkflowResponse }>( - `/workflows?limit=${limit}&offset=${offset}&query=${query}`, - { environment } - ); + const params = new URLSearchParams({ + limit: limit.toString(), + offset: offset.toString(), + query, + }); + + if (orderBy) { + params.append('orderBy', orderBy); + } + if (orderDirection) { + params.append('orderDirection', orderDirection.toUpperCase()); + } + + const { data } = await getV2<{ data: ListWorkflowResponse }>(`/workflows?${params.toString()}`, { environment }); + return data; }; diff --git a/apps/dashboard/src/components/activity/activity-table.tsx b/apps/dashboard/src/components/activity/activity-table.tsx index baf56dd9c54..c33db4e0036 100644 --- a/apps/dashboard/src/components/activity/activity-table.tsx +++ b/apps/dashboard/src/components/activity/activity-table.tsx @@ -92,7 +92,7 @@ export function ActivityTable({ Event Status Steps - Triggered Date + Triggered At diff --git a/apps/dashboard/src/components/activity/components/status-preview-card.tsx b/apps/dashboard/src/components/activity/components/status-preview-card.tsx index 5edf5dabbb0..e80721e0ba7 100644 --- a/apps/dashboard/src/components/activity/components/status-preview-card.tsx +++ b/apps/dashboard/src/components/activity/components/status-preview-card.tsx @@ -1,9 +1,9 @@ -import { format } from 'date-fns'; -import { cn } from '@/utils/ui'; import { STEP_TYPE_TO_ICON } from '@/components/icons/utils'; -import { JOB_STATUS_CONFIG } from '../constants'; -import { IActivityJob, JobStatusEnum, StepTypeEnum } from '@novu/shared'; import { STEP_TYPE_LABELS } from '@/utils/constants'; +import { cn } from '@/utils/ui'; +import { IActivityJob, JobStatusEnum, StepTypeEnum } from '@novu/shared'; +import { format } from 'date-fns'; +import { JOB_STATUS_CONFIG } from '../constants'; function getStepIcon(type?: StepTypeEnum) { const Icon = STEP_TYPE_TO_ICON[type as keyof typeof STEP_TYPE_TO_ICON]; @@ -45,9 +45,11 @@ export function StatusPreviewCard({ jobs }: StatusPreviewCardProps) { )} -
- {format(new Date(job.createdAt), 'HH:mm')} -
+ {job.createdAt && ( +
+ {format(new Date(job.createdAt), 'HH:mm')} +
+ )} ); })} diff --git a/apps/dashboard/src/components/billing/features.tsx b/apps/dashboard/src/components/billing/features.tsx index 610391e412d..1b627f3d9be 100644 --- a/apps/dashboard/src/components/billing/features.tsx +++ b/apps/dashboard/src/components/billing/features.tsx @@ -64,6 +64,14 @@ const features: Feature[] = [ [SupportedPlansEnum.ENTERPRISE]: { value: 'Unlimited' }, }, }, + { + label: 'Environments', + values: { + [SupportedPlansEnum.FREE]: { value: '2' }, + [SupportedPlansEnum.BUSINESS]: { value: '10' }, + [SupportedPlansEnum.ENTERPRISE]: { value: 'Unlimited' }, + }, + }, { label: 'Framework', isTitle: true, diff --git a/apps/dashboard/src/components/conditions-editor/conditions-editor-context.tsx b/apps/dashboard/src/components/conditions-editor/conditions-editor-context.tsx index 33c60d90ec4..8df1d3ef55f 100644 --- a/apps/dashboard/src/components/conditions-editor/conditions-editor-context.tsx +++ b/apps/dashboard/src/components/conditions-editor/conditions-editor-context.tsx @@ -5,8 +5,6 @@ import { ConditionsEditorContextType } from './types'; import { useDataRef } from '@/hooks/use-data-ref'; export const ConditionsEditorContext = createContext({ - query: { combinator: 'and', rules: [] }, - setQuery: () => {}, removeRuleOrGroup: () => {}, cloneRuleOrGroup: () => {}, getParentGroup: () => null, @@ -23,6 +21,7 @@ export function ConditionsEditorProvider({ }) { const queryRef = useDataRef(query); const queryChangeRef = useDataRef(onQueryChange); + const removeRuleOrGroup = useCallback( (path: Path) => { queryChangeRef.current(remove(queryRef.current, path)); @@ -37,11 +36,9 @@ export function ConditionsEditorProvider({ [queryChangeRef, queryRef] ); - const setQuery = useCallback((query: RuleGroupType) => queryChangeRef.current(query), [queryChangeRef]); - const getParentGroup = useCallback( (id?: string) => { - if (!id) return query; + if (!id) return queryRef.current; const findParent = (group: RuleGroupTypeAny): RuleGroupTypeAny | null => { for (const rule of group.rules) { @@ -60,14 +57,14 @@ export function ConditionsEditorProvider({ return null; }; - return findParent(query); + return findParent(queryRef.current); }, - [query] + [queryRef] ); const contextValue = useMemo( - () => ({ query, setQuery, removeRuleOrGroup, cloneRuleOrGroup, getParentGroup }), - [query, setQuery, removeRuleOrGroup, cloneRuleOrGroup, getParentGroup] + () => ({ removeRuleOrGroup, cloneRuleOrGroup, getParentGroup }), + [removeRuleOrGroup, cloneRuleOrGroup, getParentGroup] ); return {children}; diff --git a/apps/dashboard/src/components/conditions-editor/conditions-editor.tsx b/apps/dashboard/src/components/conditions-editor/conditions-editor.tsx index 45891273e96..d1b91002bf7 100644 --- a/apps/dashboard/src/components/conditions-editor/conditions-editor.tsx +++ b/apps/dashboard/src/components/conditions-editor/conditions-editor.tsx @@ -1,11 +1,9 @@ +import { useMemo } from 'react'; import { type Field, QueryBuilder, RuleGroupType } from 'react-querybuilder'; import 'react-querybuilder/dist/query-builder.css'; import { LiquidVariable } from '@/utils/parseStepVariablesToLiquidVariables'; -import { - ConditionsEditorProvider, - useConditionsEditorContext, -} from '@/components/conditions-editor/conditions-editor-context'; +import { ConditionsEditorProvider } from '@/components/conditions-editor/conditions-editor-context'; import { AddConditionAction } from '@/components/conditions-editor/add-condition-action'; import { AddGroupAction } from '@/components/conditions-editor/add-group-action'; import { OperatorSelector } from '@/components/conditions-editor/operator-selector'; @@ -20,43 +18,57 @@ const nestedGroupClassName = `[&.ruleGroup_.ruleGroup]:p-3 [&.ruleGroup_.ruleGro const ruleGroupClassName = `[&.ruleGroup]:[background:transparent] [&.ruleGroup]:[border:none] [&.ruleGroup]:p-0 ${nestedGroupClassName} [&_.ruleGroup-body_.rule]:items-start ${groupActionsClassName}`; const ruleClassName = `${ruleActionsClassName}`; -function InternalConditionsEditor({ fields, variables }: { fields: Field[]; variables: LiquidVariable[] }) { - const { query, setQuery } = useConditionsEditorContext(); +const controlClassnames = { + ruleGroup: ruleGroupClassName, + rule: ruleClassName, +}; + +const translations = { + addRule: { + label: 'Add condition', + title: 'Add condition', + }, + addGroup: { + label: 'Add group', + title: 'Add group', + }, +}; + +const controlElements = { + operatorSelector: OperatorSelector, + combinatorSelector: CombinatorSelector, + fieldSelector: FieldSelector, + valueEditor: ValueEditor, + addRuleAction: AddConditionAction, + addGroupAction: AddGroupAction, + removeGroupAction: RuleActions, + removeRuleAction: RuleActions, + cloneGroupAction: null, + cloneRuleAction: null, +}; + +function InternalConditionsEditor({ + fields, + variables, + query, + onQueryChange, +}: { + fields: Field[]; + variables: LiquidVariable[]; + query: RuleGroupType; + onQueryChange: (query: RuleGroupType) => void; +}) { + const context = useMemo(() => ({ variables }), [variables]); return ( ); } @@ -74,7 +86,7 @@ export function ConditionsEditor({ }) { return ( - + ); } diff --git a/apps/dashboard/src/components/conditions-editor/field-selector.tsx b/apps/dashboard/src/components/conditions-editor/field-selector.tsx index 1e9df5a122d..7ab493bc630 100644 --- a/apps/dashboard/src/components/conditions-editor/field-selector.tsx +++ b/apps/dashboard/src/components/conditions-editor/field-selector.tsx @@ -1,26 +1,37 @@ -import { useMemo } from 'react'; +import React, { useMemo } from 'react'; import { FieldSelectorProps } from 'react-querybuilder'; import { Code2 } from '@/components/icons/code-2'; import { VariableSelect } from '@/components/conditions-editor/variable-select'; -export const FieldSelector = ({ handleOnChange, options, ...restFieldSelectorProps }: FieldSelectorProps) => { - const optionsArray = useMemo( - () => - options.map((option) => ({ - label: option.label, - value: 'value' in option ? option.value : '', - })), - [options] - ); +export const FieldSelector = React.memo( + ({ handleOnChange, options, value, disabled }: FieldSelectorProps) => { + const optionsArray = useMemo( + () => + options.map((option) => ({ + label: option.label, + value: 'value' in option ? option.value : '', + })), + [options] + ); - return ( - } - onChange={handleOnChange} - options={optionsArray} - title="Fields" - {...restFieldSelectorProps} - /> - ); -}; + return ( + } + onChange={handleOnChange} + options={optionsArray} + title="Fields" + value={value} + disabled={disabled} + /> + ); + }, + (prevProps, nextProps) => { + return ( + prevProps.value === nextProps.value && + prevProps.disabled === nextProps.disabled && + prevProps.options === nextProps.options && + prevProps.handleOnChange === nextProps.handleOnChange + ); + } +); diff --git a/apps/dashboard/src/components/conditions-editor/operator-selector.tsx b/apps/dashboard/src/components/conditions-editor/operator-selector.tsx index c1f6caf6d55..0140efca510 100644 --- a/apps/dashboard/src/components/conditions-editor/operator-selector.tsx +++ b/apps/dashboard/src/components/conditions-editor/operator-selector.tsx @@ -1,21 +1,32 @@ +import React from 'react'; import { OperatorSelectorProps } from 'react-querybuilder'; import { Select, SelectContent, SelectTrigger, SelectValue } from '@/components/primitives/select'; import { cn } from '@/utils/ui'; import { toSelectOptions } from '@/components/conditions-editor/select-option-utils'; -export const OperatorSelector = ({ disabled, value, options, handleOnChange }: OperatorSelectorProps) => { - return ( - - ); -}; +export const OperatorSelector = React.memo( + ({ disabled, value, options, handleOnChange }: OperatorSelectorProps) => { + return ( + + ); + }, + (prevProps, nextProps) => { + return ( + prevProps.value === nextProps.value && + prevProps.disabled === nextProps.disabled && + prevProps.options === nextProps.options && + prevProps.handleOnChange === nextProps.handleOnChange + ); + } +); diff --git a/apps/dashboard/src/components/conditions-editor/rule-actions.tsx b/apps/dashboard/src/components/conditions-editor/rule-actions.tsx index 374bce58439..4a09d3a8af0 100644 --- a/apps/dashboard/src/components/conditions-editor/rule-actions.tsx +++ b/apps/dashboard/src/components/conditions-editor/rule-actions.tsx @@ -1,4 +1,4 @@ -import { useMemo } from 'react'; +import React, { useMemo } from 'react'; import { isRuleGroup, ActionWithRulesProps, getParentPath } from 'react-querybuilder'; import { RiMore2Fill } from 'react-icons/ri'; @@ -14,40 +14,45 @@ import { CompactButton } from '@/components/primitives/button-compact'; import { Delete } from '@/components/icons/delete'; import { SquareTwoStack } from '@/components/icons/square-two-stack'; -export const RuleActions = ({ path, ruleOrGroup }: ActionWithRulesProps) => { - const { removeRuleOrGroup, cloneRuleOrGroup, getParentGroup } = useConditionsEditorContext(); - const parentGroup = useMemo(() => getParentGroup(ruleOrGroup.id), [ruleOrGroup.id, getParentGroup]); - const isGroup = isRuleGroup(ruleOrGroup); - const isDuplicateDisabled = !!(parentGroup && parentGroup.rules && parentGroup.rules.length >= 10); +export const RuleActions = React.memo( + ({ path, ruleOrGroup }: ActionWithRulesProps) => { + const { removeRuleOrGroup, cloneRuleOrGroup, getParentGroup } = useConditionsEditorContext(); + const parentGroup = useMemo(() => getParentGroup(ruleOrGroup.id), [ruleOrGroup.id, getParentGroup]); + const isGroup = isRuleGroup(ruleOrGroup); + const isDuplicateDisabled = !!(parentGroup && parentGroup.rules && parentGroup.rules.length >= 10); - return ( - - - - - - - { - cloneRuleOrGroup(ruleOrGroup, getParentPath(path)); - }} - className="text-foreground-600 text-label-xs h-7" - disabled={isDuplicateDisabled} - > - Duplicate {isGroup ? `group` : `condition`} - - removeRuleOrGroup(path)} className="text-error-base text-label-xs h-7"> - - Delete {isGroup ? `group` : `condition`} - - - - - ); -}; + return ( + + + + + + + { + cloneRuleOrGroup(ruleOrGroup, getParentPath(path)); + }} + className="text-foreground-600 text-label-xs h-7" + disabled={isDuplicateDisabled} + > + Duplicate {isGroup ? `group` : `condition`} + + removeRuleOrGroup(path)} className="text-error-base text-label-xs h-7"> + + Delete {isGroup ? `group` : `condition`} + + + + + ); + }, + (prevProps, nextProps) => { + return prevProps.path === nextProps.path && prevProps.ruleOrGroup === nextProps.ruleOrGroup; + } +); diff --git a/apps/dashboard/src/components/conditions-editor/types.ts b/apps/dashboard/src/components/conditions-editor/types.ts index 0271b279b7d..6c456cfaa32 100644 --- a/apps/dashboard/src/components/conditions-editor/types.ts +++ b/apps/dashboard/src/components/conditions-editor/types.ts @@ -1,8 +1,6 @@ -import { BaseOption, RuleGroupType, RuleGroupTypeAny, RuleType, Path } from 'react-querybuilder'; +import { BaseOption, RuleGroupTypeAny, RuleType, Path } from 'react-querybuilder'; export interface ConditionsEditorContextType { - query: RuleGroupType; - setQuery: (query: RuleGroupType) => void; removeRuleOrGroup: (path: Path) => void; cloneRuleOrGroup: (ruleOrGroup: RuleGroupTypeAny | RuleType, path?: Path) => void; getParentGroup: (id?: string) => RuleGroupTypeAny | null; diff --git a/apps/dashboard/src/components/conditions-editor/value-editor.tsx b/apps/dashboard/src/components/conditions-editor/value-editor.tsx index 63e6e75aac4..7c35e7aa257 100644 --- a/apps/dashboard/src/components/conditions-editor/value-editor.tsx +++ b/apps/dashboard/src/components/conditions-editor/value-editor.tsx @@ -1,4 +1,3 @@ -import React from 'react'; import { useValueEditor, ValueEditorProps } from 'react-querybuilder'; import { useFormContext } from 'react-hook-form'; @@ -6,11 +5,11 @@ import { InputRoot, InputWrapper } from '@/components/primitives/input'; import { LiquidVariable } from '@/utils/parseStepVariablesToLiquidVariables'; import { ControlInput } from '../primitives/control-input/control-input'; -export const ValueEditor = React.memo((props: ValueEditorProps) => { +export const ValueEditor = (props: ValueEditorProps) => { const form = useFormContext(); const queryPath = 'query.rules.' + props.path.join('.rules.') + '.value'; const { error } = form.getFieldState(queryPath, form.formState); - const { variables } = props.context as { variables: LiquidVariable[] }; + const { variables = [] } = (props.context as { variables: LiquidVariable[] }) ?? {}; const { value, handleOnChange, operator, type } = props; const { valueAsArray, multiValueHandler } = useValueEditor(props); @@ -65,4 +64,4 @@ export const ValueEditor = React.memo((props: ValueEditorProps) => { {error && {error?.message}} ); -}); +}; diff --git a/apps/dashboard/src/components/conditions-editor/variable-select.tsx b/apps/dashboard/src/components/conditions-editor/variable-select.tsx index 0e89589609a..7a3db2ec68e 100644 --- a/apps/dashboard/src/components/conditions-editor/variable-select.tsx +++ b/apps/dashboard/src/components/conditions-editor/variable-select.tsx @@ -44,7 +44,7 @@ const VariablesList = React.forwardRef( {options.map((option, index) => (
  • (env as any).color).filter(Boolean)); + const availableColors = ENVIRONMENT_COLORS.filter((color) => !usedColors.has(color)); + + // If all colors are used, fall back to the original list + const colorPool = availableColors.length > 0 ? availableColors : ENVIRONMENT_COLORS; + + return colorPool[Math.floor(Math.random() * colorPool.length)]; +} + +const createEnvironmentSchema = z.object({ + name: z.string().min(1, 'Name is required'), + color: z.string(), +}); + +type CreateEnvironmentFormData = z.infer; + +type CreateEnvironmentButtonProps = ComponentProps; + +export const CreateEnvironmentButton = (props: CreateEnvironmentButtonProps) => { + const { currentOrganization } = useAuth(); + const { environments = [] } = useFetchEnvironments({ organizationId: currentOrganization?._id }); + const [isOpen, setIsOpen] = useState(false); + const { mutateAsync, isPending } = useCreateEnvironment(); + const { subscription } = useFetchSubscription(); + const navigate = useNavigate(); + const track = useTelemetry(); + + const isPaidTier = + subscription?.apiServiceLevel === ApiServiceLevelEnum.BUSINESS || + subscription?.apiServiceLevel === ApiServiceLevelEnum.ENTERPRISE; + const isTrialActive = subscription?.trial?.isActive; + const canCreateEnvironment = isPaidTier && !isTrialActive; + + const form = useForm({ + resolver: zodResolver(createEnvironmentSchema), + defaultValues: { + name: '', + color: getRandomColor(environments), + }, + }); + + const onSubmit = async (values: CreateEnvironmentFormData) => { + try { + await mutateAsync({ + name: values.name, + color: values.color, + }); + + setIsOpen(false); + + form.reset({ + name: '', + color: getRandomColor(environments), + }); + + showSuccessToast('Environment created successfully'); + } catch (e: any) { + const message = e?.response?.data?.message || e?.message || 'Failed to create environment'; + showErrorToast(Array.isArray(message) ? message[0] : message); + } + }; + + const handleClick = () => { + track(TelemetryEvent.CREATE_ENVIRONMENT_CLICK, { + createAllowed: !!canCreateEnvironment, + }); + + if (!canCreateEnvironment) { + navigate(ROUTES.SETTINGS_BILLING); + return; + } + + setIsOpen(true); + }; + + const getTooltipContent = () => { + if (!canCreateEnvironment) { + return 'Upgrade to Business plan to create custom environments'; + } + + return ''; + }; + + const button = ( + + ); + + return ( + + {canCreateEnvironment ? ( + button + ) : ( + + {button} + {getTooltipContent()} + + )} + + {canCreateEnvironment && ( + e.preventDefault()}> + + Create environment +
    + + Create a new environment to manage your notifications.{' '} + Learn more + +
    +
    + + +
    + + ( + + Name + + { + field.onChange(e); + }} + /> + + + + )} + /> + ( + + Color + + + + Will be used to identify the environment in the UI. + + )} + /> + + +
    + + + + +
    + )} +
    + ); +}; diff --git a/apps/dashboard/src/components/delete-environment-dialog.tsx b/apps/dashboard/src/components/delete-environment-dialog.tsx new file mode 100644 index 00000000000..617221a84de --- /dev/null +++ b/apps/dashboard/src/components/delete-environment-dialog.tsx @@ -0,0 +1,53 @@ +import { Button } from '@/components/primitives/button'; +import { + Dialog, + DialogContent, + DialogDescription, + DialogFooter, + DialogHeader, + DialogTitle, +} from '@/components/primitives/dialog'; +import { IEnvironment } from '@novu/shared'; + +interface DeleteEnvironmentDialogProps { + environment?: IEnvironment; + open: boolean; + onOpenChange: (open: boolean) => void; + onConfirm: () => void; + isLoading?: boolean; +} + +export const DeleteEnvironmentDialog = ({ + environment, + open, + onOpenChange, + onConfirm, + isLoading, +}: DeleteEnvironmentDialogProps) => { + if (!environment) { + return null; + } + + return ( + + + + Delete Environment + + Deleting {environment.name} will permanently remove this environment and + all the data associated with it. Including integrations, workflows, and notifications. This action cannot be + undone. Are you sure you want to proceed? + + + + + + + + + ); +}; diff --git a/apps/dashboard/src/components/edit-environment-sheet.tsx b/apps/dashboard/src/components/edit-environment-sheet.tsx new file mode 100644 index 00000000000..0b2ce590e37 --- /dev/null +++ b/apps/dashboard/src/components/edit-environment-sheet.tsx @@ -0,0 +1,156 @@ +import { Button } from '@/components/primitives/button'; +import { + Form, + FormControl, + FormField, + FormInput, + FormItem, + FormLabel, + FormMessage, +} from '@/components/primitives/form/form'; +import { Separator } from '@/components/primitives/separator'; +import { + Sheet, + SheetContent, + SheetDescription, + SheetFooter, + SheetHeader, + SheetMain, + SheetTitle, +} from '@/components/primitives/sheet'; +import { ExternalLink } from '@/components/shared/external-link'; +import { useUpdateEnvironment } from '@/hooks/use-environments'; +import { zodResolver } from '@hookform/resolvers/zod'; +import { IEnvironment } from '@novu/shared'; +import { useEffect } from 'react'; +import { useForm } from 'react-hook-form'; +import { RiArrowRightSLine } from 'react-icons/ri'; +import { z } from 'zod'; +import { ColorPicker } from './primitives/color-picker'; +import { showErrorToast, showSuccessToast } from './primitives/sonner-helpers'; + +const editEnvironmentSchema = z.object({ + name: z.string().min(1, 'Name is required'), + color: z.string().min(1, 'Color is required'), +}); + +type EditEnvironmentFormData = z.infer; + +interface EditEnvironmentSheetProps { + environment?: IEnvironment; + isOpen: boolean; + onOpenChange: (open: boolean) => void; +} + +export const EditEnvironmentSheet = ({ environment, isOpen, onOpenChange }: EditEnvironmentSheetProps) => { + const { mutateAsync: updateEnvironment, isPending } = useUpdateEnvironment(); + + const form = useForm({ + resolver: zodResolver(editEnvironmentSchema), + defaultValues: { + name: environment?.name || '', + color: environment?.color, + }, + }); + + useEffect(() => { + if (environment) { + form.reset({ + name: environment.name, + color: environment.color, + }); + } + }, [environment, form]); + + const onSubmit = async (values: EditEnvironmentFormData) => { + if (!environment) return; + + try { + await updateEnvironment({ + environment, + name: values.name, + color: values.color, + }); + onOpenChange(false); + form.reset(); + showSuccessToast('Environment updated successfully'); + } catch (e: any) { + const message = e?.response?.data?.message || e?.message || 'Failed to update environment'; + showErrorToast(Array.isArray(message) ? message[0] : message); + } + }; + + return ( + + e.preventDefault()}> + + Edit environment +
    + + Update your environment settings.{' '} + Learn more + +
    +
    + + +
    + + ( + + Name + + { + field.onChange(e); + }} + /> + + + + )} + /> + ( + + Color + + + + + + )} + /> + + +
    + + + + +
    +
    + ); +}; diff --git a/apps/dashboard/src/components/environments-list.tsx b/apps/dashboard/src/components/environments-list.tsx new file mode 100644 index 00000000000..ae55b1257c0 --- /dev/null +++ b/apps/dashboard/src/components/environments-list.tsx @@ -0,0 +1,179 @@ +import { useAuth } from '@/context/auth/hooks'; +import { useEnvironment, useFetchEnvironments } from '@/context/environment/hooks'; +import { useDeleteEnvironment } from '@/hooks/use-environments'; +import { cn } from '@/utils/ui'; +import { EnvironmentEnum, IEnvironment, PROTECTED_ENVIRONMENTS } from '@novu/shared'; +import { useState } from 'react'; +import { RiDeleteBin2Line, RiMore2Fill } from 'react-icons/ri'; +import { DeleteEnvironmentDialog } from './delete-environment-dialog'; +import { EditEnvironmentSheet } from './edit-environment-sheet'; +import { Badge } from './primitives/badge'; +import { CompactButton } from './primitives/button-compact'; +import { CopyButton } from './primitives/copy-button'; +import { + DropdownMenu, + DropdownMenuContent, + DropdownMenuGroup, + DropdownMenuItem, + DropdownMenuSeparator, + DropdownMenuTrigger, +} from './primitives/dropdown-menu'; +import { EnvironmentBranchIcon } from './primitives/environment-branch-icon'; +import { Skeleton } from './primitives/skeleton'; +import { showErrorToast, showSuccessToast } from './primitives/sonner-helpers'; +import { Table, TableBody, TableCell, TableHead, TableHeader, TableRow } from './primitives/table'; +import { TimeDisplayHoverCard } from './time-display-hover-card'; +import TruncatedText from './truncated-text'; + +const EnvironmentRowSkeleton = () => ( + + +
    + + +
    +
    + + + + + + + + + +
    +); + +export function EnvironmentsList() { + const { currentOrganization } = useAuth(); + const { environments = [], areEnvironmentsInitialLoading } = useFetchEnvironments({ + organizationId: currentOrganization?._id, + }); + const { currentEnvironment } = useEnvironment(); + const [editEnvironment, setEditEnvironment] = useState(); + const [deleteEnvironment, setDeleteEnvironment] = useState(); + const { mutateAsync: deleteEnvironmentAction, isPending: isDeletePending } = useDeleteEnvironment(); + + const onDeleteEnvironment = async () => { + if (!deleteEnvironment) return; + + try { + await deleteEnvironmentAction({ environment: deleteEnvironment }); + showSuccessToast('Environment deleted successfully'); + + setDeleteEnvironment(undefined); + } catch (e: any) { + const message = e?.response?.data?.message || e?.message || 'Failed to delete environment'; + showErrorToast(Array.isArray(message) ? message[0] : message); + } + }; + + const handleDeleteClick = (environment: IEnvironment) => { + setDeleteEnvironment(environment); + }; + + return ( + <> + + + + Name + Identifier + Last Updated + + + + + {areEnvironmentsInitialLoading + ? Array.from({ length: 3 }).map((_, i) => ) + : environments.map((environment) => ( + + +
    + +
    + {environment.name} + {environment._id === currentEnvironment?._id && ( + + Current + + )} +
    +
    +
    + +
    + + {environment.identifier} + + +
    +
    + + + {new Date(environment.updatedAt).toLocaleDateString('en-US', { + year: 'numeric', + month: 'short', + day: 'numeric', + })} + + + + {!PROTECTED_ENVIRONMENTS.includes(environment.name as EnvironmentEnum) && ( + + + + + + + setEditEnvironment(environment)}> + Edit environment + + <> + + handleDeleteClick(environment)} + disabled={ + environment._id === currentEnvironment?._id || + PROTECTED_ENVIRONMENTS.includes(environment.name as EnvironmentEnum) + } + > + + Delete environment + + + + + + )} + +
    + ))} +
    +
    + !open && setEditEnvironment(undefined)} + /> + !open && setDeleteEnvironment(undefined)} + onConfirm={onDeleteEnvironment} + isLoading={isDeletePending} + /> + + ); +} diff --git a/apps/dashboard/src/components/header-navigation/customer-support-button.tsx b/apps/dashboard/src/components/header-navigation/customer-support-button.tsx index 4ff90b45d3c..e21d231577b 100644 --- a/apps/dashboard/src/components/header-navigation/customer-support-button.tsx +++ b/apps/dashboard/src/components/header-navigation/customer-support-button.tsx @@ -1,9 +1,11 @@ import { RiQuestionFill } from 'react-icons/ri'; import { HeaderButton } from './header-button'; import { usePlainChat } from '@/hooks/use-plain-chat'; +import { useBootIntercom } from '@/hooks/use-boot-intercom'; export const CustomerSupportButton = () => { const { showPlainLiveChat } = usePlainChat(); + useBootIntercom(); return ( + )} + +); + const WorkflowListEmptyProd = ({ switchToDev }: { switchToDev: () => void }) => (
    @@ -42,29 +70,39 @@ const WorkflowListEmptyProd = ({ switchToDev }: { switchToDev: () => void }) =>
    ); -const WorkflowListEmptyDev = () => ( -
    - -
    - Create your first workflow to send notifications -

    - Workflows handle notifications across multiple channels in a single, version-controlled flow, with the ability - to manage preference for each subscriber. -

    -
    +const WorkflowListEmptyDev = () => { + const navigate = useNavigate(); + const { environmentSlug } = useParams(); -
    - - - View docs - - + return ( +
    + +
    + Create your first workflow to send notifications +

    + Workflows handle notifications across multiple channels in a single, version-controlled flow, with the ability + to manage preference for each subscriber. +

    +
    + +
    + + + View docs + + - - - +
    -
    -); + ); +}; diff --git a/apps/dashboard/src/components/workflow-list.tsx b/apps/dashboard/src/components/workflow-list.tsx index 04700bf0f63..665a876a656 100644 --- a/apps/dashboard/src/components/workflow-list.tsx +++ b/apps/dashboard/src/components/workflow-list.tsx @@ -11,13 +11,35 @@ import { } from '@/components/primitives/table'; import { WorkflowListEmpty } from '@/components/workflow-list-empty'; import { WorkflowRow } from '@/components/workflow-row'; -import { useFetchWorkflows } from '@/hooks/use-fetch-workflows'; +import { ListWorkflowResponse } from '@novu/shared'; import { RiMore2Fill } from 'react-icons/ri'; import { createSearchParams, useLocation, useSearchParams } from 'react-router-dom'; import { ServerErrorPage } from './shared/server-error-page'; -export function WorkflowList() { - const [searchParams] = useSearchParams(); +export type SortableColumn = 'name' | 'updatedAt'; + +interface WorkflowListProps { + data?: ListWorkflowResponse; + isLoading?: boolean; + isError?: boolean; + limit?: number; + orderBy?: SortableColumn; + orderDirection?: 'asc' | 'desc'; + hasActiveFilters?: boolean; + onClearFilters?: () => void; +} + +export function WorkflowList({ + data, + isLoading, + isError, + limit = 12, + orderBy, + orderDirection, + hasActiveFilters, + onClearFilters, +}: WorkflowListProps) { + const [searchParams, setSearchParams] = useSearchParams(); const location = useLocation(); const hrefFromOffset = (offset: number) => { @@ -28,34 +50,50 @@ export function WorkflowList() { }; const offset = parseInt(searchParams.get('offset') || '0'); - const limit = parseInt(searchParams.get('limit') || '12'); - const { data, isPending, isError, currentPage, totalPages } = useFetchWorkflows({ - limit, - offset, - }); + const toggleSort = (column: SortableColumn) => { + const newDirection = column === orderBy ? (orderDirection === 'desc' ? 'asc' : 'desc') : 'desc'; + searchParams.set('orderDirection', newDirection); + searchParams.set('orderBy', column); + setSearchParams(searchParams); + }; if (isError) return ; - if (!isPending && data.totalCount === 0) { - return ; + if (!isLoading && data?.totalCount === 0) { + return ; } + const currentPage = Math.floor(offset / limit) + 1; + const totalPages = Math.ceil((data?.totalCount || 0) / limit); + return ( -
    +
    - Workflows + toggleSort('name')} + > + Workflows + Status Steps Tags - Last updated + toggleSort('updatedAt')} + > + Last updated + - {isPending ? ( + {isLoading ? ( <> {new Array(limit).fill(0).map((_, index) => ( @@ -82,11 +120,7 @@ export function WorkflowList() { ))} ) : ( - <> - {data.workflows.map((workflow) => ( - - ))} - + <>{data?.workflows.map((workflow) => )} )} {data && limit < data.totalCount && ( diff --git a/apps/dashboard/src/components/workflow-row.tsx b/apps/dashboard/src/components/workflow-row.tsx index 6d09f6f786d..b18c37a013d 100644 --- a/apps/dashboard/src/components/workflow-row.tsx +++ b/apps/dashboard/src/components/workflow-row.tsx @@ -5,7 +5,11 @@ import { DropdownMenuContent, DropdownMenuGroup, DropdownMenuItem, + DropdownMenuPortal, DropdownMenuSeparator, + DropdownMenuSub, + DropdownMenuSubContent, + DropdownMenuSubTrigger, DropdownMenuTrigger, } from '@/components/primitives/dropdown-menu'; import { TableCell, TableRow } from '@/components/primitives/table'; @@ -14,7 +18,9 @@ import TruncatedText from '@/components/truncated-text'; import { WorkflowStatus } from '@/components/workflow-status'; import { WorkflowSteps } from '@/components/workflow-steps'; import { WorkflowTags } from '@/components/workflow-tags'; -import { useEnvironment } from '@/context/environment/hooks'; +import { LEGACY_DASHBOARD_URL } from '@/config'; +import { useAuth } from '@/context/auth/hooks'; +import { useEnvironment, useFetchEnvironments } from '@/context/environment/hooks'; import { useDeleteWorkflow } from '@/hooks/use-delete-workflow'; import { usePatchWorkflow } from '@/hooks/use-patch-workflow'; import { useSyncWorkflow } from '@/hooks/use-sync-workflow'; @@ -42,7 +48,6 @@ import { CopyButton } from './primitives/copy-button'; import { ToastIcon } from './primitives/sonner'; import { showToast } from './primitives/sonner-helpers'; import { TimeDisplayHoverCard } from './time-display-hover-card'; -import { LEGACY_DASHBOARD_URL } from '@/config'; type WorkflowRowProps = { workflow: WorkflowListResponseDto; @@ -126,12 +131,6 @@ export const WorkflowRow = ({ workflow }: WorkflowRowProps) => { }, }); - const onDeleteWorkflow = async () => { - await deleteWorkflow({ - workflowSlug: workflow.slug, - }); - }; - const { patchWorkflow, isPending: isPauseWorkflowPending } = usePatchWorkflow({ onSuccess: (data) => { showToast({ @@ -162,6 +161,12 @@ export const WorkflowRow = ({ workflow }: WorkflowRowProps) => { }, }); + const onDeleteWorkflow = async () => { + await deleteWorkflow({ + workflowSlug: workflow.slug, + }); + }; + const onPauseWorkflow = async () => { await patchWorkflow({ workflowSlug: workflow.slug, @@ -320,30 +325,52 @@ const SyncWorkflowMenuItem = ({ currentEnvironment: IEnvironment | undefined; isSyncable: boolean; tooltipContent: string | undefined; - onSync: () => void; + onSync: (targetEnvironmentId: string) => void; }) => { - const syncToLabel = `Sync to ${currentEnvironment?.name === 'Production' ? 'Development' : 'Production'}`; + const { currentOrganization } = useAuth(); + const { environments = [] } = useFetchEnvironments({ organizationId: currentOrganization?._id }); + const otherEnvironments = environments.filter((env: IEnvironment) => env._id !== currentEnvironment?._id); - if (isSyncable) { + if (!isSyncable) { return ( - + + + + + Sync workflow + + + + {tooltipContent} + + + ); + } + + if (otherEnvironments.length === 1) { + return ( + onSync(otherEnvironments[0]._id)}> - {syncToLabel} + {`Sync to ${otherEnvironments[0].name}`} ); } return ( - - - - - {syncToLabel} - - - - {tooltipContent} - - + + + + Sync workflow + + + + {otherEnvironments.map((env) => ( + onSync(env._id)}> + {env.name} + + ))} + + + ); }; diff --git a/apps/dashboard/src/hooks/use-environments.ts b/apps/dashboard/src/hooks/use-environments.ts new file mode 100644 index 00000000000..9c9e2802d4c --- /dev/null +++ b/apps/dashboard/src/hooks/use-environments.ts @@ -0,0 +1,37 @@ +import { createEnvironment, deleteEnvironment, updateEnvironment } from '@/api/environments'; +import { QueryKeys } from '@/utils/query-keys'; +import { IEnvironment } from '@novu/shared'; +import { useMutation, useQueryClient } from '@tanstack/react-query'; + +export function useCreateEnvironment() { + const queryClient = useQueryClient(); + + return useMutation({ + mutationFn: createEnvironment, + onSuccess: () => { + queryClient.invalidateQueries({ queryKey: [QueryKeys.myEnvironments] }); + }, + }); +} + +export function useUpdateEnvironment() { + const queryClient = useQueryClient(); + + return useMutation({ + mutationFn: updateEnvironment, + onSuccess: () => { + queryClient.invalidateQueries({ queryKey: [QueryKeys.myEnvironments] }); + }, + }); +} + +export function useDeleteEnvironment() { + const queryClient = useQueryClient(); + + return useMutation({ + mutationFn: ({ environment }: { environment: IEnvironment }) => deleteEnvironment({ environment }), + onSuccess: () => { + queryClient.invalidateQueries({ queryKey: [QueryKeys.myEnvironments] }); + }, + }); +} diff --git a/apps/dashboard/src/hooks/use-fetch-workflows.ts b/apps/dashboard/src/hooks/use-fetch-workflows.ts index a28fc3bad1b..a8082124317 100644 --- a/apps/dashboard/src/hooks/use-fetch-workflows.ts +++ b/apps/dashboard/src/hooks/use-fetch-workflows.ts @@ -1,20 +1,28 @@ -import { keepPreviousData, useQuery } from '@tanstack/react-query'; import { getWorkflows } from '@/api/workflows'; import { QueryKeys } from '@/utils/query-keys'; +import { keepPreviousData, useQuery } from '@tanstack/react-query'; import { useEnvironment } from '../context/environment/hooks'; interface UseWorkflowsParams { limit?: number; offset?: number; query?: string; + orderBy?: string; + orderDirection?: 'asc' | 'desc'; } -export function useFetchWorkflows({ limit = 12, offset = 0, query = '' }: UseWorkflowsParams = {}) { +export function useFetchWorkflows({ + limit = 12, + offset = 0, + query = '', + orderBy = '', + orderDirection = 'desc', +}: UseWorkflowsParams = {}) { const { currentEnvironment } = useEnvironment(); const workflowsQuery = useQuery({ - queryKey: [QueryKeys.fetchWorkflows, currentEnvironment?._id, { limit, offset, query }], - queryFn: () => getWorkflows({ environment: currentEnvironment!, limit, offset, query }), + queryKey: [QueryKeys.fetchWorkflows, currentEnvironment?._id, { limit, offset, query, orderBy, orderDirection }], + queryFn: () => getWorkflows({ environment: currentEnvironment!, limit, offset, query, orderBy, orderDirection }), placeholderData: keepPreviousData, enabled: !!currentEnvironment?._id, refetchOnWindowFocus: true, diff --git a/apps/dashboard/src/hooks/use-plain-chat.ts b/apps/dashboard/src/hooks/use-plain-chat.ts index e32dd8d2d80..dcec633911c 100644 --- a/apps/dashboard/src/hooks/use-plain-chat.ts +++ b/apps/dashboard/src/hooks/use-plain-chat.ts @@ -2,7 +2,6 @@ import { useEffect, useState } from 'react'; import { PLAIN_SUPPORT_CHAT_APP_ID } from '@/config'; import * as Sentry from '@sentry/react'; import { useAuth } from '@/context/auth/hooks'; -import { useBootIntercom } from './use-boot-intercom'; // Add type declaration for Plain chat widget declare global { @@ -20,8 +19,6 @@ export const usePlainChat = () => { const isLiveChatVisible = currentUser?.servicesHashes?.plain && PLAIN_SUPPORT_CHAT_APP_ID !== undefined; - useBootIntercom(); - useEffect(() => { if (isFirstRender && isLiveChatVisible) { try { @@ -31,6 +28,7 @@ export const usePlainChat = () => { hideBranding: true, title: 'Chat with us', customerDetails: { + fullName: `${currentUser.firstName} ${currentUser.lastName}`, email: currentUser?.email, emailHash: currentUser?.servicesHashes?.plain, externalId: currentUser?._id, diff --git a/apps/dashboard/src/hooks/use-sync-workflow.tsx b/apps/dashboard/src/hooks/use-sync-workflow.tsx index cce12d5142a..cb4b6e445ec 100644 --- a/apps/dashboard/src/hooks/use-sync-workflow.tsx +++ b/apps/dashboard/src/hooks/use-sync-workflow.tsx @@ -5,19 +5,23 @@ import { ToastIcon } from '@/components/primitives/sonner'; import { showToast } from '@/components/primitives/sonner-helpers'; import { SuccessButtonToast } from '@/components/success-button-toast'; import TruncatedText from '@/components/truncated-text'; -import { useEnvironment } from '@/context/environment/hooks'; +import { useAuth } from '@/context/auth/hooks'; +import { useEnvironment, useFetchEnvironments } from '@/context/environment/hooks'; +import { buildRoute, ROUTES } from '@/utils/routes'; import type { IEnvironment, WorkflowListResponseDto, WorkflowResponseDto } from '@novu/shared'; import { WorkflowOriginEnum, WorkflowStatusEnum } from '@novu/shared'; import { useMutation } from '@tanstack/react-query'; import { useMemo, useState } from 'react'; -import { toast } from 'sonner'; import { useNavigate } from 'react-router-dom'; -import { buildRoute, ROUTES } from '@/utils/routes'; +import { toast } from 'sonner'; export function useSyncWorkflow(workflow: WorkflowResponseDto | WorkflowListResponseDto) { - const { currentEnvironment, oppositeEnvironment, switchEnvironment } = useEnvironment(); + const { currentEnvironment } = useEnvironment(); + const { currentOrganization } = useAuth(); + const { environments = [] } = useFetchEnvironments({ organizationId: currentOrganization?._id }); const [isLoading, setIsLoading] = useState(false); const [showConfirmModal, setShowConfirmModal] = useState(false); + const [targetEnvironmentId, setTargetEnvironmentId] = useState(); const navigate = useNavigate(); let loadingToast: string | number | undefined = undefined; @@ -27,31 +31,31 @@ export function useSyncWorkflow(workflow: WorkflowResponseDto | WorkflowListResp [workflow.origin, workflow.status] ); - // TODO: Move UI logic outside of a hook to the Sync related UI component const getTooltipContent = () => { if (workflow.origin === WorkflowOriginEnum.EXTERNAL) { - return `Code-first workflows cannot be synced to ${oppositeEnvironment?.name} using dashboard.`; + return 'Code-first workflows cannot be synced using dashboard.'; } if (workflow.origin === WorkflowOriginEnum.NOVU_CLOUD_V1) { - return `V1 workflows cannot be synced to ${oppositeEnvironment?.name} using dashboard. Please visit the legacy portal.`; + return 'V1 workflows cannot be synced using dashboard. Please visit the legacy portal.'; } if (workflow.status === WorkflowStatusEnum.ERROR) { - return `This workflow has errors and cannot be synced to ${oppositeEnvironment?.name}. Please fix the errors first.`; + return 'This workflow has errors and cannot be synced. Please fix the errors first.'; } }; - const safeSync = async () => { + const safeSync = async (envId: string) => { try { - if (await isWorkflowInTargetEnvironment()) { + setTargetEnvironmentId(envId); + if (await isWorkflowInTargetEnvironment(envId)) { setShowConfirmModal(true); return; } } catch (error) { if (error instanceof NovuApiError && error.status === 404) { - await syncWorkflowMutation(); + await syncWorkflowMutation(envId); return; } @@ -66,7 +70,6 @@ export function useSyncWorkflow(workflow: WorkflowResponseDto | WorkflowListResp toast.dismiss(loadingToast); setIsLoading(false); - // TODO: Move UI logic outside of a hook to the Sync related UI component return showToast({ variant: 'lg', className: 'gap-3', @@ -83,7 +86,6 @@ export function useSyncWorkflow(workflow: WorkflowResponseDto | WorkflowListResp onAction={() => { close(); const targetSlug = environment?.slug || ''; - switchEnvironment(targetSlug); navigate(buildRoute(ROUTES.WORKFLOWS, { environmentSlug: targetSlug })); }} @@ -106,19 +108,23 @@ export function useSyncWorkflow(workflow: WorkflowResponseDto | WorkflowListResp }; const { mutateAsync: syncWorkflowMutation, isPending } = useMutation({ - mutationFn: async () => - syncWorkflow({ + mutationFn: async (targetEnvId: string) => { + const targetEnvironment = environments.find((env) => env._id === targetEnvId); + + return syncWorkflow({ environment: currentEnvironment!, workflowSlug: workflow.slug, - payload: { targetEnvironmentId: oppositeEnvironment?._id || '' }, - }).then((res) => ({ workflow: res.data, environment: oppositeEnvironment || undefined })), - onMutate: async () => { + payload: { targetEnvironmentId: targetEnvId }, + }).then((res) => ({ workflow: res.data, environment: targetEnvironment })); + }, + onMutate: async (targetEnvId) => { + const targetEnvironment = environments.find((env) => env._id === targetEnvId); setIsLoading(true); loadingToast = toast.loading( <> - Syncing workflow {workflow.name} to {oppositeEnvironment?.name}... + Syncing workflow {workflow.name} to {targetEnvironment?.name}... ); @@ -128,9 +134,9 @@ export function useSyncWorkflow(workflow: WorkflowResponseDto | WorkflowListResp }); const { mutateAsync: isWorkflowInTargetEnvironment } = useMutation({ - mutationFn: async () => { + mutationFn: async (targetEnvId: string) => { const { data } = await getV2<{ data: WorkflowResponseDto }>( - `/workflows/${workflow.workflowId}?environmentId=${oppositeEnvironment?._id || ''}`, + `/workflows/${workflow.workflowId}?environmentId=${targetEnvId}`, { environment: currentEnvironment! } ); return data; @@ -143,26 +149,32 @@ export function useSyncWorkflow(workflow: WorkflowResponseDto | WorkflowListResp isSyncable, isLoading, tooltipContent: getTooltipContent(), - PromoteConfirmModal: () => ( - { - syncWorkflowMutation(); - setShowConfirmModal(false); - }} - title={`Sync workflow to ${oppositeEnvironment?.name}`} - description={ - <> - Workflow {workflow.name} already exists in{' '} - {oppositeEnvironment?.name}.
    -
    - Proceeding will overwrite the existing workflow. - - } - confirmButtonText="Proceed" - isLoading={isPending} - /> - ), + PromoteConfirmModal: () => { + const targetEnvironment = environments.find((env) => env._id === targetEnvironmentId); + + return ( + { + if (targetEnvironmentId) { + syncWorkflowMutation(targetEnvironmentId); + setShowConfirmModal(false); + } + }} + title={`Sync workflow to ${targetEnvironment?.name}`} + description={ + <> + Workflow {workflow.name} already exists + in {targetEnvironment?.name}.
    +
    + Proceeding will overwrite the existing workflow. + + } + confirmButtonText="Proceed" + isLoading={isPending} + /> + ); + }, }; } diff --git a/apps/dashboard/src/main.tsx b/apps/dashboard/src/main.tsx index 38cd5173ed2..2035ac91b69 100644 --- a/apps/dashboard/src/main.tsx +++ b/apps/dashboard/src/main.tsx @@ -1,40 +1,44 @@ -import { StrictMode } from 'react'; -import { createBrowserRouter, RouterProvider } from 'react-router-dom'; -import { createRoot } from 'react-dom/client'; import ErrorPage from '@/components/error-page'; -import { RootRoute, AuthRoute, DashboardRoute, CatchAllRoute } from './routes'; -import { OnboardingParentRoute } from './routes/onboarding'; +import { ConfigureWorkflow } from '@/components/workflow-editor/configure-workflow'; +import { EditStepConditions } from '@/components/workflow-editor/steps/conditions/edit-step-conditions'; +import { ConfigureStep } from '@/components/workflow-editor/steps/configure-step'; +import { ConfigureStepTemplate } from '@/components/workflow-editor/steps/configure-step-template'; import { - WorkflowsPage, - SignInPage, - SignUpPage, + ActivityFeed, + ApiKeysPage, + CreateWorkflowPage, + IntegrationsListPage, OrganizationListPage, QuestionnairePage, + SettingsPage, + SignInPage, + SignUpPage, + TemplateModal, UsecaseSelectPage, - ApiKeysPage, WelcomePage, - IntegrationsListPage, - SettingsPage, - ActivityFeed, + WorkflowsPage, } from '@/pages'; + +import { StrictMode } from 'react'; +import { createRoot } from 'react-dom/client'; +import { createBrowserRouter, RouterProvider } from 'react-router-dom'; +import { CreateIntegrationSidebar } from './components/integrations/components/create-integration-sidebar'; +import { UpdateIntegrationSidebar } from './components/integrations/components/update-integration-sidebar'; +import { ChannelPreferences } from './components/workflow-editor/channel-preferences'; +import { FeatureFlagsProvider } from './context/feature-flags-provider'; import './index.css'; -import { ROUTES } from './utils/routes'; import { EditWorkflowPage } from './pages/edit-workflow'; -import { TestWorkflowPage } from './pages/test-workflow'; -import { initializeSentry } from './utils/sentry'; -import { overrideZodErrorMap } from './utils/validation'; -import { InboxUsecasePage } from './pages/inbox-usecase-page'; +import { EnvironmentsPage } from './pages/environments'; import { InboxEmbedPage } from './pages/inbox-embed-page'; -import { ConfigureWorkflow } from '@/components/workflow-editor/configure-workflow'; import { InboxEmbedSuccessPage } from './pages/inbox-embed-success-page'; -import { ChannelPreferences } from './components/workflow-editor/channel-preferences'; -import { FeatureFlagsProvider } from './context/feature-flags-provider'; -import { ConfigureStep } from '@/components/workflow-editor/steps/configure-step'; -import { ConfigureStepTemplate } from '@/components/workflow-editor/steps/configure-step-template'; +import { InboxUsecasePage } from './pages/inbox-usecase-page'; import { RedirectToLegacyStudioAuth } from './pages/redirect-to-legacy-studio-auth'; -import { CreateIntegrationSidebar } from './components/integrations/components/create-integration-sidebar'; -import { UpdateIntegrationSidebar } from './components/integrations/components/update-integration-sidebar'; -import { EditStepConditions } from '@/components/workflow-editor/steps/conditions/edit-step-conditions'; +import { TestWorkflowPage } from './pages/test-workflow'; +import { AuthRoute, CatchAllRoute, DashboardRoute, RootRoute } from './routes'; +import { OnboardingParentRoute } from './routes/onboarding'; +import { ROUTES } from './utils/routes'; +import { initializeSentry } from './utils/sentry'; +import { overrideZodErrorMap } from './utils/validation'; initializeSentry(); overrideZodErrorMap(); @@ -101,11 +105,29 @@ const router = createBrowserRouter([ { path: ROUTES.WORKFLOWS, element: , + children: [ + { + path: ROUTES.TEMPLATE_STORE, + element: , + }, + { + path: ROUTES.TEMPLATE_STORE_CREATE_WORKFLOW, + element: , + }, + { + path: ROUTES.WORKFLOWS_CREATE, + element: , + }, + ], }, { path: ROUTES.API_KEYS, element: , }, + { + path: ROUTES.ENVIRONMENTS, + element: , + }, { path: ROUTES.ACTIVITY_FEED, element: , diff --git a/apps/dashboard/src/components/create-workflow-button.tsx b/apps/dashboard/src/pages/create-workflow.tsx similarity index 74% rename from apps/dashboard/src/components/create-workflow-button.tsx rename to apps/dashboard/src/pages/create-workflow.tsx index 21b1b58f306..651b8f251e5 100644 --- a/apps/dashboard/src/components/create-workflow-button.tsx +++ b/apps/dashboard/src/pages/create-workflow.tsx @@ -8,23 +8,27 @@ import { SheetHeader, SheetMain, SheetTitle, - SheetTrigger, } from '@/components/primitives/sheet'; import { ExternalLink } from '@/components/shared/external-link'; import { CreateWorkflowForm } from '@/components/workflow-editor/create-workflow-form'; import { useCreateWorkflow } from '@/hooks/use-create-workflow'; -import { ComponentProps, forwardRef, useState } from 'react'; import { RiArrowRightSLine } from 'react-icons/ri'; +import { useNavigate } from 'react-router-dom'; -export const CreateWorkflowButton = forwardRef>((props, ref) => { - const [isOpen, setIsOpen] = useState(false); - const { submit, isLoading: isCreating } = useCreateWorkflow({ - onSuccess: () => setIsOpen(false), - }); +export function CreateWorkflowPage() { + const navigate = useNavigate(); + + const { submit, isLoading: isCreating } = useCreateWorkflow(); return ( - - + { + if (!isOpen) { + navigate(-1); + } + }} + > e.preventDefault()}> Create workflow @@ -55,6 +59,4 @@ export const CreateWorkflowButton = forwardRef ); -}); - -CreateWorkflowButton.displayName = 'CreateWorkflowButton'; +} diff --git a/apps/dashboard/src/pages/environments.tsx b/apps/dashboard/src/pages/environments.tsx new file mode 100644 index 00000000000..c87c610f38d --- /dev/null +++ b/apps/dashboard/src/pages/environments.tsx @@ -0,0 +1,29 @@ +import { PageMeta } from '@/components/page-meta'; +import { useEffect } from 'react'; +import { CreateEnvironmentButton } from '../components/create-environment-button'; +import { DashboardLayout } from '../components/dashboard-layout'; +import { EnvironmentsList } from '../components/environments-list'; +import { useTelemetry } from '../hooks/use-telemetry'; +import { TelemetryEvent } from '../utils/telemetry'; + +export function EnvironmentsPage() { + const track = useTelemetry(); + + useEffect(() => { + track(TelemetryEvent.ENVIRONMENTS_PAGE_VIEWED); + }, [track]); + + return ( + <> + + Environments}> +
    +
    + +
    + +
    +
    + + ); +} diff --git a/apps/dashboard/src/pages/index.ts b/apps/dashboard/src/pages/index.ts index d2cc1621da0..349d40e3f10 100644 --- a/apps/dashboard/src/pages/index.ts +++ b/apps/dashboard/src/pages/index.ts @@ -1,11 +1,12 @@ -export * from './workflows'; -export * from './sign-in'; -export * from './sign-up'; +export * from './activity-feed'; +export * from './api-keys'; +export * from './create-workflow'; +export * from './integrations-list-page'; export * from './organization-list'; export * from './questionnaire-page'; +export * from './settings'; +export * from './sign-in'; +export * from './sign-up'; export * from './usecase-select-page'; -export * from './api-keys'; export * from './welcome-page'; -export * from './integrations-list-page'; -export * from './settings'; -export * from './activity-feed'; +export * from './workflows'; diff --git a/apps/dashboard/src/pages/workflows.tsx b/apps/dashboard/src/pages/workflows.tsx index df6ad1fe132..495322002dd 100644 --- a/apps/dashboard/src/pages/workflows.tsx +++ b/apps/dashboard/src/pages/workflows.tsx @@ -1,33 +1,124 @@ -import { CreateWorkflowButton } from '@/components/create-workflow-button'; import { DashboardLayout } from '@/components/dashboard-layout'; import { OptInModal } from '@/components/opt-in-modal'; import { PageMeta } from '@/components/page-meta'; import { Button } from '@/components/primitives/button'; +import { Input } from '@/components/primitives/input'; +import { ScrollArea, ScrollBar } from '@/components/primitives/scroll-area'; +import { useDebounce } from '@/hooks/use-debounce'; import { useFeatureFlag } from '@/hooks/use-feature-flag'; +import { useFetchWorkflows } from '@/hooks/use-fetch-workflows'; import { useTelemetry } from '@/hooks/use-telemetry'; import { TelemetryEvent } from '@/utils/telemetry'; -import { FeatureFlagsKeysEnum } from '@novu/shared'; -import { useEffect, useState } from 'react'; -import { RiArrowDownSLine, RiFileAddLine, RiFileMarkedLine, RiRouteFill } from 'react-icons/ri'; +import { FeatureFlagsKeysEnum, StepTypeEnum } from '@novu/shared'; +import { useEffect } from 'react'; +import { useForm } from 'react-hook-form'; +import { + RiArrowDownSLine, + RiArrowRightSLine, + RiFileAddLine, + RiFileMarkedLine, + RiRouteFill, + RiSearchLine, +} from 'react-icons/ri'; +import { Outlet, useNavigate, useParams, useSearchParams } from 'react-router-dom'; import { ButtonGroupItem, ButtonGroupRoot } from '../components/primitives/button-group'; +import { LinkButton } from '../components/primitives/button-link'; import { DropdownMenu, DropdownMenuContent, DropdownMenuItem, DropdownMenuTrigger, } from '../components/primitives/dropdown-menu'; +import { Form, FormField, FormItem } from '../components/primitives/form/form'; +import { getTemplates, WorkflowTemplate } from '../components/template-store/templates'; +import { WorkflowCard } from '../components/template-store/workflow-card'; import { WorkflowTemplateModal } from '../components/template-store/workflow-template-modal'; -import { WorkflowList } from '../components/workflow-list'; +import { SortableColumn, WorkflowList } from '../components/workflow-list'; +import { buildRoute, ROUTES } from '../utils/routes'; + +interface WorkflowFilters { + query: string; +} export const WorkflowsPage = () => { + const { environmentSlug } = useParams(); const track = useTelemetry(); + const navigate = useNavigate(); const isTemplateStoreEnabled = useFeatureFlag(FeatureFlagsKeysEnum.IS_V2_TEMPLATE_STORE_ENABLED); - const [shouldOpenTemplateModal, setShouldOpenTemplateModal] = useState(false); + const [searchParams, setSearchParams] = useSearchParams({ + orderDirection: 'desc', + orderBy: 'updatedAt', + query: '', + }); + const form = useForm({ + defaultValues: { + query: searchParams.get('query') || '', + }, + }); + + const updateSearchParam = (value: string) => { + if (value) { + searchParams.set('query', value); + } else { + searchParams.delete('query'); + } + setSearchParams(searchParams); + }; + + const debouncedSearch = useDebounce((value: string) => updateSearchParam(value), 500); + + const clearFilters = () => { + form.reset({ query: '' }); + }; + + useEffect(() => { + const subscription = form.watch((value: { query?: string }) => { + debouncedSearch(value.query || ''); + }); + + return () => { + subscription.unsubscribe(); + debouncedSearch.cancel(); + }; + }, [form, debouncedSearch]); + const templates = getTemplates(); + const popularTemplates = templates.filter((template) => template.isPopular).slice(0, 4); + + const offset = parseInt(searchParams.get('offset') || '0'); + const limit = parseInt(searchParams.get('limit') || '12'); + + const { + data: workflowsData, + isPending, + isError, + } = useFetchWorkflows({ + limit, + offset, + orderBy: searchParams.get('orderBy') as SortableColumn, + orderDirection: searchParams.get('orderDirection') as 'asc' | 'desc', + query: searchParams.get('query') || '', + }); + + const hasActiveFilters = searchParams.get('query') && searchParams.get('query') !== null; + + const shouldShowStartWith = + isTemplateStoreEnabled && workflowsData && workflowsData.totalCount < 5 && !hasActiveFilters; useEffect(() => { track(TelemetryEvent.WORKFLOWS_PAGE_VISIT); }, [track]); + const handleTemplateClick = (template: WorkflowTemplate) => { + track(TelemetryEvent.TEMPLATE_WORKFLOW_CLICK); + + navigate( + buildRoute(ROUTES.TEMPLATE_STORE_CREATE_WORKFLOW, { + environmentSlug: environmentSlug || '', + templateId: template.id, + }) + '?source=template-store-card-row' + ); + }; + return ( <> @@ -35,21 +126,34 @@ export const WorkflowsPage = () => {
    -
    + + + ( + + + + )} + /> + + {isTemplateStoreEnabled ? ( - - - + @@ -64,14 +168,29 @@ export const WorkflowsPage = () => { - +
    { + track(TelemetryEvent.CREATE_WORKFLOW_CLICK); + navigate(buildRoute(ROUTES.WORKFLOWS_CREATE, { environmentSlug: environmentSlug || '' })); + }} + >
    Blank Workflow
    - +
    - setShouldOpenTemplateModal(true)}> + { + navigate( + buildRoute(ROUTES.TEMPLATE_STORE, { + environmentSlug: environmentSlug || '', + }) + '?source=create-workflow-dropdown' + ); + }} + > View Workflow Gallery @@ -80,17 +199,104 @@ export const WorkflowsPage = () => {
    ) : ( - - - + )} - {shouldOpenTemplateModal && }
    - + {shouldShowStartWith && ( +
    +
    +
    Start with
    + + navigate( + buildRoute(ROUTES.TEMPLATE_STORE, { + environmentSlug: environmentSlug || '', + }) + '?source=start-with' + ) + } + trailingIcon={RiArrowRightSLine} + > + Explore templates + +
    + +
    +
    { + track(TelemetryEvent.CREATE_WORKFLOW_CLICK); + + navigate(buildRoute(ROUTES.WORKFLOWS_CREATE, { environmentSlug: environmentSlug || '' })); + }} + > + +
    + {popularTemplates.map((template) => ( + step.type as StepTypeEnum)} + onClick={() => handleTemplateClick(template)} + /> + ))} +
    + +
    +
    + )} + +
    + {shouldShowStartWith &&
    Your Workflows
    } + +
    + ); }; + +export const TemplateModal = () => { + const navigate = useNavigate(); + const { templateId, environmentSlug } = useParams(); + const templates = getTemplates(); + const selectedTemplate = templateId ? templates.find((template) => template.id === templateId) : undefined; + + const handleCloseTemplateModal = () => { + navigate(buildRoute(ROUTES.WORKFLOWS, { environmentSlug: environmentSlug || '' })); + }; + + return ( + { + if (!isOpen) { + handleCloseTemplateModal(); + } + }} + selectedTemplate={selectedTemplate} + /> + ); +}; diff --git a/apps/dashboard/src/utils/routes.ts b/apps/dashboard/src/utils/routes.ts index 869d2285989..1129017e15a 100644 --- a/apps/dashboard/src/utils/routes.ts +++ b/apps/dashboard/src/utils/routes.ts @@ -28,7 +28,11 @@ export const ROUTES = { INTEGRATIONS_CONNECT_PROVIDER: '/integrations/connect/:providerId', INTEGRATIONS_UPDATE: '/integrations/:integrationId/update', API_KEYS: '/env/:environmentSlug/api-keys', + ENVIRONMENTS: '/env/:environmentSlug/environments', ACTIVITY_FEED: '/env/:environmentSlug/activity-feed', + TEMPLATE_STORE: '/env/:environmentSlug/workflows/templates', + WORKFLOWS_CREATE: '/env/:environmentSlug/workflows/create', + TEMPLATE_STORE_CREATE_WORKFLOW: '/env/:environmentSlug/workflows/templates/:templateId', } as const; export const buildRoute = (route: string, params: Record) => { diff --git a/apps/dashboard/src/utils/telemetry.ts b/apps/dashboard/src/utils/telemetry.ts index b87a0df82c3..c1dfc3c7c9a 100644 --- a/apps/dashboard/src/utils/telemetry.ts +++ b/apps/dashboard/src/utils/telemetry.ts @@ -42,4 +42,9 @@ export enum TelemetryEvent { TEMPLATE_MODAL_OPENED = 'Template Modal Opened - [Template Store]', TEMPLATE_CATEGORY_SELECTED = 'Template Category Selected - [Template Store]', CREATE_WORKFLOW_FROM_TEMPLATE = 'Create Workflow From Template - [Template Store]', + CREATE_WORKFLOW_CLICK = 'Create Workflow Click', + GENERATE_WORKFLOW_CLICK = 'Generate Workflow Click', + TEMPLATE_WORKFLOW_CLICK = 'Template Workflow Click', + ENVIRONMENTS_PAGE_VIEWED = 'Environments Page Viewed', + CREATE_ENVIRONMENT_CLICK = 'Create Environment Click', } diff --git a/apps/inbound-mail/package.json b/apps/inbound-mail/package.json index a248e81675c..9a5e33058e7 100644 --- a/apps/inbound-mail/package.json +++ b/apps/inbound-mail/package.json @@ -1,6 +1,6 @@ { "name": "@novu/inbound-mail", - "version": "2.1.0", + "version": "2.1.1", "description": "", "author": "", "private": true, diff --git a/apps/web/package.json b/apps/web/package.json index 7113daf5c89..34d4a74b671 100644 --- a/apps/web/package.json +++ b/apps/web/package.json @@ -1,6 +1,6 @@ { "name": "@novu/web", - "version": "2.1.0", + "version": "2.1.1", "private": true, "scripts": { "start": "pnpm panda --watch & cross-env NODE_OPTIONS=--max_old_space_size=8192 DISABLE_ESLINT_PLUGIN=true PORT=4200 react-app-rewired start", @@ -211,4 +211,4 @@ "type:app" ] } -} +} \ No newline at end of file diff --git a/apps/web/src/components/layout/components/v2/HeaderNav.tsx b/apps/web/src/components/layout/components/v2/HeaderNav.tsx index 2da9a717d3a..d3e8128c832 100644 --- a/apps/web/src/components/layout/components/v2/HeaderNav.tsx +++ b/apps/web/src/components/layout/components/v2/HeaderNav.tsx @@ -72,6 +72,7 @@ export function HeaderNav() { alt: 'Novu Logo', }, customerDetails: { + fullName: `${currentUser.firstName} ${currentUser.lastName}`, email: currentUser?.email, emailHash: currentUser?.servicesHashes?.plain, externalId: currentUser?._id, diff --git a/apps/web/src/components/nav/EnvironmentSelect/EnvironmentSelect.tsx b/apps/web/src/components/nav/EnvironmentSelect/EnvironmentSelect.tsx index 7534d9b52bf..8042b4f1e12 100644 --- a/apps/web/src/components/nav/EnvironmentSelect/EnvironmentSelect.tsx +++ b/apps/web/src/components/nav/EnvironmentSelect/EnvironmentSelect.tsx @@ -1,9 +1,9 @@ -import { Select, IconConstruction, IconRocketLaunch } from '@novu/design-system'; +import { IconConstruction, IconRocketLaunch, Select } from '@novu/design-system'; import { css } from '@novu/novui/css'; -import { navSelectStyles } from '../NavSelect.styles'; -import { useEnvironment } from '../../providers/EnvironmentProvider'; import { BaseEnvironmentEnum } from '../../../constants/BaseEnvironmentEnum'; +import { useEnvironment } from '../../providers/EnvironmentProvider'; +import { navSelectStyles } from '../NavSelect.styles'; export function EnvironmentSelect() { const { environment, environments, isLoaded, switchEnvironment } = useEnvironment(); diff --git a/apps/web/src/ee/clerk/components/QuestionnaireForm.tsx b/apps/web/src/ee/clerk/components/QuestionnaireForm.tsx index 0f80fa299f6..2134c0a1bb3 100644 --- a/apps/web/src/ee/clerk/components/QuestionnaireForm.tsx +++ b/apps/web/src/ee/clerk/components/QuestionnaireForm.tsx @@ -1,28 +1,19 @@ -import { useState } from 'react'; -import { useLocation, useNavigate, useSearchParams } from 'react-router-dom'; -import { Controller, useForm } from 'react-hook-form'; -import { useMutation } from '@tanstack/react-query'; import { Group, Input as MantineInput } from '@mantine/core'; -import { captureException } from '@sentry/react'; -import { - FeatureFlagsKeysEnum, - IResponseError, - UpdateExternalOrganizationDto, - JobTitleEnum, - jobTitleToLabelMapper, -} from '@novu/shared'; import { Button, inputStyles, Select } from '@novu/design-system'; +import { FeatureFlagsKeysEnum, JobTitleEnum, jobTitleToLabelMapper, UpdateExternalOrganizationDto } from '@novu/shared'; +import { useState } from 'react'; +import { Controller, useForm } from 'react-hook-form'; +import { useLocation, useNavigate, useSearchParams } from 'react-router-dom'; import styled from '@emotion/styled/macro'; import { api } from '../../../api/api.client'; -import { useAuth } from '../../../hooks/useAuth'; -import { useFeatureFlag, useVercelIntegration } from '../../../hooks'; -import { ROUTES } from '../../../constants/routes'; +import { identifyUser } from '../../../api/telemetry'; import { useSegment } from '../../../components/providers/SegmentProvider'; -import { BRIDGE_SYNC_SAMPLE_ENDPOINT } from '../../../config/index'; -import { DynamicCheckBox } from '../../../pages/auth/components/dynamic-checkbox/DynamicCheckBox'; +import { ROUTES } from '../../../constants/routes'; +import { useFeatureFlag, useVercelIntegration } from '../../../hooks'; +import { useAuth } from '../../../hooks/useAuth'; import { useWebContainerSupported } from '../../../hooks/useWebContainerSupport'; -import { identifyUser } from '../../../api/telemetry'; +import { DynamicCheckBox } from '../../../pages/auth/components/dynamic-checkbox/DynamicCheckBox'; import { hubspotCookie } from '../../../utils'; function updateClerkOrgMetadata(data: UpdateExternalOrganizationDto) { @@ -86,17 +77,6 @@ export function QuestionnaireForm() { await updateOrganization({ ...data }); setLoading(false); - try { - await api.post(`/v1/bridge/sync?source=sample-workspace`, { - bridgeUrl: BRIDGE_SYNC_SAMPLE_ENDPOINT, - }); - } catch (e) { - // eslint-disable-next-line no-console - console.error(e); - - captureException(e); - } - const vercelRedirectData = localStorage.getItem('vercel_redirect_data'); if (vercelRedirectData) { diff --git a/apps/webhook/package.json b/apps/webhook/package.json index 924d5ef4b5f..edd52890119 100644 --- a/apps/webhook/package.json +++ b/apps/webhook/package.json @@ -1,6 +1,6 @@ { "name": "@novu/webhook", - "version": "2.1.0", + "version": "2.1.1", "description": "", "author": "", "private": true, diff --git a/apps/widget/package.json b/apps/widget/package.json index cdd5d6a3b47..b5e7f620138 100644 --- a/apps/widget/package.json +++ b/apps/widget/package.json @@ -1,6 +1,6 @@ { "name": "@novu/widget", - "version": "2.1.0", + "version": "2.1.1", "private": true, "scripts": { "start": "DISABLE_ESLINT_PLUGIN=true react-app-rewired start", diff --git a/apps/worker/package.json b/apps/worker/package.json index bc7f39e5cb0..2e049362d7e 100644 --- a/apps/worker/package.json +++ b/apps/worker/package.json @@ -1,6 +1,6 @@ { "name": "@novu/worker", - "version": "2.1.0", + "version": "2.1.1", "description": "description", "author": "", "private": "true", diff --git a/apps/worker/src/.env.development b/apps/worker/src/.env.development index 14a55d5485f..9e21df14288 100644 --- a/apps/worker/src/.env.development +++ b/apps/worker/src/.env.development @@ -10,8 +10,6 @@ MAX_NOVU_INTEGRATION_SMS_REQUESTS=20 # Storage Service # STORAGE_SERVICE= -# Metrics Service -# METRICS_SERVICE= # Redis REDIS_HOST=localhost diff --git a/apps/worker/src/.env.production b/apps/worker/src/.env.production index 227c2b91e8c..5f492fa3be0 100644 --- a/apps/worker/src/.env.production +++ b/apps/worker/src/.env.production @@ -13,9 +13,6 @@ MAX_NOVU_INTEGRATION_SMS_REQUESTS=20 # Storage Service # STORAGE_SERVICE= -# Metrics Service -# METRICS_SERVICE= - # Redis # REDIS_HOST=localhost REDIS_PORT=6379 diff --git a/apps/worker/src/.env.test b/apps/worker/src/.env.test index 7676c8ca8e6..fefe19d28c8 100644 --- a/apps/worker/src/.env.test +++ b/apps/worker/src/.env.test @@ -14,8 +14,6 @@ NOVU_SMS_INTEGRATION_SENDER=1234567890 # Storage Service # STORAGE_SERVICE= -# Metrics Service -# METRICS_SERVICE= # Redis REDIS_PORT=6379 diff --git a/apps/worker/src/.example.env b/apps/worker/src/.example.env index 3e936c036b1..35f826a00b4 100644 --- a/apps/worker/src/.example.env +++ b/apps/worker/src/.example.env @@ -12,9 +12,6 @@ MAX_NOVU_INTEGRATION_SMS_REQUESTS=20 # Storage Service # STORAGE_SERVICE= -# Metrics Service -# METRICS_SERVICE= - # Redis REDIS_PORT=6379 REDIS_HOST=localhost diff --git a/apps/worker/src/app/workflow/services/standard.worker.ts b/apps/worker/src/app/workflow/services/standard.worker.ts index d2ed0dc5103..a858830ddfa 100644 --- a/apps/worker/src/app/workflow/services/standard.worker.ts +++ b/apps/worker/src/app/workflow/services/standard.worker.ts @@ -169,12 +169,12 @@ export class StandardWorker extends StandardWorkerService { } if (shouldHandleLastFailedJob) { - const handleLastFailedJobCommand = HandleLastFailedJobCommand.create({ - ...minimalData, - error, - }); - - await this.handleLastFailedJob.execute(handleLastFailedJobCommand); + await this.handleLastFailedJob.execute( + HandleLastFailedJobCommand.create({ + ...minimalData, + error, + }) + ); } } catch (anotherError) { Logger.error(anotherError, `Failed to set job ${jobId} as failed`, LOG_CONTEXT); @@ -183,16 +183,14 @@ export class StandardWorker extends StandardWorkerService { private getBackoffStrategies = () => { return async (attemptsMade: number, type: string, eventError: Error, eventJob: Job): Promise => { - const command = { + return await this.webhookFilterBackoffStrategy.execute({ attemptsMade, environmentId: eventJob?.data?._environmentId, eventError, eventJob, organizationId: eventJob?.data?._organizationId, userId: eventJob?.data?._userId, - }; - - return await this.webhookFilterBackoffStrategy.execute(command); + }); }; }; } diff --git a/apps/worker/src/app/workflow/usecases/add-job/add-job.usecase.ts b/apps/worker/src/app/workflow/usecases/add-job/add-job.usecase.ts index a29a6bdf30b..2dc4d6e81a5 100644 --- a/apps/worker/src/app/workflow/usecases/add-job/add-job.usecase.ts +++ b/apps/worker/src/app/workflow/usecases/add-job/add-job.usecase.ts @@ -1,6 +1,7 @@ import { forwardRef, Inject, Injectable, Logger } from '@nestjs/common'; import { parseExpression as parseCronExpression } from 'cron-parser'; import { differenceInMilliseconds } from 'date-fns'; +import _ from 'lodash'; import { JobEntity, JobRepository, JobStatusEnum } from '@novu/dal'; import { @@ -134,8 +135,14 @@ export class AddJob { const filterVariables = shouldRun.variables; const filtered = !shouldRun.passed; + let digestResult: { + digestAmount: number; + digestCreationResult: DigestCreationResultEnum; + cronExpression?: string; + } | null = null; + if (job.type === StepTypeEnum.DIGEST) { - const digestResult = await this.handleDigest(command, filterVariables, job, digestAmount, filtered); + digestResult = await this.handleDigest(command, filterVariables, job, digestAmount, filtered); if (isShouldHaltJobExecution(digestResult.digestCreationResult)) { return; @@ -160,22 +167,24 @@ export class AddJob { const delay = this.getExecutionDelayAmount(filtered, digestAmount, delayAmount); - await this.validateDeferDuration(delay, job, command); + await this.validateDeferDuration(delay, job, command, digestResult?.cronExpression); await this.queueJob(job, delay); } - private async validateDeferDuration(delay: number, job: JobEntity, command: AddJobCommand) { + private async validateDeferDuration(delay: number, job: JobEntity, command: AddJobCommand, cronExpression?: string) { const errors = await this.tierRestrictionsValidateUsecase.execute( TierRestrictionsValidateCommand.create({ deferDurationMs: delay, stepType: job.type, organizationId: command.organizationId, + cron: cronExpression, }) ); + if (errors.length > 0) { - const errorMessages = errors?.map((error) => error.message).join(', '); - Logger.warn({ errors, jobId: job._id }, errorMessages, LOG_CONTEXT); + const uniqueErrors = _.uniq(errors.map((error) => error.message)); + Logger.warn({ errors, jobId: job._id }, uniqueErrors, LOG_CONTEXT); await this.executionLogRoute.execute( ExecutionLogRouteCommand.create({ @@ -185,11 +194,9 @@ export class AddJob { status: ExecutionDetailsStatusEnum.FAILED, isTest: false, isRetry: false, - raw: JSON.stringify({ errors: errorMessages }), + raw: JSON.stringify({ errors: uniqueErrors }), }) ); - - throw new Error(DetailEnum.DEFER_DURATION_LIMIT_EXCEEDED); } } @@ -254,12 +261,14 @@ export class AddJob { private async updateMetadata(response: ExecuteOutput, command: AddJobCommand) { let metadata = {} as IWorkflowStepMetadata; const outputs = response.outputs as DigestOutput; + // digest value is pre-computed by framework and passed as digestKey + const outputDigestValue = outputs?.digestKey; const digestType = getDigestType(outputs); if (isTimedDigestOutput(outputs)) { metadata = { type: DigestTypeEnum.TIMED, - digestKey: outputs?.digestKey, + digestValue: outputDigestValue, timed: { cronExpression: outputs?.cron }, } as IDigestTimedMetadata; @@ -271,7 +280,7 @@ export class AddJob { { $set: { 'digest.type': metadata.type, - 'digest.digestKey': metadata.digestKey, + 'digest.digestValue': metadata.digestValue, 'digest.amount': metadata.amount, 'digest.unit': metadata.unit, 'digest.timed.cronExpression': metadata.timed?.cronExpression, @@ -284,7 +293,7 @@ export class AddJob { metadata = { type: digestType, amount: outputs?.amount, - digestKey: outputs?.digestKey, + digestValue: outputDigestValue, unit: outputs.unit ? castUnitToDigestUnitEnum(outputs?.unit) : undefined, backoff: digestType === DigestTypeEnum.BACKOFF, backoffAmount: outputs.lookBackWindow?.amount, @@ -299,7 +308,7 @@ export class AddJob { { $set: { 'digest.type': metadata.type, - 'digest.digestKey': metadata.digestKey, + 'digest.digestValue': metadata.digestValue, 'digest.amount': metadata.amount, 'digest.unit': metadata.unit, 'digest.backoff': metadata.backoff, @@ -314,7 +323,7 @@ export class AddJob { metadata = { type: digestType, amount: outputs?.amount, - digestKey: outputs?.digestKey, + digestValue: outputDigestValue, unit: outputs.unit ? castUnitToDigestUnitEnum(outputs?.unit) : undefined, } as IDigestRegularMetadata; @@ -326,7 +335,7 @@ export class AddJob { { $set: { 'digest.type': metadata.type, - 'digest.digestKey': metadata.digestKey, + 'digest.digestValue': metadata.digestValue, 'digest.amount': metadata.amount, 'digest.unit': metadata.unit, }, @@ -387,7 +396,7 @@ export class AddJob { await this.handleDigestSkip(command, job); } - return { digestAmount, digestCreationResult }; + return { digestAmount, digestCreationResult, cronExpression: bridgeResponse?.outputs?.cron as string | undefined }; } private mapBridgeTimedDigestAmount(bridgeResponse: ExecuteOutput | null): number | null { diff --git a/apps/worker/src/app/workflow/usecases/add-job/merge-or-create-digest.usecase.ts b/apps/worker/src/app/workflow/usecases/add-job/merge-or-create-digest.usecase.ts index efc8742c72d..f8cdb977c37 100644 --- a/apps/worker/src/app/workflow/usecases/add-job/merge-or-create-digest.usecase.ts +++ b/apps/worker/src/app/workflow/usecases/add-job/merge-or-create-digest.usecase.ts @@ -47,7 +47,7 @@ export class MergeOrCreateDigest { const digestMeta = job.digest as IDigestBaseMetadata; const digestKey = digestMeta?.digestKey; - const digestValue = getNestedValue(job.payload, digestKey); + const digestValue = digestMeta?.digestValue ?? getNestedValue(job.payload, digestKey); const digestAction = command.filtered ? { digestResult: DigestCreationResultEnum.SKIPPED } @@ -150,9 +150,13 @@ export class MergeOrCreateDigest { } private getLockKey(job: JobEntity, digestKey: string | undefined, digestValue: string | number | undefined): string { - let resource = `environment:${job._environmentId}:template:${job._templateId}:subscriber:${job._subscriberId}`; + const resource = `environment:${job._environmentId}:template:${job._templateId}:subscriber:${job._subscriberId}`; if (digestKey && digestValue) { - resource = `${resource}:digestKey:${digestKey}:digestValue:${digestValue}`; + return `${resource}:digestKey:${digestKey}:digestValue:${digestValue}`; + } + + if (digestValue) { + return `${resource}:digestValue:${digestValue}`; } return resource; diff --git a/apps/worker/src/app/workflow/usecases/run-job/run-job.usecase.ts b/apps/worker/src/app/workflow/usecases/run-job/run-job.usecase.ts index 6863acb511d..aa6f5aff766 100644 --- a/apps/worker/src/app/workflow/usecases/run-job/run-job.usecase.ts +++ b/apps/worker/src/app/workflow/usecases/run-job/run-job.usecase.ts @@ -1,4 +1,4 @@ -import { Injectable, Logger } from '@nestjs/common'; +import { forwardRef, Inject, Injectable, Logger } from '@nestjs/common'; import { JobEntity, JobRepository, JobStatusEnum, NotificationRepository } from '@novu/dal'; import { StepTypeEnum } from '@novu/shared'; import { setUser } from '@sentry/node'; @@ -11,9 +11,11 @@ import { } from '@novu/application-generic'; import { RunJobCommand } from './run-job.command'; -import { QueueNextJob, QueueNextJobCommand } from '../queue-next-job'; import { SendMessage, SendMessageCommand } from '../send-message'; import { PlatformException, EXCEPTION_MESSAGE_ON_WEBHOOK_FILTER } from '../../../shared/utils'; +import { SetJobAsFailed } from '../update-job-status/set-job-as-failed.usecase'; +import { AddJob } from '../add-job'; +import { SetJobAsFailedCommand } from '../update-job-status/set-job-as.command'; const nr = require('newrelic'); @@ -24,7 +26,8 @@ export class RunJob { constructor( private jobRepository: JobRepository, private sendMessage: SendMessage, - private queueNextJob: QueueNextJob, + @Inject(forwardRef(() => AddJob)) private addJobUsecase: AddJob, + @Inject(forwardRef(() => SetJobAsFailed)) private setJobAsFailed: SetJobAsFailed, private storageHelperService: StorageHelperService, private notificationRepository: NotificationRepository, private logger?: PinoLogger @@ -114,19 +117,7 @@ export class RunJob { throw error; } finally { if (shouldQueueNextJob) { - const newJob = await this.queueNextJob.execute( - QueueNextJobCommand.create({ - parentId: job._id, - environmentId: job._environmentId, - organizationId: job._organizationId, - userId: job._userId, - }) - ); - - // Only remove the attachments if that is the last job - if (!newJob) { - await this.storageHelperService.deleteAttachments(job.payload?.attachments); - } + await this.tryQueueNextJobs(job); } else { // Remove the attachments if the job should not be queued await this.storageHelperService.deleteAttachments(job.payload?.attachments); @@ -134,6 +125,73 @@ export class RunJob { } } + /** + * Attempts to queue subsequent jobs in the workflow chain. + * If queueNextJob.execute returns undefined, we stop the workflow. + * Otherwise, we continue trying to queue the next job in the chain. + */ + private async tryQueueNextJobs(job: JobEntity): Promise { + let currentFailedJob: JobEntity | null = job; + let nextJob: JobEntity | null = null; + if (!currentFailedJob) { + return; + } + + let shouldContinue = true; + + while (shouldContinue) { + try { + if (!currentFailedJob) { + return; + } + + nextJob = await this.jobRepository.findOne({ + _environmentId: currentFailedJob._environmentId, + _parentId: currentFailedJob._id, + }); + + if (!nextJob) { + return; + } + + await this.addJobUsecase.execute({ + userId: nextJob._userId, + environmentId: nextJob._environmentId, + organizationId: nextJob._organizationId, + jobId: nextJob._id, + job: nextJob, + }); + + shouldContinue = false; + } catch (error: any) { + if (!nextJob) { + return; + } + + await this.setJobAsFailed.execute( + SetJobAsFailedCommand.create({ + environmentId: nextJob._environmentId, + jobId: nextJob._id, + organizationId: nextJob._organizationId, + userId: nextJob._userId, + }), + error + ); + + if (nextJob.step.shouldStopOnFail || this.shouldBackoff(error)) { + shouldContinue = false; + throw error; + } + + currentFailedJob = nextJob; + } finally { + if (nextJob) { + await this.storageHelperService.deleteAttachments(nextJob.payload?.attachments); + } + } + } + } + private assignLogger(job) { try { this.logger?.assign({ diff --git a/apps/worker/src/config/env.validators.ts b/apps/worker/src/config/env.validators.ts index 837f9568f44..87981502841 100644 --- a/apps/worker/src/config/env.validators.ts +++ b/apps/worker/src/config/env.validators.ts @@ -1,5 +1,5 @@ -import { json, port, str, num, ValidatorSpec, makeValidator, bool, CleanedEnv, cleanEnv, url } from 'envalid'; import { DEFAULT_NOTIFICATION_RETENTION_DAYS, FeatureFlagsKeysEnum, StringifyEnv } from '@novu/shared'; +import { bool, CleanedEnv, cleanEnv, json, makeValidator, num, port, str, url, ValidatorSpec } from 'envalid'; export function validateEnv() { return cleanEnv(process.env, envValidators); @@ -31,7 +31,6 @@ export const envValidators = { MAX_NOVU_INTEGRATION_MAIL_REQUESTS: num({ default: 300 }), NOVU_EMAIL_INTEGRATION_API_KEY: str({ default: '' }), STORAGE_SERVICE: str({ default: undefined }), - METRICS_SERVICE: str({ default: '' }), REDIS_HOST: str(), REDIS_PORT: port(), REDIS_PASSWORD: str({ default: undefined }), diff --git a/apps/ws/package.json b/apps/ws/package.json index ce1825d8bd0..a94ec9b2233 100644 --- a/apps/ws/package.json +++ b/apps/ws/package.json @@ -1,6 +1,6 @@ { "name": "@novu/ws", - "version": "2.1.0", + "version": "2.1.1", "description": "", "author": "", "private": true, diff --git a/docker/community/.env.example b/docker/community/.env.example index 5a4329b1a0d..af692cc591c 100644 --- a/docker/community/.env.example +++ b/docker/community/.env.example @@ -7,6 +7,9 @@ SUBSCRIBER_WIDGET_JWT_EXPIRATION_TIME="15 days" # used to encrypt/decrypt the provider credentials STORE_ENCRYPTION_KEY= +# Random generated secret key. +NOVU_SECRET_KEY= + # Host HOST_NAME=http://localhost diff --git a/docker/community/docker-compose.yml b/docker/community/docker-compose.yml index 3e9ce03d765..cafaaad0c3b 100644 --- a/docker/community/docker-compose.yml +++ b/docker/community/docker-compose.yml @@ -81,6 +81,7 @@ services: AWS_SECRET_ACCESS_KEY: ${AWS_SECRET_ACCESS_KEY} JWT_SECRET: ${JWT_SECRET} STORE_ENCRYPTION_KEY: ${STORE_ENCRYPTION_KEY} + NOVU_SECRET_KEY: ${NOVU_SECRET_KEY} SUBSCRIBER_WIDGET_JWT_EXPIRATION_TIME: ${SUBSCRIBER_WIDGET_JWT_EXPIRATION_TIME} SENTRY_DSN: ${SENTRY_DSN} NEW_RELIC_APP_NAME: ${NEW_RELIC_APP_NAME} diff --git a/enterprise/packages/auth/package.json b/enterprise/packages/auth/package.json index 1f689ca11b9..f1350088b44 100644 --- a/enterprise/packages/auth/package.json +++ b/enterprise/packages/auth/package.json @@ -13,7 +13,7 @@ }, "dependencies": { "@clerk/backend": "^1.6.2", - "@clerk/clerk-sdk-node": "^5.0.19", + "@clerk/clerk-sdk-node": "^5.1.6", "@novu/application-generic": "workspace:*", "@novu/dal": "workspace:*", "@novu/shared": "workspace:*", diff --git a/lerna.json b/lerna.json index 81c41ba6d76..8e8181c5782 100644 --- a/lerna.json +++ b/lerna.json @@ -2,18 +2,11 @@ "npmClient": "pnpm", "useNx": true, "useWorkspaces": true, - "packages": [ - "apps/*", - "libs/*", - "packages/*", - "enterprise/packages/*", - "enterprise/packages/*/*", - "providers/*" - ], + "packages": ["apps/*", "libs/*", "packages/*", "enterprise/packages/*", "enterprise/packages/*/*", "providers/*"], "command": { "publish": { "message": "chore(release): publish - ci skip" } }, - "version": "2.1.0" + "version": "2.1.1" } diff --git a/libs/application-generic/package.json b/libs/application-generic/package.json index 844ad144e17..0ef5609e393 100644 --- a/libs/application-generic/package.json +++ b/libs/application-generic/package.json @@ -38,12 +38,12 @@ "reflect-metadata": "0.2.2" }, "dependencies": { - "@aws-sdk/client-cloudwatch": "^3.567.0", "@aws-sdk/client-s3": "^3.567.0", "@aws-sdk/s3-request-presigner": "^3.567.0", "@azure/storage-blob": "^12.11.0", "@google-cloud/storage": "^6.2.3", "@hokify/agenda": "^6.3.0", + "@maily-to/render": "^0.0.17", "@novu/dal": "workspace:*", "@novu/framework": "workspace:*", "@novu/providers": "workspace:*", @@ -74,6 +74,7 @@ "bullmq": "^3.10.2", "class-transformer": "0.5.1", "class-validator": "0.14.1", + "cron-parser": "^4.9.0", "date-fns": "^2.29.2", "got": "^11.8.6", "handlebars": "^4.7.7", @@ -83,7 +84,6 @@ "launchdarkly-node-server-sdk": "^7.0.1", "lodash": "^4.17.15", "mixpanel": "^0.17.0", - "cron-parser": "^4.9.0", "nanoid": "^3.1.20", "nestjs-otel": "6.1.1", "nestjs-pino": "4.1.0", @@ -96,7 +96,6 @@ "rxjs": "7.8.1", "sanitize-html": "^2.4.0", "shortid": "^2.2.16", - "@maily-to/render": "^0.0.17", "zod": "^3.23.8", "zod-to-json-schema": "^3.23.3" }, diff --git a/libs/application-generic/src/commands/project.command.ts b/libs/application-generic/src/commands/project.command.ts index 9e18e7ba316..fb8bda7cdc6 100644 --- a/libs/application-generic/src/commands/project.command.ts +++ b/libs/application-generic/src/commands/project.command.ts @@ -75,7 +75,7 @@ export abstract class PaginatedListCommand extends EnvironmentWithUserObjectComm @IsDefined() @IsString() - orderByField: string; + orderBy: string; } export abstract class EnvironmentWithSubscriber extends BaseCommand { diff --git a/libs/application-generic/src/index.ts b/libs/application-generic/src/index.ts index 6e3370e3e90..ee2461e8dc8 100644 --- a/libs/application-generic/src/index.ts +++ b/libs/application-generic/src/index.ts @@ -18,4 +18,3 @@ export * from './usecases'; export * from './utils'; export * from './utils/inject-auth-providers'; export * from './schemas/control'; -export * from './utils/process-node-attrs'; diff --git a/libs/application-generic/src/modules/metrics.module.ts b/libs/application-generic/src/modules/metrics.module.ts index e04ed04672b..e1af4e185c6 100644 --- a/libs/application-generic/src/modules/metrics.module.ts +++ b/libs/application-generic/src/modules/metrics.module.ts @@ -1,18 +1,10 @@ import { Module, Provider } from '@nestjs/common'; import { MetricsService, metricsServiceList } from '../services/metrics'; -import { - AwsMetricsService, - AzureMetricsService, - GCPMetricsService, - NewRelicMetricsService, -} from '../services/metrics/metrics.service'; +import { NewRelicMetricsService } from '../services/metrics/metrics.service'; const PROVIDERS: Provider[] = [ MetricsService, NewRelicMetricsService, - GCPMetricsService, - AzureMetricsService, - AwsMetricsService, metricsServiceList, ]; diff --git a/libs/application-generic/src/services/metrics/index.ts b/libs/application-generic/src/services/metrics/index.ts index 240ec4a3f5d..35271bc5a8c 100644 --- a/libs/application-generic/src/services/metrics/index.ts +++ b/libs/application-generic/src/services/metrics/index.ts @@ -1,25 +1,9 @@ -import { - AzureMetricsService, - GCPMetricsService, - AwsMetricsService, - MetricsService, - NewRelicMetricsService, -} from './metrics.service'; +import { MetricsService, NewRelicMetricsService } from './metrics.service'; export const metricsServiceList = { provide: 'MetricsServices', - useFactory: ( - newRelicMetricsService: NewRelicMetricsService, - gcsMetricsService: GCPMetricsService, - azureMetricsService: AzureMetricsService, - awsMetricsService: AwsMetricsService, - ) => { - const allMetricsServices = [ - newRelicMetricsService, - gcsMetricsService, - azureMetricsService, - awsMetricsService, - ]; + useFactory: (newRelicMetricsService: NewRelicMetricsService) => { + const allMetricsServices = [newRelicMetricsService]; const activeMetricsServices = allMetricsServices.filter((service) => service.isActive(process.env), @@ -27,12 +11,7 @@ export const metricsServiceList = { return activeMetricsServices; }, - inject: [ - NewRelicMetricsService, - GCPMetricsService, - AzureMetricsService, - AwsMetricsService, - ], + inject: [NewRelicMetricsService], }; export { MetricsService }; diff --git a/libs/application-generic/src/services/metrics/metrics.service.spec.ts b/libs/application-generic/src/services/metrics/metrics.service.spec.ts index 1bbe0a69b41..d1f31b9c09b 100644 --- a/libs/application-generic/src/services/metrics/metrics.service.spec.ts +++ b/libs/application-generic/src/services/metrics/metrics.service.spec.ts @@ -1,13 +1,7 @@ import { Test, TestingModule } from '@nestjs/testing'; -import { - MetricsService, - NewRelicMetricsService, - AwsMetricsService, - GCPMetricsService, - AzureMetricsService, -} from './metrics.service'; import { metricsServiceList } from './index'; import { IMetricsService } from './metrics.interface'; +import { MetricsService, NewRelicMetricsService } from './metrics.service'; describe('MetricsService', () => { let service: MetricsService; @@ -17,28 +11,12 @@ describe('MetricsService', () => { providers: [ MetricsService, NewRelicMetricsService, - AwsMetricsService, - GCPMetricsService, - AzureMetricsService, { provide: 'MetricsServices', - useFactory: ( - newRelicMetricsService: NewRelicMetricsService, - gcsMetricsService: GCPMetricsService, - azureMetricsService: AzureMetricsService, - awsMetricsService: AwsMetricsService, - ) => [ + useFactory: (newRelicMetricsService: NewRelicMetricsService) => [ newRelicMetricsService, - gcsMetricsService, - azureMetricsService, - awsMetricsService, - ], - inject: [ - NewRelicMetricsService, - GCPMetricsService, - AzureMetricsService, - AwsMetricsService, ], + inject: [NewRelicMetricsService], }, ], }).compile(); @@ -59,19 +37,9 @@ describe('MetricsService', () => { NewRelicMetricsService.prototype, 'recordMetric', ); - const spyAws = jest.spyOn(AwsMetricsService.prototype, 'recordMetric'); - const spyGcs = jest.spyOn(GCPMetricsService.prototype, 'recordMetric'); - const spyAzure = jest.spyOn( - AzureMetricsService.prototype, - 'recordMetric', - ); - service.recordMetric(metricName, metricValue); expect(spyNewRelic).toHaveBeenCalledWith(metricName, metricValue); - expect(spyAws).toHaveBeenCalledWith(metricName, metricValue); - expect(spyGcs).toHaveBeenCalledWith(metricName, metricValue); - expect(spyAzure).toHaveBeenCalledWith(metricName, metricValue); }); }); @@ -83,9 +51,6 @@ describe('MetricsService', () => { metricsServiceList, MetricsService, NewRelicMetricsService, - AwsMetricsService, - GCPMetricsService, - AzureMetricsService, ], }).compile()) as TestingModule ).get('MetricsServices'); @@ -104,64 +69,5 @@ describe('MetricsService', () => { delete process.env.NEW_RELIC_LICENSE_KEY; }); }); - - describe('AWS', () => { - it('should contain AwsMetricsService if METRICS_SERVICE is set to AWS and Credentials and Region are set', async () => { - process.env.METRICS_SERVICE = 'AWS'; - process.env.AWS_ACCESS_KEY_ID = 'test'; - process.env.AWS_SECRET_ACCESS_KEY = 'test'; - process.env.AWS_REGION = 'test'; - const metricsServices = await createServices(); - - expect( - metricsServices.some( - (metricsService) => metricsService instanceof AwsMetricsService, - ), - ).toBe(true); - delete process.env.METRICS_SERVICE; - delete process.env.AWS_ACCESS_KEY_ID; - delete process.env.AWS_SECRET_ACCESS_KEY; - delete process.env.AWS_REGION; - }); - - it('should throw an error if METRICS_SERVICE is set to AWS and Credentials or Region are not set', async () => { - process.env.METRICS_SERVICE = 'AWS'; - delete process.env.AWS_ACCESS_KEY_ID; - delete process.env.AWS_SECRET_ACCESS_KEY; - delete process.env.AWS_REGION; - await expect(createServices()).rejects.toThrow( - 'Missing AWS credentials', - ); - delete process.env.METRICS_SERVICE; - }); - }); - - describe('GCP', () => { - it('should contain GCPMetricsService if METRICS_SERVICE is set to GCS', async () => { - process.env.METRICS_SERVICE = 'GCP'; - const metricsServices = await createServices(); - - expect( - metricsServices.some( - (metricsService) => metricsService instanceof GCPMetricsService, - ), - ).toBe(true); - delete process.env.METRICS_SERVICE; - }); - }); - - describe('Azure', () => { - it('should contain AzureMetricsService if METRICS_SERVICE is set to AZURE', async () => { - process.env.METRICS_SERVICE = 'AZURE'; - const metricsServices = await createServices(); - - expect( - metricsServices.some( - (metricsService) => metricsService instanceof AzureMetricsService, - ), - ).toBe(true); - delete process.env.METRICS_SERVICE; - }); - }); }); }); diff --git a/libs/application-generic/src/services/metrics/metrics.service.ts b/libs/application-generic/src/services/metrics/metrics.service.ts index 5f88b42d27e..43d60ccd609 100644 --- a/libs/application-generic/src/services/metrics/metrics.service.ts +++ b/libs/application-generic/src/services/metrics/metrics.service.ts @@ -1,13 +1,8 @@ import { Inject, Injectable, Logger } from '@nestjs/common'; -import { - CloudWatchClient, - PutMetricDataCommand, -} from '@aws-sdk/client-cloudwatch'; import { IMetricsService } from './metrics.interface'; const nr = require('newrelic'); -const NAMESPACE = 'Novu'; const LOG_CONTEXT = 'MetricsService'; @Injectable() @@ -46,52 +41,3 @@ export class NewRelicMetricsService implements IMetricsService { return !!env.NEW_RELIC_LICENSE_KEY; } } - -@Injectable() -export class AwsMetricsService implements IMetricsService { - private client = new CloudWatchClient(); - - async recordMetric(name: string, value: number): Promise { - const command = new PutMetricDataCommand({ - Namespace: NAMESPACE, - MetricData: [{ MetricName: name, Value: value, StorageResolution: 1 }], - }); - await this.client.send(command); - } - - isActive(env: Record): boolean { - if (env.METRICS_SERVICE === 'AWS') { - if ( - !!env.AWS_ACCESS_KEY_ID && - !!env.AWS_SECRET_ACCESS_KEY && - !!env.AWS_REGION - ) { - return true; - } else { - throw new Error('Missing AWS credentials'); - } - } - } -} - -@Injectable() -export class GCPMetricsService implements IMetricsService { - async recordMetric(name: string, value: number): Promise { - throw new Error('Method not implemented.'); - } - - isActive(env: Record): boolean { - return env.METRICS_SERVICE === 'GCP'; - } -} - -@Injectable() -export class AzureMetricsService implements IMetricsService { - async recordMetric(key: string, value: number): Promise { - throw new Error('Method not implemented.'); - } - - isActive(env: Record): boolean { - return env.METRICS_SERVICE === 'AZURE'; - } -} diff --git a/libs/application-generic/src/usecases/tier-restrictions-validate/tier-restrictions-validate.usecase.ts b/libs/application-generic/src/usecases/tier-restrictions-validate/tier-restrictions-validate.usecase.ts index 3aade636a7c..9b9c07d1ac4 100644 --- a/libs/application-generic/src/usecases/tier-restrictions-validate/tier-restrictions-validate.usecase.ts +++ b/libs/application-generic/src/usecases/tier-restrictions-validate/tier-restrictions-validate.usecase.ts @@ -8,7 +8,7 @@ import { } from '@novu/shared'; import { CommunityOrganizationRepository } from '@novu/dal'; -import { differenceInMilliseconds } from 'date-fns'; +import { differenceInMilliseconds, addYears, isAfter } from 'date-fns'; import { TierRestrictionsValidateCommand } from './tier-restrictions-validate.command'; import { @@ -43,19 +43,20 @@ export class TierRestrictionsValidateUsecase { const apiServiceLevel = ( await this.organizationRepository.findById(command.organizationId) )?.apiServiceLevel; + const maxDelayMs = getMaxDelay(apiServiceLevel); if (isCronExpression(command.cron)) { - // TODO: Implement cron expression validation - - /* - * const deferDurationMs = this.buildCronDeltaDeferDuration(command); - * const issue = buildIssue( - * deferDurationMs, - * getMaxDelay(apiServiceLevel), - * ErrorEnum.TIER_LIMIT_EXCEEDED, - * 'cron', - * ); - */ + if (this.isCronDeltaDeferDurationExceededTier(command.cron, maxDelayMs)) { + return [ + { + controlKey: 'cron', + error: ErrorEnum.TIER_LIMIT_EXCEEDED, + message: + `The maximum delay allowed is ${msToDays(maxDelayMs)} days. ` + + 'Please contact our support team to discuss extending this limit for your use case.', + }, + ]; + } return []; } @@ -65,13 +66,13 @@ export class TierRestrictionsValidateUsecase { const amountIssue = buildIssue( deferDurationMs, - getMaxDelay(apiServiceLevel), + maxDelayMs, ErrorEnum.TIER_LIMIT_EXCEEDED, 'amount', ); const unitIssue = buildIssue( deferDurationMs, - getMaxDelay(apiServiceLevel), + maxDelayMs, ErrorEnum.TIER_LIMIT_EXCEEDED, 'unit', ); @@ -82,14 +83,37 @@ export class TierRestrictionsValidateUsecase { return []; } - private buildCronDeltaDeferDuration( - command: TierRestrictionsValidateCommand, - ): number | null { - const cronExpression = parseCronExpression(command.cron); - const firstTime = cronExpression.next().toDate(); - const secondTime = cronExpression.next().toDate(); + private isCronDeltaDeferDurationExceededTier( + cron: string, + maxDelayMs: number, + ): boolean { + const cronExpression = parseCronExpression(cron); + const firstDate = cronExpression.next().toDate(); + const twoYearsFromFirst = addYears(firstDate, 2); + let previousDate = firstDate; + const MAX_ITERATIONS = 50; + + for (let i = 0; i < MAX_ITERATIONS; i += 1) { + const currentDate = cronExpression.next().toDate(); + + // If we've gone past two years from the first date, the intervals are safe + if (isAfter(currentDate, twoYearsFromFirst)) { + return false; + } + + const deferDurationMs = differenceInMilliseconds( + currentDate, + previousDate, + ); + + if (deferDurationMs > maxDelayMs) { + return true; + } + + previousDate = currentDate; + } - return differenceInMilliseconds(firstTime, secondTime); + return false; } } @@ -142,6 +166,10 @@ const isCronExpression = (cron: string) => { }; const isRegularDeferAction = (command: TierRestrictionsValidateCommand) => { + if (command.deferDurationMs) { + return true; + } + return ( !!command.amount && isNumber(command.amount) && diff --git a/libs/application-generic/src/utils/process-node-attrs.ts b/libs/application-generic/src/utils/process-node-attrs.ts deleted file mode 100644 index 4715c1fa292..00000000000 --- a/libs/application-generic/src/utils/process-node-attrs.ts +++ /dev/null @@ -1,160 +0,0 @@ -import { JSONContent } from '@maily-to/render'; - -export enum MailyContentTypeEnum { - VARIABLE = 'variable', - FOR = 'for', - BUTTON = 'button', - IMAGE = 'image', - LINK = 'link', -} - -export enum MailyAttrsEnum { - ID = 'id', - SHOW_IF_KEY = 'showIfKey', - EACH_KEY = 'each', - FALLBACK = 'fallback', - IS_SRC_VARIABLE = 'isSrcVariable', - IS_EXTERNAL_LINK_VARIABLE = 'isExternalLinkVariable', - IS_TEXT_VARIABLE = 'isTextVariable', - IS_URL_VARIABLE = 'isUrlVariable', - TEXT = 'text', - URL = 'url', - SRC = 'src', - EXTERNAL_LINK = 'externalLink', - HREF = 'href', -} - -export const MAILY_ITERABLE_MARK = '0'; - -const MAILY_FIRST_CITIZEN_VARIABLE_KEY = [ - MailyAttrsEnum.ID, - MailyAttrsEnum.SHOW_IF_KEY, - MailyAttrsEnum.EACH_KEY, -]; - -export const variableAttributeConfig = (type: MailyContentTypeEnum) => { - const commonConfig = [ - /* - * Maily Variable Map - * * maily_id equals to maily_variable - * * https://github.com/arikchakma/maily.to/blob/ebcf233eb1d4b16fb568fb702bf0756678db38d0/packages/render/src/maily.tsx#L787 - */ - { attr: MailyAttrsEnum.ID, flag: MailyAttrsEnum.ID }, - /* - * showIfKey is always a maily_variable - */ - { attr: MailyAttrsEnum.SHOW_IF_KEY, flag: MailyAttrsEnum.SHOW_IF_KEY }, - { attr: MailyAttrsEnum.EACH_KEY, flag: MailyAttrsEnum.EACH_KEY }, - ]; - - if (type === MailyContentTypeEnum.BUTTON) { - return [ - { attr: MailyAttrsEnum.TEXT, flag: MailyAttrsEnum.IS_TEXT_VARIABLE }, - { attr: MailyAttrsEnum.URL, flag: MailyAttrsEnum.IS_URL_VARIABLE }, - ...commonConfig, - ]; - } - - if (type === MailyContentTypeEnum.IMAGE) { - return [ - { attr: MailyAttrsEnum.SRC, flag: MailyAttrsEnum.IS_SRC_VARIABLE }, - { - attr: MailyAttrsEnum.EXTERNAL_LINK, - flag: MailyAttrsEnum.IS_EXTERNAL_LINK_VARIABLE, - }, - ...commonConfig, - ]; - } - - if (type === MailyContentTypeEnum.LINK) { - return [ - { attr: MailyAttrsEnum.HREF, flag: MailyAttrsEnum.IS_URL_VARIABLE }, - ...commonConfig, - ]; - } - - return commonConfig; -}; - -function processAttributes( - attrs: Record, - type: MailyContentTypeEnum, - forLoopVariable?: string, -): void { - if (!attrs) return; - - const typeConfig = variableAttributeConfig(type); - - for (const { attr, flag } of typeConfig) { - if (attrs[flag] && attrs[attr]) { - attrs[attr] = wrapInLiquidOutput( - attrs[attr] as string, - attrs.fallback as string, - forLoopVariable, - ); - if (!MAILY_FIRST_CITIZEN_VARIABLE_KEY.includes(flag)) { - // eslint-disable-next-line no-param-reassign - attrs[flag] = false; - } - } - } -} - -export function processNodeAttrs( - node: JSONContent, - forLoopVariable?: string, -): JSONContent { - if (!node.attrs) return node; - - processAttributes( - node.attrs, - node.type as MailyContentTypeEnum, - forLoopVariable, - ); - - return node; -} - -export function processNodeMarks(node: JSONContent): JSONContent { - if (!node.marks) return node; - - for (const mark of node.marks) { - if (mark.attrs) { - processAttributes(mark.attrs, mark.type as MailyContentTypeEnum); - } - } - - return node; -} - -function wrapInLiquidOutput( - variableName: string, - fallback?: string, - forLoopVariable?: string, -): string { - const sanitizedVariableName = sanitizeVariableName(variableName); - const sanitizedForLoopVariable = - forLoopVariable && sanitizeVariableName(forLoopVariable); - - /* - * Handle loop variable replacement - * payload.comments.name => payload.comments[0].name - */ - const processedName = - sanitizedForLoopVariable && - sanitizedVariableName.startsWith(sanitizedForLoopVariable) - ? sanitizedVariableName.replace( - sanitizedForLoopVariable, - `${sanitizedForLoopVariable}[${MAILY_ITERABLE_MARK}]`, - ) - : sanitizedVariableName; - - // Build liquid output syntax - const fallbackSuffix = fallback ? ` | default: '${fallback}'` : ''; - - return `{{ ${processedName}${fallbackSuffix} }}`; -} - -function sanitizeVariableName(variableName: string): string { - return variableName.replace(/{{|}}/g, '').trim(); -} diff --git a/libs/dal/src/repositories/environment/environment.entity.ts b/libs/dal/src/repositories/environment/environment.entity.ts index 6e2efd86448..48bf7fc2a74 100644 --- a/libs/dal/src/repositories/environment/environment.entity.ts +++ b/libs/dal/src/repositories/environment/environment.entity.ts @@ -2,8 +2,8 @@ import { Types } from 'mongoose'; import { EncryptedSecret, IApiRateLimitMaximum } from '@novu/shared'; -import type { OrganizationId } from '../organization'; import type { ChangePropsValueType } from '../../types/helpers'; +import type { OrganizationId } from '../organization'; export interface IApiKey { /* @@ -44,6 +44,8 @@ export class EnvironmentEntity { _parentId: string; + color?: string; + echo: { url: string; }; diff --git a/libs/dal/src/repositories/environment/environment.schema.ts b/libs/dal/src/repositories/environment/environment.schema.ts index 8d12cd9103b..63b6438a7da 100644 --- a/libs/dal/src/repositories/environment/environment.schema.ts +++ b/libs/dal/src/repositories/environment/environment.schema.ts @@ -1,5 +1,5 @@ -import mongoose, { Schema } from 'mongoose'; import { ApiRateLimitCategoryEnum } from '@novu/shared'; +import mongoose, { Schema } from 'mongoose'; import { schemaOptions } from '../schema-default.options'; import { EnvironmentDBModel } from './environment.entity'; @@ -57,6 +57,7 @@ const environmentSchema = new Schema( type: Schema.Types.ObjectId, ref: 'Environment', }, + color: Schema.Types.String, }, schemaOptions ); diff --git a/libs/dal/src/repositories/job/job.repository.ts b/libs/dal/src/repositories/job/job.repository.ts index b5db39c6236..f295bc9a9a1 100644 --- a/libs/dal/src/repositories/job/job.repository.ts +++ b/libs/dal/src/repositories/job/job.repository.ts @@ -189,9 +189,10 @@ export class JobRepository extends BaseRepository) { const query = { updatedAt: { $gte: this.getBackoffDate(metadata), @@ -276,7 +281,7 @@ export class JobRepository extends BaseRepository( digestKey: { type: Schema.Types.String, }, + digestValue: { + type: Schema.Types.String, + }, type: { type: Schema.Types.String, }, diff --git a/libs/dal/src/repositories/notification-template/notification-template.repository.ts b/libs/dal/src/repositories/notification-template/notification-template.repository.ts index e9e23fe18cd..6718962f076 100644 --- a/libs/dal/src/repositories/notification-template/notification-template.repository.ts +++ b/libs/dal/src/repositories/notification-template/notification-template.repository.ts @@ -1,12 +1,13 @@ import { FilterQuery } from 'mongoose'; import { SoftDeleteModel } from 'mongoose-delete'; -import { BaseRepository } from '../base-repository'; -import { NotificationTemplate } from './notification-template.schema'; -import { NotificationTemplateDBModel, NotificationTemplateEntity } from './notification-template.entity'; +import { DirectionEnum } from '@novu/shared'; import { DalException } from '../../shared'; import type { EnforceEnvOrOrgIds } from '../../types/enforce'; +import { BaseRepository } from '../base-repository'; import { EnvironmentRepository } from '../environment'; +import { NotificationTemplateDBModel, NotificationTemplateEntity } from './notification-template.entity'; +import { NotificationTemplate } from './notification-template.schema'; type NotificationTemplateQuery = FilterQuery & EnforceEnvOrOrgIds; // eslint-disable-next-line @typescript-eslint/naming-convention @@ -194,7 +195,9 @@ export class NotificationTemplateRepository extends BaseRepository< skip: number = 0, limit: number = 10, query?: string, - excludeNewDashboardWorkflows: boolean = false + excludeNewDashboardWorkflows: boolean = false, + orderBy: string = 'createdAt', + orderDirection: DirectionEnum = DirectionEnum.DESC ): Promise<{ totalCount: number; data: NotificationTemplateEntity[] }> { const searchQuery: FilterQuery = {}; @@ -219,7 +222,7 @@ export class NotificationTemplateRepository extends BaseRepository< _organizationId: organizationId, ...searchQuery, }) - .sort({ createdAt: -1 }) + .sort({ [orderBy]: orderDirection === DirectionEnum.ASC ? 1 : -1 }) .skip(skip) .limit(limit) .populate({ path: 'notificationGroup' }) diff --git a/libs/dal/src/repositories/organization/organization.repository.ts b/libs/dal/src/repositories/organization/organization.repository.ts index ba4955316c8..5cb97dd1564 100644 --- a/libs/dal/src/repositories/organization/organization.repository.ts +++ b/libs/dal/src/repositories/organization/organization.repository.ts @@ -1,4 +1,3 @@ -import { ApiServiceLevelEnum } from '@novu/shared'; import { Inject } from '@nestjs/common'; import { IPartnerConfiguration, OrganizationEntity } from './organization.entity'; import { IOrganizationRepository } from './organization-repository.interface'; diff --git a/libs/dal/src/repositories/user/community.user.repository.ts b/libs/dal/src/repositories/user/community.user.repository.ts index 56c7804250a..356cc26d2c4 100644 --- a/libs/dal/src/repositories/user/community.user.repository.ts +++ b/libs/dal/src/repositories/user/community.user.repository.ts @@ -49,4 +49,8 @@ export class CommunityUserRepository } ); } + + async findUserSessions(userId: string): Promise<[]> { + throw new Error('Not implemented'); + } } diff --git a/libs/dal/src/repositories/user/user-repository.interface.ts b/libs/dal/src/repositories/user/user-repository.interface.ts index bb6f34918ac..53b591720ad 100644 --- a/libs/dal/src/repositories/user/user-repository.interface.ts +++ b/libs/dal/src/repositories/user/user-repository.interface.ts @@ -10,6 +10,7 @@ export interface IUserRepository extends IUserRepositoryMongo { token: string, resetTokenCount: IUserResetTokenCount ): Promise<{ matched: number; modified: number }>; + findUserSessions(userId: string): Promise<[]>; } /** diff --git a/libs/dal/src/repositories/user/user.repository.ts b/libs/dal/src/repositories/user/user.repository.ts index a9144e7566d..e460d87aa18 100644 --- a/libs/dal/src/repositories/user/user.repository.ts +++ b/libs/dal/src/repositories/user/user.repository.ts @@ -26,6 +26,10 @@ export class UserRepository implements IUserRepository { return this.userRepository.updatePasswordResetToken(userId, token, resetTokenCount); } + async findUserSessions(userId: string): Promise<[]> { + return this.userRepository.findUserSessions(userId); + } + create(data: any, options?: any): Promise { return this.userRepository.create(data, options); } diff --git a/libs/testing/package.json b/libs/testing/package.json index 54764e4f108..d0e766c6908 100644 --- a/libs/testing/package.json +++ b/libs/testing/package.json @@ -22,7 +22,7 @@ "types": "dist/index.d.ts", "dependencies": { "@clerk/backend": "^1.6.2", - "@clerk/clerk-sdk-node": "^5.0.19", + "@clerk/clerk-sdk-node": "^5.1.6", "@clerk/types": "^4.6.1", "@faker-js/faker": "^6.0.0", "@novu/dal": "workspace:*", diff --git a/packages/framework/src/client.ts b/packages/framework/src/client.ts index e760163670a..b47295073c4 100644 --- a/packages/framework/src/client.ts +++ b/packages/framework/src/client.ts @@ -312,7 +312,8 @@ export class Client { // Only evaluate a skip condition when the step is the current step and not in preview mode. if (!isPreview && stepId === event.stepId) { - const controls = await this.createStepControls(step, event); + const templateControls = await this.createStepControls(step, event); + const controls = await this.compileControls(templateControls, event); const shouldSkip = await this.shouldSkip(options?.skip as typeof step.options.skip, controls); if (shouldSkip) { diff --git a/packages/framework/src/servers/h3.ts b/packages/framework/src/servers/h3.ts index 9970b717af7..b4ea17d7f4f 100644 --- a/packages/framework/src/servers/h3.ts +++ b/packages/framework/src/servers/h3.ts @@ -1,4 +1,4 @@ -import { getHeader, getQuery, type H3Event, readBody, send, setHeaders, type EventHandlerRequest } from 'h3'; +import { getHeader, getQuery, type H3Event, readBody, send, setHeaders } from 'h3'; import { NovuRequestHandler, type ServeHandlerOptions } from '../handler'; import { type SupportedFrameworkName } from '../types'; @@ -49,7 +49,7 @@ export const serve = (options: ServeHandlerOptions) => { const handler = new NovuRequestHandler({ frameworkName, ...options, - handler: (event: H3Event) => { + handler: (event: H3Event) => { return { body: () => readBody(event), headers: (key) => getHeader(event, key), @@ -59,6 +59,7 @@ export const serve = (options: ServeHandlerOptions) => { String(event.path), `${process.env.NODE_ENV === 'development' ? 'http' : 'https'}://${String(getHeader(event, 'host'))}` ), + // eslint-disable-next-line @typescript-eslint/no-base-to-string queryString: (key) => String(getQuery(event)[key]), transformResponse: (actionRes) => { const { res } = event.node; diff --git a/packages/framework/src/servers/nuxt.ts b/packages/framework/src/servers/nuxt.ts index 6b32be4382a..9fb3c3e4c42 100644 --- a/packages/framework/src/servers/nuxt.ts +++ b/packages/framework/src/servers/nuxt.ts @@ -27,6 +27,7 @@ export const serve = (options: ServeHandlerOptions) => { * TODO: Fix this */ handler: (event: H3Event) => ({ + // eslint-disable-next-line @typescript-eslint/no-base-to-string queryString: (key) => String(getQuery(event)[key]), body: () => readBody(event), headers: (key) => getHeader(event, key), diff --git a/packages/shared/src/clients/workflows-client.ts b/packages/shared/src/clients/workflows-client.ts index 1efbbfcd439..79ac6198fa1 100644 --- a/packages/shared/src/clients/workflows-client.ts +++ b/packages/shared/src/clients/workflows-client.ts @@ -1,4 +1,3 @@ -import { createNovuBaseClient, HttpError, NovuRestResult } from './novu-base-client'; import { CreateWorkflowDto, GeneratePreviewRequestDto, @@ -13,6 +12,7 @@ import { WorkflowResponseDto, WorkflowTestDataResponseDto, } from '../dto'; +import { createNovuBaseClient, HttpError, NovuRestResult } from './novu-base-client'; export const createWorkflowClient = (baseUrl: string, headers: HeadersInit = {}) => { const baseClient = createNovuBaseClient(baseUrl, headers); @@ -76,8 +76,8 @@ export const createWorkflowClient = (baseUrl: string, headers: HeadersInit = {}) if (queryParams.orderDirection) { query.append('orderDirection', queryParams.orderDirection); } - if (queryParams.orderByField) { - query.append('orderByField', queryParams.orderByField); + if (queryParams.orderBy) { + query.append('orderBy', queryParams.orderBy); } if (queryParams.query) { query.append('query', queryParams.query); diff --git a/packages/shared/src/consts/productFeatureEnabledForServiceLevel.ts b/packages/shared/src/consts/productFeatureEnabledForServiceLevel.ts index b4671200c0d..0646f00c298 100644 --- a/packages/shared/src/consts/productFeatureEnabledForServiceLevel.ts +++ b/packages/shared/src/consts/productFeatureEnabledForServiceLevel.ts @@ -3,5 +3,6 @@ import { ApiServiceLevelEnum, ProductFeatureKeyEnum } from '../types'; export const productFeatureEnabledForServiceLevel: Record = Object.freeze( { [ProductFeatureKeyEnum.TRANSLATIONS]: [ApiServiceLevelEnum.BUSINESS, ApiServiceLevelEnum.ENTERPRISE], + [ProductFeatureKeyEnum.MANAGE_ENVIRONMENTS]: [ApiServiceLevelEnum.BUSINESS, ApiServiceLevelEnum.ENTERPRISE], } ); diff --git a/packages/shared/src/dto/workflows/control-schemas.ts b/packages/shared/src/dto/workflows/control-schemas.ts deleted file mode 100644 index 37a2041bd61..00000000000 --- a/packages/shared/src/dto/workflows/control-schemas.ts +++ /dev/null @@ -1,12 +0,0 @@ -export type TipTapNode = { - type?: string; - attrs?: Record; - content?: TipTapNode[]; - marks?: { - type: string; - attrs?: Record; - [key: string]: any; - }[]; - text?: string; - [key: string]: any; -}; diff --git a/packages/shared/src/dto/workflows/get-list-query-params.ts b/packages/shared/src/dto/workflows/get-list-query-params.ts index d6b6434bedd..a69afe16764 100644 --- a/packages/shared/src/dto/workflows/get-list-query-params.ts +++ b/packages/shared/src/dto/workflows/get-list-query-params.ts @@ -1,6 +1,6 @@ -import { WorkflowResponseDto } from './workflow.dto'; import { LimitOffsetPaginationDto } from '../../types'; +import { WorkflowResponseDto } from './workflow.dto'; -export class GetListQueryParams extends LimitOffsetPaginationDto { +export class GetListQueryParams extends LimitOffsetPaginationDto { query?: string; } diff --git a/packages/shared/src/dto/workflows/index.ts b/packages/shared/src/dto/workflows/index.ts index 8c91d5414b7..b7c87b41c3b 100644 --- a/packages/shared/src/dto/workflows/index.ts +++ b/packages/shared/src/dto/workflows/index.ts @@ -1,4 +1,3 @@ -export * from './control-schemas'; export * from './create-workflow-deprecated.dto'; export * from './generate-preview-request.dto'; export * from './get-list-query-params'; diff --git a/packages/shared/src/entities/environment/environment.interface.ts b/packages/shared/src/entities/environment/environment.interface.ts index 85dbe4b2f85..6f9479a1661 100644 --- a/packages/shared/src/entities/environment/environment.interface.ts +++ b/packages/shared/src/entities/environment/environment.interface.ts @@ -10,7 +10,7 @@ export interface IEnvironment { widget: IWidgetSettings; dns?: IDnsSettings; apiRateLimits?: IApiRateLimitMaximum; - + color: string; branding?: { color: string; logo: string; @@ -26,6 +26,9 @@ export interface IEnvironment { bridge?: { url?: string; }; + + createdAt: Date; + updatedAt: Date; } export interface IWidgetSettings { diff --git a/packages/shared/src/entities/step/index.ts b/packages/shared/src/entities/step/index.ts index ed408e14904..9e57ebb1c58 100644 --- a/packages/shared/src/entities/step/index.ts +++ b/packages/shared/src/entities/step/index.ts @@ -88,6 +88,7 @@ export interface IAmountAndUnitDigest { export interface IDigestBaseMetadata extends IAmountAndUnitDigest { digestKey?: string; + digestValue?: string; } export interface IDigestRegularMetadata extends IDigestBaseMetadata { diff --git a/packages/shared/src/types/billing.ts b/packages/shared/src/types/billing.ts index 8e2fa630c11..3444ae4494c 100644 --- a/packages/shared/src/types/billing.ts +++ b/packages/shared/src/types/billing.ts @@ -1,3 +1,4 @@ export enum ProductFeatureKeyEnum { TRANSLATIONS, + MANAGE_ENVIRONMENTS, } diff --git a/packages/shared/src/types/environment.ts b/packages/shared/src/types/environment.ts index 20de5689805..e73cefacae8 100644 --- a/packages/shared/src/types/environment.ts +++ b/packages/shared/src/types/environment.ts @@ -4,3 +4,7 @@ export enum EnvironmentEnum { DEVELOPMENT = 'Development', PRODUCTION = 'Production', } + +export const PROTECTED_ENVIRONMENTS = [EnvironmentEnum.DEVELOPMENT, EnvironmentEnum.PRODUCTION] as const; + +export type EnvironmentName = EnvironmentEnum | string; diff --git a/packages/shared/src/types/feature-flags.ts b/packages/shared/src/types/feature-flags.ts index 815c8b6efc5..3241708574c 100644 --- a/packages/shared/src/types/feature-flags.ts +++ b/packages/shared/src/types/feature-flags.ts @@ -31,6 +31,7 @@ export enum FeatureFlagsKeysEnum { IS_AI_TEMPLATE_STORE_ENABLED = 'IS_AI_TEMPLATE_STORE_ENABLED', IS_CONTROLS_AUTOCOMPLETE_ENABLED = 'IS_CONTROLS_AUTOCOMPLETE_ENABLED', IS_EMAIL_INLINE_CSS_DISABLED = 'IS_EMAIL_INLINE_CSS_DISABLED', + IS_ENVIRONMENT_MANAGEMENT_ENABLED = 'IS_ENVIRONMENT_MANAGEMENT_ENABLED', IS_EVENT_QUOTA_LIMITING_ENABLED = 'IS_EVENT_QUOTA_LIMITING_ENABLED', IS_HUBSPOT_ONBOARDING_ENABLED = 'IS_HUBSPOT_ONBOARDING_ENABLED', IS_INTEGRATION_INVALIDATION_DISABLED = 'IS_INTEGRATION_INVALIDATION_DISABLED', diff --git a/packages/shared/src/types/response.ts b/packages/shared/src/types/response.ts index 662c2f7e6e3..ae7ce209f54 100644 --- a/packages/shared/src/types/response.ts +++ b/packages/shared/src/types/response.ts @@ -22,7 +22,7 @@ export class LimitOffsetPaginationDto> { limit: string; offset: string; orderDirection?: DirectionEnum; - orderByField?: K; + orderBy?: K; } export interface IPaginationParams { diff --git a/pnpm-lock.yaml b/pnpm-lock.yaml index 053985e4d15..4ff7d5fc0d6 100644 --- a/pnpm-lock.yaml +++ b/pnpm-lock.yaml @@ -137,10 +137,10 @@ importers: version: 4.13.0(typescript@5.6.2) eslint-config-airbnb-base: specifier: ^15.0.0 - version: 15.0.0(eslint-plugin-import@2.29.1(@typescript-eslint/parser@8.18.2(eslint@8.57.1)(typescript@5.6.2))(eslint-import-resolver-webpack@0.13.8)(eslint@8.57.1))(eslint@8.57.1) + version: 15.0.0(eslint-plugin-import@2.29.1)(eslint@8.57.1) eslint-config-airbnb-typescript: specifier: ^18.0.0 - version: 18.0.0(@typescript-eslint/eslint-plugin@8.18.2(@typescript-eslint/parser@8.18.2(eslint@8.57.1)(typescript@5.6.2))(eslint@8.57.1)(typescript@5.6.2))(@typescript-eslint/parser@8.18.2(eslint@8.57.1)(typescript@5.6.2))(eslint-plugin-import@2.29.1(@typescript-eslint/parser@8.18.2(eslint@8.57.1)(typescript@5.6.2))(eslint-import-resolver-webpack@0.13.8)(eslint@8.57.1))(eslint@8.57.1) + version: 18.0.0(@typescript-eslint/eslint-plugin@8.18.2(@typescript-eslint/parser@8.18.2(eslint@8.57.1)(typescript@5.6.2))(eslint@8.57.1)(typescript@5.6.2))(@typescript-eslint/parser@8.18.2(eslint@8.57.1)(typescript@5.6.2))(eslint-plugin-import@2.29.1)(eslint@8.57.1) eslint-config-auto: specifier: ^0.9.0 version: 0.9.0(typescript@5.6.2) @@ -242,7 +242,7 @@ importers: version: 12.1.1(eslint@8.57.1) eslint-plugin-sonarjs: specifier: ^2.0.1 - version: 2.0.1(@typescript-eslint/parser@8.18.2(eslint@8.57.1)(typescript@5.6.2))(eslint-import-resolver-webpack@0.13.8(eslint-plugin-import@2.29.1)(webpack@5.94.0(@swc/core@1.3.107(@swc/helpers@0.5.12))))(eslint@8.57.1) + version: 2.0.1(@typescript-eslint/parser@8.18.2(eslint@8.57.1)(typescript@5.6.2))(eslint-import-resolver-webpack@0.13.8)(eslint@8.57.1) eslint-plugin-spellcheck: specifier: 0.0.20 version: 0.0.20(eslint@8.57.1) @@ -395,13 +395,13 @@ importers: version: 10.4.1(@nestjs/common@10.4.1(class-transformer@0.5.1)(class-validator@0.14.1)(reflect-metadata@0.2.2)(rxjs@7.8.1))(@nestjs/core@10.4.1) '@nestjs/swagger': specifier: 7.4.0 - version: 7.4.0(@nestjs/common@10.4.1(class-transformer@0.5.1)(class-validator@0.14.1)(reflect-metadata@0.2.2)(rxjs@7.8.1))(@nestjs/core@10.4.1(@nestjs/common@10.4.1(class-transformer@0.5.1)(class-validator@0.14.1)(reflect-metadata@0.2.2)(rxjs@7.8.1))(@nestjs/platform-express@10.4.1)(@nestjs/websockets@10.4.1)(encoding@0.1.13)(reflect-metadata@0.2.2)(rxjs@7.8.1))(class-transformer@0.5.1)(class-validator@0.14.1)(reflect-metadata@0.2.2) + version: 7.4.0(@nestjs/common@10.4.1(class-transformer@0.5.1)(class-validator@0.14.1)(reflect-metadata@0.2.2)(rxjs@7.8.1))(@nestjs/core@10.4.1)(class-transformer@0.5.1)(class-validator@0.14.1)(reflect-metadata@0.2.2) '@nestjs/terminus': specifier: 10.2.3 - version: 10.2.3(@grpc/grpc-js@1.12.4)(@grpc/proto-loader@0.7.13)(@nestjs/axios@3.0.3(@nestjs/common@10.4.1(class-transformer@0.5.1)(class-validator@0.14.1)(reflect-metadata@0.2.2)(rxjs@7.8.1))(axios@1.6.8)(rxjs@7.8.1))(@nestjs/common@10.4.1(class-transformer@0.5.1)(class-validator@0.14.1)(reflect-metadata@0.2.2)(rxjs@7.8.1))(@nestjs/core@10.4.1(@nestjs/common@10.4.1(class-transformer@0.5.1)(class-validator@0.14.1)(reflect-metadata@0.2.2)(rxjs@7.8.1))(@nestjs/platform-express@10.4.1)(@nestjs/websockets@10.4.1)(encoding@0.1.13)(reflect-metadata@0.2.2)(rxjs@7.8.1))(mongoose@8.6.0(@aws-sdk/credential-providers@3.637.0(@aws-sdk/client-sso-oidc@3.726.0(@aws-sdk/client-sts@3.726.0)))(gcp-metadata@5.3.0(encoding@0.1.13))(socks@2.7.1))(reflect-metadata@0.2.2)(rxjs@7.8.1) + version: 10.2.3(@grpc/grpc-js@1.12.4)(@grpc/proto-loader@0.7.13)(@nestjs/axios@3.0.3(@nestjs/common@10.4.1(class-transformer@0.5.1)(class-validator@0.14.1)(reflect-metadata@0.2.2)(rxjs@7.8.1))(axios@1.6.8)(rxjs@7.8.1))(@nestjs/common@10.4.1(class-transformer@0.5.1)(class-validator@0.14.1)(reflect-metadata@0.2.2)(rxjs@7.8.1))(@nestjs/core@10.4.1)(mongoose@8.6.0(@aws-sdk/credential-providers@3.637.0(@aws-sdk/client-sso-oidc@3.726.0(@aws-sdk/client-sts@3.726.0)))(gcp-metadata@5.3.0(encoding@0.1.13))(socks@2.7.1))(reflect-metadata@0.2.2)(rxjs@7.8.1) '@nestjs/throttler': specifier: 6.2.1 - version: 6.2.1(@nestjs/common@10.4.1(class-transformer@0.5.1)(class-validator@0.14.1)(reflect-metadata@0.2.2)(rxjs@7.8.1))(@nestjs/core@10.4.1(@nestjs/common@10.4.1(class-transformer@0.5.1)(class-validator@0.14.1)(reflect-metadata@0.2.2)(rxjs@7.8.1))(@nestjs/platform-express@10.4.1)(@nestjs/websockets@10.4.1)(encoding@0.1.13)(reflect-metadata@0.2.2)(rxjs@7.8.1))(reflect-metadata@0.2.2) + version: 6.2.1(@nestjs/common@10.4.1(class-transformer@0.5.1)(class-validator@0.14.1)(reflect-metadata@0.2.2)(rxjs@7.8.1))(@nestjs/core@10.4.1)(reflect-metadata@0.2.2) '@novu/api': specifier: 0.1.0 version: 0.1.0(zod@3.23.8) @@ -437,7 +437,7 @@ importers: version: 7.114.0 '@sentry/nestjs': specifier: ^8.33.1 - version: 8.33.1(@nestjs/common@10.4.1(class-transformer@0.5.1)(class-validator@0.14.1)(reflect-metadata@0.2.2)(rxjs@7.8.1))(@nestjs/core@10.4.1(@nestjs/common@10.4.1(class-transformer@0.5.1)(class-validator@0.14.1)(reflect-metadata@0.2.2)(rxjs@7.8.1))(@nestjs/platform-express@10.4.1)(@nestjs/websockets@10.4.1)(encoding@0.1.13)(reflect-metadata@0.2.2)(rxjs@7.8.1)) + version: 8.33.1(@nestjs/common@10.4.1(class-transformer@0.5.1)(class-validator@0.14.1)(reflect-metadata@0.2.2)(rxjs@7.8.1))(@nestjs/core@10.4.1) '@sentry/node': specifier: ^8.33.1 version: 8.33.1 @@ -524,7 +524,7 @@ importers: version: 3.3.6 nest-raven: specifier: 10.1.0 - version: 10.1.0(@nestjs/common@10.4.1(class-transformer@0.5.1)(class-validator@0.14.1)(reflect-metadata@0.2.2)(rxjs@7.8.1))(@nestjs/core@10.4.1(@nestjs/common@10.4.1(class-transformer@0.5.1)(class-validator@0.14.1)(reflect-metadata@0.2.2)(rxjs@7.8.1))(@nestjs/platform-express@10.4.1)(@nestjs/websockets@10.4.1)(encoding@0.1.13)(reflect-metadata@0.2.2)(rxjs@7.8.1))(@sentry/node@8.33.1)(class-transformer@0.5.1)(class-validator@0.14.1)(graphql@16.9.0)(reflect-metadata@0.2.2)(rxjs@7.8.1) + version: 10.1.0(@nestjs/common@10.4.1(class-transformer@0.5.1)(class-validator@0.14.1)(reflect-metadata@0.2.2)(rxjs@7.8.1))(@nestjs/core@10.4.1)(@sentry/node@8.33.1)(class-transformer@0.5.1)(class-validator@0.14.1)(graphql@16.9.0)(reflect-metadata@0.2.2)(rxjs@7.8.1) newrelic: specifier: ^12.8.1 version: 12.8.2 @@ -613,7 +613,7 @@ importers: version: 10.1.4(chokidar@3.6.0)(typescript@5.6.2) '@nestjs/testing': specifier: 10.4.1 - version: 10.4.1(@nestjs/common@10.4.1(class-transformer@0.5.1)(class-validator@0.14.1)(reflect-metadata@0.2.2)(rxjs@7.8.1))(@nestjs/core@10.4.1(@nestjs/common@10.4.1(class-transformer@0.5.1)(class-validator@0.14.1)(reflect-metadata@0.2.2)(rxjs@7.8.1))(@nestjs/platform-express@10.4.1)(@nestjs/websockets@10.4.1)(encoding@0.1.13)(reflect-metadata@0.2.2)(rxjs@7.8.1))(@nestjs/platform-express@10.4.1(@nestjs/common@10.4.1(class-transformer@0.5.1)(class-validator@0.14.1)(reflect-metadata@0.2.2)(rxjs@7.8.1))(@nestjs/core@10.4.1)) + version: 10.4.1(@nestjs/common@10.4.1(class-transformer@0.5.1)(class-validator@0.14.1)(reflect-metadata@0.2.2)(rxjs@7.8.1))(@nestjs/core@10.4.1)(@nestjs/platform-express@10.4.1) '@stoplight/spectral-cli': specifier: ^6.11.0 version: 6.11.0(encoding@0.1.13) @@ -1037,7 +1037,7 @@ importers: version: 7.114.0 '@sentry/nestjs': specifier: ^8.33.1 - version: 8.33.1(@nestjs/common@10.4.1(class-transformer@0.5.1)(class-validator@0.14.1)(reflect-metadata@0.2.2)(rxjs@7.8.1))(@nestjs/core@10.4.1(@nestjs/common@10.4.1(class-transformer@0.5.1)(class-validator@0.14.1)(reflect-metadata@0.2.2)(rxjs@7.8.1))(@nestjs/platform-express@10.4.1)(@nestjs/websockets@10.4.1)(encoding@0.1.13)(reflect-metadata@0.2.2)(rxjs@7.8.1)) + version: 8.33.1(@nestjs/common@10.4.1(class-transformer@0.5.1)(class-validator@0.14.1)(reflect-metadata@0.2.2)(rxjs@7.8.1))(@nestjs/core@10.4.1) '@sentry/node': specifier: ^8.33.1 version: 8.33.1 @@ -1534,7 +1534,7 @@ importers: version: 7.4.2 '@storybook/preset-create-react-app': specifier: ^7.4.2 - version: 7.4.2(ucmnrhmq4kewpo24xrp57f5r6y) + version: 7.4.2(@babel/core@7.22.11)(@types/webpack@5.28.5(@swc/core@1.3.107(@swc/helpers@0.5.12))(esbuild@0.18.20))(react-refresh@0.11.0)(react-scripts@5.0.1(@babel/plugin-syntax-flow@7.24.7(@babel/core@7.22.11))(@babel/plugin-transform-react-jsx@7.25.2(@babel/core@7.22.11))(@swc/core@1.3.107(@swc/helpers@0.5.12))(@types/babel__core@7.20.5)(@types/webpack@5.28.5(@swc/core@1.3.107(@swc/helpers@0.5.12))(esbuild@0.18.20))(esbuild@0.18.20)(eslint-import-resolver-webpack@0.13.8)(eslint@9.9.1(jiti@1.21.6))(react@18.3.1)(sass@1.77.8)(ts-node@10.9.1(@swc/core@1.3.107(@swc/helpers@0.5.12))(@types/node@18.16.9)(typescript@5.6.2))(type-fest@2.19.0)(typescript@5.6.2)(vue-template-compiler@2.7.16)(webpack-hot-middleware@2.26.1))(type-fest@2.19.0)(typescript@5.6.2)(webpack-dev-server@4.11.1(webpack@5.78.0(@swc/core@1.3.107(@swc/helpers@0.5.12))(esbuild@0.18.20)))(webpack-hot-middleware@2.26.1)(webpack@5.78.0(@swc/core@1.3.107(@swc/helpers@0.5.12))(esbuild@0.18.20)) '@storybook/react': specifier: ^7.4.2 version: 7.4.2(encoding@0.1.13)(react-dom@18.3.1(react@18.3.1))(react@18.3.1)(typescript@5.6.2) @@ -1570,13 +1570,13 @@ importers: version: 4.1.0(less@4.1.3)(webpack@5.78.0(@swc/core@1.3.107(@swc/helpers@0.5.12))(esbuild@0.18.20)) react-app-rewired: specifier: ^2.2.1 - version: 2.2.1(react-scripts@5.0.1(@babel/plugin-syntax-flow@7.24.7(@babel/core@7.22.11))(@babel/plugin-transform-react-jsx@7.25.2(@babel/core@7.22.11))(@swc/core@1.3.107(@swc/helpers@0.5.12))(@types/babel__core@7.20.5)(@types/webpack@5.28.5(@swc/core@1.3.107(@swc/helpers@0.5.12))(esbuild@0.18.20))(esbuild@0.18.20)(eslint-import-resolver-webpack@0.13.8(eslint-plugin-import@2.29.1)(webpack@5.94.0(@swc/core@1.3.107(@swc/helpers@0.5.12))))(eslint@9.9.1(jiti@1.21.6))(react@18.3.1)(sass@1.77.8)(ts-node@10.9.1(@swc/core@1.3.107(@swc/helpers@0.5.12))(@types/node@18.16.9)(typescript@5.6.2))(type-fest@2.19.0)(typescript@5.6.2)(vue-template-compiler@2.7.16)(webpack-hot-middleware@2.26.1)) + version: 2.2.1(react-scripts@5.0.1(@babel/plugin-syntax-flow@7.24.7(@babel/core@7.22.11))(@babel/plugin-transform-react-jsx@7.25.2(@babel/core@7.22.11))(@swc/core@1.3.107(@swc/helpers@0.5.12))(@types/babel__core@7.20.5)(@types/webpack@5.28.5(@swc/core@1.3.107(@swc/helpers@0.5.12))(esbuild@0.18.20))(esbuild@0.18.20)(eslint-import-resolver-webpack@0.13.8)(eslint@9.9.1(jiti@1.21.6))(react@18.3.1)(sass@1.77.8)(ts-node@10.9.1(@swc/core@1.3.107(@swc/helpers@0.5.12))(@types/node@18.16.9)(typescript@5.6.2))(type-fest@2.19.0)(typescript@5.6.2)(vue-template-compiler@2.7.16)(webpack-hot-middleware@2.26.1)) react-error-overlay: specifier: 6.0.11 version: 6.0.11 react-scripts: specifier: ^5.0.1 - version: 5.0.1(@babel/plugin-syntax-flow@7.24.7(@babel/core@7.22.11))(@babel/plugin-transform-react-jsx@7.25.2(@babel/core@7.22.11))(@swc/core@1.3.107(@swc/helpers@0.5.12))(@types/babel__core@7.20.5)(@types/webpack@5.28.5(@swc/core@1.3.107(@swc/helpers@0.5.12))(esbuild@0.18.20))(esbuild@0.18.20)(eslint-import-resolver-webpack@0.13.8(eslint-plugin-import@2.29.1)(webpack@5.94.0(@swc/core@1.3.107(@swc/helpers@0.5.12))))(eslint@9.9.1(jiti@1.21.6))(react@18.3.1)(sass@1.77.8)(ts-node@10.9.1(@swc/core@1.3.107(@swc/helpers@0.5.12))(@types/node@18.16.9)(typescript@5.6.2))(type-fest@2.19.0)(typescript@5.6.2)(vue-template-compiler@2.7.16)(webpack-hot-middleware@2.26.1) + version: 5.0.1(@babel/plugin-syntax-flow@7.24.7(@babel/core@7.22.11))(@babel/plugin-transform-react-jsx@7.25.2(@babel/core@7.22.11))(@swc/core@1.3.107(@swc/helpers@0.5.12))(@types/babel__core@7.20.5)(@types/webpack@5.28.5(@swc/core@1.3.107(@swc/helpers@0.5.12))(esbuild@0.18.20))(esbuild@0.18.20)(eslint-import-resolver-webpack@0.13.8)(eslint@9.9.1(jiti@1.21.6))(react@18.3.1)(sass@1.77.8)(ts-node@10.9.1(@swc/core@1.3.107(@swc/helpers@0.5.12))(@types/node@18.16.9)(typescript@5.6.2))(type-fest@2.19.0)(typescript@5.6.2)(vue-template-compiler@2.7.16)(webpack-hot-middleware@2.26.1) sinon: specifier: 9.2.4 version: 9.2.4 @@ -1615,7 +1615,7 @@ importers: version: 10.4.1(@nestjs/common@10.4.1(class-transformer@0.5.1)(class-validator@0.14.1)(reflect-metadata@0.2.2)(rxjs@7.8.1))(@nestjs/core@10.4.1) '@nestjs/terminus': specifier: 10.2.3 - version: 10.2.3(@grpc/grpc-js@1.12.4)(@grpc/proto-loader@0.7.13)(@nestjs/axios@3.0.3(@nestjs/common@10.4.1(class-transformer@0.5.1)(class-validator@0.14.1)(reflect-metadata@0.2.2)(rxjs@7.8.1))(axios@1.6.8)(rxjs@7.8.1))(@nestjs/common@10.4.1(class-transformer@0.5.1)(class-validator@0.14.1)(reflect-metadata@0.2.2)(rxjs@7.8.1))(@nestjs/core@10.4.1(@nestjs/common@10.4.1(class-transformer@0.5.1)(class-validator@0.14.1)(reflect-metadata@0.2.2)(rxjs@7.8.1))(@nestjs/platform-express@10.4.1)(@nestjs/websockets@10.4.1)(encoding@0.1.13)(reflect-metadata@0.2.2)(rxjs@7.8.1))(mongoose@8.6.0(@aws-sdk/credential-providers@3.637.0(@aws-sdk/client-sso-oidc@3.726.0(@aws-sdk/client-sts@3.726.0)))(gcp-metadata@5.3.0(encoding@0.1.13))(socks@2.7.1))(reflect-metadata@0.2.2)(rxjs@7.8.1) + version: 10.2.3(@grpc/grpc-js@1.12.4)(@grpc/proto-loader@0.7.13)(@nestjs/axios@3.0.3(@nestjs/common@10.4.1(class-transformer@0.5.1)(class-validator@0.14.1)(reflect-metadata@0.2.2)(rxjs@7.8.1))(axios@1.6.8)(rxjs@7.8.1))(@nestjs/common@10.4.1(class-transformer@0.5.1)(class-validator@0.14.1)(reflect-metadata@0.2.2)(rxjs@7.8.1))(@nestjs/core@10.4.1)(mongoose@8.6.0(@aws-sdk/credential-providers@3.637.0(@aws-sdk/client-sso-oidc@3.726.0(@aws-sdk/client-sts@3.726.0)))(gcp-metadata@5.3.0(encoding@0.1.13))(socks@2.7.1))(reflect-metadata@0.2.2)(rxjs@7.8.1) '@novu/application-generic': specifier: workspace:* version: link:../../libs/application-generic @@ -1639,7 +1639,7 @@ importers: version: 7.114.0 '@sentry/nestjs': specifier: ^8.33.1 - version: 8.33.1(@nestjs/common@10.4.1(class-transformer@0.5.1)(class-validator@0.14.1)(reflect-metadata@0.2.2)(rxjs@7.8.1))(@nestjs/core@10.4.1(@nestjs/common@10.4.1(class-transformer@0.5.1)(class-validator@0.14.1)(reflect-metadata@0.2.2)(rxjs@7.8.1))(@nestjs/platform-express@10.4.1)(@nestjs/websockets@10.4.1)(encoding@0.1.13)(reflect-metadata@0.2.2)(rxjs@7.8.1)) + version: 8.33.1(@nestjs/common@10.4.1(class-transformer@0.5.1)(class-validator@0.14.1)(reflect-metadata@0.2.2)(rxjs@7.8.1))(@nestjs/core@10.4.1) '@sentry/node': specifier: ^8.33.1 version: 8.33.1 @@ -1672,7 +1672,7 @@ importers: version: 4.17.21 nest-raven: specifier: 10.1.0 - version: 10.1.0(@nestjs/common@10.4.1(class-transformer@0.5.1)(class-validator@0.14.1)(reflect-metadata@0.2.2)(rxjs@7.8.1))(@nestjs/core@10.4.1(@nestjs/common@10.4.1(class-transformer@0.5.1)(class-validator@0.14.1)(reflect-metadata@0.2.2)(rxjs@7.8.1))(@nestjs/platform-express@10.4.1)(@nestjs/websockets@10.4.1)(encoding@0.1.13)(reflect-metadata@0.2.2)(rxjs@7.8.1))(@sentry/node@8.33.1)(class-transformer@0.5.1)(class-validator@0.14.1)(graphql@16.9.0)(reflect-metadata@0.2.2)(rxjs@7.8.1) + version: 10.1.0(@nestjs/common@10.4.1(class-transformer@0.5.1)(class-validator@0.14.1)(reflect-metadata@0.2.2)(rxjs@7.8.1))(@nestjs/core@10.4.1)(@sentry/node@8.33.1)(class-transformer@0.5.1)(class-validator@0.14.1)(graphql@16.9.0)(reflect-metadata@0.2.2)(rxjs@7.8.1) newrelic: specifier: ^12.8.1 version: 12.8.2 @@ -1697,7 +1697,7 @@ importers: version: 10.1.4(chokidar@3.6.0)(typescript@5.6.2) '@nestjs/testing': specifier: 10.4.1 - version: 10.4.1(@nestjs/common@10.4.1(class-transformer@0.5.1)(class-validator@0.14.1)(reflect-metadata@0.2.2)(rxjs@7.8.1))(@nestjs/core@10.4.1(@nestjs/common@10.4.1(class-transformer@0.5.1)(class-validator@0.14.1)(reflect-metadata@0.2.2)(rxjs@7.8.1))(@nestjs/platform-express@10.4.1)(@nestjs/websockets@10.4.1)(encoding@0.1.13)(reflect-metadata@0.2.2)(rxjs@7.8.1))(@nestjs/platform-express@10.4.1(@nestjs/common@10.4.1(class-transformer@0.5.1)(class-validator@0.14.1)(reflect-metadata@0.2.2)(rxjs@7.8.1))(@nestjs/core@10.4.1)) + version: 10.4.1(@nestjs/common@10.4.1(class-transformer@0.5.1)(class-validator@0.14.1)(reflect-metadata@0.2.2)(rxjs@7.8.1))(@nestjs/core@10.4.1)(@nestjs/platform-express@10.4.1) '@types/chai': specifier: ^4.3.4 version: 4.3.4 @@ -1914,10 +1914,10 @@ importers: version: 4.1.0(less@4.1.3)(webpack@5.78.0(@swc/core@1.3.107(@swc/helpers@0.5.12))) react-app-rewired: specifier: ^2.2.1 - version: 2.2.1(react-scripts@5.0.1(@babel/plugin-syntax-flow@7.24.7(@babel/core@7.25.2))(@babel/plugin-transform-react-jsx@7.25.2(@babel/core@7.25.2))(@swc/core@1.3.107(@swc/helpers@0.5.12))(@types/babel__core@7.20.5)(@types/webpack@5.28.5(@swc/core@1.3.107(@swc/helpers@0.5.12)))(eslint-import-resolver-webpack@0.13.8(eslint-plugin-import@2.29.1)(webpack@5.94.0(@swc/core@1.3.107(@swc/helpers@0.5.12))))(eslint@9.9.1(jiti@1.21.6))(react@18.3.1)(sass@1.77.8)(ts-node@10.9.1(@swc/core@1.3.107(@swc/helpers@0.5.12))(@types/node@18.16.9)(typescript@5.6.2))(type-fest@2.19.0)(typescript@5.6.2)(vue-template-compiler@2.7.16)(webpack-hot-middleware@2.26.1)) + version: 2.2.1(react-scripts@5.0.1(@babel/plugin-syntax-flow@7.24.7(@babel/core@7.25.2))(@babel/plugin-transform-react-jsx@7.25.2(@babel/core@7.25.2))(@swc/core@1.3.107(@swc/helpers@0.5.12))(@types/babel__core@7.20.5)(@types/webpack@5.28.5(@swc/core@1.3.107(@swc/helpers@0.5.12)))(eslint-import-resolver-webpack@0.13.8)(eslint@9.9.1(jiti@1.21.6))(react@18.3.1)(sass@1.77.8)(ts-node@10.9.1(@swc/core@1.3.107(@swc/helpers@0.5.12))(@types/node@18.16.9)(typescript@5.6.2))(type-fest@2.19.0)(typescript@5.6.2)(vue-template-compiler@2.7.16)(webpack-hot-middleware@2.26.1)) react-scripts: specifier: ^5.0.1 - version: 5.0.1(@babel/plugin-syntax-flow@7.24.7(@babel/core@7.25.2))(@babel/plugin-transform-react-jsx@7.25.2(@babel/core@7.25.2))(@swc/core@1.3.107(@swc/helpers@0.5.12))(@types/babel__core@7.20.5)(@types/webpack@5.28.5(@swc/core@1.3.107(@swc/helpers@0.5.12)))(eslint-import-resolver-webpack@0.13.8(eslint-plugin-import@2.29.1)(webpack@5.94.0(@swc/core@1.3.107(@swc/helpers@0.5.12))))(eslint@9.9.1(jiti@1.21.6))(react@18.3.1)(sass@1.77.8)(ts-node@10.9.1(@swc/core@1.3.107(@swc/helpers@0.5.12))(@types/node@18.16.9)(typescript@5.6.2))(type-fest@2.19.0)(typescript@5.6.2)(vue-template-compiler@2.7.16)(webpack-hot-middleware@2.26.1) + version: 5.0.1(@babel/plugin-syntax-flow@7.24.7(@babel/core@7.25.2))(@babel/plugin-transform-react-jsx@7.25.2(@babel/core@7.25.2))(@swc/core@1.3.107(@swc/helpers@0.5.12))(@types/babel__core@7.20.5)(@types/webpack@5.28.5(@swc/core@1.3.107(@swc/helpers@0.5.12)))(eslint-import-resolver-webpack@0.13.8)(eslint@9.9.1(jiti@1.21.6))(react@18.3.1)(sass@1.77.8)(ts-node@10.9.1(@swc/core@1.3.107(@swc/helpers@0.5.12))(@types/node@18.16.9)(typescript@5.6.2))(type-fest@2.19.0)(typescript@5.6.2)(vue-template-compiler@2.7.16)(webpack-hot-middleware@2.26.1) typescript: specifier: 5.6.2 version: 5.6.2 @@ -1947,13 +1947,13 @@ importers: version: 10.4.1(@nestjs/common@10.4.1(class-transformer@0.5.1)(class-validator@0.14.1)(reflect-metadata@0.2.2)(rxjs@7.8.1))(@nestjs/core@10.4.1) '@nestjs/schedule': specifier: ^4.1.1 - version: 4.1.1(@nestjs/common@10.4.1(class-transformer@0.5.1)(class-validator@0.14.1)(reflect-metadata@0.2.2)(rxjs@7.8.1))(@nestjs/core@10.4.1(@nestjs/common@10.4.1(class-transformer@0.5.1)(class-validator@0.14.1)(reflect-metadata@0.2.2)(rxjs@7.8.1))(@nestjs/platform-express@10.4.1)(@nestjs/websockets@10.4.1)(encoding@0.1.13)(reflect-metadata@0.2.2)(rxjs@7.8.1)) + version: 4.1.1(@nestjs/common@10.4.1(class-transformer@0.5.1)(class-validator@0.14.1)(reflect-metadata@0.2.2)(rxjs@7.8.1))(@nestjs/core@10.4.1) '@nestjs/swagger': specifier: 7.4.0 - version: 7.4.0(@nestjs/common@10.4.1(class-transformer@0.5.1)(class-validator@0.14.1)(reflect-metadata@0.2.2)(rxjs@7.8.1))(@nestjs/core@10.4.1(@nestjs/common@10.4.1(class-transformer@0.5.1)(class-validator@0.14.1)(reflect-metadata@0.2.2)(rxjs@7.8.1))(@nestjs/platform-express@10.4.1)(@nestjs/websockets@10.4.1)(encoding@0.1.13)(reflect-metadata@0.2.2)(rxjs@7.8.1))(class-transformer@0.5.1)(class-validator@0.14.1)(reflect-metadata@0.2.2) + version: 7.4.0(@nestjs/common@10.4.1(class-transformer@0.5.1)(class-validator@0.14.1)(reflect-metadata@0.2.2)(rxjs@7.8.1))(@nestjs/core@10.4.1)(class-transformer@0.5.1)(class-validator@0.14.1)(reflect-metadata@0.2.2) '@nestjs/terminus': specifier: 10.2.3 - version: 10.2.3(@grpc/grpc-js@1.12.4)(@grpc/proto-loader@0.7.13)(@nestjs/axios@3.0.3(@nestjs/common@10.4.1(class-transformer@0.5.1)(class-validator@0.14.1)(reflect-metadata@0.2.2)(rxjs@7.8.1))(axios@1.6.8)(rxjs@7.8.1))(@nestjs/common@10.4.1(class-transformer@0.5.1)(class-validator@0.14.1)(reflect-metadata@0.2.2)(rxjs@7.8.1))(@nestjs/core@10.4.1(@nestjs/common@10.4.1(class-transformer@0.5.1)(class-validator@0.14.1)(reflect-metadata@0.2.2)(rxjs@7.8.1))(@nestjs/platform-express@10.4.1)(@nestjs/websockets@10.4.1)(encoding@0.1.13)(reflect-metadata@0.2.2)(rxjs@7.8.1))(mongoose@8.6.0(@aws-sdk/credential-providers@3.637.0(@aws-sdk/client-sso-oidc@3.726.0(@aws-sdk/client-sts@3.726.0)))(gcp-metadata@5.3.0(encoding@0.1.13))(socks@2.7.1))(reflect-metadata@0.2.2)(rxjs@7.8.1) + version: 10.2.3(@grpc/grpc-js@1.12.4)(@grpc/proto-loader@0.7.13)(@nestjs/axios@3.0.3(@nestjs/common@10.4.1(class-transformer@0.5.1)(class-validator@0.14.1)(reflect-metadata@0.2.2)(rxjs@7.8.1))(axios@1.6.8)(rxjs@7.8.1))(@nestjs/common@10.4.1(class-transformer@0.5.1)(class-validator@0.14.1)(reflect-metadata@0.2.2)(rxjs@7.8.1))(@nestjs/core@10.4.1)(mongoose@8.6.0(@aws-sdk/credential-providers@3.637.0(@aws-sdk/client-sso-oidc@3.726.0(@aws-sdk/client-sts@3.726.0)))(gcp-metadata@5.3.0(encoding@0.1.13))(socks@2.7.1))(reflect-metadata@0.2.2)(rxjs@7.8.1) '@novu/application-generic': specifier: workspace:* version: link:../../libs/application-generic @@ -1980,7 +1980,7 @@ importers: version: 7.114.0 '@sentry/nestjs': specifier: ^8.33.1 - version: 8.33.1(@nestjs/common@10.4.1(class-transformer@0.5.1)(class-validator@0.14.1)(reflect-metadata@0.2.2)(rxjs@7.8.1))(@nestjs/core@10.4.1(@nestjs/common@10.4.1(class-transformer@0.5.1)(class-validator@0.14.1)(reflect-metadata@0.2.2)(rxjs@7.8.1))(@nestjs/platform-express@10.4.1)(@nestjs/websockets@10.4.1)(encoding@0.1.13)(reflect-metadata@0.2.2)(rxjs@7.8.1)) + version: 8.33.1(@nestjs/common@10.4.1(class-transformer@0.5.1)(class-validator@0.14.1)(reflect-metadata@0.2.2)(rxjs@7.8.1))(@nestjs/core@10.4.1) '@sentry/node': specifier: ^8.33.1 version: 8.33.1 @@ -2037,7 +2037,7 @@ importers: version: 4.17.21 nest-raven: specifier: 10.1.0 - version: 10.1.0(@nestjs/common@10.4.1(class-transformer@0.5.1)(class-validator@0.14.1)(reflect-metadata@0.2.2)(rxjs@7.8.1))(@nestjs/core@10.4.1(@nestjs/common@10.4.1(class-transformer@0.5.1)(class-validator@0.14.1)(reflect-metadata@0.2.2)(rxjs@7.8.1))(@nestjs/platform-express@10.4.1)(@nestjs/websockets@10.4.1)(encoding@0.1.13)(reflect-metadata@0.2.2)(rxjs@7.8.1))(@sentry/node@8.33.1)(class-transformer@0.5.1)(class-validator@0.14.1)(graphql@16.9.0)(reflect-metadata@0.2.2)(rxjs@7.8.1) + version: 10.1.0(@nestjs/common@10.4.1(class-transformer@0.5.1)(class-validator@0.14.1)(reflect-metadata@0.2.2)(rxjs@7.8.1))(@nestjs/core@10.4.1)(@sentry/node@8.33.1)(class-transformer@0.5.1)(class-validator@0.14.1)(graphql@16.9.0)(reflect-metadata@0.2.2)(rxjs@7.8.1) newrelic: specifier: ^12.8.1 version: 12.8.2 @@ -2081,7 +2081,7 @@ importers: version: 10.1.4(chokidar@3.6.0)(typescript@5.6.2) '@nestjs/testing': specifier: 10.4.1 - version: 10.4.1(@nestjs/common@10.4.1(class-transformer@0.5.1)(class-validator@0.14.1)(reflect-metadata@0.2.2)(rxjs@7.8.1))(@nestjs/core@10.4.1(@nestjs/common@10.4.1(class-transformer@0.5.1)(class-validator@0.14.1)(reflect-metadata@0.2.2)(rxjs@7.8.1))(@nestjs/platform-express@10.4.1)(@nestjs/websockets@10.4.1)(encoding@0.1.13)(reflect-metadata@0.2.2)(rxjs@7.8.1))(@nestjs/platform-express@10.4.1(@nestjs/common@10.4.1(class-transformer@0.5.1)(class-validator@0.14.1)(reflect-metadata@0.2.2)(rxjs@7.8.1))(@nestjs/core@10.4.1)) + version: 10.4.1(@nestjs/common@10.4.1(class-transformer@0.5.1)(class-validator@0.14.1)(reflect-metadata@0.2.2)(rxjs@7.8.1))(@nestjs/core@10.4.1)(@nestjs/platform-express@10.4.1) '@types/bcrypt': specifier: ^3.0.0 version: 3.0.1 @@ -2159,13 +2159,13 @@ importers: version: 10.4.1(@nestjs/common@10.4.1(class-transformer@0.5.1)(class-validator@0.14.1)(reflect-metadata@0.2.2)(rxjs@7.8.1))(@nestjs/websockets@10.4.1)(rxjs@7.8.1) '@nestjs/serve-static': specifier: 4.0.2 - version: 4.0.2(@nestjs/common@10.4.1(class-transformer@0.5.1)(class-validator@0.14.1)(reflect-metadata@0.2.2)(rxjs@7.8.1))(@nestjs/core@10.4.1(@nestjs/common@10.4.1(class-transformer@0.5.1)(class-validator@0.14.1)(reflect-metadata@0.2.2)(rxjs@7.8.1))(@nestjs/platform-express@10.4.1)(@nestjs/websockets@10.4.1)(encoding@0.1.13)(reflect-metadata@0.2.2)(rxjs@7.8.1))(express@4.21.0) + version: 4.0.2(@nestjs/common@10.4.1(class-transformer@0.5.1)(class-validator@0.14.1)(reflect-metadata@0.2.2)(rxjs@7.8.1))(@nestjs/core@10.4.1)(express@4.21.0) '@nestjs/swagger': specifier: 7.4.0 - version: 7.4.0(@nestjs/common@10.4.1(class-transformer@0.5.1)(class-validator@0.14.1)(reflect-metadata@0.2.2)(rxjs@7.8.1))(@nestjs/core@10.4.1(@nestjs/common@10.4.1(class-transformer@0.5.1)(class-validator@0.14.1)(reflect-metadata@0.2.2)(rxjs@7.8.1))(@nestjs/platform-express@10.4.1)(@nestjs/websockets@10.4.1)(encoding@0.1.13)(reflect-metadata@0.2.2)(rxjs@7.8.1))(class-transformer@0.5.1)(class-validator@0.14.1)(reflect-metadata@0.2.2) + version: 7.4.0(@nestjs/common@10.4.1(class-transformer@0.5.1)(class-validator@0.14.1)(reflect-metadata@0.2.2)(rxjs@7.8.1))(@nestjs/core@10.4.1)(class-transformer@0.5.1)(class-validator@0.14.1)(reflect-metadata@0.2.2) '@nestjs/terminus': specifier: 10.2.3 - version: 10.2.3(@grpc/grpc-js@1.12.4)(@grpc/proto-loader@0.7.13)(@nestjs/axios@3.0.3(@nestjs/common@10.4.1(class-transformer@0.5.1)(class-validator@0.14.1)(reflect-metadata@0.2.2)(rxjs@7.8.1))(axios@1.7.7)(rxjs@7.8.1))(@nestjs/common@10.4.1(class-transformer@0.5.1)(class-validator@0.14.1)(reflect-metadata@0.2.2)(rxjs@7.8.1))(@nestjs/core@10.4.1(@nestjs/common@10.4.1(class-transformer@0.5.1)(class-validator@0.14.1)(reflect-metadata@0.2.2)(rxjs@7.8.1))(@nestjs/platform-express@10.4.1)(@nestjs/websockets@10.4.1)(encoding@0.1.13)(reflect-metadata@0.2.2)(rxjs@7.8.1))(mongoose@8.6.0(@aws-sdk/credential-providers@3.637.0(@aws-sdk/client-sso-oidc@3.726.0(@aws-sdk/client-sts@3.726.0)))(gcp-metadata@5.3.0(encoding@0.1.13))(socks@2.7.1))(reflect-metadata@0.2.2)(rxjs@7.8.1) + version: 10.2.3(@grpc/grpc-js@1.12.4)(@grpc/proto-loader@0.7.13)(@nestjs/axios@3.0.3(@nestjs/common@10.4.1(class-transformer@0.5.1)(class-validator@0.14.1)(reflect-metadata@0.2.2)(rxjs@7.8.1))(axios@1.7.7)(rxjs@7.8.1))(@nestjs/common@10.4.1(class-transformer@0.5.1)(class-validator@0.14.1)(reflect-metadata@0.2.2)(rxjs@7.8.1))(@nestjs/core@10.4.1)(mongoose@8.6.0(@aws-sdk/credential-providers@3.637.0(@aws-sdk/client-sso-oidc@3.726.0(@aws-sdk/client-sts@3.726.0)))(gcp-metadata@5.3.0(encoding@0.1.13))(socks@2.7.1))(reflect-metadata@0.2.2)(rxjs@7.8.1) '@nestjs/websockets': specifier: 10.4.1 version: 10.4.1(@nestjs/common@10.4.1(class-transformer@0.5.1)(class-validator@0.14.1)(reflect-metadata@0.2.2)(rxjs@7.8.1))(@nestjs/core@10.4.1)(@nestjs/platform-socket.io@10.4.1)(reflect-metadata@0.2.2)(rxjs@7.8.1) @@ -2189,7 +2189,7 @@ importers: version: 7.114.0 '@sentry/nestjs': specifier: ^8.33.1 - version: 8.33.1(@nestjs/common@10.4.1(class-transformer@0.5.1)(class-validator@0.14.1)(reflect-metadata@0.2.2)(rxjs@7.8.1))(@nestjs/core@10.4.1(@nestjs/common@10.4.1(class-transformer@0.5.1)(class-validator@0.14.1)(reflect-metadata@0.2.2)(rxjs@7.8.1))(@nestjs/platform-express@10.4.1)(@nestjs/websockets@10.4.1)(encoding@0.1.13)(reflect-metadata@0.2.2)(rxjs@7.8.1)) + version: 8.33.1(@nestjs/common@10.4.1(class-transformer@0.5.1)(class-validator@0.14.1)(reflect-metadata@0.2.2)(rxjs@7.8.1))(@nestjs/core@10.4.1) '@sentry/node': specifier: ^8.33.1 version: 8.33.1 @@ -2231,7 +2231,7 @@ importers: version: 4.17.21 nest-raven: specifier: 10.1.0 - version: 10.1.0(@nestjs/common@10.4.1(class-transformer@0.5.1)(class-validator@0.14.1)(reflect-metadata@0.2.2)(rxjs@7.8.1))(@nestjs/core@10.4.1(@nestjs/common@10.4.1(class-transformer@0.5.1)(class-validator@0.14.1)(reflect-metadata@0.2.2)(rxjs@7.8.1))(@nestjs/platform-express@10.4.1)(@nestjs/websockets@10.4.1)(encoding@0.1.13)(reflect-metadata@0.2.2)(rxjs@7.8.1))(@sentry/node@8.33.1)(class-transformer@0.5.1)(class-validator@0.14.1)(graphql@16.9.0)(reflect-metadata@0.2.2)(rxjs@7.8.1) + version: 10.1.0(@nestjs/common@10.4.1(class-transformer@0.5.1)(class-validator@0.14.1)(reflect-metadata@0.2.2)(rxjs@7.8.1))(@nestjs/core@10.4.1)(@sentry/node@8.33.1)(class-transformer@0.5.1)(class-validator@0.14.1)(graphql@16.9.0)(reflect-metadata@0.2.2)(rxjs@7.8.1) newrelic: specifier: ^12.8.1 version: 12.8.2 @@ -2259,7 +2259,7 @@ importers: version: 10.1.4(chokidar@3.6.0)(typescript@5.6.2) '@nestjs/testing': specifier: 10.4.1 - version: 10.4.1(@nestjs/common@10.4.1(class-transformer@0.5.1)(class-validator@0.14.1)(reflect-metadata@0.2.2)(rxjs@7.8.1))(@nestjs/core@10.4.1(@nestjs/common@10.4.1(class-transformer@0.5.1)(class-validator@0.14.1)(reflect-metadata@0.2.2)(rxjs@7.8.1))(@nestjs/platform-express@10.4.1)(@nestjs/websockets@10.4.1)(encoding@0.1.13)(reflect-metadata@0.2.2)(rxjs@7.8.1))(@nestjs/platform-express@10.4.1(@nestjs/common@10.4.1(class-transformer@0.5.1)(class-validator@0.14.1)(reflect-metadata@0.2.2)(rxjs@7.8.1))(@nestjs/core@10.4.1)) + version: 10.4.1(@nestjs/common@10.4.1(class-transformer@0.5.1)(class-validator@0.14.1)(reflect-metadata@0.2.2)(rxjs@7.8.1))(@nestjs/core@10.4.1)(@nestjs/platform-express@10.4.1) '@types/chai': specifier: ^4.2.11 version: 4.3.4 @@ -2315,8 +2315,8 @@ importers: specifier: ^1.6.2 version: 1.6.2(react-dom@18.3.1(react@18.3.1))(react@18.3.1) '@clerk/clerk-sdk-node': - specifier: ^5.0.19 - version: 5.0.19(react-dom@18.3.1(react@18.3.1))(react@18.3.1) + specifier: ^5.1.6 + version: 5.1.6(react-dom@18.3.1(react@18.3.1))(react@18.3.1) '@nestjs/common': specifier: 10.4.1 version: 10.4.1(class-transformer@0.5.1)(class-validator@0.14.1)(reflect-metadata@0.2.2)(rxjs@7.8.1) @@ -2331,7 +2331,7 @@ importers: version: 10.0.3(@nestjs/common@10.4.1(class-transformer@0.5.1)(class-validator@0.14.1)(reflect-metadata@0.2.2)(rxjs@7.8.1))(passport@0.7.0) '@nestjs/swagger': specifier: 7.4.0 - version: 7.4.0(@nestjs/common@10.4.1(class-transformer@0.5.1)(class-validator@0.14.1)(reflect-metadata@0.2.2)(rxjs@7.8.1))(@nestjs/core@10.4.1(@nestjs/common@10.4.1(class-transformer@0.5.1)(class-validator@0.14.1)(reflect-metadata@0.2.2)(rxjs@7.8.1))(@nestjs/platform-express@10.4.1)(@nestjs/websockets@10.4.1)(encoding@0.1.13)(reflect-metadata@0.2.2)(rxjs@7.8.1))(class-transformer@0.5.1)(class-validator@0.14.1)(reflect-metadata@0.2.2) + version: 7.4.0(@nestjs/common@10.4.1(class-transformer@0.5.1)(class-validator@0.14.1)(reflect-metadata@0.2.2)(rxjs@7.8.1))(@nestjs/core@10.4.1)(class-transformer@0.5.1)(class-validator@0.14.1)(reflect-metadata@0.2.2) '@novu/application-generic': specifier: workspace:* version: link:../../../libs/application-generic @@ -2413,10 +2413,10 @@ importers: version: 10.4.1(@nestjs/common@10.4.1(class-transformer@0.5.1)(class-validator@0.14.1)(reflect-metadata@0.2.2)(rxjs@7.8.1))(@nestjs/core@10.4.1) '@nestjs/swagger': specifier: 7.4.0 - version: 7.4.0(@nestjs/common@10.4.1(class-transformer@0.5.1)(class-validator@0.14.1)(reflect-metadata@0.2.2)(rxjs@7.8.1))(@nestjs/core@10.4.1(@nestjs/common@10.4.1(class-transformer@0.5.1)(class-validator@0.14.1)(reflect-metadata@0.2.2)(rxjs@7.8.1))(@nestjs/platform-express@10.4.1)(@nestjs/websockets@10.4.1)(encoding@0.1.13)(reflect-metadata@0.2.2)(rxjs@7.8.1))(class-transformer@0.5.1)(class-validator@0.14.1)(reflect-metadata@0.2.2) + version: 7.4.0(@nestjs/common@10.4.1(class-transformer@0.5.1)(class-validator@0.14.1)(reflect-metadata@0.2.2)(rxjs@7.8.1))(@nestjs/core@10.4.1)(class-transformer@0.5.1)(class-validator@0.14.1)(reflect-metadata@0.2.2) '@nestjs/throttler': specifier: 6.2.1 - version: 6.2.1(@nestjs/common@10.4.1(class-transformer@0.5.1)(class-validator@0.14.1)(reflect-metadata@0.2.2)(rxjs@7.8.1))(@nestjs/core@10.4.1(@nestjs/common@10.4.1(class-transformer@0.5.1)(class-validator@0.14.1)(reflect-metadata@0.2.2)(rxjs@7.8.1))(@nestjs/platform-express@10.4.1)(@nestjs/websockets@10.4.1)(encoding@0.1.13)(reflect-metadata@0.2.2)(rxjs@7.8.1))(reflect-metadata@0.2.2) + version: 6.2.1(@nestjs/common@10.4.1(class-transformer@0.5.1)(class-validator@0.14.1)(reflect-metadata@0.2.2)(rxjs@7.8.1))(@nestjs/core@10.4.1)(reflect-metadata@0.2.2) '@novu/application-generic': specifier: workspace:* version: link:../../../libs/application-generic @@ -2575,7 +2575,7 @@ importers: version: 10.4.1(@nestjs/common@10.4.1(class-transformer@0.5.1)(class-validator@0.14.1)(reflect-metadata@0.2.2)(rxjs@7.8.1))(@nestjs/core@10.4.1) '@nestjs/swagger': specifier: 7.4.0 - version: 7.4.0(@nestjs/common@10.4.1(class-transformer@0.5.1)(class-validator@0.14.1)(reflect-metadata@0.2.2)(rxjs@7.8.1))(@nestjs/core@10.4.1(@nestjs/common@10.4.1(class-transformer@0.5.1)(class-validator@0.14.1)(reflect-metadata@0.2.2)(rxjs@7.8.1))(@nestjs/platform-express@10.4.1)(@nestjs/websockets@10.4.1)(encoding@0.1.13)(reflect-metadata@0.2.2)(rxjs@7.8.1))(class-transformer@0.5.1)(class-validator@0.14.1)(reflect-metadata@0.2.2) + version: 7.4.0(@nestjs/common@10.4.1(class-transformer@0.5.1)(class-validator@0.14.1)(reflect-metadata@0.2.2)(rxjs@7.8.1))(@nestjs/core@10.4.1)(class-transformer@0.5.1)(class-validator@0.14.1)(reflect-metadata@0.2.2) '@novu/application-generic': specifier: workspace:* version: link:../../../libs/application-generic @@ -2640,9 +2640,6 @@ importers: libs/application-generic: dependencies: - '@aws-sdk/client-cloudwatch': - specifier: ^3.567.0 - version: 3.575.0 '@aws-sdk/client-s3': specifier: ^3.567.0 version: 3.575.0 @@ -2675,13 +2672,13 @@ importers: version: 10.0.3(@nestjs/common@10.4.1(class-transformer@0.5.1)(class-validator@0.14.1)(reflect-metadata@0.2.2)(rxjs@7.8.1))(passport@0.7.0) '@nestjs/swagger': specifier: 7.4.0 - version: 7.4.0(@nestjs/common@10.4.1(class-transformer@0.5.1)(class-validator@0.14.1)(reflect-metadata@0.2.2)(rxjs@7.8.1))(@nestjs/core@10.4.1(@nestjs/common@10.4.1(class-transformer@0.5.1)(class-validator@0.14.1)(reflect-metadata@0.2.2)(rxjs@7.8.1))(@nestjs/platform-express@10.4.1)(@nestjs/websockets@10.4.1)(encoding@0.1.13)(reflect-metadata@0.2.2)(rxjs@7.8.1))(class-transformer@0.5.1)(class-validator@0.14.1)(reflect-metadata@0.2.2) + version: 7.4.0(@nestjs/common@10.4.1(class-transformer@0.5.1)(class-validator@0.14.1)(reflect-metadata@0.2.2)(rxjs@7.8.1))(@nestjs/core@10.4.1)(class-transformer@0.5.1)(class-validator@0.14.1)(reflect-metadata@0.2.2) '@nestjs/terminus': specifier: 10.2.3 - version: 10.2.3(@grpc/grpc-js@1.12.4)(@grpc/proto-loader@0.7.13)(@nestjs/axios@3.0.3(@nestjs/common@10.4.1(class-transformer@0.5.1)(class-validator@0.14.1)(reflect-metadata@0.2.2)(rxjs@7.8.1))(axios@1.6.8)(rxjs@7.8.1))(@nestjs/common@10.4.1(class-transformer@0.5.1)(class-validator@0.14.1)(reflect-metadata@0.2.2)(rxjs@7.8.1))(@nestjs/core@10.4.1(@nestjs/common@10.4.1(class-transformer@0.5.1)(class-validator@0.14.1)(reflect-metadata@0.2.2)(rxjs@7.8.1))(@nestjs/platform-express@10.4.1)(@nestjs/websockets@10.4.1)(encoding@0.1.13)(reflect-metadata@0.2.2)(rxjs@7.8.1))(mongoose@8.6.0(@aws-sdk/credential-providers@3.637.0(@aws-sdk/client-sso-oidc@3.575.0))(gcp-metadata@5.3.0(encoding@0.1.13))(socks@2.7.1))(reflect-metadata@0.2.2)(rxjs@7.8.1) + version: 10.2.3(@grpc/grpc-js@1.12.4)(@grpc/proto-loader@0.7.13)(@nestjs/axios@3.0.3(@nestjs/common@10.4.1(class-transformer@0.5.1)(class-validator@0.14.1)(reflect-metadata@0.2.2)(rxjs@7.8.1))(axios@1.6.8)(rxjs@7.8.1))(@nestjs/common@10.4.1(class-transformer@0.5.1)(class-validator@0.14.1)(reflect-metadata@0.2.2)(rxjs@7.8.1))(@nestjs/core@10.4.1)(mongoose@8.6.0(@aws-sdk/credential-providers@3.637.0(@aws-sdk/client-sso-oidc@3.575.0))(gcp-metadata@5.3.0(encoding@0.1.13))(socks@2.7.1))(reflect-metadata@0.2.2)(rxjs@7.8.1) '@nestjs/testing': specifier: 10.4.1 - version: 10.4.1(@nestjs/common@10.4.1(class-transformer@0.5.1)(class-validator@0.14.1)(reflect-metadata@0.2.2)(rxjs@7.8.1))(@nestjs/core@10.4.1(@nestjs/common@10.4.1(class-transformer@0.5.1)(class-validator@0.14.1)(reflect-metadata@0.2.2)(rxjs@7.8.1))(@nestjs/platform-express@10.4.1)(@nestjs/websockets@10.4.1)(encoding@0.1.13)(reflect-metadata@0.2.2)(rxjs@7.8.1))(@nestjs/platform-express@10.4.1(@nestjs/common@10.4.1(class-transformer@0.5.1)(class-validator@0.14.1)(reflect-metadata@0.2.2)(rxjs@7.8.1))(@nestjs/core@10.4.1)) + version: 10.4.1(@nestjs/common@10.4.1(class-transformer@0.5.1)(class-validator@0.14.1)(reflect-metadata@0.2.2)(rxjs@7.8.1))(@nestjs/core@10.4.1)(@nestjs/platform-express@10.4.1) '@novu/dal': specifier: workspace:* version: link:../dal @@ -2807,7 +2804,7 @@ importers: version: 3.3.7 nestjs-otel: specifier: 6.1.1 - version: 6.1.1(@nestjs/common@10.4.1(class-transformer@0.5.1)(class-validator@0.14.1)(reflect-metadata@0.2.2)(rxjs@7.8.1))(@nestjs/core@10.4.1(@nestjs/common@10.4.1(class-transformer@0.5.1)(class-validator@0.14.1)(reflect-metadata@0.2.2)(rxjs@7.8.1))(@nestjs/platform-express@10.4.1)(@nestjs/websockets@10.4.1)(encoding@0.1.13)(reflect-metadata@0.2.2)(rxjs@7.8.1)) + version: 6.1.1(@nestjs/common@10.4.1(class-transformer@0.5.1)(class-validator@0.14.1)(reflect-metadata@0.2.2)(rxjs@7.8.1))(@nestjs/core@10.4.1) nestjs-pino: specifier: 4.1.0 version: 4.1.0(@nestjs/common@10.4.1(class-transformer@0.5.1)(class-validator@0.14.1)(reflect-metadata@0.2.2)(rxjs@7.8.1))(pino-http@8.3.3) @@ -3465,8 +3462,8 @@ importers: specifier: ^1.6.2 version: 1.6.2(react-dom@18.3.1(react@18.3.1))(react@18.3.1) '@clerk/clerk-sdk-node': - specifier: ^5.0.19 - version: 5.0.19(react-dom@18.3.1(react@18.3.1))(react@18.3.1) + specifier: ^5.1.6 + version: 5.1.6(react-dom@18.3.1(react@18.3.1))(react@18.3.1) '@clerk/types': specifier: ^4.6.1 version: 4.6.1 @@ -3780,7 +3777,7 @@ importers: version: 5.3.0 compression-webpack-plugin: specifier: ^10.0.0 - version: 10.0.0(webpack@5.78.0(@swc/core@1.7.26(@swc/helpers@0.5.12))(esbuild@0.23.1)(webpack-cli@5.1.4)) + version: 10.0.0(webpack@5.78.0) concurrently: specifier: ^5.3.0 version: 5.3.0 @@ -3828,7 +3825,7 @@ importers: version: 1.0.7(tailwindcss@3.4.4(ts-node@10.9.2(@swc/core@1.7.26(@swc/helpers@0.5.12))(@types/node@20.16.5)(typescript@5.6.2))) terser-webpack-plugin: specifier: ^5.3.9 - version: 5.3.9(@swc/core@1.7.26(@swc/helpers@0.5.12))(esbuild@0.23.1)(webpack@5.78.0(@swc/core@1.7.26(@swc/helpers@0.5.12))(esbuild@0.23.1)(webpack-cli@5.1.4)) + version: 5.3.9(@swc/core@1.7.26(@swc/helpers@0.5.12))(esbuild@0.23.1)(webpack@5.78.0) tiny-glob: specifier: ^0.2.9 version: 0.2.9 @@ -3837,7 +3834,7 @@ importers: version: 29.1.2(@babel/core@7.25.2)(@jest/types@29.6.3)(babel-jest@29.7.0(@babel/core@7.25.2))(esbuild@0.23.1)(jest@29.7.0(@types/node@20.16.5)(babel-plugin-macros@3.1.0)(ts-node@10.9.2(@swc/core@1.7.26(@swc/helpers@0.5.12))(@types/node@20.16.5)(typescript@5.6.2)))(typescript@5.6.2) ts-loader: specifier: ~9.4.0 - version: 9.4.4(typescript@5.6.2)(webpack@5.78.0(@swc/core@1.7.26(@swc/helpers@0.5.12))(esbuild@0.23.1)(webpack-cli@5.1.4)) + version: 9.4.4(typescript@5.6.2)(webpack@5.78.0) tsup: specifier: ^8.1.0 version: 8.1.0(@microsoft/api-extractor@7.47.7(@types/node@20.16.5))(@swc/core@1.7.26(@swc/helpers@0.5.12))(postcss@8.4.38)(ts-node@10.9.2(@swc/core@1.7.26(@swc/helpers@0.5.12))(@types/node@20.16.5)(typescript@5.6.2))(typescript@5.6.2) @@ -4099,7 +4096,7 @@ importers: version: 7.4.2(encoding@0.1.13)(react-dom@18.3.1(react@18.3.1))(react@18.3.1)(typescript@5.6.2) '@storybook/react-webpack5': specifier: ^7.4.2 - version: 7.4.2(@babel/core@7.25.2)(@swc/core@1.7.26(@swc/helpers@0.5.12))(@swc/helpers@0.5.12)(@types/react-dom@18.3.0)(@types/react@18.3.3)(@types/webpack@5.28.5(@swc/core@1.7.26(@swc/helpers@0.5.12))(webpack-cli@5.1.4(webpack-bundle-analyzer@4.9.0)(webpack@5.78.0)))(encoding@0.1.13)(react-dom@18.3.1(react@18.3.1))(react@18.3.1)(type-fest@2.19.0)(typescript@5.6.2)(webpack-cli@5.1.4(webpack-bundle-analyzer@4.9.0)(webpack@5.78.0))(webpack-hot-middleware@2.26.1) + version: 7.4.2(@babel/core@7.25.2)(@swc/core@1.7.26(@swc/helpers@0.5.12))(@swc/helpers@0.5.12)(@types/react-dom@18.3.0)(@types/react@18.3.3)(@types/webpack@5.28.5(@swc/core@1.7.26(@swc/helpers@0.5.12))(webpack-cli@5.1.4))(encoding@0.1.13)(react-dom@18.3.1(react@18.3.1))(react@18.3.1)(type-fest@2.19.0)(typescript@5.6.2)(webpack-cli@5.1.4)(webpack-hot-middleware@2.26.1) '@testing-library/dom': specifier: ^9.3.0 version: 9.3.0 @@ -4132,10 +4129,10 @@ importers: version: 8.8.2 babel-loader: specifier: ^8.2.4 - version: 8.3.0(@babel/core@7.25.2)(webpack@5.78.0(@swc/core@1.7.26(@swc/helpers@0.5.12))(webpack-cli@5.1.4)) + version: 8.3.0(@babel/core@7.25.2)(webpack@5.78.0) compression-webpack-plugin: specifier: ^10.0.0 - version: 10.0.0(webpack@5.78.0(@swc/core@1.7.26(@swc/helpers@0.5.12))(webpack-cli@5.1.4)) + version: 10.0.0(webpack@5.78.0) jest: specifier: ^29.3.1 version: 29.5.0(@types/node@20.16.5)(babel-plugin-macros@3.1.0)(ts-node@10.9.2(@swc/core@1.7.26(@swc/helpers@0.5.12))(@types/node@20.16.5)(typescript@5.6.2)) @@ -4159,19 +4156,19 @@ importers: version: 7.4.2(encoding@0.1.13) terser-webpack-plugin: specifier: ^5.3.9 - version: 5.3.10(@swc/core@1.7.26(@swc/helpers@0.5.12))(webpack@5.78.0(@swc/core@1.7.26(@swc/helpers@0.5.12))(webpack-cli@5.1.4)) + version: 5.3.10(@swc/core@1.7.26(@swc/helpers@0.5.12))(webpack@5.78.0) ts-jest: specifier: ^29.0.3 version: 29.1.0(@babel/core@7.25.2)(@jest/types@29.6.3)(babel-jest@29.7.0(@babel/core@7.25.2))(jest@29.5.0(@types/node@20.16.5)(babel-plugin-macros@3.1.0)(ts-node@10.9.2(@swc/core@1.7.26(@swc/helpers@0.5.12))(@types/node@20.16.5)(typescript@5.6.2)))(typescript@5.6.2) ts-loader: specifier: ~9.4.0 - version: 9.4.4(typescript@5.6.2)(webpack@5.78.0(@swc/core@1.7.26(@swc/helpers@0.5.12))(webpack-cli@5.1.4)) + version: 9.4.4(typescript@5.6.2)(webpack@5.78.0) typescript: specifier: 5.6.2 version: 5.6.2 url-loader: specifier: ^4.1.1 - version: 4.1.1(file-loader@6.2.0(webpack@5.78.0(@swc/core@1.7.26(@swc/helpers@0.5.12))(webpack-cli@5.1.4)))(webpack@5.78.0(@swc/core@1.7.26(@swc/helpers@0.5.12))(webpack-cli@5.1.4)) + version: 4.1.1(file-loader@6.2.0(webpack@5.78.0))(webpack@5.78.0) webpack: specifier: ^5.74.0 version: 5.78.0(@swc/core@1.7.26(@swc/helpers@0.5.12))(webpack-cli@5.1.4) @@ -4642,7 +4639,7 @@ importers: version: 10.1.4(chokidar@3.6.0)(typescript@5.6.2) '@nestjs/testing': specifier: 10.4.1 - version: 10.4.1(@nestjs/common@10.4.1(class-transformer@0.5.1)(class-validator@0.14.1)(reflect-metadata@0.2.2)(rxjs@7.8.1))(@nestjs/core@10.4.1(@nestjs/common@10.4.1(class-transformer@0.5.1)(class-validator@0.14.1)(reflect-metadata@0.2.2)(rxjs@7.8.1))(@nestjs/platform-express@10.4.1)(@nestjs/websockets@10.4.1)(encoding@0.1.13)(reflect-metadata@0.2.2)(rxjs@7.8.1))(@nestjs/platform-express@10.4.1(@nestjs/common@10.4.1(class-transformer@0.5.1)(class-validator@0.14.1)(reflect-metadata@0.2.2)(rxjs@7.8.1))(@nestjs/core@10.4.1)) + version: 10.4.1(@nestjs/common@10.4.1(class-transformer@0.5.1)(class-validator@0.14.1)(reflect-metadata@0.2.2)(rxjs@7.8.1))(@nestjs/core@10.4.1)(@nestjs/platform-express@10.4.1) '@swc/core': specifier: ^1.7.26 version: 1.7.26(@swc/helpers@0.5.12) @@ -4924,10 +4921,6 @@ packages: '@aws-crypto/util@5.2.0': resolution: {integrity: sha512-4RkU9EsI6ZpBve5fseQlGNUWKMa1RLPQ1dnjnQoe07ldfIzcsGb5hC5W0Dm7u423KWzawlrpbjXBrXCEv9zazQ==} - '@aws-sdk/client-cloudwatch@3.575.0': - resolution: {integrity: sha512-BGwMHWm6hQiGPoIBcPJRcfAjiU1D5qx+tkwf9bhnFTEkKNaPupfDTwz9aUmKDHQTOLyPRKuhGNOuuLNhzipPqA==} - engines: {node: '>=16.0.0'} - '@aws-sdk/client-cognito-identity@3.504.0': resolution: {integrity: sha512-WsQY6CRDC9Y1rKjpsk187EHKES6nLmM9sD6iHAKZFLhi/DiYsy8SIafPFPEvluubYlheeLzgUB8Oxpj6Z69hlA==} engines: {node: '>=14.0.0'} @@ -7224,8 +7217,8 @@ packages: bundledDependencies: - is-unicode-supported - '@clerk/backend@1.4.2': - resolution: {integrity: sha512-6Lc1dTLKmOEZz5CRnHbow0EDdrU4pwuN1SMW/cdQxgGhfWkDjkwgsHSseWq54XL5HQuwAsZBaAeapY2XKRmspw==} + '@clerk/backend@1.22.0': + resolution: {integrity: sha512-nu6dAigcsqMLt2K+seBKb+goWzsKgxylFA9O9vcXPeNKk+rsrFPQVIv/X1Kt5O/IgZNbmRFch4MSF9XU6rqIRw==} engines: {node: '>=18.17.0'} '@clerk/backend@1.6.2': @@ -7239,8 +7232,8 @@ packages: react: ^18 || ^19.0.0-0 react-dom: ^18 || ^19.0.0-0 - '@clerk/clerk-sdk-node@5.0.19': - resolution: {integrity: sha512-LnjXgqRGJpBIhkaBQCLixfuGFZNDDpTUv9uuSj3tp5X6DQbmRSPKX0Vm8WwXJmIr67JYoAE+6N3j/P2StVTZ7w==} + '@clerk/clerk-sdk-node@5.1.6': + resolution: {integrity: sha512-KeF5p0XP0gNoCx+YIHrfrkNNADBz8ZabwPAhOiJZ9Wo14r93WlzRA51IE0Qgteej8IpWwnvKu4/MpiV7FFoLqA==} engines: {node: '>=18.17.0'} '@clerk/shared@2.11.5': @@ -7255,12 +7248,12 @@ packages: react-dom: optional: true - '@clerk/shared@2.4.0': - resolution: {integrity: sha512-2UI0OeRB8IIliiALydLTQjYYj3qhPNb/LU9/3tfBbz/PNbCEZkd18AuYKB9N3zIm2MIHbOSMLt7SoZJKsRB8+A==} + '@clerk/shared@2.20.6': + resolution: {integrity: sha512-DnxXbPPUNQQMOlAQFEn0iAiwPtjO4yKVabSgs+qn86VsNWWHnG//uHJzB6nmDk8n5RYGe/vMum73/Lz/Pg1SgQ==} engines: {node: '>=18.17.0'} peerDependencies: - react: '>=18 || >=19.0.0-beta' - react-dom: '>=18 || >=19.0.0-beta' + react: ^18.0.0 || ^19.0.0 || ^19.0.0-0 + react-dom: ^18.0.0 || ^19.0.0 || ^19.0.0-0 peerDependenciesMeta: react: optional: true @@ -7291,12 +7284,12 @@ packages: resolution: {integrity: sha512-/RLWXsz4yp9uFvJhDZDyZGRDyx3VdHRyPYQS7onhGVTY846X6iCzJVlMFzdpzW3PITxMBgCI9MjgKdH50vBPBA==} engines: {node: '>=18.17.0'} - '@clerk/types@4.6.1': - resolution: {integrity: sha512-QFeNKPYDmTJ88l5QYG0SPwbABk42wRMalW3M/wAtr+wnQxBCXyX2XRZe9h4g2rH1VF+wG4Xe56abeeD+xE4iEw==} + '@clerk/types@4.40.2': + resolution: {integrity: sha512-Jkt8YiqtFSRvCsN+8/MR2V0Cl83xtuqZVuV6ygQ6VJeSbmmFP0q5CSowlJ8mon2bELrlUnWH011dhAjELmvXqg==} engines: {node: '>=18.17.0'} - '@clerk/types@4.9.0': - resolution: {integrity: sha512-t+PDtVItnwit8u+YefMBVfJVdENdRAndMiIwGGJgnZ7+DyPjOwqomVy3VmcyycUdkdHcnx16UKm3a0rh2JQG/w==} + '@clerk/types@4.6.1': + resolution: {integrity: sha512-QFeNKPYDmTJ88l5QYG0SPwbABk42wRMalW3M/wAtr+wnQxBCXyX2XRZe9h4g2rH1VF+wG4Xe56abeeD+xE4iEw==} engines: {node: '>=18.17.0'} '@codemirror/autocomplete@6.18.3': @@ -14679,10 +14672,6 @@ packages: '@smithy/md5-js@3.0.0': resolution: {integrity: sha512-Tm0vrrVzjlD+6RCQTx7D3Ls58S3FUH1ZCtU1MIh/qQmaOo1H9lMN2as6CikcEwgattnA9SURSdoJJ27xMcEfMA==} - '@smithy/middleware-compression@3.0.0': - resolution: {integrity: sha512-ZOIYISXy3cNyy/yddpmVrpo/MR7wPqPgzwwuR9ZpQrTx0PnzLOzESFTj+Vg0Ev6uh/7XhoCbOnJukrq5y48TAw==} - engines: {node: '>=16.0.0'} - '@smithy/middleware-content-length@2.0.3': resolution: {integrity: sha512-2FiZ5vu2+iMRL8XWNaREUqqNHjtBubaY9Jb2b3huZ9EbgrXsJfCszK6PPidHTLe+B4T7AISqdF4ZSp9VPXuelg==} engines: {node: '>=14.0.0'} @@ -21100,6 +21089,10 @@ packages: resolution: {integrity: sha512-yki5XnKuf750l50uGTllt6kKILY4nQ1eNIQatoXEByZ5dWgnKqbnqmTrBE5B4N7lrMJKQ2ytWMiTO2o0v6Ew/w==} engines: {node: '>= 0.6'} + cookie@1.0.2: + resolution: {integrity: sha512-9Kr/j4O16ISv8zBBhJoi4bXOYNTkFLOqSL3UDB0njXxCXNezjeyVrJyGOWtgfs/q2km1gwBcfH8q1yEGoMYunA==} + engines: {node: '>=18'} + cookiejar@2.1.4: resolution: {integrity: sha512-LDx6oHrK+PhzLKJU9j5S7/Y3jM/mUHvD/DeI1WQmJn652iPC5Y4TBzC9l+5OMOXlyTTA+SmVUPm0HQUwpD5Jqw==} @@ -23516,9 +23509,6 @@ packages: fflate@0.4.8: resolution: {integrity: sha512-FJqqoDBR00Mdj9ppamLa/Y7vxm+PRmNWA67N846RvsoYVMKB4q3y/de5PA7gUmRMYK/8CMz2GDZQmCRN1wBcWA==} - fflate@0.8.1: - resolution: {integrity: sha512-/exOvEuc+/iaUm105QIiOt4LpBdMTWsXxqR0HDF35vx3fmaKzw7354gTilCh5rkzEt8WYyG//ku3h3nRmd7CHQ==} - fflate@0.8.2: resolution: {integrity: sha512-cPJU47OaAoCbg0pBvzsgpTPhmhqI5eJjh/JIu8tPj5q+T7iLvW/JAYUqmE7KOB4R1ZyEhzBaIQpQpardBF5z8A==} @@ -32583,6 +32573,10 @@ packages: resolution: {integrity: sha512-YTywJG93yxwHLgrYLZjlC75moVEX04LZM4FHfihjHe1FCXm+QaLOFfSf535aXOAd0ArVQMWUAe8ZPm4VtWyXaA==} engines: {node: '>=12'} + snakecase-keys@8.0.1: + resolution: {integrity: sha512-Sj51kE1zC7zh6TDlNNz0/Jn1n5HiHdoQErxO8jLtnyrkJW/M5PrI7x05uDgY3BO7OUQYKCvmeMurW6BPUdwEOw==} + engines: {node: '>=18'} + snapdragon-node@2.1.1: resolution: {integrity: sha512-O27l4xaMYt/RSQ5TR3vpWCAB5Kb/czIcqUFOM/C4fYcLnbZUc1PkjTAMjof2pBWaSTwOUd6qUHcFGVGj7aIwnw==} engines: {node: '>=0.10.0'} @@ -36366,54 +36360,6 @@ snapshots: '@smithy/util-utf8': 2.3.0 tslib: 2.7.0 - '@aws-sdk/client-cloudwatch@3.575.0': - dependencies: - '@aws-crypto/sha256-browser': 3.0.0 - '@aws-crypto/sha256-js': 3.0.0 - '@aws-sdk/client-sso-oidc': 3.575.0 - '@aws-sdk/client-sts': 3.575.0(@aws-sdk/client-sso-oidc@3.575.0) - '@aws-sdk/core': 3.575.0 - '@aws-sdk/credential-provider-node': 3.575.0(@aws-sdk/client-sso-oidc@3.575.0)(@aws-sdk/client-sts@3.575.0) - '@aws-sdk/middleware-host-header': 3.575.0 - '@aws-sdk/middleware-logger': 3.575.0 - '@aws-sdk/middleware-recursion-detection': 3.575.0 - '@aws-sdk/middleware-user-agent': 3.575.0 - '@aws-sdk/region-config-resolver': 3.575.0 - '@aws-sdk/types': 3.575.0 - '@aws-sdk/util-endpoints': 3.575.0 - '@aws-sdk/util-user-agent-browser': 3.575.0 - '@aws-sdk/util-user-agent-node': 3.575.0 - '@smithy/config-resolver': 3.0.0 - '@smithy/core': 2.0.0 - '@smithy/fetch-http-handler': 3.0.0 - '@smithy/hash-node': 3.0.0 - '@smithy/invalid-dependency': 3.0.0 - '@smithy/middleware-compression': 3.0.0 - '@smithy/middleware-content-length': 3.0.0 - '@smithy/middleware-endpoint': 3.0.0 - '@smithy/middleware-retry': 3.0.0 - '@smithy/middleware-serde': 3.0.0 - '@smithy/middleware-stack': 3.0.0 - '@smithy/node-config-provider': 3.0.0 - '@smithy/node-http-handler': 3.0.0 - '@smithy/protocol-http': 4.0.0 - '@smithy/smithy-client': 3.0.0 - '@smithy/types': 3.0.0 - '@smithy/url-parser': 3.0.0 - '@smithy/util-base64': 3.0.0 - '@smithy/util-body-length-browser': 3.0.0 - '@smithy/util-body-length-node': 3.0.0 - '@smithy/util-defaults-mode-browser': 3.0.0 - '@smithy/util-defaults-mode-node': 3.0.0 - '@smithy/util-endpoints': 2.0.0 - '@smithy/util-middleware': 3.0.0 - '@smithy/util-retry': 3.0.0 - '@smithy/util-utf8': 3.0.0 - '@smithy/util-waiter': 3.0.0 - tslib: 2.6.2 - transitivePeerDependencies: - - aws-crt - '@aws-sdk/client-cognito-identity@3.504.0': dependencies: '@aws-crypto/sha256-browser': 3.0.0 @@ -43750,12 +43696,12 @@ snapshots: picocolors: 1.1.1 sisteransi: 1.0.5 - '@clerk/backend@1.4.2(react-dom@18.3.1(react@18.3.1))(react@18.3.1)': + '@clerk/backend@1.22.0(react-dom@18.3.1(react@18.3.1))(react@18.3.1)': dependencies: - '@clerk/shared': 2.4.0(react-dom@18.3.1(react@18.3.1))(react@18.3.1) - '@clerk/types': 4.9.0 - cookie: 0.5.0 - snakecase-keys: 5.4.4 + '@clerk/shared': 2.20.6(react-dom@18.3.1(react@18.3.1))(react@18.3.1) + '@clerk/types': 4.40.2 + cookie: 1.0.2 + snakecase-keys: 8.0.1 tslib: 2.4.1 transitivePeerDependencies: - react @@ -43780,11 +43726,11 @@ snapshots: react-dom: 18.3.1(react@18.3.1) tslib: 2.4.1 - '@clerk/clerk-sdk-node@5.0.19(react-dom@18.3.1(react@18.3.1))(react@18.3.1)': + '@clerk/clerk-sdk-node@5.1.6(react-dom@18.3.1(react@18.3.1))(react@18.3.1)': dependencies: - '@clerk/backend': 1.4.2(react-dom@18.3.1(react@18.3.1))(react@18.3.1) - '@clerk/shared': 2.4.0(react-dom@18.3.1(react@18.3.1))(react@18.3.1) - '@clerk/types': 4.9.0 + '@clerk/backend': 1.22.0(react-dom@18.3.1(react@18.3.1))(react@18.3.1) + '@clerk/shared': 2.20.6(react-dom@18.3.1(react@18.3.1))(react@18.3.1) + '@clerk/types': 4.40.2 tslib: 2.4.1 transitivePeerDependencies: - react @@ -43801,9 +43747,10 @@ snapshots: react: 18.3.1 react-dom: 18.3.1(react@18.3.1) - '@clerk/shared@2.4.0(react-dom@18.3.1(react@18.3.1))(react@18.3.1)': + '@clerk/shared@2.20.6(react-dom@18.3.1(react@18.3.1))(react@18.3.1)': dependencies: - '@clerk/types': 4.9.0 + '@clerk/types': 4.40.2 + dequal: 2.0.3 glob-to-regexp: 0.4.1 js-cookie: 3.0.5 std-env: 3.7.0 @@ -43836,11 +43783,11 @@ snapshots: dependencies: csstype: 3.1.1 - '@clerk/types@4.6.1': + '@clerk/types@4.40.2': dependencies: csstype: 3.1.1 - '@clerk/types@4.9.0': + '@clerk/types@4.6.1': dependencies: csstype: 3.1.1 @@ -47805,7 +47752,7 @@ snapshots: transitivePeerDependencies: - encoding - '@nestjs/graphql@12.0.9(@nestjs/common@10.4.1(class-transformer@0.5.1)(class-validator@0.14.1)(reflect-metadata@0.2.2)(rxjs@7.8.1))(@nestjs/core@10.4.1(@nestjs/common@10.4.1(class-transformer@0.5.1)(class-validator@0.14.1)(reflect-metadata@0.2.2)(rxjs@7.8.1))(@nestjs/platform-express@10.4.1)(@nestjs/websockets@10.4.1)(encoding@0.1.13)(reflect-metadata@0.2.2)(rxjs@7.8.1))(class-transformer@0.5.1)(class-validator@0.14.1)(graphql@16.9.0)(reflect-metadata@0.2.2)': + '@nestjs/graphql@12.0.9(@nestjs/common@10.4.1(class-transformer@0.5.1)(class-validator@0.14.1)(reflect-metadata@0.2.2)(rxjs@7.8.1))(@nestjs/core@10.4.1)(class-transformer@0.5.1)(class-validator@0.14.1)(graphql@16.9.0)(reflect-metadata@0.2.2)': dependencies: '@graphql-tools/merge': 9.0.0(graphql@16.9.0) '@graphql-tools/schema': 10.0.0(graphql@16.9.0) @@ -47885,7 +47832,7 @@ snapshots: - supports-color - utf-8-validate - '@nestjs/schedule@4.1.1(@nestjs/common@10.4.1(class-transformer@0.5.1)(class-validator@0.14.1)(reflect-metadata@0.2.2)(rxjs@7.8.1))(@nestjs/core@10.4.1(@nestjs/common@10.4.1(class-transformer@0.5.1)(class-validator@0.14.1)(reflect-metadata@0.2.2)(rxjs@7.8.1))(@nestjs/platform-express@10.4.1)(@nestjs/websockets@10.4.1)(encoding@0.1.13)(reflect-metadata@0.2.2)(rxjs@7.8.1))': + '@nestjs/schedule@4.1.1(@nestjs/common@10.4.1(class-transformer@0.5.1)(class-validator@0.14.1)(reflect-metadata@0.2.2)(rxjs@7.8.1))(@nestjs/core@10.4.1)': dependencies: '@nestjs/common': 10.4.1(class-transformer@0.5.1)(class-validator@0.14.1)(reflect-metadata@0.2.2)(rxjs@7.8.1) '@nestjs/core': 10.4.1(@nestjs/common@10.4.1(class-transformer@0.5.1)(class-validator@0.14.1)(reflect-metadata@0.2.2)(rxjs@7.8.1))(@nestjs/platform-express@10.4.1)(@nestjs/websockets@10.4.1)(encoding@0.1.13)(reflect-metadata@0.2.2)(rxjs@7.8.1) @@ -47914,7 +47861,7 @@ snapshots: transitivePeerDependencies: - chokidar - '@nestjs/serve-static@4.0.2(@nestjs/common@10.4.1(class-transformer@0.5.1)(class-validator@0.14.1)(reflect-metadata@0.2.2)(rxjs@7.8.1))(@nestjs/core@10.4.1(@nestjs/common@10.4.1(class-transformer@0.5.1)(class-validator@0.14.1)(reflect-metadata@0.2.2)(rxjs@7.8.1))(@nestjs/platform-express@10.4.1)(@nestjs/websockets@10.4.1)(encoding@0.1.13)(reflect-metadata@0.2.2)(rxjs@7.8.1))(express@4.21.0)': + '@nestjs/serve-static@4.0.2(@nestjs/common@10.4.1(class-transformer@0.5.1)(class-validator@0.14.1)(reflect-metadata@0.2.2)(rxjs@7.8.1))(@nestjs/core@10.4.1)(express@4.21.0)': dependencies: '@nestjs/common': 10.4.1(class-transformer@0.5.1)(class-validator@0.14.1)(reflect-metadata@0.2.2)(rxjs@7.8.1) '@nestjs/core': 10.4.1(@nestjs/common@10.4.1(class-transformer@0.5.1)(class-validator@0.14.1)(reflect-metadata@0.2.2)(rxjs@7.8.1))(@nestjs/platform-express@10.4.1)(@nestjs/websockets@10.4.1)(encoding@0.1.13)(reflect-metadata@0.2.2)(rxjs@7.8.1) @@ -47922,7 +47869,7 @@ snapshots: optionalDependencies: express: 4.21.0 - '@nestjs/swagger@7.4.0(@nestjs/common@10.4.1(class-transformer@0.5.1)(class-validator@0.14.1)(reflect-metadata@0.2.2)(rxjs@7.8.1))(@nestjs/core@10.4.1(@nestjs/common@10.4.1(class-transformer@0.5.1)(class-validator@0.14.1)(reflect-metadata@0.2.2)(rxjs@7.8.1))(@nestjs/platform-express@10.4.1)(@nestjs/websockets@10.4.1)(encoding@0.1.13)(reflect-metadata@0.2.2)(rxjs@7.8.1))(class-transformer@0.5.1)(class-validator@0.14.1)(reflect-metadata@0.2.2)': + '@nestjs/swagger@7.4.0(@nestjs/common@10.4.1(class-transformer@0.5.1)(class-validator@0.14.1)(reflect-metadata@0.2.2)(rxjs@7.8.1))(@nestjs/core@10.4.1)(class-transformer@0.5.1)(class-validator@0.14.1)(reflect-metadata@0.2.2)': dependencies: '@microsoft/tsdoc': 0.15.0 '@nestjs/common': 10.4.1(class-transformer@0.5.1)(class-validator@0.14.1)(reflect-metadata@0.2.2)(rxjs@7.8.1) @@ -47937,7 +47884,7 @@ snapshots: class-transformer: 0.5.1 class-validator: 0.14.1 - '@nestjs/terminus@10.2.3(@grpc/grpc-js@1.12.4)(@grpc/proto-loader@0.7.13)(@nestjs/axios@3.0.3(@nestjs/common@10.4.1(class-transformer@0.5.1)(class-validator@0.14.1)(reflect-metadata@0.2.2)(rxjs@7.8.1))(axios@1.6.8)(rxjs@7.8.1))(@nestjs/common@10.4.1(class-transformer@0.5.1)(class-validator@0.14.1)(reflect-metadata@0.2.2)(rxjs@7.8.1))(@nestjs/core@10.4.1(@nestjs/common@10.4.1(class-transformer@0.5.1)(class-validator@0.14.1)(reflect-metadata@0.2.2)(rxjs@7.8.1))(@nestjs/platform-express@10.4.1)(@nestjs/websockets@10.4.1)(encoding@0.1.13)(reflect-metadata@0.2.2)(rxjs@7.8.1))(mongoose@8.6.0(@aws-sdk/credential-providers@3.637.0(@aws-sdk/client-sso-oidc@3.575.0))(gcp-metadata@5.3.0(encoding@0.1.13))(socks@2.7.1))(reflect-metadata@0.2.2)(rxjs@7.8.1)': + '@nestjs/terminus@10.2.3(@grpc/grpc-js@1.12.4)(@grpc/proto-loader@0.7.13)(@nestjs/axios@3.0.3(@nestjs/common@10.4.1(class-transformer@0.5.1)(class-validator@0.14.1)(reflect-metadata@0.2.2)(rxjs@7.8.1))(axios@1.6.8)(rxjs@7.8.1))(@nestjs/common@10.4.1(class-transformer@0.5.1)(class-validator@0.14.1)(reflect-metadata@0.2.2)(rxjs@7.8.1))(@nestjs/core@10.4.1)(mongoose@8.6.0(@aws-sdk/credential-providers@3.637.0(@aws-sdk/client-sso-oidc@3.575.0))(gcp-metadata@5.3.0(encoding@0.1.13))(socks@2.7.1))(reflect-metadata@0.2.2)(rxjs@7.8.1)': dependencies: '@nestjs/common': 10.4.1(class-transformer@0.5.1)(class-validator@0.14.1)(reflect-metadata@0.2.2)(rxjs@7.8.1) '@nestjs/core': 10.4.1(@nestjs/common@10.4.1(class-transformer@0.5.1)(class-validator@0.14.1)(reflect-metadata@0.2.2)(rxjs@7.8.1))(@nestjs/platform-express@10.4.1)(@nestjs/websockets@10.4.1)(encoding@0.1.13)(reflect-metadata@0.2.2)(rxjs@7.8.1) @@ -47951,7 +47898,7 @@ snapshots: '@nestjs/axios': 3.0.3(@nestjs/common@10.4.1(class-transformer@0.5.1)(class-validator@0.14.1)(reflect-metadata@0.2.2)(rxjs@7.8.1))(axios@1.6.8)(rxjs@7.8.1) mongoose: 8.6.0(@aws-sdk/credential-providers@3.637.0(@aws-sdk/client-sso-oidc@3.575.0))(gcp-metadata@5.3.0(encoding@0.1.13))(socks@2.7.1) - '@nestjs/terminus@10.2.3(@grpc/grpc-js@1.12.4)(@grpc/proto-loader@0.7.13)(@nestjs/axios@3.0.3(@nestjs/common@10.4.1(class-transformer@0.5.1)(class-validator@0.14.1)(reflect-metadata@0.2.2)(rxjs@7.8.1))(axios@1.6.8)(rxjs@7.8.1))(@nestjs/common@10.4.1(class-transformer@0.5.1)(class-validator@0.14.1)(reflect-metadata@0.2.2)(rxjs@7.8.1))(@nestjs/core@10.4.1(@nestjs/common@10.4.1(class-transformer@0.5.1)(class-validator@0.14.1)(reflect-metadata@0.2.2)(rxjs@7.8.1))(@nestjs/platform-express@10.4.1)(@nestjs/websockets@10.4.1)(encoding@0.1.13)(reflect-metadata@0.2.2)(rxjs@7.8.1))(mongoose@8.6.0(@aws-sdk/credential-providers@3.637.0(@aws-sdk/client-sso-oidc@3.726.0(@aws-sdk/client-sts@3.726.0)))(gcp-metadata@5.3.0(encoding@0.1.13))(socks@2.7.1))(reflect-metadata@0.2.2)(rxjs@7.8.1)': + '@nestjs/terminus@10.2.3(@grpc/grpc-js@1.12.4)(@grpc/proto-loader@0.7.13)(@nestjs/axios@3.0.3(@nestjs/common@10.4.1(class-transformer@0.5.1)(class-validator@0.14.1)(reflect-metadata@0.2.2)(rxjs@7.8.1))(axios@1.6.8)(rxjs@7.8.1))(@nestjs/common@10.4.1(class-transformer@0.5.1)(class-validator@0.14.1)(reflect-metadata@0.2.2)(rxjs@7.8.1))(@nestjs/core@10.4.1)(mongoose@8.6.0(@aws-sdk/credential-providers@3.637.0(@aws-sdk/client-sso-oidc@3.726.0(@aws-sdk/client-sts@3.726.0)))(gcp-metadata@5.3.0(encoding@0.1.13))(socks@2.7.1))(reflect-metadata@0.2.2)(rxjs@7.8.1)': dependencies: '@nestjs/common': 10.4.1(class-transformer@0.5.1)(class-validator@0.14.1)(reflect-metadata@0.2.2)(rxjs@7.8.1) '@nestjs/core': 10.4.1(@nestjs/common@10.4.1(class-transformer@0.5.1)(class-validator@0.14.1)(reflect-metadata@0.2.2)(rxjs@7.8.1))(@nestjs/platform-express@10.4.1)(@nestjs/websockets@10.4.1)(encoding@0.1.13)(reflect-metadata@0.2.2)(rxjs@7.8.1) @@ -47965,7 +47912,7 @@ snapshots: '@nestjs/axios': 3.0.3(@nestjs/common@10.4.1(class-transformer@0.5.1)(class-validator@0.14.1)(reflect-metadata@0.2.2)(rxjs@7.8.1))(axios@1.6.8)(rxjs@7.8.1) mongoose: 8.6.0(@aws-sdk/credential-providers@3.637.0(@aws-sdk/client-sso-oidc@3.726.0(@aws-sdk/client-sts@3.726.0)))(gcp-metadata@5.3.0(encoding@0.1.13))(socks@2.7.1) - '@nestjs/terminus@10.2.3(@grpc/grpc-js@1.12.4)(@grpc/proto-loader@0.7.13)(@nestjs/axios@3.0.3(@nestjs/common@10.4.1(class-transformer@0.5.1)(class-validator@0.14.1)(reflect-metadata@0.2.2)(rxjs@7.8.1))(axios@1.7.7)(rxjs@7.8.1))(@nestjs/common@10.4.1(class-transformer@0.5.1)(class-validator@0.14.1)(reflect-metadata@0.2.2)(rxjs@7.8.1))(@nestjs/core@10.4.1(@nestjs/common@10.4.1(class-transformer@0.5.1)(class-validator@0.14.1)(reflect-metadata@0.2.2)(rxjs@7.8.1))(@nestjs/platform-express@10.4.1)(@nestjs/websockets@10.4.1)(encoding@0.1.13)(reflect-metadata@0.2.2)(rxjs@7.8.1))(mongoose@8.6.0(@aws-sdk/credential-providers@3.637.0(@aws-sdk/client-sso-oidc@3.726.0(@aws-sdk/client-sts@3.726.0)))(gcp-metadata@5.3.0(encoding@0.1.13))(socks@2.7.1))(reflect-metadata@0.2.2)(rxjs@7.8.1)': + '@nestjs/terminus@10.2.3(@grpc/grpc-js@1.12.4)(@grpc/proto-loader@0.7.13)(@nestjs/axios@3.0.3(@nestjs/common@10.4.1(class-transformer@0.5.1)(class-validator@0.14.1)(reflect-metadata@0.2.2)(rxjs@7.8.1))(axios@1.7.7)(rxjs@7.8.1))(@nestjs/common@10.4.1(class-transformer@0.5.1)(class-validator@0.14.1)(reflect-metadata@0.2.2)(rxjs@7.8.1))(@nestjs/core@10.4.1)(mongoose@8.6.0(@aws-sdk/credential-providers@3.637.0(@aws-sdk/client-sso-oidc@3.726.0(@aws-sdk/client-sts@3.726.0)))(gcp-metadata@5.3.0(encoding@0.1.13))(socks@2.7.1))(reflect-metadata@0.2.2)(rxjs@7.8.1)': dependencies: '@nestjs/common': 10.4.1(class-transformer@0.5.1)(class-validator@0.14.1)(reflect-metadata@0.2.2)(rxjs@7.8.1) '@nestjs/core': 10.4.1(@nestjs/common@10.4.1(class-transformer@0.5.1)(class-validator@0.14.1)(reflect-metadata@0.2.2)(rxjs@7.8.1))(@nestjs/platform-express@10.4.1)(@nestjs/websockets@10.4.1)(encoding@0.1.13)(reflect-metadata@0.2.2)(rxjs@7.8.1) @@ -47979,7 +47926,7 @@ snapshots: '@nestjs/axios': 3.0.3(@nestjs/common@10.4.1(class-transformer@0.5.1)(class-validator@0.14.1)(reflect-metadata@0.2.2)(rxjs@7.8.1))(axios@1.7.7)(rxjs@7.8.1) mongoose: 8.6.0(@aws-sdk/credential-providers@3.637.0(@aws-sdk/client-sso-oidc@3.726.0(@aws-sdk/client-sts@3.726.0)))(gcp-metadata@5.3.0(encoding@0.1.13))(socks@2.7.1) - '@nestjs/testing@10.4.1(@nestjs/common@10.4.1(class-transformer@0.5.1)(class-validator@0.14.1)(reflect-metadata@0.2.2)(rxjs@7.8.1))(@nestjs/core@10.4.1(@nestjs/common@10.4.1(class-transformer@0.5.1)(class-validator@0.14.1)(reflect-metadata@0.2.2)(rxjs@7.8.1))(@nestjs/platform-express@10.4.1)(@nestjs/websockets@10.4.1)(encoding@0.1.13)(reflect-metadata@0.2.2)(rxjs@7.8.1))(@nestjs/platform-express@10.4.1(@nestjs/common@10.4.1(class-transformer@0.5.1)(class-validator@0.14.1)(reflect-metadata@0.2.2)(rxjs@7.8.1))(@nestjs/core@10.4.1))': + '@nestjs/testing@10.4.1(@nestjs/common@10.4.1(class-transformer@0.5.1)(class-validator@0.14.1)(reflect-metadata@0.2.2)(rxjs@7.8.1))(@nestjs/core@10.4.1)(@nestjs/platform-express@10.4.1)': dependencies: '@nestjs/common': 10.4.1(class-transformer@0.5.1)(class-validator@0.14.1)(reflect-metadata@0.2.2)(rxjs@7.8.1) '@nestjs/core': 10.4.1(@nestjs/common@10.4.1(class-transformer@0.5.1)(class-validator@0.14.1)(reflect-metadata@0.2.2)(rxjs@7.8.1))(@nestjs/platform-express@10.4.1)(@nestjs/websockets@10.4.1)(encoding@0.1.13)(reflect-metadata@0.2.2)(rxjs@7.8.1) @@ -47987,7 +47934,7 @@ snapshots: optionalDependencies: '@nestjs/platform-express': 10.4.1(@nestjs/common@10.4.1(class-transformer@0.5.1)(class-validator@0.14.1)(reflect-metadata@0.2.2)(rxjs@7.8.1))(@nestjs/core@10.4.1) - '@nestjs/throttler@6.2.1(@nestjs/common@10.4.1(class-transformer@0.5.1)(class-validator@0.14.1)(reflect-metadata@0.2.2)(rxjs@7.8.1))(@nestjs/core@10.4.1(@nestjs/common@10.4.1(class-transformer@0.5.1)(class-validator@0.14.1)(reflect-metadata@0.2.2)(rxjs@7.8.1))(@nestjs/platform-express@10.4.1)(@nestjs/websockets@10.4.1)(encoding@0.1.13)(reflect-metadata@0.2.2)(rxjs@7.8.1))(reflect-metadata@0.2.2)': + '@nestjs/throttler@6.2.1(@nestjs/common@10.4.1(class-transformer@0.5.1)(class-validator@0.14.1)(reflect-metadata@0.2.2)(rxjs@7.8.1))(@nestjs/core@10.4.1)(reflect-metadata@0.2.2)': dependencies: '@nestjs/common': 10.4.1(class-transformer@0.5.1)(class-validator@0.14.1)(reflect-metadata@0.2.2)(rxjs@7.8.1) '@nestjs/core': 10.4.1(@nestjs/common@10.4.1(class-transformer@0.5.1)(class-validator@0.14.1)(reflect-metadata@0.2.2)(rxjs@7.8.1))(@nestjs/platform-express@10.4.1)(@nestjs/websockets@10.4.1)(encoding@0.1.13)(reflect-metadata@0.2.2)(rxjs@7.8.1) @@ -50592,7 +50539,7 @@ snapshots: webpack-dev-server: 4.11.1(webpack@5.78.0(@swc/core@1.3.107(@swc/helpers@0.5.12))) webpack-hot-middleware: 2.26.1 - '@pmmmwh/react-refresh-webpack-plugin@0.5.10(@types/webpack@5.28.5(@swc/core@1.7.26(@swc/helpers@0.5.12))(webpack-cli@5.1.4(webpack-bundle-analyzer@4.9.0)(webpack@5.78.0)))(react-refresh@0.11.0)(type-fest@2.19.0)(webpack-hot-middleware@2.26.1)(webpack@5.78.0(@swc/core@1.7.26(@swc/helpers@0.5.12))(webpack-cli@5.1.4))': + '@pmmmwh/react-refresh-webpack-plugin@0.5.10(@types/webpack@5.28.5(@swc/core@1.7.26(@swc/helpers@0.5.12))(webpack-cli@5.1.4))(react-refresh@0.11.0)(type-fest@2.19.0)(webpack-hot-middleware@2.26.1)(webpack@5.78.0)': dependencies: ansi-html-community: 0.0.8 common-path-prefix: 3.0.0 @@ -50606,7 +50553,7 @@ snapshots: source-map: 0.7.4 webpack: 5.78.0(@swc/core@1.7.26(@swc/helpers@0.5.12))(webpack-cli@5.1.4) optionalDependencies: - '@types/webpack': 5.28.5(@swc/core@1.7.26(@swc/helpers@0.5.12))(webpack-cli@5.1.4(webpack-bundle-analyzer@4.9.0)(webpack@5.78.0)) + '@types/webpack': 5.28.5(@swc/core@1.7.26(@swc/helpers@0.5.12))(webpack-cli@5.1.4) type-fest: 2.19.0 webpack-hot-middleware: 2.26.1 @@ -53081,7 +53028,7 @@ snapshots: '@sentry/types': 7.114.0 '@sentry/utils': 7.114.0 - '@sentry/nestjs@8.33.1(@nestjs/common@10.4.1(class-transformer@0.5.1)(class-validator@0.14.1)(reflect-metadata@0.2.2)(rxjs@7.8.1))(@nestjs/core@10.4.1(@nestjs/common@10.4.1(class-transformer@0.5.1)(class-validator@0.14.1)(reflect-metadata@0.2.2)(rxjs@7.8.1))(@nestjs/platform-express@10.4.1)(@nestjs/websockets@10.4.1)(encoding@0.1.13)(reflect-metadata@0.2.2)(rxjs@7.8.1))': + '@sentry/nestjs@8.33.1(@nestjs/common@10.4.1(class-transformer@0.5.1)(class-validator@0.14.1)(reflect-metadata@0.2.2)(rxjs@7.8.1))(@nestjs/core@10.4.1)': dependencies: '@nestjs/common': 10.4.1(class-transformer@0.5.1)(class-validator@0.14.1)(reflect-metadata@0.2.2)(rxjs@7.8.1) '@nestjs/core': 10.4.1(@nestjs/common@10.4.1(class-transformer@0.5.1)(class-validator@0.14.1)(reflect-metadata@0.2.2)(rxjs@7.8.1))(@nestjs/platform-express@10.4.1)(@nestjs/websockets@10.4.1)(encoding@0.1.13)(reflect-metadata@0.2.2)(rxjs@7.8.1) @@ -53645,18 +53592,6 @@ snapshots: '@smithy/util-utf8': 3.0.0 tslib: 2.7.0 - '@smithy/middleware-compression@3.0.0': - dependencies: - '@smithy/is-array-buffer': 3.0.0 - '@smithy/node-config-provider': 3.0.0 - '@smithy/protocol-http': 4.0.0 - '@smithy/types': 3.0.0 - '@smithy/util-config-provider': 3.0.0 - '@smithy/util-middleware': 3.0.0 - '@smithy/util-utf8': 3.0.0 - fflate: 0.8.1 - tslib: 2.7.0 - '@smithy/middleware-content-length@2.0.3': dependencies: '@smithy/protocol-http': 2.0.3 @@ -56210,7 +56145,7 @@ snapshots: - uglify-js - webpack-cli - '@storybook/builder-webpack5@7.4.2(@swc/helpers@0.5.12)(@types/react-dom@18.3.0)(@types/react@18.3.3)(encoding@0.1.13)(react-dom@18.3.1(react@18.3.1))(react@18.3.1)(typescript@5.6.2)(webpack-cli@5.1.4(webpack-bundle-analyzer@4.9.0)(webpack@5.78.0))': + '@storybook/builder-webpack5@7.4.2(@swc/helpers@0.5.12)(@types/react-dom@18.3.0)(@types/react@18.3.3)(encoding@0.1.13)(react-dom@18.3.1(react@18.3.1))(react@18.3.1)(typescript@5.6.2)(webpack-cli@5.1.4)': dependencies: '@babel/core': 7.23.2 '@storybook/addons': 7.4.2(react-dom@18.3.1(react@18.3.1))(react@18.3.1) @@ -56232,30 +56167,30 @@ snapshots: '@swc/core': 1.3.107(@swc/helpers@0.5.12) '@types/node': 16.11.7 '@types/semver': 7.3.13 - babel-loader: 9.1.2(@babel/core@7.23.2)(webpack@5.78.0(@swc/core@1.7.26(@swc/helpers@0.5.12))(webpack-cli@5.1.4)) + babel-loader: 9.1.2(@babel/core@7.23.2)(webpack@5.78.0) babel-plugin-named-exports-order: 0.0.2 browser-assert: 1.2.1 case-sensitive-paths-webpack-plugin: 2.4.0 constants-browserify: 1.0.0 - css-loader: 6.7.3(webpack@5.78.0(@swc/core@1.7.26(@swc/helpers@0.5.12))(webpack-cli@5.1.4)) + css-loader: 6.7.3(webpack@5.78.0) express: 4.21.0 - fork-ts-checker-webpack-plugin: 8.0.0(typescript@5.6.2)(webpack@5.78.0(@swc/core@1.7.26(@swc/helpers@0.5.12))(webpack-cli@5.1.4)) + fork-ts-checker-webpack-plugin: 8.0.0(typescript@5.6.2)(webpack@5.78.0) fs-extra: 11.2.0 - html-webpack-plugin: 5.5.3(webpack@5.78.0(@swc/core@1.7.26(@swc/helpers@0.5.12))(webpack-cli@5.1.4)) + html-webpack-plugin: 5.5.3(webpack@5.78.0) path-browserify: 1.0.1 process: 0.11.10 react: 18.3.1 react-dom: 18.3.1(react@18.3.1) semver: 7.6.3 - style-loader: 3.3.2(webpack@5.78.0(@swc/core@1.7.26(@swc/helpers@0.5.12))(webpack-cli@5.1.4)) - swc-loader: 0.2.3(@swc/core@1.3.107(@swc/helpers@0.5.12))(webpack@5.78.0(@swc/core@1.7.26(@swc/helpers@0.5.12))(webpack-cli@5.1.4)) - terser-webpack-plugin: 5.3.10(@swc/core@1.3.107(@swc/helpers@0.5.12))(webpack@5.78.0(@swc/core@1.7.26(@swc/helpers@0.5.12))(webpack-cli@5.1.4)) + style-loader: 3.3.2(webpack@5.78.0) + swc-loader: 0.2.3(@swc/core@1.3.107(@swc/helpers@0.5.12))(webpack@5.78.0) + terser-webpack-plugin: 5.3.10(@swc/core@1.3.107(@swc/helpers@0.5.12))(webpack@5.78.0) ts-dedent: 2.2.0 url: 0.11.4 util: 0.12.5 util-deprecate: 1.0.2 - webpack: 5.78.0(@swc/core@1.3.107(@swc/helpers@0.5.12))(webpack-cli@5.1.4(webpack-bundle-analyzer@4.9.0)(webpack@5.78.0)) - webpack-dev-middleware: 6.1.1(webpack@5.78.0(@swc/core@1.7.26(@swc/helpers@0.5.12))(webpack-cli@5.1.4)) + webpack: 5.78.0(@swc/core@1.3.107(@swc/helpers@0.5.12))(webpack-cli@5.1.4) + webpack-dev-middleware: 6.1.1(webpack@5.78.0) webpack-hot-middleware: 2.25.3 webpack-virtual-modules: 0.5.0 optionalDependencies: @@ -56906,7 +56841,7 @@ snapshots: '@storybook/postinstall@7.4.2': {} - '@storybook/preset-create-react-app@7.4.2(ucmnrhmq4kewpo24xrp57f5r6y)': + '@storybook/preset-create-react-app@7.4.2(@babel/core@7.22.11)(@types/webpack@5.28.5(@swc/core@1.3.107(@swc/helpers@0.5.12))(esbuild@0.18.20))(react-refresh@0.11.0)(react-scripts@5.0.1(@babel/plugin-syntax-flow@7.24.7(@babel/core@7.22.11))(@babel/plugin-transform-react-jsx@7.25.2(@babel/core@7.22.11))(@swc/core@1.3.107(@swc/helpers@0.5.12))(@types/babel__core@7.20.5)(@types/webpack@5.28.5(@swc/core@1.3.107(@swc/helpers@0.5.12))(esbuild@0.18.20))(esbuild@0.18.20)(eslint-import-resolver-webpack@0.13.8)(eslint@9.9.1(jiti@1.21.6))(react@18.3.1)(sass@1.77.8)(ts-node@10.9.1(@swc/core@1.3.107(@swc/helpers@0.5.12))(@types/node@18.16.9)(typescript@5.6.2))(type-fest@2.19.0)(typescript@5.6.2)(vue-template-compiler@2.7.16)(webpack-hot-middleware@2.26.1))(type-fest@2.19.0)(typescript@5.6.2)(webpack-dev-server@4.11.1(webpack@5.78.0(@swc/core@1.3.107(@swc/helpers@0.5.12))(esbuild@0.18.20)))(webpack-hot-middleware@2.26.1)(webpack@5.78.0(@swc/core@1.3.107(@swc/helpers@0.5.12))(esbuild@0.18.20))': dependencies: '@babel/core': 7.22.11 '@pmmmwh/react-refresh-webpack-plugin': 0.5.10(@types/webpack@5.28.5(@swc/core@1.3.107(@swc/helpers@0.5.12))(esbuild@0.18.20))(react-refresh@0.11.0)(type-fest@2.19.0)(webpack-dev-server@4.11.1(webpack@5.78.0(@swc/core@1.3.107(@swc/helpers@0.5.12))(esbuild@0.18.20)))(webpack-hot-middleware@2.26.1)(webpack@5.78.0(@swc/core@1.3.107(@swc/helpers@0.5.12))(esbuild@0.18.20)) @@ -56915,7 +56850,7 @@ snapshots: '@types/babel__core': 7.20.0 babel-plugin-react-docgen: 4.2.1 pnp-webpack-plugin: 1.7.0(typescript@5.6.2) - react-scripts: 5.0.1(@babel/plugin-syntax-flow@7.24.7(@babel/core@7.22.11))(@babel/plugin-transform-react-jsx@7.25.2(@babel/core@7.22.11))(@swc/core@1.3.107(@swc/helpers@0.5.12))(@types/babel__core@7.20.5)(@types/webpack@5.28.5(@swc/core@1.3.107(@swc/helpers@0.5.12))(esbuild@0.18.20))(esbuild@0.18.20)(eslint-import-resolver-webpack@0.13.8(eslint-plugin-import@2.29.1)(webpack@5.94.0(@swc/core@1.3.107(@swc/helpers@0.5.12))))(eslint@9.9.1(jiti@1.21.6))(react@18.3.1)(sass@1.77.8)(ts-node@10.9.1(@swc/core@1.3.107(@swc/helpers@0.5.12))(@types/node@18.16.9)(typescript@5.6.2))(type-fest@2.19.0)(typescript@5.6.2)(vue-template-compiler@2.7.16)(webpack-hot-middleware@2.26.1) + react-scripts: 5.0.1(@babel/plugin-syntax-flow@7.24.7(@babel/core@7.22.11))(@babel/plugin-transform-react-jsx@7.25.2(@babel/core@7.22.11))(@swc/core@1.3.107(@swc/helpers@0.5.12))(@types/babel__core@7.20.5)(@types/webpack@5.28.5(@swc/core@1.3.107(@swc/helpers@0.5.12))(esbuild@0.18.20))(esbuild@0.18.20)(eslint-import-resolver-webpack@0.13.8)(eslint@9.9.1(jiti@1.21.6))(react@18.3.1)(sass@1.77.8)(ts-node@10.9.1(@swc/core@1.3.107(@swc/helpers@0.5.12))(@types/node@18.16.9)(typescript@5.6.2))(type-fest@2.19.0)(typescript@5.6.2)(vue-template-compiler@2.7.16)(webpack-hot-middleware@2.26.1) semver: 7.5.4 transitivePeerDependencies: - '@types/webpack' @@ -57003,16 +56938,16 @@ snapshots: - webpack-hot-middleware - webpack-plugin-serve - '@storybook/preset-react-webpack@7.4.2(@babel/core@7.25.2)(@swc/core@1.7.26(@swc/helpers@0.5.12))(@types/webpack@5.28.5(@swc/core@1.7.26(@swc/helpers@0.5.12))(webpack-cli@5.1.4(webpack-bundle-analyzer@4.9.0)(webpack@5.78.0)))(encoding@0.1.13)(react-dom@18.3.1(react@18.3.1))(react@18.3.1)(type-fest@2.19.0)(typescript@5.6.2)(webpack-cli@5.1.4(webpack-bundle-analyzer@4.9.0)(webpack@5.78.0))(webpack-hot-middleware@2.26.1)': + '@storybook/preset-react-webpack@7.4.2(@babel/core@7.25.2)(@swc/core@1.7.26(@swc/helpers@0.5.12))(@types/webpack@5.28.5(@swc/core@1.7.26(@swc/helpers@0.5.12))(webpack-cli@5.1.4))(encoding@0.1.13)(react-dom@18.3.1(react@18.3.1))(react@18.3.1)(type-fest@2.19.0)(typescript@5.6.2)(webpack-cli@5.1.4)(webpack-hot-middleware@2.26.1)': dependencies: '@babel/preset-flow': 7.22.15(@babel/core@7.25.2) '@babel/preset-react': 7.22.15(@babel/core@7.25.2) - '@pmmmwh/react-refresh-webpack-plugin': 0.5.10(@types/webpack@5.28.5(@swc/core@1.7.26(@swc/helpers@0.5.12))(webpack-cli@5.1.4(webpack-bundle-analyzer@4.9.0)(webpack@5.78.0)))(react-refresh@0.11.0)(type-fest@2.19.0)(webpack-hot-middleware@2.26.1)(webpack@5.78.0(@swc/core@1.7.26(@swc/helpers@0.5.12))(webpack-cli@5.1.4)) + '@pmmmwh/react-refresh-webpack-plugin': 0.5.10(@types/webpack@5.28.5(@swc/core@1.7.26(@swc/helpers@0.5.12))(webpack-cli@5.1.4))(react-refresh@0.11.0)(type-fest@2.19.0)(webpack-hot-middleware@2.26.1)(webpack@5.78.0) '@storybook/core-webpack': 7.4.2(encoding@0.1.13) '@storybook/docs-tools': 7.4.2(encoding@0.1.13) '@storybook/node-logger': 7.4.2 '@storybook/react': 7.4.2(encoding@0.1.13)(react-dom@18.3.1(react@18.3.1))(react@18.3.1)(typescript@5.6.2) - '@storybook/react-docgen-typescript-plugin': 1.0.6--canary.9.0c3f3b7.0(typescript@5.6.2)(webpack@5.78.0(@swc/core@1.7.26(@swc/helpers@0.5.12))(webpack-cli@5.1.4)) + '@storybook/react-docgen-typescript-plugin': 1.0.6--canary.9.0c3f3b7.0(typescript@5.6.2)(webpack@5.78.0) '@types/node': 16.11.7 '@types/semver': 7.5.8 babel-plugin-add-react-displayname: 0.0.5 @@ -57126,7 +57061,7 @@ snapshots: transitivePeerDependencies: - supports-color - '@storybook/react-docgen-typescript-plugin@1.0.6--canary.9.0c3f3b7.0(typescript@5.6.2)(webpack@5.78.0(@swc/core@1.7.26(@swc/helpers@0.5.12))(webpack-cli@5.1.4))': + '@storybook/react-docgen-typescript-plugin@1.0.6--canary.9.0c3f3b7.0(typescript@5.6.2)(webpack@5.78.0(@swc/core@1.7.26(@swc/helpers@0.5.12)))': dependencies: debug: 4.3.6(supports-color@8.1.1) endent: 2.1.0 @@ -57136,11 +57071,11 @@ snapshots: react-docgen-typescript: 2.2.2(typescript@5.6.2) tslib: 2.7.0 typescript: 5.6.2 - webpack: 5.78.0(@swc/core@1.7.26(@swc/helpers@0.5.12))(webpack-cli@5.1.4) + webpack: 5.78.0(@swc/core@1.7.26(@swc/helpers@0.5.12)) transitivePeerDependencies: - supports-color - '@storybook/react-docgen-typescript-plugin@1.0.6--canary.9.0c3f3b7.0(typescript@5.6.2)(webpack@5.78.0(@swc/core@1.7.26(@swc/helpers@0.5.12)))': + '@storybook/react-docgen-typescript-plugin@1.0.6--canary.9.0c3f3b7.0(typescript@5.6.2)(webpack@5.78.0)': dependencies: debug: 4.3.6(supports-color@8.1.1) endent: 2.1.0 @@ -57150,7 +57085,7 @@ snapshots: react-docgen-typescript: 2.2.2(typescript@5.6.2) tslib: 2.7.0 typescript: 5.6.2 - webpack: 5.78.0(@swc/core@1.7.26(@swc/helpers@0.5.12)) + webpack: 5.78.0(@swc/core@1.7.26(@swc/helpers@0.5.12))(webpack-cli@5.1.4) transitivePeerDependencies: - supports-color @@ -57246,10 +57181,10 @@ snapshots: - webpack-hot-middleware - webpack-plugin-serve - '@storybook/react-webpack5@7.4.2(@babel/core@7.25.2)(@swc/core@1.7.26(@swc/helpers@0.5.12))(@swc/helpers@0.5.12)(@types/react-dom@18.3.0)(@types/react@18.3.3)(@types/webpack@5.28.5(@swc/core@1.7.26(@swc/helpers@0.5.12))(webpack-cli@5.1.4(webpack-bundle-analyzer@4.9.0)(webpack@5.78.0)))(encoding@0.1.13)(react-dom@18.3.1(react@18.3.1))(react@18.3.1)(type-fest@2.19.0)(typescript@5.6.2)(webpack-cli@5.1.4(webpack-bundle-analyzer@4.9.0)(webpack@5.78.0))(webpack-hot-middleware@2.26.1)': + '@storybook/react-webpack5@7.4.2(@babel/core@7.25.2)(@swc/core@1.7.26(@swc/helpers@0.5.12))(@swc/helpers@0.5.12)(@types/react-dom@18.3.0)(@types/react@18.3.3)(@types/webpack@5.28.5(@swc/core@1.7.26(@swc/helpers@0.5.12))(webpack-cli@5.1.4))(encoding@0.1.13)(react-dom@18.3.1(react@18.3.1))(react@18.3.1)(type-fest@2.19.0)(typescript@5.6.2)(webpack-cli@5.1.4)(webpack-hot-middleware@2.26.1)': dependencies: - '@storybook/builder-webpack5': 7.4.2(@swc/helpers@0.5.12)(@types/react-dom@18.3.0)(@types/react@18.3.3)(encoding@0.1.13)(react-dom@18.3.1(react@18.3.1))(react@18.3.1)(typescript@5.6.2)(webpack-cli@5.1.4(webpack-bundle-analyzer@4.9.0)(webpack@5.78.0)) - '@storybook/preset-react-webpack': 7.4.2(@babel/core@7.25.2)(@swc/core@1.7.26(@swc/helpers@0.5.12))(@types/webpack@5.28.5(@swc/core@1.7.26(@swc/helpers@0.5.12))(webpack-cli@5.1.4(webpack-bundle-analyzer@4.9.0)(webpack@5.78.0)))(encoding@0.1.13)(react-dom@18.3.1(react@18.3.1))(react@18.3.1)(type-fest@2.19.0)(typescript@5.6.2)(webpack-cli@5.1.4(webpack-bundle-analyzer@4.9.0)(webpack@5.78.0))(webpack-hot-middleware@2.26.1) + '@storybook/builder-webpack5': 7.4.2(@swc/helpers@0.5.12)(@types/react-dom@18.3.0)(@types/react@18.3.3)(encoding@0.1.13)(react-dom@18.3.1(react@18.3.1))(react@18.3.1)(typescript@5.6.2)(webpack-cli@5.1.4) + '@storybook/preset-react-webpack': 7.4.2(@babel/core@7.25.2)(@swc/core@1.7.26(@swc/helpers@0.5.12))(@types/webpack@5.28.5(@swc/core@1.7.26(@swc/helpers@0.5.12))(webpack-cli@5.1.4))(encoding@0.1.13)(react-dom@18.3.1(react@18.3.1))(react@18.3.1)(type-fest@2.19.0)(typescript@5.6.2)(webpack-cli@5.1.4)(webpack-hot-middleware@2.26.1) '@storybook/react': 7.4.2(encoding@0.1.13)(react-dom@18.3.1(react@18.3.1))(react@18.3.1)(typescript@5.6.2) '@types/node': 16.11.7 react: 18.3.1 @@ -59319,7 +59254,7 @@ snapshots: - webpack-cli optional: true - '@types/webpack@5.28.5(@swc/core@1.7.26(@swc/helpers@0.5.12))(webpack-cli@5.1.4(webpack-bundle-analyzer@4.9.0)(webpack@5.78.0))': + '@types/webpack@5.28.5(@swc/core@1.7.26(@swc/helpers@0.5.12))(webpack-cli@5.1.4)': dependencies: '@types/node': 20.16.5 tapable: 2.2.1 @@ -60958,36 +60893,21 @@ snapshots: '@webcontainer/api@1.2.0': {} - '@webpack-cli/configtest@2.1.1(webpack-cli@5.1.4(webpack-bundle-analyzer@4.10.1)(webpack@5.78.0))(webpack@5.78.0(@swc/core@1.7.26(@swc/helpers@0.5.12))(esbuild@0.23.1)(webpack-cli@5.1.4))': + '@webpack-cli/configtest@2.1.1(webpack-cli@5.1.4)(webpack@5.78.0)': dependencies: webpack: 5.78.0(@swc/core@1.7.26(@swc/helpers@0.5.12))(esbuild@0.23.1)(webpack-cli@5.1.4) webpack-cli: 5.1.4(webpack-bundle-analyzer@4.10.1)(webpack@5.78.0) - '@webpack-cli/configtest@2.1.1(webpack-cli@5.1.4(webpack-bundle-analyzer@4.9.0)(webpack@5.78.0))(webpack@5.78.0(@swc/core@1.7.26(@swc/helpers@0.5.12))(webpack-cli@5.1.4))': - dependencies: - webpack: 5.78.0(@swc/core@1.7.26(@swc/helpers@0.5.12))(webpack-cli@5.1.4) - webpack-cli: 5.1.4(webpack-bundle-analyzer@4.9.0)(webpack@5.78.0) - - '@webpack-cli/info@2.0.2(webpack-cli@5.1.4(webpack-bundle-analyzer@4.10.1)(webpack@5.78.0))(webpack@5.78.0(@swc/core@1.7.26(@swc/helpers@0.5.12))(esbuild@0.23.1)(webpack-cli@5.1.4))': + '@webpack-cli/info@2.0.2(webpack-cli@5.1.4)(webpack@5.78.0)': dependencies: webpack: 5.78.0(@swc/core@1.7.26(@swc/helpers@0.5.12))(esbuild@0.23.1)(webpack-cli@5.1.4) webpack-cli: 5.1.4(webpack-bundle-analyzer@4.10.1)(webpack@5.78.0) - '@webpack-cli/info@2.0.2(webpack-cli@5.1.4(webpack-bundle-analyzer@4.9.0)(webpack@5.78.0))(webpack@5.78.0(@swc/core@1.7.26(@swc/helpers@0.5.12))(webpack-cli@5.1.4))': - dependencies: - webpack: 5.78.0(@swc/core@1.7.26(@swc/helpers@0.5.12))(webpack-cli@5.1.4) - webpack-cli: 5.1.4(webpack-bundle-analyzer@4.9.0)(webpack@5.78.0) - - '@webpack-cli/serve@2.0.5(webpack-cli@5.1.4(webpack-bundle-analyzer@4.10.1)(webpack@5.78.0))(webpack@5.78.0(@swc/core@1.7.26(@swc/helpers@0.5.12))(esbuild@0.23.1)(webpack-cli@5.1.4))': + '@webpack-cli/serve@2.0.5(webpack-cli@5.1.4)(webpack@5.78.0)': dependencies: webpack: 5.78.0(@swc/core@1.7.26(@swc/helpers@0.5.12))(esbuild@0.23.1)(webpack-cli@5.1.4) webpack-cli: 5.1.4(webpack-bundle-analyzer@4.10.1)(webpack@5.78.0) - '@webpack-cli/serve@2.0.5(webpack-cli@5.1.4(webpack-bundle-analyzer@4.9.0)(webpack@5.78.0))(webpack@5.78.0(@swc/core@1.7.26(@swc/helpers@0.5.12))(webpack-cli@5.1.4))': - dependencies: - webpack: 5.78.0(@swc/core@1.7.26(@swc/helpers@0.5.12))(webpack-cli@5.1.4) - webpack-cli: 5.1.4(webpack-bundle-analyzer@4.9.0)(webpack@5.78.0) - '@wry/context@0.4.4': dependencies: '@types/node': 20.16.5 @@ -62156,7 +62076,7 @@ snapshots: schema-utils: 2.7.1 webpack: 5.78.0(@swc/core@1.3.107(@swc/helpers@0.5.12)) - babel-loader@8.3.0(@babel/core@7.25.2)(webpack@5.78.0(@swc/core@1.7.26(@swc/helpers@0.5.12))(webpack-cli@5.1.4)): + babel-loader@8.3.0(@babel/core@7.25.2)(webpack@5.78.0): dependencies: '@babel/core': 7.25.2 find-cache-dir: 3.3.2 @@ -62179,7 +62099,7 @@ snapshots: schema-utils: 4.0.0 webpack: 5.78.0(@swc/core@1.3.107(@swc/helpers@0.5.12)) - babel-loader@9.1.2(@babel/core@7.23.2)(webpack@5.78.0(@swc/core@1.7.26(@swc/helpers@0.5.12))(webpack-cli@5.1.4)): + babel-loader@9.1.2(@babel/core@7.23.2)(webpack@5.78.0): dependencies: '@babel/core': 7.23.2 find-cache-dir: 3.3.2 @@ -63758,18 +63678,12 @@ snapshots: dependencies: mime-db: 1.52.0 - compression-webpack-plugin@10.0.0(webpack@5.78.0(@swc/core@1.7.26(@swc/helpers@0.5.12))(esbuild@0.23.1)(webpack-cli@5.1.4)): + compression-webpack-plugin@10.0.0(webpack@5.78.0): dependencies: schema-utils: 4.0.0 serialize-javascript: 6.0.1 webpack: 5.78.0(@swc/core@1.7.26(@swc/helpers@0.5.12))(esbuild@0.23.1)(webpack-cli@5.1.4) - compression-webpack-plugin@10.0.0(webpack@5.78.0(@swc/core@1.7.26(@swc/helpers@0.5.12))(webpack-cli@5.1.4)): - dependencies: - schema-utils: 4.0.0 - serialize-javascript: 6.0.1 - webpack: 5.78.0(@swc/core@1.7.26(@swc/helpers@0.5.12))(webpack-cli@5.1.4) - compression@1.7.4: dependencies: accepts: 1.3.8 @@ -63951,6 +63865,8 @@ snapshots: cookie@0.7.2: {} + cookie@1.0.2: {} + cookiejar@2.1.4: {} copy-anything@2.0.6: @@ -64474,7 +64390,7 @@ snapshots: semver: 7.6.3 webpack: 5.78.0(@swc/core@1.3.107(@swc/helpers@0.5.12)) - css-loader@6.7.3(webpack@5.78.0(@swc/core@1.7.26(@swc/helpers@0.5.12))(webpack-cli@5.1.4)): + css-loader@6.7.3(webpack@5.78.0): dependencies: icss-utils: 5.1.0(postcss@8.4.47) postcss: 8.4.47 @@ -66064,7 +65980,7 @@ snapshots: - supports-color - typescript - eslint-config-airbnb-base@15.0.0(eslint-plugin-import@2.29.1(@typescript-eslint/parser@8.18.2(eslint@8.57.1)(typescript@5.6.2))(eslint-import-resolver-webpack@0.13.8)(eslint@8.57.1))(eslint@8.57.1): + eslint-config-airbnb-base@15.0.0(eslint-plugin-import@2.29.1)(eslint@8.57.1): dependencies: confusing-browser-globals: 1.0.11 eslint: 8.57.1 @@ -66073,12 +65989,12 @@ snapshots: object.entries: 1.1.8 semver: 6.3.1 - eslint-config-airbnb-typescript@18.0.0(@typescript-eslint/eslint-plugin@8.18.2(@typescript-eslint/parser@8.18.2(eslint@8.57.1)(typescript@5.6.2))(eslint@8.57.1)(typescript@5.6.2))(@typescript-eslint/parser@8.18.2(eslint@8.57.1)(typescript@5.6.2))(eslint-plugin-import@2.29.1(@typescript-eslint/parser@8.18.2(eslint@8.57.1)(typescript@5.6.2))(eslint-import-resolver-webpack@0.13.8)(eslint@8.57.1))(eslint@8.57.1): + eslint-config-airbnb-typescript@18.0.0(@typescript-eslint/eslint-plugin@8.18.2(@typescript-eslint/parser@8.18.2(eslint@8.57.1)(typescript@5.6.2))(eslint@8.57.1)(typescript@5.6.2))(@typescript-eslint/parser@8.18.2(eslint@8.57.1)(typescript@5.6.2))(eslint-plugin-import@2.29.1)(eslint@8.57.1): dependencies: '@typescript-eslint/eslint-plugin': 8.18.2(@typescript-eslint/parser@8.18.2(eslint@8.57.1)(typescript@5.6.2))(eslint@8.57.1)(typescript@5.6.2) '@typescript-eslint/parser': 8.18.2(eslint@8.57.1)(typescript@5.6.2) eslint: 8.57.1 - eslint-config-airbnb-base: 15.0.0(eslint-plugin-import@2.29.1(@typescript-eslint/parser@8.18.2(eslint@8.57.1)(typescript@5.6.2))(eslint-import-resolver-webpack@0.13.8)(eslint@8.57.1))(eslint@8.57.1) + eslint-config-airbnb-base: 15.0.0(eslint-plugin-import@2.29.1)(eslint@8.57.1) transitivePeerDependencies: - eslint-plugin-import @@ -66101,7 +66017,7 @@ snapshots: dependencies: eslint: 8.57.1 - eslint-config-react-app@7.0.1(@babel/plugin-syntax-flow@7.24.7(@babel/core@7.22.11))(@babel/plugin-transform-react-jsx@7.25.2(@babel/core@7.22.11))(eslint-import-resolver-webpack@0.13.8(eslint-plugin-import@2.29.1)(webpack@5.94.0(@swc/core@1.3.107(@swc/helpers@0.5.12))))(eslint@9.9.1(jiti@1.21.6))(jest@27.5.1(ts-node@10.9.1(@swc/core@1.3.107(@swc/helpers@0.5.12))(@types/node@18.16.9)(typescript@5.6.2)))(typescript@5.6.2): + eslint-config-react-app@7.0.1(@babel/plugin-syntax-flow@7.24.7(@babel/core@7.22.11))(@babel/plugin-transform-react-jsx@7.25.2(@babel/core@7.22.11))(eslint-import-resolver-webpack@0.13.8)(eslint@9.9.1(jiti@1.21.6))(jest@27.5.1(ts-node@10.9.1(@swc/core@1.3.107(@swc/helpers@0.5.12))(@types/node@18.16.9)(typescript@5.6.2)))(typescript@5.6.2): dependencies: '@babel/core': 7.21.4 '@babel/eslint-parser': 7.25.1(@babel/core@7.21.4)(eslint@9.9.1(jiti@1.21.6)) @@ -66112,7 +66028,7 @@ snapshots: confusing-browser-globals: 1.0.11 eslint: 9.9.1(jiti@1.21.6) eslint-plugin-flowtype: 8.0.3(@babel/plugin-syntax-flow@7.24.7(@babel/core@7.22.11))(@babel/plugin-transform-react-jsx@7.25.2(@babel/core@7.22.11))(eslint@9.9.1(jiti@1.21.6)) - eslint-plugin-import: 2.29.1(@typescript-eslint/parser@5.62.0(eslint@9.9.1(jiti@1.21.6))(typescript@5.6.2))(eslint-import-resolver-webpack@0.13.8(eslint-plugin-import@2.29.1)(webpack@5.94.0(@swc/core@1.3.107(@swc/helpers@0.5.12))))(eslint@9.9.1(jiti@1.21.6)) + eslint-plugin-import: 2.29.1(@typescript-eslint/parser@5.62.0(eslint@9.9.1(jiti@1.21.6))(typescript@5.6.2))(eslint-import-resolver-webpack@0.13.8)(eslint@9.9.1(jiti@1.21.6)) eslint-plugin-jest: 25.7.0(@typescript-eslint/eslint-plugin@5.62.0(@typescript-eslint/parser@5.62.0(eslint@9.9.1(jiti@1.21.6))(typescript@5.6.2))(eslint@9.9.1(jiti@1.21.6))(typescript@5.6.2))(eslint@9.9.1(jiti@1.21.6))(jest@27.5.1(ts-node@10.9.1(@swc/core@1.3.107(@swc/helpers@0.5.12))(@types/node@18.16.9)(typescript@5.6.2)))(typescript@5.6.2) eslint-plugin-jsx-a11y: 6.9.0(eslint@9.9.1(jiti@1.21.6)) eslint-plugin-react: 7.35.0(eslint@9.9.1(jiti@1.21.6)) @@ -66128,7 +66044,7 @@ snapshots: - jest - supports-color - eslint-config-react-app@7.0.1(@babel/plugin-syntax-flow@7.24.7(@babel/core@7.25.2))(@babel/plugin-transform-react-jsx@7.25.2(@babel/core@7.25.2))(eslint-import-resolver-webpack@0.13.8(eslint-plugin-import@2.29.1)(webpack@5.94.0(@swc/core@1.3.107(@swc/helpers@0.5.12))))(eslint@9.9.1(jiti@1.21.6))(jest@27.5.1(ts-node@10.9.1(@swc/core@1.3.107(@swc/helpers@0.5.12))(@types/node@18.16.9)(typescript@5.6.2)))(typescript@5.6.2): + eslint-config-react-app@7.0.1(@babel/plugin-syntax-flow@7.24.7(@babel/core@7.25.2))(@babel/plugin-transform-react-jsx@7.25.2(@babel/core@7.25.2))(eslint-import-resolver-webpack@0.13.8)(eslint@9.9.1(jiti@1.21.6))(jest@27.5.1(ts-node@10.9.1(@swc/core@1.3.107(@swc/helpers@0.5.12))(@types/node@18.16.9)(typescript@5.6.2)))(typescript@5.6.2): dependencies: '@babel/core': 7.21.4 '@babel/eslint-parser': 7.25.1(@babel/core@7.21.4)(eslint@9.9.1(jiti@1.21.6)) @@ -66139,7 +66055,7 @@ snapshots: confusing-browser-globals: 1.0.11 eslint: 9.9.1(jiti@1.21.6) eslint-plugin-flowtype: 8.0.3(@babel/plugin-syntax-flow@7.24.7(@babel/core@7.25.2))(@babel/plugin-transform-react-jsx@7.25.2(@babel/core@7.25.2))(eslint@9.9.1(jiti@1.21.6)) - eslint-plugin-import: 2.29.1(@typescript-eslint/parser@5.62.0(eslint@9.9.1(jiti@1.21.6))(typescript@5.6.2))(eslint-import-resolver-webpack@0.13.8(eslint-plugin-import@2.29.1)(webpack@5.94.0(@swc/core@1.3.107(@swc/helpers@0.5.12))))(eslint@9.9.1(jiti@1.21.6)) + eslint-plugin-import: 2.29.1(@typescript-eslint/parser@5.62.0(eslint@9.9.1(jiti@1.21.6))(typescript@5.6.2))(eslint-import-resolver-webpack@0.13.8)(eslint@9.9.1(jiti@1.21.6)) eslint-plugin-jest: 25.7.0(@typescript-eslint/eslint-plugin@5.62.0(@typescript-eslint/parser@5.62.0(eslint@9.9.1(jiti@1.21.6))(typescript@5.6.2))(eslint@9.9.1(jiti@1.21.6))(typescript@5.6.2))(eslint@9.9.1(jiti@1.21.6))(jest@27.5.1(ts-node@10.9.1(@swc/core@1.3.107(@swc/helpers@0.5.12))(@types/node@18.16.9)(typescript@5.6.2)))(typescript@5.6.2) eslint-plugin-jsx-a11y: 6.9.0(eslint@9.9.1(jiti@1.21.6)) eslint-plugin-react: 7.35.0(eslint@9.9.1(jiti@1.21.6)) @@ -66181,7 +66097,7 @@ snapshots: transitivePeerDependencies: - supports-color - eslint-module-utils@2.8.2(@typescript-eslint/parser@5.62.0(eslint@9.9.1(jiti@1.21.6))(typescript@5.6.2))(eslint-import-resolver-node@0.3.9)(eslint-import-resolver-webpack@0.13.8(eslint-plugin-import@2.29.1)(webpack@5.94.0(@swc/core@1.3.107(@swc/helpers@0.5.12))))(eslint@9.9.1(jiti@1.21.6)): + eslint-module-utils@2.8.2(@typescript-eslint/parser@5.62.0(eslint@9.9.1(jiti@1.21.6))(typescript@5.6.2))(eslint-import-resolver-node@0.3.9)(eslint-import-resolver-webpack@0.13.8)(eslint@9.9.1(jiti@1.21.6)): dependencies: debug: 3.2.7(supports-color@8.1.1) optionalDependencies: @@ -66192,7 +66108,7 @@ snapshots: transitivePeerDependencies: - supports-color - eslint-module-utils@2.8.2(@typescript-eslint/parser@8.18.2(eslint@8.57.1)(typescript@5.6.2))(eslint-import-resolver-node@0.3.9)(eslint-import-resolver-webpack@0.13.8(eslint-plugin-import@2.29.1)(webpack@5.94.0(@swc/core@1.3.107(@swc/helpers@0.5.12))))(eslint@8.57.1): + eslint-module-utils@2.8.2(@typescript-eslint/parser@8.18.2(eslint@8.57.1)(typescript@5.6.2))(eslint-import-resolver-node@0.3.9)(eslint-import-resolver-webpack@0.13.8)(eslint@8.57.1): dependencies: debug: 3.2.7(supports-color@8.1.1) optionalDependencies: @@ -66270,7 +66186,7 @@ snapshots: dependencies: htmlparser2: 9.1.0 - eslint-plugin-import@2.29.1(@typescript-eslint/parser@5.62.0(eslint@9.9.1(jiti@1.21.6))(typescript@5.6.2))(eslint-import-resolver-webpack@0.13.8(eslint-plugin-import@2.29.1)(webpack@5.94.0(@swc/core@1.3.107(@swc/helpers@0.5.12))))(eslint@9.9.1(jiti@1.21.6)): + eslint-plugin-import@2.29.1(@typescript-eslint/parser@5.62.0(eslint@9.9.1(jiti@1.21.6))(typescript@5.6.2))(eslint-import-resolver-webpack@0.13.8)(eslint@9.9.1(jiti@1.21.6)): dependencies: array-includes: 3.1.8 array.prototype.findlastindex: 1.2.5 @@ -66280,7 +66196,7 @@ snapshots: doctrine: 2.1.0 eslint: 9.9.1(jiti@1.21.6) eslint-import-resolver-node: 0.3.9 - eslint-module-utils: 2.8.2(@typescript-eslint/parser@5.62.0(eslint@9.9.1(jiti@1.21.6))(typescript@5.6.2))(eslint-import-resolver-node@0.3.9)(eslint-import-resolver-webpack@0.13.8(eslint-plugin-import@2.29.1)(webpack@5.94.0(@swc/core@1.3.107(@swc/helpers@0.5.12))))(eslint@9.9.1(jiti@1.21.6)) + eslint-module-utils: 2.8.2(@typescript-eslint/parser@5.62.0(eslint@9.9.1(jiti@1.21.6))(typescript@5.6.2))(eslint-import-resolver-node@0.3.9)(eslint-import-resolver-webpack@0.13.8)(eslint@9.9.1(jiti@1.21.6)) hasown: 2.0.2 is-core-module: 2.15.1 is-glob: 4.0.3 @@ -66307,7 +66223,7 @@ snapshots: doctrine: 2.1.0 eslint: 8.57.1 eslint-import-resolver-node: 0.3.9 - eslint-module-utils: 2.8.2(@typescript-eslint/parser@8.18.2(eslint@8.57.1)(typescript@5.6.2))(eslint-import-resolver-node@0.3.9)(eslint-import-resolver-webpack@0.13.8(eslint-plugin-import@2.29.1)(webpack@5.94.0(@swc/core@1.3.107(@swc/helpers@0.5.12))))(eslint@8.57.1) + eslint-module-utils: 2.8.2(@typescript-eslint/parser@8.18.2(eslint@8.57.1)(typescript@5.6.2))(eslint-import-resolver-node@0.3.9)(eslint-import-resolver-webpack@0.13.8)(eslint@8.57.1) hasown: 2.0.2 is-core-module: 2.15.1 is-glob: 4.0.3 @@ -66541,7 +66457,7 @@ snapshots: dependencies: eslint: 8.57.1 - eslint-plugin-sonarjs@2.0.1(@typescript-eslint/parser@8.18.2(eslint@8.57.1)(typescript@5.6.2))(eslint-import-resolver-webpack@0.13.8(eslint-plugin-import@2.29.1)(webpack@5.94.0(@swc/core@1.3.107(@swc/helpers@0.5.12))))(eslint@8.57.1): + eslint-plugin-sonarjs@2.0.1(@typescript-eslint/parser@8.18.2(eslint@8.57.1)(typescript@5.6.2))(eslint-import-resolver-webpack@0.13.8)(eslint@8.57.1): dependencies: '@babel/core': 7.24.3 '@babel/eslint-parser': 7.24.1(@babel/core@7.24.3)(eslint@8.57.1) @@ -67345,8 +67261,6 @@ snapshots: fflate@0.4.8: {} - fflate@0.8.1: {} - fflate@0.8.2: {} figures@1.7.0: @@ -67386,7 +67300,7 @@ snapshots: schema-utils: 3.3.0 webpack: 5.78.0(@swc/core@1.3.107(@swc/helpers@0.5.12)) - file-loader@6.2.0(webpack@5.78.0(@swc/core@1.7.26(@swc/helpers@0.5.12))(webpack-cli@5.1.4)): + file-loader@6.2.0(webpack@5.78.0): dependencies: loader-utils: 2.0.4 schema-utils: 3.3.0 @@ -67712,7 +67626,7 @@ snapshots: typescript: 5.6.2 webpack: 5.78.0(@swc/core@1.3.107(@swc/helpers@0.5.12)) - fork-ts-checker-webpack-plugin@8.0.0(typescript@5.6.2)(webpack@5.78.0(@swc/core@1.7.26(@swc/helpers@0.5.12))(webpack-cli@5.1.4)): + fork-ts-checker-webpack-plugin@8.0.0(typescript@5.6.2)(webpack@5.78.0): dependencies: '@babel/code-frame': 7.24.7 chalk: 4.1.2 @@ -68960,7 +68874,7 @@ snapshots: tapable: 2.2.1 webpack: 5.78.0(@swc/core@1.3.107(@swc/helpers@0.5.12)) - html-webpack-plugin@5.5.3(webpack@5.78.0(@swc/core@1.7.26(@swc/helpers@0.5.12))(webpack-cli@5.1.4)): + html-webpack-plugin@5.5.3(webpack@5.78.0): dependencies: '@types/html-minifier-terser': 6.1.0 html-minifier-terser: 6.1.0 @@ -74309,13 +74223,13 @@ snapshots: neo-async@2.6.2: {} - nest-raven@10.1.0(@nestjs/common@10.4.1(class-transformer@0.5.1)(class-validator@0.14.1)(reflect-metadata@0.2.2)(rxjs@7.8.1))(@nestjs/core@10.4.1(@nestjs/common@10.4.1(class-transformer@0.5.1)(class-validator@0.14.1)(reflect-metadata@0.2.2)(rxjs@7.8.1))(@nestjs/platform-express@10.4.1)(@nestjs/websockets@10.4.1)(encoding@0.1.13)(reflect-metadata@0.2.2)(rxjs@7.8.1))(@sentry/node@8.33.1)(class-transformer@0.5.1)(class-validator@0.14.1)(graphql@16.9.0)(reflect-metadata@0.2.2)(rxjs@7.8.1): + nest-raven@10.1.0(@nestjs/common@10.4.1(class-transformer@0.5.1)(class-validator@0.14.1)(reflect-metadata@0.2.2)(rxjs@7.8.1))(@nestjs/core@10.4.1)(@sentry/node@8.33.1)(class-transformer@0.5.1)(class-validator@0.14.1)(graphql@16.9.0)(reflect-metadata@0.2.2)(rxjs@7.8.1): dependencies: '@nestjs/common': 10.4.1(class-transformer@0.5.1)(class-validator@0.14.1)(reflect-metadata@0.2.2)(rxjs@7.8.1) '@sentry/node': 8.33.1 rxjs: 7.8.1 optionalDependencies: - '@nestjs/graphql': 12.0.9(@nestjs/common@10.4.1(class-transformer@0.5.1)(class-validator@0.14.1)(reflect-metadata@0.2.2)(rxjs@7.8.1))(@nestjs/core@10.4.1(@nestjs/common@10.4.1(class-transformer@0.5.1)(class-validator@0.14.1)(reflect-metadata@0.2.2)(rxjs@7.8.1))(@nestjs/platform-express@10.4.1)(@nestjs/websockets@10.4.1)(encoding@0.1.13)(reflect-metadata@0.2.2)(rxjs@7.8.1))(class-transformer@0.5.1)(class-validator@0.14.1)(graphql@16.9.0)(reflect-metadata@0.2.2) + '@nestjs/graphql': 12.0.9(@nestjs/common@10.4.1(class-transformer@0.5.1)(class-validator@0.14.1)(reflect-metadata@0.2.2)(rxjs@7.8.1))(@nestjs/core@10.4.1)(class-transformer@0.5.1)(class-validator@0.14.1)(graphql@16.9.0)(reflect-metadata@0.2.2) transitivePeerDependencies: - '@apollo/subgraph' - '@nestjs/core' @@ -74329,7 +74243,7 @@ snapshots: nested-error-stacks@2.0.1: {} - nestjs-otel@6.1.1(@nestjs/common@10.4.1(class-transformer@0.5.1)(class-validator@0.14.1)(reflect-metadata@0.2.2)(rxjs@7.8.1))(@nestjs/core@10.4.1(@nestjs/common@10.4.1(class-transformer@0.5.1)(class-validator@0.14.1)(reflect-metadata@0.2.2)(rxjs@7.8.1))(@nestjs/platform-express@10.4.1)(@nestjs/websockets@10.4.1)(encoding@0.1.13)(reflect-metadata@0.2.2)(rxjs@7.8.1)): + nestjs-otel@6.1.1(@nestjs/common@10.4.1(class-transformer@0.5.1)(class-validator@0.14.1)(reflect-metadata@0.2.2)(rxjs@7.8.1))(@nestjs/core@10.4.1): dependencies: '@nestjs/common': 10.4.1(class-transformer@0.5.1)(class-validator@0.14.1)(reflect-metadata@0.2.2)(rxjs@7.8.1) '@nestjs/core': 10.4.1(@nestjs/common@10.4.1(class-transformer@0.5.1)(class-validator@0.14.1)(reflect-metadata@0.2.2)(rxjs@7.8.1))(@nestjs/platform-express@10.4.1)(@nestjs/websockets@10.4.1)(encoding@0.1.13)(reflect-metadata@0.2.2)(rxjs@7.8.1) @@ -78000,14 +77914,14 @@ snapshots: regenerator-runtime: 0.13.11 whatwg-fetch: 3.6.2 - react-app-rewired@2.2.1(react-scripts@5.0.1(@babel/plugin-syntax-flow@7.24.7(@babel/core@7.22.11))(@babel/plugin-transform-react-jsx@7.25.2(@babel/core@7.22.11))(@swc/core@1.3.107(@swc/helpers@0.5.12))(@types/babel__core@7.20.5)(@types/webpack@5.28.5(@swc/core@1.3.107(@swc/helpers@0.5.12))(esbuild@0.18.20))(esbuild@0.18.20)(eslint-import-resolver-webpack@0.13.8(eslint-plugin-import@2.29.1)(webpack@5.94.0(@swc/core@1.3.107(@swc/helpers@0.5.12))))(eslint@9.9.1(jiti@1.21.6))(react@18.3.1)(sass@1.77.8)(ts-node@10.9.1(@swc/core@1.3.107(@swc/helpers@0.5.12))(@types/node@18.16.9)(typescript@5.6.2))(type-fest@2.19.0)(typescript@5.6.2)(vue-template-compiler@2.7.16)(webpack-hot-middleware@2.26.1)): + react-app-rewired@2.2.1(react-scripts@5.0.1(@babel/plugin-syntax-flow@7.24.7(@babel/core@7.22.11))(@babel/plugin-transform-react-jsx@7.25.2(@babel/core@7.22.11))(@swc/core@1.3.107(@swc/helpers@0.5.12))(@types/babel__core@7.20.5)(@types/webpack@5.28.5(@swc/core@1.3.107(@swc/helpers@0.5.12))(esbuild@0.18.20))(esbuild@0.18.20)(eslint-import-resolver-webpack@0.13.8)(eslint@9.9.1(jiti@1.21.6))(react@18.3.1)(sass@1.77.8)(ts-node@10.9.1(@swc/core@1.3.107(@swc/helpers@0.5.12))(@types/node@18.16.9)(typescript@5.6.2))(type-fest@2.19.0)(typescript@5.6.2)(vue-template-compiler@2.7.16)(webpack-hot-middleware@2.26.1)): dependencies: - react-scripts: 5.0.1(@babel/plugin-syntax-flow@7.24.7(@babel/core@7.22.11))(@babel/plugin-transform-react-jsx@7.25.2(@babel/core@7.22.11))(@swc/core@1.3.107(@swc/helpers@0.5.12))(@types/babel__core@7.20.5)(@types/webpack@5.28.5(@swc/core@1.3.107(@swc/helpers@0.5.12))(esbuild@0.18.20))(esbuild@0.18.20)(eslint-import-resolver-webpack@0.13.8(eslint-plugin-import@2.29.1)(webpack@5.94.0(@swc/core@1.3.107(@swc/helpers@0.5.12))))(eslint@9.9.1(jiti@1.21.6))(react@18.3.1)(sass@1.77.8)(ts-node@10.9.1(@swc/core@1.3.107(@swc/helpers@0.5.12))(@types/node@18.16.9)(typescript@5.6.2))(type-fest@2.19.0)(typescript@5.6.2)(vue-template-compiler@2.7.16)(webpack-hot-middleware@2.26.1) + react-scripts: 5.0.1(@babel/plugin-syntax-flow@7.24.7(@babel/core@7.22.11))(@babel/plugin-transform-react-jsx@7.25.2(@babel/core@7.22.11))(@swc/core@1.3.107(@swc/helpers@0.5.12))(@types/babel__core@7.20.5)(@types/webpack@5.28.5(@swc/core@1.3.107(@swc/helpers@0.5.12))(esbuild@0.18.20))(esbuild@0.18.20)(eslint-import-resolver-webpack@0.13.8)(eslint@9.9.1(jiti@1.21.6))(react@18.3.1)(sass@1.77.8)(ts-node@10.9.1(@swc/core@1.3.107(@swc/helpers@0.5.12))(@types/node@18.16.9)(typescript@5.6.2))(type-fest@2.19.0)(typescript@5.6.2)(vue-template-compiler@2.7.16)(webpack-hot-middleware@2.26.1) semver: 5.7.2 - react-app-rewired@2.2.1(react-scripts@5.0.1(@babel/plugin-syntax-flow@7.24.7(@babel/core@7.25.2))(@babel/plugin-transform-react-jsx@7.25.2(@babel/core@7.25.2))(@swc/core@1.3.107(@swc/helpers@0.5.12))(@types/babel__core@7.20.5)(@types/webpack@5.28.5(@swc/core@1.3.107(@swc/helpers@0.5.12)))(eslint-import-resolver-webpack@0.13.8(eslint-plugin-import@2.29.1)(webpack@5.94.0(@swc/core@1.3.107(@swc/helpers@0.5.12))))(eslint@9.9.1(jiti@1.21.6))(react@18.3.1)(sass@1.77.8)(ts-node@10.9.1(@swc/core@1.3.107(@swc/helpers@0.5.12))(@types/node@18.16.9)(typescript@5.6.2))(type-fest@2.19.0)(typescript@5.6.2)(vue-template-compiler@2.7.16)(webpack-hot-middleware@2.26.1)): + react-app-rewired@2.2.1(react-scripts@5.0.1(@babel/plugin-syntax-flow@7.24.7(@babel/core@7.25.2))(@babel/plugin-transform-react-jsx@7.25.2(@babel/core@7.25.2))(@swc/core@1.3.107(@swc/helpers@0.5.12))(@types/babel__core@7.20.5)(@types/webpack@5.28.5(@swc/core@1.3.107(@swc/helpers@0.5.12)))(eslint-import-resolver-webpack@0.13.8)(eslint@9.9.1(jiti@1.21.6))(react@18.3.1)(sass@1.77.8)(ts-node@10.9.1(@swc/core@1.3.107(@swc/helpers@0.5.12))(@types/node@18.16.9)(typescript@5.6.2))(type-fest@2.19.0)(typescript@5.6.2)(vue-template-compiler@2.7.16)(webpack-hot-middleware@2.26.1)): dependencies: - react-scripts: 5.0.1(@babel/plugin-syntax-flow@7.24.7(@babel/core@7.25.2))(@babel/plugin-transform-react-jsx@7.25.2(@babel/core@7.25.2))(@swc/core@1.3.107(@swc/helpers@0.5.12))(@types/babel__core@7.20.5)(@types/webpack@5.28.5(@swc/core@1.3.107(@swc/helpers@0.5.12)))(eslint-import-resolver-webpack@0.13.8(eslint-plugin-import@2.29.1)(webpack@5.94.0(@swc/core@1.3.107(@swc/helpers@0.5.12))))(eslint@9.9.1(jiti@1.21.6))(react@18.3.1)(sass@1.77.8)(ts-node@10.9.1(@swc/core@1.3.107(@swc/helpers@0.5.12))(@types/node@18.16.9)(typescript@5.6.2))(type-fest@2.19.0)(typescript@5.6.2)(vue-template-compiler@2.7.16)(webpack-hot-middleware@2.26.1) + react-scripts: 5.0.1(@babel/plugin-syntax-flow@7.24.7(@babel/core@7.25.2))(@babel/plugin-transform-react-jsx@7.25.2(@babel/core@7.25.2))(@swc/core@1.3.107(@swc/helpers@0.5.12))(@types/babel__core@7.20.5)(@types/webpack@5.28.5(@swc/core@1.3.107(@swc/helpers@0.5.12)))(eslint-import-resolver-webpack@0.13.8)(eslint@9.9.1(jiti@1.21.6))(react@18.3.1)(sass@1.77.8)(ts-node@10.9.1(@swc/core@1.3.107(@swc/helpers@0.5.12))(@types/node@18.16.9)(typescript@5.6.2))(type-fest@2.19.0)(typescript@5.6.2)(vue-template-compiler@2.7.16)(webpack-hot-middleware@2.26.1) semver: 5.7.2 react-chartjs-2@4.3.1(chart.js@3.9.1)(react@18.3.1): @@ -78462,7 +78376,7 @@ snapshots: transitivePeerDependencies: - supports-color - react-scripts@5.0.1(@babel/plugin-syntax-flow@7.24.7(@babel/core@7.22.11))(@babel/plugin-transform-react-jsx@7.25.2(@babel/core@7.22.11))(@swc/core@1.3.107(@swc/helpers@0.5.12))(@types/babel__core@7.20.5)(@types/webpack@5.28.5(@swc/core@1.3.107(@swc/helpers@0.5.12))(esbuild@0.18.20))(esbuild@0.18.20)(eslint-import-resolver-webpack@0.13.8(eslint-plugin-import@2.29.1)(webpack@5.94.0(@swc/core@1.3.107(@swc/helpers@0.5.12))))(eslint@9.9.1(jiti@1.21.6))(react@18.3.1)(sass@1.77.8)(ts-node@10.9.1(@swc/core@1.3.107(@swc/helpers@0.5.12))(@types/node@18.16.9)(typescript@5.6.2))(type-fest@2.19.0)(typescript@5.6.2)(vue-template-compiler@2.7.16)(webpack-hot-middleware@2.26.1): + react-scripts@5.0.1(@babel/plugin-syntax-flow@7.24.7(@babel/core@7.22.11))(@babel/plugin-transform-react-jsx@7.25.2(@babel/core@7.22.11))(@swc/core@1.3.107(@swc/helpers@0.5.12))(@types/babel__core@7.20.5)(@types/webpack@5.28.5(@swc/core@1.3.107(@swc/helpers@0.5.12))(esbuild@0.18.20))(esbuild@0.18.20)(eslint-import-resolver-webpack@0.13.8)(eslint@9.9.1(jiti@1.21.6))(react@18.3.1)(sass@1.77.8)(ts-node@10.9.1(@swc/core@1.3.107(@swc/helpers@0.5.12))(@types/node@18.16.9)(typescript@5.6.2))(type-fest@2.19.0)(typescript@5.6.2)(vue-template-compiler@2.7.16)(webpack-hot-middleware@2.26.1): dependencies: '@babel/core': 7.21.4 '@pmmmwh/react-refresh-webpack-plugin': 0.5.10(@types/webpack@5.28.5(@swc/core@1.3.107(@swc/helpers@0.5.12))(esbuild@0.18.20))(react-refresh@0.11.0)(type-fest@2.19.0)(webpack-dev-server@4.11.1(webpack@5.78.0(@swc/core@1.3.107(@swc/helpers@0.5.12))(esbuild@0.18.20)))(webpack-hot-middleware@2.26.1)(webpack@5.78.0(@swc/core@1.3.107(@swc/helpers@0.5.12))(esbuild@0.18.20)) @@ -78480,7 +78394,7 @@ snapshots: dotenv: 10.0.0 dotenv-expand: 5.1.0 eslint: 9.9.1(jiti@1.21.6) - eslint-config-react-app: 7.0.1(@babel/plugin-syntax-flow@7.24.7(@babel/core@7.22.11))(@babel/plugin-transform-react-jsx@7.25.2(@babel/core@7.22.11))(eslint-import-resolver-webpack@0.13.8(eslint-plugin-import@2.29.1)(webpack@5.94.0(@swc/core@1.3.107(@swc/helpers@0.5.12))))(eslint@9.9.1(jiti@1.21.6))(jest@27.5.1(ts-node@10.9.1(@swc/core@1.3.107(@swc/helpers@0.5.12))(@types/node@18.16.9)(typescript@5.6.2)))(typescript@5.6.2) + eslint-config-react-app: 7.0.1(@babel/plugin-syntax-flow@7.24.7(@babel/core@7.22.11))(@babel/plugin-transform-react-jsx@7.25.2(@babel/core@7.22.11))(eslint-import-resolver-webpack@0.13.8)(eslint@9.9.1(jiti@1.21.6))(jest@27.5.1(ts-node@10.9.1(@swc/core@1.3.107(@swc/helpers@0.5.12))(@types/node@18.16.9)(typescript@5.6.2)))(typescript@5.6.2) eslint-webpack-plugin: 3.2.0(eslint@9.9.1(jiti@1.21.6))(webpack@5.78.0(@swc/core@1.3.107(@swc/helpers@0.5.12))(esbuild@0.18.20)) file-loader: 6.2.0(webpack@5.78.0(@swc/core@1.3.107(@swc/helpers@0.5.12))(esbuild@0.18.20)) fs-extra: 10.1.0 @@ -78548,7 +78462,7 @@ snapshots: - webpack-hot-middleware - webpack-plugin-serve - react-scripts@5.0.1(@babel/plugin-syntax-flow@7.24.7(@babel/core@7.25.2))(@babel/plugin-transform-react-jsx@7.25.2(@babel/core@7.25.2))(@swc/core@1.3.107(@swc/helpers@0.5.12))(@types/babel__core@7.20.5)(@types/webpack@5.28.5(@swc/core@1.3.107(@swc/helpers@0.5.12)))(eslint-import-resolver-webpack@0.13.8(eslint-plugin-import@2.29.1)(webpack@5.94.0(@swc/core@1.3.107(@swc/helpers@0.5.12))))(eslint@9.9.1(jiti@1.21.6))(react@18.3.1)(sass@1.77.8)(ts-node@10.9.1(@swc/core@1.3.107(@swc/helpers@0.5.12))(@types/node@18.16.9)(typescript@5.6.2))(type-fest@2.19.0)(typescript@5.6.2)(vue-template-compiler@2.7.16)(webpack-hot-middleware@2.26.1): + react-scripts@5.0.1(@babel/plugin-syntax-flow@7.24.7(@babel/core@7.25.2))(@babel/plugin-transform-react-jsx@7.25.2(@babel/core@7.25.2))(@swc/core@1.3.107(@swc/helpers@0.5.12))(@types/babel__core@7.20.5)(@types/webpack@5.28.5(@swc/core@1.3.107(@swc/helpers@0.5.12)))(eslint-import-resolver-webpack@0.13.8)(eslint@9.9.1(jiti@1.21.6))(react@18.3.1)(sass@1.77.8)(ts-node@10.9.1(@swc/core@1.3.107(@swc/helpers@0.5.12))(@types/node@18.16.9)(typescript@5.6.2))(type-fest@2.19.0)(typescript@5.6.2)(vue-template-compiler@2.7.16)(webpack-hot-middleware@2.26.1): dependencies: '@babel/core': 7.21.4 '@pmmmwh/react-refresh-webpack-plugin': 0.5.10(@types/webpack@5.28.5(@swc/core@1.3.107(@swc/helpers@0.5.12)))(react-refresh@0.11.0)(type-fest@2.19.0)(webpack-dev-server@4.11.1(webpack@5.78.0(@swc/core@1.3.107(@swc/helpers@0.5.12))))(webpack-hot-middleware@2.26.1)(webpack@5.78.0(@swc/core@1.3.107(@swc/helpers@0.5.12))) @@ -78566,7 +78480,7 @@ snapshots: dotenv: 10.0.0 dotenv-expand: 5.1.0 eslint: 9.9.1(jiti@1.21.6) - eslint-config-react-app: 7.0.1(@babel/plugin-syntax-flow@7.24.7(@babel/core@7.25.2))(@babel/plugin-transform-react-jsx@7.25.2(@babel/core@7.25.2))(eslint-import-resolver-webpack@0.13.8(eslint-plugin-import@2.29.1)(webpack@5.94.0(@swc/core@1.3.107(@swc/helpers@0.5.12))))(eslint@9.9.1(jiti@1.21.6))(jest@27.5.1(ts-node@10.9.1(@swc/core@1.3.107(@swc/helpers@0.5.12))(@types/node@18.16.9)(typescript@5.6.2)))(typescript@5.6.2) + eslint-config-react-app: 7.0.1(@babel/plugin-syntax-flow@7.24.7(@babel/core@7.25.2))(@babel/plugin-transform-react-jsx@7.25.2(@babel/core@7.25.2))(eslint-import-resolver-webpack@0.13.8)(eslint@9.9.1(jiti@1.21.6))(jest@27.5.1(ts-node@10.9.1(@swc/core@1.3.107(@swc/helpers@0.5.12))(@types/node@18.16.9)(typescript@5.6.2)))(typescript@5.6.2) eslint-webpack-plugin: 3.2.0(eslint@9.9.1(jiti@1.21.6))(webpack@5.78.0(@swc/core@1.3.107(@swc/helpers@0.5.12))) file-loader: 6.2.0(webpack@5.78.0(@swc/core@1.3.107(@swc/helpers@0.5.12))) fs-extra: 10.1.0 @@ -80070,6 +79984,12 @@ snapshots: snake-case: 3.0.4 type-fest: 2.19.0 + snakecase-keys@8.0.1: + dependencies: + map-obj: 4.3.0 + snake-case: 3.0.4 + type-fest: 4.18.2 + snapdragon-node@2.1.1: dependencies: define-property: 1.0.0 @@ -80801,7 +80721,7 @@ snapshots: dependencies: webpack: 5.78.0(@swc/core@1.3.107(@swc/helpers@0.5.12)) - style-loader@3.3.2(webpack@5.78.0(@swc/core@1.7.26(@swc/helpers@0.5.12))(webpack-cli@5.1.4)): + style-loader@3.3.2(webpack@5.78.0): dependencies: webpack: 5.78.0(@swc/core@1.7.26(@swc/helpers@0.5.12))(webpack-cli@5.1.4) @@ -81103,7 +81023,7 @@ snapshots: '@swc/core': 1.3.107(@swc/helpers@0.5.12) webpack: 5.78.0(@swc/core@1.3.107(@swc/helpers@0.5.12)) - swc-loader@0.2.3(@swc/core@1.3.107(@swc/helpers@0.5.12))(webpack@5.78.0(@swc/core@1.7.26(@swc/helpers@0.5.12))(webpack-cli@5.1.4)): + swc-loader@0.2.3(@swc/core@1.3.107(@swc/helpers@0.5.12))(webpack@5.78.0): dependencies: '@swc/core': 1.3.107(@swc/helpers@0.5.12) webpack: 5.78.0(@swc/core@1.7.26(@swc/helpers@0.5.12))(webpack-cli@5.1.4) @@ -81448,7 +81368,7 @@ snapshots: optionalDependencies: '@swc/core': 1.3.107(@swc/helpers@0.5.12) - terser-webpack-plugin@5.3.10(@swc/core@1.3.107(@swc/helpers@0.5.12))(webpack@5.78.0(@swc/core@1.7.26(@swc/helpers@0.5.12))(webpack-cli@5.1.4)): + terser-webpack-plugin@5.3.10(@swc/core@1.3.107(@swc/helpers@0.5.12))(webpack@5.78.0): dependencies: '@jridgewell/trace-mapping': 0.3.25 jest-worker: 27.5.1 @@ -81470,7 +81390,7 @@ snapshots: optionalDependencies: '@swc/core': 1.3.107(@swc/helpers@0.5.12) - terser-webpack-plugin@5.3.10(@swc/core@1.7.26(@swc/helpers@0.5.12))(esbuild@0.23.1)(webpack@5.78.0(@swc/core@1.7.26(@swc/helpers@0.5.12))(esbuild@0.23.1)(webpack-cli@5.1.4)): + terser-webpack-plugin@5.3.10(@swc/core@1.7.26(@swc/helpers@0.5.12))(esbuild@0.23.1)(webpack@5.78.0): dependencies: '@jridgewell/trace-mapping': 0.3.25 jest-worker: 27.5.1 @@ -81482,25 +81402,25 @@ snapshots: '@swc/core': 1.7.26(@swc/helpers@0.5.12) esbuild: 0.23.1 - terser-webpack-plugin@5.3.10(@swc/core@1.7.26(@swc/helpers@0.5.12))(webpack@5.78.0(@swc/core@1.7.26(@swc/helpers@0.5.12))(webpack-cli@5.1.4)): + terser-webpack-plugin@5.3.10(@swc/core@1.7.26(@swc/helpers@0.5.12))(webpack@5.78.0(@swc/core@1.7.26(@swc/helpers@0.5.12))): dependencies: '@jridgewell/trace-mapping': 0.3.25 jest-worker: 27.5.1 schema-utils: 3.3.0 serialize-javascript: 6.0.2 terser: 5.31.6 - webpack: 5.78.0(@swc/core@1.7.26(@swc/helpers@0.5.12))(webpack-cli@5.1.4) + webpack: 5.78.0(@swc/core@1.7.26(@swc/helpers@0.5.12)) optionalDependencies: '@swc/core': 1.7.26(@swc/helpers@0.5.12) - terser-webpack-plugin@5.3.10(@swc/core@1.7.26(@swc/helpers@0.5.12))(webpack@5.78.0(@swc/core@1.7.26(@swc/helpers@0.5.12))): + terser-webpack-plugin@5.3.10(@swc/core@1.7.26(@swc/helpers@0.5.12))(webpack@5.78.0): dependencies: '@jridgewell/trace-mapping': 0.3.25 jest-worker: 27.5.1 schema-utils: 3.3.0 serialize-javascript: 6.0.2 terser: 5.31.6 - webpack: 5.78.0(@swc/core@1.7.26(@swc/helpers@0.5.12)) + webpack: 5.78.0(@swc/core@1.7.26(@swc/helpers@0.5.12))(webpack-cli@5.1.4) optionalDependencies: '@swc/core': 1.7.26(@swc/helpers@0.5.12) @@ -81538,7 +81458,7 @@ snapshots: optionalDependencies: '@swc/core': 1.3.107(@swc/helpers@0.5.12) - terser-webpack-plugin@5.3.9(@swc/core@1.7.26(@swc/helpers@0.5.12))(esbuild@0.23.1)(webpack@5.78.0(@swc/core@1.7.26(@swc/helpers@0.5.12))(esbuild@0.23.1)(webpack-cli@5.1.4)): + terser-webpack-plugin@5.3.9(@swc/core@1.7.26(@swc/helpers@0.5.12))(esbuild@0.23.1)(webpack@5.78.0): dependencies: '@jridgewell/trace-mapping': 0.3.25 jest-worker: 27.5.1 @@ -82075,7 +81995,7 @@ snapshots: typescript: 5.6.2 webpack: 5.94.0(@swc/core@1.7.26(@swc/helpers@0.5.12)) - ts-loader@9.4.4(typescript@5.6.2)(webpack@5.78.0(@swc/core@1.7.26(@swc/helpers@0.5.12))(esbuild@0.23.1)(webpack-cli@5.1.4)): + ts-loader@9.4.4(typescript@5.6.2)(webpack@5.78.0): dependencies: chalk: 4.1.2 enhanced-resolve: 5.17.1 @@ -82084,15 +82004,6 @@ snapshots: typescript: 5.6.2 webpack: 5.78.0(@swc/core@1.7.26(@swc/helpers@0.5.12))(esbuild@0.23.1)(webpack-cli@5.1.4) - ts-loader@9.4.4(typescript@5.6.2)(webpack@5.78.0(@swc/core@1.7.26(@swc/helpers@0.5.12))(webpack-cli@5.1.4)): - dependencies: - chalk: 4.1.2 - enhanced-resolve: 5.17.1 - micromatch: 4.0.8 - semver: 7.6.3 - typescript: 5.6.2 - webpack: 5.78.0(@swc/core@1.7.26(@swc/helpers@0.5.12))(webpack-cli@5.1.4) - ts-loader@9.4.4(typescript@5.6.2)(webpack@5.94.0(@swc/core@1.7.26(@swc/helpers@0.5.12))): dependencies: chalk: 4.1.2 @@ -83029,14 +82940,14 @@ snapshots: url-join@5.0.0: {} - url-loader@4.1.1(file-loader@6.2.0(webpack@5.78.0(@swc/core@1.7.26(@swc/helpers@0.5.12))(webpack-cli@5.1.4)))(webpack@5.78.0(@swc/core@1.7.26(@swc/helpers@0.5.12))(webpack-cli@5.1.4)): + url-loader@4.1.1(file-loader@6.2.0(webpack@5.78.0))(webpack@5.78.0): dependencies: loader-utils: 2.0.4 mime-types: 2.1.35 schema-utils: 3.3.0 webpack: 5.78.0(@swc/core@1.7.26(@swc/helpers@0.5.12))(webpack-cli@5.1.4) optionalDependencies: - file-loader: 6.2.0(webpack@5.78.0(@swc/core@1.7.26(@swc/helpers@0.5.12))(webpack-cli@5.1.4)) + file-loader: 6.2.0(webpack@5.78.0) url-loader@4.1.1(file-loader@6.2.0(webpack@5.94.0(@swc/core@1.7.26(@swc/helpers@0.5.12))))(webpack@5.94.0(@swc/core@1.7.26(@swc/helpers@0.5.12))): dependencies: @@ -84132,9 +84043,9 @@ snapshots: webpack-cli@5.1.4(webpack-bundle-analyzer@4.10.1)(webpack@5.78.0): dependencies: '@discoveryjs/json-ext': 0.5.7 - '@webpack-cli/configtest': 2.1.1(webpack-cli@5.1.4(webpack-bundle-analyzer@4.10.1)(webpack@5.78.0))(webpack@5.78.0(@swc/core@1.7.26(@swc/helpers@0.5.12))(esbuild@0.23.1)(webpack-cli@5.1.4)) - '@webpack-cli/info': 2.0.2(webpack-cli@5.1.4(webpack-bundle-analyzer@4.10.1)(webpack@5.78.0))(webpack@5.78.0(@swc/core@1.7.26(@swc/helpers@0.5.12))(esbuild@0.23.1)(webpack-cli@5.1.4)) - '@webpack-cli/serve': 2.0.5(webpack-cli@5.1.4(webpack-bundle-analyzer@4.10.1)(webpack@5.78.0))(webpack@5.78.0(@swc/core@1.7.26(@swc/helpers@0.5.12))(esbuild@0.23.1)(webpack-cli@5.1.4)) + '@webpack-cli/configtest': 2.1.1(webpack-cli@5.1.4)(webpack@5.78.0) + '@webpack-cli/info': 2.0.2(webpack-cli@5.1.4)(webpack@5.78.0) + '@webpack-cli/serve': 2.0.5(webpack-cli@5.1.4)(webpack@5.78.0) colorette: 2.0.20 commander: 10.0.1 cross-spawn: 7.0.3 @@ -84151,9 +84062,9 @@ snapshots: webpack-cli@5.1.4(webpack-bundle-analyzer@4.9.0)(webpack@5.78.0): dependencies: '@discoveryjs/json-ext': 0.5.7 - '@webpack-cli/configtest': 2.1.1(webpack-cli@5.1.4(webpack-bundle-analyzer@4.9.0)(webpack@5.78.0))(webpack@5.78.0(@swc/core@1.7.26(@swc/helpers@0.5.12))(webpack-cli@5.1.4)) - '@webpack-cli/info': 2.0.2(webpack-cli@5.1.4(webpack-bundle-analyzer@4.9.0)(webpack@5.78.0))(webpack@5.78.0(@swc/core@1.7.26(@swc/helpers@0.5.12))(webpack-cli@5.1.4)) - '@webpack-cli/serve': 2.0.5(webpack-cli@5.1.4(webpack-bundle-analyzer@4.9.0)(webpack@5.78.0))(webpack@5.78.0(@swc/core@1.7.26(@swc/helpers@0.5.12))(webpack-cli@5.1.4)) + '@webpack-cli/configtest': 2.1.1(webpack-cli@5.1.4)(webpack@5.78.0) + '@webpack-cli/info': 2.0.2(webpack-cli@5.1.4)(webpack@5.78.0) + '@webpack-cli/serve': 2.0.5(webpack-cli@5.1.4)(webpack@5.78.0) colorette: 2.0.20 commander: 10.0.1 cross-spawn: 7.0.3 @@ -84205,7 +84116,7 @@ snapshots: optionalDependencies: webpack: 5.78.0(@swc/core@1.3.107(@swc/helpers@0.5.12)) - webpack-dev-middleware@6.1.1(webpack@5.78.0(@swc/core@1.7.26(@swc/helpers@0.5.12))(webpack-cli@5.1.4)): + webpack-dev-middleware@6.1.1(webpack@5.78.0): dependencies: colorette: 2.0.19 memfs: 3.5.0 @@ -84401,7 +84312,7 @@ snapshots: - esbuild - uglify-js - webpack@5.78.0(@swc/core@1.3.107(@swc/helpers@0.5.12))(webpack-cli@5.1.4(webpack-bundle-analyzer@4.9.0)(webpack@5.78.0)): + webpack@5.78.0(@swc/core@1.3.107(@swc/helpers@0.5.12))(webpack-cli@5.1.4): dependencies: '@types/eslint-scope': 3.7.4 '@types/estree': 0.0.51 @@ -84424,7 +84335,7 @@ snapshots: neo-async: 2.6.2 schema-utils: 3.3.0 tapable: 2.2.1 - terser-webpack-plugin: 5.3.10(@swc/core@1.3.107(@swc/helpers@0.5.12))(webpack@5.78.0(@swc/core@1.7.26(@swc/helpers@0.5.12))(webpack-cli@5.1.4)) + terser-webpack-plugin: 5.3.10(@swc/core@1.3.107(@swc/helpers@0.5.12))(webpack@5.78.0) watchpack: 2.4.2 webpack-sources: 3.2.3 optionalDependencies: @@ -84488,7 +84399,7 @@ snapshots: neo-async: 2.6.2 schema-utils: 3.3.0 tapable: 2.2.1 - terser-webpack-plugin: 5.3.10(@swc/core@1.7.26(@swc/helpers@0.5.12))(esbuild@0.23.1)(webpack@5.78.0(@swc/core@1.7.26(@swc/helpers@0.5.12))(esbuild@0.23.1)(webpack-cli@5.1.4)) + terser-webpack-plugin: 5.3.10(@swc/core@1.7.26(@swc/helpers@0.5.12))(esbuild@0.23.1)(webpack@5.78.0) watchpack: 2.4.2 webpack-sources: 3.2.3 optionalDependencies: @@ -84521,7 +84432,7 @@ snapshots: neo-async: 2.6.2 schema-utils: 3.3.0 tapable: 2.2.1 - terser-webpack-plugin: 5.3.10(@swc/core@1.7.26(@swc/helpers@0.5.12))(webpack@5.78.0(@swc/core@1.7.26(@swc/helpers@0.5.12))(webpack-cli@5.1.4)) + terser-webpack-plugin: 5.3.10(@swc/core@1.7.26(@swc/helpers@0.5.12))(webpack@5.78.0) watchpack: 2.4.2 webpack-sources: 3.2.3 optionalDependencies: