Skip to content
Open
Show file tree
Hide file tree
Changes from all commits
Commits
File filter

Filter by extension

Filter by extension

Conversations
Failed to load comments.
Loading
Jump to
Jump to file
Failed to load files.
Loading
Diff view
Diff view
20 changes: 7 additions & 13 deletions apps/api/src/app/environments-v2/dtos/diff-environment.dto.ts
Original file line number Diff line number Diff line change
@@ -1,7 +1,7 @@
import { ApiProperty, ApiPropertyOptional } from '@nestjs/swagger';
import { Type } from 'class-transformer';
import { IsBoolean, IsDateString, IsEnum, IsNumber, IsOptional, IsString, ValidateNested } from 'class-validator';
import { DependencyReasonEnum, ResourceTypeEnum } from '../types/sync.types';
import { DependencyReasonEnum, DiffActionEnum, ResourceTypeEnum } from '../types/sync.types';

export class DiffEnvironmentRequestDto {
@ApiPropertyOptional({
Expand Down Expand Up @@ -36,17 +36,17 @@ export class UserInfoDto {
export class ResourceInfoDto {
@ApiPropertyOptional({
description: 'Resource ID (workflow ID or step ID)',
type: 'string',
nullable: true,
example: 'welcome-email-workflow',
})
@IsOptional()
@IsString()
id: string | null;

@ApiPropertyOptional({
description: 'Resource name (workflow name or step name)',
type: 'string',
nullable: true,
example: 'Welcome Email Workflow',
})
@IsOptional()
@IsString()
Expand Down Expand Up @@ -99,17 +99,17 @@ export class ResourceDiffDto {
description: 'Type of resource',
enum: [...Object.values(ResourceTypeEnum)],
enumName: 'ResourceTypeEnum',
example: 'workflow',
})
@IsEnum(ResourceTypeEnum)
resourceType: ResourceTypeEnum;

@ApiProperty({
description: 'Type of change',
enum: ['added', 'modified', 'deleted', 'unchanged', 'moved'],
enum: [...Object.values(DiffActionEnum)],
enumName: 'DiffActionEnum',
})
@IsEnum(['added', 'modified', 'deleted', 'unchanged', 'moved'])
action: 'added' | 'modified' | 'deleted' | 'unchanged' | 'moved';
@IsEnum(DiffActionEnum)
action: DiffActionEnum;

@ApiPropertyOptional({
type: 'object',
Expand Down Expand Up @@ -174,28 +174,24 @@ export class ResourceDependencyDto {
description: 'Type of dependent resource',
enum: [...Object.values(ResourceTypeEnum)],
enumName: 'ResourceTypeEnum',
example: 'layout',
})
@IsEnum(ResourceTypeEnum)
resourceType: ResourceTypeEnum;

@ApiProperty({
description: 'ID of the dependent resource',
example: 'layout-id-123',
})
@IsString()
resourceId: string;

@ApiProperty({
description: 'Name of the dependent resource',
example: 'Email Layout Template',
})
@IsString()
resourceName: string;

@ApiProperty({
description: 'Whether this dependency blocks the operation',
example: true,
})
@IsBoolean()
isBlocking: boolean;
Expand All @@ -204,7 +200,6 @@ export class ResourceDependencyDto {
description: 'Reason for the dependency',
enum: [...Object.values(DependencyReasonEnum)],
enumName: 'DependencyReasonEnum',
example: 'LAYOUT_REQUIRED_FOR_WORKFLOW',
})
@IsEnum(DependencyReasonEnum)
reason: DependencyReasonEnum;
Expand All @@ -215,7 +210,6 @@ export class ResourceDiffResultDto {
description: 'Type of resource being compared',
enum: [...Object.values(ResourceTypeEnum)],
enumName: 'ResourceTypeEnum',
example: 'workflow',
})
@IsEnum(ResourceTypeEnum)
resourceType: ResourceTypeEnum;
Expand Down
67 changes: 41 additions & 26 deletions apps/api/src/app/environments-v2/dtos/publish-environment.dto.ts
Original file line number Diff line number Diff line change
@@ -1,14 +1,13 @@
import { ApiProperty, ApiPropertyOptional } from '@nestjs/swagger';
import { Type } from 'class-transformer';
import { IsArray, IsBoolean, IsEnum, IsNumber, IsOptional, IsString, Max, Min, ValidateNested } from 'class-validator';
import { ResourceTypeEnum } from '../types/sync.types';
import { ResourceTypeEnum, SyncActionEnum } from '../types/sync.types';

export class ResourceToPublishDto {
@ApiProperty({
description: 'Type of resource to publish',
enum: Object.values(ResourceTypeEnum),
enumName: 'ResourceTypeEnum',
example: ResourceTypeEnum.WORKFLOW,
})
@IsEnum(ResourceTypeEnum)
resourceType: ResourceTypeEnum;
Expand Down Expand Up @@ -41,10 +40,6 @@ export class PublishEnvironmentRequestDto {
@ApiPropertyOptional({
description: 'Array of specific resources to publish. If not provided, all resources will be published.',
type: [ResourceToPublishDto],
example: [
{ resourceType: 'workflow', resourceId: 'workflow-id-1' },
{ resourceType: 'layout', resourceId: 'layout-id-1' },
],
})
@IsOptional()
@IsArray()
Expand All @@ -54,27 +49,39 @@ export class PublishEnvironmentRequestDto {
}

export class SyncedWorkflowDto {
@ApiProperty({ description: 'Resource type' })
resourceType: string;
@ApiProperty({
description: 'Resource type',
enum: Object.values(ResourceTypeEnum),
enumName: 'ResourceTypeEnum',
})
resourceType: ResourceTypeEnum;

@ApiProperty({ description: 'Workflow ID' })
@ApiProperty({ description: 'Resource ID' })
resourceId: string;

@ApiProperty({ description: 'Workflow name' })
@ApiProperty({ description: 'Resource name' })
resourceName: string;

@ApiProperty({ description: 'Sync action performed' })
action: 'created' | 'updated' | 'skipped' | 'deleted';
@ApiProperty({
description: 'Sync action performed',
enum: Object.values(SyncActionEnum),
enumName: 'SyncActionEnum',
})
action: SyncActionEnum;
}

export class FailedWorkflowDto {
@ApiProperty({ description: 'Resource type' })
resourceType: string;
@ApiProperty({
description: 'Resource type',
enum: Object.values(ResourceTypeEnum),
enumName: 'ResourceTypeEnum',
})
resourceType: ResourceTypeEnum;

@ApiProperty({ description: 'Workflow ID' })
@ApiProperty({ description: 'Resource ID' })
resourceId: string;

@ApiProperty({ description: 'Workflow name' })
@ApiProperty({ description: 'Resource name' })
resourceName: string;

@ApiProperty({ description: 'Error message' })
Expand All @@ -85,33 +92,41 @@ export class FailedWorkflowDto {
}

export class SkippedWorkflowDto {
@ApiProperty({ description: 'Resource type' })
resourceType: string;
@ApiProperty({
description: 'Resource type',
enum: Object.values(ResourceTypeEnum),
enumName: 'ResourceTypeEnum',
})
resourceType: ResourceTypeEnum;

@ApiProperty({ description: 'Workflow ID' })
@ApiProperty({ description: 'Resource ID' })
resourceId: string;

@ApiProperty({ description: 'Workflow name' })
@ApiProperty({ description: 'Resource name' })
resourceName: string;

@ApiProperty({ description: 'Reason for skipping' })
reason: string;
}

export class SyncResultDto {
@ApiProperty({ description: 'Resource type that was synced' })
resourceType: string;
@ApiProperty({
description: 'Resource type that was synced',
enum: Object.values(ResourceTypeEnum),
enumName: 'ResourceTypeEnum',
})
resourceType: ResourceTypeEnum;

@ApiProperty({ type: [SyncedWorkflowDto], description: 'Successfully synced workflows' })
@ApiProperty({ type: [SyncedWorkflowDto], description: 'Successfully synced resources' })
successful: SyncedWorkflowDto[];

@ApiProperty({ type: [FailedWorkflowDto], description: 'Failed workflow syncs' })
@ApiProperty({ type: [FailedWorkflowDto], description: 'Failed resource syncs' })
failed: FailedWorkflowDto[];

@ApiProperty({ type: [SkippedWorkflowDto], description: 'Skipped workflows' })
@ApiProperty({ type: [SkippedWorkflowDto], description: 'Skipped resources' })
skipped: SkippedWorkflowDto[];

@ApiProperty({ description: 'Total number of workflows processed' })
@ApiProperty({ description: 'Total number of resources processed' })
totalProcessed: number;
}

Expand Down
32 changes: 27 additions & 5 deletions apps/api/src/app/environments-v2/environments.controller.ts
Original file line number Diff line number Diff line change
Expand Up @@ -8,7 +8,7 @@ import {
Post,
UseInterceptors,
} from '@nestjs/common';
import { ApiExcludeEndpoint, ApiOperation, ApiParam, ApiTags } from '@nestjs/swagger';
import { ApiBody, ApiOperation, ApiParam, ApiTags } from '@nestjs/swagger';
import { RequirePermissions, SkipPermissionsCheck } from '@novu/application-generic';
import { PermissionsEnum, UserSessionData } from '@novu/shared';
import { RequireAuthentication } from '../auth/framework/auth.decorator';
Expand Down Expand Up @@ -73,10 +73,21 @@ export class EnvironmentsController {

@Post('/:targetEnvironmentId/publish')
@HttpCode(200)
@ApiOperation({ summary: 'Publish all workflows from source to target environment' })
@ApiOperation({
summary: 'Publish resources to target environment',
description:
'Publishes all workflows and resources from the source environment to the target environment. Optionally specify specific resources to publish or use dryRun mode to preview changes.',
})
@ApiParam({
name: 'targetEnvironmentId',
description: 'Target environment ID (MongoDB ObjectId) to publish resources to',
type: String,
example: '6615943e7ace93b0540ae377',
})
@ApiBody({ type: PublishEnvironmentRequestDto, description: 'Publish request configuration' })
@ApiResponse(PublishEnvironmentResponseDto)
@ExternalApiAccessible()
@ApiExcludeEndpoint()
@SdkMethodName('publish')
@RequirePermissions(PermissionsEnum.ENVIRONMENT_WRITE)
async publishEnvironment(
@UserSession() user: UserSessionData,
Expand All @@ -96,10 +107,21 @@ export class EnvironmentsController {

@Post('/:targetEnvironmentId/diff')
@HttpCode(200)
@ApiOperation({ summary: 'Compare workflows between source and target environments' })
@ApiOperation({
summary: 'Compare resources between environments',
description:
'Compares workflows and other resources between the source and target environments, returning detailed diff information including additions, modifications, and deletions.',
})
@ApiParam({
name: 'targetEnvironmentId',
description: 'Target environment ID (MongoDB ObjectId) to compare against',
type: String,
example: '6615943e7ace93b0540ae377',
})
@ApiBody({ type: DiffEnvironmentRequestDto, description: 'Diff request configuration' })
@ApiResponse(DiffEnvironmentResponseDto)
@ExternalApiAccessible()
@ApiExcludeEndpoint()
@SdkMethodName('diff')
@RequirePermissions(PermissionsEnum.ENVIRONMENT_WRITE)
async diffEnvironment(
@UserSession() user: UserSessionData,
Expand Down
9 changes: 8 additions & 1 deletion apps/api/src/app/environments-v2/types/sync.types.ts
Original file line number Diff line number Diff line change
Expand Up @@ -13,6 +13,13 @@ export enum DependencyReasonEnum {
LAYOUT_EXISTS_IN_TARGET = 'LAYOUT_EXISTS_IN_TARGET',
}

export enum SyncActionEnum {
CREATED = 'created',
UPDATED = 'updated',
SKIPPED = 'skipped',
DELETED = 'deleted',
}

export interface IResourceDependency {
resourceType: ResourceTypeEnum;
resourceId: string;
Expand Down Expand Up @@ -44,7 +51,7 @@ export interface ISyncedEntity {
resourceType: ResourceTypeEnum;
resourceId: string;
resourceName: string;
action: 'created' | 'updated' | 'skipped' | 'deleted';
action: SyncActionEnum;
}

export interface IFailedEntity {
Expand Down
Original file line number Diff line number Diff line change
@@ -1,7 +1,7 @@
import { Instrument, PinoLogger } from '@novu/application-generic';
import { LayoutEntity } from '@novu/dal';
import { capitalize } from '../../../../../shared/services/helper/helper.service';
import { IResourceToPublish, ISyncContext, ISyncResult, ResourceTypeEnum } from '../../../../types/sync.types';
import { IResourceToPublish, ISyncContext, ISyncResult, ResourceTypeEnum, SyncActionEnum } from '../../../../types/sync.types';
import { SyncResultBuilder } from '../../builders/sync-result.builder';
import { SKIP_REASONS, SYNC_ACTIONS } from '../../constants/sync.constants';
import { IBaseComparator, IBaseDeleteService, IBaseRepositoryService, IBaseSyncService } from '../interfaces';
Expand All @@ -10,7 +10,7 @@ interface IResourceSyncDecision<T> {
resource: T;
targetResource?: T;
sync: boolean;
action: 'created' | 'updated' | 'skipped';
action: SyncActionEnum.CREATED | SyncActionEnum.UPDATED | SyncActionEnum.SKIPPED;
reason?: string;
}

Expand Down Expand Up @@ -162,7 +162,7 @@ export abstract class BaseSyncOperation<T> {
resultBuilder.addSuccess(
this.repositoryService.getResourceIdentifier(decision.resource),
this.getResourceName(decision.resource),
decision.action as 'created' | 'updated'
decision.action
);
this.logger.info(this.getSyncSuccessMessage(this.getResourceName(decision.resource), decision.action));
} else {
Expand Down Expand Up @@ -289,7 +289,7 @@ export abstract class BaseSyncOperation<T> {
context: ISyncContext,
resource: T,
targetResource?: T
): Promise<{ sync: boolean; action: 'created' | 'updated' | 'skipped'; reason?: string }> {
): Promise<{ sync: boolean; action: SyncActionEnum.CREATED | SyncActionEnum.UPDATED | SyncActionEnum.SKIPPED; reason?: string }> {
if (!targetResource) {
return { sync: true, action: SYNC_ACTIONS.CREATED };
}
Expand Down
Original file line number Diff line number Diff line change
@@ -1,4 +1,11 @@
import { IFailedEntity, ISkippedEntity, ISyncedEntity, ISyncResult, ResourceTypeEnum } from '../../../types/sync.types';
import {
IFailedEntity,
ISkippedEntity,
ISyncedEntity,
ISyncResult,
ResourceTypeEnum,
SyncActionEnum,
} from '../../../types/sync.types';

export class SyncResultBuilder {
private successful: ISyncedEntity[] = [];
Expand All @@ -7,7 +14,7 @@ export class SyncResultBuilder {

constructor(private readonly resourceType: ResourceTypeEnum) {}

addSuccess(resourceId: string, resourceName: string, action: 'created' | 'updated' | 'deleted'): this {
addSuccess(resourceId: string, resourceName: string, action: SyncActionEnum): this {
this.successful.push({
resourceType: this.resourceType,
resourceId,
Expand Down
Original file line number Diff line number Diff line change
@@ -1,12 +1,14 @@
import { SyncActionEnum } from '../../../types/sync.types';

export const SYNC_CONSTANTS = {
BATCH_SIZE: 100,
} as const;

export const SYNC_ACTIONS = {
CREATED: 'created',
UPDATED: 'updated',
SKIPPED: 'skipped',
DELETED: 'deleted',
CREATED: SyncActionEnum.CREATED,
UPDATED: SyncActionEnum.UPDATED,
SKIPPED: SyncActionEnum.SKIPPED,
DELETED: SyncActionEnum.DELETED,
} as const;

export const SKIP_REASONS = {
Expand Down
Loading