mirror of
				https://github.com/Unleash/unleash.git
				synced 2025-10-27 11:02:16 +01:00 
			
		
		
		
	feat: dependencies import validation (#5023)
This commit is contained in:
		
							parent
							
								
									b6d945befc
								
							
						
					
					
						commit
						2263a1f062
					
				@ -340,7 +340,7 @@ test('validate import data', async () => {
 | 
				
			|||||||
        errors: [
 | 
					        errors: [
 | 
				
			||||||
            {
 | 
					            {
 | 
				
			||||||
                message:
 | 
					                message:
 | 
				
			||||||
                    'We detected the following custom strategy in the import file that needs to be created first:',
 | 
					                    'We detected the following custom strategy that needs to be created first:',
 | 
				
			||||||
                affectedItems: ['customStrategy'],
 | 
					                affectedItems: ['customStrategy'],
 | 
				
			||||||
            },
 | 
					            },
 | 
				
			||||||
            {
 | 
					            {
 | 
				
			||||||
@ -351,7 +351,7 @@ test('validate import data', async () => {
 | 
				
			|||||||
            {
 | 
					            {
 | 
				
			||||||
                affectedItems: ['customSegment'],
 | 
					                affectedItems: ['customSegment'],
 | 
				
			||||||
                message:
 | 
					                message:
 | 
				
			||||||
                    'We detected the following segments in the import file that need to be created first:',
 | 
					                    'We detected the following segments that need to be created first:',
 | 
				
			||||||
            },
 | 
					            },
 | 
				
			||||||
        ],
 | 
					        ],
 | 
				
			||||||
        warnings: [
 | 
					        warnings: [
 | 
				
			||||||
 | 
				
			|||||||
@ -52,6 +52,7 @@ import { FeatureNameCheckResultWithFeaturePattern } from '../feature-toggle/feat
 | 
				
			|||||||
import { IDependentFeaturesReadModel } from '../dependent-features/dependent-features-read-model-type';
 | 
					import { IDependentFeaturesReadModel } from '../dependent-features/dependent-features-read-model-type';
 | 
				
			||||||
import groupBy from 'lodash.groupby';
 | 
					import groupBy from 'lodash.groupby';
 | 
				
			||||||
import { ISegmentService } from '../../segments/segment-service-interface';
 | 
					import { ISegmentService } from '../../segments/segment-service-interface';
 | 
				
			||||||
 | 
					import { FeatureDependenciesSchema } from '../../openapi/spec/feature-dependencies-schema';
 | 
				
			||||||
 | 
					
 | 
				
			||||||
export type IImportService = {
 | 
					export type IImportService = {
 | 
				
			||||||
    validate(
 | 
					    validate(
 | 
				
			||||||
@ -194,6 +195,7 @@ export default class ExportImportService
 | 
				
			|||||||
            featureNameCheckResult,
 | 
					            featureNameCheckResult,
 | 
				
			||||||
            featureLimitResult,
 | 
					            featureLimitResult,
 | 
				
			||||||
            unsupportedSegments,
 | 
					            unsupportedSegments,
 | 
				
			||||||
 | 
					            unsupportedDependencies,
 | 
				
			||||||
        ] = await Promise.all([
 | 
					        ] = await Promise.all([
 | 
				
			||||||
            this.getUnsupportedStrategies(dto),
 | 
					            this.getUnsupportedStrategies(dto),
 | 
				
			||||||
            this.getUsedCustomStrategies(dto),
 | 
					            this.getUsedCustomStrategies(dto),
 | 
				
			||||||
@ -210,6 +212,7 @@ export default class ExportImportService
 | 
				
			|||||||
            this.getInvalidFeatureNames(dto),
 | 
					            this.getInvalidFeatureNames(dto),
 | 
				
			||||||
            this.getFeatureLimit(dto),
 | 
					            this.getFeatureLimit(dto),
 | 
				
			||||||
            this.getUnsupportedSegments(dto),
 | 
					            this.getUnsupportedSegments(dto),
 | 
				
			||||||
 | 
					            this.getMissingDependencies(dto),
 | 
				
			||||||
        ]);
 | 
					        ]);
 | 
				
			||||||
 | 
					
 | 
				
			||||||
        const errors = ImportValidationMessages.compileErrors({
 | 
					        const errors = ImportValidationMessages.compileErrors({
 | 
				
			||||||
@ -221,6 +224,7 @@ export default class ExportImportService
 | 
				
			|||||||
            featureNameCheckResult,
 | 
					            featureNameCheckResult,
 | 
				
			||||||
            featureLimitResult,
 | 
					            featureLimitResult,
 | 
				
			||||||
            segments: unsupportedSegments,
 | 
					            segments: unsupportedSegments,
 | 
				
			||||||
 | 
					            dependencies: unsupportedDependencies,
 | 
				
			||||||
        });
 | 
					        });
 | 
				
			||||||
        const warnings = ImportValidationMessages.compileWarnings({
 | 
					        const warnings = ImportValidationMessages.compileWarnings({
 | 
				
			||||||
            archivedFeatures,
 | 
					            archivedFeatures,
 | 
				
			||||||
@ -250,6 +254,7 @@ export default class ExportImportService
 | 
				
			|||||||
            this.importPermissionsService.verifyPermissions(dto, user, mode),
 | 
					            this.importPermissionsService.verifyPermissions(dto, user, mode),
 | 
				
			||||||
            this.verifyFeatures(dto),
 | 
					            this.verifyFeatures(dto),
 | 
				
			||||||
            this.verifySegments(dto),
 | 
					            this.verifySegments(dto),
 | 
				
			||||||
 | 
					            this.verifyDependencies(dto),
 | 
				
			||||||
        ]);
 | 
					        ]);
 | 
				
			||||||
    }
 | 
					    }
 | 
				
			||||||
 | 
					
 | 
				
			||||||
@ -459,6 +464,32 @@ export default class ExportImportService
 | 
				
			|||||||
            : [];
 | 
					            : [];
 | 
				
			||||||
    }
 | 
					    }
 | 
				
			||||||
 | 
					
 | 
				
			||||||
 | 
					    private async getMissingDependencies(
 | 
				
			||||||
 | 
					        dto: ImportTogglesSchema,
 | 
				
			||||||
 | 
					    ): Promise<string[]> {
 | 
				
			||||||
 | 
					        const dependentFeatures =
 | 
				
			||||||
 | 
					            dto.data.dependencies?.flatMap((dependency) =>
 | 
				
			||||||
 | 
					                dependency.dependencies.map((d) => d.feature),
 | 
				
			||||||
 | 
					            ) || [];
 | 
				
			||||||
 | 
					        const importedFeatures = dto.data.features.map((f) => f.name);
 | 
				
			||||||
 | 
					
 | 
				
			||||||
 | 
					        const missingFromImported = dependentFeatures.filter(
 | 
				
			||||||
 | 
					            (feature) => !importedFeatures.includes(feature),
 | 
				
			||||||
 | 
					        );
 | 
				
			||||||
 | 
					
 | 
				
			||||||
 | 
					        let missingFeatures: string[] = [];
 | 
				
			||||||
 | 
					
 | 
				
			||||||
 | 
					        if (missingFromImported.length) {
 | 
				
			||||||
 | 
					            const featuresFromStore = (
 | 
				
			||||||
 | 
					                await this.toggleStore.getAllByNames(missingFromImported)
 | 
				
			||||||
 | 
					            ).map((f) => f.name);
 | 
				
			||||||
 | 
					            missingFeatures = missingFromImported.filter(
 | 
				
			||||||
 | 
					                (feature) => !featuresFromStore.includes(feature),
 | 
				
			||||||
 | 
					            );
 | 
				
			||||||
 | 
					        }
 | 
				
			||||||
 | 
					        return missingFeatures;
 | 
				
			||||||
 | 
					    }
 | 
				
			||||||
 | 
					
 | 
				
			||||||
    private async verifySegments(dto: ImportTogglesSchema) {
 | 
					    private async verifySegments(dto: ImportTogglesSchema) {
 | 
				
			||||||
        const unsupportedSegments = await this.getUnsupportedSegments(dto);
 | 
					        const unsupportedSegments = await this.getUnsupportedSegments(dto);
 | 
				
			||||||
        if (unsupportedSegments.length > 0) {
 | 
					        if (unsupportedSegments.length > 0) {
 | 
				
			||||||
@ -468,6 +499,17 @@ export default class ExportImportService
 | 
				
			|||||||
        }
 | 
					        }
 | 
				
			||||||
    }
 | 
					    }
 | 
				
			||||||
 | 
					
 | 
				
			||||||
 | 
					    private async verifyDependencies(dto: ImportTogglesSchema) {
 | 
				
			||||||
 | 
					        const unsupportedDependencies = await this.getMissingDependencies(dto);
 | 
				
			||||||
 | 
					        if (unsupportedDependencies.length > 0) {
 | 
				
			||||||
 | 
					            throw new BadDataError(
 | 
				
			||||||
 | 
					                `The following dependent features are missing: ${unsupportedDependencies.join(
 | 
				
			||||||
 | 
					                    ', ',
 | 
				
			||||||
 | 
					                )}`,
 | 
				
			||||||
 | 
					            );
 | 
				
			||||||
 | 
					        }
 | 
				
			||||||
 | 
					    }
 | 
				
			||||||
 | 
					
 | 
				
			||||||
    private async verifyContextFields(dto: ImportTogglesSchema) {
 | 
					    private async verifyContextFields(dto: ImportTogglesSchema) {
 | 
				
			||||||
        const unsupportedContextFields = await this.getUnsupportedContextFields(
 | 
					        const unsupportedContextFields = await this.getUnsupportedContextFields(
 | 
				
			||||||
            dto,
 | 
					            dto,
 | 
				
			||||||
 | 
				
			|||||||
@ -1006,6 +1006,16 @@ test('validate import data', async () => {
 | 
				
			|||||||
                },
 | 
					                },
 | 
				
			||||||
                createdContextField,
 | 
					                createdContextField,
 | 
				
			||||||
            ],
 | 
					            ],
 | 
				
			||||||
 | 
					            dependencies: [
 | 
				
			||||||
 | 
					                {
 | 
				
			||||||
 | 
					                    feature: 'childFeature',
 | 
				
			||||||
 | 
					                    dependencies: [
 | 
				
			||||||
 | 
					                        {
 | 
				
			||||||
 | 
					                            feature: 'parentFeature',
 | 
				
			||||||
 | 
					                        },
 | 
				
			||||||
 | 
					                    ],
 | 
				
			||||||
 | 
					                },
 | 
				
			||||||
 | 
					            ],
 | 
				
			||||||
        },
 | 
					        },
 | 
				
			||||||
    };
 | 
					    };
 | 
				
			||||||
 | 
					
 | 
				
			||||||
@ -1023,7 +1033,7 @@ test('validate import data', async () => {
 | 
				
			|||||||
        errors: [
 | 
					        errors: [
 | 
				
			||||||
            {
 | 
					            {
 | 
				
			||||||
                message:
 | 
					                message:
 | 
				
			||||||
                    'We detected the following custom strategy in the import file that needs to be created first:',
 | 
					                    'We detected the following custom strategy that needs to be created first:',
 | 
				
			||||||
                affectedItems: ['customStrategy'],
 | 
					                affectedItems: ['customStrategy'],
 | 
				
			||||||
            },
 | 
					            },
 | 
				
			||||||
            {
 | 
					            {
 | 
				
			||||||
@ -1051,9 +1061,14 @@ test('validate import data', async () => {
 | 
				
			|||||||
            },
 | 
					            },
 | 
				
			||||||
            {
 | 
					            {
 | 
				
			||||||
                message:
 | 
					                message:
 | 
				
			||||||
                    'We detected the following segments in the import file that need to be created first:',
 | 
					                    'We detected the following segments that need to be created first:',
 | 
				
			||||||
                affectedItems: ['customSegment'],
 | 
					                affectedItems: ['customSegment'],
 | 
				
			||||||
            },
 | 
					            },
 | 
				
			||||||
 | 
					            {
 | 
				
			||||||
 | 
					                affectedItems: ['parentFeature'],
 | 
				
			||||||
 | 
					                message:
 | 
				
			||||||
 | 
					                    'We detected the following dependencies that need to be created first:',
 | 
				
			||||||
 | 
					            },
 | 
				
			||||||
        ],
 | 
					        ],
 | 
				
			||||||
        warnings: [
 | 
					        warnings: [
 | 
				
			||||||
            {
 | 
					            {
 | 
				
			||||||
 | 
				
			|||||||
@ -15,6 +15,7 @@ export interface IErrorsParams {
 | 
				
			|||||||
    featureNameCheckResult: FeatureNameCheckResultWithFeaturePattern;
 | 
					    featureNameCheckResult: FeatureNameCheckResultWithFeaturePattern;
 | 
				
			||||||
    featureLimitResult: ProjectFeaturesLimit;
 | 
					    featureLimitResult: ProjectFeaturesLimit;
 | 
				
			||||||
    segments: string[];
 | 
					    segments: string[];
 | 
				
			||||||
 | 
					    dependencies: string[];
 | 
				
			||||||
}
 | 
					}
 | 
				
			||||||
 | 
					
 | 
				
			||||||
export interface IWarningParams {
 | 
					export interface IWarningParams {
 | 
				
			||||||
@ -48,13 +49,14 @@ export class ImportValidationMessages {
 | 
				
			|||||||
        featureNameCheckResult,
 | 
					        featureNameCheckResult,
 | 
				
			||||||
        featureLimitResult,
 | 
					        featureLimitResult,
 | 
				
			||||||
        segments,
 | 
					        segments,
 | 
				
			||||||
 | 
					        dependencies,
 | 
				
			||||||
    }: IErrorsParams): ImportTogglesValidateItemSchema[] {
 | 
					    }: IErrorsParams): ImportTogglesValidateItemSchema[] {
 | 
				
			||||||
        const errors: ImportTogglesValidateItemSchema[] = [];
 | 
					        const errors: ImportTogglesValidateItemSchema[] = [];
 | 
				
			||||||
 | 
					
 | 
				
			||||||
        if (strategies.length > 0) {
 | 
					        if (strategies.length > 0) {
 | 
				
			||||||
            errors.push({
 | 
					            errors.push({
 | 
				
			||||||
                message:
 | 
					                message:
 | 
				
			||||||
                    'We detected the following custom strategy in the import file that needs to be created first:',
 | 
					                    'We detected the following custom strategy that needs to be created first:',
 | 
				
			||||||
                affectedItems: strategies.map((strategy) => strategy.name),
 | 
					                affectedItems: strategies.map((strategy) => strategy.name),
 | 
				
			||||||
            });
 | 
					            });
 | 
				
			||||||
        }
 | 
					        }
 | 
				
			||||||
@ -111,11 +113,19 @@ export class ImportValidationMessages {
 | 
				
			|||||||
        if (segments.length > 0) {
 | 
					        if (segments.length > 0) {
 | 
				
			||||||
            errors.push({
 | 
					            errors.push({
 | 
				
			||||||
                message:
 | 
					                message:
 | 
				
			||||||
                    'We detected the following segments in the import file that need to be created first:',
 | 
					                    'We detected the following segments that need to be created first:',
 | 
				
			||||||
                affectedItems: segments,
 | 
					                affectedItems: segments,
 | 
				
			||||||
            });
 | 
					            });
 | 
				
			||||||
        }
 | 
					        }
 | 
				
			||||||
 | 
					
 | 
				
			||||||
 | 
					        if (dependencies.length > 0) {
 | 
				
			||||||
 | 
					            errors.push({
 | 
				
			||||||
 | 
					                message:
 | 
				
			||||||
 | 
					                    'We detected the following dependencies that need to be created first:',
 | 
				
			||||||
 | 
					                affectedItems: dependencies,
 | 
				
			||||||
 | 
					            });
 | 
				
			||||||
 | 
					        }
 | 
				
			||||||
 | 
					
 | 
				
			||||||
        return errors;
 | 
					        return errors;
 | 
				
			||||||
    }
 | 
					    }
 | 
				
			||||||
 | 
					
 | 
				
			||||||
 | 
				
			|||||||
@ -14,6 +14,8 @@ import { legalValueSchema } from './legal-value-schema';
 | 
				
			|||||||
import { tagTypeSchema } from './tag-type-schema';
 | 
					import { tagTypeSchema } from './tag-type-schema';
 | 
				
			||||||
import { featureEnvironmentSchema } from './feature-environment-schema';
 | 
					import { featureEnvironmentSchema } from './feature-environment-schema';
 | 
				
			||||||
import { strategyVariantSchema } from './strategy-variant-schema';
 | 
					import { strategyVariantSchema } from './strategy-variant-schema';
 | 
				
			||||||
 | 
					import { featureDependenciesSchema } from './feature-dependencies-schema';
 | 
				
			||||||
 | 
					import { dependentFeatureSchema } from './dependent-feature-schema';
 | 
				
			||||||
 | 
					
 | 
				
			||||||
export const importTogglesSchema = {
 | 
					export const importTogglesSchema = {
 | 
				
			||||||
    $id: '#/components/schemas/importTogglesSchema',
 | 
					    $id: '#/components/schemas/importTogglesSchema',
 | 
				
			||||||
@ -56,6 +58,8 @@ export const importTogglesSchema = {
 | 
				
			|||||||
            parametersSchema,
 | 
					            parametersSchema,
 | 
				
			||||||
            legalValueSchema,
 | 
					            legalValueSchema,
 | 
				
			||||||
            tagTypeSchema,
 | 
					            tagTypeSchema,
 | 
				
			||||||
 | 
					            featureDependenciesSchema,
 | 
				
			||||||
 | 
					            dependentFeatureSchema,
 | 
				
			||||||
        },
 | 
					        },
 | 
				
			||||||
    },
 | 
					    },
 | 
				
			||||||
} as const;
 | 
					} as const;
 | 
				
			||||||
 | 
				
			|||||||
@ -163,3 +163,4 @@ export * from './update-feature-strategy-segments-schema';
 | 
				
			|||||||
export * from './dependent-feature-schema';
 | 
					export * from './dependent-feature-schema';
 | 
				
			||||||
export * from './create-dependent-feature-schema';
 | 
					export * from './create-dependent-feature-schema';
 | 
				
			||||||
export * from './parent-feature-options-schema';
 | 
					export * from './parent-feature-options-schema';
 | 
				
			||||||
 | 
					export * from './feature-dependencies-schema';
 | 
				
			||||||
 | 
				
			|||||||
		Loading…
	
		Reference in New Issue
	
	Block a user