1
0
mirror of https://github.com/Unleash/unleash.git synced 2024-10-18 20:09:08 +02:00
unleash.unleash/src/lib/services/feature-toggle-service-v2.ts
Ivar Conradi Østhus 815a75a5b4
Wip/environments (#880)
Adds environment support

This PR adds environments as a first-class concept in Unleash.

It necessitated a full rewrite on how we connect feature <-> strategy, as well as a rethink on which levels environments makes sense.

This enables PUTs on strategy configurations for a feature, since all strategies now have ids.

This also updates export/import format. The importer handles both formats, but export is no longer possible in version 1 of the export format, only in version 2, with strategy configurations for a feature as a separate object.

Co-authored-by: Christopher Kolstad <chriswk@getunleash.ai>
Co-authored-by: Fredrik Oseberg <fredrik.no@gmail.com>
Co-authored-by: Ivar Conradi Østhus <ivarconr@gmail.com>
2021-07-07 10:46:50 +02:00

513 lines
16 KiB
TypeScript

/* eslint-disable prettier/prettier */
import { IUnleashConfig } from '../types/option';
import { IUnleashStores } from '../types/stores';
import { Logger } from '../logger';
import FeatureStrategiesStore, {
FeatureConfigurationClient,
IFeatureStrategy,
} from '../db/feature-strategy-store';
import FeatureToggleStore from '../db/feature-toggle-store';
import {
FeatureToggle,
FeatureToggleDTO,
FeatureToggleWithEnvironment,
IFeatureEnvironmentInfo, IFeatureOverview,
IFeatureToggleQuery, IProjectHealthReport,
IProjectOverview,
IStrategyConfig
} from '../types/model';
import ProjectStore from '../db/project-store';
import BadDataError from '../error/bad-data-error';
import { FOREIGN_KEY_VIOLATION } from '../error/db-error';
import NameExistsError from '../error/name-exists-error';
import { featureMetadataSchema, nameSchema } from '../schema/feature-schema';
import EventStore from '../db/event-store';
import {
FEATURE_ARCHIVED,
FEATURE_CREATED, FEATURE_DELETED, FEATURE_REVIVED,
FEATURE_STALE_OFF,
FEATURE_STALE_ON,
FEATURE_UPDATED
} from '../types/events';
import FeatureTagStore from '../db/feature-tag-store';
import EnvironmentStore from '../db/environment-store';
import { GLOBAL_ENV } from '../types/environment';
import NotFoundError from '../error/notfound-error';
import FeatureTypeStore from '../db/feature-type-store';
import { MILLISECONDS_IN_DAY } from '../util/constants';
class FeatureToggleServiceV2 {
private logger: Logger;
private featureStrategiesStore: FeatureStrategiesStore;
private featureToggleStore: FeatureToggleStore;
private featureTagStore: FeatureTagStore;
private projectStore: ProjectStore;
private environmentStore: EnvironmentStore;
private eventStore: EventStore;
private featureTypeStore: FeatureTypeStore;
constructor(
{
featureStrategiesStore,
featureToggleStore,
projectStore,
eventStore,
featureTagStore,
environmentStore,
featureTypeStore,
}: Pick<
IUnleashStores,
| 'featureStrategiesStore'
| 'featureToggleStore'
| 'projectStore'
| 'eventStore'
| 'featureTagStore'
| 'environmentStore'
| 'featureTypeStore'
>,
{ getLogger }: Pick<IUnleashConfig, 'getLogger'>,
) {
this.logger = getLogger('services/feature-toggle-service-v2.ts');
this.featureStrategiesStore = featureStrategiesStore;
this.featureToggleStore = featureToggleStore;
this.featureTagStore = featureTagStore;
this.projectStore = projectStore;
this.eventStore = eventStore;
this.environmentStore = environmentStore;
this.featureTypeStore = featureTypeStore;
}
async createStrategy(
strategyConfig: Omit<IStrategyConfig, 'id'>,
projectName: string,
featureName: string,
environment: string = GLOBAL_ENV,
): Promise<IStrategyConfig> {
try {
const newFeatureStrategy = await this.featureStrategiesStore.createStrategyConfig(
{
strategyName: strategyConfig.name,
constraints: strategyConfig.constraints,
parameters: strategyConfig.parameters,
projectName,
featureName,
environment,
},
);
return {
id: newFeatureStrategy.id,
name: newFeatureStrategy.strategyName,
constraints: newFeatureStrategy.constraints,
parameters: newFeatureStrategy.parameters,
};
} catch (e) {
if (e.code === FOREIGN_KEY_VIOLATION) {
throw new BadDataError(
'You have not added the current environment to the project',
);
}
throw e;
}
}
/**
* PUT /api/admin/projects/:projectName/features/:featureName/strategies/:strategyId ?
* {
*
* }
* @param id
* @param updates
*/
async updateStrategy(
id: string,
updates: Partial<IFeatureStrategy>,
): Promise<IFeatureStrategy> {
const exists = await this.featureStrategiesStore.hasStrategy(id);
if (exists) {
return this.featureStrategiesStore.updateStrategy(id, updates);
}
throw new NotFoundError(`Could not find strategy with id ${id}`);
}
async getStrategiesForEnvironment(
projectName: string,
featureName: string,
environment: string = GLOBAL_ENV,
): Promise<IStrategyConfig[]> {
const hasEnv = await this.featureStrategiesStore.featureHasEnvironment(
environment,
featureName,
);
if (hasEnv) {
const featureStrategies = await this.featureStrategiesStore.getStrategiesForFeature(
projectName,
featureName,
environment,
);
return featureStrategies.map(strat => ({
id: strat.id,
name: strat.strategyName,
constraints: strat.constraints,
parameters: strat.parameters,
}));
}
throw new NotFoundError(
`Feature ${featureName} does not have environment ${environment}`,
);
}
/**
* GET /api/admin/projects/:projectName/features/:featureName
* @param featureName
* @param archived - return archived or non archived toggles
*/
async getFeature(
featureName: string,
archived: boolean = false
): Promise<FeatureToggleWithEnvironment> {
return this.featureStrategiesStore.getFeatureToggleAdmin(featureName, archived);
}
async getClientFeatures(
query?: IFeatureToggleQuery,
archived: boolean = false,
): Promise<FeatureConfigurationClient[]> {
return this.featureStrategiesStore.getFeatures(query, archived, false);
}
/**
* 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
* @param includeStrategyId - Include id for strategies
* @returns
*/
async getFeatureToggles(
query?: IFeatureToggleQuery,
archived: boolean = false
): Promise<FeatureToggle[]> {
return this.featureStrategiesStore.getFeatures(query, archived, true);
}
async getFeatureToggle(
featureName: string,
): Promise<FeatureToggleWithEnvironment> {
return this.featureStrategiesStore.getFeatureToggleAdmin(featureName);
}
async createFeatureToggle(
projectId: string,
value: FeatureToggleDTO,
userName: string,
): Promise<FeatureToggle> {
this.logger.info(`${userName} creates feature toggle ${value.name}`);
await this.validateName(value.name);
await this.projectStore.hasProject(projectId);
const featureData = await featureMetadataSchema.validateAsync(value);
const createdToggle = await this.featureToggleStore.createFeature(
projectId,
featureData,
);
await this.environmentStore.connectFeatureToEnvironmentsForProject(
featureData.name,
projectId,
);
await this.eventStore.store({
type: FEATURE_CREATED,
createdBy: userName,
data: featureData,
});
return createdToggle;
}
/**
* @deprecated
* @param featureName
* @returns
*/
async getProjectId(featureName: string): Promise<string> {
return this.featureToggleStore.getProjectId(featureName);
}
async updateFeatureToggle(
projectId: string,
updatedFeature: FeatureToggleDTO,
userName: string,
): Promise<FeatureToggle> {
this.logger.info(
`${userName} updates feature toggle ${updatedFeature.name}`,
);
await this.featureToggleStore.hasFeature(updatedFeature.name);
const featureToggle = await this.featureToggleStore.updateFeature(
projectId,
updatedFeature,
);
const tags =
(await this.featureTagStore.getAllTagsForFeature(
updatedFeature.name,
)) || [];
await this.eventStore.store({
type: FEATURE_UPDATED,
createdBy: userName,
data: featureToggle,
tags,
});
return featureToggle;
}
async getFeatureCountForProject(projectId: string): Promise<number> {
return this.featureToggleStore.count({
archived: false,
project: projectId,
});
}
async removeAllStrategiesForEnv(
toggleName: string,
environment: string = GLOBAL_ENV,
): Promise<void> {
await this.featureStrategiesStore.removeAllStrategiesForEnv(
toggleName,
environment,
);
}
async getStrategy(strategyId: string): Promise<IStrategyConfig> {
const strategy = await this.featureStrategiesStore.getStrategyById(
strategyId,
);
return {
id: strategy.id,
name: strategy.strategyName,
constraints: strategy.constraints || [],
parameters: strategy.parameters,
};
}
async getEnvironmentInfo(
project: string,
environment: string,
featureName: string,
): Promise<IFeatureEnvironmentInfo> {
const envMetadata = await this.featureStrategiesStore.getEnvironmentMetaData(
environment,
featureName,
);
const strategies = await this.featureStrategiesStore.getStrategiesForFeature(
project,
featureName,
environment,
);
return {
name: featureName,
environment,
enabled: envMetadata.enabled,
strategies,
};
}
async deleteEnvironment(
projectId: string,
environment: string,
): Promise<void> {
await this.featureStrategiesStore.deleteConfigurationsForProjectAndEnvironment(
projectId,
environment,
);
await this.projectStore.deleteEnvironment(projectId, environment);
}
/** Validations */
async validateName(name: string): Promise<string> {
await nameSchema.validateAsync({ name });
await this.validateUniqueFeatureName(name);
return name;
}
async validateUniqueFeatureName(name: string): Promise<void> {
let msg;
try {
const feature = await this.featureToggleStore.hasFeature(name);
msg = feature.archived
? 'An archived toggle with that name already exists'
: 'A toggle with that name already exists';
} catch (error) {
return;
}
throw new NameExistsError(msg);
}
async hasFeature(name: string): Promise<boolean> {
return this.featureToggleStore.exists(name);
}
async updateStale(
featureName: string,
isStale: boolean,
userName: string,
): Promise<any> {
const feature = await this.featureToggleStore.getFeatureMetadata(
featureName,
);
feature.stale = isStale;
await this.featureToggleStore.updateFeature(feature.project, feature);
const tags =
(await this.featureTagStore.getAllTagsForFeature(featureName)) ||
[];
await this.eventStore.store({
type: isStale ? FEATURE_STALE_ON : FEATURE_STALE_OFF,
createdBy: userName,
data: feature,
tags,
});
return feature;
}
async archiveToggle(name: string, userName: string): Promise<void> {
await this.featureToggleStore.hasFeature(name);
await this.featureToggleStore.archiveFeature(name);
const tags =
(await this.featureTagStore.getAllTagsForFeature(name)) || [];
await this.eventStore.store({
type: FEATURE_ARCHIVED,
createdBy: userName,
data: { name },
tags,
});
}
async updateEnabled(
featureName: string,
environment: string,
enabled: boolean,
userName: string,
): Promise<FeatureToggle> {
const hasEnvironment = await this.featureStrategiesStore.featureHasEnvironment(
environment,
featureName,
);
if (hasEnvironment) {
const newEnabled = await this.featureStrategiesStore.toggleEnvironmentEnabledStatus(
environment,
featureName,
enabled,
);
const feature = await this.featureToggleStore.getFeatureMetadata(
featureName,
);
const tags =
(await this.featureTagStore.getAllTagsForFeature(
featureName,
)) || [];
await this.eventStore.store({
type: FEATURE_UPDATED,
createdBy: userName,
data: { ...feature, enabled: newEnabled },
tags,
});
return feature;
}
throw new NotFoundError(
`Could not find environment ${environment} for feature: ${featureName}`,
);
}
// @deprecated
async toggle(
featureName: string,
environment: string,
userName: string,
): Promise<FeatureToggle> {
await this.featureToggleStore.hasFeature(featureName);
const isEnabled = await this.featureStrategiesStore.isEnvironmentEnabled(
featureName,
environment,
);
return this.updateEnabled(
featureName,
environment,
!isEnabled,
userName,
);
}
// @deprecated
// eslint-disable-next-line @typescript-eslint/explicit-module-boundary-types
async updateField(
featureName: string,
field: string,
value: any,
userName: string,
): Promise<any> {
const feature = await this.featureToggleStore.getFeatureMetadata(
featureName,
);
feature[field] = value;
await this.featureToggleStore.updateFeature(feature.project, feature);
const tags =
(await this.featureTagStore.getAllTagsForFeature(featureName)) ||
[];
await this.eventStore.store({
type: FEATURE_UPDATED,
createdBy: userName,
data: feature,
tags,
});
return feature;
}
async getArchivedFeatures(): Promise<FeatureToggle[]> {
return this.getFeatureToggles({}, true);
}
async deleteFeature(featureName: string, userName: string): Promise<void> {
await this.featureToggleStore.deleteFeature(featureName);
await this.eventStore.store({
type: FEATURE_DELETED,
createdBy: userName,
data: {
featureName
},
})
}
async reviveToggle(featureName: string, userName: string): Promise<void> {
const data = await this.featureToggleStore.reviveFeature(featureName);
const tags = await this.featureTagStore.getAllTagsForFeature(featureName);
await this.eventStore.store({
type: FEATURE_REVIVED,
createdBy: userName,
data,
tags
});
}
async getMetadataForAllFeatures(archived: boolean): Promise<FeatureToggle[]> {
return this.featureToggleStore.getFeatures(archived);
}
}
module.exports = FeatureToggleServiceV2;
export default FeatureToggleServiceV2;