mirror of
https://github.com/Unleash/unleash.git
synced 2025-08-23 13:46:45 +02: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