mirror of
https://github.com/Unleash/unleash.git
synced 2025-04-24 01:18:01 +02:00
feat: add project and environment columns to events (#942)
* feat: add project and environment columns to events * Added events for feature_strategy update * fix duplicate test key for dbInit * Fix argument list for toggleService calls in tests
This commit is contained in:
parent
37d6c4886a
commit
f85f66d4f5
@ -12,6 +12,8 @@ const EVENT_COLUMNS = [
|
|||||||
'created_at',
|
'created_at',
|
||||||
'data',
|
'data',
|
||||||
'tags',
|
'tags',
|
||||||
|
'project',
|
||||||
|
'environment',
|
||||||
];
|
];
|
||||||
|
|
||||||
export interface IEventTable {
|
export interface IEventTable {
|
||||||
@ -20,6 +22,8 @@ export interface IEventTable {
|
|||||||
created_by: string;
|
created_by: string;
|
||||||
created_at: Date;
|
created_at: Date;
|
||||||
data: any;
|
data: any;
|
||||||
|
project?: string;
|
||||||
|
environment?: string;
|
||||||
tags: [];
|
tags: [];
|
||||||
}
|
}
|
||||||
|
|
||||||
@ -126,6 +130,19 @@ class EventStore extends EventEmitter implements IEventStore {
|
|||||||
}
|
}
|
||||||
}
|
}
|
||||||
|
|
||||||
|
async getEventsFilterByProject(project: string): Promise<IEvent[]> {
|
||||||
|
try {
|
||||||
|
const rows = await this.db
|
||||||
|
.select(EVENT_COLUMNS)
|
||||||
|
.from(TABLE)
|
||||||
|
.where({ project })
|
||||||
|
.orderBy('created_at', 'desc');
|
||||||
|
return rows.map(this.rowToEvent);
|
||||||
|
} catch (err) {
|
||||||
|
return [];
|
||||||
|
}
|
||||||
|
}
|
||||||
|
|
||||||
rowToEvent(row: IEventTable): IEvent {
|
rowToEvent(row: IEventTable): IEvent {
|
||||||
return {
|
return {
|
||||||
id: row.id,
|
id: row.id,
|
||||||
@ -134,6 +151,8 @@ class EventStore extends EventEmitter implements IEventStore {
|
|||||||
createdAt: row.created_at,
|
createdAt: row.created_at,
|
||||||
data: row.data,
|
data: row.data,
|
||||||
tags: row.tags || [],
|
tags: row.tags || [],
|
||||||
|
project: row.project,
|
||||||
|
environment: row.environment,
|
||||||
};
|
};
|
||||||
}
|
}
|
||||||
|
|
||||||
@ -143,6 +162,8 @@ class EventStore extends EventEmitter implements IEventStore {
|
|||||||
created_by: e.createdBy,
|
created_by: e.createdBy,
|
||||||
data: e.data,
|
data: e.data,
|
||||||
tags: JSON.stringify(e.tags),
|
tags: JSON.stringify(e.tags),
|
||||||
|
project: e.project,
|
||||||
|
environment: e.environment,
|
||||||
};
|
};
|
||||||
}
|
}
|
||||||
}
|
}
|
||||||
|
@ -24,7 +24,14 @@ export default class EventController extends Controller {
|
|||||||
|
|
||||||
// eslint-disable-next-line @typescript-eslint/explicit-module-boundary-types
|
// eslint-disable-next-line @typescript-eslint/explicit-module-boundary-types
|
||||||
async getEvents(req, res): Promise<void> {
|
async getEvents(req, res): Promise<void> {
|
||||||
const events = await this.eventService.getEvents();
|
let events;
|
||||||
|
if (req.query?.project) {
|
||||||
|
events = await this.eventService.getEventsForProject(
|
||||||
|
req.query.project,
|
||||||
|
);
|
||||||
|
} else {
|
||||||
|
events = await this.eventService.getEvents();
|
||||||
|
}
|
||||||
eventDiffer.addDiffs(events);
|
eventDiffer.addDiffs(events);
|
||||||
res.json({ version, events });
|
res.json({ version, events });
|
||||||
}
|
}
|
||||||
|
@ -174,6 +174,7 @@ class FeatureController extends Controller {
|
|||||||
s,
|
s,
|
||||||
createdFeature.project,
|
createdFeature.project,
|
||||||
createdFeature.name,
|
createdFeature.name,
|
||||||
|
userName,
|
||||||
),
|
),
|
||||||
),
|
),
|
||||||
);
|
);
|
||||||
@ -219,6 +220,7 @@ class FeatureController extends Controller {
|
|||||||
s,
|
s,
|
||||||
projectId,
|
projectId,
|
||||||
featureName,
|
featureName,
|
||||||
|
userName,
|
||||||
),
|
),
|
||||||
),
|
),
|
||||||
);
|
);
|
||||||
|
@ -220,14 +220,16 @@ export default class ProjectFeaturesController extends Controller {
|
|||||||
}
|
}
|
||||||
|
|
||||||
async addStrategy(
|
async addStrategy(
|
||||||
req: Request<FeatureStrategyParams, any, IStrategyConfig, any>,
|
req: IAuthRequest<FeatureStrategyParams, any, IStrategyConfig, any>,
|
||||||
res: Response,
|
res: Response,
|
||||||
): Promise<void> {
|
): Promise<void> {
|
||||||
const { projectId, featureName, environment } = req.params;
|
const { projectId, featureName, environment } = req.params;
|
||||||
|
const userName = extractUsername(req);
|
||||||
const featureStrategy = await this.featureService.createStrategy(
|
const featureStrategy = await this.featureService.createStrategy(
|
||||||
req.body,
|
req.body,
|
||||||
projectId,
|
projectId,
|
||||||
featureName,
|
featureName,
|
||||||
|
userName,
|
||||||
environment,
|
environment,
|
||||||
);
|
);
|
||||||
res.status(200).json(featureStrategy);
|
res.status(200).json(featureStrategy);
|
||||||
@ -248,34 +250,42 @@ export default class ProjectFeaturesController extends Controller {
|
|||||||
}
|
}
|
||||||
|
|
||||||
async updateStrategy(
|
async updateStrategy(
|
||||||
req: Request<StrategyIdParams, any, StrategyUpdateBody, any>,
|
req: IAuthRequest<StrategyIdParams, any, StrategyUpdateBody, any>,
|
||||||
res: Response,
|
res: Response,
|
||||||
): Promise<void> {
|
): Promise<void> {
|
||||||
const { strategyId } = req.params;
|
const { strategyId, environment, projectId } = req.params;
|
||||||
|
const userName = extractUsername(req);
|
||||||
const updatedStrategy = await this.featureService.updateStrategy(
|
const updatedStrategy = await this.featureService.updateStrategy(
|
||||||
strategyId,
|
strategyId,
|
||||||
|
environment,
|
||||||
|
projectId,
|
||||||
|
userName,
|
||||||
req.body,
|
req.body,
|
||||||
);
|
);
|
||||||
res.status(200).json(updatedStrategy);
|
res.status(200).json(updatedStrategy);
|
||||||
}
|
}
|
||||||
|
|
||||||
async patchStrategy(
|
async patchStrategy(
|
||||||
req: Request<StrategyIdParams, any, Operation[], any>,
|
req: IAuthRequest<StrategyIdParams, any, Operation[], any>,
|
||||||
res: Response,
|
res: Response,
|
||||||
): Promise<void> {
|
): Promise<void> {
|
||||||
const { strategyId } = req.params;
|
const { strategyId, projectId, environment } = req.params;
|
||||||
|
const userName = extractUsername(req);
|
||||||
const patch = req.body;
|
const patch = req.body;
|
||||||
const strategy = await this.featureService.getStrategy(strategyId);
|
const strategy = await this.featureService.getStrategy(strategyId);
|
||||||
const { newDocument } = applyPatch(strategy, patch);
|
const { newDocument } = applyPatch(strategy, patch);
|
||||||
const updatedStrategy = await this.featureService.updateStrategy(
|
const updatedStrategy = await this.featureService.updateStrategy(
|
||||||
strategyId,
|
strategyId,
|
||||||
|
environment,
|
||||||
|
projectId,
|
||||||
|
userName,
|
||||||
newDocument,
|
newDocument,
|
||||||
);
|
);
|
||||||
res.status(200).json(updatedStrategy);
|
res.status(200).json(updatedStrategy);
|
||||||
}
|
}
|
||||||
|
|
||||||
async getStrategy(
|
async getStrategy(
|
||||||
req: Request<StrategyIdParams, any, any, any>,
|
req: IAuthRequest<StrategyIdParams, any, any, any>,
|
||||||
res: Response,
|
res: Response,
|
||||||
): Promise<void> {
|
): Promise<void> {
|
||||||
this.logger.info('Getting strategy');
|
this.logger.info('Getting strategy');
|
||||||
@ -286,18 +296,25 @@ export default class ProjectFeaturesController extends Controller {
|
|||||||
}
|
}
|
||||||
|
|
||||||
async deleteStrategy(
|
async deleteStrategy(
|
||||||
req: Request<StrategyIdParams, any, any, any>,
|
req: IAuthRequest<StrategyIdParams, any, any, any>,
|
||||||
res: Response,
|
res: Response,
|
||||||
): Promise<void> {
|
): Promise<void> {
|
||||||
this.logger.info('Deleting strategy');
|
this.logger.info('Deleting strategy');
|
||||||
|
const { environment, projectId } = req.params;
|
||||||
|
const userName = extractUsername(req);
|
||||||
const { strategyId } = req.params;
|
const { strategyId } = req.params;
|
||||||
this.logger.info(strategyId);
|
this.logger.info(strategyId);
|
||||||
const strategy = await this.featureService.deleteStrategy(strategyId);
|
const strategy = await this.featureService.deleteStrategy(
|
||||||
|
strategyId,
|
||||||
|
userName,
|
||||||
|
projectId,
|
||||||
|
environment,
|
||||||
|
);
|
||||||
res.status(200).json(strategy);
|
res.status(200).json(strategy);
|
||||||
}
|
}
|
||||||
|
|
||||||
async updateStrategyParameter(
|
async updateStrategyParameter(
|
||||||
req: Request<
|
req: IAuthRequest<
|
||||||
StrategyIdParams,
|
StrategyIdParams,
|
||||||
any,
|
any,
|
||||||
{ name: string; value: string | number },
|
{ name: string; value: string | number },
|
||||||
@ -305,7 +322,8 @@ export default class ProjectFeaturesController extends Controller {
|
|||||||
>,
|
>,
|
||||||
res: Response,
|
res: Response,
|
||||||
): Promise<void> {
|
): Promise<void> {
|
||||||
const { strategyId } = req.params;
|
const { strategyId, environment, projectId } = req.params;
|
||||||
|
const userName = extractUsername(req);
|
||||||
const { name, value } = req.body;
|
const { name, value } = req.body;
|
||||||
|
|
||||||
const updatedStrategy =
|
const updatedStrategy =
|
||||||
@ -313,6 +331,9 @@ export default class ProjectFeaturesController extends Controller {
|
|||||||
strategyId,
|
strategyId,
|
||||||
name,
|
name,
|
||||||
value,
|
value,
|
||||||
|
userName,
|
||||||
|
projectId,
|
||||||
|
environment,
|
||||||
);
|
);
|
||||||
res.status(200).json(updatedStrategy);
|
res.status(200).json(updatedStrategy);
|
||||||
}
|
}
|
||||||
|
@ -28,6 +28,10 @@ export default class EventService {
|
|||||||
(e: IEvent) => e.type !== FEATURE_METADATA_UPDATED,
|
(e: IEvent) => e.type !== FEATURE_METADATA_UPDATED,
|
||||||
);
|
);
|
||||||
}
|
}
|
||||||
|
|
||||||
|
async getEventsForProject(project: string): Promise<IEvent[]> {
|
||||||
|
return this.eventStore.getEventsFilterByProject(project);
|
||||||
|
}
|
||||||
}
|
}
|
||||||
|
|
||||||
module.exports = EventService;
|
module.exports = EventService;
|
||||||
|
@ -10,11 +10,14 @@ import {
|
|||||||
FEATURE_ARCHIVED,
|
FEATURE_ARCHIVED,
|
||||||
FEATURE_CREATED,
|
FEATURE_CREATED,
|
||||||
FEATURE_DELETED,
|
FEATURE_DELETED,
|
||||||
|
FEATURE_METADATA_UPDATED,
|
||||||
FEATURE_REVIVED,
|
FEATURE_REVIVED,
|
||||||
FEATURE_STALE_OFF,
|
FEATURE_STALE_OFF,
|
||||||
FEATURE_STALE_ON,
|
FEATURE_STALE_ON,
|
||||||
|
FEATURE_STRATEGY_ADD,
|
||||||
|
FEATURE_STRATEGY_REMOVE,
|
||||||
|
FEATURE_STRATEGY_UPDATE,
|
||||||
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';
|
||||||
@ -88,16 +91,11 @@ 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'>,
|
||||||
projectId: string,
|
projectId: string,
|
||||||
featureName: string,
|
featureName: string,
|
||||||
|
userName: string,
|
||||||
environment: string = GLOBAL_ENV,
|
environment: string = GLOBAL_ENV,
|
||||||
): Promise<IStrategyConfig> {
|
): Promise<IStrategyConfig> {
|
||||||
try {
|
try {
|
||||||
@ -111,12 +109,20 @@ class FeatureToggleServiceV2 {
|
|||||||
featureName,
|
featureName,
|
||||||
environment,
|
environment,
|
||||||
});
|
});
|
||||||
return {
|
const data = {
|
||||||
id: newFeatureStrategy.id,
|
id: newFeatureStrategy.id,
|
||||||
name: newFeatureStrategy.strategyName,
|
name: newFeatureStrategy.strategyName,
|
||||||
constraints: newFeatureStrategy.constraints,
|
constraints: newFeatureStrategy.constraints,
|
||||||
parameters: newFeatureStrategy.parameters,
|
parameters: newFeatureStrategy.parameters,
|
||||||
};
|
};
|
||||||
|
await this.eventStore.store({
|
||||||
|
type: FEATURE_STRATEGY_ADD,
|
||||||
|
project: projectId,
|
||||||
|
createdBy: userName,
|
||||||
|
environment,
|
||||||
|
data,
|
||||||
|
});
|
||||||
|
return data;
|
||||||
} catch (e) {
|
} catch (e) {
|
||||||
if (e.code === FOREIGN_KEY_VIOLATION) {
|
if (e.code === FOREIGN_KEY_VIOLATION) {
|
||||||
throw new BadDataError(
|
throw new BadDataError(
|
||||||
@ -126,6 +132,12 @@ class FeatureToggleServiceV2 {
|
|||||||
throw e;
|
throw e;
|
||||||
}
|
}
|
||||||
}
|
}
|
||||||
|
/*
|
||||||
|
TODO after 4.1.0 release:
|
||||||
|
- add FEATURE_STRATEGY_ADD event
|
||||||
|
- add FEATURE_STRATEGY_REMOVE event
|
||||||
|
- add FEATURE_STRATEGY_UPDATE event
|
||||||
|
*/
|
||||||
|
|
||||||
/**
|
/**
|
||||||
* PUT /api/admin/projects/:projectId/features/:featureName/strategies/:strategyId ?
|
* PUT /api/admin/projects/:projectId/features/:featureName/strategies/:strategyId ?
|
||||||
@ -139,6 +151,9 @@ class FeatureToggleServiceV2 {
|
|||||||
// TODO: verify projectId is not changed from URL!
|
// TODO: verify projectId is not changed from URL!
|
||||||
async updateStrategy(
|
async updateStrategy(
|
||||||
id: string,
|
id: string,
|
||||||
|
environment: string,
|
||||||
|
project: string,
|
||||||
|
userName: string,
|
||||||
updates: Partial<IFeatureStrategy>,
|
updates: Partial<IFeatureStrategy>,
|
||||||
): Promise<IStrategyConfig> {
|
): Promise<IStrategyConfig> {
|
||||||
const existingStrategy = await this.featureStrategiesStore.get(id);
|
const existingStrategy = await this.featureStrategiesStore.get(id);
|
||||||
@ -147,12 +162,20 @@ class FeatureToggleServiceV2 {
|
|||||||
id,
|
id,
|
||||||
updates,
|
updates,
|
||||||
);
|
);
|
||||||
return {
|
const data = {
|
||||||
id: strategy.id,
|
id: strategy.id,
|
||||||
name: strategy.strategyName,
|
name: strategy.strategyName,
|
||||||
constraints: strategy.constraints || [],
|
constraints: strategy.constraints || [],
|
||||||
parameters: strategy.parameters,
|
parameters: strategy.parameters,
|
||||||
};
|
};
|
||||||
|
await this.eventStore.store({
|
||||||
|
type: FEATURE_STRATEGY_UPDATE,
|
||||||
|
project,
|
||||||
|
environment,
|
||||||
|
createdBy: userName,
|
||||||
|
data,
|
||||||
|
});
|
||||||
|
return data;
|
||||||
}
|
}
|
||||||
throw new NotFoundError(`Could not find strategy with id ${id}`);
|
throw new NotFoundError(`Could not find strategy with id ${id}`);
|
||||||
}
|
}
|
||||||
@ -162,6 +185,9 @@ class FeatureToggleServiceV2 {
|
|||||||
id: string,
|
id: string,
|
||||||
name: string,
|
name: string,
|
||||||
value: string | number,
|
value: string | number,
|
||||||
|
userName: string,
|
||||||
|
project: string,
|
||||||
|
environment: string,
|
||||||
): Promise<IStrategyConfig> {
|
): Promise<IStrategyConfig> {
|
||||||
const existingStrategy = await this.featureStrategiesStore.get(id);
|
const existingStrategy = await this.featureStrategiesStore.get(id);
|
||||||
if (existingStrategy.id === id) {
|
if (existingStrategy.id === id) {
|
||||||
@ -170,12 +196,20 @@ class FeatureToggleServiceV2 {
|
|||||||
id,
|
id,
|
||||||
existingStrategy,
|
existingStrategy,
|
||||||
);
|
);
|
||||||
return {
|
const data = {
|
||||||
id: strategy.id,
|
id: strategy.id,
|
||||||
name: strategy.strategyName,
|
name: strategy.strategyName,
|
||||||
constraints: strategy.constraints || [],
|
constraints: strategy.constraints || [],
|
||||||
parameters: strategy.parameters,
|
parameters: strategy.parameters,
|
||||||
};
|
};
|
||||||
|
await this.eventStore.store({
|
||||||
|
type: FEATURE_STRATEGY_UPDATE,
|
||||||
|
project,
|
||||||
|
environment,
|
||||||
|
createdBy: userName,
|
||||||
|
data,
|
||||||
|
});
|
||||||
|
return data;
|
||||||
}
|
}
|
||||||
throw new NotFoundError(`Could not find strategy with id ${id}`);
|
throw new NotFoundError(`Could not find strategy with id ${id}`);
|
||||||
}
|
}
|
||||||
@ -188,8 +222,22 @@ class FeatureToggleServiceV2 {
|
|||||||
* @param id
|
* @param id
|
||||||
* @param updates
|
* @param updates
|
||||||
*/
|
*/
|
||||||
async deleteStrategy(id: string): Promise<void> {
|
async deleteStrategy(
|
||||||
return this.featureStrategiesStore.delete(id);
|
id: string,
|
||||||
|
userName: string,
|
||||||
|
project: string = 'default',
|
||||||
|
environment: string = GLOBAL_ENV,
|
||||||
|
): Promise<void> {
|
||||||
|
await this.featureStrategiesStore.delete(id);
|
||||||
|
await this.eventStore.store({
|
||||||
|
type: FEATURE_STRATEGY_REMOVE,
|
||||||
|
project,
|
||||||
|
environment,
|
||||||
|
createdBy: userName,
|
||||||
|
data: {
|
||||||
|
id,
|
||||||
|
},
|
||||||
|
});
|
||||||
}
|
}
|
||||||
|
|
||||||
async getStrategiesForEnvironment(
|
async getStrategiesForEnvironment(
|
||||||
@ -309,6 +357,7 @@ class FeatureToggleServiceV2 {
|
|||||||
await this.eventStore.store({
|
await this.eventStore.store({
|
||||||
type: FEATURE_CREATED,
|
type: FEATURE_CREATED,
|
||||||
createdBy: userName,
|
createdBy: userName,
|
||||||
|
project: projectId,
|
||||||
data,
|
data,
|
||||||
});
|
});
|
||||||
|
|
||||||
@ -341,6 +390,7 @@ class FeatureToggleServiceV2 {
|
|||||||
type: FEATURE_METADATA_UPDATED,
|
type: FEATURE_METADATA_UPDATED,
|
||||||
createdBy: userName,
|
createdBy: userName,
|
||||||
data: featureToggle,
|
data: featureToggle,
|
||||||
|
project: projectId,
|
||||||
tags,
|
tags,
|
||||||
});
|
});
|
||||||
return featureToggle;
|
return featureToggle;
|
||||||
@ -455,12 +505,13 @@ class FeatureToggleServiceV2 {
|
|||||||
createdBy: userName,
|
createdBy: userName,
|
||||||
data,
|
data,
|
||||||
tags,
|
tags,
|
||||||
|
project: feature.project,
|
||||||
});
|
});
|
||||||
return feature;
|
return feature;
|
||||||
}
|
}
|
||||||
|
|
||||||
async archiveToggle(name: string, userName: string): Promise<void> {
|
async archiveToggle(name: string, userName: string): Promise<void> {
|
||||||
await this.featureToggleStore.get(name);
|
const feature = await this.featureToggleStore.get(name);
|
||||||
await this.featureToggleStore.archive(name);
|
await this.featureToggleStore.archive(name);
|
||||||
const tags =
|
const tags =
|
||||||
(await this.featureTagStore.getAllTagsForFeature(name)) || [];
|
(await this.featureTagStore.getAllTagsForFeature(name)) || [];
|
||||||
@ -468,6 +519,7 @@ class FeatureToggleServiceV2 {
|
|||||||
type: FEATURE_ARCHIVED,
|
type: FEATURE_ARCHIVED,
|
||||||
createdBy: userName,
|
createdBy: userName,
|
||||||
data: { name },
|
data: { name },
|
||||||
|
project: feature.project,
|
||||||
tags,
|
tags,
|
||||||
});
|
});
|
||||||
}
|
}
|
||||||
@ -514,6 +566,8 @@ class FeatureToggleServiceV2 {
|
|||||||
createdBy: userName,
|
createdBy: userName,
|
||||||
data,
|
data,
|
||||||
tags,
|
tags,
|
||||||
|
project: projectId,
|
||||||
|
environment,
|
||||||
});
|
});
|
||||||
return feature;
|
return feature;
|
||||||
}
|
}
|
||||||
@ -583,6 +637,7 @@ class FeatureToggleServiceV2 {
|
|||||||
type: event || FEATURE_UPDATED,
|
type: event || FEATURE_UPDATED,
|
||||||
createdBy: userName,
|
createdBy: userName,
|
||||||
data,
|
data,
|
||||||
|
project: data.project,
|
||||||
tags,
|
tags,
|
||||||
});
|
});
|
||||||
return feature;
|
return feature;
|
||||||
@ -612,6 +667,7 @@ class FeatureToggleServiceV2 {
|
|||||||
type: FEATURE_REVIVED,
|
type: FEATURE_REVIVED,
|
||||||
createdBy: userName,
|
createdBy: userName,
|
||||||
data,
|
data,
|
||||||
|
project: data.project,
|
||||||
tags,
|
tags,
|
||||||
});
|
});
|
||||||
}
|
}
|
||||||
|
@ -131,6 +131,7 @@ export default class ProjectService {
|
|||||||
type: PROJECT_CREATED,
|
type: PROJECT_CREATED,
|
||||||
createdBy: getCreatedBy(user),
|
createdBy: getCreatedBy(user),
|
||||||
data,
|
data,
|
||||||
|
project: newProject.id,
|
||||||
});
|
});
|
||||||
|
|
||||||
return data;
|
return data;
|
||||||
@ -146,6 +147,7 @@ export default class ProjectService {
|
|||||||
type: PROJECT_UPDATED,
|
type: PROJECT_UPDATED,
|
||||||
createdBy: getCreatedBy(user),
|
createdBy: getCreatedBy(user),
|
||||||
data: project,
|
data: project,
|
||||||
|
project: project.id,
|
||||||
});
|
});
|
||||||
}
|
}
|
||||||
|
|
||||||
@ -211,10 +213,11 @@ export default class ProjectService {
|
|||||||
await this.eventStore.store({
|
await this.eventStore.store({
|
||||||
type: PROJECT_DELETED,
|
type: PROJECT_DELETED,
|
||||||
createdBy: getCreatedBy(user),
|
createdBy: getCreatedBy(user),
|
||||||
|
project: id,
|
||||||
data: { id },
|
data: { id },
|
||||||
});
|
});
|
||||||
|
|
||||||
this.accessService.removeDefaultProjectRoles(user, id);
|
await this.accessService.removeDefaultProjectRoles(user, id);
|
||||||
}
|
}
|
||||||
|
|
||||||
async validateId(id: string): Promise<boolean> {
|
async validateId(id: string): Promise<boolean> {
|
||||||
|
@ -9,6 +9,9 @@ export const FEATURE_REVIVED = 'feature-revived';
|
|||||||
export const FEATURE_IMPORT = 'feature-import';
|
export const FEATURE_IMPORT = 'feature-import';
|
||||||
export const FEATURE_TAGGED = 'feature-tagged';
|
export const FEATURE_TAGGED = 'feature-tagged';
|
||||||
export const FEATURE_TAG_IMPORT = 'feature-tag-import';
|
export const FEATURE_TAG_IMPORT = 'feature-tag-import';
|
||||||
|
export const FEATURE_STRATEGY_UPDATE = 'feature-strategy-update';
|
||||||
|
export const FEATURE_STRATEGY_ADD = 'feature-strategy-add';
|
||||||
|
export const FEATURE_STRATEGY_REMOVE = 'feature-strategy-remove';
|
||||||
export const DROP_FEATURE_TAGS = 'drop-feature-tags';
|
export const DROP_FEATURE_TAGS = 'drop-feature-tags';
|
||||||
export const FEATURE_UNTAGGED = 'feature-untagged';
|
export const FEATURE_UNTAGGED = 'feature-untagged';
|
||||||
export const FEATURE_STALE_ON = 'feature-stale-on';
|
export const FEATURE_STALE_ON = 'feature-stale-on';
|
||||||
|
@ -197,6 +197,8 @@ export interface IAddonConfig {
|
|||||||
export interface ICreateEvent {
|
export interface ICreateEvent {
|
||||||
type: string;
|
type: string;
|
||||||
createdBy: string;
|
createdBy: string;
|
||||||
|
project?: string;
|
||||||
|
environment?: string;
|
||||||
data?: any;
|
data?: any;
|
||||||
tags?: ITag[];
|
tags?: ITag[];
|
||||||
}
|
}
|
||||||
|
@ -7,4 +7,5 @@ export interface IEventStore extends Store<IEvent, number>, EventEmitter {
|
|||||||
batchStore(events: ICreateEvent[]): Promise<void>;
|
batchStore(events: ICreateEvent[]): Promise<void>;
|
||||||
getEvents(): Promise<IEvent[]>;
|
getEvents(): Promise<IEvent[]>;
|
||||||
getEventsFilterByType(name: string): Promise<IEvent[]>;
|
getEventsFilterByType(name: string): Promise<IEvent[]>;
|
||||||
|
getEventsFilterByProject(project: string): Promise<IEvent[]>;
|
||||||
}
|
}
|
||||||
|
@ -0,0 +1,29 @@
|
|||||||
|
'use strict';
|
||||||
|
|
||||||
|
exports.up = function (db, cb) {
|
||||||
|
db.runSql(
|
||||||
|
`
|
||||||
|
ALTER TABLE events
|
||||||
|
ADD COLUMN project TEXT;
|
||||||
|
ALTER TABLE events
|
||||||
|
ADD COLUMN environment TEXT;
|
||||||
|
CREATE INDEX events_project_idx ON events(project);
|
||||||
|
CREATE INDEX events_environment_idx ON events(environment);
|
||||||
|
`,
|
||||||
|
cb,
|
||||||
|
);
|
||||||
|
};
|
||||||
|
|
||||||
|
exports.down = function (db, cb) {
|
||||||
|
db.runSql(
|
||||||
|
`
|
||||||
|
DROP INDEX events_environment_idx;
|
||||||
|
DROP INDEX events_project_idx;
|
||||||
|
ALTER TABLE events
|
||||||
|
DROP COLUMN environment;
|
||||||
|
ALTER TABLE events
|
||||||
|
DROP COLUMN project;
|
||||||
|
`,
|
||||||
|
cb,
|
||||||
|
);
|
||||||
|
};
|
@ -1,13 +1,17 @@
|
|||||||
import { setupApp } from '../../helpers/test-helper';
|
import { IUnleashTest, setupApp } from '../../helpers/test-helper';
|
||||||
import dbInit from '../../helpers/database-init';
|
import dbInit, { ITestDb } from '../../helpers/database-init';
|
||||||
import getLogger from '../../../fixtures/no-logger';
|
import getLogger from '../../../fixtures/no-logger';
|
||||||
|
import { FEATURE_CREATED } from '../../../../lib/types/events';
|
||||||
|
import { IEventStore } from '../../../../lib/types/stores/event-store';
|
||||||
|
|
||||||
let app;
|
let app: IUnleashTest;
|
||||||
let db;
|
let db: ITestDb;
|
||||||
|
let eventStore: IEventStore;
|
||||||
|
|
||||||
beforeAll(async () => {
|
beforeAll(async () => {
|
||||||
db = await dbInit('event_api_serial', getLogger);
|
db = await dbInit('event_api_serial', getLogger);
|
||||||
app = await setupApp(db.stores);
|
app = await setupApp(db.stores);
|
||||||
|
eventStore = db.stores.eventStore;
|
||||||
});
|
});
|
||||||
|
|
||||||
afterAll(async () => {
|
afterAll(async () => {
|
||||||
@ -30,3 +34,29 @@ test('returns events given a name', async () => {
|
|||||||
.expect('Content-Type', /json/)
|
.expect('Content-Type', /json/)
|
||||||
.expect(200);
|
.expect(200);
|
||||||
});
|
});
|
||||||
|
|
||||||
|
test('Can filter by project', async () => {
|
||||||
|
await eventStore.store({
|
||||||
|
type: FEATURE_CREATED,
|
||||||
|
project: 'something-else',
|
||||||
|
data: { id: 'some-other-feature' },
|
||||||
|
tags: [],
|
||||||
|
createdBy: 'test-user',
|
||||||
|
environment: 'test',
|
||||||
|
});
|
||||||
|
await eventStore.store({
|
||||||
|
type: FEATURE_CREATED,
|
||||||
|
project: 'default',
|
||||||
|
data: { id: 'feature' },
|
||||||
|
tags: [],
|
||||||
|
createdBy: 'test-user',
|
||||||
|
environment: 'test',
|
||||||
|
});
|
||||||
|
await app.request
|
||||||
|
.get('/api/admin/events?project=default')
|
||||||
|
.expect(200)
|
||||||
|
.expect((res) => {
|
||||||
|
expect(res.body.events).toHaveLength(1);
|
||||||
|
expect(res.body.events[0].data.id).toEqual('feature');
|
||||||
|
});
|
||||||
|
});
|
||||||
|
@ -32,6 +32,7 @@ beforeAll(async () => {
|
|||||||
strategy,
|
strategy,
|
||||||
projectId,
|
projectId,
|
||||||
toggle.name,
|
toggle.name,
|
||||||
|
username,
|
||||||
);
|
);
|
||||||
};
|
};
|
||||||
|
|
||||||
@ -273,6 +274,7 @@ test('can not toggle of feature that does not exist', async () => {
|
|||||||
test('can toggle a feature that does exist', async () => {
|
test('can toggle a feature that does exist', async () => {
|
||||||
expect.assertions(0);
|
expect.assertions(0);
|
||||||
const featureName = 'existing.feature';
|
const featureName = 'existing.feature';
|
||||||
|
const username = 'toggle-feature';
|
||||||
const feature =
|
const feature =
|
||||||
await app.services.featureToggleServiceV2.createFeatureToggle(
|
await app.services.featureToggleServiceV2.createFeatureToggle(
|
||||||
'default',
|
'default',
|
||||||
@ -285,6 +287,7 @@ test('can toggle a feature that does exist', async () => {
|
|||||||
defaultStrategy,
|
defaultStrategy,
|
||||||
'default',
|
'default',
|
||||||
featureName,
|
featureName,
|
||||||
|
username,
|
||||||
);
|
);
|
||||||
return app.request
|
return app.request
|
||||||
.post(`/api/admin/features/${feature.name}/toggle`)
|
.post(`/api/admin/features/${feature.name}/toggle`)
|
||||||
|
@ -57,6 +57,7 @@ beforeAll(async () => {
|
|||||||
},
|
},
|
||||||
project,
|
project,
|
||||||
feature1,
|
feature1,
|
||||||
|
username,
|
||||||
);
|
);
|
||||||
await featureToggleServiceV2.createStrategy(
|
await featureToggleServiceV2.createStrategy(
|
||||||
{
|
{
|
||||||
@ -66,6 +67,7 @@ beforeAll(async () => {
|
|||||||
},
|
},
|
||||||
project,
|
project,
|
||||||
feature1,
|
feature1,
|
||||||
|
username,
|
||||||
environment,
|
environment,
|
||||||
);
|
);
|
||||||
|
|
||||||
@ -85,6 +87,7 @@ beforeAll(async () => {
|
|||||||
},
|
},
|
||||||
project,
|
project,
|
||||||
feature2,
|
feature2,
|
||||||
|
username,
|
||||||
environment,
|
environment,
|
||||||
);
|
);
|
||||||
|
|
||||||
@ -104,6 +107,7 @@ beforeAll(async () => {
|
|||||||
},
|
},
|
||||||
project2,
|
project2,
|
||||||
feature3,
|
feature3,
|
||||||
|
username,
|
||||||
environment,
|
environment,
|
||||||
);
|
);
|
||||||
});
|
});
|
||||||
@ -135,7 +139,7 @@ test('returns feature toggle with :global: config', async () => {
|
|||||||
});
|
});
|
||||||
});
|
});
|
||||||
|
|
||||||
test('returns feature toggle with :global: config', async () => {
|
test('returns feature toggle with testing environment config', async () => {
|
||||||
const token = await apiTokenService.createApiToken({
|
const token = await apiTokenService.createApiToken({
|
||||||
type: ApiTokenType.CLIENT,
|
type: ApiTokenType.CLIENT,
|
||||||
username,
|
username,
|
||||||
|
@ -27,6 +27,8 @@ afterAll(async () => {
|
|||||||
});
|
});
|
||||||
|
|
||||||
test('Should create feature toggle strategy configuration', async () => {
|
test('Should create feature toggle strategy configuration', async () => {
|
||||||
|
const projectId = 'default';
|
||||||
|
const username = 'feature-toggle';
|
||||||
const config: Omit<IStrategyConfig, 'id'> = {
|
const config: Omit<IStrategyConfig, 'id'> = {
|
||||||
name: 'default',
|
name: 'default',
|
||||||
constraints: [],
|
constraints: [],
|
||||||
@ -43,8 +45,9 @@ test('Should create feature toggle strategy configuration', async () => {
|
|||||||
|
|
||||||
const createdConfig = await service.createStrategy(
|
const createdConfig = await service.createStrategy(
|
||||||
config,
|
config,
|
||||||
'default',
|
projectId,
|
||||||
'Demo',
|
'Demo',
|
||||||
|
username,
|
||||||
);
|
);
|
||||||
|
|
||||||
expect(createdConfig.name).toEqual('default');
|
expect(createdConfig.name).toEqual('default');
|
||||||
@ -52,6 +55,8 @@ test('Should create feature toggle strategy configuration', async () => {
|
|||||||
});
|
});
|
||||||
|
|
||||||
test('Should be able to update existing strategy configuration', async () => {
|
test('Should be able to update existing strategy configuration', async () => {
|
||||||
|
const projectId = 'default';
|
||||||
|
const username = 'existing-strategy';
|
||||||
const config: Omit<IStrategyConfig, 'id'> = {
|
const config: Omit<IStrategyConfig, 'id'> = {
|
||||||
name: 'default',
|
name: 'default',
|
||||||
constraints: [],
|
constraints: [],
|
||||||
@ -59,7 +64,7 @@ test('Should be able to update existing strategy configuration', async () => {
|
|||||||
};
|
};
|
||||||
|
|
||||||
await service.createFeatureToggle(
|
await service.createFeatureToggle(
|
||||||
'default',
|
projectId,
|
||||||
{
|
{
|
||||||
name: 'update-existing-strategy',
|
name: 'update-existing-strategy',
|
||||||
},
|
},
|
||||||
@ -70,11 +75,18 @@ test('Should be able to update existing strategy configuration', async () => {
|
|||||||
config,
|
config,
|
||||||
'default',
|
'default',
|
||||||
'update-existing-strategy',
|
'update-existing-strategy',
|
||||||
|
username,
|
||||||
);
|
);
|
||||||
expect(createdConfig.name).toEqual('default');
|
expect(createdConfig.name).toEqual('default');
|
||||||
const updatedConfig = await service.updateStrategy(createdConfig.id, {
|
const updatedConfig = await service.updateStrategy(
|
||||||
|
createdConfig.id,
|
||||||
|
GLOBAL_ENV,
|
||||||
|
projectId,
|
||||||
|
username,
|
||||||
|
{
|
||||||
parameters: { b2b: true },
|
parameters: { b2b: true },
|
||||||
});
|
},
|
||||||
|
);
|
||||||
expect(createdConfig.id).toEqual(updatedConfig.id);
|
expect(createdConfig.id).toEqual(updatedConfig.id);
|
||||||
expect(updatedConfig.parameters).toEqual({ b2b: true });
|
expect(updatedConfig.parameters).toEqual({ b2b: true });
|
||||||
});
|
});
|
||||||
@ -96,7 +108,7 @@ test('Should include legacy props in event log when updating strategy configurat
|
|||||||
userName,
|
userName,
|
||||||
);
|
);
|
||||||
|
|
||||||
await service.createStrategy(config, 'default', featureName);
|
await service.createStrategy(config, 'default', featureName, userName);
|
||||||
await service.updateEnabled(
|
await service.updateEnabled(
|
||||||
'default',
|
'default',
|
||||||
featureName,
|
featureName,
|
||||||
@ -112,6 +124,7 @@ test('Should include legacy props in event log when updating strategy configurat
|
|||||||
});
|
});
|
||||||
|
|
||||||
test('Should be able to get strategy by id', async () => {
|
test('Should be able to get strategy by id', async () => {
|
||||||
|
const userName = 'strategy';
|
||||||
const config: Omit<IStrategyConfig, 'id'> = {
|
const config: Omit<IStrategyConfig, 'id'> = {
|
||||||
name: 'default',
|
name: 'default',
|
||||||
constraints: [],
|
constraints: [],
|
||||||
@ -130,6 +143,7 @@ test('Should be able to get strategy by id', async () => {
|
|||||||
config,
|
config,
|
||||||
'default',
|
'default',
|
||||||
'Demo',
|
'Demo',
|
||||||
|
userName,
|
||||||
);
|
);
|
||||||
const fetchedConfig = await service.getStrategy(createdConfig.id);
|
const fetchedConfig = await service.getStrategy(createdConfig.id);
|
||||||
expect(fetchedConfig).toEqual(createdConfig);
|
expect(fetchedConfig).toEqual(createdConfig);
|
||||||
|
@ -10,7 +10,7 @@ let userStore: IUserStore;
|
|||||||
let currentUser;
|
let currentUser;
|
||||||
|
|
||||||
beforeAll(async () => {
|
beforeAll(async () => {
|
||||||
db = await dbInit('project_store_serial', getLogger);
|
db = await dbInit('user_feedback_store', getLogger);
|
||||||
stores = db.stores;
|
stores = db.stores;
|
||||||
userFeedbackStore = stores.userFeedbackStore;
|
userFeedbackStore = stores.userFeedbackStore;
|
||||||
userStore = stores.userStore;
|
userStore = stores.userStore;
|
||||||
|
4
src/test/fixtures/fake-event-store.ts
vendored
4
src/test/fixtures/fake-event-store.ts
vendored
@ -57,6 +57,10 @@ class FakeEventStore extends EventEmitter implements IEventStore {
|
|||||||
async getEventsFilterByType(type: string): Promise<IEvent[]> {
|
async getEventsFilterByType(type: string): Promise<IEvent[]> {
|
||||||
return this.events.filter((e) => e.type === type);
|
return this.events.filter((e) => e.type === type);
|
||||||
}
|
}
|
||||||
|
|
||||||
|
async getEventsFilterByProject(project: string): Promise<IEvent[]> {
|
||||||
|
return this.events.filter((e) => e.project === project);
|
||||||
|
}
|
||||||
}
|
}
|
||||||
|
|
||||||
module.exports = FakeEventStore;
|
module.exports = FakeEventStore;
|
||||||
|
Loading…
Reference in New Issue
Block a user