mirror of
https://github.com/Unleash/unleash.git
synced 2024-12-22 19:07:54 +01:00
feat: clean up events (#1089)
Co-authored-by: Christopher Kolstad <chriswk@getunleash.ai>
This commit is contained in:
parent
5b748a3cfc
commit
d8478dd928
@ -660,13 +660,19 @@ paths:
|
||||
operationId: get-admin-events
|
||||
summary: Fetch all changes in the Unleash system
|
||||
description: |-
|
||||
Returns one of the six event types:
|
||||
Returns one of the twelve event types:
|
||||
- feature-created
|
||||
- feature-updated
|
||||
- feature-metadata-updated
|
||||
- feature-project-change
|
||||
- feature-archived
|
||||
- feature-revived
|
||||
- strategy-created
|
||||
- strategy-deleted
|
||||
- feature-strategy-update
|
||||
- feature-strategy-add
|
||||
- feature-strategy-remove
|
||||
- feature-stale-on
|
||||
- feature-stale-off
|
||||
- feature-environment-enabled
|
||||
- feature-environment-disabled
|
||||
tags:
|
||||
- Events
|
||||
responses:
|
||||
@ -1548,15 +1554,21 @@ components:
|
||||
type: number
|
||||
example: 55
|
||||
type:
|
||||
description: One of the six event types
|
||||
description: Identifies the event
|
||||
type: string
|
||||
enum:
|
||||
- feature-created
|
||||
- feature-updated
|
||||
- feature-metadata-updated
|
||||
- feature-project-change
|
||||
- feature-archived
|
||||
- feature-revived
|
||||
- strategy-created
|
||||
- strategy-deleted
|
||||
- feature-strategy-update
|
||||
- feature-strategy-add
|
||||
- feature-strategy-remove
|
||||
- feature-stale-on
|
||||
- feature-stale-off
|
||||
- feature-environment-enabled
|
||||
- feature-environment-disabled
|
||||
minLength: 1
|
||||
example: feature-updated
|
||||
createdBy:
|
||||
@ -1568,53 +1580,21 @@ components:
|
||||
type: string
|
||||
example: '2016-12-09T14:56:36.730Z'
|
||||
data:
|
||||
$ref: '#/components/schemas/featureToggleSchema'
|
||||
diffs:
|
||||
description: |-
|
||||
The JSON differences between the current and last version of the Feature Toggle.
|
||||
(Uses the [deep-diff Node.js module](https://www.npmjs.com/package/deep-diff))
|
||||
externalDocs:
|
||||
description: Activation strategies
|
||||
url: 'https://www.npmjs.com/package/deep-diff#differences'
|
||||
type: array
|
||||
items:
|
||||
required:
|
||||
- kind
|
||||
- lhs
|
||||
- rhs
|
||||
properties:
|
||||
kind:
|
||||
description: |-
|
||||
The kind of change:
|
||||
- **N** - a newly-added property or element
|
||||
- **D** - a property or element was deleted
|
||||
- **E** - a property or element was edited
|
||||
- **A** - a change occurred within an array
|
||||
type: string
|
||||
enum:
|
||||
- 'N'
|
||||
- D
|
||||
- E
|
||||
- A
|
||||
example: E
|
||||
path:
|
||||
type: array
|
||||
items:
|
||||
required:
|
||||
- pathItem
|
||||
properties:
|
||||
pathItem:
|
||||
description: The property path (from the left-hand-side root)
|
||||
type: string
|
||||
example: enabled
|
||||
lhs:
|
||||
description: The value on the left-hand-side of the comparison (*undefined* if **kind** is *N*)
|
||||
type: boolean
|
||||
example: true
|
||||
rhs:
|
||||
description: The value on the right-hand-side of the comparison (*undefined* if **kind** is *D*)
|
||||
type: boolean
|
||||
example: false
|
||||
description: The current state of the updated resource
|
||||
type: object
|
||||
preData:
|
||||
description: The previous state of the updated resource
|
||||
type: object
|
||||
featureName:
|
||||
description: Name of the feature toggle (if event related to a feature toggle)
|
||||
type: string
|
||||
project:
|
||||
description: Name of the project (if event related to a project resource)
|
||||
type: string
|
||||
environment:
|
||||
description: Name of the environment (if event related to a environment resource)
|
||||
type: string
|
||||
|
||||
x-tags:
|
||||
- Responses
|
||||
200export:
|
||||
|
@ -85,7 +85,6 @@
|
||||
"db-migrate": "0.11.12",
|
||||
"db-migrate-pg": "1.2.2",
|
||||
"db-migrate-shared": "1.2.0",
|
||||
"deep-diff": "^1.0.2",
|
||||
"deepmerge": "^4.2.2",
|
||||
"errorhandler": "^1.5.1",
|
||||
"express": "^4.17.1",
|
||||
|
@ -2,7 +2,8 @@ import fetch, { Response } from 'node-fetch';
|
||||
import { addonDefinitionSchema } from './addon-schema';
|
||||
import { IUnleashConfig } from '../types/option';
|
||||
import { Logger } from '../logger';
|
||||
import { IAddonDefinition, IEvent } from '../types/model';
|
||||
import { IAddonDefinition } from '../types/model';
|
||||
import { IEvent } from '../types/events';
|
||||
|
||||
export default abstract class Addon {
|
||||
logger: Logger;
|
||||
|
@ -2,13 +2,13 @@ import {
|
||||
FEATURE_ARCHIVED,
|
||||
FEATURE_CREATED,
|
||||
FEATURE_ENVIRONMENT_DISABLED,
|
||||
IEvent,
|
||||
} from '../types/events';
|
||||
import { Logger } from '../logger';
|
||||
|
||||
import DatadogAddon from './datadog';
|
||||
|
||||
import noLogger from '../../test/fixtures/no-logger';
|
||||
import { IEvent } from '../types/model';
|
||||
|
||||
let fetchRetryCalls: any[] = [];
|
||||
|
||||
@ -45,6 +45,7 @@ test('Should call datadog webhook', async () => {
|
||||
createdAt: new Date(),
|
||||
type: FEATURE_CREATED,
|
||||
createdBy: 'some@user.com',
|
||||
featureName: 'some-toggle',
|
||||
data: {
|
||||
name: 'some-toggle',
|
||||
enabled: false,
|
||||
@ -72,6 +73,7 @@ test('Should call datadog webhook for archived toggle', async () => {
|
||||
createdAt: new Date(),
|
||||
type: FEATURE_ARCHIVED,
|
||||
createdBy: 'some@user.com',
|
||||
featureName: 'some-toggle',
|
||||
data: {
|
||||
name: 'some-toggle',
|
||||
},
|
||||
@ -99,6 +101,7 @@ test(`Should call datadog webhook for toggled environment`, async () => {
|
||||
createdBy: 'some@user.com',
|
||||
environment: 'development',
|
||||
project: 'default',
|
||||
featureName: 'some-toggle',
|
||||
data: {
|
||||
name: 'some-toggle',
|
||||
},
|
||||
|
@ -1,12 +1,13 @@
|
||||
import Addon from './addon';
|
||||
|
||||
import definition from './datadog-definition';
|
||||
import { IAddonConfig, IEvent } from '../types/model';
|
||||
import { IAddonConfig } from '../types/model';
|
||||
import {
|
||||
FeatureEventFormatter,
|
||||
FeatureEventFormatterMd,
|
||||
LinkStyle,
|
||||
} from './feature-event-formatter-md';
|
||||
import { IEvent } from '../types/events';
|
||||
|
||||
export default class DatadogAddon extends Addon {
|
||||
private msgFormatter: FeatureEventFormatter;
|
||||
|
@ -1,4 +1,3 @@
|
||||
import { IEvent } from '../types/model';
|
||||
import {
|
||||
FEATURE_CREATED,
|
||||
FEATURE_UPDATED,
|
||||
@ -13,6 +12,7 @@ import {
|
||||
FEATURE_STRATEGY_REMOVE,
|
||||
FEATURE_METADATA_UPDATED,
|
||||
FEATURE_PROJECT_CHANGE,
|
||||
IEvent,
|
||||
} from '../types/events';
|
||||
|
||||
export interface FeatureEventFormatter {
|
||||
@ -44,9 +44,9 @@ export class FeatureEventFormatterMd implements FeatureEventFormatter {
|
||||
|
||||
generateFeatureLink(event: IEvent): string {
|
||||
if (this.linkStyle === LinkStyle.SLACK) {
|
||||
return `<${this.featureLink(event)}|${event.data.name}>`;
|
||||
return `<${this.featureLink(event)}|${event.featureName}>`;
|
||||
} else {
|
||||
return `[${event.data.name}](${this.featureLink(event)})`;
|
||||
return `[${event.featureName}](${this.featureLink(event)})`;
|
||||
}
|
||||
}
|
||||
|
||||
@ -70,20 +70,17 @@ export class FeatureEventFormatterMd implements FeatureEventFormatter {
|
||||
}
|
||||
|
||||
generateStrategyChangeText(event: IEvent): string {
|
||||
const { createdBy, environment, project, data, type } = event;
|
||||
const { createdBy, environment, project, data, preData, type } = event;
|
||||
const feature = this.generateFeatureLink(event);
|
||||
let action;
|
||||
let strategyText: string = '';
|
||||
if (FEATURE_STRATEGY_UPDATE === type) {
|
||||
action = 'updated in';
|
||||
strategyText = `by updating strategy ${data?.name} in *${environment}*`;
|
||||
} else if (FEATURE_STRATEGY_ADD === type) {
|
||||
action = 'added to';
|
||||
} else {
|
||||
action = 'removed from';
|
||||
strategyText = `by adding strategy ${data?.name} in *${environment}*`;
|
||||
} else if (FEATURE_STRATEGY_REMOVE === type) {
|
||||
strategyText = `by removing strategy ${preData?.name} in *${environment}*`;
|
||||
}
|
||||
const strategyText = `a ${
|
||||
data.strategyName ?? ''
|
||||
} strategy ${action} the *${environment}* environment`;
|
||||
return `${createdBy} updated *${feature}* with ${strategyText} in project *${project}*`;
|
||||
return `${createdBy} updated *${feature}* in project *${project}* ${strategyText}`;
|
||||
}
|
||||
|
||||
generateMetadataText(event: IEvent): string {
|
||||
@ -93,16 +90,16 @@ export class FeatureEventFormatterMd implements FeatureEventFormatter {
|
||||
}
|
||||
|
||||
generateProjectChangeText(event: IEvent): string {
|
||||
const { createdBy, project, data } = event;
|
||||
return `${createdBy} moved ${data.name} to ${project}`;
|
||||
const { createdBy, project, featureName } = event;
|
||||
return `${createdBy} moved ${featureName} to ${project}`;
|
||||
}
|
||||
|
||||
featureLink(event: IEvent): string {
|
||||
const { type, project = '', data } = event;
|
||||
const { type, project = '', featureName } = event;
|
||||
if (type === FEATURE_ARCHIVED) {
|
||||
return `${this.unleashUrl}/archive`;
|
||||
}
|
||||
return `${this.unleashUrl}/projects/${project}/${data.name}`;
|
||||
return `${this.unleashUrl}/projects/${project}/${featureName}`;
|
||||
}
|
||||
|
||||
getAction(type: string): string {
|
||||
|
@ -2,13 +2,13 @@ import {
|
||||
FEATURE_CREATED,
|
||||
FEATURE_ARCHIVED,
|
||||
FEATURE_ENVIRONMENT_DISABLED,
|
||||
IEvent,
|
||||
} from '../types/events';
|
||||
import { Logger } from '../logger';
|
||||
|
||||
import SlackAddon from './slack';
|
||||
|
||||
import noLogger from '../../test/fixtures/no-logger';
|
||||
import { IEvent } from '../types/model';
|
||||
|
||||
let fetchRetryCalls: any[] = [];
|
||||
|
||||
@ -46,6 +46,7 @@ test('Should call slack webhook', async () => {
|
||||
type: FEATURE_CREATED,
|
||||
createdBy: 'some@user.com',
|
||||
project: 'default',
|
||||
featureName: 'some-toggle',
|
||||
data: {
|
||||
name: 'some-toggle',
|
||||
enabled: false,
|
||||
@ -73,6 +74,7 @@ test('Should call slack webhook for archived toggle', async () => {
|
||||
id: 2,
|
||||
createdAt: new Date(),
|
||||
type: FEATURE_ARCHIVED,
|
||||
featureName: 'some-toggle',
|
||||
createdBy: 'some@user.com',
|
||||
data: {
|
||||
name: 'some-toggle',
|
||||
@ -101,6 +103,7 @@ test(`Should call webhook for toggled environment`, async () => {
|
||||
createdBy: 'some@user.com',
|
||||
environment: 'development',
|
||||
project: 'default',
|
||||
featureName: 'some-toggle',
|
||||
data: {
|
||||
name: 'some-toggle',
|
||||
},
|
||||
@ -127,6 +130,7 @@ test('Should use default channel', async () => {
|
||||
createdAt: new Date(),
|
||||
type: FEATURE_CREATED,
|
||||
createdBy: 'some@user.com',
|
||||
featureName: 'some-toggle',
|
||||
data: {
|
||||
name: 'some-toggle',
|
||||
enabled: false,
|
||||
@ -156,6 +160,7 @@ test('Should override default channel with data from tag', async () => {
|
||||
createdAt: new Date(),
|
||||
type: FEATURE_CREATED,
|
||||
createdBy: 'some@user.com',
|
||||
featureName: 'some-toggle',
|
||||
data: {
|
||||
name: 'some-toggle',
|
||||
enabled: false,
|
||||
@ -191,6 +196,7 @@ test('Should post to all channels in tags', async () => {
|
||||
createdAt: new Date(),
|
||||
type: FEATURE_CREATED,
|
||||
createdBy: 'some@user.com',
|
||||
featureName: 'some-toggle',
|
||||
data: {
|
||||
name: 'some-toggle',
|
||||
enabled: false,
|
||||
|
@ -1,13 +1,14 @@
|
||||
import Addon from './addon';
|
||||
|
||||
import slackDefinition from './slack-definition';
|
||||
import { IAddonConfig, IEvent } from '../types/model';
|
||||
import { IAddonConfig } from '../types/model';
|
||||
|
||||
import {
|
||||
FeatureEventFormatter,
|
||||
FeatureEventFormatterMd,
|
||||
LinkStyle,
|
||||
} from './feature-event-formatter-md';
|
||||
import { IEvent } from '../types/events';
|
||||
|
||||
export default class SlackAddon extends Addon {
|
||||
private msgFormatter: FeatureEventFormatter;
|
||||
|
@ -4,12 +4,12 @@ import {
|
||||
FEATURE_ARCHIVED,
|
||||
FEATURE_CREATED,
|
||||
FEATURE_ENVIRONMENT_DISABLED,
|
||||
IEvent,
|
||||
} from '../types/events';
|
||||
|
||||
import TeamsAddon from './teams';
|
||||
|
||||
import noLogger from '../../test/fixtures/no-logger';
|
||||
import { IEvent } from '../types/model';
|
||||
|
||||
let fetchRetryCalls: any[];
|
||||
|
||||
@ -46,6 +46,7 @@ test('Should call teams webhook', async () => {
|
||||
createdAt: new Date(),
|
||||
type: FEATURE_CREATED,
|
||||
createdBy: 'some@user.com',
|
||||
featureName: 'some-toggle',
|
||||
data: {
|
||||
name: 'some-toggle',
|
||||
enabled: false,
|
||||
@ -73,6 +74,7 @@ test('Should call teams webhook for archived toggle', async () => {
|
||||
createdAt: new Date(),
|
||||
type: FEATURE_ARCHIVED,
|
||||
createdBy: 'some@user.com',
|
||||
featureName: 'some-toggle',
|
||||
data: {
|
||||
name: 'some-toggle',
|
||||
},
|
||||
@ -100,6 +102,7 @@ test(`Should call teams webhook for toggled environment`, async () => {
|
||||
createdBy: 'some@user.com',
|
||||
environment: 'development',
|
||||
project: 'default',
|
||||
featureName: 'some-toggle',
|
||||
data: {
|
||||
name: 'some-toggle',
|
||||
},
|
||||
|
@ -1,11 +1,12 @@
|
||||
import Addon from './addon';
|
||||
|
||||
import teamsDefinition from './teams-definition';
|
||||
import { IAddonConfig, IEvent } from '../types/model';
|
||||
import { IAddonConfig } from '../types/model';
|
||||
import {
|
||||
FeatureEventFormatter,
|
||||
FeatureEventFormatterMd,
|
||||
} from './feature-event-formatter-md';
|
||||
import { IEvent } from '../types/events';
|
||||
|
||||
export default class TeamsAddon extends Addon {
|
||||
private msgFormatter: FeatureEventFormatter;
|
||||
|
@ -1,11 +1,10 @@
|
||||
import { Logger } from '../logger';
|
||||
|
||||
import { FEATURE_CREATED } from '../types/events';
|
||||
import { FEATURE_CREATED, IEvent } from '../types/events';
|
||||
|
||||
import WebhookAddon from './webhook';
|
||||
|
||||
import noLogger from '../../test/fixtures/no-logger';
|
||||
import { IEvent } from '../types/model';
|
||||
|
||||
let fetchRetryCalls: any[] = [];
|
||||
|
||||
@ -39,6 +38,7 @@ test('Should handle event without "bodyTemplate"', () => {
|
||||
createdAt: new Date(),
|
||||
type: FEATURE_CREATED,
|
||||
createdBy: 'some@user.com',
|
||||
featureName: 'some-toggle',
|
||||
data: {
|
||||
name: 'some-toggle',
|
||||
enabled: false,
|
||||
@ -63,6 +63,7 @@ test('Should format event with "bodyTemplate"', () => {
|
||||
createdAt: new Date(),
|
||||
type: FEATURE_CREATED,
|
||||
createdBy: 'some@user.com',
|
||||
featureName: 'some-toggle',
|
||||
data: {
|
||||
name: 'some-toggle',
|
||||
enabled: false,
|
||||
|
@ -2,15 +2,20 @@ import Mustache from 'mustache';
|
||||
import Addon from './addon';
|
||||
import definition from './webhook-definition';
|
||||
import { LogProvider } from '../logger';
|
||||
import { IEvent } from '../types/model';
|
||||
import { IEvent } from '../types/events';
|
||||
|
||||
interface IParameters {
|
||||
url: string;
|
||||
bodyTemplate?: string;
|
||||
contentType?: string;
|
||||
}
|
||||
|
||||
export default class Webhook extends Addon {
|
||||
constructor(args: { getLogger: LogProvider }) {
|
||||
super(definition, args);
|
||||
}
|
||||
|
||||
// eslint-disable-next-line @typescript-eslint/explicit-module-boundary-types
|
||||
async handleEvent(event: IEvent, parameters: any): Promise<void> {
|
||||
async handleEvent(event: IEvent, parameters: IParameters): Promise<void> {
|
||||
const { url, bodyTemplate, contentType } = parameters;
|
||||
const context = {
|
||||
event,
|
||||
|
@ -1,9 +1,9 @@
|
||||
import { EventEmitter } from 'events';
|
||||
import { Knex } from 'knex';
|
||||
import { DROP_FEATURES } from '../types/events';
|
||||
import { DROP_FEATURES, IEvent, IBaseEvent } from '../types/events';
|
||||
import { LogProvider, Logger } from '../logger';
|
||||
import { IEventStore } from '../types/stores/event-store';
|
||||
import { ICreateEvent, IEvent } from '../types/model';
|
||||
import { ITag } from '../types/model';
|
||||
|
||||
const EVENT_COLUMNS = [
|
||||
'id',
|
||||
@ -11,7 +11,9 @@ const EVENT_COLUMNS = [
|
||||
'created_by',
|
||||
'created_at',
|
||||
'data',
|
||||
'pre_data',
|
||||
'tags',
|
||||
'feature_name',
|
||||
'project',
|
||||
'environment',
|
||||
];
|
||||
@ -21,10 +23,12 @@ export interface IEventTable {
|
||||
type: string;
|
||||
created_by: string;
|
||||
created_at: Date;
|
||||
data: any;
|
||||
data?: any;
|
||||
pre_data?: any;
|
||||
feature_name?: string;
|
||||
project?: string;
|
||||
environment?: string;
|
||||
tags: [];
|
||||
tags: ITag[];
|
||||
}
|
||||
|
||||
const TABLE = 'events';
|
||||
@ -40,7 +44,7 @@ class EventStore extends EventEmitter implements IEventStore {
|
||||
this.logger = getLogger('lib/db/event-store.ts');
|
||||
}
|
||||
|
||||
async store(event: ICreateEvent): Promise<void> {
|
||||
async store(event: IBaseEvent): Promise<void> {
|
||||
try {
|
||||
const rows = await this.db(TABLE)
|
||||
.insert(this.eventToDbRow(event))
|
||||
@ -52,7 +56,7 @@ class EventStore extends EventEmitter implements IEventStore {
|
||||
}
|
||||
}
|
||||
|
||||
async batchStore(events: ICreateEvent[]): Promise<void> {
|
||||
async batchStore(events: IBaseEvent[]): Promise<void> {
|
||||
try {
|
||||
const savedRows = await this.db(TABLE)
|
||||
.insert(events.map(this.eventToDbRow))
|
||||
@ -129,6 +133,7 @@ class EventStore extends EventEmitter implements IEventStore {
|
||||
.orderBy('created_at', 'desc');
|
||||
return rows.map(this.rowToEvent);
|
||||
} catch (err) {
|
||||
this.logger.error(err);
|
||||
return [];
|
||||
}
|
||||
}
|
||||
@ -146,6 +151,19 @@ class EventStore extends EventEmitter implements IEventStore {
|
||||
}
|
||||
}
|
||||
|
||||
async getEventsForFeature(featureName: string): Promise<IEvent[]> {
|
||||
try {
|
||||
const rows = await this.db
|
||||
.select(EVENT_COLUMNS)
|
||||
.from(TABLE)
|
||||
.where({ feature_name: featureName })
|
||||
.orderBy('created_at', 'desc');
|
||||
return rows.map(this.rowToEvent);
|
||||
} catch (err) {
|
||||
return [];
|
||||
}
|
||||
}
|
||||
|
||||
rowToEvent(row: IEventTable): IEvent {
|
||||
return {
|
||||
id: row.id,
|
||||
@ -153,23 +171,27 @@ class EventStore extends EventEmitter implements IEventStore {
|
||||
createdBy: row.created_by,
|
||||
createdAt: row.created_at,
|
||||
data: row.data,
|
||||
preData: row.pre_data,
|
||||
tags: row.tags || [],
|
||||
featureName: row.feature_name,
|
||||
project: row.project,
|
||||
environment: row.environment,
|
||||
};
|
||||
}
|
||||
|
||||
eventToDbRow(e: ICreateEvent): any {
|
||||
eventToDbRow(e: IBaseEvent): Omit<IEventTable, 'id' | 'created_at'> {
|
||||
return {
|
||||
type: e.type,
|
||||
created_by: e.createdBy,
|
||||
data: e.data,
|
||||
pre_data: e.preData,
|
||||
//@ts-ignore workaround for json-array
|
||||
tags: JSON.stringify(e.tags),
|
||||
feature_name: e.featureName,
|
||||
project: e.project,
|
||||
environment: e.environment,
|
||||
};
|
||||
}
|
||||
}
|
||||
|
||||
module.exports = EventStore;
|
||||
export default EventStore;
|
||||
|
@ -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,
|
||||
};
|
@ -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);
|
||||
});
|
@ -53,6 +53,7 @@ test('should collect metrics for requests', async () => {
|
||||
|
||||
test('should collect metrics for updated toggles', async () => {
|
||||
stores.eventStore.emit(FEATURE_UPDATED, {
|
||||
featureName: 'TestToggle',
|
||||
data: { name: 'TestToggle' },
|
||||
});
|
||||
|
||||
|
@ -7,6 +7,9 @@ import {
|
||||
FEATURE_ARCHIVED,
|
||||
FEATURE_CREATED,
|
||||
FEATURE_REVIVED,
|
||||
FEATURE_STRATEGY_ADD,
|
||||
FEATURE_STRATEGY_REMOVE,
|
||||
FEATURE_STRATEGY_UPDATE,
|
||||
FEATURE_UPDATED,
|
||||
} from './types/events';
|
||||
import { IUnleashConfig } from './types/option';
|
||||
@ -123,17 +126,26 @@ export default class MetricsMonitor {
|
||||
dbDuration.labels(store, action).observe(time);
|
||||
});
|
||||
|
||||
eventStore.on(FEATURE_CREATED, ({ data }) => {
|
||||
featureToggleUpdateTotal.labels(data.name).inc();
|
||||
eventStore.on(FEATURE_CREATED, ({ featureName }) => {
|
||||
featureToggleUpdateTotal.labels(featureName).inc();
|
||||
});
|
||||
eventStore.on(FEATURE_UPDATED, ({ data }) => {
|
||||
featureToggleUpdateTotal.labels(data.name).inc();
|
||||
eventStore.on(FEATURE_UPDATED, ({ featureName }) => {
|
||||
featureToggleUpdateTotal.labels(featureName).inc();
|
||||
});
|
||||
eventStore.on(FEATURE_ARCHIVED, ({ data }) => {
|
||||
featureToggleUpdateTotal.labels(data.name).inc();
|
||||
eventStore.on(FEATURE_STRATEGY_ADD, ({ featureName }) => {
|
||||
featureToggleUpdateTotal.labels(featureName).inc();
|
||||
});
|
||||
eventStore.on(FEATURE_REVIVED, ({ data }) => {
|
||||
featureToggleUpdateTotal.labels(data.name).inc();
|
||||
eventStore.on(FEATURE_STRATEGY_REMOVE, ({ featureName }) => {
|
||||
featureToggleUpdateTotal.labels(featureName).inc();
|
||||
});
|
||||
eventStore.on(FEATURE_STRATEGY_UPDATE, ({ featureName }) => {
|
||||
featureToggleUpdateTotal.labels(featureName).inc();
|
||||
});
|
||||
eventStore.on(FEATURE_ARCHIVED, ({ featureName }) => {
|
||||
featureToggleUpdateTotal.labels(featureName).inc();
|
||||
});
|
||||
eventStore.on(FEATURE_REVIVED, ({ featureName }) => {
|
||||
featureToggleUpdateTotal.labels(featureName).inc();
|
||||
});
|
||||
|
||||
clientMetricsStore.on('metrics', (m) => {
|
||||
|
@ -7,13 +7,13 @@ import Controller from '../controller';
|
||||
|
||||
import { extractUsername } from '../../util/extract-user';
|
||||
import { DELETE_FEATURE, UPDATE_FEATURE } from '../../types/permissions';
|
||||
import FeatureToggleServiceV2 from '../../services/feature-toggle-service';
|
||||
import FeatureToggleService from '../../services/feature-toggle-service';
|
||||
import { IAuthRequest } from '../unleash-types';
|
||||
|
||||
export default class ArchiveController extends Controller {
|
||||
private readonly logger: Logger;
|
||||
|
||||
private featureService: FeatureToggleServiceV2;
|
||||
private featureService: FeatureToggleService;
|
||||
|
||||
constructor(
|
||||
config: IUnleashConfig,
|
||||
|
@ -1,11 +1,10 @@
|
||||
import { Request, Response } from 'express';
|
||||
import { IUnleashConfig } from '../../types/option';
|
||||
import { IUnleashServices } from '../../types/services';
|
||||
import EventService from '../../services/event-service';
|
||||
import { ADMIN } from '../../types/permissions';
|
||||
|
||||
const Controller = require('../controller');
|
||||
|
||||
const eventDiffer = require('../../event-differ');
|
||||
import { IEvent } from '../../types/events';
|
||||
import Controller from '../controller';
|
||||
|
||||
const version = 1;
|
||||
|
||||
@ -22,32 +21,31 @@ export default class EventController extends Controller {
|
||||
this.get('/:name', this.getEventsForToggle);
|
||||
}
|
||||
|
||||
// eslint-disable-next-line @typescript-eslint/explicit-module-boundary-types
|
||||
async getEvents(req, res): Promise<void> {
|
||||
let events;
|
||||
if (req.query?.project) {
|
||||
events = await this.eventService.getEventsForProject(
|
||||
req.query.project,
|
||||
);
|
||||
async getEvents(
|
||||
req: Request<any, any, any, { project?: string }>,
|
||||
res: Response,
|
||||
): Promise<void> {
|
||||
const { project } = req.query;
|
||||
let events: IEvent[];
|
||||
if (project) {
|
||||
events = await this.eventService.getEventsForProject(project);
|
||||
} else {
|
||||
events = await this.eventService.getEvents();
|
||||
}
|
||||
eventDiffer.addDiffs(events);
|
||||
res.json({ version, events });
|
||||
}
|
||||
|
||||
// eslint-disable-next-line @typescript-eslint/explicit-module-boundary-types
|
||||
async getEventsForToggle(req, res): Promise<void> {
|
||||
async getEventsForToggle(
|
||||
req: Request<{ name: string }>,
|
||||
res: Response,
|
||||
): Promise<void> {
|
||||
const toggleName = req.params.name;
|
||||
const events = await this.eventService.getEventsForToggle(toggleName);
|
||||
|
||||
if (events) {
|
||||
eventDiffer.addDiffs(events);
|
||||
res.json({ toggleName, events });
|
||||
} else {
|
||||
res.status(404).json({ error: 'Could not find events' });
|
||||
}
|
||||
}
|
||||
}
|
||||
|
||||
module.exports = EventController;
|
||||
|
@ -11,7 +11,7 @@ import {
|
||||
} from '../../types/permissions';
|
||||
import { IUnleashConfig } from '../../types/option';
|
||||
import { IUnleashServices } from '../../types/services';
|
||||
import FeatureToggleServiceV2 from '../../services/feature-toggle-service';
|
||||
import FeatureToggleService from '../../services/feature-toggle-service';
|
||||
import { featureSchema, querySchema } from '../../schema/feature-schema';
|
||||
import { IFeatureToggleQuery } from '../../types/model';
|
||||
import FeatureTagService from '../../services/feature-tag-service';
|
||||
@ -23,7 +23,7 @@ const version = 1;
|
||||
class FeatureController extends Controller {
|
||||
private tagService: FeatureTagService;
|
||||
|
||||
private service: FeatureToggleServiceV2;
|
||||
private service: FeatureToggleService;
|
||||
|
||||
constructor(
|
||||
config: IUnleashConfig,
|
||||
|
@ -3,7 +3,7 @@ import { applyPatch, Operation } from 'fast-json-patch';
|
||||
import Controller from '../../controller';
|
||||
import { IUnleashConfig } from '../../../types/option';
|
||||
import { IUnleashServices } from '../../../types/services';
|
||||
import FeatureToggleServiceV2 from '../../../services/feature-toggle-service';
|
||||
import FeatureToggleService from '../../../services/feature-toggle-service';
|
||||
import { Logger } from '../../../logger';
|
||||
import { CREATE_FEATURE, UPDATE_FEATURE } from '../../../types/permissions';
|
||||
import {
|
||||
@ -52,7 +52,7 @@ type ProjectFeaturesServices = Pick<
|
||||
>;
|
||||
|
||||
export default class ProjectFeaturesController extends Controller {
|
||||
private featureService: FeatureToggleServiceV2;
|
||||
private featureService: FeatureToggleService;
|
||||
|
||||
private readonly logger: Logger;
|
||||
|
||||
|
@ -103,4 +103,3 @@ class StateController extends Controller {
|
||||
}
|
||||
}
|
||||
export default StateController;
|
||||
module.exports = StateController;
|
||||
|
@ -3,7 +3,7 @@ import { Response } from 'express';
|
||||
import Controller from '../controller';
|
||||
import { IUnleashServices } from '../../types/services';
|
||||
import { IUnleashConfig } from '../../types/option';
|
||||
import FeatureToggleServiceV2 from '../../services/feature-toggle-service';
|
||||
import FeatureToggleService from '../../services/feature-toggle-service';
|
||||
import { Logger } from '../../logger';
|
||||
import { querySchema } from '../../schema/feature-schema';
|
||||
import { IFeatureToggleQuery } from '../../types/model';
|
||||
@ -22,7 +22,7 @@ interface QueryOverride {
|
||||
export default class FeatureController extends Controller {
|
||||
private readonly logger: Logger;
|
||||
|
||||
private featureToggleServiceV2: FeatureToggleServiceV2;
|
||||
private featureToggleServiceV2: FeatureToggleService;
|
||||
|
||||
private readonly cache: boolean;
|
||||
|
||||
|
@ -1,11 +1,12 @@
|
||||
import Addon from '../addons/addon';
|
||||
import getLogger from '../../test/fixtures/no-logger';
|
||||
import { IAddonDefinition, IEvent } from '../types/model';
|
||||
import { IAddonDefinition } from '../types/model';
|
||||
import {
|
||||
FEATURE_ARCHIVED,
|
||||
FEATURE_CREATED,
|
||||
FEATURE_REVIVED,
|
||||
FEATURE_UPDATED,
|
||||
IEvent,
|
||||
} from '../types/events';
|
||||
|
||||
const definition: IAddonDefinition = {
|
||||
|
@ -1,7 +1,7 @@
|
||||
import { applicationSchema } from './metrics-schema';
|
||||
import { Projection } from './projection';
|
||||
import { clientMetricsSchema } from './client-metrics-schema';
|
||||
import { APPLICATION_CREATED } from '../../types/events';
|
||||
import { APPLICATION_CREATED, IBaseEvent } from '../../types/events';
|
||||
import { IApplication, IYesNoCount } from './models';
|
||||
import { IUnleashStores } from '../../types/stores';
|
||||
import { IUnleashConfig } from '../../types/option';
|
||||
@ -15,12 +15,7 @@ import { IStrategyStore } from '../../types/stores/strategy-store';
|
||||
import { IClientMetricsStore } from '../../types/stores/client-metrics-store';
|
||||
import { IClientInstanceStore } from '../../types/stores/client-instance-store';
|
||||
import { IApplicationQuery } from '../../types/query';
|
||||
import {
|
||||
IClientApp,
|
||||
ICreateEvent,
|
||||
IMetricCounts,
|
||||
IMetricsBucket,
|
||||
} from '../../types/model';
|
||||
import { IClientApp, IMetricCounts, IMetricsBucket } from '../../types/model';
|
||||
import { clientRegisterSchema } from './register-schema';
|
||||
|
||||
import {
|
||||
@ -207,7 +202,7 @@ export default class ClientMetricsService {
|
||||
}
|
||||
}
|
||||
|
||||
appToEvent(app: IClientApp): ICreateEvent {
|
||||
appToEvent(app: IClientApp): IBaseEvent {
|
||||
return {
|
||||
type: APPLICATION_CREATED,
|
||||
createdBy: app.clientIp,
|
||||
|
@ -2,7 +2,7 @@ import { IUnleashConfig } from '../types/option';
|
||||
import { IUnleashStores } from '../types/stores';
|
||||
import { Logger } from '../logger';
|
||||
import { IEventStore } from '../types/stores/event-store';
|
||||
import { IEvent } from '../types/model';
|
||||
import { IEvent } from '../types/events';
|
||||
|
||||
export default class EventService {
|
||||
private logger: Logger;
|
||||
@ -22,7 +22,7 @@ export default class EventService {
|
||||
}
|
||||
|
||||
async getEventsForToggle(name: string): Promise<IEvent[]> {
|
||||
return this.eventStore.getEventsFilterByType(name);
|
||||
return this.eventStore.getEventsForFeature(name);
|
||||
}
|
||||
|
||||
async getEventsForProject(project: string): Promise<IEvent[]> {
|
||||
|
@ -37,6 +37,7 @@ class FeatureTagService {
|
||||
return this.featureTagStore.getAllTagsForFeature(featureName);
|
||||
}
|
||||
|
||||
// TODO: add project Id
|
||||
async addTag(
|
||||
featureName: string,
|
||||
tag: ITag,
|
||||
@ -50,10 +51,8 @@ class FeatureTagService {
|
||||
await this.eventStore.store({
|
||||
type: FEATURE_TAGGED,
|
||||
createdBy: userName,
|
||||
data: {
|
||||
featureName,
|
||||
tag: validatedTag,
|
||||
},
|
||||
featureName,
|
||||
data: validatedTag,
|
||||
});
|
||||
return validatedTag;
|
||||
}
|
||||
@ -67,14 +66,13 @@ class FeatureTagService {
|
||||
await this.eventStore.store({
|
||||
type: TAG_CREATED,
|
||||
createdBy: userName,
|
||||
data: {
|
||||
tag,
|
||||
},
|
||||
data: tag,
|
||||
});
|
||||
}
|
||||
}
|
||||
}
|
||||
|
||||
// TODO: add project Id
|
||||
async removeTag(
|
||||
featureName: string,
|
||||
tag: ITag,
|
||||
@ -84,10 +82,8 @@ class FeatureTagService {
|
||||
await this.eventStore.store({
|
||||
type: FEATURE_UNTAGGED,
|
||||
createdBy: userName,
|
||||
data: {
|
||||
featureName,
|
||||
tag,
|
||||
},
|
||||
featureName,
|
||||
data: tag,
|
||||
});
|
||||
}
|
||||
}
|
||||
|
@ -7,19 +7,17 @@ import InvalidOperationError from '../error/invalid-operation-error';
|
||||
import { FOREIGN_KEY_VIOLATION } from '../error/db-error';
|
||||
import { featureMetadataSchema, nameSchema } from '../schema/feature-schema';
|
||||
import {
|
||||
FEATURE_ARCHIVED,
|
||||
FEATURE_CREATED,
|
||||
FEATURE_DELETED,
|
||||
FEATURE_ENVIRONMENT_DISABLED,
|
||||
FEATURE_ENVIRONMENT_ENABLED,
|
||||
FEATURE_METADATA_UPDATED,
|
||||
FEATURE_PROJECT_CHANGE,
|
||||
FEATURE_REVIVED,
|
||||
FEATURE_STALE_OFF,
|
||||
FEATURE_STALE_ON,
|
||||
FEATURE_STRATEGY_ADD,
|
||||
FEATURE_STRATEGY_REMOVE,
|
||||
FEATURE_STRATEGY_UPDATE,
|
||||
FeatureArchivedEvent,
|
||||
FeatureChangeProjectEvent,
|
||||
FeatureCreatedEvent,
|
||||
FeatureDeletedEvent,
|
||||
FeatureEnvironmentEvent,
|
||||
FeatureMetadataUpdateEvent,
|
||||
FeatureRevivedEvent,
|
||||
FeatureStaleEvent,
|
||||
FeatureStrategyAddEvent,
|
||||
FeatureStrategyRemoveEvent,
|
||||
FeatureStrategyUpdateEvent,
|
||||
FEATURE_UPDATED,
|
||||
} from '../types/events';
|
||||
import NotFoundError from '../error/notfound-error';
|
||||
@ -57,7 +55,7 @@ interface IFeatureStrategyContext extends IFeatureContext {
|
||||
environment: string;
|
||||
}
|
||||
|
||||
class FeatureToggleServiceV2 {
|
||||
class FeatureToggleService {
|
||||
private logger: Logger;
|
||||
|
||||
private featureStrategiesStore: IFeatureStrategiesStore;
|
||||
@ -66,7 +64,7 @@ class FeatureToggleServiceV2 {
|
||||
|
||||
private featureToggleClientStore: IFeatureToggleClientStore;
|
||||
|
||||
private featureTagStore: IFeatureTagStore;
|
||||
private tagStore: IFeatureTagStore;
|
||||
|
||||
private featureEnvironmentStore: IFeatureEnvironmentStore;
|
||||
|
||||
@ -99,7 +97,7 @@ class FeatureToggleServiceV2 {
|
||||
this.featureStrategiesStore = featureStrategiesStore;
|
||||
this.featureToggleStore = featureToggleStore;
|
||||
this.featureToggleClientStore = featureToggleClientStore;
|
||||
this.featureTagStore = featureTagStore;
|
||||
this.tagStore = featureTagStore;
|
||||
this.projectStore = projectStore;
|
||||
this.eventStore = eventStore;
|
||||
this.featureEnvironmentStore = featureEnvironmentStore;
|
||||
@ -135,9 +133,9 @@ class FeatureToggleServiceV2 {
|
||||
}
|
||||
|
||||
async patchFeature(
|
||||
projectId: string,
|
||||
project: string,
|
||||
featureName: string,
|
||||
userName: string,
|
||||
createdBy: string,
|
||||
operations: Operation[],
|
||||
): Promise<FeatureToggle> {
|
||||
const featureToggle = await this.getFeatureMetadata(featureName);
|
||||
@ -146,26 +144,45 @@ class FeatureToggleServiceV2 {
|
||||
deepClone(featureToggle),
|
||||
operations,
|
||||
);
|
||||
|
||||
const updated = await this.updateFeatureToggle(
|
||||
projectId,
|
||||
project,
|
||||
newDocument,
|
||||
userName,
|
||||
createdBy,
|
||||
);
|
||||
|
||||
if (featureToggle.stale !== newDocument.stale) {
|
||||
await this.eventStore.store({
|
||||
type: newDocument.stale ? FEATURE_STALE_ON : FEATURE_STALE_OFF,
|
||||
data: updated,
|
||||
project: projectId,
|
||||
createdBy: userName,
|
||||
});
|
||||
const tags = await this.tagStore.getAllTagsForFeature(featureName);
|
||||
|
||||
await this.eventStore.store(
|
||||
new FeatureStaleEvent({
|
||||
stale: newDocument.stale,
|
||||
project,
|
||||
featureName,
|
||||
createdBy,
|
||||
tags,
|
||||
}),
|
||||
);
|
||||
}
|
||||
|
||||
return updated;
|
||||
}
|
||||
|
||||
featureStrategyToPublic(
|
||||
featureStrategy: IFeatureStrategy,
|
||||
): IStrategyConfig {
|
||||
return {
|
||||
id: featureStrategy.id,
|
||||
name: featureStrategy.strategyName,
|
||||
constraints: featureStrategy.constraints || [],
|
||||
parameters: featureStrategy.parameters,
|
||||
};
|
||||
}
|
||||
|
||||
async createStrategy(
|
||||
strategyConfig: Omit<IStrategyConfig, 'id'>,
|
||||
context: IFeatureStrategyContext,
|
||||
userName: string,
|
||||
createdBy: string,
|
||||
): Promise<IStrategyConfig> {
|
||||
const { featureName, projectId, environment } = context;
|
||||
await this.validateFeatureContext(context);
|
||||
@ -180,24 +197,20 @@ class FeatureToggleServiceV2 {
|
||||
featureName,
|
||||
environment,
|
||||
});
|
||||
const data = {
|
||||
id: newFeatureStrategy.id,
|
||||
name: newFeatureStrategy.strategyName,
|
||||
constraints: newFeatureStrategy.constraints,
|
||||
parameters: newFeatureStrategy.parameters,
|
||||
};
|
||||
await this.eventStore.store({
|
||||
type: FEATURE_STRATEGY_ADD,
|
||||
project: projectId,
|
||||
createdBy: userName,
|
||||
environment,
|
||||
data: {
|
||||
...data,
|
||||
name: featureName, // Done like this since we use data as our return object.
|
||||
strategyName: newFeatureStrategy.strategyName,
|
||||
},
|
||||
});
|
||||
return data;
|
||||
|
||||
const tags = await this.tagStore.getAllTagsForFeature(featureName);
|
||||
const strategy = this.featureStrategyToPublic(newFeatureStrategy);
|
||||
await this.eventStore.store(
|
||||
new FeatureStrategyAddEvent({
|
||||
project: projectId,
|
||||
featureName,
|
||||
createdBy,
|
||||
environment,
|
||||
data: strategy,
|
||||
tags,
|
||||
}),
|
||||
);
|
||||
return strategy;
|
||||
} catch (e) {
|
||||
if (e.code === FOREIGN_KEY_VIOLATION) {
|
||||
throw new BadDataError(
|
||||
@ -224,7 +237,7 @@ class FeatureToggleServiceV2 {
|
||||
context: IFeatureStrategyContext,
|
||||
userName: string,
|
||||
): Promise<IStrategyConfig> {
|
||||
const { projectId, environment } = context;
|
||||
const { projectId, environment, featureName } = context;
|
||||
const existingStrategy = await this.featureStrategiesStore.get(id);
|
||||
this.validateFeatureStrategyContext(existingStrategy, context);
|
||||
|
||||
@ -233,20 +246,22 @@ class FeatureToggleServiceV2 {
|
||||
id,
|
||||
updates,
|
||||
);
|
||||
const data = {
|
||||
id: strategy.id,
|
||||
name: strategy.strategyName,
|
||||
featureName: strategy.featureName,
|
||||
constraints: strategy.constraints || [],
|
||||
parameters: strategy.parameters,
|
||||
};
|
||||
await this.eventStore.store({
|
||||
type: FEATURE_STRATEGY_UPDATE,
|
||||
project: projectId,
|
||||
environment,
|
||||
createdBy: userName,
|
||||
data,
|
||||
});
|
||||
|
||||
// Store event!
|
||||
const tags = await this.tagStore.getAllTagsForFeature(featureName);
|
||||
const data = this.featureStrategyToPublic(strategy);
|
||||
const preData = this.featureStrategyToPublic(existingStrategy);
|
||||
await this.eventStore.store(
|
||||
new FeatureStrategyUpdateEvent({
|
||||
project: projectId,
|
||||
featureName,
|
||||
environment,
|
||||
createdBy: userName,
|
||||
data,
|
||||
preData,
|
||||
tags,
|
||||
}),
|
||||
);
|
||||
return data;
|
||||
}
|
||||
throw new NotFoundError(`Could not find strategy with id ${id}`);
|
||||
@ -259,7 +274,7 @@ class FeatureToggleServiceV2 {
|
||||
context: IFeatureStrategyContext,
|
||||
userName: string,
|
||||
): Promise<IStrategyConfig> {
|
||||
const { projectId, environment } = context;
|
||||
const { projectId, environment, featureName } = context;
|
||||
|
||||
const existingStrategy = await this.featureStrategiesStore.get(id);
|
||||
this.validateFeatureStrategyContext(existingStrategy, context);
|
||||
@ -270,19 +285,20 @@ class FeatureToggleServiceV2 {
|
||||
id,
|
||||
existingStrategy,
|
||||
);
|
||||
const data = {
|
||||
id: strategy.id,
|
||||
name: strategy.strategyName,
|
||||
constraints: strategy.constraints || [],
|
||||
parameters: strategy.parameters,
|
||||
};
|
||||
await this.eventStore.store({
|
||||
type: FEATURE_STRATEGY_UPDATE,
|
||||
project: projectId,
|
||||
environment,
|
||||
createdBy: userName,
|
||||
data,
|
||||
});
|
||||
const tags = await this.tagStore.getAllTagsForFeature(featureName);
|
||||
const data = this.featureStrategyToPublic(strategy);
|
||||
const preData = this.featureStrategyToPublic(existingStrategy);
|
||||
await this.eventStore.store(
|
||||
new FeatureStrategyUpdateEvent({
|
||||
featureName,
|
||||
project: projectId,
|
||||
environment,
|
||||
createdBy: userName,
|
||||
data,
|
||||
preData,
|
||||
tags,
|
||||
}),
|
||||
);
|
||||
return data;
|
||||
}
|
||||
throw new NotFoundError(`Could not find strategy with id ${id}`);
|
||||
@ -300,23 +316,27 @@ class FeatureToggleServiceV2 {
|
||||
async deleteStrategy(
|
||||
id: string,
|
||||
context: IFeatureStrategyContext,
|
||||
userName: string,
|
||||
createdBy: string,
|
||||
): Promise<void> {
|
||||
const existingStrategy = await this.featureStrategiesStore.get(id);
|
||||
const { featureName, projectId, environment } = context;
|
||||
this.validateFeatureStrategyContext(existingStrategy, context);
|
||||
|
||||
await this.featureStrategiesStore.delete(id);
|
||||
await this.eventStore.store({
|
||||
type: FEATURE_STRATEGY_REMOVE,
|
||||
project: projectId,
|
||||
environment,
|
||||
createdBy: userName,
|
||||
data: {
|
||||
id,
|
||||
name: featureName,
|
||||
},
|
||||
});
|
||||
|
||||
const tags = await this.tagStore.getAllTagsForFeature(featureName);
|
||||
const preData = this.featureStrategyToPublic(existingStrategy);
|
||||
|
||||
await this.eventStore.store(
|
||||
new FeatureStrategyRemoveEvent({
|
||||
featureName,
|
||||
project: projectId,
|
||||
environment,
|
||||
createdBy,
|
||||
preData,
|
||||
tags,
|
||||
}),
|
||||
);
|
||||
|
||||
// If there are no strategies left for environment disable it
|
||||
await this.featureEnvironmentStore.disableEnvironmentIfNoStrategies(
|
||||
@ -419,32 +439,36 @@ class FeatureToggleServiceV2 {
|
||||
async createFeatureToggle(
|
||||
projectId: string,
|
||||
value: FeatureToggleDTO,
|
||||
userName: string,
|
||||
createdBy: string,
|
||||
): Promise<FeatureToggle> {
|
||||
this.logger.info(`${userName} creates feature toggle ${value.name}`);
|
||||
this.logger.info(`${createdBy} creates feature toggle ${value.name}`);
|
||||
await this.validateName(value.name);
|
||||
const exists = await this.projectStore.hasProject(projectId);
|
||||
if (exists) {
|
||||
const featureData = await featureMetadataSchema.validateAsync(
|
||||
value,
|
||||
);
|
||||
const featureName = featureData.name;
|
||||
const createdToggle = await this.featureToggleStore.create(
|
||||
projectId,
|
||||
featureData,
|
||||
);
|
||||
await this.featureEnvironmentStore.connectFeatureToEnvironmentsForProject(
|
||||
featureData.name,
|
||||
featureName,
|
||||
projectId,
|
||||
);
|
||||
|
||||
const data = { ...featureData, project: projectId };
|
||||
const tags = await this.tagStore.getAllTagsForFeature(featureName);
|
||||
|
||||
await this.eventStore.store({
|
||||
type: FEATURE_CREATED,
|
||||
createdBy: userName,
|
||||
project: projectId,
|
||||
data,
|
||||
});
|
||||
await this.eventStore.store(
|
||||
new FeatureCreatedEvent({
|
||||
featureName,
|
||||
createdBy,
|
||||
project: projectId,
|
||||
data: createdToggle,
|
||||
tags,
|
||||
}),
|
||||
);
|
||||
|
||||
return createdToggle;
|
||||
}
|
||||
@ -516,21 +540,24 @@ class FeatureToggleServiceV2 {
|
||||
updatedFeature,
|
||||
);
|
||||
|
||||
const preData = await this.featureToggleStore.get(featureName);
|
||||
|
||||
const featureToggle = await this.featureToggleStore.update(
|
||||
projectId,
|
||||
featureData,
|
||||
);
|
||||
const tags = await this.featureTagStore.getAllTagsForFeature(
|
||||
featureName,
|
||||
);
|
||||
const tags = await this.tagStore.getAllTagsForFeature(featureName);
|
||||
|
||||
await this.eventStore.store({
|
||||
type: FEATURE_METADATA_UPDATED,
|
||||
createdBy: userName,
|
||||
data: featureToggle,
|
||||
project: projectId,
|
||||
tags,
|
||||
});
|
||||
await this.eventStore.store(
|
||||
new FeatureMetadataUpdateEvent({
|
||||
createdBy: userName,
|
||||
data: featureToggle,
|
||||
preData,
|
||||
featureName,
|
||||
project: projectId,
|
||||
tags,
|
||||
}),
|
||||
);
|
||||
return featureToggle;
|
||||
}
|
||||
|
||||
@ -587,6 +614,7 @@ class FeatureToggleServiceV2 {
|
||||
};
|
||||
}
|
||||
|
||||
// todo: store events for this change.
|
||||
async deleteEnvironment(
|
||||
projectId: string,
|
||||
environment: string,
|
||||
@ -609,7 +637,7 @@ class FeatureToggleServiceV2 {
|
||||
}
|
||||
|
||||
async validateUniqueFeatureName(name: string): Promise<void> {
|
||||
let msg;
|
||||
let msg: string;
|
||||
try {
|
||||
const feature = await this.featureToggleStore.get(name);
|
||||
msg = feature.archived
|
||||
@ -628,46 +656,48 @@ class FeatureToggleServiceV2 {
|
||||
async updateStale(
|
||||
featureName: string,
|
||||
isStale: boolean,
|
||||
userName: string,
|
||||
createdBy: string,
|
||||
): Promise<any> {
|
||||
const feature = await this.featureToggleStore.get(featureName);
|
||||
const { project } = feature;
|
||||
feature.stale = isStale;
|
||||
await this.featureToggleStore.update(feature.project, feature);
|
||||
const tags = await this.featureTagStore.getAllTagsForFeature(
|
||||
featureName,
|
||||
);
|
||||
const data = await this.getFeatureToggleLegacy(featureName);
|
||||
await this.featureToggleStore.update(project, feature);
|
||||
const tags = await this.tagStore.getAllTagsForFeature(featureName);
|
||||
|
||||
await this.eventStore.store(
|
||||
new FeatureStaleEvent({
|
||||
stale: isStale,
|
||||
project,
|
||||
featureName,
|
||||
createdBy,
|
||||
tags,
|
||||
}),
|
||||
);
|
||||
|
||||
await this.eventStore.store({
|
||||
type: isStale ? FEATURE_STALE_ON : FEATURE_STALE_OFF,
|
||||
createdBy: userName,
|
||||
data,
|
||||
tags,
|
||||
project: feature.project,
|
||||
});
|
||||
return feature;
|
||||
}
|
||||
|
||||
async archiveToggle(name: string, userName: string): Promise<void> {
|
||||
const feature = await this.featureToggleStore.get(name);
|
||||
await this.featureToggleStore.archive(name);
|
||||
const tags =
|
||||
(await this.featureTagStore.getAllTagsForFeature(name)) || [];
|
||||
await this.eventStore.store({
|
||||
type: FEATURE_ARCHIVED,
|
||||
createdBy: userName,
|
||||
data: { name },
|
||||
project: feature.project,
|
||||
tags,
|
||||
});
|
||||
// todo: add projectId
|
||||
async archiveToggle(featureName: string, createdBy: string): Promise<void> {
|
||||
const feature = await this.featureToggleStore.get(featureName);
|
||||
await this.featureToggleStore.archive(featureName);
|
||||
const tags = await this.tagStore.getAllTagsForFeature(featureName);
|
||||
await this.eventStore.store(
|
||||
new FeatureArchivedEvent({
|
||||
featureName,
|
||||
createdBy,
|
||||
project: feature.project,
|
||||
tags,
|
||||
}),
|
||||
);
|
||||
}
|
||||
|
||||
async updateEnabled(
|
||||
projectId: string,
|
||||
project: string,
|
||||
featureName: string,
|
||||
environment: string,
|
||||
enabled: boolean,
|
||||
userName: string,
|
||||
createdBy: string,
|
||||
): Promise<FeatureToggle> {
|
||||
const hasEnvironment =
|
||||
await this.featureEnvironmentStore.featureHasEnvironment(
|
||||
@ -678,7 +708,7 @@ class FeatureToggleServiceV2 {
|
||||
if (hasEnvironment) {
|
||||
if (enabled) {
|
||||
const strategies = await this.getStrategiesForEnvironment(
|
||||
projectId,
|
||||
project,
|
||||
featureName,
|
||||
environment,
|
||||
);
|
||||
@ -697,19 +727,19 @@ class FeatureToggleServiceV2 {
|
||||
const feature = await this.featureToggleStore.get(featureName);
|
||||
|
||||
if (updatedEnvironmentStatus > 0) {
|
||||
const tags = await this.featureTagStore.getAllTagsForFeature(
|
||||
const tags = await this.tagStore.getAllTagsForFeature(
|
||||
featureName,
|
||||
);
|
||||
await this.eventStore.store({
|
||||
type: enabled
|
||||
? FEATURE_ENVIRONMENT_ENABLED
|
||||
: FEATURE_ENVIRONMENT_DISABLED,
|
||||
createdBy: userName,
|
||||
data: { name: featureName },
|
||||
tags,
|
||||
project: projectId,
|
||||
environment,
|
||||
});
|
||||
await this.eventStore.store(
|
||||
new FeatureEnvironmentEvent({
|
||||
enabled,
|
||||
project,
|
||||
featureName,
|
||||
environment,
|
||||
createdBy,
|
||||
tags,
|
||||
}),
|
||||
);
|
||||
}
|
||||
return feature;
|
||||
}
|
||||
@ -718,18 +748,20 @@ class FeatureToggleServiceV2 {
|
||||
);
|
||||
}
|
||||
|
||||
// @deprecated
|
||||
async storeFeatureUpdatedEventLegacy(
|
||||
featureName: string,
|
||||
userName: string,
|
||||
createdBy: string,
|
||||
): Promise<FeatureToggleLegacy> {
|
||||
const tags = await this.featureTagStore.getAllTagsForFeature(
|
||||
featureName,
|
||||
);
|
||||
const tags = await this.tagStore.getAllTagsForFeature(featureName);
|
||||
const feature = await this.getFeatureToggleLegacy(featureName);
|
||||
|
||||
// Legacy event. Will not be used from v4.3.
|
||||
// We do not include 'preData' on purpose.
|
||||
await this.eventStore.store({
|
||||
type: FEATURE_UPDATED,
|
||||
createdBy: userName,
|
||||
createdBy,
|
||||
featureName,
|
||||
data: feature,
|
||||
tags,
|
||||
project: feature.project,
|
||||
@ -759,6 +791,7 @@ class FeatureToggleServiceV2 {
|
||||
);
|
||||
}
|
||||
|
||||
// @deprecated
|
||||
async getFeatureToggleLegacy(
|
||||
featureName: string,
|
||||
): Promise<FeatureToggleLegacy> {
|
||||
@ -777,56 +810,57 @@ class FeatureToggleServiceV2 {
|
||||
async changeProject(
|
||||
featureName: string,
|
||||
newProject: string,
|
||||
userName: string,
|
||||
createdBy: string,
|
||||
): Promise<void> {
|
||||
const feature = await this.featureToggleStore.get(featureName);
|
||||
const oldProject = feature.project;
|
||||
feature.project = newProject;
|
||||
await this.featureToggleStore.update(newProject, feature);
|
||||
|
||||
const tags = await this.featureTagStore.getAllTagsForFeature(
|
||||
featureName,
|
||||
);
|
||||
await this.eventStore.store({
|
||||
type: FEATURE_PROJECT_CHANGE,
|
||||
createdBy: userName,
|
||||
data: {
|
||||
name: feature.name,
|
||||
const tags = await this.tagStore.getAllTagsForFeature(featureName);
|
||||
await this.eventStore.store(
|
||||
new FeatureChangeProjectEvent({
|
||||
createdBy,
|
||||
oldProject,
|
||||
newProject,
|
||||
},
|
||||
project: newProject,
|
||||
tags,
|
||||
});
|
||||
featureName,
|
||||
tags,
|
||||
}),
|
||||
);
|
||||
}
|
||||
|
||||
async getArchivedFeatures(): Promise<FeatureToggle[]> {
|
||||
return this.getFeatureToggles({}, true);
|
||||
}
|
||||
|
||||
async deleteFeature(featureName: string, userName: string): Promise<void> {
|
||||
// TODO: add project id.
|
||||
async deleteFeature(featureName: string, createdBy: string): Promise<void> {
|
||||
const toggle = await this.featureToggleStore.get(featureName);
|
||||
const tags = await this.tagStore.getAllTagsForFeature(featureName);
|
||||
await this.featureToggleStore.delete(featureName);
|
||||
await this.eventStore.store({
|
||||
type: FEATURE_DELETED,
|
||||
createdBy: userName,
|
||||
data: {
|
||||
await this.eventStore.store(
|
||||
new FeatureDeletedEvent({
|
||||
featureName,
|
||||
},
|
||||
});
|
||||
project: toggle.project,
|
||||
createdBy,
|
||||
preData: toggle,
|
||||
tags,
|
||||
}),
|
||||
);
|
||||
}
|
||||
|
||||
async reviveToggle(featureName: string, userName: string): Promise<void> {
|
||||
const data = await this.featureToggleStore.revive(featureName);
|
||||
const tags = await this.featureTagStore.getAllTagsForFeature(
|
||||
featureName,
|
||||
// TODO: add project id.
|
||||
async reviveToggle(featureName: string, createdBy: string): Promise<void> {
|
||||
const toggle = await this.featureToggleStore.revive(featureName);
|
||||
const tags = await this.tagStore.getAllTagsForFeature(featureName);
|
||||
await this.eventStore.store(
|
||||
new FeatureRevivedEvent({
|
||||
createdBy,
|
||||
featureName,
|
||||
project: toggle.project,
|
||||
tags,
|
||||
}),
|
||||
);
|
||||
await this.eventStore.store({
|
||||
type: FEATURE_REVIVED,
|
||||
createdBy: userName,
|
||||
data,
|
||||
project: data.project,
|
||||
tags,
|
||||
});
|
||||
}
|
||||
|
||||
async getMetadataForAllFeatures(
|
||||
@ -850,5 +884,4 @@ class FeatureToggleServiceV2 {
|
||||
}
|
||||
}
|
||||
|
||||
module.exports = FeatureToggleServiceV2;
|
||||
export default FeatureToggleServiceV2;
|
||||
export default FeatureToggleService;
|
||||
|
@ -11,7 +11,7 @@ import {
|
||||
import { IFeatureToggleStore } from '../types/stores/feature-toggle-store';
|
||||
import { IFeatureTypeStore } from '../types/stores/feature-type-store';
|
||||
import { IProjectStore } from '../types/stores/project-store';
|
||||
import FeatureToggleServiceV2 from './feature-toggle-service';
|
||||
import FeatureToggleService from './feature-toggle-service';
|
||||
import { hoursToMilliseconds } from 'date-fns';
|
||||
import Timer = NodeJS.Timer;
|
||||
|
||||
@ -28,7 +28,7 @@ export default class ProjectHealthService {
|
||||
|
||||
private healthRatingTimer: Timer;
|
||||
|
||||
private featureToggleService: FeatureToggleServiceV2;
|
||||
private featureToggleService: FeatureToggleService;
|
||||
|
||||
constructor(
|
||||
{
|
||||
@ -40,7 +40,7 @@ export default class ProjectHealthService {
|
||||
'projectStore' | 'featureTypeStore' | 'featureToggleStore'
|
||||
>,
|
||||
{ getLogger }: Pick<IUnleashConfig, 'getLogger'>,
|
||||
featureToggleService: FeatureToggleServiceV2,
|
||||
featureToggleService: FeatureToggleService,
|
||||
) {
|
||||
this.logger = getLogger('services/project-health-service.ts');
|
||||
this.projectStore = projectStore;
|
||||
|
@ -27,7 +27,7 @@ import { IFeatureEnvironmentStore } from '../types/stores/feature-environment-st
|
||||
import { IProjectQuery, IProjectStore } from '../types/stores/project-store';
|
||||
import { IRole } from '../types/stores/access-store';
|
||||
import { IEventStore } from '../types/stores/event-store';
|
||||
import FeatureToggleServiceV2 from './feature-toggle-service';
|
||||
import FeatureToggleService from './feature-toggle-service';
|
||||
import { CREATE_FEATURE, UPDATE_FEATURE } from '../types/permissions';
|
||||
import NoAccessError from '../error/no-access-error';
|
||||
import IncompatibleProjectError from '../error/incompatible-project-error';
|
||||
@ -58,7 +58,7 @@ export default class ProjectService {
|
||||
|
||||
private logger: any;
|
||||
|
||||
private featureToggleService: FeatureToggleServiceV2;
|
||||
private featureToggleService: FeatureToggleService;
|
||||
|
||||
private environmentsEnabled: boolean = false;
|
||||
|
||||
@ -81,7 +81,7 @@ export default class ProjectService {
|
||||
>,
|
||||
config: IUnleashConfig,
|
||||
accessService: AccessService,
|
||||
featureToggleService: FeatureToggleServiceV2,
|
||||
featureToggleService: FeatureToggleService,
|
||||
) {
|
||||
this.store = projectStore;
|
||||
this.environmentStore = environmentStore;
|
||||
@ -161,16 +161,17 @@ export default class ProjectService {
|
||||
}
|
||||
|
||||
async updateProject(updatedProject: IProject, user: User): Promise<void> {
|
||||
await this.store.get(updatedProject.id);
|
||||
const preData = await this.store.get(updatedProject.id);
|
||||
const project = await projectSchema.validateAsync(updatedProject);
|
||||
|
||||
await this.store.update(project);
|
||||
|
||||
await this.eventStore.store({
|
||||
type: PROJECT_UPDATED,
|
||||
project: project.id,
|
||||
createdBy: getCreatedBy(user),
|
||||
data: project,
|
||||
project: project.id,
|
||||
preData,
|
||||
});
|
||||
}
|
||||
|
||||
@ -258,7 +259,6 @@ export default class ProjectService {
|
||||
type: PROJECT_DELETED,
|
||||
createdBy: getCreatedBy(user),
|
||||
project: id,
|
||||
data: { id },
|
||||
});
|
||||
|
||||
await this.accessService.removeDefaultProjectRoles(user, id);
|
||||
@ -289,6 +289,7 @@ export default class ProjectService {
|
||||
};
|
||||
}
|
||||
|
||||
// TODO: should be an event too
|
||||
async addUser(
|
||||
projectId: string,
|
||||
roleId: number,
|
||||
@ -313,6 +314,7 @@ export default class ProjectService {
|
||||
await this.accessService.addUserToRole(userId, role.id);
|
||||
}
|
||||
|
||||
// TODO: should be an event too
|
||||
async removeUser(
|
||||
projectId: string,
|
||||
roleId: number,
|
||||
|
@ -206,26 +206,29 @@ class UserService {
|
||||
await this.store.setPasswordHash(user.id, passwordHash);
|
||||
}
|
||||
|
||||
await this.updateChangeLog(USER_CREATED, user, updatedBy);
|
||||
await this.eventStore.store({
|
||||
type: USER_CREATED,
|
||||
createdBy: this.getCreatedBy(updatedBy),
|
||||
data: this.mapUserToData(user),
|
||||
});
|
||||
|
||||
return user;
|
||||
}
|
||||
|
||||
private async updateChangeLog(
|
||||
type: string,
|
||||
user: IUser,
|
||||
updatedBy: User = systemUser,
|
||||
): Promise<void> {
|
||||
await this.eventStore.store({
|
||||
type,
|
||||
createdBy: updatedBy.username || updatedBy.email,
|
||||
data: {
|
||||
id: user.id,
|
||||
name: user.name,
|
||||
username: user.username,
|
||||
email: user.email,
|
||||
},
|
||||
});
|
||||
private getCreatedBy(updatedBy: User = systemUser) {
|
||||
return updatedBy.username || updatedBy.email;
|
||||
}
|
||||
|
||||
private mapUserToData(user?: IUser): any {
|
||||
if (!user) {
|
||||
return undefined;
|
||||
}
|
||||
return {
|
||||
id: user.id,
|
||||
name: user.name,
|
||||
username: user.username,
|
||||
email: user.email,
|
||||
};
|
||||
}
|
||||
|
||||
async updateUser(
|
||||
@ -236,17 +239,43 @@ class UserService {
|
||||
Joi.assert(email, Joi.string().email(), 'Email');
|
||||
}
|
||||
|
||||
const preUser = await this.store.get(id);
|
||||
|
||||
if (rootRole) {
|
||||
await this.accessService.setUserRootRole(id, rootRole);
|
||||
}
|
||||
|
||||
const user = await this.store.update(id, { name, email });
|
||||
|
||||
await this.updateChangeLog(USER_UPDATED, user, updatedBy);
|
||||
await this.eventStore.store({
|
||||
type: USER_UPDATED,
|
||||
createdBy: this.getCreatedBy(updatedBy),
|
||||
data: this.mapUserToData(user),
|
||||
preData: this.mapUserToData(preUser),
|
||||
});
|
||||
|
||||
return user;
|
||||
}
|
||||
|
||||
async deleteUser(userId: number, updatedBy?: User): Promise<void> {
|
||||
const user = await this.store.get(userId);
|
||||
const roles = await this.accessService.getRolesForUser(userId);
|
||||
await Promise.all(
|
||||
roles.map((role) =>
|
||||
this.accessService.removeUserFromRole(userId, role.id),
|
||||
),
|
||||
);
|
||||
await this.sessionService.deleteSessionsForUser(userId);
|
||||
|
||||
await this.store.delete(userId);
|
||||
|
||||
await this.eventStore.store({
|
||||
type: USER_DELETED,
|
||||
createdBy: this.getCreatedBy(updatedBy),
|
||||
preData: this.mapUserToData(user),
|
||||
});
|
||||
}
|
||||
|
||||
async loginUser(usernameOrEmail: string, password: string): Promise<IUser> {
|
||||
const settings = await this.settingService.get<SimpleAuthSettings>(
|
||||
simpleAuthKey,
|
||||
@ -323,21 +352,6 @@ class UserService {
|
||||
return this.store.setPasswordHash(userId, passwordHash);
|
||||
}
|
||||
|
||||
async deleteUser(userId: number, updatedBy?: User): Promise<void> {
|
||||
const user = await this.store.get(userId);
|
||||
const roles = await this.accessService.getRolesForUser(userId);
|
||||
await Promise.all(
|
||||
roles.map((role) =>
|
||||
this.accessService.removeUserFromRole(userId, role.id),
|
||||
),
|
||||
);
|
||||
await this.sessionService.deleteSessionsForUser(userId);
|
||||
|
||||
await this.store.delete(userId);
|
||||
|
||||
await this.updateChangeLog(USER_DELETED, user, updatedBy);
|
||||
}
|
||||
|
||||
async getUserForToken(token: string): Promise<ITokenUser> {
|
||||
const { createdBy, userId } = await this.resetTokenService.isValid(
|
||||
token,
|
||||
|
@ -1,4 +1,8 @@
|
||||
import { FeatureToggle, IStrategyConfig, ITag } from './model';
|
||||
|
||||
export const APPLICATION_CREATED = 'application-created';
|
||||
|
||||
// feature event types
|
||||
export const FEATURE_CREATED = 'feature-created';
|
||||
export const FEATURE_DELETED = 'feature-deleted';
|
||||
export const FEATURE_UPDATED = 'feature-updated';
|
||||
@ -17,6 +21,9 @@ export const FEATURE_UNTAGGED = 'feature-untagged';
|
||||
export const FEATURE_STALE_ON = 'feature-stale-on';
|
||||
export const FEATURE_STALE_OFF = 'feature-stale-off';
|
||||
export const DROP_FEATURES = 'drop-features';
|
||||
export const FEATURE_ENVIRONMENT_ENABLED = 'feature-environment-enabled';
|
||||
export const FEATURE_ENVIRONMENT_DISABLED = 'feature-environment-disabled';
|
||||
|
||||
export const STRATEGY_CREATED = 'strategy-created';
|
||||
export const STRATEGY_DELETED = 'strategy-deleted';
|
||||
export const STRATEGY_DEPRECATED = 'strategy-deprecated';
|
||||
@ -50,5 +57,296 @@ export const USER_UPDATED = 'user-updated';
|
||||
export const USER_DELETED = 'user-deleted';
|
||||
export const DROP_ENVIRONMENTS = 'drop-environments';
|
||||
export const ENVIRONMENT_IMPORT = 'environment-import';
|
||||
export const FEATURE_ENVIRONMENT_ENABLED = 'feature-environment-enabled';
|
||||
export const FEATURE_ENVIRONMENT_DISABLED = 'feature-environment-disabled';
|
||||
|
||||
export interface IBaseEvent {
|
||||
type: string;
|
||||
createdBy: string;
|
||||
project?: string;
|
||||
environment?: string;
|
||||
featureName?: string;
|
||||
data?: any;
|
||||
preData?: any;
|
||||
tags?: ITag[];
|
||||
}
|
||||
|
||||
export interface IEvent extends IBaseEvent {
|
||||
id: number;
|
||||
createdAt: Date;
|
||||
}
|
||||
|
||||
class BaseEvent implements IBaseEvent {
|
||||
readonly type: string;
|
||||
|
||||
readonly createdBy: string;
|
||||
|
||||
readonly tags: ITag[];
|
||||
|
||||
constructor(type: string, createdBy: string, tags: ITag[] = []) {
|
||||
this.type = type;
|
||||
this.createdBy = createdBy;
|
||||
this.tags = tags;
|
||||
}
|
||||
}
|
||||
|
||||
export class FeatureStaleEvent extends BaseEvent {
|
||||
readonly project: string;
|
||||
|
||||
readonly featureName: string;
|
||||
|
||||
constructor(p: {
|
||||
stale: boolean;
|
||||
project: string;
|
||||
featureName: string;
|
||||
createdBy: string;
|
||||
tags: ITag[];
|
||||
}) {
|
||||
super(
|
||||
p.stale ? FEATURE_STALE_ON : FEATURE_STALE_OFF,
|
||||
p.createdBy,
|
||||
p.tags,
|
||||
);
|
||||
this.project = p.project;
|
||||
this.featureName = p.featureName;
|
||||
}
|
||||
}
|
||||
|
||||
export class FeatureEnvironmentEvent extends BaseEvent {
|
||||
readonly project: string;
|
||||
|
||||
readonly featureName: string;
|
||||
|
||||
readonly environment: string;
|
||||
|
||||
constructor(p: {
|
||||
enabled: boolean;
|
||||
project: string;
|
||||
featureName: string;
|
||||
environment: string;
|
||||
createdBy: string;
|
||||
tags: ITag[];
|
||||
}) {
|
||||
super(
|
||||
p.enabled
|
||||
? FEATURE_ENVIRONMENT_ENABLED
|
||||
: FEATURE_ENVIRONMENT_DISABLED,
|
||||
p.createdBy,
|
||||
p.tags,
|
||||
);
|
||||
this.project = p.project;
|
||||
this.featureName = p.featureName;
|
||||
this.environment = p.environment;
|
||||
}
|
||||
}
|
||||
|
||||
export class FeatureChangeProjectEvent extends BaseEvent {
|
||||
readonly project: string;
|
||||
|
||||
readonly featureName: string;
|
||||
|
||||
readonly data: {
|
||||
oldProject: string;
|
||||
newProject: string;
|
||||
};
|
||||
|
||||
constructor(p: {
|
||||
oldProject: string;
|
||||
newProject: string;
|
||||
featureName: string;
|
||||
createdBy: string;
|
||||
tags: ITag[];
|
||||
}) {
|
||||
super(FEATURE_PROJECT_CHANGE, p.createdBy, p.tags);
|
||||
const { newProject, oldProject, featureName } = p;
|
||||
this.project = newProject;
|
||||
this.featureName = featureName;
|
||||
this.data = { newProject, oldProject };
|
||||
}
|
||||
}
|
||||
|
||||
export class FeatureCreatedEvent extends BaseEvent {
|
||||
readonly project: string;
|
||||
|
||||
readonly featureName: string;
|
||||
|
||||
readonly data: FeatureToggle;
|
||||
|
||||
constructor(p: {
|
||||
project: string;
|
||||
featureName: string;
|
||||
createdBy: string;
|
||||
data: FeatureToggle;
|
||||
tags: ITag[];
|
||||
}) {
|
||||
super(FEATURE_CREATED, p.createdBy, p.tags);
|
||||
const { project, featureName, data } = p;
|
||||
this.project = project;
|
||||
this.featureName = featureName;
|
||||
this.data = data;
|
||||
}
|
||||
}
|
||||
|
||||
export class FeatureArchivedEvent extends BaseEvent {
|
||||
readonly project: string;
|
||||
|
||||
readonly featureName: string;
|
||||
|
||||
constructor(p: {
|
||||
project: string;
|
||||
featureName: string;
|
||||
createdBy: string;
|
||||
tags: ITag[];
|
||||
}) {
|
||||
super(FEATURE_ARCHIVED, p.createdBy, p.tags);
|
||||
const { project, featureName } = p;
|
||||
this.project = project;
|
||||
this.featureName = featureName;
|
||||
}
|
||||
}
|
||||
|
||||
export class FeatureRevivedEvent extends BaseEvent {
|
||||
readonly project: string;
|
||||
|
||||
readonly featureName: string;
|
||||
|
||||
constructor(p: {
|
||||
project: string;
|
||||
featureName: string;
|
||||
createdBy: string;
|
||||
tags: ITag[];
|
||||
}) {
|
||||
super(FEATURE_REVIVED, p.createdBy, p.tags);
|
||||
const { project, featureName } = p;
|
||||
this.project = project;
|
||||
this.featureName = featureName;
|
||||
}
|
||||
}
|
||||
|
||||
export class FeatureDeletedEvent extends BaseEvent {
|
||||
readonly project: string;
|
||||
|
||||
readonly featureName: string;
|
||||
|
||||
readonly preData: FeatureToggle;
|
||||
|
||||
constructor(p: {
|
||||
project: string;
|
||||
featureName: string;
|
||||
preData: FeatureToggle;
|
||||
createdBy: string;
|
||||
tags: ITag[];
|
||||
}) {
|
||||
super(FEATURE_DELETED, p.createdBy, p.tags);
|
||||
const { project, featureName, preData } = p;
|
||||
this.project = project;
|
||||
this.featureName = featureName;
|
||||
this.preData = preData;
|
||||
}
|
||||
}
|
||||
|
||||
export class FeatureMetadataUpdateEvent extends BaseEvent {
|
||||
readonly project: string;
|
||||
|
||||
readonly featureName: string;
|
||||
|
||||
readonly data: FeatureToggle;
|
||||
|
||||
readonly preData: FeatureToggle;
|
||||
|
||||
constructor(p: {
|
||||
featureName: string;
|
||||
createdBy: string;
|
||||
project: string;
|
||||
data: FeatureToggle;
|
||||
preData: FeatureToggle;
|
||||
tags: ITag[];
|
||||
}) {
|
||||
super(FEATURE_METADATA_UPDATED, p.createdBy, p.tags);
|
||||
const { project, featureName, data, preData } = p;
|
||||
this.project = project;
|
||||
this.featureName = featureName;
|
||||
this.data = data;
|
||||
this.preData = preData;
|
||||
}
|
||||
}
|
||||
|
||||
export class FeatureStrategyAddEvent extends BaseEvent {
|
||||
readonly project: string;
|
||||
|
||||
readonly featureName: string;
|
||||
|
||||
readonly environment: string;
|
||||
|
||||
readonly data: IStrategyConfig;
|
||||
|
||||
constructor(p: {
|
||||
project: string;
|
||||
featureName: string;
|
||||
environment: string;
|
||||
createdBy: string;
|
||||
data: IStrategyConfig;
|
||||
tags: ITag[];
|
||||
}) {
|
||||
super(FEATURE_STRATEGY_ADD, p.createdBy, p.tags);
|
||||
const { project, featureName, environment, data } = p;
|
||||
this.project = project;
|
||||
this.featureName = featureName;
|
||||
this.environment = environment;
|
||||
this.data = data;
|
||||
}
|
||||
}
|
||||
|
||||
export class FeatureStrategyUpdateEvent extends BaseEvent {
|
||||
readonly project: string;
|
||||
|
||||
readonly featureName: string;
|
||||
|
||||
readonly environment: string;
|
||||
|
||||
readonly data: IStrategyConfig;
|
||||
|
||||
readonly preData: IStrategyConfig;
|
||||
|
||||
constructor(p: {
|
||||
project: string;
|
||||
featureName: string;
|
||||
environment: string;
|
||||
createdBy: string;
|
||||
data: IStrategyConfig;
|
||||
preData: IStrategyConfig;
|
||||
tags: ITag[];
|
||||
}) {
|
||||
super(FEATURE_STRATEGY_UPDATE, p.createdBy, p.tags);
|
||||
const { project, featureName, environment, data, preData } = p;
|
||||
this.project = project;
|
||||
this.featureName = featureName;
|
||||
this.environment = environment;
|
||||
this.data = data;
|
||||
this.preData = preData;
|
||||
}
|
||||
}
|
||||
|
||||
export class FeatureStrategyRemoveEvent extends BaseEvent {
|
||||
readonly project: string;
|
||||
|
||||
readonly featureName: string;
|
||||
|
||||
readonly environment: string;
|
||||
|
||||
readonly preData: IStrategyConfig;
|
||||
|
||||
constructor(p: {
|
||||
project: string;
|
||||
featureName: string;
|
||||
environment: string;
|
||||
createdBy: string;
|
||||
preData: IStrategyConfig;
|
||||
tags: ITag[];
|
||||
}) {
|
||||
super(FEATURE_STRATEGY_REMOVE, p.createdBy, p.tags);
|
||||
const { project, featureName, environment, preData } = p;
|
||||
this.project = project;
|
||||
this.featureName = featureName;
|
||||
this.environment = environment;
|
||||
this.preData = preData;
|
||||
}
|
||||
}
|
||||
|
@ -196,20 +196,6 @@ export interface IAddonConfig {
|
||||
unleashUrl: string;
|
||||
}
|
||||
|
||||
export interface ICreateEvent {
|
||||
type: string;
|
||||
createdBy: string;
|
||||
project?: string;
|
||||
environment?: string;
|
||||
data?: any;
|
||||
tags?: ITag[];
|
||||
}
|
||||
|
||||
export interface IEvent extends ICreateEvent {
|
||||
id: number;
|
||||
createdAt: Date;
|
||||
}
|
||||
|
||||
export interface IUserWithRole {
|
||||
id: number;
|
||||
roleId: number;
|
||||
|
@ -1,11 +1,12 @@
|
||||
import EventEmitter from 'events';
|
||||
import { ICreateEvent, IEvent } from '../model';
|
||||
import { IBaseEvent, IEvent } from '../events';
|
||||
import { Store } from './store';
|
||||
|
||||
export interface IEventStore extends Store<IEvent, number>, EventEmitter {
|
||||
store(event: ICreateEvent): Promise<void>;
|
||||
batchStore(events: ICreateEvent[]): Promise<void>;
|
||||
store(event: IBaseEvent): Promise<void>;
|
||||
batchStore(events: IBaseEvent[]): Promise<void>;
|
||||
getEvents(): Promise<IEvent[]>;
|
||||
getEventsFilterByType(name: string): Promise<IEvent[]>;
|
||||
getEventsForFeature(featureName: string): Promise<IEvent[]>;
|
||||
getEventsFilterByProject(project: string): Promise<IEvent[]>;
|
||||
}
|
||||
|
@ -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,
|
||||
);
|
||||
};
|
@ -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);
|
||||
};
|
@ -556,9 +556,8 @@ test('Patching feature toggles to stale should trigger FEATURE_STALE_ON event',
|
||||
const events = await db.stores.eventStore.getAll({
|
||||
type: FEATURE_STALE_ON,
|
||||
});
|
||||
const updateForOurToggle = events.find((e) => e.data.name === name);
|
||||
const updateForOurToggle = events.find((e) => e.featureName === name);
|
||||
expect(updateForOurToggle).toBeTruthy();
|
||||
expect(updateForOurToggle.data.stale).toBe(true);
|
||||
});
|
||||
|
||||
test('Patching feature toggles to active (turning stale to false) should trigger FEATURE_STALE_OFF event', async () => {
|
||||
@ -581,9 +580,8 @@ test('Patching feature toggles to active (turning stale to false) should trigger
|
||||
const events = await db.stores.eventStore.getAll({
|
||||
type: FEATURE_STALE_OFF,
|
||||
});
|
||||
const updateForOurToggle = events.find((e) => e.data.name === name);
|
||||
const updateForOurToggle = events.find((e) => e.featureName === name);
|
||||
expect(updateForOurToggle).toBeTruthy();
|
||||
expect(updateForOurToggle.data.stale).toBe(false);
|
||||
});
|
||||
|
||||
test('Should archive feature toggle', async () => {
|
||||
@ -1149,9 +1147,9 @@ test('Deleting a strategy should include name of feature strategy was deleted fr
|
||||
type: FEATURE_STRATEGY_REMOVE,
|
||||
});
|
||||
expect(events).toHaveLength(1);
|
||||
expect(events[0].data.name).toBe(featureName);
|
||||
expect(events[0].featureName).toBe(featureName);
|
||||
expect(events[0].environment).toBe(environment);
|
||||
expect(events[0].data.id).toBe(strategyId);
|
||||
expect(events[0].preData.id).toBe(strategyId);
|
||||
});
|
||||
|
||||
test('Enabling environment creates a FEATURE_ENVIRONMENT_ENABLED event', async () => {
|
||||
@ -1193,7 +1191,7 @@ test('Enabling environment creates a FEATURE_ENVIRONMENT_ENABLED event', async (
|
||||
const events = await db.stores.eventStore.getAll({
|
||||
type: FEATURE_ENVIRONMENT_ENABLED,
|
||||
});
|
||||
const enabledEvents = events.filter((e) => e.data.name === featureName);
|
||||
const enabledEvents = events.filter((e) => e.featureName === featureName);
|
||||
expect(enabledEvents).toHaveLength(1);
|
||||
});
|
||||
test('Disabling environment creates a FEATURE_ENVIRONMENT_DISABLED event', async () => {
|
||||
@ -1243,7 +1241,7 @@ test('Disabling environment creates a FEATURE_ENVIRONMENT_DISABLED event', async
|
||||
const events = await db.stores.eventStore.getAll({
|
||||
type: FEATURE_ENVIRONMENT_DISABLED,
|
||||
});
|
||||
const ourFeatureEvent = events.find((e) => e.data.name === featureName);
|
||||
const ourFeatureEvent = events.find((e) => e.featureName === featureName);
|
||||
expect(ourFeatureEvent).toBeTruthy();
|
||||
});
|
||||
|
||||
|
@ -44,8 +44,6 @@ afterEach(async () => {
|
||||
});
|
||||
|
||||
test('returns empty list of users', async () => {
|
||||
expect.assertions(1);
|
||||
|
||||
return app.request
|
||||
.get('/api/admin/user-admin')
|
||||
.expect('Content-Type', /json/)
|
||||
@ -56,8 +54,6 @@ test('returns empty list of users', async () => {
|
||||
});
|
||||
|
||||
test('creates and returns all users', async () => {
|
||||
expect.assertions(2);
|
||||
|
||||
const createUserRequests = [...Array(20).keys()].map((i) =>
|
||||
app.request
|
||||
.post('/api/admin/user-admin')
|
||||
@ -82,8 +78,6 @@ test('creates and returns all users', async () => {
|
||||
});
|
||||
|
||||
test('creates editor-user without password', async () => {
|
||||
expect.assertions(3);
|
||||
|
||||
return app.request
|
||||
.post('/api/admin/user-admin')
|
||||
.send({
|
||||
@ -101,8 +95,6 @@ test('creates editor-user without password', async () => {
|
||||
});
|
||||
|
||||
test('creates admin-user with password', async () => {
|
||||
expect.assertions(6);
|
||||
|
||||
const { body } = await app.request
|
||||
.post('/api/admin/user-admin')
|
||||
.send({
|
||||
@ -129,8 +121,6 @@ test('creates admin-user with password', async () => {
|
||||
});
|
||||
|
||||
test('requires known root role', async () => {
|
||||
expect.assertions(0);
|
||||
|
||||
return app.request
|
||||
.post('/api/admin/user-admin')
|
||||
.send({
|
||||
@ -186,16 +176,12 @@ test('get a single user', async () => {
|
||||
});
|
||||
|
||||
test('should delete user', async () => {
|
||||
expect.assertions(0);
|
||||
|
||||
const user = await userStore.insert({ email: 'some@mail.com' });
|
||||
|
||||
return app.request.delete(`/api/admin/user-admin/${user.id}`).expect(200);
|
||||
});
|
||||
|
||||
test('validator should require strong password', async () => {
|
||||
expect.assertions(0);
|
||||
|
||||
return app.request
|
||||
.post('/api/admin/user-admin/validate-password')
|
||||
.send({ password: 'simple' })
|
||||
@ -203,8 +189,6 @@ test('validator should require strong password', async () => {
|
||||
});
|
||||
|
||||
test('validator should accept strong password', async () => {
|
||||
expect.assertions(0);
|
||||
|
||||
return app.request
|
||||
.post('/api/admin/user-admin/validate-password')
|
||||
.send({ password: 'simple123-_ASsad' })
|
||||
@ -212,8 +196,6 @@ test('validator should accept strong password', async () => {
|
||||
});
|
||||
|
||||
test('should change password', async () => {
|
||||
expect.assertions(0);
|
||||
|
||||
const user = await userStore.insert({ email: 'some@mail.com' });
|
||||
|
||||
return app.request
|
||||
@ -223,8 +205,6 @@ test('should change password', async () => {
|
||||
});
|
||||
|
||||
test('should search for users', async () => {
|
||||
expect.assertions(2);
|
||||
|
||||
await userStore.insert({ email: 'some@mail.com' });
|
||||
await userStore.insert({ email: 'another@mail.com' });
|
||||
await userStore.insert({ email: 'another2@mail.com' });
|
||||
@ -241,8 +221,6 @@ test('should search for users', async () => {
|
||||
});
|
||||
|
||||
test('Creates a user and includes inviteLink and emailConfigured', async () => {
|
||||
expect.assertions(5);
|
||||
|
||||
return app.request
|
||||
.post('/api/admin/user-admin')
|
||||
.send({
|
||||
@ -300,7 +278,6 @@ test('Creates a user but does not send email if sendEmail is set to false', asyn
|
||||
});
|
||||
|
||||
test('generates USER_CREATED event', async () => {
|
||||
expect.assertions(5);
|
||||
const email = 'some@getunelash.ai';
|
||||
const name = 'Some Name';
|
||||
|
||||
@ -325,20 +302,16 @@ test('generates USER_CREATED event', async () => {
|
||||
});
|
||||
|
||||
test('generates USER_DELETED event', async () => {
|
||||
expect.assertions(3);
|
||||
|
||||
const user = await userStore.insert({ email: 'some@mail.com' });
|
||||
await app.request.delete(`/api/admin/user-admin/${user.id}`);
|
||||
|
||||
const events = await eventStore.getEvents();
|
||||
expect(events[0].type).toBe(USER_DELETED);
|
||||
expect(events[0].data.id).toBe(user.id);
|
||||
expect(events[0].data.email).toBe(user.email);
|
||||
expect(events[0].preData.id).toBe(user.id);
|
||||
expect(events[0].preData.email).toBe(user.email);
|
||||
});
|
||||
|
||||
test('generates USER_UPDATED event', async () => {
|
||||
expect.assertions(3);
|
||||
|
||||
const { body } = await app.request
|
||||
.post('/api/admin/user-admin')
|
||||
.send({
|
||||
|
@ -1,4 +1,4 @@
|
||||
import FeatureToggleServiceV2 from '../../../lib/services/feature-toggle-service';
|
||||
import FeatureToggleService from '../../../lib/services/feature-toggle-service';
|
||||
import { IStrategyConfig } from '../../../lib/types/model';
|
||||
import { createTestConfig } from '../../config/test-config';
|
||||
import dbInit from '../helpers/database-init';
|
||||
@ -6,7 +6,7 @@ import { DEFAULT_ENV } from '../../../lib/util/constants';
|
||||
|
||||
let stores;
|
||||
let db;
|
||||
let service: FeatureToggleServiceV2;
|
||||
let service: FeatureToggleService;
|
||||
|
||||
beforeAll(async () => {
|
||||
const config = createTestConfig();
|
||||
@ -15,7 +15,7 @@ beforeAll(async () => {
|
||||
config.getLogger,
|
||||
);
|
||||
stores = db.stores;
|
||||
service = new FeatureToggleServiceV2(stores, config);
|
||||
service = new FeatureToggleService(stores, config);
|
||||
});
|
||||
|
||||
afterAll(async () => {
|
||||
|
@ -1,6 +1,6 @@
|
||||
import dbInit, { ITestDb } from '../helpers/database-init';
|
||||
import getLogger from '../../fixtures/no-logger';
|
||||
import FeatureToggleServiceV2 from '../../../lib/services/feature-toggle-service';
|
||||
import FeatureToggleService from '../../../lib/services/feature-toggle-service';
|
||||
import { AccessService } from '../../../lib/services/access-service';
|
||||
import ProjectService from '../../../lib/services/project-service';
|
||||
import ProjectHealthService from '../../../lib/services/project-health-service';
|
||||
@ -25,7 +25,7 @@ beforeAll(async () => {
|
||||
email: 'test@getunleash.io',
|
||||
});
|
||||
accessService = new AccessService(stores, config);
|
||||
featureToggleService = new FeatureToggleServiceV2(stores, config);
|
||||
featureToggleService = new FeatureToggleService(stores, config);
|
||||
projectService = new ProjectService(
|
||||
stores,
|
||||
config,
|
||||
|
@ -1,6 +1,6 @@
|
||||
import dbInit, { ITestDb } from '../helpers/database-init';
|
||||
import getLogger from '../../fixtures/no-logger';
|
||||
import FeatureToggleServiceV2 from '../../../lib/services/feature-toggle-service';
|
||||
import FeatureToggleService from '../../../lib/services/feature-toggle-service';
|
||||
import ProjectService from '../../../lib/services/project-service';
|
||||
import { AccessService } from '../../../lib/services/access-service';
|
||||
import {
|
||||
@ -17,7 +17,7 @@ let db: ITestDb;
|
||||
|
||||
let projectService;
|
||||
let accessService;
|
||||
let featureToggleService: FeatureToggleServiceV2;
|
||||
let featureToggleService: FeatureToggleService;
|
||||
let user;
|
||||
|
||||
beforeAll(async () => {
|
||||
@ -33,7 +33,7 @@ beforeAll(async () => {
|
||||
experimental: { environments: { enabled: true } },
|
||||
});
|
||||
accessService = new AccessService(stores, config);
|
||||
featureToggleService = new FeatureToggleServiceV2(stores, config);
|
||||
featureToggleService = new FeatureToggleService(stores, config);
|
||||
projectService = new ProjectService(
|
||||
stores,
|
||||
config,
|
||||
|
@ -1,11 +1,11 @@
|
||||
import {
|
||||
APPLICATION_CREATED,
|
||||
FEATURE_CREATED,
|
||||
IEvent,
|
||||
} from '../../../lib/types/events';
|
||||
|
||||
import dbInit from '../helpers/database-init';
|
||||
import getLogger from '../../fixtures/no-logger';
|
||||
import { IEvent } from '../../../lib/types/model';
|
||||
import { IEventStore } from '../../../lib/types/stores/event-store';
|
||||
import { IUnleashStores } from '../../../lib/types';
|
||||
|
||||
|
6
src/test/fixtures/fake-event-store.ts
vendored
6
src/test/fixtures/fake-event-store.ts
vendored
@ -1,6 +1,6 @@
|
||||
import EventEmitter from 'events';
|
||||
import { IEventStore } from '../../lib/types/stores/event-store';
|
||||
import { IEvent } from '../../lib/types/model';
|
||||
import { IEvent } from '../../lib/types/events';
|
||||
|
||||
class FakeEventStore extends EventEmitter implements IEventStore {
|
||||
events: IEvent[];
|
||||
@ -11,6 +11,10 @@ class FakeEventStore extends EventEmitter implements IEventStore {
|
||||
this.events = [];
|
||||
}
|
||||
|
||||
async getEventsForFeature(featureName: string): Promise<IEvent[]> {
|
||||
return this.events.filter((e) => e.featureName === featureName);
|
||||
}
|
||||
|
||||
store(event: IEvent): Promise<void> {
|
||||
this.events.push(event);
|
||||
this.emit(event.type, event);
|
||||
|
@ -13,14 +13,23 @@ The Datadog addon will perform a single retry if the HTTP POST against the Datad
|
||||
|
||||
#### Events {#events}
|
||||
|
||||
You can choose to trigger updates for the following events (we might add more event types in the future):
|
||||
You can choose to trigger updates for the following events:
|
||||
|
||||
- feature-created
|
||||
- feature-updated
|
||||
- feature-updated (*)
|
||||
- feature-metadata-updated
|
||||
- feature-project-change
|
||||
- feature-archived
|
||||
- feature-revived
|
||||
- feature-strategy-update
|
||||
- feature-strategy-add
|
||||
- feature-strategy-remove
|
||||
- feature-stale-on
|
||||
- feature-stale-off
|
||||
- feature-environment-enabled
|
||||
- feature-environment-disabled
|
||||
|
||||
> *) Deprecated, and will not be used after transition to environments in Unleash v4.3
|
||||
|
||||
#### Parameters {#parameters}
|
||||
|
||||
|
@ -13,14 +13,23 @@ The Slack addon will perform a single retry if the HTTP POST against the Slack W
|
||||
|
||||
#### Events {#events}
|
||||
|
||||
You can choose to trigger updates for the following events (we might add more event types in the future):
|
||||
You can choose to trigger updates for the following events:
|
||||
|
||||
- feature-created
|
||||
- feature-updated
|
||||
- feature-updated (*)
|
||||
- feature-metadata-updated
|
||||
- feature-project-change
|
||||
- feature-archived
|
||||
- feature-revived
|
||||
- feature-strategy-update
|
||||
- feature-strategy-add
|
||||
- feature-strategy-remove
|
||||
- feature-stale-on
|
||||
- feature-stale-off
|
||||
- feature-environment-enabled
|
||||
- feature-environment-disabled
|
||||
|
||||
> *) Deprecated, and will not be used after transition to environments in Unleash v4.3
|
||||
|
||||
#### Parameters {#parameters}
|
||||
|
||||
|
@ -13,14 +13,23 @@ The Microsoft Teams addon will perform a single retry if the HTTP POST against t
|
||||
|
||||
#### Events {#events}
|
||||
|
||||
You can choose to trigger updates for the following events (we might add more event types in the future):
|
||||
You can choose to trigger updates for the following events:
|
||||
|
||||
- feature-created
|
||||
- feature-updated
|
||||
- feature-updated (*)
|
||||
- feature-metadata-updated
|
||||
- feature-project-change
|
||||
- feature-archived
|
||||
- feature-revived
|
||||
- feature-strategy-update
|
||||
- feature-strategy-add
|
||||
- feature-strategy-remove
|
||||
- feature-stale-on
|
||||
- feature-stale-off
|
||||
- feature-environment-enabled
|
||||
- feature-environment-disabled
|
||||
|
||||
> *) Deprecated, and will not be used after transition to environments in Unleash v4.3
|
||||
|
||||
#### Parameters {#parameters}
|
||||
|
||||
|
@ -16,13 +16,20 @@ The webhook will perform a single retry if the HTTP POST call fails (either a 50
|
||||
You can choose to trigger updates for the following events (we might add more event types in the future):
|
||||
|
||||
- feature-created
|
||||
- feature-updated
|
||||
- feature-updated (*)
|
||||
- feature-metadata-updated
|
||||
- feature-project-change
|
||||
- feature-archived
|
||||
- feature-revived
|
||||
- feature-strategy-update
|
||||
- feature-strategy-add
|
||||
- feature-strategy-remove
|
||||
- feature-stale-on
|
||||
- feature-stale-off
|
||||
- feature-environment-enabled
|
||||
- feature-environment-disabled
|
||||
|
||||
(we will add more events in the future!)
|
||||
> *) Deprecated, and will not be used after transition to environments in Unleash v4.3
|
||||
|
||||
#### Parameters {#parameters}
|
||||
|
||||
|
@ -13,39 +13,208 @@ Used to fetch all changes in the unleash system.
|
||||
|
||||
Defined event types:
|
||||
|
||||
### Feature Toggle events:
|
||||
|
||||
- feature-created
|
||||
- feature-deleted
|
||||
- feature-updated
|
||||
- feature-metadata-updated
|
||||
- feature-project-change
|
||||
- feature-archived
|
||||
- feature-revived
|
||||
- feature-import
|
||||
- feature-tagged
|
||||
- feature-tag-import
|
||||
- feature-strategy-update
|
||||
- feature-strategy-add
|
||||
- feature-strategy-remove
|
||||
- drop-feature-tags
|
||||
- feature-untagged
|
||||
- feature-stale-on
|
||||
- feature-stale-off
|
||||
- drop-features
|
||||
- feature-environment-enabled
|
||||
- feature-environment-disabled
|
||||
|
||||
### Strategy Events
|
||||
|
||||
- strategy-created
|
||||
- strategy-deleted
|
||||
- strategy-deprecated
|
||||
- strategy-reactivated
|
||||
- strategy-updated
|
||||
- strategy-import
|
||||
- drop-strategies
|
||||
|
||||
### Context field events
|
||||
|
||||
- context-field-created
|
||||
- context-field-updated
|
||||
- context-field-deleted
|
||||
|
||||
### Project events
|
||||
|
||||
- project-created
|
||||
- project-updated
|
||||
- project-deleted
|
||||
- project-import
|
||||
- drop-projects
|
||||
|
||||
### Tag events
|
||||
|
||||
- tag-created
|
||||
- tag-deleted
|
||||
- tag-import
|
||||
- drop-tags
|
||||
|
||||
|
||||
### Tag type events
|
||||
- tag-type-created
|
||||
- tag-type-updated
|
||||
- tag-type-deleted
|
||||
- application-created
|
||||
- tag-type-updated
|
||||
- tag-type-import
|
||||
- drop-tag-types
|
||||
|
||||
|
||||
### Addon events
|
||||
- addon-config-created
|
||||
- addon-config-updated
|
||||
- addon-config-deleted
|
||||
|
||||
|
||||
### User events
|
||||
- user-created
|
||||
- user-updated
|
||||
- user-deleted
|
||||
|
||||
|
||||
### Environment events (Enterprise)
|
||||
|
||||
- drop-environments
|
||||
- environment-import
|
||||
|
||||
**Response**
|
||||
|
||||
```json
|
||||
{
|
||||
"version": 1,
|
||||
"events": [
|
||||
{
|
||||
"id": 454,
|
||||
"type": "feature-updated",
|
||||
"createdBy": "unknown",
|
||||
"createdAt": "2016-08-24T11:22:01.354Z",
|
||||
"data": {
|
||||
"name": "eid.bankid.mobile",
|
||||
"description": "",
|
||||
"strategy": "default",
|
||||
"enabled": true,
|
||||
"parameters": {}
|
||||
},
|
||||
"diffs": [{ "kind": "E", "path": ["enabled"], "lhs": false, "rhs": true }]
|
||||
}
|
||||
]
|
||||
"version": 2,
|
||||
"events": [{
|
||||
"id": 187,
|
||||
"type": "feature-metadata-updated",
|
||||
"createdBy": "admin",
|
||||
"createdAt": "2021-11-11T09:42:14.271Z",
|
||||
"data": {
|
||||
"name": "HelloEvents!",
|
||||
"description": "Hello Events update!",
|
||||
"type": "release",
|
||||
"project": "default",
|
||||
"stale": false,
|
||||
"variants": [],
|
||||
"createdAt": "2021-11-11T09:40:51.077Z",
|
||||
"lastSeenAt": null
|
||||
},
|
||||
"preData": {
|
||||
"name": "HelloEvents!",
|
||||
"description": "Hello Events!",
|
||||
"type": "release",
|
||||
"project": "default",
|
||||
"stale": false,
|
||||
"variants": [],
|
||||
"createdAt": "2021-11-11T09:40:51.077Z",
|
||||
"lastSeenAt": null
|
||||
},
|
||||
"tags": [{
|
||||
"value": "team-x",
|
||||
"type": "simple"
|
||||
}],
|
||||
"featureName": "HelloEvents!",
|
||||
"project": "default",
|
||||
"environment": null
|
||||
}, {
|
||||
"id": 186,
|
||||
"type": "feature-tagged",
|
||||
"createdBy": "admin",
|
||||
"createdAt": "2021-11-11T09:41:20.464Z",
|
||||
"data": {
|
||||
"type": "simple",
|
||||
"value": "team-x"
|
||||
},
|
||||
"preData": null,
|
||||
"tags": [],
|
||||
"featureName": "HelloEvents!",
|
||||
"project": null,
|
||||
"environment": null
|
||||
}, {
|
||||
"id": 184,
|
||||
"type": "feature-environment-enabled",
|
||||
"createdBy": "admin",
|
||||
"createdAt": "2021-11-11T09:41:03.782Z",
|
||||
"data": null,
|
||||
"preData": null,
|
||||
"tags": [],
|
||||
"featureName": "HelloEvents!",
|
||||
"project": "default",
|
||||
"environment": "default"
|
||||
}, {
|
||||
"id": 183,
|
||||
"type": "feature-strategy-add",
|
||||
"createdBy": "admin",
|
||||
"createdAt": "2021-11-11T09:41:00.740Z",
|
||||
"data": {
|
||||
"id": "88e1df00-1951-452f-a063-6f5e18476f87",
|
||||
"name": "flexibleRollout",
|
||||
"constraints": [],
|
||||
"parameters": {
|
||||
"groupId": "HelloEvents!",
|
||||
"rollout": 51,
|
||||
"stickiness": "default"
|
||||
}
|
||||
},
|
||||
"preData": null,
|
||||
"tags": [],
|
||||
"featureName": "HelloEvents!",
|
||||
"project": "default",
|
||||
"environment": "default"
|
||||
}, {
|
||||
"id": 182,
|
||||
"type": "feature-created",
|
||||
"createdBy": "admin",
|
||||
"createdAt": "2021-11-11T09:40:51.083Z",
|
||||
"data": {
|
||||
"name": "HelloEvents!",
|
||||
"description": "Hello Events!",
|
||||
"type": "release",
|
||||
"project": "default",
|
||||
"stale": false,
|
||||
"variants": [],
|
||||
"createdAt": "2021-11-11T09:40:51.077Z",
|
||||
"lastSeenAt": null
|
||||
},
|
||||
"preData": null,
|
||||
"tags": [],
|
||||
"featureName": "HelloEvents!",
|
||||
"project": "default",
|
||||
"environment": null
|
||||
}]
|
||||
}
|
||||
```
|
||||
|
||||
All events will implement the following interface:
|
||||
|
||||
```js
|
||||
|
||||
interface IEvent {
|
||||
id: number;
|
||||
createdAt: Date;
|
||||
type: string;
|
||||
createdBy: string;
|
||||
project?: string;
|
||||
environment?: string;
|
||||
featureName?: string;
|
||||
data?: any;
|
||||
preData?: any;
|
||||
tags?: ITag[];
|
||||
}
|
||||
```
|
||||
|
||||
|
||||
|
@ -2113,11 +2113,6 @@ dedent@^0.7.0:
|
||||
resolved "https://registry.npmjs.org/dedent/-/dedent-0.7.0.tgz"
|
||||
integrity sha1-JJXduvbrh0q7Dhvp3yLS5aVEMmw=
|
||||
|
||||
deep-diff@^1.0.2:
|
||||
version "1.0.2"
|
||||
resolved "https://registry.npmjs.org/deep-diff/-/deep-diff-1.0.2.tgz"
|
||||
integrity sha512-aWS3UIVH+NPGCD1kki+DCU9Dua032iSsO43LqQpcs4R3+dVv7tX0qBGjiVHJHjplsoUM2XRO/KB92glqc68awg==
|
||||
|
||||
deep-extend@^0.6.0:
|
||||
version "0.6.0"
|
||||
resolved "https://registry.npmjs.org/deep-extend/-/deep-extend-0.6.0.tgz"
|
||||
|
Loading…
Reference in New Issue
Block a user