mirror of
				https://github.com/Unleash/unleash.git
				synced 2025-10-27 11:02:16 +01:00 
			
		
		
		
	feat: import feature strategies (#2885)
This commit is contained in:
		
							parent
							
								
									b12962e7d2
								
							
						
					
					
						commit
						5569101f30
					
				| @ -1,5 +1,10 @@ | ||||
| import { IUnleashConfig } from '../types/option'; | ||||
| import { FeatureToggle, IFeatureStrategy, ITag } from '../types/model'; | ||||
| import { | ||||
|     FeatureToggle, | ||||
|     IFeatureEnvironment, | ||||
|     IFeatureStrategy, | ||||
|     ITag, | ||||
| } from '../types/model'; | ||||
| import { Logger } from '../logger'; | ||||
| import { IFeatureTagStore } from '../types/stores/feature-tag-store'; | ||||
| import { IProjectStore } from '../types/stores/project-store'; | ||||
| @ -26,8 +31,8 @@ export interface IExportQuery { | ||||
| 
 | ||||
| export interface IImportDTO { | ||||
|     data: IExportData; | ||||
|     project?: string; | ||||
|     environment?: string; | ||||
|     project: string; | ||||
|     environment: string; | ||||
| } | ||||
| 
 | ||||
| export interface IExportData { | ||||
| @ -35,6 +40,7 @@ export interface IExportData { | ||||
|     tags?: ITag[]; | ||||
|     contextFields?: IContextFieldDto[]; | ||||
|     featureStrategies: IFeatureStrategy[]; | ||||
|     featureEnvironments: IFeatureEnvironment[]; | ||||
| } | ||||
| 
 | ||||
| export default class ExportImportService { | ||||
| @ -93,25 +99,62 @@ export default class ExportImportService { | ||||
|     } | ||||
| 
 | ||||
|     async export(query: ExportQuerySchema): Promise<IExportData> { | ||||
|         const features = await this.toggleStore.getAllByNames(query.features); | ||||
|         const featureStrategies = | ||||
|             await this.featureStrategiesStore.getAllByFeatures( | ||||
|                 query.features, | ||||
|                 query.environment, | ||||
|             ); | ||||
|         return { features, featureStrategies }; | ||||
|         const [features, featureEnvironments, featureStrategies] = | ||||
|             await Promise.all([ | ||||
|                 this.toggleStore.getAllByNames(query.features), | ||||
|                 ( | ||||
|                     await this.featureEnvironmentStore.getAll({ | ||||
|                         environment: query.environment, | ||||
|                     }) | ||||
|                 ).filter((item) => query.features.includes(item.featureName)), | ||||
|                 this.featureStrategiesStore.getAllByFeatures( | ||||
|                     query.features, | ||||
|                     query.environment, | ||||
|                 ), | ||||
|             ]); | ||||
|         return { features, featureStrategies, featureEnvironments }; | ||||
|     } | ||||
| 
 | ||||
|     async import(dto: IImportDTO, user: User): Promise<void> { | ||||
|         await Promise.all( | ||||
|             dto.data.features.map((feature) => | ||||
|                 this.featureToggleService.createFeatureToggle( | ||||
|                     dto.project || feature.project, | ||||
|                     dto.project, | ||||
|                     feature, | ||||
|                     user.name, | ||||
|                 ), | ||||
|             ), | ||||
|         ); | ||||
|         await Promise.all( | ||||
|             dto.data.featureStrategies.map((featureStrategy) => | ||||
|                 this.featureToggleService.unprotectedCreateStrategy( | ||||
|                     { | ||||
|                         name: featureStrategy.strategyName, | ||||
|                         constraints: featureStrategy.constraints, | ||||
|                         parameters: featureStrategy.parameters, | ||||
|                         segments: featureStrategy.segments, | ||||
|                         sortOrder: featureStrategy.sortOrder, | ||||
|                     }, | ||||
|                     { | ||||
|                         featureName: featureStrategy.featureName, | ||||
|                         environment: dto.environment, | ||||
|                         projectId: dto.project, | ||||
|                     }, | ||||
|                     user.name, | ||||
|                 ), | ||||
|             ), | ||||
|         ); | ||||
|         await Promise.all( | ||||
|             dto.data.featureEnvironments.map((featureEnvironment) => | ||||
|                 this.featureToggleService.unprotectedUpdateEnabled( | ||||
|                     dto.project, | ||||
|                     featureEnvironment.featureName, | ||||
|                     dto.environment, | ||||
|                     featureEnvironment.enabled, | ||||
|                     user.name, | ||||
|                 ), | ||||
|             ), | ||||
|         ); | ||||
|     } | ||||
| } | ||||
| 
 | ||||
|  | ||||
| @ -5,14 +5,26 @@ import { | ||||
| import dbInit, { ITestDb } from '../../helpers/database-init'; | ||||
| import getLogger from '../../../fixtures/no-logger'; | ||||
| import { IEventStore } from 'lib/types/stores/event-store'; | ||||
| import { FeatureToggle, FeatureToggleDTO, IStrategyConfig } from 'lib/types'; | ||||
| import { | ||||
|     FeatureToggle, | ||||
|     FeatureToggleDTO, | ||||
|     IEnvironmentStore, | ||||
|     IFeatureStrategy, | ||||
|     IFeatureToggleStore, | ||||
|     IProjectStore, | ||||
|     IStrategyConfig, | ||||
| } from 'lib/types'; | ||||
| import { DEFAULT_ENV } from '../../../../lib/util'; | ||||
| import { IImportDTO } from '../../../../lib/services/export-import-service'; | ||||
| 
 | ||||
| let app: IUnleashTest; | ||||
| let db: ITestDb; | ||||
| let eventStore: IEventStore; | ||||
| let environmentStore: IEnvironmentStore; | ||||
| let projectStore: IProjectStore; | ||||
| let toggleStore: IFeatureToggleStore; | ||||
| 
 | ||||
| const defaultStrategy = { | ||||
| const defaultStrategy: IStrategyConfig = { | ||||
|     name: 'default', | ||||
|     parameters: {}, | ||||
|     constraints: [], | ||||
| @ -38,6 +50,24 @@ const createToggle = async ( | ||||
|     } | ||||
| }; | ||||
| 
 | ||||
| const createProject = async (project: string, environment: string) => { | ||||
|     await db.stores.environmentStore.create({ | ||||
|         name: environment, | ||||
|         type: 'production', | ||||
|     }); | ||||
|     await db.stores.projectStore.create({ | ||||
|         name: project, | ||||
|         description: '', | ||||
|         id: project, | ||||
|     }); | ||||
|     await app.request | ||||
|         .post(`/api/admin/projects/${project}/environments`) | ||||
|         .send({ | ||||
|             environment, | ||||
|         }) | ||||
|         .expect(200); | ||||
| }; | ||||
| 
 | ||||
| beforeAll(async () => { | ||||
|     db = await dbInit('export_import_api_serial', getLogger); | ||||
|     app = await setupAppWithCustomConfig(db.stores, { | ||||
| @ -48,10 +78,16 @@ beforeAll(async () => { | ||||
|         }, | ||||
|     }); | ||||
|     eventStore = db.stores.eventStore; | ||||
|     environmentStore = db.stores.environmentStore; | ||||
|     projectStore = db.stores.projectStore; | ||||
|     toggleStore = db.stores.featureToggleStore; | ||||
| }); | ||||
| 
 | ||||
| beforeEach(async () => { | ||||
|     await eventStore.deleteAll(); | ||||
|     await toggleStore.deleteAll(); | ||||
|     await projectStore.deleteAll(); | ||||
|     await environmentStore.deleteAll(); | ||||
| }); | ||||
| 
 | ||||
| afterAll(async () => { | ||||
| @ -59,11 +95,8 @@ afterAll(async () => { | ||||
|     await db.destroy(); | ||||
| }); | ||||
| 
 | ||||
| afterEach(async () => { | ||||
|     await db.stores.featureToggleStore.deleteAll(); | ||||
| }); | ||||
| 
 | ||||
| test('exports features', async () => { | ||||
|     await createProject('default', 'default'); | ||||
|     const strategy = { | ||||
|         name: 'default', | ||||
|         parameters: { rollout: '100', stickiness: 'default' }, | ||||
| @ -106,10 +139,19 @@ test('exports features', async () => { | ||||
|             }, | ||||
|         ], | ||||
|         featureStrategies: [resultStrategy], | ||||
|         featureEnvironments: [ | ||||
|             { | ||||
|                 enabled: false, | ||||
|                 environment: 'default', | ||||
|                 featureName: 'first_feature', | ||||
|                 variants: [], | ||||
|             }, | ||||
|         ], | ||||
|     }); | ||||
| }); | ||||
| 
 | ||||
| test('returns all features, when no feature was defined', async () => { | ||||
|     await createProject('default', 'default'); | ||||
|     await createToggle({ | ||||
|         name: 'first_feature', | ||||
|         description: 'the #1 feature', | ||||
| @ -130,24 +172,100 @@ test('returns all features, when no feature was defined', async () => { | ||||
|     expect(body.features).toHaveLength(2); | ||||
| }); | ||||
| 
 | ||||
| test('import features', async () => { | ||||
|     const feature: FeatureToggle = { project: 'ignore', name: 'first_feature' }; | ||||
| test('import features to existing project and environment', async () => { | ||||
|     const feature = 'first_feature'; | ||||
|     const project = 'new_project'; | ||||
|     const environment = 'staging'; | ||||
|     const variants = [ | ||||
|         { | ||||
|             name: 'variantA', | ||||
|             weight: 500, | ||||
|             payload: { | ||||
|                 type: 'string', | ||||
|                 value: 'payloadA', | ||||
|             }, | ||||
|             overrides: [], | ||||
|             stickiness: 'default', | ||||
|             weightType: 'variable', | ||||
|         }, | ||||
|         { | ||||
|             name: 'variantB', | ||||
|             weight: 500, | ||||
|             payload: { | ||||
|                 type: 'string', | ||||
|                 value: 'payloadB', | ||||
|             }, | ||||
|             overrides: [], | ||||
|             stickiness: 'default', | ||||
|             weightType: 'variable', | ||||
|         }, | ||||
|     ]; | ||||
|     const exportedFeature: FeatureToggle = { | ||||
|         project: 'old_project', | ||||
|         name: 'first_feature', | ||||
|         variants, | ||||
|     }; | ||||
|     const exportedStrategy: IFeatureStrategy = { | ||||
|         id: '798cb25a-2abd-47bd-8a95-40ec13472309', | ||||
|         featureName: feature, | ||||
|         projectId: 'old_project', | ||||
|         environment: 'old_environment', | ||||
|         strategyName: 'default', | ||||
|         parameters: {}, | ||||
|         constraints: [], | ||||
|     }; | ||||
|     const importPayload: IImportDTO = { | ||||
|         data: { | ||||
|             features: [exportedFeature], | ||||
|             featureStrategies: [exportedStrategy], | ||||
|             featureEnvironments: [ | ||||
|                 { | ||||
|                     enabled: true, | ||||
|                     featureName: 'first_feature', | ||||
|                     environment: 'irrelevant', | ||||
|                 }, | ||||
|             ], | ||||
|         }, | ||||
|         project: project, | ||||
|         environment: environment, | ||||
|     }; | ||||
|     await createProject(project, environment); | ||||
| 
 | ||||
|     await app.request | ||||
|         .post('/api/admin/features-batch/import') | ||||
|         .send({ | ||||
|             data: { features: [feature] }, | ||||
|             project: 'default', | ||||
|             environment: 'custom_environment', | ||||
|         }) | ||||
|         .send(importPayload) | ||||
|         .set('Content-Type', 'application/json') | ||||
|         .expect(201); | ||||
| 
 | ||||
|     const { body } = await app.request | ||||
|         .get('/api/admin/features/first_feature') | ||||
|     const { body: importedFeature } = await app.request | ||||
|         .get(`/api/admin/features/${feature}`) | ||||
|         .expect(200); | ||||
|     expect(importedFeature).toMatchObject({ | ||||
|         name: 'first_feature', | ||||
|         project: project, | ||||
|         variants, | ||||
|     }); | ||||
| 
 | ||||
|     const { body: importedFeatureEnvironment } = await app.request | ||||
|         .get( | ||||
|             `/api/admin/projects/${project}/features/${feature}/environments/${environment}`, | ||||
|         ) | ||||
|         .expect(200); | ||||
| 
 | ||||
|     expect(body).toMatchObject({ | ||||
|         name: 'first_feature', | ||||
|         project: 'default', | ||||
|     expect(importedFeatureEnvironment).toMatchObject({ | ||||
|         name: feature, | ||||
|         environment, | ||||
|         enabled: true, | ||||
|         strategies: [ | ||||
|             { | ||||
|                 featureName: feature, | ||||
|                 projectId: project, | ||||
|                 environment: environment, | ||||
|                 parameters: {}, | ||||
|                 constraints: [], | ||||
|                 sortOrder: 9999, | ||||
|                 name: 'default', | ||||
|             }, | ||||
|         ], | ||||
|     }); | ||||
| }); | ||||
|  | ||||
		Loading…
	
		Reference in New Issue
	
	Block a user