1
0
mirror of https://github.com/Unleash/unleash.git synced 2025-06-23 01:16:27 +02:00

Merge branch 'master' into feat/splash

This commit is contained in:
Fredrik Strand Oseberg 2021-11-12 13:19:36 +01:00 committed by GitHub
commit c369b77b24
No known key found for this signature in database
GPG Key ID: 4AEE18F83AFDEB23
49 changed files with 1037 additions and 805 deletions

View File

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

View File

@ -85,7 +85,6 @@
"db-migrate": "0.11.12", "db-migrate": "0.11.12",
"db-migrate-pg": "1.2.2", "db-migrate-pg": "1.2.2",
"db-migrate-shared": "1.2.0", "db-migrate-shared": "1.2.0",
"deep-diff": "^1.0.2",
"deepmerge": "^4.2.2", "deepmerge": "^4.2.2",
"errorhandler": "^1.5.1", "errorhandler": "^1.5.1",
"express": "^4.17.1", "express": "^4.17.1",
@ -112,7 +111,7 @@
"response-time": "^2.3.2", "response-time": "^2.3.2",
"serve-favicon": "^2.5.0", "serve-favicon": "^2.5.0",
"stoppable": "^1.1.0", "stoppable": "^1.1.0",
"unleash-frontend": "4.2.12", "unleash-frontend": "4.2.13",
"uuid": "^8.3.2" "uuid": "^8.3.2"
}, },
"devDependencies": { "devDependencies": {

View File

@ -2,7 +2,8 @@ import fetch, { Response } from 'node-fetch';
import { addonDefinitionSchema } from './addon-schema'; import { addonDefinitionSchema } from './addon-schema';
import { IUnleashConfig } from '../types/option'; import { IUnleashConfig } from '../types/option';
import { Logger } from '../logger'; 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 { export default abstract class Addon {
logger: Logger; logger: Logger;

View File

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

View File

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

View File

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

View File

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

View File

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

View File

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

View File

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

View File

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

View File

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

View File

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

View File

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

View File

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

View File

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

View File

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

View File

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

View File

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

View File

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

View File

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

View File

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

View File

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

View File

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

View File

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

View File

@ -11,7 +11,7 @@ import {
import { IFeatureToggleStore } from '../types/stores/feature-toggle-store'; import { IFeatureToggleStore } from '../types/stores/feature-toggle-store';
import { IFeatureTypeStore } from '../types/stores/feature-type-store'; import { IFeatureTypeStore } from '../types/stores/feature-type-store';
import { IProjectStore } from '../types/stores/project-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 { hoursToMilliseconds } from 'date-fns';
import Timer = NodeJS.Timer; import Timer = NodeJS.Timer;
@ -28,7 +28,7 @@ export default class ProjectHealthService {
private healthRatingTimer: Timer; private healthRatingTimer: Timer;
private featureToggleService: FeatureToggleServiceV2; private featureToggleService: FeatureToggleService;
constructor( constructor(
{ {
@ -40,7 +40,7 @@ export default class ProjectHealthService {
'projectStore' | 'featureTypeStore' | 'featureToggleStore' 'projectStore' | 'featureTypeStore' | 'featureToggleStore'
>, >,
{ getLogger }: Pick<IUnleashConfig, 'getLogger'>, { getLogger }: Pick<IUnleashConfig, 'getLogger'>,
featureToggleService: FeatureToggleServiceV2, featureToggleService: FeatureToggleService,
) { ) {
this.logger = getLogger('services/project-health-service.ts'); this.logger = getLogger('services/project-health-service.ts');
this.projectStore = projectStore; 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 { IProjectQuery, IProjectStore } from '../types/stores/project-store';
import { IRole } from '../types/stores/access-store'; import { IRole } from '../types/stores/access-store';
import { IEventStore } from '../types/stores/event-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 { CREATE_FEATURE, UPDATE_FEATURE } from '../types/permissions';
import NoAccessError from '../error/no-access-error'; import NoAccessError from '../error/no-access-error';
import IncompatibleProjectError from '../error/incompatible-project-error'; import IncompatibleProjectError from '../error/incompatible-project-error';
@ -58,7 +58,7 @@ export default class ProjectService {
private logger: any; private logger: any;
private featureToggleService: FeatureToggleServiceV2; private featureToggleService: FeatureToggleService;
private environmentsEnabled: boolean = false; private environmentsEnabled: boolean = false;
@ -81,7 +81,7 @@ export default class ProjectService {
>, >,
config: IUnleashConfig, config: IUnleashConfig,
accessService: AccessService, accessService: AccessService,
featureToggleService: FeatureToggleServiceV2, featureToggleService: FeatureToggleService,
) { ) {
this.store = projectStore; this.store = projectStore;
this.environmentStore = environmentStore; this.environmentStore = environmentStore;
@ -161,16 +161,17 @@ export default class ProjectService {
} }
async updateProject(updatedProject: IProject, user: User): Promise<void> { 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); const project = await projectSchema.validateAsync(updatedProject);
await this.store.update(project); await this.store.update(project);
await this.eventStore.store({ await this.eventStore.store({
type: PROJECT_UPDATED, type: PROJECT_UPDATED,
project: project.id,
createdBy: getCreatedBy(user), createdBy: getCreatedBy(user),
data: project, data: project,
project: project.id, preData,
}); });
} }
@ -258,7 +259,6 @@ export default class ProjectService {
type: PROJECT_DELETED, type: PROJECT_DELETED,
createdBy: getCreatedBy(user), createdBy: getCreatedBy(user),
project: id, project: id,
data: { id },
}); });
await this.accessService.removeDefaultProjectRoles(user, id); await this.accessService.removeDefaultProjectRoles(user, id);
@ -289,6 +289,7 @@ export default class ProjectService {
}; };
} }
// TODO: should be an event too
async addUser( async addUser(
projectId: string, projectId: string,
roleId: number, roleId: number,
@ -313,6 +314,7 @@ export default class ProjectService {
await this.accessService.addUserToRole(userId, role.id); await this.accessService.addUserToRole(userId, role.id);
} }
// TODO: should be an event too
async removeUser( async removeUser(
projectId: string, projectId: string,
roleId: number, roleId: number,

View File

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

View File

@ -1,4 +1,8 @@
import { FeatureToggle, IStrategyConfig, ITag } from './model';
export const APPLICATION_CREATED = 'application-created'; export const APPLICATION_CREATED = 'application-created';
// feature event types
export const FEATURE_CREATED = 'feature-created'; export const FEATURE_CREATED = 'feature-created';
export const FEATURE_DELETED = 'feature-deleted'; export const FEATURE_DELETED = 'feature-deleted';
export const FEATURE_UPDATED = 'feature-updated'; export const FEATURE_UPDATED = 'feature-updated';
@ -17,6 +21,9 @@ export const FEATURE_UNTAGGED = 'feature-untagged';
export const FEATURE_STALE_ON = 'feature-stale-on'; export const FEATURE_STALE_ON = 'feature-stale-on';
export const FEATURE_STALE_OFF = 'feature-stale-off'; export const FEATURE_STALE_OFF = 'feature-stale-off';
export const DROP_FEATURES = 'drop-features'; 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_CREATED = 'strategy-created';
export const STRATEGY_DELETED = 'strategy-deleted'; export const STRATEGY_DELETED = 'strategy-deleted';
export const STRATEGY_DEPRECATED = 'strategy-deprecated'; export const STRATEGY_DEPRECATED = 'strategy-deprecated';
@ -50,5 +57,296 @@ export const USER_UPDATED = 'user-updated';
export const USER_DELETED = 'user-deleted'; export const USER_DELETED = 'user-deleted';
export const DROP_ENVIRONMENTS = 'drop-environments'; export const DROP_ENVIRONMENTS = 'drop-environments';
export const ENVIRONMENT_IMPORT = 'environment-import'; 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; 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 { export interface IUserWithRole {
id: number; id: number;
roleId: number; roleId: number;

View File

@ -1,11 +1,12 @@
import EventEmitter from 'events'; import EventEmitter from 'events';
import { ICreateEvent, IEvent } from '../model'; import { IBaseEvent, IEvent } from '../events';
import { Store } from './store'; import { Store } from './store';
export interface IEventStore extends Store<IEvent, number>, EventEmitter { export interface IEventStore extends Store<IEvent, number>, EventEmitter {
store(event: ICreateEvent): Promise<void>; store(event: IBaseEvent): Promise<void>;
batchStore(events: ICreateEvent[]): Promise<void>; batchStore(events: IBaseEvent[]): Promise<void>;
getEvents(): Promise<IEvent[]>; getEvents(): Promise<IEvent[]>;
getEventsFilterByType(name: string): Promise<IEvent[]>; getEventsFilterByType(name: string): Promise<IEvent[]>;
getEventsForFeature(featureName: string): Promise<IEvent[]>;
getEventsFilterByProject(project: 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({ const events = await db.stores.eventStore.getAll({
type: FEATURE_STALE_ON, 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).toBeTruthy();
expect(updateForOurToggle.data.stale).toBe(true);
}); });
test('Patching feature toggles to active (turning stale to false) should trigger FEATURE_STALE_OFF event', async () => { 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({ const events = await db.stores.eventStore.getAll({
type: FEATURE_STALE_OFF, 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).toBeTruthy();
expect(updateForOurToggle.data.stale).toBe(false);
}); });
test('Should archive feature toggle', async () => { 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, type: FEATURE_STRATEGY_REMOVE,
}); });
expect(events).toHaveLength(1); 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].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 () => { 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({ const events = await db.stores.eventStore.getAll({
type: FEATURE_ENVIRONMENT_ENABLED, 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); expect(enabledEvents).toHaveLength(1);
}); });
test('Disabling environment creates a FEATURE_ENVIRONMENT_DISABLED event', async () => { 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({ const events = await db.stores.eventStore.getAll({
type: FEATURE_ENVIRONMENT_DISABLED, type: FEATURE_ENVIRONMENT_DISABLED,
}); });
const ourFeatureEvent = events.find((e) => e.data.name === featureName); const ourFeatureEvent = events.find((e) => e.featureName === featureName);
expect(ourFeatureEvent).toBeTruthy(); expect(ourFeatureEvent).toBeTruthy();
}); });

View File

@ -44,8 +44,6 @@ afterEach(async () => {
}); });
test('returns empty list of users', async () => { test('returns empty list of users', async () => {
expect.assertions(1);
return app.request return app.request
.get('/api/admin/user-admin') .get('/api/admin/user-admin')
.expect('Content-Type', /json/) .expect('Content-Type', /json/)
@ -56,8 +54,6 @@ test('returns empty list of users', async () => {
}); });
test('creates and returns all users', async () => { test('creates and returns all users', async () => {
expect.assertions(2);
const createUserRequests = [...Array(20).keys()].map((i) => const createUserRequests = [...Array(20).keys()].map((i) =>
app.request app.request
.post('/api/admin/user-admin') .post('/api/admin/user-admin')
@ -82,8 +78,6 @@ test('creates and returns all users', async () => {
}); });
test('creates editor-user without password', async () => { test('creates editor-user without password', async () => {
expect.assertions(3);
return app.request return app.request
.post('/api/admin/user-admin') .post('/api/admin/user-admin')
.send({ .send({
@ -101,8 +95,6 @@ test('creates editor-user without password', async () => {
}); });
test('creates admin-user with password', async () => { test('creates admin-user with password', async () => {
expect.assertions(6);
const { body } = await app.request const { body } = await app.request
.post('/api/admin/user-admin') .post('/api/admin/user-admin')
.send({ .send({
@ -129,8 +121,6 @@ test('creates admin-user with password', async () => {
}); });
test('requires known root role', async () => { test('requires known root role', async () => {
expect.assertions(0);
return app.request return app.request
.post('/api/admin/user-admin') .post('/api/admin/user-admin')
.send({ .send({
@ -186,16 +176,12 @@ test('get a single user', async () => {
}); });
test('should delete user', async () => { test('should delete user', async () => {
expect.assertions(0);
const user = await userStore.insert({ email: 'some@mail.com' }); const user = await userStore.insert({ email: 'some@mail.com' });
return app.request.delete(`/api/admin/user-admin/${user.id}`).expect(200); return app.request.delete(`/api/admin/user-admin/${user.id}`).expect(200);
}); });
test('validator should require strong password', async () => { test('validator should require strong password', async () => {
expect.assertions(0);
return app.request return app.request
.post('/api/admin/user-admin/validate-password') .post('/api/admin/user-admin/validate-password')
.send({ password: 'simple' }) .send({ password: 'simple' })
@ -203,8 +189,6 @@ test('validator should require strong password', async () => {
}); });
test('validator should accept strong password', async () => { test('validator should accept strong password', async () => {
expect.assertions(0);
return app.request return app.request
.post('/api/admin/user-admin/validate-password') .post('/api/admin/user-admin/validate-password')
.send({ password: 'simple123-_ASsad' }) .send({ password: 'simple123-_ASsad' })
@ -212,8 +196,6 @@ test('validator should accept strong password', async () => {
}); });
test('should change password', async () => { test('should change password', async () => {
expect.assertions(0);
const user = await userStore.insert({ email: 'some@mail.com' }); const user = await userStore.insert({ email: 'some@mail.com' });
return app.request return app.request
@ -223,8 +205,6 @@ test('should change password', async () => {
}); });
test('should search for users', async () => { test('should search for users', async () => {
expect.assertions(2);
await userStore.insert({ email: 'some@mail.com' }); await userStore.insert({ email: 'some@mail.com' });
await userStore.insert({ email: 'another@mail.com' }); await userStore.insert({ email: 'another@mail.com' });
await userStore.insert({ email: 'another2@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 () => { test('Creates a user and includes inviteLink and emailConfigured', async () => {
expect.assertions(5);
return app.request return app.request
.post('/api/admin/user-admin') .post('/api/admin/user-admin')
.send({ .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 () => { test('generates USER_CREATED event', async () => {
expect.assertions(5);
const email = 'some@getunelash.ai'; const email = 'some@getunelash.ai';
const name = 'Some Name'; const name = 'Some Name';
@ -325,20 +302,16 @@ test('generates USER_CREATED event', async () => {
}); });
test('generates USER_DELETED event', async () => { test('generates USER_DELETED event', async () => {
expect.assertions(3);
const user = await userStore.insert({ email: 'some@mail.com' }); const user = await userStore.insert({ email: 'some@mail.com' });
await app.request.delete(`/api/admin/user-admin/${user.id}`); await app.request.delete(`/api/admin/user-admin/${user.id}`);
const events = await eventStore.getEvents(); const events = await eventStore.getEvents();
expect(events[0].type).toBe(USER_DELETED); expect(events[0].type).toBe(USER_DELETED);
expect(events[0].data.id).toBe(user.id); expect(events[0].preData.id).toBe(user.id);
expect(events[0].data.email).toBe(user.email); expect(events[0].preData.email).toBe(user.email);
}); });
test('generates USER_UPDATED event', async () => { test('generates USER_UPDATED event', async () => {
expect.assertions(3);
const { body } = await app.request const { body } = await app.request
.post('/api/admin/user-admin') .post('/api/admin/user-admin')
.send({ .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 { IStrategyConfig } from '../../../lib/types/model';
import { createTestConfig } from '../../config/test-config'; import { createTestConfig } from '../../config/test-config';
import dbInit from '../helpers/database-init'; import dbInit from '../helpers/database-init';
@ -6,7 +6,7 @@ import { DEFAULT_ENV } from '../../../lib/util/constants';
let stores; let stores;
let db; let db;
let service: FeatureToggleServiceV2; let service: FeatureToggleService;
beforeAll(async () => { beforeAll(async () => {
const config = createTestConfig(); const config = createTestConfig();
@ -15,7 +15,7 @@ beforeAll(async () => {
config.getLogger, config.getLogger,
); );
stores = db.stores; stores = db.stores;
service = new FeatureToggleServiceV2(stores, config); service = new FeatureToggleService(stores, config);
}); });
afterAll(async () => { afterAll(async () => {

View File

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

View File

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

View File

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

View File

@ -1,6 +1,6 @@
import EventEmitter from 'events'; import EventEmitter from 'events';
import { IEventStore } from '../../lib/types/stores/event-store'; 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 { class FakeEventStore extends EventEmitter implements IEventStore {
events: IEvent[]; events: IEvent[];
@ -11,6 +11,10 @@ class FakeEventStore extends EventEmitter implements IEventStore {
this.events = []; this.events = [];
} }
async getEventsForFeature(featureName: string): Promise<IEvent[]> {
return this.events.filter((e) => e.featureName === featureName);
}
store(event: IEvent): Promise<void> { store(event: IEvent): Promise<void> {
this.events.push(event); this.events.push(event);
this.emit(event.type, 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} #### 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-created
- feature-updated - feature-updated (*)
- feature-metadata-updated
- feature-project-change
- feature-archived - feature-archived
- feature-revived - feature-revived
- feature-strategy-update
- feature-strategy-add
- feature-strategy-remove
- feature-stale-on - feature-stale-on
- feature-stale-off - 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} #### 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} #### 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-created
- feature-updated - feature-updated (*)
- feature-metadata-updated
- feature-project-change
- feature-archived - feature-archived
- feature-revived - feature-revived
- feature-strategy-update
- feature-strategy-add
- feature-strategy-remove
- feature-stale-on - feature-stale-on
- feature-stale-off - 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} #### 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} #### 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-created
- feature-updated - feature-updated (*)
- feature-metadata-updated
- feature-project-change
- feature-archived - feature-archived
- feature-revived - feature-revived
- feature-strategy-update
- feature-strategy-add
- feature-strategy-remove
- feature-stale-on - feature-stale-on
- feature-stale-off - 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} #### 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): You can choose to trigger updates for the following events (we might add more event types in the future):
- feature-created - feature-created
- feature-updated - feature-updated (*)
- feature-metadata-updated
- feature-project-change
- feature-archived - feature-archived
- feature-revived - feature-revived
- feature-strategy-update
- feature-strategy-add
- feature-strategy-remove
- feature-stale-on - feature-stale-on
- feature-stale-off - 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} #### Parameters {#parameters}

View File

@ -13,39 +13,208 @@ Used to fetch all changes in the unleash system.
Defined event types: Defined event types:
### Feature Toggle events:
- feature-created - feature-created
- feature-deleted
- feature-updated - feature-updated
- feature-metadata-updated
- feature-project-change
- feature-archived - feature-archived
- feature-revived - 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-created
- strategy-deleted - 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-created
- tag-deleted - tag-deleted
- tag-import
- drop-tags
### Tag type events
- tag-type-created - tag-type-created
- tag-type-updated
- tag-type-deleted - 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** **Response**
```json ```json
{ {
"version": 1, "version": 2,
"events": [ "events": [{
{ "id": 187,
"id": 454, "type": "feature-metadata-updated",
"type": "feature-updated", "createdBy": "admin",
"createdBy": "unknown", "createdAt": "2021-11-11T09:42:14.271Z",
"createdAt": "2016-08-24T11:22:01.354Z",
"data": { "data": {
"name": "eid.bankid.mobile", "name": "HelloEvents!",
"description": "", "description": "Hello Events update!",
"strategy": "default", "type": "release",
"enabled": true, "project": "default",
"parameters": {} "stale": false,
"variants": [],
"createdAt": "2021-11-11T09:40:51.077Z",
"lastSeenAt": null
}, },
"diffs": [{ "kind": "E", "path": ["enabled"], "lhs": false, "rhs": true }] "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" resolved "https://registry.npmjs.org/dedent/-/dedent-0.7.0.tgz"
integrity sha1-JJXduvbrh0q7Dhvp3yLS5aVEMmw= 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: deep-extend@^0.6.0:
version "0.6.0" version "0.6.0"
resolved "https://registry.npmjs.org/deep-extend/-/deep-extend-0.6.0.tgz" resolved "https://registry.npmjs.org/deep-extend/-/deep-extend-0.6.0.tgz"
@ -7045,10 +7040,10 @@ universalify@^0.1.0, universalify@^0.1.2:
resolved "https://registry.npmjs.org/universalify/-/universalify-0.1.2.tgz" resolved "https://registry.npmjs.org/universalify/-/universalify-0.1.2.tgz"
integrity sha512-rBJeI5CXAlmy1pV+617WB9J63U6XcazHHF2f2dbJix4XzpUF0RS3Zbj0FGIOCAva5P/d/GBOYaACQ1w+0azUkg== integrity sha512-rBJeI5CXAlmy1pV+617WB9J63U6XcazHHF2f2dbJix4XzpUF0RS3Zbj0FGIOCAva5P/d/GBOYaACQ1w+0azUkg==
unleash-frontend@4.2.12: unleash-frontend@4.2.13:
version "4.2.12" version "4.2.13"
resolved "https://registry.yarnpkg.com/unleash-frontend/-/unleash-frontend-4.2.12.tgz#489d10ddbfabebe97ecbced97400f35e79cfb4a6" resolved "https://registry.yarnpkg.com/unleash-frontend/-/unleash-frontend-4.2.13.tgz#8ed3155ab8430506dd49290e6b6cd7c89b220512"
integrity sha512-YPRrPUTZlrkMWJlgEDNSw8uRP+7c0/A/QQ0KhRbNjtukejhE/7cncBpcEzXDu7q7cNCUbqFiwXigCY0BEgDEKQ== integrity sha512-UE8AJuTfuhJoKOSpq2VGlLH6mxC/VHs+mMFjkSuuCsrmKyCkZCPhttIO+jNFgRIAQkeEniPjF/3D/wWAeR1YXQ==
unpipe@1.0.0, unpipe@~1.0.0: unpipe@1.0.0, unpipe@~1.0.0:
version "1.0.0" version "1.0.0"