mirror of
				https://github.com/Unleash/unleash.git
				synced 2025-10-27 11:02:16 +01:00 
			
		
		
		
	feat: Import limit validation (#4669)
This commit is contained in:
		
							parent
							
								
									ed6547b6f1
								
							
						
					
					
						commit
						2b2f5e20fa
					
				| @ -41,7 +41,10 @@ import { | ||||
|     TagTypeService, | ||||
| } from '../../services'; | ||||
| import { isValidField } from './import-context-validation'; | ||||
| import { IImportTogglesStore } from './import-toggles-store-type'; | ||||
| import { | ||||
|     IImportTogglesStore, | ||||
|     ProjectFeaturesLimit, | ||||
| } from './import-toggles-store-type'; | ||||
| import { ImportPermissionsService, Mode } from './import-permissions-service'; | ||||
| import { ImportValidationMessages } from './import-validation-messages'; | ||||
| import { findDuplicates } from '../../util/findDuplicates'; | ||||
| @ -158,6 +161,7 @@ export default class ExportImportService { | ||||
|             missingPermissions, | ||||
|             duplicateFeatures, | ||||
|             featureNameCheckResult, | ||||
|             featureLimitResult, | ||||
|         ] = await Promise.all([ | ||||
|             this.getUnsupportedStrategies(dto), | ||||
|             this.getUsedCustomStrategies(dto), | ||||
| @ -172,6 +176,7 @@ export default class ExportImportService { | ||||
|             ), | ||||
|             this.getDuplicateFeatures(dto), | ||||
|             this.getInvalidFeatureNames(dto), | ||||
|             this.getFeatureLimit(dto), | ||||
|         ]); | ||||
| 
 | ||||
|         const errors = ImportValidationMessages.compileErrors({ | ||||
| @ -181,6 +186,7 @@ export default class ExportImportService { | ||||
|             otherProjectFeatures, | ||||
|             duplicateFeatures, | ||||
|             featureNameCheckResult, | ||||
|             featureLimitResult, | ||||
|         }); | ||||
|         const warnings = ImportValidationMessages.compileWarnings({ | ||||
|             archivedFeatures, | ||||
| @ -511,6 +517,16 @@ export default class ExportImportService { | ||||
|         ); | ||||
|     } | ||||
| 
 | ||||
|     private async getFeatureLimit({ | ||||
|         project, | ||||
|         data, | ||||
|     }: ImportTogglesSchema): Promise<ProjectFeaturesLimit> { | ||||
|         return this.importTogglesStore.getProjectFeaturesLimit( | ||||
|             [...new Set(data.features.map((f) => f.name))], | ||||
|             project, | ||||
|         ); | ||||
|     } | ||||
| 
 | ||||
|     private async getUnsupportedStrategies( | ||||
|         dto: ImportTogglesSchema, | ||||
|     ): Promise<FeatureStrategySchema[]> { | ||||
|  | ||||
| @ -844,7 +844,8 @@ test('reject import with duplicate features', async () => { | ||||
| }); | ||||
| 
 | ||||
| test('validate import data', async () => { | ||||
|     await createProjects(); | ||||
|     const featureLimit = 1; | ||||
|     await createProjects([DEFAULT_PROJECT], featureLimit); | ||||
| 
 | ||||
|     const contextField: IContextFieldDto = { | ||||
|         name: 'validate_context_field', | ||||
| @ -864,7 +865,11 @@ test('validate import data', async () => { | ||||
|         ...defaultImportPayload, | ||||
|         data: { | ||||
|             ...defaultImportPayload.data, | ||||
|             features: [exportedFeature, exportedFeature], | ||||
|             features: [ | ||||
|                 exportedFeature, | ||||
|                 exportedFeature, | ||||
|                 anotherExportedFeature, | ||||
|             ], | ||||
|             featureStrategies: [{ name: 'customStrategy' }], | ||||
|             segments: [{ id: 1, name: 'customSegment' }], | ||||
|             contextFields: [ | ||||
| @ -909,7 +914,15 @@ test('validate import data', async () => { | ||||
| 
 | ||||
|             { | ||||
|                 message: expect.stringMatching(/\btestpattern.+\b/), | ||||
|                 affectedItems: [defaultFeatureName], | ||||
|                 affectedItems: [ | ||||
|                     defaultFeatureName, | ||||
|                     anotherExportedFeature.name, | ||||
|                 ], | ||||
|             }, | ||||
|             { | ||||
|                 message: | ||||
|                     'We detected you want to create 2 new features to a project that already has 0 existing features, exceeding the maximum limit of 1.', | ||||
|                 affectedItems: [], | ||||
|             }, | ||||
|         ], | ||||
|         warnings: [ | ||||
|  | ||||
| @ -1,3 +1,9 @@ | ||||
| export interface ProjectFeaturesLimit { | ||||
|     limit: number; | ||||
|     newFeaturesCount: number; | ||||
|     currentFeaturesCount: number; | ||||
| } | ||||
| 
 | ||||
| export interface IImportTogglesStore { | ||||
|     deleteStrategiesForFeatures( | ||||
|         featureNames: string[], | ||||
| @ -16,6 +22,11 @@ export interface IImportTogglesStore { | ||||
|         project: string, | ||||
|     ): Promise<string[]>; | ||||
| 
 | ||||
|     getProjectFeaturesLimit( | ||||
|         featureNames: string[], | ||||
|         project: string, | ||||
|     ): Promise<ProjectFeaturesLimit>; | ||||
| 
 | ||||
|     deleteTagsForFeatures(tags: string[]): Promise<void>; | ||||
| 
 | ||||
|     strategiesExistForFeatures( | ||||
|  | ||||
| @ -1,10 +1,14 @@ | ||||
| import { IImportTogglesStore } from './import-toggles-store-type'; | ||||
| import { | ||||
|     IImportTogglesStore, | ||||
|     ProjectFeaturesLimit, | ||||
| } from './import-toggles-store-type'; | ||||
| import { Db } from '../../db/db'; | ||||
| 
 | ||||
| const T = { | ||||
|     featureStrategies: 'feature_strategies', | ||||
|     features: 'features', | ||||
|     featureTag: 'feature_tag', | ||||
|     projectSettings: 'project_settings', | ||||
| }; | ||||
| export class ImportTogglesStore implements IImportTogglesStore { | ||||
|     private db: Db; | ||||
| @ -86,6 +90,38 @@ export class ImportTogglesStore implements IImportTogglesStore { | ||||
|         return rows.map((row) => row.name); | ||||
|     } | ||||
| 
 | ||||
|     async getProjectFeaturesLimit( | ||||
|         featureNames: string[], | ||||
|         project: string, | ||||
|     ): Promise<ProjectFeaturesLimit> { | ||||
|         const row = await this.db(T.projectSettings) | ||||
|             .select(['feature_limit']) | ||||
|             .where('project', project) | ||||
|             .first(); | ||||
|         const limit: number = row?.feature_limit ?? Number.MAX_SAFE_INTEGER; | ||||
| 
 | ||||
|         const existingFeaturesCount = await this.db(T.features) | ||||
|             .whereIn('name', featureNames) | ||||
|             .andWhere('project', project) | ||||
|             .where('archived_at', null) | ||||
|             .count() | ||||
|             .then((res) => Number(res[0].count)); | ||||
| 
 | ||||
|         const newFeaturesCount = featureNames.length - existingFeaturesCount; | ||||
| 
 | ||||
|         const currentFeaturesCount = await this.db(T.features) | ||||
|             .where('project', project) | ||||
|             .count() | ||||
|             .where('archived_at', null) | ||||
|             .then((res) => Number(res[0].count)); | ||||
| 
 | ||||
|         return { | ||||
|             limit, | ||||
|             newFeaturesCount, | ||||
|             currentFeaturesCount, | ||||
|         }; | ||||
|     } | ||||
| 
 | ||||
|     async deleteTagsForFeatures(features: string[]): Promise<void> { | ||||
|         return this.db(T.featureTag).whereIn('feature_name', features).del(); | ||||
|     } | ||||
|  | ||||
| @ -4,6 +4,7 @@ import { | ||||
|     ImportTogglesValidateItemSchema, | ||||
| } from '../../openapi'; | ||||
| import { IContextFieldDto } from '../../types/stores/context-field-store'; | ||||
| import { ProjectFeaturesLimit } from './import-toggles-store-type'; | ||||
| 
 | ||||
| export interface IErrorsParams { | ||||
|     projectName: string; | ||||
| @ -12,6 +13,7 @@ export interface IErrorsParams { | ||||
|     otherProjectFeatures: string[]; | ||||
|     duplicateFeatures: string[]; | ||||
|     featureNameCheckResult: FeatureNameCheckResult; | ||||
|     featureLimitResult: ProjectFeaturesLimit; | ||||
| } | ||||
| 
 | ||||
| export interface IWarningParams { | ||||
| @ -43,6 +45,7 @@ export class ImportValidationMessages { | ||||
|         otherProjectFeatures, | ||||
|         duplicateFeatures, | ||||
|         featureNameCheckResult, | ||||
|         featureLimitResult, | ||||
|     }: IErrorsParams): ImportTogglesValidateItemSchema[] { | ||||
|         const errors: ImportTogglesValidateItemSchema[] = []; | ||||
| 
 | ||||
| @ -92,6 +95,16 @@ export class ImportValidationMessages { | ||||
|                 affectedItems: [...featureNameCheckResult.invalidNames].sort(), | ||||
|             }); | ||||
|         } | ||||
|         if ( | ||||
|             featureLimitResult.currentFeaturesCount + | ||||
|                 featureLimitResult.newFeaturesCount > | ||||
|             featureLimitResult.limit | ||||
|         ) { | ||||
|             errors.push({ | ||||
|                 message: `We detected you want to create ${featureLimitResult.newFeaturesCount} new features to a project that already has ${featureLimitResult.currentFeaturesCount} existing features, exceeding the maximum limit of ${featureLimitResult.limit}.`, | ||||
|                 affectedItems: [], | ||||
|             }); | ||||
|         } | ||||
| 
 | ||||
|         return errors; | ||||
|     } | ||||
|  | ||||
		Loading…
	
		Reference in New Issue
	
	Block a user