mirror of
https://github.com/Unleash/unleash.git
synced 2025-07-26 13:48:33 +02:00
fix: Cleanup new features API with env support (#929)
This commit is contained in:
parent
15102fe318
commit
90962434d9
@ -86,6 +86,7 @@
|
||||
"errorhandler": "^1.5.1",
|
||||
"express": "^4.17.1",
|
||||
"express-session": "^1.17.1",
|
||||
"fast-json-patch": "^3.1.0",
|
||||
"gravatar-url": "^3.1.0",
|
||||
"helmet": "^4.1.0",
|
||||
"joi": "^17.3.0",
|
||||
|
@ -13,12 +13,6 @@ interface IEnvironmentsTable {
|
||||
created_at?: Date;
|
||||
}
|
||||
|
||||
interface IFeatureEnvironmentRow {
|
||||
environment: string;
|
||||
feature_name: string;
|
||||
enabled: boolean;
|
||||
}
|
||||
|
||||
function mapRow(row: IEnvironmentsTable): IEnvironment {
|
||||
return {
|
||||
name: row.name,
|
||||
@ -100,71 +94,9 @@ export default class EnvironmentStore implements IEnvironmentStore {
|
||||
return env;
|
||||
}
|
||||
|
||||
async connectProject(
|
||||
environment: string,
|
||||
projectId: string,
|
||||
): Promise<void> {
|
||||
await this.db('project_environments').insert({
|
||||
environment_name: environment,
|
||||
project_id: projectId,
|
||||
});
|
||||
}
|
||||
|
||||
async connectFeatures(
|
||||
environment: string,
|
||||
projectId: string,
|
||||
): Promise<void> {
|
||||
const featuresToEnable = await this.db('features')
|
||||
.select('name')
|
||||
.where({
|
||||
project: projectId,
|
||||
});
|
||||
const rows: IFeatureEnvironmentRow[] = featuresToEnable.map((f) => ({
|
||||
environment,
|
||||
feature_name: f.name,
|
||||
enabled: false,
|
||||
}));
|
||||
if (rows.length > 0) {
|
||||
await this.db<IFeatureEnvironmentRow>('feature_environments')
|
||||
.insert(rows)
|
||||
.onConflict(['environment', 'feature_name'])
|
||||
.ignore();
|
||||
}
|
||||
}
|
||||
|
||||
async delete(name: string): Promise<void> {
|
||||
await this.db(TABLE).where({ name }).del();
|
||||
}
|
||||
|
||||
async disconnectProjectFromEnv(
|
||||
environment: string,
|
||||
projectId: string,
|
||||
): Promise<void> {
|
||||
await this.db('project_environments')
|
||||
.where({ environment_name: environment, project_id: projectId })
|
||||
.del();
|
||||
}
|
||||
|
||||
async connectFeatureToEnvironmentsForProject(
|
||||
featureName: string,
|
||||
project_id: string,
|
||||
): Promise<void> {
|
||||
const environmentsToEnable = await this.db('project_environments')
|
||||
.select('environment_name')
|
||||
.where({ project_id });
|
||||
await Promise.all(
|
||||
environmentsToEnable.map(async (env) => {
|
||||
await this.db('feature_environments')
|
||||
.insert({
|
||||
environment: env.environment_name,
|
||||
feature_name: featureName,
|
||||
enabled: false,
|
||||
})
|
||||
.onConflict(['environment', 'feature_name'])
|
||||
.ignore();
|
||||
}),
|
||||
);
|
||||
}
|
||||
|
||||
destroy(): void {}
|
||||
}
|
||||
|
@ -12,6 +12,12 @@ import NotFoundError from '../error/notfound-error';
|
||||
|
||||
const T = { featureEnvs: 'feature_environments' };
|
||||
|
||||
interface IFeatureEnvironmentRow {
|
||||
environment: string;
|
||||
feature_name: string;
|
||||
enabled: boolean;
|
||||
}
|
||||
|
||||
export class FeatureEnvironmentStore implements IFeatureEnvironmentStore {
|
||||
private db: Knex;
|
||||
|
||||
@ -86,18 +92,19 @@ export class FeatureEnvironmentStore implements IFeatureEnvironmentStore {
|
||||
}));
|
||||
}
|
||||
|
||||
async connectEnvironmentAndFeature(
|
||||
feature_name: string,
|
||||
async addEnvironmentToFeature(
|
||||
featureName: string,
|
||||
environment: string,
|
||||
enabled: boolean = false,
|
||||
): Promise<void> {
|
||||
await this.db('feature_environments')
|
||||
.insert({ feature_name, environment, enabled })
|
||||
.insert({ feature_name: featureName, environment, enabled })
|
||||
.onConflict(['environment', 'feature_name'])
|
||||
.merge('enabled');
|
||||
}
|
||||
|
||||
async disconnectEnvironmentFromProject(
|
||||
// TODO: move to project store.
|
||||
async disconnectFeatures(
|
||||
environment: string,
|
||||
project: string,
|
||||
): Promise<void> {
|
||||
@ -114,15 +121,6 @@ export class FeatureEnvironmentStore implements IFeatureEnvironmentStore {
|
||||
});
|
||||
}
|
||||
|
||||
async enableEnvironmentForFeature(
|
||||
feature_name: string,
|
||||
environment: string,
|
||||
): Promise<void> {
|
||||
await this.db(T.featureEnvs)
|
||||
.update({ enabled: true })
|
||||
.where({ feature_name, environment });
|
||||
}
|
||||
|
||||
async featureHasEnvironment(
|
||||
environment: string,
|
||||
featureName: string,
|
||||
@ -135,15 +133,6 @@ export class FeatureEnvironmentStore implements IFeatureEnvironmentStore {
|
||||
return present;
|
||||
}
|
||||
|
||||
async getAllFeatureEnvironments(): Promise<IFeatureEnvironment[]> {
|
||||
const rows = await this.db(T.featureEnvs);
|
||||
return rows.map((r) => ({
|
||||
environment: r.environment,
|
||||
featureName: r.feature_name,
|
||||
enabled: r.enabled,
|
||||
}));
|
||||
}
|
||||
|
||||
async getEnvironmentMetaData(
|
||||
environment: string,
|
||||
featureName: string,
|
||||
@ -176,13 +165,15 @@ export class FeatureEnvironmentStore implements IFeatureEnvironmentStore {
|
||||
}
|
||||
|
||||
async removeEnvironmentForFeature(
|
||||
feature_name: string,
|
||||
featureName: string,
|
||||
environment: string,
|
||||
): Promise<void> {
|
||||
await this.db(T.featureEnvs).where({ feature_name, environment }).del();
|
||||
await this.db(T.featureEnvs)
|
||||
.where({ feature_name: featureName, environment })
|
||||
.del();
|
||||
}
|
||||
|
||||
async toggleEnvironmentEnabledStatus(
|
||||
async setEnvironmentEnabledStatus(
|
||||
environment: string,
|
||||
featureName: string,
|
||||
enabled: boolean,
|
||||
@ -192,4 +183,66 @@ export class FeatureEnvironmentStore implements IFeatureEnvironmentStore {
|
||||
.where({ environment, feature_name: featureName });
|
||||
return enabled;
|
||||
}
|
||||
|
||||
async connectProject(
|
||||
environment: string,
|
||||
projectId: string,
|
||||
): Promise<void> {
|
||||
await this.db('project_environments').insert({
|
||||
environment_name: environment,
|
||||
project_id: projectId,
|
||||
});
|
||||
}
|
||||
|
||||
async connectFeatures(
|
||||
environment: string,
|
||||
projectId: string,
|
||||
): Promise<void> {
|
||||
const featuresToEnable = await this.db('features')
|
||||
.select('name')
|
||||
.where({
|
||||
project: projectId,
|
||||
});
|
||||
const rows: IFeatureEnvironmentRow[] = featuresToEnable.map((f) => ({
|
||||
environment,
|
||||
feature_name: f.name,
|
||||
enabled: false,
|
||||
}));
|
||||
if (rows.length > 0) {
|
||||
await this.db<IFeatureEnvironmentRow>('feature_environments')
|
||||
.insert(rows)
|
||||
.onConflict(['environment', 'feature_name'])
|
||||
.ignore();
|
||||
}
|
||||
}
|
||||
|
||||
async disconnectProject(
|
||||
environment: string,
|
||||
projectId: string,
|
||||
): Promise<void> {
|
||||
await this.db('project_environments')
|
||||
.where({ environment_name: environment, project_id: projectId })
|
||||
.del();
|
||||
}
|
||||
|
||||
async connectFeatureToEnvironmentsForProject(
|
||||
featureName: string,
|
||||
projectId: string,
|
||||
): Promise<void> {
|
||||
const environmentsToEnable = await this.db('project_environments')
|
||||
.select('environment_name')
|
||||
.where({ project_id: projectId });
|
||||
await Promise.all(
|
||||
environmentsToEnable.map(async (env) => {
|
||||
await this.db('feature_environments')
|
||||
.insert({
|
||||
environment: env.environment_name,
|
||||
feature_name: featureName,
|
||||
enabled: false,
|
||||
})
|
||||
.onConflict(['environment', 'feature_name'])
|
||||
.ignore();
|
||||
}),
|
||||
);
|
||||
}
|
||||
}
|
||||
|
@ -8,9 +8,9 @@ import NotFoundError from '../error/notfound-error';
|
||||
import {
|
||||
FeatureToggleWithEnvironment,
|
||||
IConstraint,
|
||||
IEnvironmentOverview,
|
||||
IFeatureOverview,
|
||||
IFeatureStrategy,
|
||||
IFeatureToggleClient,
|
||||
IFeatureToggleQuery,
|
||||
IStrategyConfig,
|
||||
} from '../types/model';
|
||||
import { IFeatureStrategiesStore } from '../types/stores/feature-strategies-store';
|
||||
@ -54,7 +54,7 @@ function mapRow(row: IFeatureStrategiesTable): IFeatureStrategy {
|
||||
return {
|
||||
id: row.id,
|
||||
featureName: row.feature_name,
|
||||
projectName: row.project_name,
|
||||
projectId: row.project_name,
|
||||
environment: row.environment,
|
||||
strategyName: row.strategy_name,
|
||||
parameters: row.parameters,
|
||||
@ -67,7 +67,7 @@ function mapInput(input: IFeatureStrategy): IFeatureStrategiesTable {
|
||||
return {
|
||||
id: input.id,
|
||||
feature_name: input.featureName,
|
||||
project_name: input.projectName,
|
||||
project_name: input.projectId,
|
||||
environment: input.environment,
|
||||
strategy_name: input.strategyName,
|
||||
parameters: input.parameters,
|
||||
@ -136,10 +136,15 @@ class FeatureStrategiesStore implements IFeatureStrategiesStore {
|
||||
const row = await this.db(T.featureStrategies)
|
||||
.where({ id: key })
|
||||
.first();
|
||||
|
||||
if (!row) {
|
||||
throw new NotFoundError(`Could not find strategy with id=${key}`);
|
||||
}
|
||||
|
||||
return mapRow(row);
|
||||
}
|
||||
|
||||
async createStrategyConfig(
|
||||
async createStrategyFeatureEnv(
|
||||
strategyConfig: Omit<IFeatureStrategy, 'id' | 'createdAt'>,
|
||||
): Promise<IFeatureStrategy> {
|
||||
const strategyRow = mapInput({ ...strategyConfig, id: uuidv4() });
|
||||
@ -149,43 +154,12 @@ class FeatureStrategiesStore implements IFeatureStrategiesStore {
|
||||
return mapRow(rows[0]);
|
||||
}
|
||||
|
||||
async getStrategiesForToggle(
|
||||
async removeAllStrategiesForFeatureEnv(
|
||||
featureName: string,
|
||||
): Promise<IFeatureStrategy[]> {
|
||||
const stopTimer = this.timer('getAll');
|
||||
const rows = await this.db
|
||||
.select(COLUMNS)
|
||||
.where('feature_name', featureName)
|
||||
.from<IFeatureStrategiesTable>(T.featureStrategies);
|
||||
|
||||
stopTimer();
|
||||
return rows.map(mapRow);
|
||||
}
|
||||
|
||||
async getAllFeatureStrategies(): Promise<IFeatureStrategy[]> {
|
||||
const rows = await this.db(T.featureStrategies).select(COLUMNS);
|
||||
return rows.map(mapRow);
|
||||
}
|
||||
|
||||
async getStrategiesForEnvironment(
|
||||
environment: string,
|
||||
): Promise<IFeatureStrategy[]> {
|
||||
const stopTimer = this.timer('getAll');
|
||||
const rows = await this.db
|
||||
.select(COLUMNS)
|
||||
.where({ environment })
|
||||
.from<IFeatureStrategiesTable>(T.featureStrategies);
|
||||
|
||||
stopTimer();
|
||||
return rows.map(mapRow);
|
||||
}
|
||||
|
||||
async removeAllStrategiesForEnv(
|
||||
feature_name: string,
|
||||
environment: string,
|
||||
): Promise<void> {
|
||||
await this.db('feature_strategies')
|
||||
.where({ feature_name, environment })
|
||||
.where({ feature_name: featureName, environment })
|
||||
.del();
|
||||
}
|
||||
|
||||
@ -199,37 +173,24 @@ class FeatureStrategiesStore implements IFeatureStrategiesStore {
|
||||
return rows.map(mapRow);
|
||||
}
|
||||
|
||||
async getStrategiesForFeature(
|
||||
project_name: string,
|
||||
feature_name: string,
|
||||
async getStrategiesForFeatureEnv(
|
||||
projectId: string,
|
||||
featureName: string,
|
||||
environment: string,
|
||||
): Promise<IFeatureStrategy[]> {
|
||||
const stopTimer = this.timer('getForFeature');
|
||||
const rows = await this.db<IFeatureStrategiesTable>(
|
||||
T.featureStrategies,
|
||||
).where({
|
||||
project_name,
|
||||
feature_name,
|
||||
project_name: projectId,
|
||||
feature_name: featureName,
|
||||
environment,
|
||||
});
|
||||
stopTimer();
|
||||
return rows.map(mapRow);
|
||||
}
|
||||
|
||||
async getStrategiesForEnv(
|
||||
environment: string,
|
||||
): Promise<IFeatureStrategy[]> {
|
||||
const stopTimer = this.timer('getStrategiesForEnv');
|
||||
const rows = await this.db<IFeatureStrategiesTable>(
|
||||
T.featureStrategies,
|
||||
).where({
|
||||
environment,
|
||||
});
|
||||
stopTimer();
|
||||
return rows.map(mapRow);
|
||||
}
|
||||
|
||||
async getFeatureToggleAdmin(
|
||||
async getFeatureToggleWithEnvs(
|
||||
featureName: string,
|
||||
archived: boolean = false,
|
||||
): Promise<FeatureToggleWithEnvironment> {
|
||||
@ -256,11 +217,17 @@ class FeatureStrategiesStore implements IFeatureStrategiesStore {
|
||||
'feature_environments.feature_name',
|
||||
'features.name',
|
||||
)
|
||||
.fullOuterJoin(
|
||||
'feature_strategies',
|
||||
'feature_strategies.feature_name',
|
||||
'features.name',
|
||||
)
|
||||
.fullOuterJoin('feature_strategies', function () {
|
||||
this.on(
|
||||
'feature_strategies.feature_name',
|
||||
'=',
|
||||
'feature_environments.feature_name',
|
||||
).andOn(
|
||||
'feature_strategies.environment',
|
||||
'=',
|
||||
'feature_environments.environment',
|
||||
);
|
||||
})
|
||||
.where({ name: featureName, archived: archived ? 1 : 0 });
|
||||
stopTimer();
|
||||
if (rows.length > 0) {
|
||||
@ -286,7 +253,9 @@ class FeatureStrategiesStore implements IFeatureStrategiesStore {
|
||||
if (!env.strategies) {
|
||||
env.strategies = [];
|
||||
}
|
||||
env.strategies.push(this.getAdminStrategy(r));
|
||||
if (r.strategy_id) {
|
||||
env.strategies.push(this.getAdminStrategy(r));
|
||||
}
|
||||
acc.environments[r.environment] = env;
|
||||
return acc;
|
||||
}, {});
|
||||
@ -301,99 +270,65 @@ class FeatureStrategiesStore implements IFeatureStrategiesStore {
|
||||
);
|
||||
}
|
||||
|
||||
async getFeatures(
|
||||
featureQuery?: IFeatureToggleQuery,
|
||||
// eslint-disable-next-line @typescript-eslint/explicit-module-boundary-types
|
||||
private getEnvironment(r: any): IEnvironmentOverview {
|
||||
return {
|
||||
name: r.environment,
|
||||
displayName: r.display_name,
|
||||
enabled: r.enabled,
|
||||
};
|
||||
}
|
||||
|
||||
async getFeatureOverview(
|
||||
projectId: string,
|
||||
archived: boolean = false,
|
||||
isAdmin: boolean = true,
|
||||
): Promise<IFeatureToggleClient[]> {
|
||||
const environments = [':global:'];
|
||||
if (featureQuery?.environment) {
|
||||
environments.push(featureQuery.environment);
|
||||
}
|
||||
const stopTimer = this.timer('getFeatureAdmin');
|
||||
let query = this.db('features')
|
||||
): Promise<IFeatureOverview[]> {
|
||||
const rows = await this.db('features')
|
||||
.where({ project: projectId, archived })
|
||||
.select(
|
||||
'features.name as name',
|
||||
'features.description as description',
|
||||
'features.name as feature_name',
|
||||
'features.type as type',
|
||||
'features.project as project',
|
||||
'features.stale as stale',
|
||||
'features.variants as variants',
|
||||
'features.created_at as created_at',
|
||||
'features.last_seen_at as last_seen_at',
|
||||
'features.stale as stale',
|
||||
'feature_environments.enabled as enabled',
|
||||
'feature_environments.environment as environment',
|
||||
'feature_strategies.id as strategy_id',
|
||||
'feature_strategies.strategy_name as strategy_name',
|
||||
'feature_strategies.parameters as parameters',
|
||||
'feature_strategies.constraints as constraints',
|
||||
'environments.display_name as display_name',
|
||||
)
|
||||
.where({ archived })
|
||||
.whereIn('feature_environments.environment', environments)
|
||||
.fullOuterJoin(
|
||||
'feature_environments',
|
||||
'feature_environments.feature_name',
|
||||
'features.name',
|
||||
)
|
||||
.fullOuterJoin(
|
||||
'feature_strategies',
|
||||
'feature_strategies.feature_name',
|
||||
'features.name',
|
||||
'environments',
|
||||
'feature_environments.environment',
|
||||
'environments.name',
|
||||
);
|
||||
if (featureQuery) {
|
||||
if (featureQuery.tag) {
|
||||
const tagQuery = this.db
|
||||
.from('feature_tag')
|
||||
.select('feature_name')
|
||||
.whereIn(['tag_type', 'tag_value'], featureQuery.tag);
|
||||
query = query.whereIn('name', tagQuery);
|
||||
}
|
||||
if (featureQuery.project) {
|
||||
query = query.whereIn('project', featureQuery.project);
|
||||
}
|
||||
if (featureQuery.namePrefix) {
|
||||
query = query.where(
|
||||
'name',
|
||||
'like',
|
||||
`${featureQuery.namePrefix}%`,
|
||||
);
|
||||
}
|
||||
if (rows.length > 0) {
|
||||
const overview = rows.reduce((acc, r) => {
|
||||
if (acc[r.feature_name] !== undefined) {
|
||||
acc[r.feature_name].environments.push(
|
||||
this.getEnvironment(r),
|
||||
);
|
||||
} else {
|
||||
acc[r.feature_name] = {
|
||||
type: r.type,
|
||||
name: r.feature_name,
|
||||
createdAt: r.created_at,
|
||||
lastSeenAt: r.last_seen_at,
|
||||
stale: r.stale,
|
||||
environments: [this.getEnvironment(r)],
|
||||
};
|
||||
}
|
||||
return acc;
|
||||
}, {});
|
||||
return Object.values(overview).map((o: IFeatureOverview) => ({
|
||||
...o,
|
||||
environments: o.environments.filter((f) => f.name),
|
||||
}));
|
||||
}
|
||||
const rows = await query;
|
||||
stopTimer();
|
||||
const featureToggles = rows.reduce((acc, r) => {
|
||||
let feature;
|
||||
if (acc[r.name]) {
|
||||
feature = acc[r.name];
|
||||
} else {
|
||||
feature = {};
|
||||
}
|
||||
if (!feature.strategies) {
|
||||
feature.strategies = [];
|
||||
}
|
||||
if (r.strategy_name) {
|
||||
feature.strategies.push(this.getAdminStrategy(r, isAdmin));
|
||||
}
|
||||
if (feature.enabled === undefined) {
|
||||
feature.enabled = r.enabled;
|
||||
} else {
|
||||
feature.enabled = feature.enabled && r.enabled;
|
||||
}
|
||||
feature.name = r.name;
|
||||
feature.description = r.description;
|
||||
feature.project = r.project;
|
||||
feature.stale = r.stale;
|
||||
feature.type = r.type;
|
||||
feature.variants = r.variants;
|
||||
feature.project = r.project;
|
||||
if (isAdmin) {
|
||||
feature.lastSeenAt = r.last_seen_at;
|
||||
feature.createdAt = r.created_at;
|
||||
}
|
||||
acc[r.name] = feature;
|
||||
return acc;
|
||||
}, {});
|
||||
return Object.values(featureToggles);
|
||||
return [];
|
||||
}
|
||||
|
||||
async getStrategyById(id: string): Promise<IFeatureStrategy> {
|
||||
@ -432,31 +367,6 @@ class FeatureStrategiesStore implements IFeatureStrategiesStore {
|
||||
return strategy;
|
||||
}
|
||||
|
||||
async getStrategiesAndMetadataForEnvironment(
|
||||
environment: string,
|
||||
featureName: string,
|
||||
): Promise<void> {
|
||||
const rows = await this.db(T.featureEnvs)
|
||||
.select('*')
|
||||
.fullOuterJoin(
|
||||
T.featureStrategies,
|
||||
`${T.featureEnvs}.feature_name`,
|
||||
`${T.featureStrategies}.feature_name`,
|
||||
)
|
||||
.where(`${T.featureStrategies}.feature_name`, featureName)
|
||||
.andWhere(`${T.featureEnvs}.environment`, environment);
|
||||
return rows.reduce((acc, r) => {
|
||||
if (acc.strategies !== undefined) {
|
||||
acc.strategies.push(this.getAdminStrategy(r));
|
||||
} else {
|
||||
acc.enabled = r.enabled;
|
||||
acc.environment = r.environment;
|
||||
acc.strategies = [this.getAdminStrategy(r)];
|
||||
}
|
||||
return acc;
|
||||
}, {});
|
||||
}
|
||||
|
||||
async deleteConfigurationsForProjectAndEnvironment(
|
||||
projectId: String,
|
||||
environment: String,
|
||||
|
172
src/lib/db/feature-toggle-client-store.ts
Normal file
172
src/lib/db/feature-toggle-client-store.ts
Normal file
@ -0,0 +1,172 @@
|
||||
import { Knex } from 'knex';
|
||||
import EventEmitter from 'events';
|
||||
import metricsHelper from '../util/metrics-helper';
|
||||
import { DB_TIME } from '../metric-events';
|
||||
import { Logger, LogProvider } from '../logger';
|
||||
import {
|
||||
IFeatureToggleClient,
|
||||
IFeatureToggleQuery,
|
||||
IStrategyConfig,
|
||||
} from '../types/model';
|
||||
import { IFeatureToggleClientStore } from '../types/stores/feature-toggle-client-store';
|
||||
|
||||
export interface FeaturesTable {
|
||||
name: string;
|
||||
description: string;
|
||||
type: string;
|
||||
stale: boolean;
|
||||
variants: string;
|
||||
project: string;
|
||||
last_seen_at?: Date;
|
||||
created_at?: Date;
|
||||
}
|
||||
|
||||
export default class FeatureToggleClientStore
|
||||
implements IFeatureToggleClientStore
|
||||
{
|
||||
private db: Knex;
|
||||
|
||||
private logger: Logger;
|
||||
|
||||
private timer: Function;
|
||||
|
||||
constructor(db: Knex, eventBus: EventEmitter, getLogger: LogProvider) {
|
||||
this.db = db;
|
||||
this.logger = getLogger('feature-toggle-client-store.ts');
|
||||
this.timer = (action) =>
|
||||
metricsHelper.wrapTimer(eventBus, DB_TIME, {
|
||||
store: 'feature-toggle',
|
||||
action,
|
||||
});
|
||||
}
|
||||
|
||||
private getAdminStrategy(
|
||||
r: any,
|
||||
includeId: boolean = true,
|
||||
): IStrategyConfig {
|
||||
const strategy = {
|
||||
name: r.strategy_name,
|
||||
constraints: r.constraints || [],
|
||||
parameters: r.parameters,
|
||||
id: r.strategy_id,
|
||||
};
|
||||
if (!includeId) {
|
||||
delete strategy.id;
|
||||
}
|
||||
return strategy;
|
||||
}
|
||||
|
||||
private async getAll(
|
||||
featureQuery?: IFeatureToggleQuery,
|
||||
archived: boolean = false,
|
||||
isAdmin: boolean = true,
|
||||
): Promise<IFeatureToggleClient[]> {
|
||||
const environments = [':global:'];
|
||||
if (featureQuery?.environment) {
|
||||
environments.push(featureQuery.environment);
|
||||
}
|
||||
const stopTimer = this.timer('getFeatureAdmin');
|
||||
let query = this.db('features')
|
||||
.select(
|
||||
'features.name as name',
|
||||
'features.description as description',
|
||||
'features.type as type',
|
||||
'features.project as project',
|
||||
'features.stale as stale',
|
||||
'features.variants as variants',
|
||||
'features.created_at as created_at',
|
||||
'features.last_seen_at as last_seen_at',
|
||||
'feature_environments.enabled as enabled',
|
||||
'feature_environments.environment as environment',
|
||||
'feature_strategies.id as strategy_id',
|
||||
'feature_strategies.strategy_name as strategy_name',
|
||||
'feature_strategies.parameters as parameters',
|
||||
'feature_strategies.constraints as constraints',
|
||||
)
|
||||
.fullOuterJoin(
|
||||
'feature_environments',
|
||||
'feature_environments.feature_name',
|
||||
'features.name',
|
||||
)
|
||||
.fullOuterJoin('feature_strategies', function () {
|
||||
this.on(
|
||||
'feature_strategies.feature_name',
|
||||
'features.name',
|
||||
).andOn(
|
||||
'feature_strategies.environment',
|
||||
'feature_environments.environment',
|
||||
);
|
||||
})
|
||||
.whereIn('feature_environments.environment', environments)
|
||||
.where({ archived });
|
||||
if (featureQuery) {
|
||||
if (featureQuery.tag) {
|
||||
const tagQuery = this.db
|
||||
.from('feature_tag')
|
||||
.select('feature_name')
|
||||
.whereIn(['tag_type', 'tag_value'], featureQuery.tag);
|
||||
query = query.whereIn('name', tagQuery);
|
||||
}
|
||||
if (featureQuery.project) {
|
||||
query = query.whereIn('project', featureQuery.project);
|
||||
}
|
||||
if (featureQuery.namePrefix) {
|
||||
query = query.where(
|
||||
'name',
|
||||
'like',
|
||||
`${featureQuery.namePrefix}%`,
|
||||
);
|
||||
}
|
||||
}
|
||||
const rows = await query;
|
||||
stopTimer();
|
||||
const featureToggles = rows.reduce((acc, r) => {
|
||||
let feature;
|
||||
if (acc[r.name]) {
|
||||
feature = acc[r.name];
|
||||
} else {
|
||||
feature = {};
|
||||
}
|
||||
if (!feature.strategies) {
|
||||
feature.strategies = [];
|
||||
}
|
||||
if (r.strategy_name) {
|
||||
feature.strategies.push(this.getAdminStrategy(r, isAdmin));
|
||||
}
|
||||
if (feature.enabled === undefined) {
|
||||
feature.enabled = r.enabled;
|
||||
} else {
|
||||
feature.enabled = feature.enabled && r.enabled;
|
||||
}
|
||||
feature.name = r.name;
|
||||
feature.description = r.description;
|
||||
feature.project = r.project;
|
||||
feature.stale = r.stale;
|
||||
feature.type = r.type;
|
||||
feature.variants = r.variants;
|
||||
feature.project = r.project;
|
||||
if (isAdmin) {
|
||||
feature.lastSeenAt = r.last_seen_at;
|
||||
feature.createdAt = r.created_at;
|
||||
}
|
||||
acc[r.name] = feature;
|
||||
return acc;
|
||||
}, {});
|
||||
return Object.values(featureToggles);
|
||||
}
|
||||
|
||||
async getClient(
|
||||
featureQuery?: IFeatureToggleQuery,
|
||||
): Promise<IFeatureToggleClient[]> {
|
||||
return this.getAll(featureQuery, false, false);
|
||||
}
|
||||
|
||||
async getAdmin(
|
||||
featureQuery?: IFeatureToggleQuery,
|
||||
archived: boolean = false,
|
||||
): Promise<IFeatureToggleClient[]> {
|
||||
return this.getAll(featureQuery, archived, true);
|
||||
}
|
||||
}
|
||||
|
||||
module.exports = FeatureToggleClientStore;
|
@ -62,14 +62,6 @@ export default class FeatureToggleStore implements IFeatureToggleStore {
|
||||
.then((res) => Number(res[0].count));
|
||||
}
|
||||
|
||||
async getFeatureMetadata(name: string): Promise<FeatureToggle> {
|
||||
return this.db
|
||||
.first(FEATURE_COLUMNS)
|
||||
.from(TABLE)
|
||||
.where({ name, archived: 0 })
|
||||
.then(this.rowToFeature);
|
||||
}
|
||||
|
||||
async deleteAll(): Promise<void> {
|
||||
await this.db(TABLE).del();
|
||||
}
|
||||
@ -80,15 +72,21 @@ export default class FeatureToggleStore implements IFeatureToggleStore {
|
||||
return this.db
|
||||
.first(FEATURE_COLUMNS)
|
||||
.from(TABLE)
|
||||
.where({ name, archived: 0 })
|
||||
.where({ name })
|
||||
.then(this.rowToFeature);
|
||||
}
|
||||
|
||||
async getAll(): Promise<FeatureToggle[]> {
|
||||
async getAll(
|
||||
query: {
|
||||
archived?: boolean;
|
||||
project?: string;
|
||||
stale?: boolean;
|
||||
} = { archived: false },
|
||||
): Promise<FeatureToggle[]> {
|
||||
const rows = await this.db
|
||||
.select(FEATURE_COLUMNS)
|
||||
.from(TABLE)
|
||||
.where({ archived: false });
|
||||
.where(query);
|
||||
return rows.map(this.rowToFeature);
|
||||
}
|
||||
|
||||
@ -117,26 +115,6 @@ export default class FeatureToggleStore implements IFeatureToggleStore {
|
||||
});
|
||||
}
|
||||
|
||||
/**
|
||||
* TODO: Should really be a Promise<boolean> rather than returning a { featureName, archived } object
|
||||
* @param name
|
||||
*/
|
||||
async hasFeature(name: string): Promise<any> {
|
||||
return this.db
|
||||
.first('name', 'archived')
|
||||
.from(TABLE)
|
||||
.where({ name })
|
||||
.then((row) => {
|
||||
if (!row) {
|
||||
throw new NotFoundError('No feature toggle found');
|
||||
}
|
||||
return {
|
||||
name: row.name,
|
||||
archived: row.archived,
|
||||
};
|
||||
});
|
||||
}
|
||||
|
||||
async exists(name: string): Promise<boolean> {
|
||||
const result = await this.db.raw(
|
||||
'SELECT EXISTS (SELECT 1 FROM features WHERE name = ?) AS present',
|
||||
@ -150,12 +128,12 @@ export default class FeatureToggleStore implements IFeatureToggleStore {
|
||||
const rows = await this.db
|
||||
.select(FEATURE_COLUMNS)
|
||||
.from(TABLE)
|
||||
.where({ archived: 1 })
|
||||
.where({ archived: true })
|
||||
.orderBy('name', 'asc');
|
||||
return rows.map(this.rowToFeature);
|
||||
}
|
||||
|
||||
async updateLastSeenForToggles(toggleNames: string[]): Promise<void> {
|
||||
async setLastSeen(toggleNames: string[]): Promise<void> {
|
||||
const now = new Date();
|
||||
try {
|
||||
await this.db(TABLE)
|
||||
@ -206,7 +184,7 @@ export default class FeatureToggleStore implements IFeatureToggleStore {
|
||||
return row;
|
||||
}
|
||||
|
||||
async createFeature(
|
||||
async create(
|
||||
project: string,
|
||||
data: FeatureToggleDTO,
|
||||
): Promise<FeatureToggle> {
|
||||
@ -222,7 +200,7 @@ export default class FeatureToggleStore implements IFeatureToggleStore {
|
||||
return undefined;
|
||||
}
|
||||
|
||||
async updateFeature(
|
||||
async update(
|
||||
project: string,
|
||||
data: FeatureToggleDTO,
|
||||
): Promise<FeatureToggle> {
|
||||
@ -233,7 +211,7 @@ export default class FeatureToggleStore implements IFeatureToggleStore {
|
||||
return this.rowToFeature(row[0]);
|
||||
}
|
||||
|
||||
async archiveFeature(name: string): Promise<FeatureToggle> {
|
||||
async archive(name: string): Promise<FeatureToggle> {
|
||||
const row = await this.db(TABLE)
|
||||
.where({ name })
|
||||
.update({ archived: true })
|
||||
@ -247,30 +225,13 @@ export default class FeatureToggleStore implements IFeatureToggleStore {
|
||||
.del();
|
||||
}
|
||||
|
||||
async reviveFeature(name: string): Promise<FeatureToggle> {
|
||||
async revive(name: string): Promise<FeatureToggle> {
|
||||
const row = await this.db(TABLE)
|
||||
.where({ name })
|
||||
.update({ archived: false })
|
||||
.returning(FEATURE_COLUMNS);
|
||||
return this.rowToFeature(row[0]);
|
||||
}
|
||||
|
||||
async getFeaturesBy(params: {
|
||||
archived?: boolean;
|
||||
project?: string;
|
||||
stale?: boolean;
|
||||
}): Promise<FeatureToggle[]> {
|
||||
const rows = await this.db(TABLE).where(params);
|
||||
return rows.map(this.rowToFeature);
|
||||
}
|
||||
|
||||
async getFeaturesByInternal(params: {
|
||||
archived?: boolean;
|
||||
project?: string;
|
||||
stale?: boolean;
|
||||
}): Promise<FeatureToggle[]> {
|
||||
return this.db(TABLE).where(params);
|
||||
}
|
||||
}
|
||||
|
||||
module.exports = FeatureToggleStore;
|
||||
|
@ -24,6 +24,7 @@ import { AccessStore } from './access-store';
|
||||
import { ResetTokenStore } from './reset-token-store';
|
||||
import UserFeedbackStore from './user-feedback-store';
|
||||
import FeatureStrategyStore from './feature-strategy-store';
|
||||
import FeatureToggleClientStore from './feature-toggle-client-store';
|
||||
import EnvironmentStore from './environment-store';
|
||||
import FeatureTagStore from './feature-tag-store';
|
||||
import { FeatureEnvironmentStore } from './feature-environment-store';
|
||||
@ -70,6 +71,11 @@ export const createStores = (
|
||||
eventBus,
|
||||
getLogger,
|
||||
),
|
||||
featureToggleClientStore: new FeatureToggleClientStore(
|
||||
db,
|
||||
eventBus,
|
||||
getLogger,
|
||||
),
|
||||
environmentStore: new EnvironmentStore(db, eventBus, getLogger),
|
||||
featureTagStore: new FeatureTagStore(db, eventBus, getLogger),
|
||||
featureEnvironmentStore: new FeatureEnvironmentStore(
|
||||
|
@ -2,11 +2,7 @@ import { Knex } from 'knex';
|
||||
import { Logger, LogProvider } from '../logger';
|
||||
|
||||
import NotFoundError from '../error/notfound-error';
|
||||
import {
|
||||
IEnvironmentOverview,
|
||||
IFeatureOverview,
|
||||
IProject,
|
||||
} from '../types/model';
|
||||
import { IProject } from '../types/model';
|
||||
import {
|
||||
IProjectHealthUpdate,
|
||||
IProjectInsert,
|
||||
@ -159,7 +155,7 @@ class ProjectStore implements IProjectStore {
|
||||
.where({
|
||||
project_id: id,
|
||||
})
|
||||
.returning('environment_name');
|
||||
.pluck('environment_name');
|
||||
}
|
||||
|
||||
async getMembers(projectId: string): Promise<number> {
|
||||
@ -179,58 +175,6 @@ class ProjectStore implements IProjectStore {
|
||||
return members;
|
||||
}
|
||||
|
||||
async getProjectOverview(
|
||||
projectId: string,
|
||||
archived: boolean = false,
|
||||
): Promise<IFeatureOverview[]> {
|
||||
const rows = await this.db('features')
|
||||
.where({ project: projectId, archived })
|
||||
.select(
|
||||
'features.name as feature_name',
|
||||
'features.type as type',
|
||||
'features.created_at as created_at',
|
||||
'features.last_seen_at as last_seen_at',
|
||||
'features.stale as stale',
|
||||
'feature_environments.enabled as enabled',
|
||||
'feature_environments.environment as environment',
|
||||
'environments.display_name as display_name',
|
||||
)
|
||||
.fullOuterJoin(
|
||||
'feature_environments',
|
||||
'feature_environments.feature_name',
|
||||
'features.name',
|
||||
)
|
||||
.fullOuterJoin(
|
||||
'environments',
|
||||
'feature_environments.environment',
|
||||
'environments.name',
|
||||
);
|
||||
if (rows.length > 0) {
|
||||
const overview = rows.reduce((acc, r) => {
|
||||
if (acc[r.feature_name] !== undefined) {
|
||||
acc[r.feature_name].environments.push(
|
||||
this.getEnvironment(r),
|
||||
);
|
||||
} else {
|
||||
acc[r.feature_name] = {
|
||||
type: r.type,
|
||||
name: r.feature_name,
|
||||
createdAt: r.created_at,
|
||||
lastSeenAt: r.last_seen_at,
|
||||
stale: r.stale,
|
||||
environments: [this.getEnvironment(r)],
|
||||
};
|
||||
}
|
||||
return acc;
|
||||
}, {});
|
||||
return Object.values(overview).map((o: IFeatureOverview) => ({
|
||||
...o,
|
||||
environments: o.environments.filter((f) => f.name),
|
||||
}));
|
||||
}
|
||||
return [];
|
||||
}
|
||||
|
||||
async count(): Promise<number> {
|
||||
return this.db
|
||||
.count('*')
|
||||
@ -238,15 +182,6 @@ class ProjectStore implements IProjectStore {
|
||||
.then((res) => Number(res[0].count));
|
||||
}
|
||||
|
||||
// eslint-disable-next-line @typescript-eslint/explicit-module-boundary-types
|
||||
private getEnvironment(r: any): IEnvironmentOverview {
|
||||
return {
|
||||
name: r.environment,
|
||||
displayName: r.display_name,
|
||||
enabled: r.enabled,
|
||||
};
|
||||
}
|
||||
|
||||
// eslint-disable-next-line @typescript-eslint/explicit-module-boundary-types
|
||||
mapRow(row): IProject {
|
||||
if (!row) {
|
||||
|
@ -5,7 +5,6 @@ import { IUnleashConfig } from '../../types/option';
|
||||
import { IEnvironment } from '../../types/model';
|
||||
import EnvironmentService from '../../services/environment-service';
|
||||
import { Logger } from '../../logger';
|
||||
import { handleErrors } from '../util';
|
||||
import { ADMIN } from '../../types/permissions';
|
||||
|
||||
interface EnvironmentParam {
|
||||
@ -32,24 +31,16 @@ export class EnvironmentsController extends Controller {
|
||||
}
|
||||
|
||||
async getAll(req: Request, res: Response): Promise<void> {
|
||||
try {
|
||||
const environments = await this.service.getAll();
|
||||
res.status(200).json({ version: 1, environments });
|
||||
} catch (e) {
|
||||
handleErrors(res, this.logger, e);
|
||||
}
|
||||
const environments = await this.service.getAll();
|
||||
res.status(200).json({ version: 1, environments });
|
||||
}
|
||||
|
||||
async createEnv(
|
||||
req: Request<any, any, IEnvironment, any>,
|
||||
res: Response,
|
||||
): Promise<void> {
|
||||
try {
|
||||
const environment = await this.service.create(req.body);
|
||||
res.status(201).json(environment);
|
||||
} catch (e) {
|
||||
handleErrors(res, this.logger, e);
|
||||
}
|
||||
const environment = await this.service.create(req.body);
|
||||
res.status(201).json(environment);
|
||||
}
|
||||
|
||||
async getEnv(
|
||||
@ -57,12 +48,8 @@ export class EnvironmentsController extends Controller {
|
||||
res: Response,
|
||||
): Promise<void> {
|
||||
const { name } = req.params;
|
||||
try {
|
||||
const env = await this.service.get(name);
|
||||
res.status(200).json(env);
|
||||
} catch (e) {
|
||||
handleErrors(res, this.logger, e);
|
||||
}
|
||||
const env = await this.service.get(name);
|
||||
res.status(200).json(env);
|
||||
}
|
||||
|
||||
async updateEnv(
|
||||
@ -70,12 +57,8 @@ export class EnvironmentsController extends Controller {
|
||||
res: Response,
|
||||
): Promise<void> {
|
||||
const { name } = req.params;
|
||||
try {
|
||||
const env = await this.service.update(name, req.body);
|
||||
res.status(200).json(env);
|
||||
} catch (e) {
|
||||
handleErrors(res, this.logger, e);
|
||||
}
|
||||
const env = await this.service.update(name, req.body);
|
||||
res.status(200).json(env);
|
||||
}
|
||||
|
||||
async deleteEnv(
|
||||
@ -83,11 +66,7 @@ export class EnvironmentsController extends Controller {
|
||||
res: Response,
|
||||
): Promise<void> {
|
||||
const { name } = req.params;
|
||||
try {
|
||||
await this.service.delete(name);
|
||||
res.status(200).end();
|
||||
} catch (e) {
|
||||
handleErrors(res, this.logger, e);
|
||||
}
|
||||
await this.service.delete(name);
|
||||
res.status(200).end();
|
||||
}
|
||||
}
|
||||
|
@ -177,7 +177,8 @@ class FeatureController extends Controller {
|
||||
),
|
||||
);
|
||||
await this.featureService2.updateEnabled(
|
||||
validatedToggle.name,
|
||||
createdFeature.project,
|
||||
createdFeature.name,
|
||||
GLOBAL_ENV,
|
||||
enabled,
|
||||
userName,
|
||||
@ -222,6 +223,7 @@ class FeatureController extends Controller {
|
||||
);
|
||||
}
|
||||
await this.featureService2.updateEnabled(
|
||||
projectId,
|
||||
updatedFeature.name,
|
||||
GLOBAL_ENV,
|
||||
updatedFeature.enabled,
|
||||
@ -236,9 +238,11 @@ class FeatureController extends Controller {
|
||||
// Kept to keep backward compatibility
|
||||
async toggle(req: Request, res: Response): Promise<void> {
|
||||
const userName = extractUser(req);
|
||||
const name = req.params.featureName;
|
||||
const { featureName } = req.params;
|
||||
const projectId = await this.featureService2.getProjectId(featureName);
|
||||
const feature = await this.featureService2.toggle(
|
||||
name,
|
||||
projectId,
|
||||
featureName,
|
||||
GLOBAL_ENV,
|
||||
userName,
|
||||
);
|
||||
@ -248,7 +252,9 @@ class FeatureController extends Controller {
|
||||
async toggleOn(req: Request, res: Response): Promise<void> {
|
||||
const { featureName } = req.params;
|
||||
const userName = extractUser(req);
|
||||
const projectId = await this.featureService2.getProjectId(featureName);
|
||||
const feature = await this.featureService2.updateEnabled(
|
||||
projectId,
|
||||
featureName,
|
||||
GLOBAL_ENV,
|
||||
true,
|
||||
@ -260,7 +266,9 @@ class FeatureController extends Controller {
|
||||
async toggleOff(req: Request, res: Response): Promise<void> {
|
||||
const { featureName } = req.params;
|
||||
const userName = extractUser(req);
|
||||
const projectId = await this.featureService2.getProjectId(featureName);
|
||||
const feature = await this.featureService2.updateEnabled(
|
||||
projectId,
|
||||
featureName,
|
||||
GLOBAL_ENV,
|
||||
false,
|
||||
|
@ -1,6 +1,5 @@
|
||||
import { Request, Response } from 'express';
|
||||
import Controller from '../controller';
|
||||
import { handleErrors } from '../util';
|
||||
import { UPDATE_APPLICATION } from '../../types/permissions';
|
||||
import { IUnleashConfig } from '../../types/option';
|
||||
import { IUnleashServices } from '../../types/services';
|
||||
@ -42,89 +41,57 @@ class MetricsController extends Controller {
|
||||
}
|
||||
|
||||
async getSeenToggles(req: Request, res: Response): Promise<void> {
|
||||
try {
|
||||
const seenAppToggles = await this.metrics.getAppsWithToggles();
|
||||
res.json(seenAppToggles);
|
||||
} catch (e) {
|
||||
handleErrors(res, this.logger, e);
|
||||
}
|
||||
const seenAppToggles = await this.metrics.getAppsWithToggles();
|
||||
res.json(seenAppToggles);
|
||||
}
|
||||
|
||||
async getSeenApps(req: Request, res: Response): Promise<void> {
|
||||
try {
|
||||
const seenApps = await this.metrics.getSeenApps();
|
||||
res.json(seenApps);
|
||||
} catch (e) {
|
||||
handleErrors(res, this.logger, e);
|
||||
}
|
||||
const seenApps = await this.metrics.getSeenApps();
|
||||
res.json(seenApps);
|
||||
}
|
||||
|
||||
async getFeatureToggles(req: Request, res: Response): Promise<void> {
|
||||
try {
|
||||
const toggles = await this.metrics.getTogglesMetrics();
|
||||
res.json(toggles);
|
||||
} catch (e) {
|
||||
handleErrors(res, this.logger, e);
|
||||
}
|
||||
const toggles = await this.metrics.getTogglesMetrics();
|
||||
res.json(toggles);
|
||||
}
|
||||
|
||||
async getFeatureToggle(req: Request, res: Response): Promise<void> {
|
||||
try {
|
||||
const { name } = req.params;
|
||||
const data = await this.metrics.getTogglesMetrics();
|
||||
const lastHour = data.lastHour[name] || {};
|
||||
const lastMinute = data.lastMinute[name] || {};
|
||||
res.json({
|
||||
lastHour,
|
||||
lastMinute,
|
||||
});
|
||||
} catch (e) {
|
||||
handleErrors(res, this.logger, e);
|
||||
}
|
||||
const { name } = req.params;
|
||||
const data = await this.metrics.getTogglesMetrics();
|
||||
const lastHour = data.lastHour[name] || {};
|
||||
const lastMinute = data.lastMinute[name] || {};
|
||||
res.json({
|
||||
lastHour,
|
||||
lastMinute,
|
||||
});
|
||||
}
|
||||
|
||||
async deleteApplication(req: Request, res: Response): Promise<void> {
|
||||
const { appName } = req.params;
|
||||
|
||||
try {
|
||||
await this.metrics.deleteApplication(appName);
|
||||
res.status(200).end();
|
||||
} catch (e) {
|
||||
handleErrors(res, this.logger, e);
|
||||
}
|
||||
await this.metrics.deleteApplication(appName);
|
||||
res.status(200).end();
|
||||
}
|
||||
|
||||
async createApplication(req: Request, res: Response): Promise<void> {
|
||||
const input = { ...req.body, appName: req.params.appName };
|
||||
try {
|
||||
await this.metrics.createApplication(input);
|
||||
res.status(202).end();
|
||||
} catch (err) {
|
||||
handleErrors(res, this.logger, err);
|
||||
}
|
||||
await this.metrics.createApplication(input);
|
||||
res.status(202).end();
|
||||
}
|
||||
|
||||
async getApplications(req: Request, res: Response): Promise<void> {
|
||||
try {
|
||||
const query = req.query.strategyName
|
||||
? { strategyName: req.query.strategyName as string }
|
||||
: {};
|
||||
const applications = await this.metrics.getApplications(query);
|
||||
res.json({ applications });
|
||||
} catch (err) {
|
||||
handleErrors(res, this.logger, err);
|
||||
}
|
||||
const query = req.query.strategyName
|
||||
? { strategyName: req.query.strategyName as string }
|
||||
: {};
|
||||
const applications = await this.metrics.getApplications(query);
|
||||
res.json({ applications });
|
||||
}
|
||||
|
||||
async getApplication(req: Request, res: Response): Promise<void> {
|
||||
const { appName } = req.params;
|
||||
|
||||
try {
|
||||
const appDetails = await this.metrics.getApplication(appName);
|
||||
res.json(appDetails);
|
||||
} catch (err) {
|
||||
handleErrors(res, this.logger, err);
|
||||
}
|
||||
const appDetails = await this.metrics.getApplication(appName);
|
||||
res.json(appDetails);
|
||||
}
|
||||
}
|
||||
export default MetricsController;
|
||||
|
@ -4,8 +4,8 @@ import { IUnleashConfig } from '../../../types/option';
|
||||
import { IUnleashServices } from '../../../types/services';
|
||||
import { Logger } from '../../../logger';
|
||||
import EnvironmentService from '../../../services/environment-service';
|
||||
import { handleErrors } from '../../util';
|
||||
import { UPDATE_PROJECT } from '../../../types/permissions';
|
||||
import { addEnvironment } from '../../../schema/project-schema';
|
||||
|
||||
const PREFIX = '/:projectId/environments';
|
||||
|
||||
@ -49,15 +49,14 @@ export default class EnvironmentsController extends Controller {
|
||||
res: Response,
|
||||
): Promise<void> {
|
||||
const { projectId } = req.params;
|
||||
try {
|
||||
await this.environmentService.connectProjectToEnvironment(
|
||||
req.body.environment,
|
||||
projectId,
|
||||
);
|
||||
res.status(200).end();
|
||||
} catch (e) {
|
||||
handleErrors(res, this.logger, e);
|
||||
}
|
||||
|
||||
const { environment } = await addEnvironment.validateAsync(req.body);
|
||||
|
||||
await this.environmentService.addEnvironmentToProject(
|
||||
environment,
|
||||
projectId,
|
||||
);
|
||||
res.status(200).end();
|
||||
}
|
||||
|
||||
async removeEnvironmentFromProject(
|
||||
@ -65,14 +64,10 @@ export default class EnvironmentsController extends Controller {
|
||||
res: Response,
|
||||
): Promise<void> {
|
||||
const { projectId, environment } = req.params;
|
||||
try {
|
||||
await this.environmentService.removeEnvironmentFromProject(
|
||||
environment,
|
||||
projectId,
|
||||
);
|
||||
res.status(200).end();
|
||||
} catch (e) {
|
||||
handleErrors(res, this.logger, e);
|
||||
}
|
||||
await this.environmentService.removeEnvironmentFromProject(
|
||||
environment,
|
||||
projectId,
|
||||
);
|
||||
res.status(200).end();
|
||||
}
|
||||
}
|
||||
|
@ -1,17 +1,21 @@
|
||||
import { Request, Response } from 'express';
|
||||
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-v2';
|
||||
import { Logger } from '../../../logger';
|
||||
import { CREATE_FEATURE, UPDATE_FEATURE } from '../../../types/permissions';
|
||||
import {
|
||||
CREATE_FEATURE,
|
||||
DELETE_FEATURE,
|
||||
UPDATE_FEATURE,
|
||||
} from '../../../types/permissions';
|
||||
import {
|
||||
FeatureToggleDTO,
|
||||
IConstraint,
|
||||
IStrategyConfig,
|
||||
} from '../../../types/model';
|
||||
import extractUsername from '../../../extract-user';
|
||||
import ProjectHealthService from '../../../services/project-health-service';
|
||||
|
||||
interface FeatureStrategyParams {
|
||||
projectId: string;
|
||||
@ -37,7 +41,11 @@ interface StrategyUpdateBody {
|
||||
parameters?: object;
|
||||
}
|
||||
|
||||
const PATH_PREFIX = '/:projectId/features/:featureName';
|
||||
const PATH = '/:projectId/features';
|
||||
const PATH_FEATURE = `${PATH}/:featureName`;
|
||||
const PATH_ENV = `${PATH_FEATURE}/environments/:environment`;
|
||||
const PATH_STRATEGIES = `${PATH_ENV}/strategies`;
|
||||
const PATH_STRATEGY = `${PATH_STRATEGIES}/:strategyId`;
|
||||
|
||||
type ProjectFeaturesServices = Pick<
|
||||
IUnleashServices,
|
||||
@ -47,76 +55,49 @@ type ProjectFeaturesServices = Pick<
|
||||
export default class ProjectFeaturesController extends Controller {
|
||||
private featureService: FeatureToggleServiceV2;
|
||||
|
||||
private projectHealthService: ProjectHealthService;
|
||||
|
||||
private readonly logger: Logger;
|
||||
|
||||
constructor(
|
||||
config: IUnleashConfig,
|
||||
{
|
||||
featureToggleServiceV2,
|
||||
projectHealthService,
|
||||
}: ProjectFeaturesServices,
|
||||
{ featureToggleServiceV2 }: ProjectFeaturesServices,
|
||||
) {
|
||||
super(config);
|
||||
this.featureService = featureToggleServiceV2;
|
||||
this.projectHealthService = projectHealthService;
|
||||
this.logger = config.getLogger('/admin-api/project/features.ts');
|
||||
|
||||
this.post(
|
||||
`${PATH_PREFIX}/environments/:environment/strategies`,
|
||||
this.createFeatureStrategy,
|
||||
UPDATE_FEATURE,
|
||||
);
|
||||
this.get(
|
||||
`${PATH_PREFIX}/environments/:environment`,
|
||||
this.getEnvironment,
|
||||
);
|
||||
this.post(
|
||||
`${PATH_PREFIX}/environments/:environment/on`,
|
||||
this.toggleEnvironmentOn,
|
||||
UPDATE_FEATURE,
|
||||
);
|
||||
this.get(`${PATH_ENV}`, this.getEnvironment);
|
||||
this.post(`${PATH_ENV}/on`, this.toggleEnvironmentOn, UPDATE_FEATURE);
|
||||
this.post(`${PATH_ENV}/off`, this.toggleEnvironmentOff, UPDATE_FEATURE);
|
||||
|
||||
this.post(
|
||||
`${PATH_PREFIX}/environments/:environment/off`,
|
||||
this.toggleEnvironmentOff,
|
||||
UPDATE_FEATURE,
|
||||
);
|
||||
this.get(
|
||||
`${PATH_PREFIX}/environments/:environment/strategies`,
|
||||
this.getFeatureStrategies,
|
||||
);
|
||||
this.get(
|
||||
`${PATH_PREFIX}/environments/:environment/strategies/:strategyId`,
|
||||
this.getStrategy,
|
||||
);
|
||||
this.put(
|
||||
`${PATH_PREFIX}/environments/:environment/strategies/:strategyId`,
|
||||
this.updateStrategy,
|
||||
UPDATE_FEATURE,
|
||||
);
|
||||
this.post(
|
||||
'/:projectId/features',
|
||||
this.createFeatureToggle,
|
||||
CREATE_FEATURE,
|
||||
);
|
||||
this.get('/:projectId/features', this.getFeaturesForProject);
|
||||
this.get(PATH_PREFIX, this.getFeature);
|
||||
this.get(`${PATH_STRATEGIES}`, this.getStrategies);
|
||||
this.post(`${PATH_STRATEGIES}`, this.addStrategy, UPDATE_FEATURE);
|
||||
|
||||
this.get(`${PATH_STRATEGY}`, this.getStrategy);
|
||||
this.put(`${PATH_STRATEGY}`, this.updateStrategy, UPDATE_FEATURE);
|
||||
this.patch(`${PATH_STRATEGY}`, this.patchStrategy, UPDATE_FEATURE);
|
||||
this.delete(`${PATH_STRATEGY}`, this.deleteStrategy, DELETE_FEATURE);
|
||||
|
||||
this.get(PATH, this.getFeatures);
|
||||
this.post(PATH, this.createFeature, CREATE_FEATURE);
|
||||
|
||||
this.get(PATH_FEATURE, this.getFeature);
|
||||
this.put(PATH_FEATURE, this.updateFeature);
|
||||
this.patch(PATH_FEATURE, this.patchFeature);
|
||||
this.delete(PATH_FEATURE, this.archiveFeature);
|
||||
}
|
||||
|
||||
async getFeaturesForProject(
|
||||
async getFeatures(
|
||||
req: Request<ProjectParam, any, any, any>,
|
||||
res: Response,
|
||||
): Promise<void> {
|
||||
const { projectId } = req.params;
|
||||
const features = await this.featureService.getFeatureToggles({
|
||||
project: [projectId],
|
||||
});
|
||||
const features = await this.featureService.getFeatureOverview(
|
||||
projectId,
|
||||
);
|
||||
res.json({ version: 1, features });
|
||||
}
|
||||
|
||||
async createFeatureToggle(
|
||||
async createFeature(
|
||||
req: Request<ProjectParam, any, FeatureToggleDTO, any>,
|
||||
res: Response,
|
||||
): Promise<void> {
|
||||
@ -130,6 +111,64 @@ export default class ProjectFeaturesController extends Controller {
|
||||
res.status(201).json(created);
|
||||
}
|
||||
|
||||
async getFeature(
|
||||
req: Request<FeatureParams, any, any, any>,
|
||||
res: Response,
|
||||
): Promise<void> {
|
||||
const { featureName } = req.params;
|
||||
const feature = await this.featureService.getFeature(featureName);
|
||||
res.status(200).json(feature);
|
||||
}
|
||||
|
||||
async updateFeature(
|
||||
req: Request<ProjectParam, any, FeatureToggleDTO, any>,
|
||||
res: Response,
|
||||
): Promise<void> {
|
||||
const { projectId } = req.params;
|
||||
const data = req.body;
|
||||
const userName = extractUsername(req);
|
||||
const created = await this.featureService.updateFeatureToggle(
|
||||
projectId,
|
||||
data,
|
||||
userName,
|
||||
);
|
||||
res.status(200).json(created);
|
||||
}
|
||||
|
||||
async patchFeature(
|
||||
req: Request<
|
||||
{ projectId: string; featureName: string },
|
||||
any,
|
||||
Operation[],
|
||||
any
|
||||
>,
|
||||
res: Response,
|
||||
): Promise<void> {
|
||||
const { projectId, featureName } = req.params;
|
||||
const featureToggle = await this.featureService.getFeatureMetadata(
|
||||
featureName,
|
||||
);
|
||||
const { newDocument } = applyPatch(featureToggle, req.body);
|
||||
const userName = extractUsername(req);
|
||||
const updated = await this.featureService.updateFeatureToggle(
|
||||
projectId,
|
||||
newDocument,
|
||||
userName,
|
||||
);
|
||||
res.status(200).json(updated);
|
||||
}
|
||||
|
||||
// TODO: validate projectId
|
||||
async archiveFeature(
|
||||
req: Request<{ projectId: string; featureName: string }, any, any, any>,
|
||||
res: Response,
|
||||
): Promise<void> {
|
||||
const { featureName } = req.params;
|
||||
const userName = extractUsername(req);
|
||||
await this.featureService.archiveToggle(featureName, userName);
|
||||
res.status(202).send();
|
||||
}
|
||||
|
||||
async getEnvironment(
|
||||
req: Request<FeatureStrategyParams, any, any, any>,
|
||||
res: Response,
|
||||
@ -143,21 +182,13 @@ export default class ProjectFeaturesController extends Controller {
|
||||
res.status(200).json(environmentInfo);
|
||||
}
|
||||
|
||||
async getFeature(
|
||||
req: Request<FeatureParams, any, any, any>,
|
||||
res: Response,
|
||||
): Promise<void> {
|
||||
const { featureName } = req.params;
|
||||
const feature = await this.featureService.getFeature(featureName);
|
||||
res.status(200).json(feature);
|
||||
}
|
||||
|
||||
async toggleEnvironmentOn(
|
||||
req: Request<FeatureStrategyParams, any, any, any>,
|
||||
res: Response,
|
||||
): Promise<void> {
|
||||
const { featureName, environment } = req.params;
|
||||
const { featureName, environment, projectId } = req.params;
|
||||
await this.featureService.updateEnabled(
|
||||
projectId,
|
||||
featureName,
|
||||
environment,
|
||||
true,
|
||||
@ -170,8 +201,9 @@ export default class ProjectFeaturesController extends Controller {
|
||||
req: Request<FeatureStrategyParams, any, any, any>,
|
||||
res: Response,
|
||||
): Promise<void> {
|
||||
const { featureName, environment } = req.params;
|
||||
const { featureName, environment, projectId } = req.params;
|
||||
await this.featureService.updateEnabled(
|
||||
projectId,
|
||||
featureName,
|
||||
environment,
|
||||
false,
|
||||
@ -180,7 +212,7 @@ export default class ProjectFeaturesController extends Controller {
|
||||
res.status(200).end();
|
||||
}
|
||||
|
||||
async createFeatureStrategy(
|
||||
async addStrategy(
|
||||
req: Request<FeatureStrategyParams, any, IStrategyConfig, any>,
|
||||
res: Response,
|
||||
): Promise<void> {
|
||||
@ -194,7 +226,7 @@ export default class ProjectFeaturesController extends Controller {
|
||||
res.status(200).json(featureStrategy);
|
||||
}
|
||||
|
||||
async getFeatureStrategies(
|
||||
async getStrategies(
|
||||
req: Request<FeatureStrategyParams, any, any, any>,
|
||||
res: Response,
|
||||
): Promise<void> {
|
||||
@ -220,6 +252,21 @@ export default class ProjectFeaturesController extends Controller {
|
||||
res.status(200).json(updatedStrategy);
|
||||
}
|
||||
|
||||
async patchStrategy(
|
||||
req: Request<StrategyIdParams, any, Operation[], any>,
|
||||
res: Response,
|
||||
): Promise<void> {
|
||||
const { strategyId } = req.params;
|
||||
const patch = req.body;
|
||||
const strategy = await this.featureService.getStrategy(strategyId);
|
||||
const { newDocument } = applyPatch(strategy, patch);
|
||||
const updatedStrategy = await this.featureService.updateStrategy(
|
||||
strategyId,
|
||||
newDocument,
|
||||
);
|
||||
res.status(200).json(updatedStrategy);
|
||||
}
|
||||
|
||||
async getStrategy(
|
||||
req: Request<StrategyIdParams, any, any, any>,
|
||||
res: Response,
|
||||
@ -230,4 +277,46 @@ export default class ProjectFeaturesController extends Controller {
|
||||
const strategy = await this.featureService.getStrategy(strategyId);
|
||||
res.status(200).json(strategy);
|
||||
}
|
||||
|
||||
async deleteStrategy(
|
||||
req: Request<StrategyIdParams, any, any, any>,
|
||||
res: Response,
|
||||
): Promise<void> {
|
||||
this.logger.info('Deleting strategy');
|
||||
const { strategyId } = req.params;
|
||||
this.logger.info(strategyId);
|
||||
const strategy = await this.featureService.deleteStrategy(strategyId);
|
||||
res.status(200).json(strategy);
|
||||
}
|
||||
|
||||
async updateStrategyParameter(
|
||||
req: Request<
|
||||
StrategyIdParams,
|
||||
any,
|
||||
{ name: string; value: string | number },
|
||||
any
|
||||
>,
|
||||
res: Response,
|
||||
): Promise<void> {
|
||||
const { strategyId } = req.params;
|
||||
const { name, value } = req.body;
|
||||
|
||||
const updatedStrategy =
|
||||
await this.featureService.updateStrategyParameter(
|
||||
strategyId,
|
||||
name,
|
||||
value,
|
||||
);
|
||||
res.status(200).json(updatedStrategy);
|
||||
}
|
||||
|
||||
async getStrategyParameters(
|
||||
req: Request<StrategyIdParams, any, any, any>,
|
||||
res: Response,
|
||||
): Promise<void> {
|
||||
this.logger.info('Getting strategy parameters');
|
||||
const { strategyId } = req.params;
|
||||
const strategy = await this.featureService.getStrategy(strategyId);
|
||||
res.status(200).json(strategy.parameters);
|
||||
}
|
||||
}
|
||||
|
@ -5,7 +5,6 @@ import { IUnleashConfig } from '../../../types/option';
|
||||
import ProjectHealthService from '../../../services/project-health-service';
|
||||
import { Logger } from '../../../logger';
|
||||
import { IArchivedQuery, IProjectParam } from '../../../types/model';
|
||||
import { handleErrors } from '../../util';
|
||||
|
||||
export default class ProjectHealthReport extends Controller {
|
||||
private projectHealthService: ProjectHealthService;
|
||||
@ -31,15 +30,11 @@ export default class ProjectHealthReport extends Controller {
|
||||
): Promise<void> {
|
||||
const { projectId } = req.params;
|
||||
const { archived } = req.query;
|
||||
try {
|
||||
const overview = await this.projectHealthService.getProjectOverview(
|
||||
projectId,
|
||||
archived,
|
||||
);
|
||||
res.json(overview);
|
||||
} catch (e) {
|
||||
handleErrors(res, this.logger, e);
|
||||
}
|
||||
const overview = await this.projectHealthService.getProjectOverview(
|
||||
projectId,
|
||||
archived,
|
||||
);
|
||||
res.json(overview);
|
||||
}
|
||||
|
||||
async getProjectHealthReport(
|
||||
@ -47,17 +42,12 @@ export default class ProjectHealthReport extends Controller {
|
||||
res: Response,
|
||||
): Promise<void> {
|
||||
const { projectId } = req.params;
|
||||
try {
|
||||
const overview =
|
||||
await this.projectHealthService.getProjectHealthReport(
|
||||
projectId,
|
||||
);
|
||||
res.json({
|
||||
version: 2,
|
||||
...overview,
|
||||
});
|
||||
} catch (e) {
|
||||
handleErrors(res, this.logger, e);
|
||||
}
|
||||
const overview = await this.projectHealthService.getProjectHealthReport(
|
||||
projectId,
|
||||
);
|
||||
res.json({
|
||||
version: 2,
|
||||
...overview,
|
||||
});
|
||||
}
|
||||
}
|
||||
|
@ -2,7 +2,6 @@ import { Request, Response } from 'express';
|
||||
import Controller from '../controller';
|
||||
|
||||
import { UPDATE_FEATURE } from '../../types/permissions';
|
||||
import { handleErrors } from '../util';
|
||||
import extractUsername from '../../extract-user';
|
||||
import { IUnleashConfig } from '../../types/option';
|
||||
import { IUnleashServices } from '../../types/services';
|
||||
@ -32,70 +31,48 @@ class TagTypeController extends Controller {
|
||||
}
|
||||
|
||||
async getTagTypes(req: Request, res: Response): Promise<void> {
|
||||
try {
|
||||
const tagTypes = await this.tagTypeService.getAll();
|
||||
res.json({ version, tagTypes });
|
||||
} catch (e) {
|
||||
handleErrors(res, this.logger, e);
|
||||
}
|
||||
const tagTypes = await this.tagTypeService.getAll();
|
||||
res.json({ version, tagTypes });
|
||||
}
|
||||
|
||||
async validate(req: Request, res: Response): Promise<void> {
|
||||
try {
|
||||
await this.tagTypeService.validate(req.body);
|
||||
res.status(200).json({ valid: true, tagType: req.body });
|
||||
} catch (error) {
|
||||
handleErrors(res, this.logger, error);
|
||||
}
|
||||
await this.tagTypeService.validate(req.body);
|
||||
res.status(200).json({ valid: true, tagType: req.body });
|
||||
}
|
||||
|
||||
async createTagType(req: Request, res: Response): Promise<void> {
|
||||
const userName = extractUsername(req);
|
||||
try {
|
||||
const tagType = await this.tagTypeService.createTagType(
|
||||
req.body,
|
||||
userName,
|
||||
);
|
||||
res.status(201).json(tagType);
|
||||
} catch (error) {
|
||||
handleErrors(res, this.logger, error);
|
||||
}
|
||||
const tagType = await this.tagTypeService.createTagType(
|
||||
req.body,
|
||||
userName,
|
||||
);
|
||||
res.status(201).json(tagType);
|
||||
}
|
||||
|
||||
async updateTagType(req: Request, res: Response): Promise<void> {
|
||||
const { description, icon } = req.body;
|
||||
const { name } = req.params;
|
||||
const userName = extractUsername(req);
|
||||
try {
|
||||
await this.tagTypeService.updateTagType(
|
||||
{ name, description, icon },
|
||||
userName,
|
||||
);
|
||||
res.status(200).end();
|
||||
} catch (error) {
|
||||
handleErrors(res, this.logger, error);
|
||||
}
|
||||
|
||||
await this.tagTypeService.updateTagType(
|
||||
{ name, description, icon },
|
||||
userName,
|
||||
);
|
||||
res.status(200).end();
|
||||
}
|
||||
|
||||
async getTagType(req: Request, res: Response): Promise<void> {
|
||||
const { name } = req.params;
|
||||
try {
|
||||
const tagType = await this.tagTypeService.getTagType(name);
|
||||
res.json({ version, tagType });
|
||||
} catch (error) {
|
||||
handleErrors(res, this.logger, error);
|
||||
}
|
||||
|
||||
const tagType = await this.tagTypeService.getTagType(name);
|
||||
res.json({ version, tagType });
|
||||
}
|
||||
|
||||
async deleteTagType(req: Request, res: Response): Promise<void> {
|
||||
const { name } = req.params;
|
||||
const userName = extractUsername(req);
|
||||
try {
|
||||
await this.tagTypeService.deleteTagType(name, userName);
|
||||
res.status(200).end();
|
||||
} catch (error) {
|
||||
handleErrors(res, this.logger, error);
|
||||
}
|
||||
await this.tagTypeService.deleteTagType(name, userName);
|
||||
res.status(200).end();
|
||||
}
|
||||
}
|
||||
export default TagTypeController;
|
||||
|
@ -4,7 +4,6 @@ import { ADMIN } from '../../types/permissions';
|
||||
import UserService from '../../services/user-service';
|
||||
import { AccessService } from '../../services/access-service';
|
||||
import { Logger } from '../../logger';
|
||||
import { handleErrors } from '../util';
|
||||
import { IUnleashConfig } from '../../types/option';
|
||||
import { EmailService } from '../../services/email-service';
|
||||
import ResetTokenService from '../../services/reset-token-service';
|
||||
@ -72,14 +71,10 @@ export default class UserAdminController extends Controller {
|
||||
// eslint-disable-next-line @typescript-eslint/explicit-module-boundary-types
|
||||
async resetPassword(req, res): Promise<void> {
|
||||
const { user } = req;
|
||||
try {
|
||||
const receiver = req.body.id;
|
||||
const resetPasswordUrl =
|
||||
await this.userService.createResetPasswordEmail(receiver, user);
|
||||
res.json({ resetPasswordUrl });
|
||||
} catch (e) {
|
||||
handleErrors(res, this.logger, e);
|
||||
}
|
||||
const receiver = req.body.id;
|
||||
const resetPasswordUrl =
|
||||
await this.userService.createResetPasswordEmail(receiver, user);
|
||||
res.json({ resetPasswordUrl });
|
||||
}
|
||||
|
||||
// eslint-disable-next-line @typescript-eslint/explicit-module-boundary-types
|
||||
@ -103,12 +98,8 @@ export default class UserAdminController extends Controller {
|
||||
}
|
||||
|
||||
async getActiveSessions(req: Request, res: Response): Promise<void> {
|
||||
try {
|
||||
const sessions = await this.sessionService.getActiveSessions();
|
||||
res.json(sessions);
|
||||
} catch (error) {
|
||||
handleErrors(res, this.logger, error);
|
||||
}
|
||||
const sessions = await this.sessionService.getActiveSessions();
|
||||
res.json(sessions);
|
||||
}
|
||||
|
||||
// eslint-disable-next-line @typescript-eslint/explicit-module-boundary-types
|
||||
@ -210,35 +201,23 @@ export default class UserAdminController extends Controller {
|
||||
const { user, params } = req;
|
||||
const { id } = params;
|
||||
|
||||
try {
|
||||
await this.userService.deleteUser(+id, user);
|
||||
res.status(200).send();
|
||||
} catch (error) {
|
||||
handleErrors(res, this.logger, error);
|
||||
}
|
||||
await this.userService.deleteUser(+id, user);
|
||||
res.status(200).send();
|
||||
}
|
||||
|
||||
async validatePassword(req: IAuthRequest, res: Response): Promise<void> {
|
||||
const { password } = req.body;
|
||||
|
||||
try {
|
||||
this.userService.validatePassword(password);
|
||||
res.status(200).send();
|
||||
} catch (e) {
|
||||
res.status(400).send([{ msg: e.message }]);
|
||||
}
|
||||
this.userService.validatePassword(password);
|
||||
res.status(200).send();
|
||||
}
|
||||
|
||||
async changePassword(req: IAuthRequest, res: Response): Promise<void> {
|
||||
const { id } = req.params;
|
||||
const { password } = req.body;
|
||||
|
||||
try {
|
||||
await this.userService.changePassword(+id, password);
|
||||
res.status(200).send();
|
||||
} catch (e) {
|
||||
res.status(400).send([{ msg: e.message }]);
|
||||
}
|
||||
await this.userService.changePassword(+id, password);
|
||||
res.status(200).send();
|
||||
}
|
||||
}
|
||||
|
||||
|
@ -22,7 +22,7 @@ function getSetup() {
|
||||
return {
|
||||
base,
|
||||
featureToggleStore: stores.featureToggleStore,
|
||||
featureStrategiesStore: stores.featureStrategiesStore,
|
||||
featureToggleClientStore: stores.featureToggleClientStore,
|
||||
request: supertest(app),
|
||||
destroy: () => {
|
||||
services.versionService.destroy();
|
||||
@ -35,13 +35,13 @@ function getSetup() {
|
||||
let base;
|
||||
let request;
|
||||
let destroy;
|
||||
let featureStrategiesStore;
|
||||
let featureToggleClientStore;
|
||||
|
||||
beforeEach(() => {
|
||||
const setup = getSetup();
|
||||
base = setup.base;
|
||||
request = setup.request;
|
||||
featureStrategiesStore = setup.featureStrategiesStore;
|
||||
featureToggleClientStore = setup.featureToggleClientStore;
|
||||
destroy = setup.destroy;
|
||||
});
|
||||
|
||||
@ -114,7 +114,7 @@ test('if caching is not enabled all calls goes to service', async () => {
|
||||
|
||||
test('fetch single feature', async () => {
|
||||
expect.assertions(1);
|
||||
await featureStrategiesStore.createFeature({
|
||||
await featureToggleClientStore.createFeature({
|
||||
name: 'test_',
|
||||
strategies: [{ name: 'default' }],
|
||||
});
|
||||
@ -130,10 +130,10 @@ test('fetch single feature', async () => {
|
||||
|
||||
test('support name prefix', async () => {
|
||||
expect.assertions(2);
|
||||
await featureStrategiesStore.createFeature({ name: 'a_test1' });
|
||||
await featureStrategiesStore.createFeature({ name: 'a_test2' });
|
||||
await featureStrategiesStore.createFeature({ name: 'b_test1' });
|
||||
await featureStrategiesStore.createFeature({ name: 'b_test2' });
|
||||
await featureToggleClientStore.createFeature({ name: 'a_test1' });
|
||||
await featureToggleClientStore.createFeature({ name: 'a_test2' });
|
||||
await featureToggleClientStore.createFeature({ name: 'b_test1' });
|
||||
await featureToggleClientStore.createFeature({ name: 'b_test2' });
|
||||
|
||||
const namePrefix = 'b_';
|
||||
|
||||
@ -149,11 +149,11 @@ test('support name prefix', async () => {
|
||||
|
||||
test('support filtering on project', async () => {
|
||||
expect.assertions(2);
|
||||
await featureStrategiesStore.createFeature({
|
||||
await featureToggleClientStore.createFeature({
|
||||
name: 'a_test1',
|
||||
project: 'projecta',
|
||||
});
|
||||
await featureStrategiesStore.createFeature({
|
||||
await featureToggleClientStore.createFeature({
|
||||
name: 'b_test2',
|
||||
project: 'projectb',
|
||||
});
|
||||
|
@ -1,6 +1,5 @@
|
||||
import memoizee from 'memoizee';
|
||||
import { Request, Response } from 'express';
|
||||
import { handleErrors } from '../util';
|
||||
import Controller from '../controller';
|
||||
import { IUnleashServices } from '../../types/services';
|
||||
import { IUnleashConfig } from '../../types/option';
|
||||
@ -8,6 +7,7 @@ import FeatureToggleServiceV2 from '../../services/feature-toggle-service-v2';
|
||||
import { Logger } from '../../logger';
|
||||
import { querySchema } from '../../schema/feature-schema';
|
||||
import { IFeatureToggleQuery } from '../../types/model';
|
||||
import NotFoundError from '../../error/notfound-error';
|
||||
|
||||
const version = 2;
|
||||
|
||||
@ -51,28 +51,25 @@ export default class FeatureController extends Controller {
|
||||
}
|
||||
|
||||
async getAll(req: Request, res: Response): Promise<void> {
|
||||
try {
|
||||
const query = await this.prepQuery(req.query);
|
||||
let features;
|
||||
if (this.cache) {
|
||||
features = await this.cachedFeatures(query);
|
||||
} else {
|
||||
features = await this.featureToggleServiceV2.getClientFeatures(
|
||||
query,
|
||||
);
|
||||
}
|
||||
res.json({ version, features });
|
||||
} catch (e) {
|
||||
handleErrors(res, this.logger, e);
|
||||
const query = await this.prepQuery(req.query);
|
||||
let features;
|
||||
if (this.cache) {
|
||||
features = await this.cachedFeatures(query);
|
||||
} else {
|
||||
features = await this.featureToggleServiceV2.getClientFeatures(
|
||||
query,
|
||||
);
|
||||
}
|
||||
res.json({ version, features });
|
||||
}
|
||||
|
||||
async prepQuery({
|
||||
tag,
|
||||
project,
|
||||
namePrefix,
|
||||
environment,
|
||||
}: IFeatureToggleQuery): Promise<IFeatureToggleQuery> {
|
||||
if (!tag && !project && !namePrefix) {
|
||||
if (!tag && !project && !namePrefix && !environment) {
|
||||
return null;
|
||||
}
|
||||
const tagQuery = this.paramToArray(tag);
|
||||
@ -81,6 +78,7 @@ export default class FeatureController extends Controller {
|
||||
tag: tagQuery,
|
||||
project: projectQuery,
|
||||
namePrefix,
|
||||
environment,
|
||||
});
|
||||
if (query.tag) {
|
||||
query.tag = query.tag.map((q) => q.split(':'));
|
||||
@ -97,18 +95,25 @@ export default class FeatureController extends Controller {
|
||||
}
|
||||
|
||||
async getFeatureToggle(
|
||||
req: Request<{ featureName: string }, any, any, any>,
|
||||
req: Request<
|
||||
{ featureName: string },
|
||||
any,
|
||||
any,
|
||||
{ environment?: string }
|
||||
>,
|
||||
res: Response,
|
||||
): Promise<void> {
|
||||
try {
|
||||
const name = req.params.featureName;
|
||||
const featureToggle = await this.featureToggleServiceV2.getFeature(
|
||||
name,
|
||||
);
|
||||
res.json(featureToggle).end();
|
||||
} catch (err) {
|
||||
res.status(404).json({ error: 'Could not find feature' });
|
||||
const name = req.params.featureName;
|
||||
const { environment } = req.query;
|
||||
const toggles = await this.featureToggleServiceV2.getClientFeatures({
|
||||
namePrefix: name,
|
||||
environment,
|
||||
});
|
||||
const toggle = toggles.find((t) => t.name === name);
|
||||
if (!toggle) {
|
||||
throw new NotFoundError(`Could not find feature toggle ${name}`);
|
||||
}
|
||||
res.json(toggle).end();
|
||||
}
|
||||
}
|
||||
|
||||
|
@ -5,6 +5,7 @@ import getApp from '../../app';
|
||||
import { createTestConfig } from '../../../test/config/test-config';
|
||||
import { clientMetricsSchema } from '../../services/client-metrics/client-metrics-schema';
|
||||
import { createServices } from '../../services';
|
||||
import { IUnleashStores } from '../../types';
|
||||
|
||||
const eventBus = new EventEmitter();
|
||||
|
||||
@ -27,7 +28,7 @@ function getSetup() {
|
||||
}
|
||||
|
||||
let request;
|
||||
let stores;
|
||||
let stores: IUnleashStores;
|
||||
let destroy;
|
||||
|
||||
beforeEach(() => {
|
||||
@ -166,7 +167,7 @@ test('schema allow yes=<string nbr>', () => {
|
||||
|
||||
test('should set lastSeen on toggle', async () => {
|
||||
expect.assertions(1);
|
||||
stores.featureToggleStore.createFeature('default', {
|
||||
stores.featureToggleStore.create('default', {
|
||||
name: 'toggleLastSeen',
|
||||
});
|
||||
await request
|
||||
|
@ -30,18 +30,8 @@ export default class ClientMetricsController extends Controller {
|
||||
const data = req.body;
|
||||
const clientIp = req.ip;
|
||||
|
||||
try {
|
||||
await this.metrics.registerClientMetrics(data, clientIp);
|
||||
return res.status(202).end();
|
||||
} catch (e) {
|
||||
this.logger.warn('Failed to store metrics', e);
|
||||
switch (e.name) {
|
||||
case 'ValidationError':
|
||||
return res.status(400).end();
|
||||
default:
|
||||
return res.status(500).end();
|
||||
}
|
||||
}
|
||||
await this.metrics.registerClientMetrics(data, clientIp);
|
||||
return res.status(202).end();
|
||||
}
|
||||
}
|
||||
|
||||
|
@ -24,18 +24,8 @@ export default class RegisterController extends Controller {
|
||||
|
||||
async handleRegister(req: Request, res: Response): Promise<void> {
|
||||
const data = req.body;
|
||||
try {
|
||||
const clientIp = req.ip;
|
||||
await this.metrics.registerClient(data, clientIp);
|
||||
return res.status(202).end();
|
||||
} catch (err) {
|
||||
this.logger.warn('failed to register client', err);
|
||||
switch (err.name) {
|
||||
case 'ValidationError':
|
||||
return res.status(400).end();
|
||||
default:
|
||||
return res.status(500).end();
|
||||
}
|
||||
}
|
||||
const clientIp = req.ip;
|
||||
await this.metrics.registerClient(data, clientIp);
|
||||
return res.status(202).end();
|
||||
}
|
||||
}
|
||||
|
@ -98,6 +98,20 @@ export default class Controller {
|
||||
);
|
||||
}
|
||||
|
||||
patch(
|
||||
path: string,
|
||||
handler: IRequestHandler,
|
||||
permission?: string,
|
||||
...acceptedContentTypes: string[]
|
||||
): void {
|
||||
this.app.patch(
|
||||
path,
|
||||
checkPermission(permission),
|
||||
requireContentType(...acceptedContentTypes),
|
||||
this.wrap(handler.bind(this)),
|
||||
);
|
||||
}
|
||||
|
||||
delete(path: string, handler: IRequestHandler, permission?: string): void {
|
||||
this.app.delete(
|
||||
path,
|
||||
|
@ -35,6 +35,7 @@ export const handleErrors: (
|
||||
case 'NotFoundError':
|
||||
return res.status(404).json(error).end();
|
||||
case 'InvalidOperationError':
|
||||
return res.status(403).json(error).end();
|
||||
case 'NameExistsError':
|
||||
return res.status(409).json(error).end();
|
||||
case 'ValidationError':
|
||||
|
6
src/lib/schema/project-schema.ts
Normal file
6
src/lib/schema/project-schema.ts
Normal file
@ -0,0 +1,6 @@
|
||||
import joi from 'joi';
|
||||
|
||||
export const addEnvironment = joi
|
||||
.object()
|
||||
.keys({ environment: joi.string().required() })
|
||||
.options({ stripUnknown: true, allowUnknown: false, abortEarly: false });
|
@ -231,7 +231,7 @@ export class AccessService {
|
||||
}
|
||||
|
||||
async createDefaultProjectRoles(
|
||||
owner: User,
|
||||
owner: IUser,
|
||||
projectId: string,
|
||||
): Promise<void> {
|
||||
if (!projectId) {
|
||||
|
@ -140,7 +140,7 @@ export default class ClientMetricsService {
|
||||
): Promise<void> {
|
||||
const value = await clientMetricsSchema.validateAsync(data);
|
||||
const toggleNames = Object.keys(value.bucket.toggles);
|
||||
await this.featureToggleStore.updateLastSeenForToggles(toggleNames);
|
||||
await this.featureToggleStore.setLastSeen(toggleNames);
|
||||
await this.clientMetricsStore.insert(value);
|
||||
await this.clientInstanceStore.insert({
|
||||
appName: value.appName,
|
||||
@ -262,7 +262,7 @@ export default class ClientMetricsService {
|
||||
this.clientApplicationsStore.get(appName),
|
||||
this.clientInstanceStore.getByAppName(appName),
|
||||
this.strategyStore.getAll(),
|
||||
this.featureToggleStore.getFeatures(false),
|
||||
this.featureToggleStore.getAll(),
|
||||
]);
|
||||
|
||||
return {
|
||||
|
@ -66,13 +66,19 @@ export default class EnvironmentService {
|
||||
throw new NotFoundError(`Could not find environment ${name}`);
|
||||
}
|
||||
|
||||
async connectProjectToEnvironment(
|
||||
async addEnvironmentToProject(
|
||||
environment: string,
|
||||
projectId: string,
|
||||
): Promise<void> {
|
||||
try {
|
||||
await this.environmentStore.connectProject(environment, projectId);
|
||||
await this.environmentStore.connectFeatures(environment, projectId);
|
||||
await this.featureEnvironmentStore.connectProject(
|
||||
environment,
|
||||
projectId,
|
||||
);
|
||||
await this.featureEnvironmentStore.connectFeatures(
|
||||
environment,
|
||||
projectId,
|
||||
);
|
||||
} catch (e) {
|
||||
if (e.code === UNIQUE_CONSTRAINT_VIOLATION) {
|
||||
throw new NameExistsError(
|
||||
@ -87,11 +93,11 @@ export default class EnvironmentService {
|
||||
environment: string,
|
||||
projectId: string,
|
||||
): Promise<void> {
|
||||
await this.featureEnvironmentStore.disconnectEnvironmentFromProject(
|
||||
await this.featureEnvironmentStore.disconnectFeatures(
|
||||
environment,
|
||||
projectId,
|
||||
);
|
||||
await this.environmentStore.disconnectProjectFromEnv(
|
||||
await this.featureEnvironmentStore.disconnectProject(
|
||||
environment,
|
||||
projectId,
|
||||
);
|
||||
|
@ -1,10 +1,10 @@
|
||||
/* eslint-disable prettier/prettier */
|
||||
import { IUnleashConfig } from '../types/option';
|
||||
import { IUnleashStores } from '../types/stores';
|
||||
import { Logger } from '../logger';
|
||||
import BadDataError from '../error/bad-data-error';
|
||||
import { FOREIGN_KEY_VIOLATION } from '../error/db-error';
|
||||
import NameExistsError from '../error/name-exists-error';
|
||||
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,
|
||||
@ -22,9 +22,7 @@ import {
|
||||
FeatureConfigurationClient,
|
||||
IFeatureStrategiesStore,
|
||||
} from '../types/stores/feature-strategies-store';
|
||||
import { IFeatureTypeStore } from '../types/stores/feature-type-store';
|
||||
import { IEventStore } from '../types/stores/event-store';
|
||||
import { IEnvironmentStore } from '../types/stores/environment-store';
|
||||
import { IProjectStore } from '../types/stores/project-store';
|
||||
import { IFeatureTagStore } from '../types/stores/feature-tag-store';
|
||||
import { IFeatureToggleStore } from '../types/stores/feature-toggle-store';
|
||||
@ -34,11 +32,13 @@ import {
|
||||
FeatureToggleWithEnvironment,
|
||||
FeatureToggleWithEnvironmentLegacy,
|
||||
IFeatureEnvironmentInfo,
|
||||
IFeatureOverview,
|
||||
IFeatureStrategy,
|
||||
IFeatureToggleQuery,
|
||||
IStrategyConfig,
|
||||
} from '../types/model';
|
||||
import { IFeatureEnvironmentStore } from '../types/stores/feature-environment-store';
|
||||
import { IFeatureToggleClientStore } from '../types/stores/feature-toggle-client-store';
|
||||
|
||||
class FeatureToggleServiceV2 {
|
||||
private logger: Logger;
|
||||
@ -47,37 +47,33 @@ class FeatureToggleServiceV2 {
|
||||
|
||||
private featureToggleStore: IFeatureToggleStore;
|
||||
|
||||
private featureToggleClientStore: IFeatureToggleClientStore;
|
||||
|
||||
private featureTagStore: IFeatureTagStore;
|
||||
|
||||
private featureEnvironmentStore: IFeatureEnvironmentStore;
|
||||
|
||||
private projectStore: IProjectStore;
|
||||
|
||||
private environmentStore: IEnvironmentStore;
|
||||
|
||||
private eventStore: IEventStore;
|
||||
|
||||
private featureTypeStore: IFeatureTypeStore;
|
||||
|
||||
constructor(
|
||||
{
|
||||
featureStrategiesStore,
|
||||
featureToggleStore,
|
||||
featureToggleClientStore,
|
||||
projectStore,
|
||||
eventStore,
|
||||
featureTagStore,
|
||||
environmentStore,
|
||||
featureTypeStore,
|
||||
featureEnvironmentStore,
|
||||
}: Pick<
|
||||
IUnleashStores,
|
||||
| 'featureStrategiesStore'
|
||||
| 'featureToggleStore'
|
||||
| 'featureToggleClientStore'
|
||||
| 'projectStore'
|
||||
| 'eventStore'
|
||||
| 'featureTagStore'
|
||||
| 'environmentStore'
|
||||
| 'featureTypeStore'
|
||||
| 'featureEnvironmentStore'
|
||||
>,
|
||||
{ getLogger }: Pick<IUnleashConfig, 'getLogger'>,
|
||||
@ -85,11 +81,10 @@ class FeatureToggleServiceV2 {
|
||||
this.logger = getLogger('services/feature-toggle-service-v2.ts');
|
||||
this.featureStrategiesStore = featureStrategiesStore;
|
||||
this.featureToggleStore = featureToggleStore;
|
||||
this.featureToggleClientStore = featureToggleClientStore;
|
||||
this.featureTagStore = featureTagStore;
|
||||
this.projectStore = projectStore;
|
||||
this.eventStore = eventStore;
|
||||
this.environmentStore = environmentStore;
|
||||
this.featureTypeStore = featureTypeStore;
|
||||
this.featureEnvironmentStore = featureEnvironmentStore;
|
||||
}
|
||||
|
||||
@ -101,17 +96,17 @@ class FeatureToggleServiceV2 {
|
||||
*/
|
||||
async createStrategy(
|
||||
strategyConfig: Omit<IStrategyConfig, 'id'>,
|
||||
projectName: string,
|
||||
projectId: string,
|
||||
featureName: string,
|
||||
environment: string = GLOBAL_ENV,
|
||||
): Promise<IStrategyConfig> {
|
||||
try {
|
||||
const newFeatureStrategy =
|
||||
await this.featureStrategiesStore.createStrategyConfig({
|
||||
await this.featureStrategiesStore.createStrategyFeatureEnv({
|
||||
strategyName: strategyConfig.name,
|
||||
constraints: strategyConfig.constraints,
|
||||
parameters: strategyConfig.parameters,
|
||||
projectName,
|
||||
projectId,
|
||||
featureName,
|
||||
environment,
|
||||
});
|
||||
@ -132,26 +127,72 @@ class FeatureToggleServiceV2 {
|
||||
}
|
||||
|
||||
/**
|
||||
* PUT /api/admin/projects/:projectName/features/:featureName/strategies/:strategyId ?
|
||||
* PUT /api/admin/projects/:projectId/features/:featureName/strategies/:strategyId ?
|
||||
* {
|
||||
*
|
||||
* }
|
||||
* @param id
|
||||
* @param updates
|
||||
*/
|
||||
|
||||
// TODO: verify projectId is not changed from URL!
|
||||
async updateStrategy(
|
||||
id: string,
|
||||
updates: Partial<IFeatureStrategy>,
|
||||
): Promise<IFeatureStrategy> {
|
||||
const exists = await this.featureStrategiesStore.exists(id);
|
||||
if (exists) {
|
||||
return this.featureStrategiesStore.updateStrategy(id, updates);
|
||||
): Promise<IStrategyConfig> {
|
||||
const existingStrategy = await this.featureStrategiesStore.get(id);
|
||||
if (existingStrategy.id === id) {
|
||||
const strategy = await this.featureStrategiesStore.updateStrategy(
|
||||
id,
|
||||
updates,
|
||||
);
|
||||
return {
|
||||
id: strategy.id,
|
||||
name: strategy.strategyName,
|
||||
constraints: strategy.constraints || [],
|
||||
parameters: strategy.parameters,
|
||||
};
|
||||
}
|
||||
throw new NotFoundError(`Could not find strategy with id ${id}`);
|
||||
}
|
||||
|
||||
// TODO: verify projectId is not changed from URL!
|
||||
async updateStrategyParameter(
|
||||
id: string,
|
||||
name: string,
|
||||
value: string | number,
|
||||
): Promise<IStrategyConfig> {
|
||||
const existingStrategy = await this.featureStrategiesStore.get(id);
|
||||
if (existingStrategy.id === id) {
|
||||
existingStrategy.parameters[name] = value;
|
||||
const strategy = await this.featureStrategiesStore.updateStrategy(
|
||||
id,
|
||||
existingStrategy,
|
||||
);
|
||||
return {
|
||||
id: strategy.id,
|
||||
name: strategy.strategyName,
|
||||
constraints: strategy.constraints || [],
|
||||
parameters: strategy.parameters,
|
||||
};
|
||||
}
|
||||
throw new NotFoundError(`Could not find strategy with id ${id}`);
|
||||
}
|
||||
|
||||
/**
|
||||
* DELETE /api/admin/projects/:projectId/features/:featureName/strategies/:strategyId ?
|
||||
* {
|
||||
*
|
||||
* }
|
||||
* @param id
|
||||
* @param updates
|
||||
*/
|
||||
async deleteStrategy(id: string): Promise<void> {
|
||||
return this.featureStrategiesStore.delete(id);
|
||||
}
|
||||
|
||||
async getStrategiesForEnvironment(
|
||||
projectName: string,
|
||||
project: string,
|
||||
featureName: string,
|
||||
environment: string = GLOBAL_ENV,
|
||||
): Promise<IStrategyConfig[]> {
|
||||
@ -161,8 +202,8 @@ class FeatureToggleServiceV2 {
|
||||
);
|
||||
if (hasEnv) {
|
||||
const featureStrategies =
|
||||
await this.featureStrategiesStore.getStrategiesForFeature(
|
||||
projectName,
|
||||
await this.featureStrategiesStore.getStrategiesForFeatureEnv(
|
||||
project,
|
||||
featureName,
|
||||
environment,
|
||||
);
|
||||
@ -179,7 +220,7 @@ class FeatureToggleServiceV2 {
|
||||
}
|
||||
|
||||
/**
|
||||
* GET /api/admin/projects/:projectName/features/:featureName
|
||||
* GET /api/admin/projects/:project/features/:featureName
|
||||
* @param featureName
|
||||
* @param archived - return archived or non archived toggles
|
||||
*/
|
||||
@ -187,20 +228,27 @@ class FeatureToggleServiceV2 {
|
||||
featureName: string,
|
||||
archived: boolean = false,
|
||||
): Promise<FeatureToggleWithEnvironment> {
|
||||
return this.featureStrategiesStore.getFeatureToggleAdmin(
|
||||
return this.featureStrategiesStore.getFeatureToggleWithEnvs(
|
||||
featureName,
|
||||
archived,
|
||||
);
|
||||
}
|
||||
|
||||
async getFeatureMetadata(featureName: string): Promise<FeatureToggle> {
|
||||
return this.featureToggleStore.get(featureName);
|
||||
}
|
||||
|
||||
async getClientFeatures(
|
||||
query?: IFeatureToggleQuery,
|
||||
archived: boolean = false,
|
||||
): Promise<FeatureConfigurationClient[]> {
|
||||
return this.featureStrategiesStore.getFeatures(query, archived, false);
|
||||
return this.featureToggleClientStore.getClient(query);
|
||||
}
|
||||
|
||||
/**
|
||||
*
|
||||
* Warn: Legacy!
|
||||
*
|
||||
*
|
||||
* Used to retrieve metadata of all feature toggles defined in Unleash.
|
||||
* @param query - Allow you to limit search based on criteria such as project, tags, namePrefix. See @IFeatureToggleQuery
|
||||
* @param archived - Return archived or active toggles
|
||||
@ -211,13 +259,23 @@ class FeatureToggleServiceV2 {
|
||||
query?: IFeatureToggleQuery,
|
||||
archived: boolean = false,
|
||||
): Promise<FeatureToggle[]> {
|
||||
return this.featureStrategiesStore.getFeatures(query, archived, true);
|
||||
return this.featureToggleClientStore.getAdmin(query, archived);
|
||||
}
|
||||
|
||||
async getFeatureOverview(
|
||||
projectId: string,
|
||||
archived: boolean = false,
|
||||
): Promise<IFeatureOverview[]> {
|
||||
return this.featureStrategiesStore.getFeatureOverview(
|
||||
projectId,
|
||||
archived,
|
||||
);
|
||||
}
|
||||
|
||||
async getFeatureToggle(
|
||||
featureName: string,
|
||||
): Promise<FeatureToggleWithEnvironment> {
|
||||
return this.featureStrategiesStore.getFeatureToggleAdmin(
|
||||
return this.featureStrategiesStore.getFeatureToggleWithEnvs(
|
||||
featureName,
|
||||
false,
|
||||
);
|
||||
@ -235,11 +293,11 @@ class FeatureToggleServiceV2 {
|
||||
const featureData = await featureMetadataSchema.validateAsync(
|
||||
value,
|
||||
);
|
||||
const createdToggle = await this.featureToggleStore.createFeature(
|
||||
const createdToggle = await this.featureToggleStore.create(
|
||||
projectId,
|
||||
featureData,
|
||||
);
|
||||
await this.environmentStore.connectFeatureToEnvironmentsForProject(
|
||||
await this.featureEnvironmentStore.connectFeatureToEnvironmentsForProject(
|
||||
featureData.name,
|
||||
projectId,
|
||||
);
|
||||
@ -263,15 +321,19 @@ class FeatureToggleServiceV2 {
|
||||
userName: string,
|
||||
): Promise<FeatureToggle> {
|
||||
const featureName = updatedFeature.name;
|
||||
this.logger.info(
|
||||
`${userName} updates feature toggle ${featureName}`,
|
||||
);
|
||||
this.logger.info(`${userName} updates feature toggle ${featureName}`);
|
||||
|
||||
const featureToggle = await this.featureToggleStore.updateFeature(
|
||||
projectId,
|
||||
const featureData = await featureMetadataSchema.validateAsync(
|
||||
updatedFeature,
|
||||
);
|
||||
const tags = await this.featureTagStore.getAllTagsForFeature(featureName);
|
||||
|
||||
const featureToggle = await this.featureToggleStore.update(
|
||||
projectId,
|
||||
featureData,
|
||||
);
|
||||
const tags = await this.featureTagStore.getAllTagsForFeature(
|
||||
featureName,
|
||||
);
|
||||
|
||||
await this.eventStore.store({
|
||||
type: FEATURE_METADATA_UPDATED,
|
||||
@ -293,7 +355,7 @@ class FeatureToggleServiceV2 {
|
||||
toggleName: string,
|
||||
environment: string = GLOBAL_ENV,
|
||||
): Promise<void> {
|
||||
await this.featureStrategiesStore.removeAllStrategiesForEnv(
|
||||
await this.featureStrategiesStore.removeAllStrategiesForFeatureEnv(
|
||||
toggleName,
|
||||
environment,
|
||||
);
|
||||
@ -322,7 +384,7 @@ class FeatureToggleServiceV2 {
|
||||
featureName,
|
||||
);
|
||||
const strategies =
|
||||
await this.featureStrategiesStore.getStrategiesForFeature(
|
||||
await this.featureStrategiesStore.getStrategiesForFeatureEnv(
|
||||
project,
|
||||
featureName,
|
||||
environment,
|
||||
@ -359,7 +421,7 @@ class FeatureToggleServiceV2 {
|
||||
async validateUniqueFeatureName(name: string): Promise<void> {
|
||||
let msg;
|
||||
try {
|
||||
const feature = await this.featureToggleStore.hasFeature(name);
|
||||
const feature = await this.featureToggleStore.get(name);
|
||||
msg = feature.archived
|
||||
? 'An archived toggle with that name already exists'
|
||||
: 'A toggle with that name already exists';
|
||||
@ -378,12 +440,12 @@ class FeatureToggleServiceV2 {
|
||||
isStale: boolean,
|
||||
userName: string,
|
||||
): Promise<any> {
|
||||
const feature = await this.featureToggleStore.getFeatureMetadata(
|
||||
const feature = await this.featureToggleStore.get(featureName);
|
||||
feature.stale = isStale;
|
||||
await this.featureToggleStore.update(feature.project, feature);
|
||||
const tags = await this.featureTagStore.getAllTagsForFeature(
|
||||
featureName,
|
||||
);
|
||||
feature.stale = isStale;
|
||||
await this.featureToggleStore.updateFeature(feature.project, feature);
|
||||
const tags = await this.featureTagStore.getAllTagsForFeature(featureName);
|
||||
const data = await this.getFeatureToggleLegacy(featureName);
|
||||
|
||||
await this.eventStore.store({
|
||||
@ -396,8 +458,8 @@ class FeatureToggleServiceV2 {
|
||||
}
|
||||
|
||||
async archiveToggle(name: string, userName: string): Promise<void> {
|
||||
await this.featureToggleStore.hasFeature(name);
|
||||
await this.featureToggleStore.archiveFeature(name);
|
||||
await this.featureToggleStore.get(name);
|
||||
await this.featureToggleStore.archive(name);
|
||||
const tags =
|
||||
(await this.featureTagStore.getAllTagsForFeature(name)) || [];
|
||||
await this.eventStore.store({
|
||||
@ -409,6 +471,7 @@ class FeatureToggleServiceV2 {
|
||||
}
|
||||
|
||||
async updateEnabled(
|
||||
projectId: string,
|
||||
featureName: string,
|
||||
environment: string,
|
||||
enabled: boolean,
|
||||
@ -419,16 +482,29 @@ class FeatureToggleServiceV2 {
|
||||
environment,
|
||||
featureName,
|
||||
);
|
||||
|
||||
if (hasEnvironment) {
|
||||
await this.featureEnvironmentStore.toggleEnvironmentEnabledStatus(
|
||||
environment,
|
||||
if (enabled) {
|
||||
const strategies = await this.getStrategiesForEnvironment(
|
||||
projectId,
|
||||
featureName,
|
||||
enabled,
|
||||
environment,
|
||||
);
|
||||
const feature = await this.featureToggleStore.getFeatureMetadata(
|
||||
if (strategies.length === 0) {
|
||||
throw new InvalidOperationError(
|
||||
'You can not enable the environment before it has strategies',
|
||||
);
|
||||
}
|
||||
}
|
||||
await this.featureEnvironmentStore.setEnvironmentEnabledStatus(
|
||||
environment,
|
||||
featureName,
|
||||
enabled,
|
||||
);
|
||||
const feature = await this.featureToggleStore.get(featureName);
|
||||
const tags = await this.featureTagStore.getAllTagsForFeature(
|
||||
featureName,
|
||||
);
|
||||
const tags = await this.featureTagStore.getAllTagsForFeature(featureName);
|
||||
const data = await this.getFeatureToggleLegacy(featureName);
|
||||
|
||||
await this.eventStore.store({
|
||||
@ -446,17 +522,19 @@ class FeatureToggleServiceV2 {
|
||||
|
||||
// @deprecated
|
||||
async toggle(
|
||||
projectId: string,
|
||||
featureName: string,
|
||||
environment: string,
|
||||
userName: string,
|
||||
): Promise<FeatureToggle> {
|
||||
await this.featureToggleStore.hasFeature(featureName);
|
||||
await this.featureToggleStore.get(featureName);
|
||||
const isEnabled =
|
||||
await this.featureEnvironmentStore.isEnvironmentEnabled(
|
||||
featureName,
|
||||
environment,
|
||||
);
|
||||
return this.updateEnabled(
|
||||
projectId,
|
||||
featureName,
|
||||
environment,
|
||||
!isEnabled,
|
||||
@ -464,13 +542,20 @@ class FeatureToggleServiceV2 {
|
||||
);
|
||||
}
|
||||
|
||||
async getFeatureToggleLegacy(featureName: string): Promise<FeatureToggleWithEnvironmentLegacy> {
|
||||
const feature = await this.featureStrategiesStore.getFeatureToggleAdmin(featureName);
|
||||
const globalEnv = feature.environments.find(e => e.name === GLOBAL_ENV);
|
||||
async getFeatureToggleLegacy(
|
||||
featureName: string,
|
||||
): Promise<FeatureToggleWithEnvironmentLegacy> {
|
||||
const feature =
|
||||
await this.featureStrategiesStore.getFeatureToggleWithEnvs(
|
||||
featureName,
|
||||
);
|
||||
const globalEnv = feature.environments.find(
|
||||
(e) => e.name === GLOBAL_ENV,
|
||||
);
|
||||
const strategies = globalEnv?.strategies || [];
|
||||
const enabled = globalEnv?.enabled || false;
|
||||
|
||||
return {...feature, enabled, strategies };
|
||||
return { ...feature, enabled, strategies };
|
||||
}
|
||||
|
||||
// @deprecated
|
||||
@ -482,14 +567,13 @@ class FeatureToggleServiceV2 {
|
||||
userName: string,
|
||||
event?: string,
|
||||
): Promise<any> {
|
||||
const feature = await this.featureToggleStore.getFeatureMetadata(
|
||||
const feature = await this.featureToggleStore.get(featureName);
|
||||
feature[field] = value;
|
||||
await this.featureToggleStore.update(feature.project, feature);
|
||||
const tags = await this.featureTagStore.getAllTagsForFeature(
|
||||
featureName,
|
||||
);
|
||||
feature[field] = value;
|
||||
await this.featureToggleStore.updateFeature(feature.project, feature);
|
||||
const tags = await this.featureTagStore.getAllTagsForFeature(featureName);
|
||||
|
||||
|
||||
// Workaround to support pre 4.1 format
|
||||
const data = await this.getFeatureToggleLegacy(featureName);
|
||||
|
||||
@ -518,7 +602,7 @@ class FeatureToggleServiceV2 {
|
||||
}
|
||||
|
||||
async reviveToggle(featureName: string, userName: string): Promise<void> {
|
||||
const data = await this.featureToggleStore.reviveFeature(featureName);
|
||||
const data = await this.featureToggleStore.revive(featureName);
|
||||
const tags = await this.featureTagStore.getAllTagsForFeature(
|
||||
featureName,
|
||||
);
|
||||
@ -533,14 +617,11 @@ class FeatureToggleServiceV2 {
|
||||
async getMetadataForAllFeatures(
|
||||
archived: boolean,
|
||||
): Promise<FeatureToggle[]> {
|
||||
return this.featureToggleStore.getFeatures(archived);
|
||||
return this.featureToggleStore.getAll({ archived });
|
||||
}
|
||||
|
||||
async getProjectId(name: string): Promise<string> {
|
||||
const { project } = await this.featureToggleStore.getFeatureMetadata(
|
||||
name,
|
||||
);
|
||||
return project;
|
||||
return this.featureToggleStore.getProjectId(name);
|
||||
}
|
||||
}
|
||||
|
||||
|
@ -58,7 +58,11 @@ export const createServices = (
|
||||
const featureToggleServiceV2 = new FeatureToggleServiceV2(stores, config);
|
||||
const environmentService = new EnvironmentService(stores, config);
|
||||
const featureTagService = new FeatureTagService(stores, config);
|
||||
const projectHealthService = new ProjectHealthService(stores, config);
|
||||
const projectHealthService = new ProjectHealthService(
|
||||
stores,
|
||||
config,
|
||||
featureToggleServiceV2,
|
||||
);
|
||||
const projectService = new ProjectService(
|
||||
stores,
|
||||
config,
|
||||
|
@ -16,6 +16,7 @@ import { IFeatureToggleStore } from '../types/stores/feature-toggle-store';
|
||||
import { IFeatureTypeStore } from '../types/stores/feature-type-store';
|
||||
import { IProjectStore } from '../types/stores/project-store';
|
||||
import Timer = NodeJS.Timer;
|
||||
import FeatureToggleServiceV2 from './feature-toggle-service-v2';
|
||||
|
||||
export default class ProjectHealthService {
|
||||
private logger: Logger;
|
||||
@ -30,6 +31,8 @@ export default class ProjectHealthService {
|
||||
|
||||
private healthRatingTimer: Timer;
|
||||
|
||||
private featureToggleService: FeatureToggleServiceV2;
|
||||
|
||||
constructor(
|
||||
{
|
||||
projectStore,
|
||||
@ -40,6 +43,7 @@ export default class ProjectHealthService {
|
||||
'projectStore' | 'featureTypeStore' | 'featureToggleStore'
|
||||
>,
|
||||
{ getLogger }: Pick<IUnleashConfig, 'getLogger'>,
|
||||
featureToggleService: FeatureToggleServiceV2,
|
||||
) {
|
||||
this.logger = getLogger('services/project-health-service.ts');
|
||||
this.projectStore = projectStore;
|
||||
@ -50,14 +54,16 @@ export default class ProjectHealthService {
|
||||
() => this.setHealthRating(),
|
||||
MILLISECONDS_IN_ONE_HOUR,
|
||||
).unref();
|
||||
this.featureToggleService = featureToggleService;
|
||||
}
|
||||
|
||||
// TODO: duplicate from project-service.
|
||||
async getProjectOverview(
|
||||
projectId: string,
|
||||
archived: boolean = false,
|
||||
): Promise<IProjectOverview> {
|
||||
const project = await this.projectStore.get(projectId);
|
||||
const features = await this.projectStore.getProjectOverview(
|
||||
const features = await this.featureToggleService.getFeatureOverview(
|
||||
projectId,
|
||||
archived,
|
||||
);
|
||||
@ -120,7 +126,7 @@ export default class ProjectHealthService {
|
||||
}
|
||||
|
||||
async calculateHealthRating(project: IProject): Promise<number> {
|
||||
const toggles = await this.featureToggleStore.getFeaturesBy({
|
||||
const toggles = await this.featureToggleStore.getAll({
|
||||
project: project.id,
|
||||
});
|
||||
|
||||
|
@ -24,6 +24,7 @@ import { GLOBAL_ENV } from '../types/environment';
|
||||
import { IEnvironmentStore } from '../types/stores/environment-store';
|
||||
import { IFeatureTypeStore } from '../types/stores/feature-type-store';
|
||||
import { IFeatureToggleStore } from '../types/stores/feature-toggle-store';
|
||||
import { IFeatureEnvironmentStore } from '../types/stores/feature-environment-store';
|
||||
import { IProjectStore } from '../types/stores/project-store';
|
||||
import { IRole } from '../types/stores/access-store';
|
||||
import { IEventStore } from '../types/stores/event-store';
|
||||
@ -51,6 +52,8 @@ export default class ProjectService {
|
||||
|
||||
private featureTypeStore: IFeatureTypeStore;
|
||||
|
||||
private featureEnvironmentStore: IFeatureEnvironmentStore;
|
||||
|
||||
private environmentStore: IEnvironmentStore;
|
||||
|
||||
private logger: any;
|
||||
@ -64,6 +67,7 @@ export default class ProjectService {
|
||||
featureToggleStore,
|
||||
featureTypeStore,
|
||||
environmentStore,
|
||||
featureEnvironmentStore,
|
||||
}: Pick<
|
||||
IUnleashStores,
|
||||
| 'projectStore'
|
||||
@ -71,6 +75,7 @@ export default class ProjectService {
|
||||
| 'featureToggleStore'
|
||||
| 'featureTypeStore'
|
||||
| 'environmentStore'
|
||||
| 'featureEnvironmentStore'
|
||||
>,
|
||||
config: IUnleashConfig,
|
||||
accessService: AccessService,
|
||||
@ -78,6 +83,7 @@ export default class ProjectService {
|
||||
) {
|
||||
this.store = projectStore;
|
||||
this.environmentStore = environmentStore;
|
||||
this.featureEnvironmentStore = featureEnvironmentStore;
|
||||
this.accessService = accessService;
|
||||
this.eventStore = eventStore;
|
||||
this.featureToggleStore = featureToggleStore;
|
||||
@ -117,7 +123,7 @@ export default class ProjectService {
|
||||
|
||||
await this.store.create(data);
|
||||
|
||||
await this.environmentStore.connectProject(GLOBAL_ENV, data.id);
|
||||
await this.featureEnvironmentStore.connectProject(GLOBAL_ENV, data.id);
|
||||
|
||||
await this.accessService.createDefaultProjectRoles(user, data.id);
|
||||
|
||||
@ -189,7 +195,7 @@ export default class ProjectService {
|
||||
);
|
||||
}
|
||||
|
||||
const toggles = await this.featureToggleStore.getFeaturesBy({
|
||||
const toggles = await this.featureToggleStore.getAll({
|
||||
project: id,
|
||||
archived: false,
|
||||
});
|
||||
@ -292,7 +298,7 @@ export default class ProjectService {
|
||||
archived: boolean = false,
|
||||
): Promise<IProjectOverview> {
|
||||
const project = await this.store.get(projectId);
|
||||
const features = await this.store.getProjectOverview(
|
||||
const features = await this.featureToggleService.getFeatureOverview(
|
||||
projectId,
|
||||
archived,
|
||||
);
|
||||
|
@ -56,7 +56,7 @@ test('should not import an existing feature', async () => {
|
||||
],
|
||||
};
|
||||
|
||||
await stores.featureToggleStore.createFeature('default', data.features[0]);
|
||||
await stores.featureToggleStore.create('default', data.features[0]);
|
||||
|
||||
await stateService.import({ data, keepExisting: true });
|
||||
|
||||
@ -77,7 +77,7 @@ test('should not keep existing feature if drop-before-import', async () => {
|
||||
],
|
||||
};
|
||||
|
||||
await stores.featureToggleStore.createFeature('default', data.features[0]);
|
||||
await stores.featureToggleStore.create('default', data.features[0]);
|
||||
|
||||
await stateService.import({
|
||||
data,
|
||||
@ -206,7 +206,7 @@ test('should not accept gibberish', async () => {
|
||||
test('should export featureToggles', async () => {
|
||||
const { stateService, stores } = getSetup();
|
||||
|
||||
await stores.featureToggleStore.createFeature('default', {
|
||||
await stores.featureToggleStore.create('default', {
|
||||
name: 'a-feature',
|
||||
});
|
||||
|
||||
@ -487,18 +487,18 @@ test('exporting to new format works', async () => {
|
||||
name: 'prod',
|
||||
displayName: 'Production',
|
||||
});
|
||||
await stores.featureToggleStore.createFeature('fancy', {
|
||||
await stores.featureToggleStore.create('fancy', {
|
||||
name: 'Some-feature',
|
||||
});
|
||||
await stores.strategyStore.createStrategy({ name: 'format' });
|
||||
await stores.featureEnvironmentStore.connectEnvironmentAndFeature(
|
||||
await stores.featureEnvironmentStore.addEnvironmentToFeature(
|
||||
'Some-feature',
|
||||
'dev',
|
||||
true,
|
||||
);
|
||||
await stores.featureStrategiesStore.createStrategyConfig({
|
||||
await stores.featureStrategiesStore.createStrategyFeatureEnv({
|
||||
featureName: 'Some-feature',
|
||||
projectName: 'fancy',
|
||||
projectId: 'fancy',
|
||||
strategyName: 'format',
|
||||
environment: 'dev',
|
||||
parameters: {},
|
||||
@ -527,18 +527,18 @@ test('featureStrategies can keep existing', async () => {
|
||||
name: 'prod',
|
||||
displayName: 'Production',
|
||||
});
|
||||
await stores.featureToggleStore.createFeature('fancy', {
|
||||
await stores.featureToggleStore.create('fancy', {
|
||||
name: 'Some-feature',
|
||||
});
|
||||
await stores.strategyStore.createStrategy({ name: 'format' });
|
||||
await stores.featureEnvironmentStore.connectEnvironmentAndFeature(
|
||||
await stores.featureEnvironmentStore.addEnvironmentToFeature(
|
||||
'Some-feature',
|
||||
'dev',
|
||||
true,
|
||||
);
|
||||
await stores.featureStrategiesStore.createStrategyConfig({
|
||||
await stores.featureStrategiesStore.createStrategyFeatureEnv({
|
||||
featureName: 'Some-feature',
|
||||
projectName: 'fancy',
|
||||
projectId: 'fancy',
|
||||
strategyName: 'format',
|
||||
environment: 'dev',
|
||||
parameters: {},
|
||||
@ -573,18 +573,18 @@ test('featureStrategies should not keep existing if dropBeforeImport', async ()
|
||||
name: 'prod',
|
||||
displayName: 'Production',
|
||||
});
|
||||
await stores.featureToggleStore.createFeature('fancy', {
|
||||
await stores.featureToggleStore.create('fancy', {
|
||||
name: 'Some-feature',
|
||||
});
|
||||
await stores.strategyStore.createStrategy({ name: 'format' });
|
||||
await stores.featureEnvironmentStore.connectEnvironmentAndFeature(
|
||||
await stores.featureEnvironmentStore.addEnvironmentToFeature(
|
||||
'Some-feature',
|
||||
'dev',
|
||||
true,
|
||||
);
|
||||
await stores.featureStrategiesStore.createStrategyConfig({
|
||||
await stores.featureStrategiesStore.createStrategyFeatureEnv({
|
||||
featureName: 'Some-feature',
|
||||
projectName: 'fancy',
|
||||
projectId: 'fancy',
|
||||
strategyName: 'format',
|
||||
environment: 'dev',
|
||||
parameters: {},
|
||||
|
@ -26,6 +26,7 @@ import {
|
||||
ITag,
|
||||
IImportData,
|
||||
IProject,
|
||||
IStrategyConfig,
|
||||
} from '../types/model';
|
||||
import { GLOBAL_ENV } from '../types/environment';
|
||||
import { Logger } from '../logger';
|
||||
@ -196,7 +197,7 @@ export default class StateService {
|
||||
async importFeatureEnvironments({ featureEnvironments }): Promise<void> {
|
||||
await Promise.all(
|
||||
featureEnvironments.map((env) =>
|
||||
this.featureEnvironmentStore.connectEnvironmentAndFeature(
|
||||
this.featureEnvironmentStore.addEnvironmentToFeature(
|
||||
env.featureName,
|
||||
env.environment,
|
||||
env.enabled,
|
||||
@ -213,7 +214,7 @@ export default class StateService {
|
||||
}): Promise<void> {
|
||||
const oldFeatureStrategies = dropBeforeImport
|
||||
? []
|
||||
: await this.featureStrategiesStore.getAllFeatureStrategies();
|
||||
: await this.featureStrategiesStore.getAll();
|
||||
if (dropBeforeImport) {
|
||||
this.logger.info(
|
||||
'Dropping existing strategies for feature toggles',
|
||||
@ -227,7 +228,7 @@ export default class StateService {
|
||||
: featureStrategies;
|
||||
await Promise.all(
|
||||
strategiesToImport.map((featureStrategy) =>
|
||||
this.featureStrategiesStore.createStrategyConfig(
|
||||
this.featureStrategiesStore.createStrategyFeatureEnv(
|
||||
featureStrategy,
|
||||
),
|
||||
),
|
||||
@ -239,9 +240,9 @@ export default class StateService {
|
||||
features,
|
||||
}): Promise<{ features; featureStrategies; featureEnvironments }> {
|
||||
const strategies = features.flatMap((f) =>
|
||||
f.strategies.map((strategy) => ({
|
||||
f.strategies.map((strategy: IStrategyConfig) => ({
|
||||
featureName: f.name,
|
||||
projectName: f.project,
|
||||
projectId: f.project,
|
||||
constraints: strategy.constraints || [],
|
||||
parameters: strategy.parameters || {},
|
||||
environment: GLOBAL_ENV,
|
||||
@ -289,7 +290,7 @@ export default class StateService {
|
||||
.filter(filterEqual(oldToggles))
|
||||
.map((feature) =>
|
||||
this.toggleStore
|
||||
.createFeature(feature.project, feature)
|
||||
.create(feature.project, feature)
|
||||
.then(() => {
|
||||
this.eventStore.store({
|
||||
type: FEATURE_IMPORT,
|
||||
|
@ -18,7 +18,7 @@ export interface IStrategyConfig {
|
||||
export interface IFeatureStrategy {
|
||||
id: string;
|
||||
featureName: string;
|
||||
projectName: string;
|
||||
projectId: string;
|
||||
environment: string;
|
||||
strategyName: string;
|
||||
parameters: object;
|
||||
@ -84,12 +84,12 @@ export interface IVariant {
|
||||
name: string;
|
||||
weight: number;
|
||||
weightType: string;
|
||||
payload: {
|
||||
payload?: {
|
||||
type: string;
|
||||
value: string;
|
||||
};
|
||||
stickiness: string;
|
||||
overrides: {
|
||||
overrides?: {
|
||||
contextName: string;
|
||||
values: string[];
|
||||
}[];
|
||||
@ -297,8 +297,8 @@ export interface IProject {
|
||||
id: string;
|
||||
name: string;
|
||||
description: string;
|
||||
health: number;
|
||||
createdAt: Date;
|
||||
health?: number;
|
||||
createdAt?: Date;
|
||||
}
|
||||
|
||||
export interface IProjectWithCount extends IProject {
|
||||
|
@ -21,6 +21,7 @@ import { IUserFeedbackStore } from './stores/user-feedback-store';
|
||||
import { IFeatureEnvironmentStore } from './stores/feature-environment-store';
|
||||
import { IFeatureStrategiesStore } from './stores/feature-strategies-store';
|
||||
import { IEnvironmentStore } from './stores/environment-store';
|
||||
import { IFeatureToggleClientStore } from './stores/feature-toggle-client-store';
|
||||
|
||||
export interface IUnleashStores {
|
||||
accessStore: IAccessStore;
|
||||
@ -36,6 +37,7 @@ export interface IUnleashStores {
|
||||
featureStrategiesStore: IFeatureStrategiesStore;
|
||||
featureTagStore: IFeatureTagStore;
|
||||
featureToggleStore: IFeatureToggleStore;
|
||||
featureToggleClientStore: IFeatureToggleClientStore;
|
||||
featureTypeStore: IFeatureTypeStore;
|
||||
projectStore: IProjectStore;
|
||||
resetTokenStore: IResetTokenStore;
|
||||
|
@ -2,16 +2,5 @@ import { IEnvironment } from '../model';
|
||||
import { Store } from './store';
|
||||
|
||||
export interface IEnvironmentStore extends Store<IEnvironment, string> {
|
||||
exists(name: string): Promise<boolean>;
|
||||
upsert(env: IEnvironment): Promise<IEnvironment>;
|
||||
connectProject(environment: string, projectId: string): Promise<void>;
|
||||
connectFeatures(environment: string, projectId: string): Promise<void>;
|
||||
disconnectProjectFromEnv(
|
||||
environment: string,
|
||||
projectId: string,
|
||||
): Promise<void>;
|
||||
connectFeatureToEnvironmentsForProject(
|
||||
featureName: string,
|
||||
project_id: string,
|
||||
): Promise<void>;
|
||||
}
|
||||
|
@ -8,7 +8,6 @@ export interface FeatureEnvironmentKey {
|
||||
|
||||
export interface IFeatureEnvironmentStore
|
||||
extends Store<IFeatureEnvironment, FeatureEnvironmentKey> {
|
||||
getAllFeatureEnvironments(): Promise<IFeatureEnvironment[]>;
|
||||
featureHasEnvironment(
|
||||
environment: string,
|
||||
featureName: string,
|
||||
@ -17,7 +16,7 @@ export interface IFeatureEnvironmentStore
|
||||
featureName: string,
|
||||
environment: string,
|
||||
): Promise<boolean>;
|
||||
toggleEnvironmentEnabledStatus(
|
||||
setEnvironmentEnabledStatus(
|
||||
environment: string,
|
||||
featureName: string,
|
||||
enabled: boolean,
|
||||
@ -26,21 +25,24 @@ export interface IFeatureEnvironmentStore
|
||||
environment: string,
|
||||
featureName: string,
|
||||
): Promise<IFeatureEnvironment>;
|
||||
disconnectEnvironmentFromProject(
|
||||
environment: string,
|
||||
project: string,
|
||||
): Promise<void>;
|
||||
removeEnvironmentForFeature(
|
||||
feature_name: string,
|
||||
featureName: string,
|
||||
environment: string,
|
||||
): Promise<void>;
|
||||
connectEnvironmentAndFeature(
|
||||
feature_name: string,
|
||||
addEnvironmentToFeature(
|
||||
featureName: string,
|
||||
environment: string,
|
||||
enabled: boolean,
|
||||
): Promise<void>;
|
||||
enableEnvironmentForFeature(
|
||||
feature_name: string,
|
||||
environment: string,
|
||||
|
||||
disconnectFeatures(environment: string, project: string): Promise<void>;
|
||||
connectFeatures(environment: string, projectId: string): Promise<void>;
|
||||
|
||||
connectFeatureToEnvironmentsForProject(
|
||||
featureName: string,
|
||||
projectId: string,
|
||||
): Promise<void>;
|
||||
|
||||
connectProject(environment: string, projectId: string): Promise<void>;
|
||||
disconnectProject(environment: string, projectId: string): Promise<void>;
|
||||
}
|
||||
|
@ -1,8 +1,7 @@
|
||||
import {
|
||||
FeatureToggleWithEnvironment,
|
||||
IFeatureOverview,
|
||||
IFeatureStrategy,
|
||||
IFeatureToggleClient,
|
||||
IFeatureToggleQuery,
|
||||
IStrategyConfig,
|
||||
IVariant,
|
||||
} from '../model';
|
||||
@ -18,43 +17,31 @@ export interface FeatureConfigurationClient {
|
||||
}
|
||||
export interface IFeatureStrategiesStore
|
||||
extends Store<IFeatureStrategy, string> {
|
||||
createStrategyConfig(
|
||||
createStrategyFeatureEnv(
|
||||
strategyConfig: Omit<IFeatureStrategy, 'id' | 'createdAt'>,
|
||||
): Promise<IFeatureStrategy>;
|
||||
getStrategiesForToggle(featureName: string): Promise<IFeatureStrategy[]>;
|
||||
getAllFeatureStrategies(): Promise<IFeatureStrategy[]>;
|
||||
getStrategiesForEnvironment(
|
||||
environment: string,
|
||||
): Promise<IFeatureStrategy[]>;
|
||||
removeAllStrategiesForEnv(
|
||||
feature_name: string,
|
||||
removeAllStrategiesForFeatureEnv(
|
||||
featureName: string,
|
||||
environment: string,
|
||||
): Promise<void>;
|
||||
getAll(): Promise<IFeatureStrategy[]>;
|
||||
getStrategiesForFeature(
|
||||
project_name: string,
|
||||
feature_name: string,
|
||||
getStrategiesForFeatureEnv(
|
||||
projectId: string,
|
||||
featureName: string,
|
||||
environment: string,
|
||||
): Promise<IFeatureStrategy[]>;
|
||||
getStrategiesForEnv(environment: string): Promise<IFeatureStrategy[]>;
|
||||
getFeatureToggleAdmin(
|
||||
getFeatureToggleWithEnvs(
|
||||
featureName: string,
|
||||
archived?: boolean,
|
||||
): Promise<FeatureToggleWithEnvironment>;
|
||||
getFeatures(
|
||||
featureQuery: Partial<IFeatureToggleQuery>,
|
||||
getFeatureOverview(
|
||||
projectId: string,
|
||||
archived: boolean,
|
||||
isAdmin: boolean,
|
||||
): Promise<IFeatureToggleClient[]>;
|
||||
): Promise<IFeatureOverview[]>;
|
||||
getStrategyById(id: string): Promise<IFeatureStrategy>;
|
||||
updateStrategy(
|
||||
id: string,
|
||||
updates: Partial<IFeatureStrategy>,
|
||||
): Promise<IFeatureStrategy>;
|
||||
getStrategiesAndMetadataForEnvironment(
|
||||
environment: string,
|
||||
featureName: string,
|
||||
): Promise<void>;
|
||||
deleteConfigurationsForProjectAndEnvironment(
|
||||
projectId: String,
|
||||
environment: String,
|
||||
|
13
src/lib/types/stores/feature-toggle-client-store.ts
Normal file
13
src/lib/types/stores/feature-toggle-client-store.ts
Normal file
@ -0,0 +1,13 @@
|
||||
import { IFeatureToggleClient, IFeatureToggleQuery } from '../model';
|
||||
|
||||
export interface IFeatureToggleClientStore {
|
||||
getClient(
|
||||
featureQuery: Partial<IFeatureToggleQuery>,
|
||||
): Promise<IFeatureToggleClient[]>;
|
||||
|
||||
// @Deprecated
|
||||
getAdmin(
|
||||
featureQuery: Partial<IFeatureToggleQuery>,
|
||||
archived: boolean,
|
||||
): Promise<IFeatureToggleClient[]>;
|
||||
}
|
@ -7,29 +7,13 @@ export interface IFeatureToggleQuery {
|
||||
stale: boolean;
|
||||
}
|
||||
|
||||
export interface IHasFeature {
|
||||
name: string;
|
||||
archived: boolean;
|
||||
}
|
||||
|
||||
export interface IFeatureToggleStore extends Store<FeatureToggle, string> {
|
||||
count(query: Partial<IFeatureToggleQuery>): Promise<number>;
|
||||
getFeatureMetadata(name: string): Promise<FeatureToggle>;
|
||||
getFeatures(archived: boolean): Promise<FeatureToggle[]>;
|
||||
hasFeature(name: string): Promise<IHasFeature>;
|
||||
updateLastSeenForToggles(toggleNames: string[]): Promise<void>;
|
||||
count(query?: Partial<IFeatureToggleQuery>): Promise<number>;
|
||||
setLastSeen(toggleNames: string[]): Promise<void>;
|
||||
getProjectId(name: string): Promise<string>;
|
||||
createFeature(
|
||||
project: string,
|
||||
data: FeatureToggleDTO,
|
||||
): Promise<FeatureToggle>;
|
||||
updateFeature(
|
||||
project: string,
|
||||
data: FeatureToggleDTO,
|
||||
): Promise<FeatureToggle>;
|
||||
archiveFeature(featureName: string): Promise<FeatureToggle>;
|
||||
reviveFeature(featureName: string): Promise<FeatureToggle>;
|
||||
getFeaturesBy(
|
||||
query: Partial<IFeatureToggleQuery>,
|
||||
): Promise<FeatureToggle[]>;
|
||||
create(project: string, data: FeatureToggleDTO): Promise<FeatureToggle>;
|
||||
update(project: string, data: FeatureToggleDTO): Promise<FeatureToggle>;
|
||||
archive(featureName: string): Promise<FeatureToggle>;
|
||||
revive(featureName: string): Promise<FeatureToggle>;
|
||||
getAll(query?: Partial<IFeatureToggleQuery>): Promise<FeatureToggle[]>;
|
||||
}
|
||||
|
@ -1,4 +1,4 @@
|
||||
import { IFeatureOverview, IProject } from '../model';
|
||||
import { IProject } from '../model';
|
||||
import { Store } from './store';
|
||||
|
||||
export interface IProjectInsert {
|
||||
@ -27,9 +27,5 @@ export interface IProjectStore extends Store<IProject, string> {
|
||||
deleteEnvironmentForProject(id: string, environment: string): Promise<void>;
|
||||
getEnvironmentsForProject(id: string): Promise<string[]>;
|
||||
getMembers(projectId: string): Promise<number>;
|
||||
getProjectOverview(
|
||||
projectId: string,
|
||||
archived: boolean,
|
||||
): Promise<IFeatureOverview[]>;
|
||||
count(): Promise<number>;
|
||||
}
|
||||
|
@ -1,9 +1,9 @@
|
||||
import dbInit from '../../helpers/database-init';
|
||||
import { setupApp } from '../../helpers/test-helper';
|
||||
import dbInit, { ITestDb } from '../../helpers/database-init';
|
||||
import { IUnleashTest, setupApp } from '../../helpers/test-helper';
|
||||
import getLogger from '../../../fixtures/no-logger';
|
||||
|
||||
let app;
|
||||
let db;
|
||||
let app: IUnleashTest;
|
||||
let db: ITestDb;
|
||||
|
||||
beforeAll(async () => {
|
||||
db = await dbInit('archive_test_serial', getLogger);
|
||||
|
@ -8,7 +8,7 @@ let db;
|
||||
const email = 'user@getunleash.io';
|
||||
|
||||
beforeAll(async () => {
|
||||
db = await dbInit('user_api_serial', getLogger);
|
||||
db = await dbInit('ui_bootstrap_serial', getLogger);
|
||||
app = await setupAppWithAuth(db.stores);
|
||||
});
|
||||
|
@ -1,132 +1,148 @@
|
||||
import faker from 'faker';
|
||||
import dbInit from '../../helpers/database-init';
|
||||
import { setupApp } from '../../helpers/test-helper';
|
||||
import { FeatureToggleDTO, IStrategyConfig } from 'lib/types/model';
|
||||
import dbInit, { ITestDb } from '../../helpers/database-init';
|
||||
import { IUnleashTest, setupApp } from '../../helpers/test-helper';
|
||||
import getLogger from '../../../fixtures/no-logger';
|
||||
|
||||
let app;
|
||||
let db;
|
||||
let app: IUnleashTest;
|
||||
let db: ITestDb;
|
||||
|
||||
const defaultStrategy = {
|
||||
name: 'default',
|
||||
parameters: {},
|
||||
constraints: [],
|
||||
};
|
||||
|
||||
beforeAll(async () => {
|
||||
db = await dbInit('feature_api_serial', getLogger);
|
||||
app = await setupApp(db.stores);
|
||||
await app.services.featureToggleServiceV2.createFeatureToggle(
|
||||
'default',
|
||||
{
|
||||
name: 'featureX',
|
||||
description: 'the #1 feature',
|
||||
strategies: [
|
||||
{
|
||||
name: 'default',
|
||||
parameters: {},
|
||||
},
|
||||
],
|
||||
},
|
||||
'test',
|
||||
);
|
||||
await app.services.featureToggleServiceV2.createFeatureToggle(
|
||||
'default',
|
||||
|
||||
const createToggle = async (
|
||||
toggle: FeatureToggleDTO,
|
||||
strategy: Omit<IStrategyConfig, 'id'> = defaultStrategy,
|
||||
projectId: string = 'default',
|
||||
username: string = 'test',
|
||||
) => {
|
||||
await app.services.featureToggleServiceV2.createFeatureToggle(
|
||||
projectId,
|
||||
toggle,
|
||||
username,
|
||||
);
|
||||
await app.services.featureToggleServiceV2.createStrategy(
|
||||
strategy,
|
||||
projectId,
|
||||
toggle.name,
|
||||
);
|
||||
};
|
||||
|
||||
await createToggle({
|
||||
name: 'featureX',
|
||||
description: 'the #1 feature',
|
||||
});
|
||||
|
||||
await createToggle(
|
||||
{
|
||||
name: 'featureY',
|
||||
description: 'soon to be the #1 feature',
|
||||
strategies: [
|
||||
{
|
||||
name: 'baz',
|
||||
parameters: {
|
||||
foo: 'bar',
|
||||
},
|
||||
},
|
||||
],
|
||||
},
|
||||
'userName',
|
||||
{
|
||||
name: 'baz',
|
||||
constraints: [],
|
||||
parameters: {
|
||||
foo: 'bar',
|
||||
},
|
||||
},
|
||||
);
|
||||
await app.services.featureToggleServiceV2.createFeatureToggle(
|
||||
'default',
|
||||
|
||||
await createToggle(
|
||||
{
|
||||
name: 'featureZ',
|
||||
description: 'terrible feature',
|
||||
strategies: [
|
||||
{
|
||||
name: 'baz',
|
||||
parameters: {
|
||||
foo: 'rab',
|
||||
},
|
||||
},
|
||||
],
|
||||
},
|
||||
'test',
|
||||
{
|
||||
name: 'baz',
|
||||
constraints: [],
|
||||
parameters: {
|
||||
foo: 'rab',
|
||||
},
|
||||
},
|
||||
);
|
||||
await app.services.featureToggleServiceV2.createFeatureToggle(
|
||||
'default',
|
||||
|
||||
await createToggle(
|
||||
{
|
||||
name: 'featureArchivedX',
|
||||
description: 'the #1 feature',
|
||||
strategies: [
|
||||
{
|
||||
name: 'default',
|
||||
parameters: {},
|
||||
},
|
||||
],
|
||||
},
|
||||
'test',
|
||||
{
|
||||
name: 'default',
|
||||
constraints: [],
|
||||
parameters: {},
|
||||
},
|
||||
);
|
||||
|
||||
await app.services.featureToggleServiceV2.archiveToggle(
|
||||
'featureArchivedX',
|
||||
'test',
|
||||
);
|
||||
await app.services.featureToggleServiceV2.createFeatureToggle(
|
||||
'default',
|
||||
|
||||
await createToggle(
|
||||
{
|
||||
name: 'featureArchivedY',
|
||||
description: 'soon to be the #1 feature',
|
||||
strategies: [
|
||||
{
|
||||
name: 'baz',
|
||||
parameters: {
|
||||
foo: 'bar',
|
||||
},
|
||||
},
|
||||
],
|
||||
},
|
||||
'test',
|
||||
{
|
||||
name: 'baz',
|
||||
constraints: [],
|
||||
parameters: {
|
||||
foo: 'bar',
|
||||
},
|
||||
},
|
||||
);
|
||||
|
||||
await app.services.featureToggleServiceV2.archiveToggle(
|
||||
'featureArchivedY',
|
||||
'test',
|
||||
);
|
||||
await app.services.featureToggleServiceV2.createFeatureToggle(
|
||||
'default',
|
||||
|
||||
await createToggle(
|
||||
{
|
||||
name: 'featureArchivedZ',
|
||||
description: 'terrible feature',
|
||||
strategies: [
|
||||
{
|
||||
name: 'baz',
|
||||
parameters: {
|
||||
foo: 'rab',
|
||||
},
|
||||
},
|
||||
],
|
||||
},
|
||||
'test',
|
||||
{
|
||||
name: 'baz',
|
||||
constraints: [],
|
||||
parameters: {
|
||||
foo: 'rab',
|
||||
},
|
||||
},
|
||||
);
|
||||
|
||||
await app.services.featureToggleServiceV2.archiveToggle(
|
||||
'featureArchivedZ',
|
||||
'test',
|
||||
);
|
||||
await app.services.featureToggleServiceV2.createFeatureToggle(
|
||||
'default',
|
||||
{
|
||||
name: 'feature.with.variants',
|
||||
description: 'A feature toggle with variants',
|
||||
enabled: true,
|
||||
strategies: [{ name: 'default' }],
|
||||
variants: [
|
||||
{ name: 'control', weight: 50 },
|
||||
{ name: 'new', weight: 50 },
|
||||
],
|
||||
},
|
||||
'test',
|
||||
);
|
||||
|
||||
await createToggle({
|
||||
name: 'feature.with.variants',
|
||||
description: 'A feature toggle with variants',
|
||||
variants: [
|
||||
{
|
||||
name: 'control',
|
||||
weight: 50,
|
||||
weightType: 'variable',
|
||||
overrides: [],
|
||||
stickiness: 'default',
|
||||
},
|
||||
{
|
||||
name: 'new',
|
||||
weight: 50,
|
||||
weightType: 'variable',
|
||||
overrides: [],
|
||||
stickiness: 'default',
|
||||
},
|
||||
],
|
||||
});
|
||||
});
|
||||
|
||||
afterAll(async () => {
|
||||
@ -256,14 +272,20 @@ test('can not toggle of feature that does not exist', async () => {
|
||||
|
||||
test('can toggle a feature that does exist', async () => {
|
||||
expect.assertions(0);
|
||||
const featureName = 'existing.feature';
|
||||
const feature =
|
||||
await app.services.featureToggleServiceV2.createFeatureToggle(
|
||||
'default',
|
||||
{
|
||||
name: 'existing.feature',
|
||||
name: featureName,
|
||||
},
|
||||
'test',
|
||||
);
|
||||
await app.services.featureToggleServiceV2.createStrategy(
|
||||
defaultStrategy,
|
||||
'default',
|
||||
featureName,
|
||||
);
|
||||
return app.request
|
||||
.post(`/api/admin/features/${feature.name}/toggle`)
|
||||
.set('Content-Type', 'application/json')
|
||||
|
83
src/test/e2e/api/admin/project/environments.e2e.test.ts
Normal file
83
src/test/e2e/api/admin/project/environments.e2e.test.ts
Normal file
@ -0,0 +1,83 @@
|
||||
import dbInit, { ITestDb } from '../../../helpers/database-init';
|
||||
import { IUnleashTest, setupApp } from '../../../helpers/test-helper';
|
||||
import getLogger from '../../../../fixtures/no-logger';
|
||||
|
||||
let app: IUnleashTest;
|
||||
let db: ITestDb;
|
||||
|
||||
beforeAll(async () => {
|
||||
db = await dbInit('project_environments_api_serial', getLogger);
|
||||
app = await setupApp(db.stores);
|
||||
});
|
||||
|
||||
afterEach(async () => {
|
||||
const all = await db.stores.projectStore.getEnvironmentsForProject(
|
||||
'default',
|
||||
);
|
||||
await Promise.all(
|
||||
all
|
||||
.filter((env) => env !== ':global:')
|
||||
.map(async (env) =>
|
||||
db.stores.projectStore.deleteEnvironmentForProject(
|
||||
'default',
|
||||
env,
|
||||
),
|
||||
),
|
||||
);
|
||||
});
|
||||
|
||||
afterAll(async () => {
|
||||
await app.destroy();
|
||||
await db.destroy();
|
||||
});
|
||||
|
||||
test('Should add environment to project', async () => {
|
||||
await app.request
|
||||
.post('/api/admin/environments')
|
||||
.send({ name: 'test', displayName: 'Test Env' })
|
||||
.set('Content-Type', 'application/json')
|
||||
.expect(201);
|
||||
await app.request
|
||||
.post('/api/admin/projects/default/environments')
|
||||
.send({ environment: 'test' })
|
||||
.expect(200);
|
||||
|
||||
const envs = await db.stores.projectStore.getEnvironmentsForProject(
|
||||
'default',
|
||||
);
|
||||
|
||||
const environment = envs.find((env) => env === 'test');
|
||||
|
||||
expect(environment).toBeDefined();
|
||||
expect(envs).toHaveLength(2);
|
||||
});
|
||||
|
||||
test('Should validate environment', async () => {
|
||||
await app.request
|
||||
.post('/api/admin/projects/default/environments')
|
||||
.send({ name: 'test' })
|
||||
.expect(400);
|
||||
});
|
||||
|
||||
test('Should remove environment to project', async () => {
|
||||
const name = 'test-delete';
|
||||
await app.request
|
||||
.post('/api/admin/environments')
|
||||
.send({ name, displayName: 'Test Env' })
|
||||
.set('Content-Type', 'application/json')
|
||||
.expect(201);
|
||||
await app.request
|
||||
.post('/api/admin/projects/default/environments')
|
||||
.send({ environment: name })
|
||||
.expect(200);
|
||||
|
||||
await app.request
|
||||
.delete(`/api/admin/projects/default/environments/${name}`)
|
||||
.expect(200);
|
||||
|
||||
const envs = await db.stores.projectStore.getEnvironmentsForProject(
|
||||
'default',
|
||||
);
|
||||
|
||||
expect(envs).toHaveLength(1);
|
||||
});
|
@ -1,15 +1,31 @@
|
||||
import dbInit from '../../../helpers/database-init';
|
||||
import { setupApp } from '../../../helpers/test-helper';
|
||||
import dbInit, { ITestDb } from '../../../helpers/database-init';
|
||||
import { IUnleashTest, setupApp } from '../../../helpers/test-helper';
|
||||
import getLogger from '../../../../fixtures/no-logger';
|
||||
|
||||
let app;
|
||||
let db;
|
||||
let app: IUnleashTest;
|
||||
let db: ITestDb;
|
||||
|
||||
beforeAll(async () => {
|
||||
db = await dbInit('feature_strategy_api_serial', getLogger);
|
||||
app = await setupApp(db.stores);
|
||||
});
|
||||
|
||||
afterEach(async () => {
|
||||
const all = await db.stores.projectStore.getEnvironmentsForProject(
|
||||
'default',
|
||||
);
|
||||
await Promise.all(
|
||||
all
|
||||
.filter((env) => env !== ':global:')
|
||||
.map(async (env) =>
|
||||
db.stores.projectStore.deleteEnvironmentForProject(
|
||||
'default',
|
||||
env,
|
||||
),
|
||||
),
|
||||
);
|
||||
});
|
||||
|
||||
afterAll(async () => {
|
||||
await app.destroy();
|
||||
await db.destroy();
|
||||
@ -168,8 +184,9 @@ test('Disconnecting environment from project, removes environment from features
|
||||
});
|
||||
});
|
||||
|
||||
test('Can enable/disable environment for feature', async () => {
|
||||
test('Can enable/disable environment for feature with strategies', async () => {
|
||||
const envName = 'enable-feature-environment';
|
||||
const featureName = 'com.test.enable.environment';
|
||||
// Create environment
|
||||
await app.request
|
||||
.post('/api/admin/environments')
|
||||
@ -191,23 +208,35 @@ test('Can enable/disable environment for feature', async () => {
|
||||
await app.request
|
||||
.post('/api/admin/projects/default/features')
|
||||
.send({
|
||||
name: 'com.test.enable.environment',
|
||||
strategies: [{ name: 'default' }],
|
||||
name: featureName,
|
||||
})
|
||||
.set('Content-Type', 'application/json')
|
||||
.expect(201)
|
||||
.expect((res) => {
|
||||
expect(res.body.name).toBe('com.test.enable.environment');
|
||||
expect(res.body.name).toBe(featureName);
|
||||
expect(res.body.createdAt).toBeTruthy();
|
||||
});
|
||||
|
||||
// Add strategy to it
|
||||
await app.request
|
||||
.post(
|
||||
'/api/admin/projects/default/features/com.test.enable.environment/environments/enable-feature-environment/on',
|
||||
`/api/admin/projects/default/features/${featureName}/environments/${envName}/strategies`,
|
||||
)
|
||||
.send({})
|
||||
.send({
|
||||
name: 'default',
|
||||
parameters: {
|
||||
userId: 'string',
|
||||
},
|
||||
})
|
||||
.expect(200);
|
||||
await app.request
|
||||
.get('/api/admin/projects/default/features/com.test.enable.environment')
|
||||
.post(
|
||||
`/api/admin/projects/default/features/${featureName}/environments/${envName}/on`,
|
||||
)
|
||||
.set('Content-Type', 'application/json')
|
||||
.expect(200);
|
||||
await app.request
|
||||
.get(`/api/admin/projects/default/features/${featureName}`)
|
||||
.expect(200)
|
||||
.expect('Content-Type', /json/)
|
||||
.expect((res) => {
|
||||
@ -219,12 +248,12 @@ test('Can enable/disable environment for feature', async () => {
|
||||
});
|
||||
await app.request
|
||||
.post(
|
||||
'/api/admin/projects/default/features/com.test.enable.environment/environments/enable-feature-environment/off',
|
||||
`/api/admin/projects/default/features/${featureName}/environments/${envName}/off`,
|
||||
)
|
||||
.send({})
|
||||
.expect(200);
|
||||
await app.request
|
||||
.get('/api/admin/projects/default/features/com.test.enable.environment')
|
||||
.get(`/api/admin/projects/default/features/${featureName}`)
|
||||
.expect(200)
|
||||
.expect('Content-Type', /json/)
|
||||
.expect((res) => {
|
||||
@ -261,6 +290,18 @@ test('Can use new project feature toggle endpoint to create feature toggle witho
|
||||
});
|
||||
});
|
||||
|
||||
test('Can create feature toggle without strategies', async () => {
|
||||
const name = 'new.toggle.without.strategy.2';
|
||||
await app.request
|
||||
.post('/api/admin/projects/default/features')
|
||||
.send({ name });
|
||||
const { body: toggle } = await app.request.get(
|
||||
`/api/admin/projects/default/features/${name}`,
|
||||
);
|
||||
expect(toggle.environments).toHaveLength(1);
|
||||
expect(toggle.environments[0].strategies).toHaveLength(0);
|
||||
});
|
||||
|
||||
test('Still validates feature toggle input when creating', async () => {
|
||||
await app.request
|
||||
.post('/api/admin/projects/default/features')
|
||||
@ -369,6 +410,100 @@ test('Getting feature that does not exist should yield 404', async () => {
|
||||
.expect(404);
|
||||
});
|
||||
|
||||
test('Should update feature toggle', async () => {
|
||||
const url = '/api/admin/projects/default/features';
|
||||
const name = 'new.toggle.update';
|
||||
await app.request
|
||||
.post(url)
|
||||
.send({ name, description: 'some', type: 'release' })
|
||||
.expect(201);
|
||||
await app.request
|
||||
.put(`${url}/${name}`)
|
||||
.send({ name, description: 'updated', type: 'kill-switch' })
|
||||
.expect(200);
|
||||
|
||||
const { body: toggle } = await app.request.get(`${url}/${name}`);
|
||||
|
||||
expect(toggle.name).toBe(name);
|
||||
expect(toggle.description).toBe('updated');
|
||||
expect(toggle.type).toBe('kill-switch');
|
||||
expect(toggle.archived).toBeFalsy();
|
||||
});
|
||||
|
||||
test('Should not change name of feature toggle', async () => {
|
||||
const url = '/api/admin/projects/default/features';
|
||||
const name = 'new.toggle.update.2';
|
||||
await app.request
|
||||
.post(url)
|
||||
.send({ name, description: 'some', type: 'release' })
|
||||
.expect(201);
|
||||
await app.request
|
||||
.put(`${url}/${name}`)
|
||||
.send({ name: 'new name', description: 'updated', type: 'kill-switch' })
|
||||
.expect(400);
|
||||
});
|
||||
|
||||
test('Should not change project of feature toggle even if it is part of body', async () => {
|
||||
const url = '/api/admin/projects/default/features';
|
||||
const name = 'new.toggle.update.3';
|
||||
await app.request
|
||||
.post(url)
|
||||
.send({ name, description: 'some', type: 'release' })
|
||||
.expect(201);
|
||||
const { body } = await app.request
|
||||
.put(`${url}/${name}`)
|
||||
.send({
|
||||
name,
|
||||
description: 'updated',
|
||||
type: 'kill-switch',
|
||||
project: 'new',
|
||||
})
|
||||
.expect(200);
|
||||
|
||||
expect(body.project).toBe('default');
|
||||
});
|
||||
|
||||
test('Should patch feature toggle', async () => {
|
||||
const url = '/api/admin/projects/default/features';
|
||||
const name = 'new.toggle.patch';
|
||||
await app.request
|
||||
.post(url)
|
||||
.send({ name, description: 'some', type: 'release' })
|
||||
.expect(201);
|
||||
await app.request
|
||||
.patch(`${url}/${name}`)
|
||||
.send([
|
||||
{ op: 'replace', path: '/description', value: 'New desc' },
|
||||
{ op: 'replace', path: '/type', value: 'kill-switch' },
|
||||
])
|
||||
.expect(200);
|
||||
|
||||
const { body: toggle } = await app.request.get(`${url}/${name}`);
|
||||
|
||||
expect(toggle.name).toBe(name);
|
||||
expect(toggle.description).toBe('New desc');
|
||||
expect(toggle.type).toBe('kill-switch');
|
||||
expect(toggle.archived).toBeFalsy();
|
||||
});
|
||||
|
||||
test('Should archive feature toggle', async () => {
|
||||
const url = '/api/admin/projects/default/features';
|
||||
const name = 'new.toggle.archive';
|
||||
await app.request
|
||||
.post(url)
|
||||
.send({ name, description: 'some', type: 'release' })
|
||||
.expect(201);
|
||||
await app.request.delete(`${url}/${name}`);
|
||||
|
||||
await app.request.get(`${url}/${name}`).expect(404);
|
||||
const { body } = await app.request
|
||||
.get(`/api/admin/archive/features`)
|
||||
.expect(200);
|
||||
|
||||
const toggle = body.features.find((f) => f.name === name);
|
||||
expect(toggle).toBeDefined();
|
||||
});
|
||||
|
||||
test('Can add strategy to feature toggle', async () => {
|
||||
const envName = 'add-strategy';
|
||||
// Create environment
|
||||
@ -555,6 +690,65 @@ test('Trying to update a non existing feature strategy should yield 404', async
|
||||
.expect(404);
|
||||
});
|
||||
|
||||
test('Can patch a strategy based on id', async () => {
|
||||
const BASE_URI = '/api/admin/projects/default';
|
||||
const envName = 'feature.patch.strategies';
|
||||
const featureName = 'feature.patch.strategies';
|
||||
|
||||
// Create environment
|
||||
await app.request
|
||||
.post('/api/admin/environments')
|
||||
.send({
|
||||
name: envName,
|
||||
displayName: 'Enable feature for environment',
|
||||
})
|
||||
.set('Content-Type', 'application/json')
|
||||
.expect(201);
|
||||
// Connect environment to project
|
||||
await app.request
|
||||
.post(`${BASE_URI}/environments`)
|
||||
.send({
|
||||
environment: envName,
|
||||
})
|
||||
.expect(200);
|
||||
await app.request
|
||||
.post(`${BASE_URI}/features`)
|
||||
.send({ name: featureName })
|
||||
.expect(201);
|
||||
let strategy;
|
||||
await app.request
|
||||
.post(
|
||||
`${BASE_URI}/features/${featureName}/environments/${envName}/strategies`,
|
||||
)
|
||||
.send({
|
||||
name: 'flexibleRollout',
|
||||
parameters: {
|
||||
groupId: 'demo',
|
||||
rollout: 20,
|
||||
stickiness: 'default',
|
||||
},
|
||||
})
|
||||
.expect(200)
|
||||
.expect((res) => {
|
||||
strategy = res.body;
|
||||
});
|
||||
|
||||
await app.request
|
||||
.patch(
|
||||
`${BASE_URI}/features/${featureName}/environments/${envName}/strategies/${strategy.id}`,
|
||||
)
|
||||
.send([{ op: 'replace', path: '/parameters/rollout', value: 42 }])
|
||||
.expect(200);
|
||||
await app.request
|
||||
.get(
|
||||
`${BASE_URI}/features/${featureName}/environments/${envName}/strategies/${strategy.id}`,
|
||||
)
|
||||
.expect(200)
|
||||
.expect((res) => {
|
||||
expect(res.body.parameters.rollout).toBe(42);
|
||||
});
|
||||
});
|
||||
|
||||
test('Trying to get a non existing feature strategy should yield 404', async () => {
|
||||
const envName = 'feature.non.existing.strategy.get';
|
||||
// Create environment
|
||||
@ -584,3 +778,95 @@ test('Trying to get a non existing feature strategy should yield 404', async ()
|
||||
)
|
||||
.expect(404);
|
||||
});
|
||||
|
||||
test('Can not enable environment for feature without strategies', async () => {
|
||||
const environment = 'some-env';
|
||||
const featureName = 'com.test.enable.environment.disabled';
|
||||
|
||||
// Create environment
|
||||
await app.request
|
||||
.post('/api/admin/environments')
|
||||
.send({
|
||||
name: environment,
|
||||
displayName: 'Enable feature for environment',
|
||||
})
|
||||
.set('Content-Type', 'application/json')
|
||||
.expect(201);
|
||||
// Connect environment to project
|
||||
await app.request
|
||||
.post('/api/admin/projects/default/environments')
|
||||
.send({ environment })
|
||||
.expect(200);
|
||||
|
||||
// Create feature
|
||||
await app.request
|
||||
.post('/api/admin/projects/default/features')
|
||||
.send({
|
||||
name: featureName,
|
||||
})
|
||||
.set('Content-Type', 'application/json')
|
||||
.expect(201);
|
||||
await app.request
|
||||
.post(
|
||||
`/api/admin/projects/default/features/${featureName}/environments/${environment}/on`,
|
||||
)
|
||||
.set('Content-Type', 'application/json')
|
||||
.expect(403);
|
||||
await app.request
|
||||
.get('/api/admin/projects/default/features/com.test.enable.environment')
|
||||
.expect(200)
|
||||
.expect('Content-Type', /json/)
|
||||
.expect((res) => {
|
||||
const enabledFeatureEnv = res.body.environments.find(
|
||||
(e) => e.name === environment,
|
||||
);
|
||||
expect(enabledFeatureEnv.enabled).toBe(false);
|
||||
});
|
||||
});
|
||||
|
||||
test('Can delete strategy from feature toggle', async () => {
|
||||
const envName = 'del-strategy';
|
||||
const featureName = 'feature.strategy.toggle.delete.strategy';
|
||||
// Create environment
|
||||
await app.request
|
||||
.post('/api/admin/environments')
|
||||
.send({
|
||||
name: envName,
|
||||
displayName: 'Enable feature for environment',
|
||||
})
|
||||
.set('Content-Type', 'application/json')
|
||||
.expect(201);
|
||||
// Connect environment to project
|
||||
await app.request
|
||||
.post('/api/admin/projects/default/environments')
|
||||
.send({
|
||||
environment: envName,
|
||||
})
|
||||
.expect(200);
|
||||
|
||||
await app.request
|
||||
.post('/api/admin/projects/default/features')
|
||||
.send({ name: featureName })
|
||||
.expect(201);
|
||||
await app.request
|
||||
.post(
|
||||
`/api/admin/projects/default/features/${featureName}/environments/${envName}/strategies`,
|
||||
)
|
||||
.send({
|
||||
name: 'default',
|
||||
parameters: {
|
||||
userId: 'string',
|
||||
},
|
||||
})
|
||||
.expect(200);
|
||||
const { body } = await app.request.get(
|
||||
`/api/admin/projects/default/features/${featureName}/environments/${envName}/strategies`,
|
||||
);
|
||||
const strategies = body;
|
||||
const strategyId = strategies[0].id;
|
||||
await app.request
|
||||
.delete(
|
||||
`/api/admin/projects/default/features/${featureName}/environments/${envName}/strategies/${strategyId}`,
|
||||
)
|
||||
.expect(200);
|
||||
});
|
||||
|
@ -1,11 +1,9 @@
|
||||
'use strict';
|
||||
import { IUnleashTest, setupApp } from '../../helpers/test-helper';
|
||||
import dbInit, { ITestDb } from '../../helpers/database-init';
|
||||
import getLogger from '../../../fixtures/no-logger';
|
||||
|
||||
const { setupApp } = require('../../helpers/test-helper');
|
||||
const dbInit = require('../../helpers/database-init');
|
||||
const getLogger = require('../../../fixtures/no-logger');
|
||||
|
||||
let app;
|
||||
let db;
|
||||
let app: IUnleashTest;
|
||||
let db: ITestDb;
|
||||
|
||||
beforeAll(async () => {
|
||||
db = await dbInit('feature_api_client', getLogger);
|
||||
@ -18,10 +16,14 @@ beforeAll(async () => {
|
||||
},
|
||||
'test',
|
||||
);
|
||||
await app.services.featureToggleServiceV2.createFeatureToggle('default', {
|
||||
name: 'featureY',
|
||||
description: 'soon to be the #1 feature',
|
||||
});
|
||||
await app.services.featureToggleServiceV2.createFeatureToggle(
|
||||
'default',
|
||||
{
|
||||
name: 'featureY',
|
||||
description: 'soon to be the #1 feature',
|
||||
},
|
||||
'test',
|
||||
);
|
||||
await app.services.featureToggleServiceV2.createFeatureToggle(
|
||||
'default',
|
||||
{
|
||||
@ -38,6 +40,7 @@ beforeAll(async () => {
|
||||
},
|
||||
'test',
|
||||
);
|
||||
|
||||
await app.services.featureToggleServiceV2.archiveToggle(
|
||||
'featureArchivedX',
|
||||
'test',
|
||||
@ -51,6 +54,7 @@ beforeAll(async () => {
|
||||
},
|
||||
'test',
|
||||
);
|
||||
|
||||
await app.services.featureToggleServiceV2.archiveToggle(
|
||||
'featureArchivedY',
|
||||
'test',
|
||||
@ -73,8 +77,18 @@ beforeAll(async () => {
|
||||
name: 'feature.with.variants',
|
||||
description: 'A feature toggle with variants',
|
||||
variants: [
|
||||
{ name: 'control', weight: 50 },
|
||||
{ name: 'new', weight: 50 },
|
||||
{
|
||||
name: 'control',
|
||||
weight: 50,
|
||||
weightType: 'fix',
|
||||
stickiness: 'default',
|
||||
},
|
||||
{
|
||||
name: 'new',
|
||||
weight: 50,
|
||||
weightType: 'fix',
|
||||
stickiness: 'default',
|
||||
},
|
||||
],
|
||||
},
|
||||
'test',
|
||||
@ -86,14 +100,15 @@ afterAll(async () => {
|
||||
await db.destroy();
|
||||
});
|
||||
|
||||
test('returns four feature toggles', async () =>
|
||||
test('returns four feature toggles', async () => {
|
||||
app.request
|
||||
.get('/api/client/features')
|
||||
.expect('Content-Type', /json/)
|
||||
.expect(200)
|
||||
.expect((res) => {
|
||||
expect(res.body.features.length).toBe(4);
|
||||
}));
|
||||
expect(res.body.features).toHaveLength(4);
|
||||
});
|
||||
});
|
||||
|
||||
test('returns four feature toggles without createdAt', async () =>
|
||||
app.request
|
||||
@ -101,6 +116,7 @@ test('returns four feature toggles without createdAt', async () =>
|
||||
.expect('Content-Type', /json/)
|
||||
.expect(200)
|
||||
.expect((res) => {
|
||||
expect(res.body.features).toHaveLength(4);
|
||||
expect(res.body.features[0].createdAt).toBeFalsy();
|
||||
}));
|
||||
|
||||
@ -130,11 +146,64 @@ test('Can filter features by namePrefix', async () => {
|
||||
.expect('Content-Type', /json/)
|
||||
.expect(200)
|
||||
.expect((res) => {
|
||||
expect(res.body.features.length).toBe(1);
|
||||
expect(res.body.features).toHaveLength(1);
|
||||
expect(res.body.features[0].name).toBe('feature.with.variants');
|
||||
});
|
||||
});
|
||||
|
||||
test('Can get strategies for specific environment', async () => {
|
||||
const featureName = 'test.feature.with.env';
|
||||
|
||||
// Create feature toggle
|
||||
await app.request.post('/api/admin/projects/default/features').send({
|
||||
name: featureName,
|
||||
type: 'killswitch',
|
||||
});
|
||||
|
||||
// Add global strategy
|
||||
await app.request
|
||||
.post(
|
||||
`/api/admin/projects/default/features/${featureName}/environments/:global:/strategies`,
|
||||
)
|
||||
.send({
|
||||
name: 'default',
|
||||
})
|
||||
.expect(200);
|
||||
|
||||
// create new env
|
||||
|
||||
await db.stores.environmentStore.upsert({
|
||||
name: 'testing',
|
||||
displayName: 'simple test',
|
||||
});
|
||||
|
||||
await app.services.environmentService.addEnvironmentToProject(
|
||||
'testing',
|
||||
'default',
|
||||
);
|
||||
|
||||
await app.request
|
||||
.post(
|
||||
`/api/admin/projects/default/features/${featureName}/environments/testing/strategies`,
|
||||
)
|
||||
.send({
|
||||
name: 'custom1',
|
||||
})
|
||||
.expect(200);
|
||||
|
||||
await app.request
|
||||
.get(`/api/client/features/${featureName}?environment=testing`)
|
||||
.expect('Content-Type', /json/)
|
||||
.expect(200)
|
||||
.expect((res) => {
|
||||
expect(res.body.name).toBe(featureName);
|
||||
expect(res.body.strategies).toHaveLength(2);
|
||||
expect(
|
||||
res.body.strategies.find((s) => s.name === 'custom1'),
|
||||
).toBeDefined();
|
||||
});
|
||||
});
|
||||
|
||||
test('Can use multiple filters', async () => {
|
||||
expect.assertions(3);
|
||||
|
||||
@ -174,13 +243,13 @@ test('Can use multiple filters', async () => {
|
||||
.get('/api/client/features?tag=simple:Crazy')
|
||||
.expect('Content-Type', /json/)
|
||||
.expect(200)
|
||||
.expect((res) => expect(res.body.features.length).toBe(2));
|
||||
.expect((res) => expect(res.body.features).toHaveLength(2));
|
||||
await app.request
|
||||
.get('/api/client/features?namePrefix=test&tag=simple:Crazy')
|
||||
.expect('Content-Type', /json/)
|
||||
.expect(200)
|
||||
.expect((res) => {
|
||||
expect(res.body.features.length).toBe(1);
|
||||
expect(res.body.features).toHaveLength(1);
|
||||
expect(res.body.features[0].name).toBe('test.feature');
|
||||
});
|
||||
});
|
@ -8,6 +8,8 @@ import dbState from './database.json';
|
||||
import { LogProvider } from '../../../lib/logger';
|
||||
import noLoggerProvider from '../../fixtures/no-logger';
|
||||
import EnvironmentStore from '../../../lib/db/environment-store';
|
||||
import { IUnleashStores } from '../../../lib/types';
|
||||
import { IFeatureEnvironmentStore } from '../../../lib/types/stores/feature-environment-store';
|
||||
|
||||
// require('db-migrate-shared').log.silence(false);
|
||||
|
||||
@ -51,7 +53,7 @@ function createTagTypes(store) {
|
||||
return dbState.tag_types.map((t) => store.createTagType(t));
|
||||
}
|
||||
|
||||
async function connectProject(store: EnvironmentStore): Promise<void> {
|
||||
async function connectProject(store: IFeatureEnvironmentStore): Promise<void> {
|
||||
await store.connectProject(':global:', 'default');
|
||||
}
|
||||
|
||||
@ -65,13 +67,19 @@ async function setupDatabase(stores) {
|
||||
await Promise.all(createContextFields(stores.contextFieldStore));
|
||||
await Promise.all(createProjects(stores.projectStore));
|
||||
await Promise.all(createTagTypes(stores.tagTypeStore));
|
||||
await connectProject(stores.environmentStore);
|
||||
await connectProject(stores.featureEnvironmentStore);
|
||||
}
|
||||
|
||||
export interface ITestDb {
|
||||
stores: IUnleashStores;
|
||||
reset: () => Promise<void>;
|
||||
destroy: () => Promise<void>;
|
||||
}
|
||||
|
||||
export default async function init(
|
||||
databaseSchema: String = 'test',
|
||||
getLogger: LogProvider = noLoggerProvider,
|
||||
): Promise<any> {
|
||||
): Promise<ITestDb> {
|
||||
const config = createTestConfig({
|
||||
db: {
|
||||
...dbConfig.getDb(),
|
||||
|
@ -63,15 +63,15 @@ test('Can update display name', async () => {
|
||||
|
||||
test('Can connect environment to project', async () => {
|
||||
await service.create({ name: 'test-connection', displayName: '' });
|
||||
await stores.featureToggleStore.createFeature('default', {
|
||||
await stores.featureToggleStore.create('default', {
|
||||
name: 'test-connection',
|
||||
type: 'release',
|
||||
description: '',
|
||||
stale: false,
|
||||
variants: [],
|
||||
});
|
||||
await service.connectProjectToEnvironment('test-connection', 'default');
|
||||
const overview = await stores.projectStore.getProjectOverview(
|
||||
await service.addEnvironmentToProject('test-connection', 'default');
|
||||
const overview = await stores.featureStrategiesStore.getFeatureOverview(
|
||||
'default',
|
||||
false,
|
||||
);
|
||||
@ -88,12 +88,12 @@ test('Can connect environment to project', async () => {
|
||||
|
||||
test('Can remove environment from project', async () => {
|
||||
await service.create({ name: 'removal-test', displayName: '' });
|
||||
await stores.featureToggleStore.createFeature('default', {
|
||||
await stores.featureToggleStore.create('default', {
|
||||
name: 'removal-test',
|
||||
});
|
||||
await service.removeEnvironmentFromProject('test-connection', 'default');
|
||||
await service.connectProjectToEnvironment('removal-test', 'default');
|
||||
let overview = await stores.projectStore.getProjectOverview(
|
||||
await service.addEnvironmentToProject('removal-test', 'default');
|
||||
let overview = await stores.featureStrategiesStore.getFeatureOverview(
|
||||
'default',
|
||||
false,
|
||||
);
|
||||
@ -108,7 +108,10 @@ test('Can remove environment from project', async () => {
|
||||
]);
|
||||
});
|
||||
await service.removeEnvironmentFromProject('removal-test', 'default');
|
||||
overview = await stores.projectStore.getProjectOverview('default', false);
|
||||
overview = await stores.featureStrategiesStore.getFeatureOverview(
|
||||
'default',
|
||||
false,
|
||||
);
|
||||
expect(overview.length).toBeGreaterThan(0);
|
||||
overview.forEach((o) => {
|
||||
expect(o.environments).toEqual([]);
|
||||
@ -120,9 +123,9 @@ test('Adding same environment twice should throw a NameExistsError', async () =>
|
||||
await service.removeEnvironmentFromProject('test-connection', 'default');
|
||||
await service.removeEnvironmentFromProject('removal-test', 'default');
|
||||
|
||||
await service.connectProjectToEnvironment('uniqueness-test', 'default');
|
||||
await service.addEnvironmentToProject('uniqueness-test', 'default');
|
||||
return expect(async () =>
|
||||
service.connectProjectToEnvironment('uniqueness-test', 'default'),
|
||||
service.addEnvironmentToProject('uniqueness-test', 'default'),
|
||||
).rejects.toThrow(
|
||||
new NameExistsError(
|
||||
'default already has the environment uniqueness-test enabled',
|
||||
|
@ -97,7 +97,13 @@ test('Should include legacy props in event log when updating strategy configurat
|
||||
);
|
||||
|
||||
await service.createStrategy(config, 'default', featureName);
|
||||
await service.updateEnabled(featureName, GLOBAL_ENV, true, userName);
|
||||
await service.updateEnabled(
|
||||
'default',
|
||||
featureName,
|
||||
GLOBAL_ENV,
|
||||
true,
|
||||
userName,
|
||||
);
|
||||
|
||||
const events = await eventService.getEventsForToggle(featureName);
|
||||
expect(events[0].type).toBe(FEATURE_UPDATED);
|
||||
|
@ -1,18 +1,20 @@
|
||||
import dbInit from '../helpers/database-init';
|
||||
import dbInit, { ITestDb } from '../helpers/database-init';
|
||||
import getLogger from '../../fixtures/no-logger';
|
||||
import FeatureToggleServiceV2 from '../../../lib/services/feature-toggle-service-v2';
|
||||
import { AccessService } from '../../../lib/services/access-service';
|
||||
import ProjectService from '../../../lib/services/project-service';
|
||||
import ProjectHealthService from '../../../lib/services/project-health-service';
|
||||
import { createTestConfig } from '../../config/test-config';
|
||||
import { IUnleashStores } from '../../../lib/types';
|
||||
import { IUser } from '../../../lib/server-impl';
|
||||
|
||||
let stores;
|
||||
let db;
|
||||
let stores: IUnleashStores;
|
||||
let db: ITestDb;
|
||||
let projectService;
|
||||
let accessService;
|
||||
let projectHealthService;
|
||||
let featureToggleService;
|
||||
let user;
|
||||
let user: IUser;
|
||||
|
||||
beforeAll(async () => {
|
||||
const config = createTestConfig();
|
||||
@ -30,7 +32,11 @@ beforeAll(async () => {
|
||||
accessService,
|
||||
featureToggleService,
|
||||
);
|
||||
projectHealthService = new ProjectHealthService(stores, config);
|
||||
projectHealthService = new ProjectHealthService(
|
||||
stores,
|
||||
config,
|
||||
featureToggleService,
|
||||
);
|
||||
});
|
||||
|
||||
afterAll(async () => {
|
||||
@ -43,12 +49,12 @@ test('Project with no stale toggles should have 100% health rating', async () =>
|
||||
description: 'Fancy',
|
||||
};
|
||||
const savedProject = await projectService.createProject(project, user);
|
||||
await stores.featureToggleStore.createFeature('health-rating', {
|
||||
await stores.featureToggleStore.create('health-rating', {
|
||||
name: 'health-rating-not-stale',
|
||||
description: 'new',
|
||||
stale: false,
|
||||
});
|
||||
await stores.featureToggleStore.createFeature('health-rating', {
|
||||
await stores.featureToggleStore.create('health-rating', {
|
||||
name: 'health-rating-not-stale-2',
|
||||
description: 'new too',
|
||||
stale: false,
|
||||
@ -66,22 +72,22 @@ test('Project with two stale toggles and two non stale should have 50% health ra
|
||||
description: 'Fancy',
|
||||
};
|
||||
const savedProject = await projectService.createProject(project, user);
|
||||
await stores.featureToggleStore.createFeature('health-rating-2', {
|
||||
await stores.featureToggleStore.create('health-rating-2', {
|
||||
name: 'health-rating-2-not-stale',
|
||||
description: 'new',
|
||||
stale: false,
|
||||
});
|
||||
await stores.featureToggleStore.createFeature('health-rating-2', {
|
||||
await stores.featureToggleStore.create('health-rating-2', {
|
||||
name: 'health-rating-2-not-stale-2',
|
||||
description: 'new too',
|
||||
stale: false,
|
||||
});
|
||||
await stores.featureToggleStore.createFeature('health-rating-2', {
|
||||
await stores.featureToggleStore.create('health-rating-2', {
|
||||
name: 'health-rating-2-stale-1',
|
||||
description: 'stale',
|
||||
stale: true,
|
||||
});
|
||||
await stores.featureToggleStore.createFeature('health-rating-2', {
|
||||
await stores.featureToggleStore.create('health-rating-2', {
|
||||
name: 'health-rating-2-stale-2',
|
||||
description: 'stale too',
|
||||
stale: true,
|
||||
@ -99,19 +105,19 @@ test('Project with one non-stale, one potentially stale and one stale should hav
|
||||
description: 'Fancy',
|
||||
};
|
||||
const savedProject = await projectService.createProject(project, user);
|
||||
await stores.featureToggleStore.createFeature('health-rating-3', {
|
||||
await stores.featureToggleStore.create('health-rating-3', {
|
||||
name: 'health-rating-3-not-stale',
|
||||
description: 'new',
|
||||
stale: false,
|
||||
});
|
||||
await stores.featureToggleStore.createFeature('health-rating-3', {
|
||||
await stores.featureToggleStore.create('health-rating-3', {
|
||||
name: 'health-rating-3-potentially-stale',
|
||||
description: 'new too',
|
||||
type: 'release',
|
||||
stale: false,
|
||||
createdAt: new Date(Date.UTC(2020, 1, 1)),
|
||||
});
|
||||
await stores.featureToggleStore.createFeature('health-rating-3', {
|
||||
await stores.featureToggleStore.create('health-rating-3', {
|
||||
name: 'health-rating-3-stale',
|
||||
description: 'stale',
|
||||
stale: true,
|
||||
|
@ -1,4 +1,4 @@
|
||||
import dbInit from '../helpers/database-init';
|
||||
import dbInit, { ITestDb } from '../helpers/database-init';
|
||||
import getLogger from '../../fixtures/no-logger';
|
||||
import FeatureToggleServiceV2 from '../../../lib/services/feature-toggle-service-v2';
|
||||
import ProjectService from '../../../lib/services/project-service';
|
||||
@ -13,7 +13,7 @@ import { createTestConfig } from '../../config/test-config';
|
||||
import { RoleName } from '../../../lib/types/model';
|
||||
|
||||
let stores;
|
||||
let db;
|
||||
let db: ITestDb;
|
||||
|
||||
let projectService;
|
||||
let accessService;
|
||||
@ -104,7 +104,7 @@ test('should not be able to delete project with toggles', async () => {
|
||||
description: 'Blah',
|
||||
};
|
||||
await projectService.createProject(project, user);
|
||||
await stores.featureToggleStore.createFeature(project.id, {
|
||||
await stores.featureToggleStore.create(project.id, {
|
||||
name: 'test-project-delete',
|
||||
project: project.id,
|
||||
enabled: false,
|
||||
|
@ -17,7 +17,7 @@ beforeAll(async () => {
|
||||
featureTagStore = stores.featureTagStore;
|
||||
featureToggleStore = stores.featureToggleStore;
|
||||
await stores.tagStore.createTag(tag);
|
||||
await featureToggleStore.createFeature('default', { name: featureName });
|
||||
await featureToggleStore.create('default', { name: featureName });
|
||||
});
|
||||
|
||||
afterAll(async () => {
|
||||
@ -83,7 +83,7 @@ test('should throw if feature have tag', async () => {
|
||||
|
||||
test('get all feature tags', async () => {
|
||||
await featureTagStore.tagFeature(featureName, tag);
|
||||
await featureToggleStore.createFeature('default', {
|
||||
await featureToggleStore.create('default', {
|
||||
name: 'some-other-toggle',
|
||||
});
|
||||
await featureTagStore.tagFeature('some-other-toggle', tag);
|
||||
@ -92,7 +92,7 @@ test('get all feature tags', async () => {
|
||||
});
|
||||
|
||||
test('should import feature tags', async () => {
|
||||
await featureToggleStore.createFeature('default', {
|
||||
await featureToggleStore.create('default', {
|
||||
name: 'some-other-toggle-import',
|
||||
});
|
||||
await featureTagStore.importFeatureTags([
|
||||
|
36
src/test/fixtures/fake-environment-store.ts
vendored
36
src/test/fixtures/fake-environment-store.ts
vendored
@ -31,47 +31,11 @@ export default class FakeEnvironmentStore implements IEnvironmentStore {
|
||||
return Promise.resolve(env);
|
||||
}
|
||||
|
||||
async connectProject(
|
||||
// eslint-disable-next-line @typescript-eslint/no-unused-vars
|
||||
environment: string,
|
||||
// eslint-disable-next-line @typescript-eslint/no-unused-vars
|
||||
projectId: string,
|
||||
): Promise<void> {
|
||||
return Promise.reject(new Error('Not implemented'));
|
||||
}
|
||||
|
||||
async connectFeatures(
|
||||
// eslint-disable-next-line @typescript-eslint/no-unused-vars
|
||||
environment: string,
|
||||
// eslint-disable-next-line @typescript-eslint/no-unused-vars
|
||||
projectId: string,
|
||||
): Promise<void> {
|
||||
return Promise.reject(new Error('Not implemented'));
|
||||
}
|
||||
|
||||
async delete(name: string): Promise<void> {
|
||||
this.environments = this.environments.filter((e) => e.name !== name);
|
||||
return Promise.resolve();
|
||||
}
|
||||
|
||||
async disconnectProjectFromEnv(
|
||||
// eslint-disable-next-line @typescript-eslint/no-unused-vars
|
||||
environment: string,
|
||||
// eslint-disable-next-line @typescript-eslint/no-unused-vars
|
||||
projectId: string,
|
||||
): Promise<void> {
|
||||
return Promise.reject(new Error('Not implemented'));
|
||||
}
|
||||
|
||||
async connectFeatureToEnvironmentsForProject(
|
||||
// eslint-disable-next-line @typescript-eslint/no-unused-vars
|
||||
featureName: string,
|
||||
// eslint-disable-next-line @typescript-eslint/no-unused-vars
|
||||
project_id: string,
|
||||
): Promise<void> {
|
||||
return Promise.reject(new Error('Not implemented'));
|
||||
}
|
||||
|
||||
async deleteAll(): Promise<void> {
|
||||
this.environments = [];
|
||||
}
|
||||
|
@ -10,7 +10,7 @@ export default class FakeFeatureEnvironmentStore
|
||||
{
|
||||
featureEnvironments: IFeatureEnvironment[] = [];
|
||||
|
||||
async connectEnvironmentAndFeature(
|
||||
async addEnvironmentToFeature(
|
||||
featureName: string,
|
||||
environment: string,
|
||||
enabled: boolean,
|
||||
@ -35,7 +35,7 @@ export default class FakeFeatureEnvironmentStore
|
||||
|
||||
destroy(): void {}
|
||||
|
||||
async disconnectEnvironmentFromProject(
|
||||
async disconnectFeatures(
|
||||
// eslint-disable-next-line @typescript-eslint/no-unused-vars
|
||||
environment: string,
|
||||
// eslint-disable-next-line @typescript-eslint/no-unused-vars
|
||||
@ -44,14 +44,6 @@ export default class FakeFeatureEnvironmentStore
|
||||
return Promise.resolve(undefined);
|
||||
}
|
||||
|
||||
async enableEnvironmentForFeature(
|
||||
featureName: string,
|
||||
environment: string,
|
||||
): Promise<void> {
|
||||
const fE = await this.get({ featureName, environment });
|
||||
fE.enabled = true;
|
||||
}
|
||||
|
||||
async exists(key: FeatureEnvironmentKey): Promise<boolean> {
|
||||
return this.featureEnvironments.some(
|
||||
(fE) =>
|
||||
@ -85,10 +77,6 @@ export default class FakeFeatureEnvironmentStore
|
||||
return this.featureEnvironments;
|
||||
}
|
||||
|
||||
async getAllFeatureEnvironments(): Promise<IFeatureEnvironment[]> {
|
||||
return this.getAll();
|
||||
}
|
||||
|
||||
getEnvironmentMetaData(
|
||||
environment: string,
|
||||
featureName: string,
|
||||
@ -115,7 +103,7 @@ export default class FakeFeatureEnvironmentStore
|
||||
return this.delete({ featureName, environment });
|
||||
}
|
||||
|
||||
async toggleEnvironmentEnabledStatus(
|
||||
async setEnvironmentEnabledStatus(
|
||||
environment: string,
|
||||
featureName: string,
|
||||
enabled: boolean,
|
||||
@ -124,4 +112,40 @@ export default class FakeFeatureEnvironmentStore
|
||||
fE.enabled = enabled;
|
||||
return enabled;
|
||||
}
|
||||
|
||||
async connectProject(
|
||||
// eslint-disable-next-line @typescript-eslint/no-unused-vars
|
||||
environment: string,
|
||||
// eslint-disable-next-line @typescript-eslint/no-unused-vars
|
||||
projectId: string,
|
||||
): Promise<void> {
|
||||
return Promise.reject(new Error('Not implemented'));
|
||||
}
|
||||
|
||||
async connectFeatures(
|
||||
// eslint-disable-next-line @typescript-eslint/no-unused-vars
|
||||
environment: string,
|
||||
// eslint-disable-next-line @typescript-eslint/no-unused-vars
|
||||
projectId: string,
|
||||
): Promise<void> {
|
||||
return Promise.reject(new Error('Not implemented'));
|
||||
}
|
||||
|
||||
async disconnectProject(
|
||||
// eslint-disable-next-line @typescript-eslint/no-unused-vars
|
||||
environment: string,
|
||||
// eslint-disable-next-line @typescript-eslint/no-unused-vars
|
||||
projectId: string,
|
||||
): Promise<void> {
|
||||
return Promise.reject(new Error('Not implemented'));
|
||||
}
|
||||
|
||||
async connectFeatureToEnvironmentsForProject(
|
||||
// eslint-disable-next-line @typescript-eslint/no-unused-vars
|
||||
featureName: string,
|
||||
// eslint-disable-next-line @typescript-eslint/no-unused-vars
|
||||
projectId: string,
|
||||
): Promise<void> {
|
||||
return Promise.reject(new Error('Not implemented'));
|
||||
}
|
||||
}
|
||||
|
@ -2,7 +2,7 @@ import { randomUUID } from 'crypto';
|
||||
import {
|
||||
FeatureToggle,
|
||||
FeatureToggleWithEnvironment,
|
||||
IFeatureEnvironment,
|
||||
IFeatureOverview,
|
||||
IFeatureStrategy,
|
||||
IFeatureToggleClient,
|
||||
IFeatureToggleQuery,
|
||||
@ -26,7 +26,7 @@ export default class FakeFeatureStrategiesStore
|
||||
|
||||
featureToggles: FeatureToggle[] = [];
|
||||
|
||||
async createStrategyConfig(
|
||||
async createStrategyFeatureEnv(
|
||||
strategyConfig: Omit<IFeatureStrategy, 'id' | 'createdAt'>,
|
||||
): Promise<IFeatureStrategy> {
|
||||
const newStrat = { ...strategyConfig, id: randomUUID() };
|
||||
@ -34,14 +34,6 @@ export default class FakeFeatureStrategiesStore
|
||||
return Promise.resolve(newStrat);
|
||||
}
|
||||
|
||||
async getStrategiesForToggle(
|
||||
featureName: string,
|
||||
): Promise<IFeatureStrategy[]> {
|
||||
return this.featureStrategies.filter(
|
||||
(fS) => fS.featureName === featureName,
|
||||
);
|
||||
}
|
||||
|
||||
// eslint-disable-next-line @typescript-eslint/explicit-module-boundary-types
|
||||
async createFeature(feature: any): Promise<void> {
|
||||
this.featureToggles.push({
|
||||
@ -53,24 +45,11 @@ export default class FakeFeatureStrategiesStore
|
||||
return Promise.resolve();
|
||||
}
|
||||
|
||||
async getAllFeatureStrategies(): Promise<IFeatureStrategy[]> {
|
||||
return this.featureStrategies;
|
||||
}
|
||||
|
||||
async deleteFeatureStrategies(): Promise<void> {
|
||||
this.featureStrategies = [];
|
||||
return Promise.resolve();
|
||||
}
|
||||
|
||||
async getStrategiesForEnvironment(
|
||||
environment: string,
|
||||
): Promise<IFeatureStrategy[]> {
|
||||
const stratEnvs = this.featureStrategies.filter(
|
||||
(fS) => fS.environment === environment,
|
||||
);
|
||||
return Promise.resolve(stratEnvs);
|
||||
}
|
||||
|
||||
async hasStrategy(id: string): Promise<boolean> {
|
||||
return this.featureStrategies.some((s) => s.id === id);
|
||||
}
|
||||
@ -98,7 +77,7 @@ export default class FakeFeatureStrategiesStore
|
||||
throw new Error('Method not implemented.');
|
||||
}
|
||||
|
||||
async removeAllStrategiesForEnv(
|
||||
async removeAllStrategiesForFeatureEnv(
|
||||
feature_name: string,
|
||||
environment: string,
|
||||
): Promise<void> {
|
||||
@ -122,29 +101,21 @@ export default class FakeFeatureStrategiesStore
|
||||
return Promise.resolve(this.featureStrategies);
|
||||
}
|
||||
|
||||
async getStrategiesForFeature(
|
||||
async getStrategiesForFeatureEnv(
|
||||
project_name: string,
|
||||
feature_name: string,
|
||||
environment: string,
|
||||
): Promise<IFeatureStrategy[]> {
|
||||
const rows = this.featureStrategies.filter(
|
||||
(fS) =>
|
||||
fS.projectName === project_name &&
|
||||
fS.projectId === project_name &&
|
||||
fS.featureName === feature_name &&
|
||||
fS.environment === environment,
|
||||
);
|
||||
return Promise.resolve(rows);
|
||||
}
|
||||
|
||||
async getStrategiesForEnv(
|
||||
environment: string,
|
||||
): Promise<IFeatureStrategy[]> {
|
||||
return this.featureStrategies.filter(
|
||||
(fS) => fS.environment === environment,
|
||||
);
|
||||
}
|
||||
|
||||
async getFeatureToggleAdmin(
|
||||
async getFeatureToggleWithEnvs(
|
||||
featureName: string,
|
||||
archived: boolean = false,
|
||||
): Promise<FeatureToggleWithEnvironment> {
|
||||
@ -159,6 +130,15 @@ export default class FakeFeatureStrategiesStore
|
||||
);
|
||||
}
|
||||
|
||||
async getFeatureOverview(
|
||||
// eslint-disable-next-line @typescript-eslint/no-unused-vars
|
||||
projectId: string,
|
||||
// eslint-disable-next-line @typescript-eslint/no-unused-vars
|
||||
archived: boolean,
|
||||
): Promise<IFeatureOverview[]> {
|
||||
return Promise.resolve([]);
|
||||
}
|
||||
|
||||
async getFeatures(
|
||||
featureQuery?: IFeatureToggleQuery,
|
||||
archived: boolean = false,
|
||||
@ -214,31 +194,6 @@ export default class FakeFeatureStrategiesStore
|
||||
return Promise.resolve();
|
||||
}
|
||||
|
||||
async enableEnvironmentForFeature(
|
||||
feature_name: string,
|
||||
environment: string,
|
||||
): Promise<void> {
|
||||
if (!this.environmentAndFeature.has(environment)) {
|
||||
this.environmentAndFeature.set(environment, [
|
||||
{
|
||||
featureName: feature_name,
|
||||
enabled: true,
|
||||
},
|
||||
]);
|
||||
}
|
||||
const features = this.environmentAndFeature
|
||||
.get(environment)
|
||||
.map((f) => {
|
||||
if (f.featureName === feature_name) {
|
||||
// eslint-disable-next-line no-param-reassign
|
||||
f.enabled = true;
|
||||
}
|
||||
return f;
|
||||
});
|
||||
this.environmentAndFeature.set(environment, features);
|
||||
return Promise.resolve();
|
||||
}
|
||||
|
||||
async removeEnvironmentForFeature(
|
||||
feature_name: string,
|
||||
environment: string,
|
||||
@ -275,15 +230,6 @@ export default class FakeFeatureStrategiesStore
|
||||
return Promise.resolve(this.featureStrategies.find((f) => f.id === id));
|
||||
}
|
||||
|
||||
async getStrategiesAndMetadataForEnvironment(
|
||||
// eslint-disable-next-line @typescript-eslint/no-unused-vars
|
||||
environment: string,
|
||||
// eslint-disable-next-line @typescript-eslint/no-unused-vars
|
||||
featureName: string,
|
||||
): Promise<void> {
|
||||
return Promise.resolve();
|
||||
}
|
||||
|
||||
async deleteConfigurationsForProjectAndEnvironment(
|
||||
// eslint-disable-next-line @typescript-eslint/no-unused-vars
|
||||
projectId: String,
|
||||
@ -304,17 +250,13 @@ export default class FakeFeatureStrategiesStore
|
||||
return Promise.resolve(enabled);
|
||||
}
|
||||
|
||||
async toggleEnvironmentEnabledStatus(
|
||||
async setEnvironmentEnabledStatus(
|
||||
environment: string,
|
||||
featureName: string,
|
||||
enabled: boolean,
|
||||
): Promise<boolean> {
|
||||
return Promise.resolve(enabled);
|
||||
}
|
||||
|
||||
async getAllFeatureEnvironments(): Promise<IFeatureEnvironment[]> {
|
||||
return Promise.resolve([]);
|
||||
}
|
||||
}
|
||||
|
||||
module.exports = FakeFeatureStrategiesStore;
|
||||
|
67
src/test/fixtures/fake-feature-toggle-client-store.ts
vendored
Normal file
67
src/test/fixtures/fake-feature-toggle-client-store.ts
vendored
Normal file
@ -0,0 +1,67 @@
|
||||
import {
|
||||
FeatureToggle,
|
||||
IFeatureToggleClient,
|
||||
IFeatureToggleQuery,
|
||||
} from '../../lib/types/model';
|
||||
import { IFeatureToggleClientStore } from '../../lib/types/stores/feature-toggle-client-store';
|
||||
|
||||
export default class FakeFeatureToggleClientStore
|
||||
implements IFeatureToggleClientStore
|
||||
{
|
||||
featureToggles: FeatureToggle[] = [];
|
||||
|
||||
async getFeatures(
|
||||
featureQuery?: IFeatureToggleQuery,
|
||||
archived: boolean = false,
|
||||
): Promise<IFeatureToggleClient[]> {
|
||||
const rows = this.featureToggles.filter((toggle) => {
|
||||
if (featureQuery.namePrefix) {
|
||||
if (featureQuery.project) {
|
||||
return (
|
||||
toggle.name.startsWith(featureQuery.namePrefix) &&
|
||||
featureQuery.project.includes(toggle.project)
|
||||
);
|
||||
}
|
||||
return toggle.name.startsWith(featureQuery.namePrefix);
|
||||
}
|
||||
if (featureQuery.project) {
|
||||
return featureQuery.project.includes(toggle.project);
|
||||
}
|
||||
return toggle.archived === archived;
|
||||
});
|
||||
const clientRows: IFeatureToggleClient[] = rows.map((t) => ({
|
||||
...t,
|
||||
enabled: true,
|
||||
strategies: [],
|
||||
description: t.description || '',
|
||||
type: t.type || 'Release',
|
||||
stale: t.stale || false,
|
||||
variants: [],
|
||||
}));
|
||||
return Promise.resolve(clientRows);
|
||||
}
|
||||
|
||||
async getClient(
|
||||
query?: IFeatureToggleQuery,
|
||||
): Promise<IFeatureToggleClient[]> {
|
||||
return this.getFeatures(query);
|
||||
}
|
||||
|
||||
async getAdmin(
|
||||
query?: IFeatureToggleQuery,
|
||||
archived: boolean = false,
|
||||
): Promise<IFeatureToggleClient[]> {
|
||||
return this.getFeatures(query, archived);
|
||||
}
|
||||
|
||||
// eslint-disable-next-line @typescript-eslint/explicit-module-boundary-types
|
||||
async createFeature(feature: any): Promise<void> {
|
||||
this.featureToggles.push({
|
||||
project: feature.project || 'default',
|
||||
createdAt: new Date(),
|
||||
archived: false,
|
||||
...feature,
|
||||
});
|
||||
return Promise.resolve();
|
||||
}
|
||||
}
|
26
src/test/fixtures/fake-feature-toggle-store.ts
vendored
26
src/test/fixtures/fake-feature-toggle-store.ts
vendored
@ -1,7 +1,6 @@
|
||||
import {
|
||||
IFeatureToggleQuery,
|
||||
IFeatureToggleStore,
|
||||
IHasFeature,
|
||||
} from '../../lib/types/stores/feature-toggle-store';
|
||||
import { FeatureToggle, FeatureToggleDTO } from '../../lib/types/model';
|
||||
import NotFoundError from '../../lib/error/notfound-error';
|
||||
@ -9,7 +8,7 @@ import NotFoundError from '../../lib/error/notfound-error';
|
||||
export default class FakeFeatureToggleStore implements IFeatureToggleStore {
|
||||
features: FeatureToggle[] = [];
|
||||
|
||||
async archiveFeature(featureName: string): Promise<FeatureToggle> {
|
||||
async archive(featureName: string): Promise<FeatureToggle> {
|
||||
const feature = this.features.find((f) => f.name === featureName);
|
||||
if (feature) {
|
||||
feature.archived = true;
|
||||
@ -46,7 +45,7 @@ export default class FakeFeatureToggleStore implements IFeatureToggleStore {
|
||||
};
|
||||
}
|
||||
|
||||
async createFeature(
|
||||
async create(
|
||||
project: string,
|
||||
data: FeatureToggleDTO,
|
||||
): Promise<FeatureToggle> {
|
||||
@ -88,30 +87,19 @@ export default class FakeFeatureToggleStore implements IFeatureToggleStore {
|
||||
return this.get(name);
|
||||
}
|
||||
|
||||
async getFeatures(archived: boolean): Promise<FeatureToggle[]> {
|
||||
return this.features.filter((f) => f.archived === archived);
|
||||
}
|
||||
|
||||
async getFeaturesBy(
|
||||
query: Partial<IFeatureToggleQuery>,
|
||||
): Promise<FeatureToggle[]> {
|
||||
async getBy(query: Partial<IFeatureToggleQuery>): Promise<FeatureToggle[]> {
|
||||
return this.features.filter(this.getFilterQuery(query));
|
||||
}
|
||||
|
||||
async hasFeature(featureName: string): Promise<IHasFeature> {
|
||||
const { name, archived } = await this.get(featureName);
|
||||
return { name, archived };
|
||||
}
|
||||
|
||||
async reviveFeature(featureName: string): Promise<FeatureToggle> {
|
||||
async revive(featureName: string): Promise<FeatureToggle> {
|
||||
const revive = this.features.find((f) => f.name === featureName);
|
||||
if (revive) {
|
||||
revive.archived = false;
|
||||
}
|
||||
return this.updateFeature(revive.project, revive);
|
||||
return this.update(revive.project, revive);
|
||||
}
|
||||
|
||||
async updateFeature(
|
||||
async update(
|
||||
project: string,
|
||||
data: FeatureToggleDTO,
|
||||
): Promise<FeatureToggle> {
|
||||
@ -127,7 +115,7 @@ export default class FakeFeatureToggleStore implements IFeatureToggleStore {
|
||||
throw new NotFoundError('Could not find feature to update');
|
||||
}
|
||||
|
||||
async updateLastSeenForToggles(toggleNames: string[]): Promise<void> {
|
||||
async setLastSeen(toggleNames: string[]): Promise<void> {
|
||||
toggleNames.forEach((t) => {
|
||||
const toUpdate = this.features.find((f) => f.name === t);
|
||||
if (toUpdate) {
|
||||
|
11
src/test/fixtures/fake-project-store.ts
vendored
11
src/test/fixtures/fake-project-store.ts
vendored
@ -3,7 +3,7 @@ import {
|
||||
IProjectInsert,
|
||||
IProjectStore,
|
||||
} from '../../lib/types/stores/project-store';
|
||||
import { IFeatureOverview, IProject } from '../../lib/types/model';
|
||||
import { IProject } from '../../lib/types/model';
|
||||
import NotFoundError from '../../lib/error/notfound-error';
|
||||
|
||||
export default class FakeProjectStore implements IProjectStore {
|
||||
@ -89,15 +89,6 @@ export default class FakeProjectStore implements IProjectStore {
|
||||
return Promise.resolve(0);
|
||||
}
|
||||
|
||||
async getProjectOverview(
|
||||
// eslint-disable-next-line @typescript-eslint/no-unused-vars
|
||||
projectId: string,
|
||||
// eslint-disable-next-line @typescript-eslint/no-unused-vars
|
||||
archived: boolean,
|
||||
): Promise<IFeatureOverview[]> {
|
||||
return Promise.resolve([]);
|
||||
}
|
||||
|
||||
async hasProject(id: string): Promise<boolean> {
|
||||
return this.exists(id);
|
||||
}
|
||||
|
2
src/test/fixtures/store.ts
vendored
2
src/test/fixtures/store.ts
vendored
@ -22,6 +22,7 @@ import FakeFeatureEnvironmentStore from './fake-feature-environment-store';
|
||||
import FakeApiTokenStore from './fake-api-token-store';
|
||||
import FakeFeatureTypeStore from './fake-feature-type-store';
|
||||
import FakeResetTokenStore from './fake-reset-token-store';
|
||||
import FakeFeatureToggleClientStore from './fake-feature-toggle-client-store';
|
||||
|
||||
const createStores: () => IUnleashStores = () => {
|
||||
const db = {
|
||||
@ -36,6 +37,7 @@ const createStores: () => IUnleashStores = () => {
|
||||
clientMetricsStore: new FakeClientMetricsStore(),
|
||||
clientInstanceStore: new FakeClientInstanceStore(),
|
||||
featureToggleStore: new FakeFeatureToggleStore(),
|
||||
featureToggleClientStore: new FakeFeatureToggleClientStore(),
|
||||
tagStore: new FakeTagStore(),
|
||||
tagTypeStore: new FakeTagTypeStore(),
|
||||
eventStore: new FakeEventStore(),
|
||||
|
456
websitev2/docs/api/admin/feature-toggles-api-v2.md
Normal file
456
websitev2/docs/api/admin/feature-toggles-api-v2.md
Normal file
@ -0,0 +1,456 @@
|
||||
---
|
||||
id: feature-toggles-v2
|
||||
title: /api/admin/projects/:projectId
|
||||
---
|
||||
|
||||
> In order to access the admin API endpoints you need to identify yourself. You'll need to [create an ADMIN token](/user_guide/api-token) and add an Authorization header using the token.
|
||||
|
||||
|
||||
In this document we will guide you on how you can work with feature toggles and their configuration. Please remember the following details:
|
||||
|
||||
- All feature toggles exists _inside a project_.
|
||||
- A feature toggles exists _across all environments_.
|
||||
- A feature toggle can take different configuration, activation strategies, per environment.
|
||||
|
||||
TODO: Need to explain the following in a bit more details:
|
||||
- The _:global:: environment
|
||||
|
||||
|
||||
> We will in this guide use [HTTPie](https://httpie.io) commands to show examples on how to interact with the API.
|
||||
|
||||
### Get Project Overview {#fetching-project}
|
||||
|
||||
`http://localhost:4242/api/admin/projects/:projectId`
|
||||
|
||||
This endpoint will give you an general overview of a project. It will return essential details about a project, in addition it will return all feature toggles and high level environment details per feature toggle.
|
||||
|
||||
**Example Query**
|
||||
|
||||
`http GET http://localhost:4242/api/admin/projects/default Authorization:$KEY`
|
||||
|
||||
|
||||
**Example response:**
|
||||
|
||||
```json
|
||||
{
|
||||
"description": "Default project",
|
||||
"features": [
|
||||
{
|
||||
"createdAt": "2021-08-31T08:00:33.335Z",
|
||||
"environments": [
|
||||
{
|
||||
"displayName": "Development",
|
||||
"enabled": false,
|
||||
"name": "development"
|
||||
},
|
||||
{
|
||||
"displayName": "Production",
|
||||
"enabled": false,
|
||||
"name": "production"
|
||||
}
|
||||
],
|
||||
"lastSeenAt": null,
|
||||
"name": "demo",
|
||||
"stale": false,
|
||||
"type": "release"
|
||||
},
|
||||
{
|
||||
"createdAt": "2021-08-31T09:43:13.686Z",
|
||||
"environments": [
|
||||
{
|
||||
"displayName": "Development",
|
||||
"enabled": false,
|
||||
"name": "development"
|
||||
},
|
||||
{
|
||||
"displayName": "Production",
|
||||
"enabled": false,
|
||||
"name": "production"
|
||||
}
|
||||
],
|
||||
"lastSeenAt": null,
|
||||
"name": "demo.test",
|
||||
"stale": false,
|
||||
"type": "release"
|
||||
}
|
||||
],
|
||||
"health": 100,
|
||||
"members": 2,
|
||||
"name": "Default",
|
||||
"version": 1
|
||||
}
|
||||
```
|
||||
|
||||
From the results we can see that we have received two feature toggles, _demo_, _demo.test_, and other useful metadata about the project.
|
||||
|
||||
|
||||
### Get All Feature Toggles {#fetching-toggles}
|
||||
|
||||
`http://localhost:4242/api/admin/projects/:projectId/features`
|
||||
|
||||
This endpoint will return all feature toggles and high level environment details per feature toggle for a given _projectId_
|
||||
|
||||
**Example Query**
|
||||
|
||||
`http GET http://localhost:4242/api/admin/projects/default/features Authorization:$KEY`
|
||||
|
||||
|
||||
**Example response:**
|
||||
|
||||
```json
|
||||
{
|
||||
"features": [
|
||||
{
|
||||
"createdAt": "2021-08-31T08:00:33.335Z",
|
||||
"environments": [
|
||||
{
|
||||
"displayName": "Development",
|
||||
"enabled": false,
|
||||
"name": "development"
|
||||
},
|
||||
{
|
||||
"displayName": "Production",
|
||||
"enabled": false,
|
||||
"name": "production"
|
||||
}
|
||||
],
|
||||
"lastSeenAt": null,
|
||||
"name": "demo",
|
||||
"stale": false,
|
||||
"type": "release"
|
||||
},
|
||||
{
|
||||
"createdAt": "2021-08-31T09:43:13.686Z",
|
||||
"environments": [
|
||||
{
|
||||
"displayName": "Development",
|
||||
"enabled": false,
|
||||
"name": "development"
|
||||
},
|
||||
{
|
||||
"displayName": "Production",
|
||||
"enabled": false,
|
||||
"name": "production"
|
||||
}
|
||||
],
|
||||
"lastSeenAt": null,
|
||||
"name": "demo.test",
|
||||
"stale": false,
|
||||
"type": "release"
|
||||
}
|
||||
],
|
||||
"version": 1
|
||||
}
|
||||
```
|
||||
### Create Feature Toggle {#create-toggle}
|
||||
|
||||
`http://localhost:4242/api/admin/projects/:projectId/features`
|
||||
|
||||
This endpoint will accept HTTP POST request to create a new feature toggle for a given _projectId_
|
||||
|
||||
**Example Query**
|
||||
|
||||
```sh
|
||||
echo '{"name": "demo2", "description": "A new feature toggle"}' | http POST http://localhost:4242/api/admin/projects/default/features Authorization:$KEY`
|
||||
```
|
||||
|
||||
|
||||
**Example response:**
|
||||
|
||||
```json
|
||||
HTTP/1.1 201 Created
|
||||
Access-Control-Allow-Origin: *
|
||||
Connection: keep-alive
|
||||
Content-Length: 159
|
||||
Content-Type: application/json; charset=utf-8
|
||||
Date: Tue, 07 Sep 2021 20:16:02 GMT
|
||||
ETag: W/"9f-4btEokgk0N74zuBVKKxws0IBu4w"
|
||||
Keep-Alive: timeout=60
|
||||
Vary: Accept-Encoding
|
||||
|
||||
{
|
||||
"createdAt": "2021-09-07T20:16:02.614Z",
|
||||
"description": "A new feature toggle",
|
||||
"lastSeenAt": null,
|
||||
"name": "demo2",
|
||||
"project": "default",
|
||||
"stale": false,
|
||||
"type": "release",
|
||||
"variants": null
|
||||
}
|
||||
```
|
||||
|
||||
Possible Errors:
|
||||
|
||||
- _409 Conflict_ - A toggle with that name already exists
|
||||
|
||||
|
||||
|
||||
### Get Feature Toggle {#get-toggle}
|
||||
|
||||
`http://localhost:4242/api/admin/projects/:projectId/features/:featureName`
|
||||
|
||||
This endpoint will return the feature toggles with the defined name and _projectId_. We will also see the list of environments and all activation strategies configured per environment.
|
||||
|
||||
**Example Query**
|
||||
|
||||
```sh
|
||||
http GET http://localhost:4242/api/admin/projects/default/features/demo Authorization:$KEY`
|
||||
```
|
||||
|
||||
**Example response:**
|
||||
|
||||
```json
|
||||
{
|
||||
"archived": false,
|
||||
"createdAt": "2021-08-31T08:00:33.335Z",
|
||||
"description": null,
|
||||
"environments": [
|
||||
{
|
||||
"enabled": false,
|
||||
"name": "development",
|
||||
"strategies": [
|
||||
{
|
||||
"constraints": [],
|
||||
"id": "8eaa8abb-0e03-4dbb-a440-f3bf193917ad",
|
||||
"name": "default",
|
||||
"parameters": null
|
||||
}
|
||||
]
|
||||
},
|
||||
{
|
||||
"enabled": false,
|
||||
"name": "production",
|
||||
"strategies": []
|
||||
}
|
||||
],
|
||||
"lastSeenAt": null,
|
||||
"name": "demo",
|
||||
"project": "default",
|
||||
"stale": false,
|
||||
"type": "release",
|
||||
"variants": null
|
||||
}
|
||||
```
|
||||
|
||||
Possible Errors:
|
||||
|
||||
- _404 Not Found_ - Could not find feature toggle with the provided name.
|
||||
|
||||
### Update Feature Toggle {#update-toggle}
|
||||
|
||||
`http://localhost:4242/api/admin/projects/:projectId/features/:featureName`
|
||||
|
||||
This endpoint will accept HTTP PUT request to update the feature toggle metadata.
|
||||
|
||||
**Example Query**
|
||||
|
||||
```sh
|
||||
echo '{"name": "demo", "description": "An update feature toggle", "type": "kill-switch"}' | http PUT http://localhost:4242/api/admin/projects/default/features/demo Authorization:$KEY`
|
||||
```
|
||||
|
||||
|
||||
**Example response:**
|
||||
|
||||
```json
|
||||
{
|
||||
"createdAt": "2021-09-07T20:16:02.614Z",
|
||||
"description": "An update feature toggle",
|
||||
"lastSeenAt": null,
|
||||
"name": "demo",
|
||||
"project": "default",
|
||||
"stale": false,
|
||||
"type": "kill-switch",
|
||||
"variants": null
|
||||
}
|
||||
```
|
||||
|
||||
Some fields is not possible to change via this endpoint:
|
||||
|
||||
- name
|
||||
- project
|
||||
- createdAt
|
||||
- lastSeen
|
||||
|
||||
## Patch Feature Toggle {#patch-toggle}
|
||||
|
||||
`http://localhost:4242/api/admin/projects/:projectId/features/:featureName`
|
||||
|
||||
This endpoint will accept HTTP PATCH request to update the feature toggle metadata.
|
||||
|
||||
**Example Query**
|
||||
|
||||
```sh
|
||||
echo '[{"op": "replace", "path": "/description", "value": "patched desc"}]' | http PATCH http://localhost:4242/api/admin/projects/default/features/demo Authorization:$KEY`
|
||||
```
|
||||
|
||||
|
||||
**Example response:**
|
||||
|
||||
```json
|
||||
{
|
||||
"createdAt": "2021-09-07T20:16:02.614Z",
|
||||
"description": "patched desc",
|
||||
"lastSeenAt": null,
|
||||
"name": "demo",
|
||||
"project": "default",
|
||||
"stale": false,
|
||||
"type": "release",
|
||||
"variants": null
|
||||
}
|
||||
```
|
||||
|
||||
Some fields is not possible to change via this endpoint:
|
||||
|
||||
- name
|
||||
- project
|
||||
- createdAt
|
||||
- lastSeen
|
||||
|
||||
|
||||
### Archive Feature Toggle {#archive-toggle}
|
||||
|
||||
`http://localhost:4242/api/admin/projects/:projectId/features/:featureName`
|
||||
|
||||
This endpoint will accept HTTP PUT request to update the feature toggle metadata.
|
||||
|
||||
**Example Query**
|
||||
|
||||
```sh
|
||||
http DELETE http://localhost:4242/api/admin/projects/default/features/demo Authorization:$KEY`
|
||||
```
|
||||
|
||||
|
||||
**Example response:**
|
||||
|
||||
```sh
|
||||
HTTP/1.1 202 Accepted
|
||||
Access-Control-Allow-Origin: *
|
||||
Connection: keep-alive
|
||||
Date: Wed, 08 Sep 2021 20:09:21 GMT
|
||||
Keep-Alive: timeout=60
|
||||
Transfer-Encoding: chunked
|
||||
|
||||
```
|
||||
|
||||
|
||||
### Add strategy to Feature Toggle {#add-strategy}
|
||||
|
||||
`http://localhost:4242/api/admin/projects/:projectId/features/:featureName/environments/:environment/strategies`
|
||||
|
||||
This endpoint will allow you to add a new strategy to a feature toggle in a given environment.
|
||||
|
||||
**Example Query**
|
||||
|
||||
```sh
|
||||
echo '{"name": "flexibleRollout", "parameters": { "rollout": 20, "groupId": "demo", "stickiness": "default" }}' | \
|
||||
http POST http://localhost:4242/api/admin/projects/default/features/demo/environments/production/strategies Authorization:$KEY
|
||||
```
|
||||
|
||||
**Example response:**
|
||||
|
||||
```json
|
||||
{
|
||||
"constraints": [],
|
||||
"id": "77bbe972-ffce-49b2-94d9-326593e2228e",
|
||||
"name": "flexibleRollout",
|
||||
"parameters": {
|
||||
"groupId": "demo",
|
||||
"rollout": 20,
|
||||
"stickiness": "default"
|
||||
}
|
||||
}
|
||||
```
|
||||
|
||||
### Update strategy configuration {#update-strategy}
|
||||
|
||||
**Example Query**
|
||||
|
||||
```sh
|
||||
echo '{"name": "flexibleRollout", "parameters": { "rollout": 25, "groupId": "demo","stickiness": "default" }}' | \
|
||||
http PUT http://localhost:4242/api/admin/projects/default/features/demo/environments/production/strategies/77bbe972-ffce-49b2-94d9-326593e2228e Authorization:$KEY
|
||||
```
|
||||
|
||||
**Example response:**
|
||||
|
||||
```json
|
||||
{
|
||||
"constraints": [],
|
||||
"id": "77bbe972-ffce-49b2-94d9-326593e2228e",
|
||||
"name": "flexibleRollout",
|
||||
"parameters": {
|
||||
"groupId": "demo",
|
||||
"rollout": 20,
|
||||
"stickiness": "default"
|
||||
}
|
||||
}
|
||||
```
|
||||
|
||||
## Patch strategy configuration {#patch-strategy}
|
||||
|
||||
**Example Query**
|
||||
|
||||
```sh
|
||||
echo '[{"op": "replace", "path": "/parameters/rollout", "value": 50}]' | \
|
||||
http PATCH http://localhost:4242/api/admin/projects/default/features/demo/environments/production/strategies/ea5404e5-0c0d-488c-93b2-0a2200534827 Authorization:$KEY
|
||||
```
|
||||
|
||||
**Example response:**
|
||||
|
||||
```json
|
||||
{
|
||||
"constraints": [],
|
||||
"id": "ea5404e5-0c0d-488c-93b2-0a2200534827",
|
||||
"name": "flexibleRollout",
|
||||
"parameters": {
|
||||
"groupId": "demo",
|
||||
"rollout": 50,
|
||||
"stickiness": "default"
|
||||
}
|
||||
}
|
||||
```
|
||||
|
||||
|
||||
### Delete strategy from Feature Toggle {#delete-strategy}
|
||||
|
||||
**Example Query**
|
||||
|
||||
```sh
|
||||
http DELETE http://localhost:4242/api/admin/projects/default/features/demo/environments/production/strategies/77bbe972-ffce-49b2-94d9-326593e2228e Authorization:$KEY
|
||||
```
|
||||
|
||||
**Example response:**
|
||||
|
||||
```sh
|
||||
HTTP/1.1 200 OK
|
||||
Access-Control-Allow-Origin: *
|
||||
Connection: keep-alive
|
||||
Content-Type: application/json; charset=utf-8
|
||||
Date: Tue, 07 Sep 2021 20:47:55 GMT
|
||||
Keep-Alive: timeout=60
|
||||
Transfer-Encoding: chunked
|
||||
Vary: Accept-Encoding
|
||||
```
|
||||
|
||||
### Enable environment for Feature Toggle {#enable-env}
|
||||
|
||||
**Example Query**
|
||||
|
||||
```sh
|
||||
http POST http://localhost:4242/api/admin/projects/default/features/demo/environments/development/on Authorization:$KEY --json
|
||||
```
|
||||
|
||||
**Example response:**
|
||||
|
||||
```sh
|
||||
HTTP/1.1 200 OK
|
||||
Access-Control-Allow-Origin: *
|
||||
Connection: keep-alive
|
||||
Date: Tue, 07 Sep 2021 20:49:51 GMT
|
||||
Keep-Alive: timeout=60
|
||||
Transfer-Encoding: chunked
|
||||
```
|
||||
|
||||
Possible Errors:
|
||||
|
||||
- _409 Conflict_ - You can not enable the environment before it has strategies.
|
@ -2815,6 +2815,11 @@ fast-glob@^3.1.1:
|
||||
merge2 "^1.3.0"
|
||||
micromatch "^4.0.4"
|
||||
|
||||
fast-json-patch@^3.1.0:
|
||||
version "3.1.0"
|
||||
resolved "https://registry.yarnpkg.com/fast-json-patch/-/fast-json-patch-3.1.0.tgz#ec8cd9b9c4c564250ec8b9140ef7a55f70acaee6"
|
||||
integrity sha512-IhpytlsVTRndz0hU5t0/MGzS/etxLlfrpG5V5M9mVbuj9TrJLWaMfsox9REM5rkuGX0T+5qjpe8XA1o0gZ42nA==
|
||||
|
||||
fast-json-stable-stringify@2.x, fast-json-stable-stringify@^2.0.0:
|
||||
version "2.1.0"
|
||||
resolved "https://registry.npmjs.org/fast-json-stable-stringify/-/fast-json-stable-stringify-2.1.0.tgz"
|
||||
|
Loading…
Reference in New Issue
Block a user