1
0
mirror of https://github.com/Unleash/unleash.git synced 2024-12-22 19:07:54 +01:00

feat: clean up events (#1089)

Co-authored-by: Christopher Kolstad <chriswk@getunleash.ai>
This commit is contained in:
Ivar Conradi Østhus 2021-11-12 13:15:51 +01:00 committed by GitHub
parent 5b748a3cfc
commit d8478dd928
No known key found for this signature in database
GPG Key ID: 4AEE18F83AFDEB23
49 changed files with 1032 additions and 800 deletions

View File

@ -660,13 +660,19 @@ paths:
operationId: get-admin-events
summary: Fetch all changes in the Unleash system
description: |-
Returns one of the six event types:
Returns one of the twelve event types:
- feature-created
- feature-updated
- feature-metadata-updated
- feature-project-change
- feature-archived
- feature-revived
- strategy-created
- strategy-deleted
- feature-strategy-update
- feature-strategy-add
- feature-strategy-remove
- feature-stale-on
- feature-stale-off
- feature-environment-enabled
- feature-environment-disabled
tags:
- Events
responses:
@ -1548,15 +1554,21 @@ components:
type: number
example: 55
type:
description: One of the six event types
description: Identifies the event
type: string
enum:
- feature-created
- feature-updated
- feature-metadata-updated
- feature-project-change
- feature-archived
- feature-revived
- strategy-created
- strategy-deleted
- feature-strategy-update
- feature-strategy-add
- feature-strategy-remove
- feature-stale-on
- feature-stale-off
- feature-environment-enabled
- feature-environment-disabled
minLength: 1
example: feature-updated
createdBy:
@ -1568,53 +1580,21 @@ components:
type: string
example: '2016-12-09T14:56:36.730Z'
data:
$ref: '#/components/schemas/featureToggleSchema'
diffs:
description: |-
The JSON differences between the current and last version of the Feature Toggle.
(Uses the [deep-diff Node.js module](https://www.npmjs.com/package/deep-diff))
externalDocs:
description: Activation strategies
url: 'https://www.npmjs.com/package/deep-diff#differences'
type: array
items:
required:
- kind
- lhs
- rhs
properties:
kind:
description: |-
The kind of change:
- **N** - a newly-added property or element
- **D** - a property or element was deleted
- **E** - a property or element was edited
- **A** - a change occurred within an array
type: string
enum:
- 'N'
- D
- E
- A
example: E
path:
type: array
items:
required:
- pathItem
properties:
pathItem:
description: The property path (from the left-hand-side root)
type: string
example: enabled
lhs:
description: The value on the left-hand-side of the comparison (*undefined* if **kind** is *N*)
type: boolean
example: true
rhs:
description: The value on the right-hand-side of the comparison (*undefined* if **kind** is *D*)
type: boolean
example: false
description: The current state of the updated resource
type: object
preData:
description: The previous state of the updated resource
type: object
featureName:
description: Name of the feature toggle (if event related to a feature toggle)
type: string
project:
description: Name of the project (if event related to a project resource)
type: string
environment:
description: Name of the environment (if event related to a environment resource)
type: string
x-tags:
- Responses
200export:

View File

@ -85,7 +85,6 @@
"db-migrate": "0.11.12",
"db-migrate-pg": "1.2.2",
"db-migrate-shared": "1.2.0",
"deep-diff": "^1.0.2",
"deepmerge": "^4.2.2",
"errorhandler": "^1.5.1",
"express": "^4.17.1",

View File

@ -2,7 +2,8 @@ import fetch, { Response } from 'node-fetch';
import { addonDefinitionSchema } from './addon-schema';
import { IUnleashConfig } from '../types/option';
import { Logger } from '../logger';
import { IAddonDefinition, IEvent } from '../types/model';
import { IAddonDefinition } from '../types/model';
import { IEvent } from '../types/events';
export default abstract class Addon {
logger: Logger;

View File

@ -2,13 +2,13 @@ import {
FEATURE_ARCHIVED,
FEATURE_CREATED,
FEATURE_ENVIRONMENT_DISABLED,
IEvent,
} from '../types/events';
import { Logger } from '../logger';
import DatadogAddon from './datadog';
import noLogger from '../../test/fixtures/no-logger';
import { IEvent } from '../types/model';
let fetchRetryCalls: any[] = [];
@ -45,6 +45,7 @@ test('Should call datadog webhook', async () => {
createdAt: new Date(),
type: FEATURE_CREATED,
createdBy: 'some@user.com',
featureName: 'some-toggle',
data: {
name: 'some-toggle',
enabled: false,
@ -72,6 +73,7 @@ test('Should call datadog webhook for archived toggle', async () => {
createdAt: new Date(),
type: FEATURE_ARCHIVED,
createdBy: 'some@user.com',
featureName: 'some-toggle',
data: {
name: 'some-toggle',
},
@ -99,6 +101,7 @@ test(`Should call datadog webhook for toggled environment`, async () => {
createdBy: 'some@user.com',
environment: 'development',
project: 'default',
featureName: 'some-toggle',
data: {
name: 'some-toggle',
},

View File

@ -1,12 +1,13 @@
import Addon from './addon';
import definition from './datadog-definition';
import { IAddonConfig, IEvent } from '../types/model';
import { IAddonConfig } from '../types/model';
import {
FeatureEventFormatter,
FeatureEventFormatterMd,
LinkStyle,
} from './feature-event-formatter-md';
import { IEvent } from '../types/events';
export default class DatadogAddon extends Addon {
private msgFormatter: FeatureEventFormatter;

View File

@ -1,4 +1,3 @@
import { IEvent } from '../types/model';
import {
FEATURE_CREATED,
FEATURE_UPDATED,
@ -13,6 +12,7 @@ import {
FEATURE_STRATEGY_REMOVE,
FEATURE_METADATA_UPDATED,
FEATURE_PROJECT_CHANGE,
IEvent,
} from '../types/events';
export interface FeatureEventFormatter {
@ -44,9 +44,9 @@ export class FeatureEventFormatterMd implements FeatureEventFormatter {
generateFeatureLink(event: IEvent): string {
if (this.linkStyle === LinkStyle.SLACK) {
return `<${this.featureLink(event)}|${event.data.name}>`;
return `<${this.featureLink(event)}|${event.featureName}>`;
} else {
return `[${event.data.name}](${this.featureLink(event)})`;
return `[${event.featureName}](${this.featureLink(event)})`;
}
}
@ -70,20 +70,17 @@ export class FeatureEventFormatterMd implements FeatureEventFormatter {
}
generateStrategyChangeText(event: IEvent): string {
const { createdBy, environment, project, data, type } = event;
const { createdBy, environment, project, data, preData, type } = event;
const feature = this.generateFeatureLink(event);
let action;
let strategyText: string = '';
if (FEATURE_STRATEGY_UPDATE === type) {
action = 'updated in';
strategyText = `by updating strategy ${data?.name} in *${environment}*`;
} else if (FEATURE_STRATEGY_ADD === type) {
action = 'added to';
} else {
action = 'removed from';
strategyText = `by adding strategy ${data?.name} in *${environment}*`;
} else if (FEATURE_STRATEGY_REMOVE === type) {
strategyText = `by removing strategy ${preData?.name} in *${environment}*`;
}
const strategyText = `a ${
data.strategyName ?? ''
} strategy ${action} the *${environment}* environment`;
return `${createdBy} updated *${feature}* with ${strategyText} in project *${project}*`;
return `${createdBy} updated *${feature}* in project *${project}* ${strategyText}`;
}
generateMetadataText(event: IEvent): string {
@ -93,16 +90,16 @@ export class FeatureEventFormatterMd implements FeatureEventFormatter {
}
generateProjectChangeText(event: IEvent): string {
const { createdBy, project, data } = event;
return `${createdBy} moved ${data.name} to ${project}`;
const { createdBy, project, featureName } = event;
return `${createdBy} moved ${featureName} to ${project}`;
}
featureLink(event: IEvent): string {
const { type, project = '', data } = event;
const { type, project = '', featureName } = event;
if (type === FEATURE_ARCHIVED) {
return `${this.unleashUrl}/archive`;
}
return `${this.unleashUrl}/projects/${project}/${data.name}`;
return `${this.unleashUrl}/projects/${project}/${featureName}`;
}
getAction(type: string): string {

View File

@ -2,13 +2,13 @@ import {
FEATURE_CREATED,
FEATURE_ARCHIVED,
FEATURE_ENVIRONMENT_DISABLED,
IEvent,
} from '../types/events';
import { Logger } from '../logger';
import SlackAddon from './slack';
import noLogger from '../../test/fixtures/no-logger';
import { IEvent } from '../types/model';
let fetchRetryCalls: any[] = [];
@ -46,6 +46,7 @@ test('Should call slack webhook', async () => {
type: FEATURE_CREATED,
createdBy: 'some@user.com',
project: 'default',
featureName: 'some-toggle',
data: {
name: 'some-toggle',
enabled: false,
@ -73,6 +74,7 @@ test('Should call slack webhook for archived toggle', async () => {
id: 2,
createdAt: new Date(),
type: FEATURE_ARCHIVED,
featureName: 'some-toggle',
createdBy: 'some@user.com',
data: {
name: 'some-toggle',
@ -101,6 +103,7 @@ test(`Should call webhook for toggled environment`, async () => {
createdBy: 'some@user.com',
environment: 'development',
project: 'default',
featureName: 'some-toggle',
data: {
name: 'some-toggle',
},
@ -127,6 +130,7 @@ test('Should use default channel', async () => {
createdAt: new Date(),
type: FEATURE_CREATED,
createdBy: 'some@user.com',
featureName: 'some-toggle',
data: {
name: 'some-toggle',
enabled: false,
@ -156,6 +160,7 @@ test('Should override default channel with data from tag', async () => {
createdAt: new Date(),
type: FEATURE_CREATED,
createdBy: 'some@user.com',
featureName: 'some-toggle',
data: {
name: 'some-toggle',
enabled: false,
@ -191,6 +196,7 @@ test('Should post to all channels in tags', async () => {
createdAt: new Date(),
type: FEATURE_CREATED,
createdBy: 'some@user.com',
featureName: 'some-toggle',
data: {
name: 'some-toggle',
enabled: false,

View File

@ -1,13 +1,14 @@
import Addon from './addon';
import slackDefinition from './slack-definition';
import { IAddonConfig, IEvent } from '../types/model';
import { IAddonConfig } from '../types/model';
import {
FeatureEventFormatter,
FeatureEventFormatterMd,
LinkStyle,
} from './feature-event-formatter-md';
import { IEvent } from '../types/events';
export default class SlackAddon extends Addon {
private msgFormatter: FeatureEventFormatter;

View File

@ -4,12 +4,12 @@ import {
FEATURE_ARCHIVED,
FEATURE_CREATED,
FEATURE_ENVIRONMENT_DISABLED,
IEvent,
} from '../types/events';
import TeamsAddon from './teams';
import noLogger from '../../test/fixtures/no-logger';
import { IEvent } from '../types/model';
let fetchRetryCalls: any[];
@ -46,6 +46,7 @@ test('Should call teams webhook', async () => {
createdAt: new Date(),
type: FEATURE_CREATED,
createdBy: 'some@user.com',
featureName: 'some-toggle',
data: {
name: 'some-toggle',
enabled: false,
@ -73,6 +74,7 @@ test('Should call teams webhook for archived toggle', async () => {
createdAt: new Date(),
type: FEATURE_ARCHIVED,
createdBy: 'some@user.com',
featureName: 'some-toggle',
data: {
name: 'some-toggle',
},
@ -100,6 +102,7 @@ test(`Should call teams webhook for toggled environment`, async () => {
createdBy: 'some@user.com',
environment: 'development',
project: 'default',
featureName: 'some-toggle',
data: {
name: 'some-toggle',
},

View File

@ -1,11 +1,12 @@
import Addon from './addon';
import teamsDefinition from './teams-definition';
import { IAddonConfig, IEvent } from '../types/model';
import { IAddonConfig } from '../types/model';
import {
FeatureEventFormatter,
FeatureEventFormatterMd,
} from './feature-event-formatter-md';
import { IEvent } from '../types/events';
export default class TeamsAddon extends Addon {
private msgFormatter: FeatureEventFormatter;

View File

@ -1,11 +1,10 @@
import { Logger } from '../logger';
import { FEATURE_CREATED } from '../types/events';
import { FEATURE_CREATED, IEvent } from '../types/events';
import WebhookAddon from './webhook';
import noLogger from '../../test/fixtures/no-logger';
import { IEvent } from '../types/model';
let fetchRetryCalls: any[] = [];
@ -39,6 +38,7 @@ test('Should handle event without "bodyTemplate"', () => {
createdAt: new Date(),
type: FEATURE_CREATED,
createdBy: 'some@user.com',
featureName: 'some-toggle',
data: {
name: 'some-toggle',
enabled: false,
@ -63,6 +63,7 @@ test('Should format event with "bodyTemplate"', () => {
createdAt: new Date(),
type: FEATURE_CREATED,
createdBy: 'some@user.com',
featureName: 'some-toggle',
data: {
name: 'some-toggle',
enabled: false,

View File

@ -2,15 +2,20 @@ import Mustache from 'mustache';
import Addon from './addon';
import definition from './webhook-definition';
import { LogProvider } from '../logger';
import { IEvent } from '../types/model';
import { IEvent } from '../types/events';
interface IParameters {
url: string;
bodyTemplate?: string;
contentType?: string;
}
export default class Webhook extends Addon {
constructor(args: { getLogger: LogProvider }) {
super(definition, args);
}
// eslint-disable-next-line @typescript-eslint/explicit-module-boundary-types
async handleEvent(event: IEvent, parameters: any): Promise<void> {
async handleEvent(event: IEvent, parameters: IParameters): Promise<void> {
const { url, bodyTemplate, contentType } = parameters;
const context = {
event,

View File

@ -1,9 +1,9 @@
import { EventEmitter } from 'events';
import { Knex } from 'knex';
import { DROP_FEATURES } from '../types/events';
import { DROP_FEATURES, IEvent, IBaseEvent } from '../types/events';
import { LogProvider, Logger } from '../logger';
import { IEventStore } from '../types/stores/event-store';
import { ICreateEvent, IEvent } from '../types/model';
import { ITag } from '../types/model';
const EVENT_COLUMNS = [
'id',
@ -11,7 +11,9 @@ const EVENT_COLUMNS = [
'created_by',
'created_at',
'data',
'pre_data',
'tags',
'feature_name',
'project',
'environment',
];
@ -21,10 +23,12 @@ export interface IEventTable {
type: string;
created_by: string;
created_at: Date;
data: any;
data?: any;
pre_data?: any;
feature_name?: string;
project?: string;
environment?: string;
tags: [];
tags: ITag[];
}
const TABLE = 'events';
@ -40,7 +44,7 @@ class EventStore extends EventEmitter implements IEventStore {
this.logger = getLogger('lib/db/event-store.ts');
}
async store(event: ICreateEvent): Promise<void> {
async store(event: IBaseEvent): Promise<void> {
try {
const rows = await this.db(TABLE)
.insert(this.eventToDbRow(event))
@ -52,7 +56,7 @@ class EventStore extends EventEmitter implements IEventStore {
}
}
async batchStore(events: ICreateEvent[]): Promise<void> {
async batchStore(events: IBaseEvent[]): Promise<void> {
try {
const savedRows = await this.db(TABLE)
.insert(events.map(this.eventToDbRow))
@ -129,6 +133,7 @@ class EventStore extends EventEmitter implements IEventStore {
.orderBy('created_at', 'desc');
return rows.map(this.rowToEvent);
} catch (err) {
this.logger.error(err);
return [];
}
}
@ -146,6 +151,19 @@ class EventStore extends EventEmitter implements IEventStore {
}
}
async getEventsForFeature(featureName: string): Promise<IEvent[]> {
try {
const rows = await this.db
.select(EVENT_COLUMNS)
.from(TABLE)
.where({ feature_name: featureName })
.orderBy('created_at', 'desc');
return rows.map(this.rowToEvent);
} catch (err) {
return [];
}
}
rowToEvent(row: IEventTable): IEvent {
return {
id: row.id,
@ -153,23 +171,27 @@ class EventStore extends EventEmitter implements IEventStore {
createdBy: row.created_by,
createdAt: row.created_at,
data: row.data,
preData: row.pre_data,
tags: row.tags || [],
featureName: row.feature_name,
project: row.project,
environment: row.environment,
};
}
eventToDbRow(e: ICreateEvent): any {
eventToDbRow(e: IBaseEvent): Omit<IEventTable, 'id' | 'created_at'> {
return {
type: e.type,
created_by: e.createdBy,
data: e.data,
pre_data: e.preData,
//@ts-ignore workaround for json-array
tags: JSON.stringify(e.tags),
feature_name: e.featureName,
project: e.project,
environment: e.environment,
};
}
}
module.exports = EventStore;
export default EventStore;

View File

@ -1,171 +0,0 @@
/* eslint-disable no-param-reassign */
'use strict';
const { diff } = require('deep-diff');
const {
STRATEGY_CREATED,
STRATEGY_DELETED,
STRATEGY_UPDATED,
STRATEGY_IMPORT,
STRATEGY_DEPRECATED,
STRATEGY_REACTIVATED,
DROP_STRATEGIES,
FEATURE_CREATED,
FEATURE_UPDATED,
FEATURE_ARCHIVED,
FEATURE_REVIVED,
FEATURE_IMPORT,
FEATURE_TAGGED,
FEATURE_UNTAGGED,
DROP_FEATURES,
CONTEXT_FIELD_CREATED,
CONTEXT_FIELD_UPDATED,
CONTEXT_FIELD_DELETED,
PROJECT_CREATED,
PROJECT_UPDATED,
PROJECT_DELETED,
TAG_CREATED,
TAG_DELETED,
TAG_TYPE_CREATED,
TAG_TYPE_DELETED,
APPLICATION_CREATED,
FEATURE_STALE_ON,
FEATURE_STALE_OFF,
USER_CREATED,
USER_UPDATED,
USER_DELETED,
} = require('./types/events');
const strategyTypes = [
STRATEGY_CREATED,
STRATEGY_DELETED,
STRATEGY_UPDATED,
STRATEGY_IMPORT,
STRATEGY_DEPRECATED,
STRATEGY_REACTIVATED,
DROP_STRATEGIES,
];
const featureTypes = [
FEATURE_CREATED,
FEATURE_UPDATED,
FEATURE_ARCHIVED,
FEATURE_REVIVED,
FEATURE_IMPORT,
FEATURE_TAGGED,
FEATURE_UNTAGGED,
DROP_FEATURES,
FEATURE_STALE_ON,
FEATURE_STALE_OFF,
];
const contextTypes = [
CONTEXT_FIELD_CREATED,
CONTEXT_FIELD_DELETED,
CONTEXT_FIELD_UPDATED,
];
const userTypes = [USER_CREATED, USER_UPDATED, USER_DELETED];
const tagTypes = [TAG_CREATED, TAG_DELETED];
const tagTypeTypes = [TAG_TYPE_CREATED, TAG_TYPE_DELETED];
const projectTypes = [PROJECT_CREATED, PROJECT_UPDATED, PROJECT_DELETED];
function baseTypeFor(event) {
if (featureTypes.indexOf(event.type) !== -1) {
return 'features';
}
if (strategyTypes.indexOf(event.type) !== -1) {
return 'strategies';
}
if (contextTypes.indexOf(event.type) !== -1) {
return 'context';
}
if (projectTypes.indexOf(event.type) !== -1) {
return 'project';
}
if (tagTypes.indexOf(event.type) !== -1) {
return 'tag';
}
if (tagTypeTypes.indexOf(event.type) !== -1) {
return 'tag-type';
}
if (userTypes.indexOf(event.type) !== -1) {
return 'user';
}
if (event.type === APPLICATION_CREATED) {
return 'application';
}
return event.type;
}
const uniqueFieldForType = (baseType) => {
if (baseType === 'user') {
return 'id';
}
return 'name';
};
function groupByBaseTypeAndName(events) {
const groups = {};
events.forEach((event) => {
const baseType = baseTypeFor(event);
const uniqueField = uniqueFieldForType(baseType);
groups[baseType] = groups[baseType] || {};
groups[baseType][event.data[uniqueField]] =
groups[baseType][event.data[uniqueField]] || [];
groups[baseType][event.data[uniqueField]].push(event);
});
return groups;
}
function eachConsecutiveEvent(events, callback) {
const groups = groupByBaseTypeAndName(events);
Object.keys(groups).forEach((baseType) => {
const group = groups[baseType];
Object.keys(group).forEach((name) => {
const currentEvents = group[name];
let left;
let right;
let i;
let l;
for (i = 0, l = currentEvents.length; i < l; i++) {
left = currentEvents[i];
right = currentEvents[i + 1];
callback(left, right);
}
});
});
}
const ignoredProps = ['createdAt', 'lastSeenAt', 'id'];
const filterProps = (path, key) => {
return ignoredProps.includes(key);
};
function addDiffs(events = []) {
// TODO: no-param-reassign
eachConsecutiveEvent(events, (left, right) => {
if (right) {
left.diffs = diff(right.data, left.data, filterProps);
left.diffs = left.diffs || [];
} else {
left.diffs = null;
}
});
}
module.exports = {
addDiffs,
};

View File

@ -1,158 +0,0 @@
'use strict';
const eventDiffer = require('./event-differ');
const { FEATURE_CREATED, FEATURE_UPDATED } = require('./types/events');
test('should not fail if events include an unknown event type', () => {
const events = [
{ type: FEATURE_CREATED, data: {} },
{ type: 'unknown-type', data: {} },
];
eventDiffer.addDiffs(events);
expect(true).toBe(true);
});
test('diffs a feature-update event', () => {
const feature = 'foo';
const desc = 'bar';
const events = [
{
type: FEATURE_UPDATED,
data: {
name: feature,
description: desc,
strategy: 'default',
enabled: true,
parameters: { value: 2 },
},
},
{
type: FEATURE_CREATED,
data: {
name: feature,
description: desc,
strategy: 'default',
enabled: false,
parameters: { value: 1 },
},
},
];
eventDiffer.addDiffs(events);
const { diffs } = events[0];
expect(diffs[0].kind === 'E').toBe(true);
expect(diffs[0].path[0] === 'enabled').toBe(true);
expect(diffs[0].kind === 'E').toBe(true);
expect(diffs[0].lhs === false).toBe(true);
expect(diffs[0].rhs).toBe(true);
expect(diffs[1].kind === 'E').toBe(true);
expect(diffs[1].path[0] === 'parameters').toBe(true);
expect(diffs[1].path[1] === 'value').toBe(true);
expect(diffs[1].kind === 'E').toBe(true);
expect(diffs[1].lhs === 1).toBe(true);
expect(events[1].diffs === null).toBe(true);
});
test('diffs only against features with the same name', () => {
const events = [
{
type: FEATURE_UPDATED,
data: {
name: 'bar',
description: 'desc',
strategy: 'default',
enabled: true,
parameters: {},
},
},
{
type: FEATURE_UPDATED,
data: {
name: 'foo',
description: 'desc',
strategy: 'default',
enabled: false,
parameters: {},
},
},
{
type: FEATURE_CREATED,
data: {
name: 'bar',
description: 'desc',
strategy: 'default',
enabled: false,
parameters: {},
},
},
{
type: FEATURE_CREATED,
data: {
name: 'foo',
description: 'desc',
strategy: 'default',
enabled: true,
parameters: {},
},
},
];
eventDiffer.addDiffs(events);
expect(events[0].diffs[0].rhs === true).toBe(true);
expect(events[1].diffs[0].rhs === false).toBe(true);
expect(events[2].diffs === null).toBe(true);
expect(events[3].diffs === null).toBe(true);
});
test('sets an empty array of diffs if nothing was changed', () => {
const events = [
{
type: FEATURE_UPDATED,
data: {
name: 'foo',
description: 'desc',
strategy: 'default',
enabled: true,
parameters: {},
},
},
{
type: FEATURE_CREATED,
data: {
name: 'foo',
description: 'desc',
strategy: 'default',
enabled: true,
parameters: {},
},
},
];
eventDiffer.addDiffs(events);
expect(events[0].diffs).toEqual([]);
});
test('sets diffs to null if there was nothing to diff against', () => {
const events = [
{
type: FEATURE_UPDATED,
data: {
name: 'foo',
description: 'desc',
strategy: 'default',
enabled: true,
parameters: {},
},
},
];
eventDiffer.addDiffs(events);
expect(events[0].diffs === null).toBe(true);
});

View File

@ -53,6 +53,7 @@ test('should collect metrics for requests', async () => {
test('should collect metrics for updated toggles', async () => {
stores.eventStore.emit(FEATURE_UPDATED, {
featureName: 'TestToggle',
data: { name: 'TestToggle' },
});

View File

@ -7,6 +7,9 @@ import {
FEATURE_ARCHIVED,
FEATURE_CREATED,
FEATURE_REVIVED,
FEATURE_STRATEGY_ADD,
FEATURE_STRATEGY_REMOVE,
FEATURE_STRATEGY_UPDATE,
FEATURE_UPDATED,
} from './types/events';
import { IUnleashConfig } from './types/option';
@ -123,17 +126,26 @@ export default class MetricsMonitor {
dbDuration.labels(store, action).observe(time);
});
eventStore.on(FEATURE_CREATED, ({ data }) => {
featureToggleUpdateTotal.labels(data.name).inc();
eventStore.on(FEATURE_CREATED, ({ featureName }) => {
featureToggleUpdateTotal.labels(featureName).inc();
});
eventStore.on(FEATURE_UPDATED, ({ data }) => {
featureToggleUpdateTotal.labels(data.name).inc();
eventStore.on(FEATURE_UPDATED, ({ featureName }) => {
featureToggleUpdateTotal.labels(featureName).inc();
});
eventStore.on(FEATURE_ARCHIVED, ({ data }) => {
featureToggleUpdateTotal.labels(data.name).inc();
eventStore.on(FEATURE_STRATEGY_ADD, ({ featureName }) => {
featureToggleUpdateTotal.labels(featureName).inc();
});
eventStore.on(FEATURE_REVIVED, ({ data }) => {
featureToggleUpdateTotal.labels(data.name).inc();
eventStore.on(FEATURE_STRATEGY_REMOVE, ({ featureName }) => {
featureToggleUpdateTotal.labels(featureName).inc();
});
eventStore.on(FEATURE_STRATEGY_UPDATE, ({ featureName }) => {
featureToggleUpdateTotal.labels(featureName).inc();
});
eventStore.on(FEATURE_ARCHIVED, ({ featureName }) => {
featureToggleUpdateTotal.labels(featureName).inc();
});
eventStore.on(FEATURE_REVIVED, ({ featureName }) => {
featureToggleUpdateTotal.labels(featureName).inc();
});
clientMetricsStore.on('metrics', (m) => {

View File

@ -7,13 +7,13 @@ import Controller from '../controller';
import { extractUsername } from '../../util/extract-user';
import { DELETE_FEATURE, UPDATE_FEATURE } from '../../types/permissions';
import FeatureToggleServiceV2 from '../../services/feature-toggle-service';
import FeatureToggleService from '../../services/feature-toggle-service';
import { IAuthRequest } from '../unleash-types';
export default class ArchiveController extends Controller {
private readonly logger: Logger;
private featureService: FeatureToggleServiceV2;
private featureService: FeatureToggleService;
constructor(
config: IUnleashConfig,

View File

@ -1,11 +1,10 @@
import { Request, Response } from 'express';
import { IUnleashConfig } from '../../types/option';
import { IUnleashServices } from '../../types/services';
import EventService from '../../services/event-service';
import { ADMIN } from '../../types/permissions';
const Controller = require('../controller');
const eventDiffer = require('../../event-differ');
import { IEvent } from '../../types/events';
import Controller from '../controller';
const version = 1;
@ -22,32 +21,31 @@ export default class EventController extends Controller {
this.get('/:name', this.getEventsForToggle);
}
// eslint-disable-next-line @typescript-eslint/explicit-module-boundary-types
async getEvents(req, res): Promise<void> {
let events;
if (req.query?.project) {
events = await this.eventService.getEventsForProject(
req.query.project,
);
async getEvents(
req: Request<any, any, any, { project?: string }>,
res: Response,
): Promise<void> {
const { project } = req.query;
let events: IEvent[];
if (project) {
events = await this.eventService.getEventsForProject(project);
} else {
events = await this.eventService.getEvents();
}
eventDiffer.addDiffs(events);
res.json({ version, events });
}
// eslint-disable-next-line @typescript-eslint/explicit-module-boundary-types
async getEventsForToggle(req, res): Promise<void> {
async getEventsForToggle(
req: Request<{ name: string }>,
res: Response,
): Promise<void> {
const toggleName = req.params.name;
const events = await this.eventService.getEventsForToggle(toggleName);
if (events) {
eventDiffer.addDiffs(events);
res.json({ toggleName, events });
} else {
res.status(404).json({ error: 'Could not find events' });
}
}
}
module.exports = EventController;

View File

@ -11,7 +11,7 @@ import {
} from '../../types/permissions';
import { IUnleashConfig } from '../../types/option';
import { IUnleashServices } from '../../types/services';
import FeatureToggleServiceV2 from '../../services/feature-toggle-service';
import FeatureToggleService from '../../services/feature-toggle-service';
import { featureSchema, querySchema } from '../../schema/feature-schema';
import { IFeatureToggleQuery } from '../../types/model';
import FeatureTagService from '../../services/feature-tag-service';
@ -23,7 +23,7 @@ const version = 1;
class FeatureController extends Controller {
private tagService: FeatureTagService;
private service: FeatureToggleServiceV2;
private service: FeatureToggleService;
constructor(
config: IUnleashConfig,

View File

@ -3,7 +3,7 @@ import { applyPatch, Operation } from 'fast-json-patch';
import Controller from '../../controller';
import { IUnleashConfig } from '../../../types/option';
import { IUnleashServices } from '../../../types/services';
import FeatureToggleServiceV2 from '../../../services/feature-toggle-service';
import FeatureToggleService from '../../../services/feature-toggle-service';
import { Logger } from '../../../logger';
import { CREATE_FEATURE, UPDATE_FEATURE } from '../../../types/permissions';
import {
@ -52,7 +52,7 @@ type ProjectFeaturesServices = Pick<
>;
export default class ProjectFeaturesController extends Controller {
private featureService: FeatureToggleServiceV2;
private featureService: FeatureToggleService;
private readonly logger: Logger;

View File

@ -103,4 +103,3 @@ class StateController extends Controller {
}
}
export default StateController;
module.exports = StateController;

View File

@ -3,7 +3,7 @@ import { Response } from 'express';
import Controller from '../controller';
import { IUnleashServices } from '../../types/services';
import { IUnleashConfig } from '../../types/option';
import FeatureToggleServiceV2 from '../../services/feature-toggle-service';
import FeatureToggleService from '../../services/feature-toggle-service';
import { Logger } from '../../logger';
import { querySchema } from '../../schema/feature-schema';
import { IFeatureToggleQuery } from '../../types/model';
@ -22,7 +22,7 @@ interface QueryOverride {
export default class FeatureController extends Controller {
private readonly logger: Logger;
private featureToggleServiceV2: FeatureToggleServiceV2;
private featureToggleServiceV2: FeatureToggleService;
private readonly cache: boolean;

View File

@ -1,11 +1,12 @@
import Addon from '../addons/addon';
import getLogger from '../../test/fixtures/no-logger';
import { IAddonDefinition, IEvent } from '../types/model';
import { IAddonDefinition } from '../types/model';
import {
FEATURE_ARCHIVED,
FEATURE_CREATED,
FEATURE_REVIVED,
FEATURE_UPDATED,
IEvent,
} from '../types/events';
const definition: IAddonDefinition = {

View File

@ -1,7 +1,7 @@
import { applicationSchema } from './metrics-schema';
import { Projection } from './projection';
import { clientMetricsSchema } from './client-metrics-schema';
import { APPLICATION_CREATED } from '../../types/events';
import { APPLICATION_CREATED, IBaseEvent } from '../../types/events';
import { IApplication, IYesNoCount } from './models';
import { IUnleashStores } from '../../types/stores';
import { IUnleashConfig } from '../../types/option';
@ -15,12 +15,7 @@ import { IStrategyStore } from '../../types/stores/strategy-store';
import { IClientMetricsStore } from '../../types/stores/client-metrics-store';
import { IClientInstanceStore } from '../../types/stores/client-instance-store';
import { IApplicationQuery } from '../../types/query';
import {
IClientApp,
ICreateEvent,
IMetricCounts,
IMetricsBucket,
} from '../../types/model';
import { IClientApp, IMetricCounts, IMetricsBucket } from '../../types/model';
import { clientRegisterSchema } from './register-schema';
import {
@ -207,7 +202,7 @@ export default class ClientMetricsService {
}
}
appToEvent(app: IClientApp): ICreateEvent {
appToEvent(app: IClientApp): IBaseEvent {
return {
type: APPLICATION_CREATED,
createdBy: app.clientIp,

View File

@ -2,7 +2,7 @@ import { IUnleashConfig } from '../types/option';
import { IUnleashStores } from '../types/stores';
import { Logger } from '../logger';
import { IEventStore } from '../types/stores/event-store';
import { IEvent } from '../types/model';
import { IEvent } from '../types/events';
export default class EventService {
private logger: Logger;
@ -22,7 +22,7 @@ export default class EventService {
}
async getEventsForToggle(name: string): Promise<IEvent[]> {
return this.eventStore.getEventsFilterByType(name);
return this.eventStore.getEventsForFeature(name);
}
async getEventsForProject(project: string): Promise<IEvent[]> {

View File

@ -37,6 +37,7 @@ class FeatureTagService {
return this.featureTagStore.getAllTagsForFeature(featureName);
}
// TODO: add project Id
async addTag(
featureName: string,
tag: ITag,
@ -50,10 +51,8 @@ class FeatureTagService {
await this.eventStore.store({
type: FEATURE_TAGGED,
createdBy: userName,
data: {
featureName,
tag: validatedTag,
},
featureName,
data: validatedTag,
});
return validatedTag;
}
@ -67,14 +66,13 @@ class FeatureTagService {
await this.eventStore.store({
type: TAG_CREATED,
createdBy: userName,
data: {
tag,
},
data: tag,
});
}
}
}
// TODO: add project Id
async removeTag(
featureName: string,
tag: ITag,
@ -84,10 +82,8 @@ class FeatureTagService {
await this.eventStore.store({
type: FEATURE_UNTAGGED,
createdBy: userName,
data: {
featureName,
tag,
},
featureName,
data: tag,
});
}
}

View File

@ -7,19 +7,17 @@ import InvalidOperationError from '../error/invalid-operation-error';
import { FOREIGN_KEY_VIOLATION } from '../error/db-error';
import { featureMetadataSchema, nameSchema } from '../schema/feature-schema';
import {
FEATURE_ARCHIVED,
FEATURE_CREATED,
FEATURE_DELETED,
FEATURE_ENVIRONMENT_DISABLED,
FEATURE_ENVIRONMENT_ENABLED,
FEATURE_METADATA_UPDATED,
FEATURE_PROJECT_CHANGE,
FEATURE_REVIVED,
FEATURE_STALE_OFF,
FEATURE_STALE_ON,
FEATURE_STRATEGY_ADD,
FEATURE_STRATEGY_REMOVE,
FEATURE_STRATEGY_UPDATE,
FeatureArchivedEvent,
FeatureChangeProjectEvent,
FeatureCreatedEvent,
FeatureDeletedEvent,
FeatureEnvironmentEvent,
FeatureMetadataUpdateEvent,
FeatureRevivedEvent,
FeatureStaleEvent,
FeatureStrategyAddEvent,
FeatureStrategyRemoveEvent,
FeatureStrategyUpdateEvent,
FEATURE_UPDATED,
} from '../types/events';
import NotFoundError from '../error/notfound-error';
@ -57,7 +55,7 @@ interface IFeatureStrategyContext extends IFeatureContext {
environment: string;
}
class FeatureToggleServiceV2 {
class FeatureToggleService {
private logger: Logger;
private featureStrategiesStore: IFeatureStrategiesStore;
@ -66,7 +64,7 @@ class FeatureToggleServiceV2 {
private featureToggleClientStore: IFeatureToggleClientStore;
private featureTagStore: IFeatureTagStore;
private tagStore: IFeatureTagStore;
private featureEnvironmentStore: IFeatureEnvironmentStore;
@ -99,7 +97,7 @@ class FeatureToggleServiceV2 {
this.featureStrategiesStore = featureStrategiesStore;
this.featureToggleStore = featureToggleStore;
this.featureToggleClientStore = featureToggleClientStore;
this.featureTagStore = featureTagStore;
this.tagStore = featureTagStore;
this.projectStore = projectStore;
this.eventStore = eventStore;
this.featureEnvironmentStore = featureEnvironmentStore;
@ -135,9 +133,9 @@ class FeatureToggleServiceV2 {
}
async patchFeature(
projectId: string,
project: string,
featureName: string,
userName: string,
createdBy: string,
operations: Operation[],
): Promise<FeatureToggle> {
const featureToggle = await this.getFeatureMetadata(featureName);
@ -146,26 +144,45 @@ class FeatureToggleServiceV2 {
deepClone(featureToggle),
operations,
);
const updated = await this.updateFeatureToggle(
projectId,
project,
newDocument,
userName,
createdBy,
);
if (featureToggle.stale !== newDocument.stale) {
await this.eventStore.store({
type: newDocument.stale ? FEATURE_STALE_ON : FEATURE_STALE_OFF,
data: updated,
project: projectId,
createdBy: userName,
});
const tags = await this.tagStore.getAllTagsForFeature(featureName);
await this.eventStore.store(
new FeatureStaleEvent({
stale: newDocument.stale,
project,
featureName,
createdBy,
tags,
}),
);
}
return updated;
}
featureStrategyToPublic(
featureStrategy: IFeatureStrategy,
): IStrategyConfig {
return {
id: featureStrategy.id,
name: featureStrategy.strategyName,
constraints: featureStrategy.constraints || [],
parameters: featureStrategy.parameters,
};
}
async createStrategy(
strategyConfig: Omit<IStrategyConfig, 'id'>,
context: IFeatureStrategyContext,
userName: string,
createdBy: string,
): Promise<IStrategyConfig> {
const { featureName, projectId, environment } = context;
await this.validateFeatureContext(context);
@ -180,24 +197,20 @@ class FeatureToggleServiceV2 {
featureName,
environment,
});
const data = {
id: newFeatureStrategy.id,
name: newFeatureStrategy.strategyName,
constraints: newFeatureStrategy.constraints,
parameters: newFeatureStrategy.parameters,
};
await this.eventStore.store({
type: FEATURE_STRATEGY_ADD,
project: projectId,
createdBy: userName,
environment,
data: {
...data,
name: featureName, // Done like this since we use data as our return object.
strategyName: newFeatureStrategy.strategyName,
},
});
return data;
const tags = await this.tagStore.getAllTagsForFeature(featureName);
const strategy = this.featureStrategyToPublic(newFeatureStrategy);
await this.eventStore.store(
new FeatureStrategyAddEvent({
project: projectId,
featureName,
createdBy,
environment,
data: strategy,
tags,
}),
);
return strategy;
} catch (e) {
if (e.code === FOREIGN_KEY_VIOLATION) {
throw new BadDataError(
@ -224,7 +237,7 @@ class FeatureToggleServiceV2 {
context: IFeatureStrategyContext,
userName: string,
): Promise<IStrategyConfig> {
const { projectId, environment } = context;
const { projectId, environment, featureName } = context;
const existingStrategy = await this.featureStrategiesStore.get(id);
this.validateFeatureStrategyContext(existingStrategy, context);
@ -233,20 +246,22 @@ class FeatureToggleServiceV2 {
id,
updates,
);
const data = {
id: strategy.id,
name: strategy.strategyName,
featureName: strategy.featureName,
constraints: strategy.constraints || [],
parameters: strategy.parameters,
};
await this.eventStore.store({
type: FEATURE_STRATEGY_UPDATE,
project: projectId,
environment,
createdBy: userName,
data,
});
// Store event!
const tags = await this.tagStore.getAllTagsForFeature(featureName);
const data = this.featureStrategyToPublic(strategy);
const preData = this.featureStrategyToPublic(existingStrategy);
await this.eventStore.store(
new FeatureStrategyUpdateEvent({
project: projectId,
featureName,
environment,
createdBy: userName,
data,
preData,
tags,
}),
);
return data;
}
throw new NotFoundError(`Could not find strategy with id ${id}`);
@ -259,7 +274,7 @@ class FeatureToggleServiceV2 {
context: IFeatureStrategyContext,
userName: string,
): Promise<IStrategyConfig> {
const { projectId, environment } = context;
const { projectId, environment, featureName } = context;
const existingStrategy = await this.featureStrategiesStore.get(id);
this.validateFeatureStrategyContext(existingStrategy, context);
@ -270,19 +285,20 @@ class FeatureToggleServiceV2 {
id,
existingStrategy,
);
const data = {
id: strategy.id,
name: strategy.strategyName,
constraints: strategy.constraints || [],
parameters: strategy.parameters,
};
await this.eventStore.store({
type: FEATURE_STRATEGY_UPDATE,
project: projectId,
environment,
createdBy: userName,
data,
});
const tags = await this.tagStore.getAllTagsForFeature(featureName);
const data = this.featureStrategyToPublic(strategy);
const preData = this.featureStrategyToPublic(existingStrategy);
await this.eventStore.store(
new FeatureStrategyUpdateEvent({
featureName,
project: projectId,
environment,
createdBy: userName,
data,
preData,
tags,
}),
);
return data;
}
throw new NotFoundError(`Could not find strategy with id ${id}`);
@ -300,23 +316,27 @@ class FeatureToggleServiceV2 {
async deleteStrategy(
id: string,
context: IFeatureStrategyContext,
userName: string,
createdBy: string,
): Promise<void> {
const existingStrategy = await this.featureStrategiesStore.get(id);
const { featureName, projectId, environment } = context;
this.validateFeatureStrategyContext(existingStrategy, context);
await this.featureStrategiesStore.delete(id);
await this.eventStore.store({
type: FEATURE_STRATEGY_REMOVE,
project: projectId,
environment,
createdBy: userName,
data: {
id,
name: featureName,
},
});
const tags = await this.tagStore.getAllTagsForFeature(featureName);
const preData = this.featureStrategyToPublic(existingStrategy);
await this.eventStore.store(
new FeatureStrategyRemoveEvent({
featureName,
project: projectId,
environment,
createdBy,
preData,
tags,
}),
);
// If there are no strategies left for environment disable it
await this.featureEnvironmentStore.disableEnvironmentIfNoStrategies(
@ -419,32 +439,36 @@ class FeatureToggleServiceV2 {
async createFeatureToggle(
projectId: string,
value: FeatureToggleDTO,
userName: string,
createdBy: string,
): Promise<FeatureToggle> {
this.logger.info(`${userName} creates feature toggle ${value.name}`);
this.logger.info(`${createdBy} creates feature toggle ${value.name}`);
await this.validateName(value.name);
const exists = await this.projectStore.hasProject(projectId);
if (exists) {
const featureData = await featureMetadataSchema.validateAsync(
value,
);
const featureName = featureData.name;
const createdToggle = await this.featureToggleStore.create(
projectId,
featureData,
);
await this.featureEnvironmentStore.connectFeatureToEnvironmentsForProject(
featureData.name,
featureName,
projectId,
);
const data = { ...featureData, project: projectId };
const tags = await this.tagStore.getAllTagsForFeature(featureName);
await this.eventStore.store({
type: FEATURE_CREATED,
createdBy: userName,
project: projectId,
data,
});
await this.eventStore.store(
new FeatureCreatedEvent({
featureName,
createdBy,
project: projectId,
data: createdToggle,
tags,
}),
);
return createdToggle;
}
@ -516,21 +540,24 @@ class FeatureToggleServiceV2 {
updatedFeature,
);
const preData = await this.featureToggleStore.get(featureName);
const featureToggle = await this.featureToggleStore.update(
projectId,
featureData,
);
const tags = await this.featureTagStore.getAllTagsForFeature(
featureName,
);
const tags = await this.tagStore.getAllTagsForFeature(featureName);
await this.eventStore.store({
type: FEATURE_METADATA_UPDATED,
createdBy: userName,
data: featureToggle,
project: projectId,
tags,
});
await this.eventStore.store(
new FeatureMetadataUpdateEvent({
createdBy: userName,
data: featureToggle,
preData,
featureName,
project: projectId,
tags,
}),
);
return featureToggle;
}
@ -587,6 +614,7 @@ class FeatureToggleServiceV2 {
};
}
// todo: store events for this change.
async deleteEnvironment(
projectId: string,
environment: string,
@ -609,7 +637,7 @@ class FeatureToggleServiceV2 {
}
async validateUniqueFeatureName(name: string): Promise<void> {
let msg;
let msg: string;
try {
const feature = await this.featureToggleStore.get(name);
msg = feature.archived
@ -628,46 +656,48 @@ class FeatureToggleServiceV2 {
async updateStale(
featureName: string,
isStale: boolean,
userName: string,
createdBy: string,
): Promise<any> {
const feature = await this.featureToggleStore.get(featureName);
const { project } = feature;
feature.stale = isStale;
await this.featureToggleStore.update(feature.project, feature);
const tags = await this.featureTagStore.getAllTagsForFeature(
featureName,
);
const data = await this.getFeatureToggleLegacy(featureName);
await this.featureToggleStore.update(project, feature);
const tags = await this.tagStore.getAllTagsForFeature(featureName);
await this.eventStore.store(
new FeatureStaleEvent({
stale: isStale,
project,
featureName,
createdBy,
tags,
}),
);
await this.eventStore.store({
type: isStale ? FEATURE_STALE_ON : FEATURE_STALE_OFF,
createdBy: userName,
data,
tags,
project: feature.project,
});
return feature;
}
async archiveToggle(name: string, userName: string): Promise<void> {
const feature = await this.featureToggleStore.get(name);
await this.featureToggleStore.archive(name);
const tags =
(await this.featureTagStore.getAllTagsForFeature(name)) || [];
await this.eventStore.store({
type: FEATURE_ARCHIVED,
createdBy: userName,
data: { name },
project: feature.project,
tags,
});
// todo: add projectId
async archiveToggle(featureName: string, createdBy: string): Promise<void> {
const feature = await this.featureToggleStore.get(featureName);
await this.featureToggleStore.archive(featureName);
const tags = await this.tagStore.getAllTagsForFeature(featureName);
await this.eventStore.store(
new FeatureArchivedEvent({
featureName,
createdBy,
project: feature.project,
tags,
}),
);
}
async updateEnabled(
projectId: string,
project: string,
featureName: string,
environment: string,
enabled: boolean,
userName: string,
createdBy: string,
): Promise<FeatureToggle> {
const hasEnvironment =
await this.featureEnvironmentStore.featureHasEnvironment(
@ -678,7 +708,7 @@ class FeatureToggleServiceV2 {
if (hasEnvironment) {
if (enabled) {
const strategies = await this.getStrategiesForEnvironment(
projectId,
project,
featureName,
environment,
);
@ -697,19 +727,19 @@ class FeatureToggleServiceV2 {
const feature = await this.featureToggleStore.get(featureName);
if (updatedEnvironmentStatus > 0) {
const tags = await this.featureTagStore.getAllTagsForFeature(
const tags = await this.tagStore.getAllTagsForFeature(
featureName,
);
await this.eventStore.store({
type: enabled
? FEATURE_ENVIRONMENT_ENABLED
: FEATURE_ENVIRONMENT_DISABLED,
createdBy: userName,
data: { name: featureName },
tags,
project: projectId,
environment,
});
await this.eventStore.store(
new FeatureEnvironmentEvent({
enabled,
project,
featureName,
environment,
createdBy,
tags,
}),
);
}
return feature;
}
@ -718,18 +748,20 @@ class FeatureToggleServiceV2 {
);
}
// @deprecated
async storeFeatureUpdatedEventLegacy(
featureName: string,
userName: string,
createdBy: string,
): Promise<FeatureToggleLegacy> {
const tags = await this.featureTagStore.getAllTagsForFeature(
featureName,
);
const tags = await this.tagStore.getAllTagsForFeature(featureName);
const feature = await this.getFeatureToggleLegacy(featureName);
// Legacy event. Will not be used from v4.3.
// We do not include 'preData' on purpose.
await this.eventStore.store({
type: FEATURE_UPDATED,
createdBy: userName,
createdBy,
featureName,
data: feature,
tags,
project: feature.project,
@ -759,6 +791,7 @@ class FeatureToggleServiceV2 {
);
}
// @deprecated
async getFeatureToggleLegacy(
featureName: string,
): Promise<FeatureToggleLegacy> {
@ -777,56 +810,57 @@ class FeatureToggleServiceV2 {
async changeProject(
featureName: string,
newProject: string,
userName: string,
createdBy: string,
): Promise<void> {
const feature = await this.featureToggleStore.get(featureName);
const oldProject = feature.project;
feature.project = newProject;
await this.featureToggleStore.update(newProject, feature);
const tags = await this.featureTagStore.getAllTagsForFeature(
featureName,
);
await this.eventStore.store({
type: FEATURE_PROJECT_CHANGE,
createdBy: userName,
data: {
name: feature.name,
const tags = await this.tagStore.getAllTagsForFeature(featureName);
await this.eventStore.store(
new FeatureChangeProjectEvent({
createdBy,
oldProject,
newProject,
},
project: newProject,
tags,
});
featureName,
tags,
}),
);
}
async getArchivedFeatures(): Promise<FeatureToggle[]> {
return this.getFeatureToggles({}, true);
}
async deleteFeature(featureName: string, userName: string): Promise<void> {
// TODO: add project id.
async deleteFeature(featureName: string, createdBy: string): Promise<void> {
const toggle = await this.featureToggleStore.get(featureName);
const tags = await this.tagStore.getAllTagsForFeature(featureName);
await this.featureToggleStore.delete(featureName);
await this.eventStore.store({
type: FEATURE_DELETED,
createdBy: userName,
data: {
await this.eventStore.store(
new FeatureDeletedEvent({
featureName,
},
});
project: toggle.project,
createdBy,
preData: toggle,
tags,
}),
);
}
async reviveToggle(featureName: string, userName: string): Promise<void> {
const data = await this.featureToggleStore.revive(featureName);
const tags = await this.featureTagStore.getAllTagsForFeature(
featureName,
// TODO: add project id.
async reviveToggle(featureName: string, createdBy: string): Promise<void> {
const toggle = await this.featureToggleStore.revive(featureName);
const tags = await this.tagStore.getAllTagsForFeature(featureName);
await this.eventStore.store(
new FeatureRevivedEvent({
createdBy,
featureName,
project: toggle.project,
tags,
}),
);
await this.eventStore.store({
type: FEATURE_REVIVED,
createdBy: userName,
data,
project: data.project,
tags,
});
}
async getMetadataForAllFeatures(
@ -850,5 +884,4 @@ class FeatureToggleServiceV2 {
}
}
module.exports = FeatureToggleServiceV2;
export default FeatureToggleServiceV2;
export default FeatureToggleService;

View File

@ -11,7 +11,7 @@ import {
import { IFeatureToggleStore } from '../types/stores/feature-toggle-store';
import { IFeatureTypeStore } from '../types/stores/feature-type-store';
import { IProjectStore } from '../types/stores/project-store';
import FeatureToggleServiceV2 from './feature-toggle-service';
import FeatureToggleService from './feature-toggle-service';
import { hoursToMilliseconds } from 'date-fns';
import Timer = NodeJS.Timer;
@ -28,7 +28,7 @@ export default class ProjectHealthService {
private healthRatingTimer: Timer;
private featureToggleService: FeatureToggleServiceV2;
private featureToggleService: FeatureToggleService;
constructor(
{
@ -40,7 +40,7 @@ export default class ProjectHealthService {
'projectStore' | 'featureTypeStore' | 'featureToggleStore'
>,
{ getLogger }: Pick<IUnleashConfig, 'getLogger'>,
featureToggleService: FeatureToggleServiceV2,
featureToggleService: FeatureToggleService,
) {
this.logger = getLogger('services/project-health-service.ts');
this.projectStore = projectStore;

View File

@ -27,7 +27,7 @@ import { IFeatureEnvironmentStore } from '../types/stores/feature-environment-st
import { IProjectQuery, IProjectStore } from '../types/stores/project-store';
import { IRole } from '../types/stores/access-store';
import { IEventStore } from '../types/stores/event-store';
import FeatureToggleServiceV2 from './feature-toggle-service';
import FeatureToggleService from './feature-toggle-service';
import { CREATE_FEATURE, UPDATE_FEATURE } from '../types/permissions';
import NoAccessError from '../error/no-access-error';
import IncompatibleProjectError from '../error/incompatible-project-error';
@ -58,7 +58,7 @@ export default class ProjectService {
private logger: any;
private featureToggleService: FeatureToggleServiceV2;
private featureToggleService: FeatureToggleService;
private environmentsEnabled: boolean = false;
@ -81,7 +81,7 @@ export default class ProjectService {
>,
config: IUnleashConfig,
accessService: AccessService,
featureToggleService: FeatureToggleServiceV2,
featureToggleService: FeatureToggleService,
) {
this.store = projectStore;
this.environmentStore = environmentStore;
@ -161,16 +161,17 @@ export default class ProjectService {
}
async updateProject(updatedProject: IProject, user: User): Promise<void> {
await this.store.get(updatedProject.id);
const preData = await this.store.get(updatedProject.id);
const project = await projectSchema.validateAsync(updatedProject);
await this.store.update(project);
await this.eventStore.store({
type: PROJECT_UPDATED,
project: project.id,
createdBy: getCreatedBy(user),
data: project,
project: project.id,
preData,
});
}
@ -258,7 +259,6 @@ export default class ProjectService {
type: PROJECT_DELETED,
createdBy: getCreatedBy(user),
project: id,
data: { id },
});
await this.accessService.removeDefaultProjectRoles(user, id);
@ -289,6 +289,7 @@ export default class ProjectService {
};
}
// TODO: should be an event too
async addUser(
projectId: string,
roleId: number,
@ -313,6 +314,7 @@ export default class ProjectService {
await this.accessService.addUserToRole(userId, role.id);
}
// TODO: should be an event too
async removeUser(
projectId: string,
roleId: number,

View File

@ -206,26 +206,29 @@ class UserService {
await this.store.setPasswordHash(user.id, passwordHash);
}
await this.updateChangeLog(USER_CREATED, user, updatedBy);
await this.eventStore.store({
type: USER_CREATED,
createdBy: this.getCreatedBy(updatedBy),
data: this.mapUserToData(user),
});
return user;
}
private async updateChangeLog(
type: string,
user: IUser,
updatedBy: User = systemUser,
): Promise<void> {
await this.eventStore.store({
type,
createdBy: updatedBy.username || updatedBy.email,
data: {
id: user.id,
name: user.name,
username: user.username,
email: user.email,
},
});
private getCreatedBy(updatedBy: User = systemUser) {
return updatedBy.username || updatedBy.email;
}
private mapUserToData(user?: IUser): any {
if (!user) {
return undefined;
}
return {
id: user.id,
name: user.name,
username: user.username,
email: user.email,
};
}
async updateUser(
@ -236,17 +239,43 @@ class UserService {
Joi.assert(email, Joi.string().email(), 'Email');
}
const preUser = await this.store.get(id);
if (rootRole) {
await this.accessService.setUserRootRole(id, rootRole);
}
const user = await this.store.update(id, { name, email });
await this.updateChangeLog(USER_UPDATED, user, updatedBy);
await this.eventStore.store({
type: USER_UPDATED,
createdBy: this.getCreatedBy(updatedBy),
data: this.mapUserToData(user),
preData: this.mapUserToData(preUser),
});
return user;
}
async deleteUser(userId: number, updatedBy?: User): Promise<void> {
const user = await this.store.get(userId);
const roles = await this.accessService.getRolesForUser(userId);
await Promise.all(
roles.map((role) =>
this.accessService.removeUserFromRole(userId, role.id),
),
);
await this.sessionService.deleteSessionsForUser(userId);
await this.store.delete(userId);
await this.eventStore.store({
type: USER_DELETED,
createdBy: this.getCreatedBy(updatedBy),
preData: this.mapUserToData(user),
});
}
async loginUser(usernameOrEmail: string, password: string): Promise<IUser> {
const settings = await this.settingService.get<SimpleAuthSettings>(
simpleAuthKey,
@ -323,21 +352,6 @@ class UserService {
return this.store.setPasswordHash(userId, passwordHash);
}
async deleteUser(userId: number, updatedBy?: User): Promise<void> {
const user = await this.store.get(userId);
const roles = await this.accessService.getRolesForUser(userId);
await Promise.all(
roles.map((role) =>
this.accessService.removeUserFromRole(userId, role.id),
),
);
await this.sessionService.deleteSessionsForUser(userId);
await this.store.delete(userId);
await this.updateChangeLog(USER_DELETED, user, updatedBy);
}
async getUserForToken(token: string): Promise<ITokenUser> {
const { createdBy, userId } = await this.resetTokenService.isValid(
token,

View File

@ -1,4 +1,8 @@
import { FeatureToggle, IStrategyConfig, ITag } from './model';
export const APPLICATION_CREATED = 'application-created';
// feature event types
export const FEATURE_CREATED = 'feature-created';
export const FEATURE_DELETED = 'feature-deleted';
export const FEATURE_UPDATED = 'feature-updated';
@ -17,6 +21,9 @@ export const FEATURE_UNTAGGED = 'feature-untagged';
export const FEATURE_STALE_ON = 'feature-stale-on';
export const FEATURE_STALE_OFF = 'feature-stale-off';
export const DROP_FEATURES = 'drop-features';
export const FEATURE_ENVIRONMENT_ENABLED = 'feature-environment-enabled';
export const FEATURE_ENVIRONMENT_DISABLED = 'feature-environment-disabled';
export const STRATEGY_CREATED = 'strategy-created';
export const STRATEGY_DELETED = 'strategy-deleted';
export const STRATEGY_DEPRECATED = 'strategy-deprecated';
@ -50,5 +57,296 @@ export const USER_UPDATED = 'user-updated';
export const USER_DELETED = 'user-deleted';
export const DROP_ENVIRONMENTS = 'drop-environments';
export const ENVIRONMENT_IMPORT = 'environment-import';
export const FEATURE_ENVIRONMENT_ENABLED = 'feature-environment-enabled';
export const FEATURE_ENVIRONMENT_DISABLED = 'feature-environment-disabled';
export interface IBaseEvent {
type: string;
createdBy: string;
project?: string;
environment?: string;
featureName?: string;
data?: any;
preData?: any;
tags?: ITag[];
}
export interface IEvent extends IBaseEvent {
id: number;
createdAt: Date;
}
class BaseEvent implements IBaseEvent {
readonly type: string;
readonly createdBy: string;
readonly tags: ITag[];
constructor(type: string, createdBy: string, tags: ITag[] = []) {
this.type = type;
this.createdBy = createdBy;
this.tags = tags;
}
}
export class FeatureStaleEvent extends BaseEvent {
readonly project: string;
readonly featureName: string;
constructor(p: {
stale: boolean;
project: string;
featureName: string;
createdBy: string;
tags: ITag[];
}) {
super(
p.stale ? FEATURE_STALE_ON : FEATURE_STALE_OFF,
p.createdBy,
p.tags,
);
this.project = p.project;
this.featureName = p.featureName;
}
}
export class FeatureEnvironmentEvent extends BaseEvent {
readonly project: string;
readonly featureName: string;
readonly environment: string;
constructor(p: {
enabled: boolean;
project: string;
featureName: string;
environment: string;
createdBy: string;
tags: ITag[];
}) {
super(
p.enabled
? FEATURE_ENVIRONMENT_ENABLED
: FEATURE_ENVIRONMENT_DISABLED,
p.createdBy,
p.tags,
);
this.project = p.project;
this.featureName = p.featureName;
this.environment = p.environment;
}
}
export class FeatureChangeProjectEvent extends BaseEvent {
readonly project: string;
readonly featureName: string;
readonly data: {
oldProject: string;
newProject: string;
};
constructor(p: {
oldProject: string;
newProject: string;
featureName: string;
createdBy: string;
tags: ITag[];
}) {
super(FEATURE_PROJECT_CHANGE, p.createdBy, p.tags);
const { newProject, oldProject, featureName } = p;
this.project = newProject;
this.featureName = featureName;
this.data = { newProject, oldProject };
}
}
export class FeatureCreatedEvent extends BaseEvent {
readonly project: string;
readonly featureName: string;
readonly data: FeatureToggle;
constructor(p: {
project: string;
featureName: string;
createdBy: string;
data: FeatureToggle;
tags: ITag[];
}) {
super(FEATURE_CREATED, p.createdBy, p.tags);
const { project, featureName, data } = p;
this.project = project;
this.featureName = featureName;
this.data = data;
}
}
export class FeatureArchivedEvent extends BaseEvent {
readonly project: string;
readonly featureName: string;
constructor(p: {
project: string;
featureName: string;
createdBy: string;
tags: ITag[];
}) {
super(FEATURE_ARCHIVED, p.createdBy, p.tags);
const { project, featureName } = p;
this.project = project;
this.featureName = featureName;
}
}
export class FeatureRevivedEvent extends BaseEvent {
readonly project: string;
readonly featureName: string;
constructor(p: {
project: string;
featureName: string;
createdBy: string;
tags: ITag[];
}) {
super(FEATURE_REVIVED, p.createdBy, p.tags);
const { project, featureName } = p;
this.project = project;
this.featureName = featureName;
}
}
export class FeatureDeletedEvent extends BaseEvent {
readonly project: string;
readonly featureName: string;
readonly preData: FeatureToggle;
constructor(p: {
project: string;
featureName: string;
preData: FeatureToggle;
createdBy: string;
tags: ITag[];
}) {
super(FEATURE_DELETED, p.createdBy, p.tags);
const { project, featureName, preData } = p;
this.project = project;
this.featureName = featureName;
this.preData = preData;
}
}
export class FeatureMetadataUpdateEvent extends BaseEvent {
readonly project: string;
readonly featureName: string;
readonly data: FeatureToggle;
readonly preData: FeatureToggle;
constructor(p: {
featureName: string;
createdBy: string;
project: string;
data: FeatureToggle;
preData: FeatureToggle;
tags: ITag[];
}) {
super(FEATURE_METADATA_UPDATED, p.createdBy, p.tags);
const { project, featureName, data, preData } = p;
this.project = project;
this.featureName = featureName;
this.data = data;
this.preData = preData;
}
}
export class FeatureStrategyAddEvent extends BaseEvent {
readonly project: string;
readonly featureName: string;
readonly environment: string;
readonly data: IStrategyConfig;
constructor(p: {
project: string;
featureName: string;
environment: string;
createdBy: string;
data: IStrategyConfig;
tags: ITag[];
}) {
super(FEATURE_STRATEGY_ADD, p.createdBy, p.tags);
const { project, featureName, environment, data } = p;
this.project = project;
this.featureName = featureName;
this.environment = environment;
this.data = data;
}
}
export class FeatureStrategyUpdateEvent extends BaseEvent {
readonly project: string;
readonly featureName: string;
readonly environment: string;
readonly data: IStrategyConfig;
readonly preData: IStrategyConfig;
constructor(p: {
project: string;
featureName: string;
environment: string;
createdBy: string;
data: IStrategyConfig;
preData: IStrategyConfig;
tags: ITag[];
}) {
super(FEATURE_STRATEGY_UPDATE, p.createdBy, p.tags);
const { project, featureName, environment, data, preData } = p;
this.project = project;
this.featureName = featureName;
this.environment = environment;
this.data = data;
this.preData = preData;
}
}
export class FeatureStrategyRemoveEvent extends BaseEvent {
readonly project: string;
readonly featureName: string;
readonly environment: string;
readonly preData: IStrategyConfig;
constructor(p: {
project: string;
featureName: string;
environment: string;
createdBy: string;
preData: IStrategyConfig;
tags: ITag[];
}) {
super(FEATURE_STRATEGY_REMOVE, p.createdBy, p.tags);
const { project, featureName, environment, preData } = p;
this.project = project;
this.featureName = featureName;
this.environment = environment;
this.preData = preData;
}
}

View File

@ -196,20 +196,6 @@ export interface IAddonConfig {
unleashUrl: string;
}
export interface ICreateEvent {
type: string;
createdBy: string;
project?: string;
environment?: string;
data?: any;
tags?: ITag[];
}
export interface IEvent extends ICreateEvent {
id: number;
createdAt: Date;
}
export interface IUserWithRole {
id: number;
roleId: number;

View File

@ -1,11 +1,12 @@
import EventEmitter from 'events';
import { ICreateEvent, IEvent } from '../model';
import { IBaseEvent, IEvent } from '../events';
import { Store } from './store';
export interface IEventStore extends Store<IEvent, number>, EventEmitter {
store(event: ICreateEvent): Promise<void>;
batchStore(events: ICreateEvent[]): Promise<void>;
store(event: IBaseEvent): Promise<void>;
batchStore(events: IBaseEvent[]): Promise<void>;
getEvents(): Promise<IEvent[]>;
getEventsFilterByType(name: string): Promise<IEvent[]>;
getEventsForFeature(featureName: string): Promise<IEvent[]>;
getEventsFilterByProject(project: string): Promise<IEvent[]>;
}

View File

@ -0,0 +1,23 @@
'use strict';
exports.up = function (db, cb) {
db.runSql(
`
ALTER TABLE events
ADD COLUMN feature_name TEXT;
CREATE INDEX feature_name_idx ON events(feature_name);
`,
cb,
);
};
exports.down = function (db, cb) {
db.runSql(
`
DROP INDEX feature_name_idx;
ALTER TABLE events
DROP COLUMN feature_name;
`,
cb,
);
};

View File

@ -0,0 +1,9 @@
'use strict';
exports.up = function (db, cb) {
db.runSql(`ALTER TABLE events ADD COLUMN pre_data jsonb;`, cb);
};
exports.down = function (db, cb) {
db.runSql(`ALTER TABLE events DROP COLUMN pre_data;`, cb);
};

View File

@ -556,9 +556,8 @@ test('Patching feature toggles to stale should trigger FEATURE_STALE_ON event',
const events = await db.stores.eventStore.getAll({
type: FEATURE_STALE_ON,
});
const updateForOurToggle = events.find((e) => e.data.name === name);
const updateForOurToggle = events.find((e) => e.featureName === name);
expect(updateForOurToggle).toBeTruthy();
expect(updateForOurToggle.data.stale).toBe(true);
});
test('Patching feature toggles to active (turning stale to false) should trigger FEATURE_STALE_OFF event', async () => {
@ -581,9 +580,8 @@ test('Patching feature toggles to active (turning stale to false) should trigger
const events = await db.stores.eventStore.getAll({
type: FEATURE_STALE_OFF,
});
const updateForOurToggle = events.find((e) => e.data.name === name);
const updateForOurToggle = events.find((e) => e.featureName === name);
expect(updateForOurToggle).toBeTruthy();
expect(updateForOurToggle.data.stale).toBe(false);
});
test('Should archive feature toggle', async () => {
@ -1149,9 +1147,9 @@ test('Deleting a strategy should include name of feature strategy was deleted fr
type: FEATURE_STRATEGY_REMOVE,
});
expect(events).toHaveLength(1);
expect(events[0].data.name).toBe(featureName);
expect(events[0].featureName).toBe(featureName);
expect(events[0].environment).toBe(environment);
expect(events[0].data.id).toBe(strategyId);
expect(events[0].preData.id).toBe(strategyId);
});
test('Enabling environment creates a FEATURE_ENVIRONMENT_ENABLED event', async () => {
@ -1193,7 +1191,7 @@ test('Enabling environment creates a FEATURE_ENVIRONMENT_ENABLED event', async (
const events = await db.stores.eventStore.getAll({
type: FEATURE_ENVIRONMENT_ENABLED,
});
const enabledEvents = events.filter((e) => e.data.name === featureName);
const enabledEvents = events.filter((e) => e.featureName === featureName);
expect(enabledEvents).toHaveLength(1);
});
test('Disabling environment creates a FEATURE_ENVIRONMENT_DISABLED event', async () => {
@ -1243,7 +1241,7 @@ test('Disabling environment creates a FEATURE_ENVIRONMENT_DISABLED event', async
const events = await db.stores.eventStore.getAll({
type: FEATURE_ENVIRONMENT_DISABLED,
});
const ourFeatureEvent = events.find((e) => e.data.name === featureName);
const ourFeatureEvent = events.find((e) => e.featureName === featureName);
expect(ourFeatureEvent).toBeTruthy();
});

View File

@ -44,8 +44,6 @@ afterEach(async () => {
});
test('returns empty list of users', async () => {
expect.assertions(1);
return app.request
.get('/api/admin/user-admin')
.expect('Content-Type', /json/)
@ -56,8 +54,6 @@ test('returns empty list of users', async () => {
});
test('creates and returns all users', async () => {
expect.assertions(2);
const createUserRequests = [...Array(20).keys()].map((i) =>
app.request
.post('/api/admin/user-admin')
@ -82,8 +78,6 @@ test('creates and returns all users', async () => {
});
test('creates editor-user without password', async () => {
expect.assertions(3);
return app.request
.post('/api/admin/user-admin')
.send({
@ -101,8 +95,6 @@ test('creates editor-user without password', async () => {
});
test('creates admin-user with password', async () => {
expect.assertions(6);
const { body } = await app.request
.post('/api/admin/user-admin')
.send({
@ -129,8 +121,6 @@ test('creates admin-user with password', async () => {
});
test('requires known root role', async () => {
expect.assertions(0);
return app.request
.post('/api/admin/user-admin')
.send({
@ -186,16 +176,12 @@ test('get a single user', async () => {
});
test('should delete user', async () => {
expect.assertions(0);
const user = await userStore.insert({ email: 'some@mail.com' });
return app.request.delete(`/api/admin/user-admin/${user.id}`).expect(200);
});
test('validator should require strong password', async () => {
expect.assertions(0);
return app.request
.post('/api/admin/user-admin/validate-password')
.send({ password: 'simple' })
@ -203,8 +189,6 @@ test('validator should require strong password', async () => {
});
test('validator should accept strong password', async () => {
expect.assertions(0);
return app.request
.post('/api/admin/user-admin/validate-password')
.send({ password: 'simple123-_ASsad' })
@ -212,8 +196,6 @@ test('validator should accept strong password', async () => {
});
test('should change password', async () => {
expect.assertions(0);
const user = await userStore.insert({ email: 'some@mail.com' });
return app.request
@ -223,8 +205,6 @@ test('should change password', async () => {
});
test('should search for users', async () => {
expect.assertions(2);
await userStore.insert({ email: 'some@mail.com' });
await userStore.insert({ email: 'another@mail.com' });
await userStore.insert({ email: 'another2@mail.com' });
@ -241,8 +221,6 @@ test('should search for users', async () => {
});
test('Creates a user and includes inviteLink and emailConfigured', async () => {
expect.assertions(5);
return app.request
.post('/api/admin/user-admin')
.send({
@ -300,7 +278,6 @@ test('Creates a user but does not send email if sendEmail is set to false', asyn
});
test('generates USER_CREATED event', async () => {
expect.assertions(5);
const email = 'some@getunelash.ai';
const name = 'Some Name';
@ -325,20 +302,16 @@ test('generates USER_CREATED event', async () => {
});
test('generates USER_DELETED event', async () => {
expect.assertions(3);
const user = await userStore.insert({ email: 'some@mail.com' });
await app.request.delete(`/api/admin/user-admin/${user.id}`);
const events = await eventStore.getEvents();
expect(events[0].type).toBe(USER_DELETED);
expect(events[0].data.id).toBe(user.id);
expect(events[0].data.email).toBe(user.email);
expect(events[0].preData.id).toBe(user.id);
expect(events[0].preData.email).toBe(user.email);
});
test('generates USER_UPDATED event', async () => {
expect.assertions(3);
const { body } = await app.request
.post('/api/admin/user-admin')
.send({

View File

@ -1,4 +1,4 @@
import FeatureToggleServiceV2 from '../../../lib/services/feature-toggle-service';
import FeatureToggleService from '../../../lib/services/feature-toggle-service';
import { IStrategyConfig } from '../../../lib/types/model';
import { createTestConfig } from '../../config/test-config';
import dbInit from '../helpers/database-init';
@ -6,7 +6,7 @@ import { DEFAULT_ENV } from '../../../lib/util/constants';
let stores;
let db;
let service: FeatureToggleServiceV2;
let service: FeatureToggleService;
beforeAll(async () => {
const config = createTestConfig();
@ -15,7 +15,7 @@ beforeAll(async () => {
config.getLogger,
);
stores = db.stores;
service = new FeatureToggleServiceV2(stores, config);
service = new FeatureToggleService(stores, config);
});
afterAll(async () => {

View File

@ -1,6 +1,6 @@
import dbInit, { ITestDb } from '../helpers/database-init';
import getLogger from '../../fixtures/no-logger';
import FeatureToggleServiceV2 from '../../../lib/services/feature-toggle-service';
import FeatureToggleService from '../../../lib/services/feature-toggle-service';
import { AccessService } from '../../../lib/services/access-service';
import ProjectService from '../../../lib/services/project-service';
import ProjectHealthService from '../../../lib/services/project-health-service';
@ -25,7 +25,7 @@ beforeAll(async () => {
email: 'test@getunleash.io',
});
accessService = new AccessService(stores, config);
featureToggleService = new FeatureToggleServiceV2(stores, config);
featureToggleService = new FeatureToggleService(stores, config);
projectService = new ProjectService(
stores,
config,

View File

@ -1,6 +1,6 @@
import dbInit, { ITestDb } from '../helpers/database-init';
import getLogger from '../../fixtures/no-logger';
import FeatureToggleServiceV2 from '../../../lib/services/feature-toggle-service';
import FeatureToggleService from '../../../lib/services/feature-toggle-service';
import ProjectService from '../../../lib/services/project-service';
import { AccessService } from '../../../lib/services/access-service';
import {
@ -17,7 +17,7 @@ let db: ITestDb;
let projectService;
let accessService;
let featureToggleService: FeatureToggleServiceV2;
let featureToggleService: FeatureToggleService;
let user;
beforeAll(async () => {
@ -33,7 +33,7 @@ beforeAll(async () => {
experimental: { environments: { enabled: true } },
});
accessService = new AccessService(stores, config);
featureToggleService = new FeatureToggleServiceV2(stores, config);
featureToggleService = new FeatureToggleService(stores, config);
projectService = new ProjectService(
stores,
config,

View File

@ -1,11 +1,11 @@
import {
APPLICATION_CREATED,
FEATURE_CREATED,
IEvent,
} from '../../../lib/types/events';
import dbInit from '../helpers/database-init';
import getLogger from '../../fixtures/no-logger';
import { IEvent } from '../../../lib/types/model';
import { IEventStore } from '../../../lib/types/stores/event-store';
import { IUnleashStores } from '../../../lib/types';

View File

@ -1,6 +1,6 @@
import EventEmitter from 'events';
import { IEventStore } from '../../lib/types/stores/event-store';
import { IEvent } from '../../lib/types/model';
import { IEvent } from '../../lib/types/events';
class FakeEventStore extends EventEmitter implements IEventStore {
events: IEvent[];
@ -11,6 +11,10 @@ class FakeEventStore extends EventEmitter implements IEventStore {
this.events = [];
}
async getEventsForFeature(featureName: string): Promise<IEvent[]> {
return this.events.filter((e) => e.featureName === featureName);
}
store(event: IEvent): Promise<void> {
this.events.push(event);
this.emit(event.type, event);

View File

@ -13,14 +13,23 @@ The Datadog addon will perform a single retry if the HTTP POST against the Datad
#### Events {#events}
You can choose to trigger updates for the following events (we might add more event types in the future):
You can choose to trigger updates for the following events:
- feature-created
- feature-updated
- feature-updated (*)
- feature-metadata-updated
- feature-project-change
- feature-archived
- feature-revived
- feature-strategy-update
- feature-strategy-add
- feature-strategy-remove
- feature-stale-on
- feature-stale-off
- feature-environment-enabled
- feature-environment-disabled
> *) Deprecated, and will not be used after transition to environments in Unleash v4.3
#### Parameters {#parameters}

View File

@ -13,14 +13,23 @@ The Slack addon will perform a single retry if the HTTP POST against the Slack W
#### Events {#events}
You can choose to trigger updates for the following events (we might add more event types in the future):
You can choose to trigger updates for the following events:
- feature-created
- feature-updated
- feature-updated (*)
- feature-metadata-updated
- feature-project-change
- feature-archived
- feature-revived
- feature-strategy-update
- feature-strategy-add
- feature-strategy-remove
- feature-stale-on
- feature-stale-off
- feature-environment-enabled
- feature-environment-disabled
> *) Deprecated, and will not be used after transition to environments in Unleash v4.3
#### Parameters {#parameters}

View File

@ -13,14 +13,23 @@ The Microsoft Teams addon will perform a single retry if the HTTP POST against t
#### Events {#events}
You can choose to trigger updates for the following events (we might add more event types in the future):
You can choose to trigger updates for the following events:
- feature-created
- feature-updated
- feature-updated (*)
- feature-metadata-updated
- feature-project-change
- feature-archived
- feature-revived
- feature-strategy-update
- feature-strategy-add
- feature-strategy-remove
- feature-stale-on
- feature-stale-off
- feature-environment-enabled
- feature-environment-disabled
> *) Deprecated, and will not be used after transition to environments in Unleash v4.3
#### Parameters {#parameters}

View File

@ -16,13 +16,20 @@ The webhook will perform a single retry if the HTTP POST call fails (either a 50
You can choose to trigger updates for the following events (we might add more event types in the future):
- feature-created
- feature-updated
- feature-updated (*)
- feature-metadata-updated
- feature-project-change
- feature-archived
- feature-revived
- feature-strategy-update
- feature-strategy-add
- feature-strategy-remove
- feature-stale-on
- feature-stale-off
- feature-environment-enabled
- feature-environment-disabled
(we will add more events in the future!)
> *) Deprecated, and will not be used after transition to environments in Unleash v4.3
#### Parameters {#parameters}

View File

@ -13,39 +13,208 @@ Used to fetch all changes in the unleash system.
Defined event types:
### Feature Toggle events:
- feature-created
- feature-deleted
- feature-updated
- feature-metadata-updated
- feature-project-change
- feature-archived
- feature-revived
- feature-import
- feature-tagged
- feature-tag-import
- feature-strategy-update
- feature-strategy-add
- feature-strategy-remove
- drop-feature-tags
- feature-untagged
- feature-stale-on
- feature-stale-off
- drop-features
- feature-environment-enabled
- feature-environment-disabled
### Strategy Events
- strategy-created
- strategy-deleted
- strategy-deprecated
- strategy-reactivated
- strategy-updated
- strategy-import
- drop-strategies
### Context field events
- context-field-created
- context-field-updated
- context-field-deleted
### Project events
- project-created
- project-updated
- project-deleted
- project-import
- drop-projects
### Tag events
- tag-created
- tag-deleted
- tag-import
- drop-tags
### Tag type events
- tag-type-created
- tag-type-updated
- tag-type-deleted
- application-created
- tag-type-updated
- tag-type-import
- drop-tag-types
### Addon events
- addon-config-created
- addon-config-updated
- addon-config-deleted
### User events
- user-created
- user-updated
- user-deleted
### Environment events (Enterprise)
- drop-environments
- environment-import
**Response**
```json
{
"version": 1,
"events": [
{
"id": 454,
"type": "feature-updated",
"createdBy": "unknown",
"createdAt": "2016-08-24T11:22:01.354Z",
"data": {
"name": "eid.bankid.mobile",
"description": "",
"strategy": "default",
"enabled": true,
"parameters": {}
},
"diffs": [{ "kind": "E", "path": ["enabled"], "lhs": false, "rhs": true }]
}
]
"version": 2,
"events": [{
"id": 187,
"type": "feature-metadata-updated",
"createdBy": "admin",
"createdAt": "2021-11-11T09:42:14.271Z",
"data": {
"name": "HelloEvents!",
"description": "Hello Events update!",
"type": "release",
"project": "default",
"stale": false,
"variants": [],
"createdAt": "2021-11-11T09:40:51.077Z",
"lastSeenAt": null
},
"preData": {
"name": "HelloEvents!",
"description": "Hello Events!",
"type": "release",
"project": "default",
"stale": false,
"variants": [],
"createdAt": "2021-11-11T09:40:51.077Z",
"lastSeenAt": null
},
"tags": [{
"value": "team-x",
"type": "simple"
}],
"featureName": "HelloEvents!",
"project": "default",
"environment": null
}, {
"id": 186,
"type": "feature-tagged",
"createdBy": "admin",
"createdAt": "2021-11-11T09:41:20.464Z",
"data": {
"type": "simple",
"value": "team-x"
},
"preData": null,
"tags": [],
"featureName": "HelloEvents!",
"project": null,
"environment": null
}, {
"id": 184,
"type": "feature-environment-enabled",
"createdBy": "admin",
"createdAt": "2021-11-11T09:41:03.782Z",
"data": null,
"preData": null,
"tags": [],
"featureName": "HelloEvents!",
"project": "default",
"environment": "default"
}, {
"id": 183,
"type": "feature-strategy-add",
"createdBy": "admin",
"createdAt": "2021-11-11T09:41:00.740Z",
"data": {
"id": "88e1df00-1951-452f-a063-6f5e18476f87",
"name": "flexibleRollout",
"constraints": [],
"parameters": {
"groupId": "HelloEvents!",
"rollout": 51,
"stickiness": "default"
}
},
"preData": null,
"tags": [],
"featureName": "HelloEvents!",
"project": "default",
"environment": "default"
}, {
"id": 182,
"type": "feature-created",
"createdBy": "admin",
"createdAt": "2021-11-11T09:40:51.083Z",
"data": {
"name": "HelloEvents!",
"description": "Hello Events!",
"type": "release",
"project": "default",
"stale": false,
"variants": [],
"createdAt": "2021-11-11T09:40:51.077Z",
"lastSeenAt": null
},
"preData": null,
"tags": [],
"featureName": "HelloEvents!",
"project": "default",
"environment": null
}]
}
```
All events will implement the following interface:
```js
interface IEvent {
id: number;
createdAt: Date;
type: string;
createdBy: string;
project?: string;
environment?: string;
featureName?: string;
data?: any;
preData?: any;
tags?: ITag[];
}
```

View File

@ -2113,11 +2113,6 @@ dedent@^0.7.0:
resolved "https://registry.npmjs.org/dedent/-/dedent-0.7.0.tgz"
integrity sha1-JJXduvbrh0q7Dhvp3yLS5aVEMmw=
deep-diff@^1.0.2:
version "1.0.2"
resolved "https://registry.npmjs.org/deep-diff/-/deep-diff-1.0.2.tgz"
integrity sha512-aWS3UIVH+NPGCD1kki+DCU9Dua032iSsO43LqQpcs4R3+dVv7tX0qBGjiVHJHjplsoUM2XRO/KB92glqc68awg==
deep-extend@^0.6.0:
version "0.6.0"
resolved "https://registry.npmjs.org/deep-extend/-/deep-extend-0.6.0.tgz"