mirror of
				https://github.com/Unleash/unleash.git
				synced 2025-10-27 11:02:16 +01:00 
			
		
		
		
	Fix/feature events (#924)
This commit is contained in:
		
							parent
							
								
									8cb147a81f
								
							
						
					
					
						commit
						d28df3e3fa
					
				| @ -108,7 +108,7 @@ | |||||||
|     "response-time": "^2.3.2", |     "response-time": "^2.3.2", | ||||||
|     "serve-favicon": "^2.5.0", |     "serve-favicon": "^2.5.0", | ||||||
|     "stoppable": "^1.1.0", |     "stoppable": "^1.1.0", | ||||||
|     "unleash-frontend": "4.1.0-beta.5", |     "unleash-frontend": "4.1.0-beta.6", | ||||||
|     "uuid": "^8.3.2" |     "uuid": "^8.3.2" | ||||||
|   }, |   }, | ||||||
|   "devDependencies": { |   "devDependencies": { | ||||||
|  | |||||||
| @ -293,6 +293,7 @@ class FeatureStrategiesStore implements IFeatureStrategiesStore { | |||||||
|             featureToggle.environments = Object.values( |             featureToggle.environments = Object.values( | ||||||
|                 featureToggle.environments, |                 featureToggle.environments, | ||||||
|             ); |             ); | ||||||
|  |             featureToggle.archived = archived; | ||||||
|             return featureToggle; |             return featureToggle; | ||||||
|         } |         } | ||||||
|         throw new NotFoundError( |         throw new NotFoundError( | ||||||
|  | |||||||
| @ -148,11 +148,17 @@ function eachConsecutiveEvent(events, callback) { | |||||||
|     }); |     }); | ||||||
| } | } | ||||||
| 
 | 
 | ||||||
| function addDiffs(events) { | const ignoredProps = ['createdAt', 'lastSeenAt', 'environments', 'id']; | ||||||
|  | 
 | ||||||
|  | const filterProps = (path, key) => { | ||||||
|  |     return ignoredProps.includes(key); | ||||||
|  | }; | ||||||
|  | 
 | ||||||
|  | function addDiffs(events = []) { | ||||||
|     // TODO: no-param-reassign
 |     // TODO: no-param-reassign
 | ||||||
|     eachConsecutiveEvent(events, (left, right) => { |     eachConsecutiveEvent(events, (left, right) => { | ||||||
|         if (right) { |         if (right) { | ||||||
|             left.diffs = diff(right.data, left.data); |             left.diffs = diff(right.data, left.data, filterProps); | ||||||
|             left.diffs = left.diffs || []; |             left.diffs = left.diffs || []; | ||||||
|         } else { |         } else { | ||||||
|             left.diffs = null; |             left.diffs = null; | ||||||
|  | |||||||
| @ -197,53 +197,39 @@ class FeatureController extends Controller { | |||||||
| 
 | 
 | ||||||
|         updatedFeature.name = featureName; |         updatedFeature.name = featureName; | ||||||
| 
 | 
 | ||||||
|         const featureToggleExists = await this.featureService2.hasFeature( |         const projectId = await this.featureService2.getProjectId( | ||||||
|             featureName, |             updatedFeature.name, | ||||||
|         ); |         ); | ||||||
|         if (featureToggleExists) { |         const value = await featureSchema.validateAsync(updatedFeature); | ||||||
|             await this.featureService2.getFeature(featureName); |  | ||||||
|             const projectId = await this.featureService2.getProjectId( |  | ||||||
|                 updatedFeature.name, |  | ||||||
|             ); |  | ||||||
|             const value = await featureSchema.validateAsync(updatedFeature); |  | ||||||
|             const { enabled } = value; |  | ||||||
|             const updatedToggle = this.featureService2.updateFeatureToggle( |  | ||||||
|                 projectId, |  | ||||||
|                 value, |  | ||||||
|                 userName, |  | ||||||
|             ); |  | ||||||
| 
 | 
 | ||||||
|             await this.featureService2.removeAllStrategiesForEnv(featureName); |         await this.featureService2.updateFeatureToggle( | ||||||
|             let strategies; |             projectId, | ||||||
|             if (updatedFeature.strategies) { |             value, | ||||||
|                 strategies = await Promise.all( |             userName, | ||||||
|                     updatedFeature.strategies.map(async (s) => |         ); | ||||||
|                         this.featureService2.createStrategy( | 
 | ||||||
|                             s, |         await this.featureService2.removeAllStrategiesForEnv(featureName); | ||||||
|                             projectId, | 
 | ||||||
|                             featureName, |         if (updatedFeature.strategies) { | ||||||
|                         ), |             await Promise.all( | ||||||
|  |                 updatedFeature.strategies.map(async (s) => | ||||||
|  |                     this.featureService2.createStrategy( | ||||||
|  |                         s, | ||||||
|  |                         projectId, | ||||||
|  |                         featureName, | ||||||
|                     ), |                     ), | ||||||
|                 ); |                 ), | ||||||
|             } |  | ||||||
|             await this.featureService2.updateEnabled( |  | ||||||
|                 updatedFeature.name, |  | ||||||
|                 GLOBAL_ENV, |  | ||||||
|                 updatedFeature.enabled, |  | ||||||
|                 userName, |  | ||||||
|             ); |             ); | ||||||
|             res.status(200).json({ |  | ||||||
|                 ...updatedToggle, |  | ||||||
|                 enabled, |  | ||||||
|                 strategies: strategies || [], |  | ||||||
|             }); |  | ||||||
|         } else { |  | ||||||
|             res.status(404) |  | ||||||
|                 .json({ |  | ||||||
|                     error: `Feature with name ${featureName} does not exist`, |  | ||||||
|                 }) |  | ||||||
|                 .end(); |  | ||||||
|         } |         } | ||||||
|  |         await this.featureService2.updateEnabled( | ||||||
|  |             updatedFeature.name, | ||||||
|  |             GLOBAL_ENV, | ||||||
|  |             updatedFeature.enabled, | ||||||
|  |             userName, | ||||||
|  |         ); | ||||||
|  | 
 | ||||||
|  |         const feature = await this.getLegacyFeatureToggle(featureName); | ||||||
|  |         res.status(200).json(feature); | ||||||
|     } |     } | ||||||
| 
 | 
 | ||||||
|     // TODO: remove?
 |     // TODO: remove?
 | ||||||
|  | |||||||
| @ -3,6 +3,7 @@ import { IUnleashStores } from '../types/stores'; | |||||||
| import { Logger } from '../logger'; | import { Logger } from '../logger'; | ||||||
| import { IEventStore } from '../types/stores/event-store'; | import { IEventStore } from '../types/stores/event-store'; | ||||||
| import { IEvent } from '../types/model'; | import { IEvent } from '../types/model'; | ||||||
|  | import { FEATURE_METADATA_UPDATED } from '../types/events'; | ||||||
| 
 | 
 | ||||||
| export default class EventService { | export default class EventService { | ||||||
|     private logger: Logger; |     private logger: Logger; | ||||||
| @ -22,7 +23,10 @@ export default class EventService { | |||||||
|     } |     } | ||||||
| 
 | 
 | ||||||
|     async getEventsForToggle(name: string): Promise<IEvent[]> { |     async getEventsForToggle(name: string): Promise<IEvent[]> { | ||||||
|         return this.eventStore.getEventsFilterByType(name); |         const events = await this.eventStore.getEventsFilterByType(name); | ||||||
|  |         return events.filter( | ||||||
|  |             (e: IEvent) => e.type !== FEATURE_METADATA_UPDATED, | ||||||
|  |         ); | ||||||
|     } |     } | ||||||
| } | } | ||||||
| 
 | 
 | ||||||
|  | |||||||
| @ -14,6 +14,7 @@ import { | |||||||
|     FEATURE_STALE_OFF, |     FEATURE_STALE_OFF, | ||||||
|     FEATURE_STALE_ON, |     FEATURE_STALE_ON, | ||||||
|     FEATURE_UPDATED, |     FEATURE_UPDATED, | ||||||
|  |     FEATURE_METADATA_UPDATED, | ||||||
| } from '../types/events'; | } from '../types/events'; | ||||||
| import { GLOBAL_ENV } from '../types/environment'; | import { GLOBAL_ENV } from '../types/environment'; | ||||||
| import NotFoundError from '../error/notfound-error'; | import NotFoundError from '../error/notfound-error'; | ||||||
| @ -31,6 +32,7 @@ import { | |||||||
|     FeatureToggle, |     FeatureToggle, | ||||||
|     FeatureToggleDTO, |     FeatureToggleDTO, | ||||||
|     FeatureToggleWithEnvironment, |     FeatureToggleWithEnvironment, | ||||||
|  |     FeatureToggleWithEnvironmentLegacy, | ||||||
|     IFeatureEnvironmentInfo, |     IFeatureEnvironmentInfo, | ||||||
|     IFeatureStrategy, |     IFeatureStrategy, | ||||||
|     IFeatureToggleQuery, |     IFeatureToggleQuery, | ||||||
| @ -91,6 +93,12 @@ class FeatureToggleServiceV2 { | |||||||
|         this.featureEnvironmentStore = featureEnvironmentStore; |         this.featureEnvironmentStore = featureEnvironmentStore; | ||||||
|     } |     } | ||||||
| 
 | 
 | ||||||
|  |     /* | ||||||
|  |     TODO after 4.1.0 release: | ||||||
|  |     - add FEATURE_STRATEGY_ADD event | ||||||
|  |     - add FEATURE_STRATEGY_REMOVE event | ||||||
|  |     - add FEATURE_STRATEGY_UPDATE event | ||||||
|  |     */ | ||||||
|     async createStrategy( |     async createStrategy( | ||||||
|         strategyConfig: Omit<IStrategyConfig, 'id'>, |         strategyConfig: Omit<IStrategyConfig, 'id'>, | ||||||
|         projectName: string, |         projectName: string, | ||||||
| @ -251,22 +259,19 @@ class FeatureToggleServiceV2 { | |||||||
|         updatedFeature: FeatureToggleDTO, |         updatedFeature: FeatureToggleDTO, | ||||||
|         userName: string, |         userName: string, | ||||||
|     ): Promise<FeatureToggle> { |     ): Promise<FeatureToggle> { | ||||||
|  |         const featureName = updatedFeature.name; | ||||||
|         this.logger.info( |         this.logger.info( | ||||||
|             `${userName} updates feature toggle ${updatedFeature.name}`, |             `${userName} updates feature toggle ${featureName}`, | ||||||
|         ); |         ); | ||||||
| 
 | 
 | ||||||
|         await this.featureToggleStore.hasFeature(updatedFeature.name); |  | ||||||
| 
 |  | ||||||
|         const featureToggle = await this.featureToggleStore.updateFeature( |         const featureToggle = await this.featureToggleStore.updateFeature( | ||||||
|             projectId, |             projectId, | ||||||
|             updatedFeature, |             updatedFeature, | ||||||
|         ); |         ); | ||||||
|         const tags = |         const tags = await this.featureTagStore.getAllTagsForFeature(featureName); | ||||||
|             (await this.featureTagStore.getAllTagsForFeature( | 
 | ||||||
|                 updatedFeature.name, |  | ||||||
|             )) || []; |  | ||||||
|         await this.eventStore.store({ |         await this.eventStore.store({ | ||||||
|             type: FEATURE_UPDATED, |             type: FEATURE_METADATA_UPDATED, | ||||||
|             createdBy: userName, |             createdBy: userName, | ||||||
|             data: featureToggle, |             data: featureToggle, | ||||||
|             tags, |             tags, | ||||||
| @ -375,14 +380,13 @@ class FeatureToggleServiceV2 { | |||||||
|         ); |         ); | ||||||
|         feature.stale = isStale; |         feature.stale = isStale; | ||||||
|         await this.featureToggleStore.updateFeature(feature.project, feature); |         await this.featureToggleStore.updateFeature(feature.project, feature); | ||||||
|         const tags = |         const tags = await this.featureTagStore.getAllTagsForFeature(featureName); | ||||||
|             (await this.featureTagStore.getAllTagsForFeature(featureName)) || |         const data = await this.getFeatureToggleLegacy(featureName); | ||||||
|             []; |  | ||||||
| 
 | 
 | ||||||
|         await this.eventStore.store({ |         await this.eventStore.store({ | ||||||
|             type: isStale ? FEATURE_STALE_ON : FEATURE_STALE_OFF, |             type: isStale ? FEATURE_STALE_ON : FEATURE_STALE_OFF, | ||||||
|             createdBy: userName, |             createdBy: userName, | ||||||
|             data: feature, |             data, | ||||||
|             tags, |             tags, | ||||||
|         }); |         }); | ||||||
|         return feature; |         return feature; | ||||||
| @ -413,8 +417,7 @@ class FeatureToggleServiceV2 { | |||||||
|                 featureName, |                 featureName, | ||||||
|             ); |             ); | ||||||
|         if (hasEnvironment) { |         if (hasEnvironment) { | ||||||
|             const newEnabled = |             await this.featureEnvironmentStore.toggleEnvironmentEnabledStatus( | ||||||
|                 await this.featureEnvironmentStore.toggleEnvironmentEnabledStatus( |  | ||||||
|                     environment, |                     environment, | ||||||
|                     featureName, |                     featureName, | ||||||
|                     enabled, |                     enabled, | ||||||
| @ -422,14 +425,13 @@ class FeatureToggleServiceV2 { | |||||||
|             const feature = await this.featureToggleStore.getFeatureMetadata( |             const feature = await this.featureToggleStore.getFeatureMetadata( | ||||||
|                 featureName, |                 featureName, | ||||||
|             ); |             ); | ||||||
|             const tags = |             const tags = await this.featureTagStore.getAllTagsForFeature(featureName); | ||||||
|                 (await this.featureTagStore.getAllTagsForFeature( |             const data = await this.getFeatureToggleLegacy(featureName); | ||||||
|                     featureName, | 
 | ||||||
|                 )) || []; |  | ||||||
|             await this.eventStore.store({ |             await this.eventStore.store({ | ||||||
|                 type: FEATURE_UPDATED, |                 type: FEATURE_UPDATED, | ||||||
|                 createdBy: userName, |                 createdBy: userName, | ||||||
|                 data: { ...feature, enabled: newEnabled }, |                 data, | ||||||
|                 tags, |                 tags, | ||||||
|             }); |             }); | ||||||
|             return feature; |             return feature; | ||||||
| @ -459,6 +461,15 @@ class FeatureToggleServiceV2 { | |||||||
|         ); |         ); | ||||||
|     } |     } | ||||||
| 
 | 
 | ||||||
|  |     async getFeatureToggleLegacy(featureName: string): Promise<FeatureToggleWithEnvironmentLegacy> { | ||||||
|  |         const feature = await this.featureStrategiesStore.getFeatureToggleAdmin(featureName); | ||||||
|  |         const globalEnv = feature.environments.find(e => e.name === GLOBAL_ENV); | ||||||
|  |         const strategies = globalEnv?.strategies || []; | ||||||
|  |         const enabled = globalEnv?.enabled || false; | ||||||
|  | 
 | ||||||
|  |         return {...feature, enabled, strategies }; | ||||||
|  |     } | ||||||
|  | 
 | ||||||
|     // @deprecated
 |     // @deprecated
 | ||||||
|     async updateField( |     async updateField( | ||||||
|         featureName: string, |         featureName: string, | ||||||
| @ -473,14 +484,16 @@ class FeatureToggleServiceV2 { | |||||||
|         ); |         ); | ||||||
|         feature[field] = value; |         feature[field] = value; | ||||||
|         await this.featureToggleStore.updateFeature(feature.project, feature); |         await this.featureToggleStore.updateFeature(feature.project, feature); | ||||||
|         const tags = |         const tags = await this.featureTagStore.getAllTagsForFeature(featureName); | ||||||
|             (await this.featureTagStore.getAllTagsForFeature(featureName)) || | 
 | ||||||
|             []; |          | ||||||
|  |         // Workaround to support pre 4.1 format
 | ||||||
|  |         const data = await this.getFeatureToggleLegacy(featureName); | ||||||
| 
 | 
 | ||||||
|         await this.eventStore.store({ |         await this.eventStore.store({ | ||||||
|             type: event || FEATURE_UPDATED, |             type: event || FEATURE_UPDATED, | ||||||
|             createdBy: userName, |             createdBy: userName, | ||||||
|             data: feature, |             data, | ||||||
|             tags, |             tags, | ||||||
|         }); |         }); | ||||||
|         return feature; |         return feature; | ||||||
|  | |||||||
| @ -2,6 +2,7 @@ export const APPLICATION_CREATED = 'application-created'; | |||||||
| export const FEATURE_CREATED = 'feature-created'; | export const FEATURE_CREATED = 'feature-created'; | ||||||
| export const FEATURE_DELETED = 'feature-deleted'; | export const FEATURE_DELETED = 'feature-deleted'; | ||||||
| export const FEATURE_UPDATED = 'feature-updated'; | export const FEATURE_UPDATED = 'feature-updated'; | ||||||
|  | export const FEATURE_METADATA_UPDATED = 'feature-metadata-updated'; | ||||||
| export const FEATURE_PROJECT_CHANGE = 'feature-project-change'; | export const FEATURE_PROJECT_CHANGE = 'feature-project-change'; | ||||||
| export const FEATURE_ARCHIVED = 'feature-archived'; | export const FEATURE_ARCHIVED = 'feature-archived'; | ||||||
| export const FEATURE_REVIVED = 'feature-revived'; | export const FEATURE_REVIVED = 'feature-revived'; | ||||||
|  | |||||||
| @ -63,6 +63,13 @@ export interface FeatureToggleWithEnvironment extends FeatureToggle { | |||||||
|     environments: IEnvironmentDetail[]; |     environments: IEnvironmentDetail[]; | ||||||
| } | } | ||||||
| 
 | 
 | ||||||
|  | // @deprecated
 | ||||||
|  | export interface FeatureToggleWithEnvironmentLegacy | ||||||
|  |     extends FeatureToggleWithEnvironment { | ||||||
|  |     strategies: IStrategyConfig[]; | ||||||
|  |     enabled: boolean; | ||||||
|  | } | ||||||
|  | 
 | ||||||
| export interface IEnvironmentDetail extends IEnvironmentOverview { | export interface IEnvironmentDetail extends IEnvironmentOverview { | ||||||
|     strategies: IStrategyConfig[]; |     strategies: IStrategyConfig[]; | ||||||
| } | } | ||||||
|  | |||||||
| @ -39,7 +39,7 @@ export interface IFeatureStrategiesStore | |||||||
|     getStrategiesForEnv(environment: string): Promise<IFeatureStrategy[]>; |     getStrategiesForEnv(environment: string): Promise<IFeatureStrategy[]>; | ||||||
|     getFeatureToggleAdmin( |     getFeatureToggleAdmin( | ||||||
|         featureName: string, |         featureName: string, | ||||||
|         archived: boolean, |         archived?: boolean, | ||||||
|     ): Promise<FeatureToggleWithEnvironment>; |     ): Promise<FeatureToggleWithEnvironment>; | ||||||
|     getFeatures( |     getFeatures( | ||||||
|         featureQuery: Partial<IFeatureToggleQuery>, |         featureQuery: Partial<IFeatureToggleQuery>, | ||||||
|  | |||||||
| @ -1,11 +1,15 @@ | |||||||
|  | import EventService from '../../../lib/services/event-service'; | ||||||
|  | import { FEATURE_UPDATED } from '../../../lib/types/events'; | ||||||
| import FeatureToggleServiceV2 from '../../../lib/services/feature-toggle-service-v2'; | import FeatureToggleServiceV2 from '../../../lib/services/feature-toggle-service-v2'; | ||||||
| import { IStrategyConfig } from '../../../lib/types/model'; | import { IStrategyConfig } from '../../../lib/types/model'; | ||||||
| import { createTestConfig } from '../../config/test-config'; | import { createTestConfig } from '../../config/test-config'; | ||||||
| import dbInit from '../helpers/database-init'; | import dbInit from '../helpers/database-init'; | ||||||
|  | import { GLOBAL_ENV } from '../../../lib/types/environment'; | ||||||
| 
 | 
 | ||||||
| let stores; | let stores; | ||||||
| let db; | let db; | ||||||
| let service: FeatureToggleServiceV2; | let service: FeatureToggleServiceV2; | ||||||
|  | let eventService: EventService; | ||||||
| 
 | 
 | ||||||
| beforeAll(async () => { | beforeAll(async () => { | ||||||
|     const config = createTestConfig(); |     const config = createTestConfig(); | ||||||
| @ -15,6 +19,7 @@ beforeAll(async () => { | |||||||
|     ); |     ); | ||||||
|     stores = db.stores; |     stores = db.stores; | ||||||
|     service = new FeatureToggleServiceV2(stores, config); |     service = new FeatureToggleServiceV2(stores, config); | ||||||
|  |     eventService = new EventService(stores, config); | ||||||
| }); | }); | ||||||
| 
 | 
 | ||||||
| afterAll(async () => { | afterAll(async () => { | ||||||
| @ -74,6 +79,32 @@ test('Should be able to update existing strategy configuration', async () => { | |||||||
|     expect(updatedConfig.parameters).toEqual({ b2b: true }); |     expect(updatedConfig.parameters).toEqual({ b2b: true }); | ||||||
| }); | }); | ||||||
| 
 | 
 | ||||||
|  | test('Should include legacy props in event log when updating strategy configuration', async () => { | ||||||
|  |     const userName = 'event-tester'; | ||||||
|  |     const featureName = 'update-existing-strategy-events'; | ||||||
|  |     const config: Omit<IStrategyConfig, 'id'> = { | ||||||
|  |         name: 'default', | ||||||
|  |         constraints: [], | ||||||
|  |         parameters: {}, | ||||||
|  |     }; | ||||||
|  | 
 | ||||||
|  |     await service.createFeatureToggle( | ||||||
|  |         'default', | ||||||
|  |         { | ||||||
|  |             name: featureName, | ||||||
|  |         }, | ||||||
|  |         userName, | ||||||
|  |     ); | ||||||
|  | 
 | ||||||
|  |     await service.createStrategy(config, 'default', featureName); | ||||||
|  |     await service.updateEnabled(featureName, GLOBAL_ENV, true, userName); | ||||||
|  | 
 | ||||||
|  |     const events = await eventService.getEventsForToggle(featureName); | ||||||
|  |     expect(events[0].type).toBe(FEATURE_UPDATED); | ||||||
|  |     expect(events[0].data.enabled).toBe(true); | ||||||
|  |     expect(events[0].data.strategies).toBeDefined(); | ||||||
|  | }); | ||||||
|  | 
 | ||||||
| test('Should be able to get strategy by id', async () => { | test('Should be able to get strategy by id', async () => { | ||||||
|     const config: Omit<IStrategyConfig, 'id'> = { |     const config: Omit<IStrategyConfig, 'id'> = { | ||||||
|         name: 'default', |         name: 'default', | ||||||
|  | |||||||
		Loading…
	
		Reference in New Issue
	
	Block a user