1
0
mirror of https://github.com/Unleash/unleash.git synced 2024-10-18 20:09:08 +02:00
unleash.unleash/src/lib/services/state-service.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

565 lines
18 KiB
TypeScript

import { stateSchema } from './state-schema';
import {
DROP_FEATURE_TAGS,
DROP_FEATURES,
DROP_PROJECTS,
DROP_STRATEGIES,
DROP_TAG_TYPES,
DROP_TAGS,
FEATURE_IMPORT,
FEATURE_TAG_IMPORT,
PROJECT_IMPORT,
STRATEGY_IMPORT,
TAG_IMPORT,
TAG_TYPE_IMPORT,
} from '../types/events';
import { filterEqual, filterExisting, parseFile, readFile } from './state-util';
import FeatureToggleStore from '../db/feature-toggle-store';
import TagTypeStore, { ITagType } from '../db/tag-type-store';
import FeatureTagStore, { IFeatureTag } from '../db/feature-tag-store';
import ProjectStore, { IProject } from '../db/project-store';
import TagStore from '../db/tag-store';
import StrategyStore, { IStrategy } from '../db/strategy-store';
import { Logger } from '../logger';
import { IUnleashStores } from '../types/stores';
import { IUnleashConfig } from '../types/option';
import EventStore from '../db/event-store';
import {
FeatureToggle,
IEnvironment,
IFeatureEnvironment,
ITag,
} from '../types/model';
import FeatureStrategiesStore, {
IFeatureStrategy,
} from '../db/feature-strategy-store';
import EnvironmentStore from '../db/environment-store';
import { GLOBAL_ENV } from '../types/environment';
export interface IBackupOption {
includeFeatureToggles: boolean;
includeStrategies: boolean;
includeProjects: boolean;
includeTags: boolean;
}
interface IImportOption {
keepExising: boolean;
dropBeforeImport: boolean;
userName: string;
}
export default class StateService {
private logger: Logger;
private toggleStore: FeatureToggleStore;
private featureStrategiesStore: FeatureStrategiesStore;
private strategyStore: StrategyStore;
private eventStore: EventStore;
private tagStore: TagStore;
private tagTypeStore: TagTypeStore;
private projectStore: ProjectStore;
private featureTagStore: FeatureTagStore;
private environmentStore: EnvironmentStore;
constructor(
stores: IUnleashStores,
{ getLogger }: Pick<IUnleashConfig, 'getLogger'>,
) {
this.eventStore = stores.eventStore;
this.toggleStore = stores.featureToggleStore;
this.strategyStore = stores.strategyStore;
this.tagStore = stores.tagStore;
this.featureStrategiesStore = stores.featureStrategiesStore;
this.tagTypeStore = stores.tagTypeStore;
this.projectStore = stores.projectStore;
this.featureTagStore = stores.featureTagStore;
this.environmentStore = stores.environmentStore;
this.logger = getLogger('services/state-service.js');
}
async importFile({
file,
dropBeforeImport,
userName,
keepExisting,
}): Promise<void> {
return readFile(file)
.then(data => parseFile(file, data))
.then(data =>
this.import({ data, userName, dropBeforeImport, keepExisting }),
);
}
async import({
data,
userName,
dropBeforeImport,
keepExisting,
}): Promise<void> {
const importData = await stateSchema.validateAsync(data);
if (importData.features) {
let projectData;
if (!importData.version || importData.version === 1) {
projectData = await this.convertLegacyFeatures(importData);
} else {
projectData = importData;
}
const {
features,
featureStrategies,
featureEnvironments,
} = projectData;
await this.importFeatures({
features,
userName,
dropBeforeImport,
keepExisting,
});
await this.importFeatureEnvironments({
featureEnvironments,
});
await this.importFeatureStrategies({
featureStrategies,
userName,
dropBeforeImport,
keepExisting,
});
}
if (importData.strategies) {
await this.importStrategies({
strategies: data.strategies,
userName,
dropBeforeImport,
keepExisting,
});
}
if (importData.projects) {
await this.importProjects({
projects: data.projects,
userName,
dropBeforeImport,
keepExisting,
});
}
if (importData.tagTypes && importData.tags) {
await this.importTagData({
tagTypes: data.tagTypes,
tags: data.tags,
featureTags:
data.featureTags.map(t => ({
featureName: t.featureName,
tagValue: t.tagValue || t.value,
tagType: t.tagType || t.type,
})) || [],
userName,
dropBeforeImport,
keepExisting,
});
}
}
async importFeatureEnvironments({ featureEnvironments }): Promise<void> {
await Promise.all(
featureEnvironments.map(env =>
this.featureStrategiesStore.connectEnvironmentAndFeature(
env.featureName,
env.environment,
env.enabled,
),
),
);
}
async importFeatureStrategies({
featureStrategies,
userName,
dropBeforeImport,
keepExisting,
}): Promise<void> {
const oldFeatureStrategies = dropBeforeImport
? []
: await this.featureStrategiesStore.getAllFeatureStrategies();
if (dropBeforeImport) {
this.logger.info(
'Dropping existing strategies for feature toggles',
);
await this.featureStrategiesStore.deleteFeatureStrategies();
}
const strategiesToImport = keepExisting
? featureStrategies.filter(
s => !oldFeatureStrategies.some(o => o.id === s.id),
)
: featureStrategies;
await Promise.all(
strategiesToImport.map(featureStrategy =>
this.featureStrategiesStore.createStrategyConfig(
featureStrategy,
),
),
);
}
async convertLegacyFeatures({
features,
}): Promise<{ features; featureStrategies; featureEnvironments }> {
const strategies = features.flatMap(f =>
f.strategies.map(strategy => ({
featureName: f.name,
projectName: f.project,
constraints: strategy.constraints || [],
parameters: strategy.parameters || {},
environment: GLOBAL_ENV,
strategyName: strategy.name,
})),
);
const newFeatures = features;
const featureEnvironments = features.map(feature => ({
featureName: feature.name,
environment: GLOBAL_ENV,
enabled: feature.enabled,
}));
return {
features: newFeatures,
featureStrategies: strategies,
featureEnvironments,
};
}
async importFeatures({
features,
userName,
dropBeforeImport,
keepExisting,
}): Promise<void> {
this.logger.info(`Importing ${features.length} feature toggles`);
const oldToggles = dropBeforeImport
? []
: await this.toggleStore.getFeatures();
if (dropBeforeImport) {
this.logger.info('Dropping existing feature toggles');
await this.toggleStore.dropFeatures();
await this.eventStore.store({
type: DROP_FEATURES,
createdBy: userName,
data: { name: 'all-features' },
});
}
await Promise.all(
features
.filter(filterExisting(keepExisting, oldToggles))
.filter(filterEqual(oldToggles))
.map(feature =>
this.toggleStore
.createFeature(feature.project, feature)
.then(() => {
this.eventStore.store({
type: FEATURE_IMPORT,
createdBy: userName,
data: feature,
});
}),
),
);
}
async importStrategies({
strategies,
userName,
dropBeforeImport,
keepExisting,
}): Promise<void> {
this.logger.info(`Importing ${strategies.length} strategies`);
const oldStrategies = dropBeforeImport
? []
: await this.strategyStore.getStrategies();
if (dropBeforeImport) {
this.logger.info('Dropping existing strategies');
await this.strategyStore.dropStrategies();
await this.eventStore.store({
type: DROP_STRATEGIES,
createdBy: userName,
data: { name: 'all-strategies' },
});
}
await Promise.all(
strategies
.filter(filterExisting(keepExisting, oldStrategies))
.filter(filterEqual(oldStrategies))
.map(strategy =>
this.strategyStore.importStrategy(strategy).then(() => {
this.eventStore.store({
type: STRATEGY_IMPORT,
createdBy: userName,
data: strategy,
});
}),
),
);
}
async importProjects({
projects,
userName,
dropBeforeImport,
keepExisting,
}): Promise<void> {
this.logger.info(`Import ${projects.length} projects`);
const oldProjects = dropBeforeImport
? []
: await this.projectStore.getAll();
if (dropBeforeImport) {
this.logger.info('Dropping existing projects');
await this.projectStore.dropProjects();
await this.eventStore.store({
type: DROP_PROJECTS,
createdBy: userName,
data: { name: 'all-projects' },
});
}
const projectsToImport = projects.filter(project =>
keepExisting
? !oldProjects.some(old => old.id === project.id)
: true,
);
if (projectsToImport.length > 0) {
const importedProjects = await this.projectStore.importProjects(
projectsToImport,
);
const importedProjectEvents = importedProjects.map(project => ({
type: PROJECT_IMPORT,
createdBy: userName,
data: project,
}));
await this.eventStore.batchStore(importedProjectEvents);
}
}
async importTagData({
tagTypes,
tags,
featureTags,
userName,
dropBeforeImport,
keepExisting,
}): Promise<void> {
this.logger.info(
`Importing ${tagTypes.length} tagtypes, ${tags.length} tags and ${featureTags.length} feature tags`,
);
const oldTagTypes = dropBeforeImport
? []
: await this.tagTypeStore.getAll();
const oldTags = dropBeforeImport ? [] : await this.tagStore.getAll();
const oldFeatureTags = dropBeforeImport
? []
: await this.featureTagStore.getAllFeatureTags();
if (dropBeforeImport) {
this.logger.info(
'Dropping all existing featuretags, tags and tagtypes',
);
await this.featureTagStore.dropFeatureTags();
await this.tagStore.dropTags();
await this.tagTypeStore.dropTagTypes();
await this.eventStore.batchStore([
{
type: DROP_FEATURE_TAGS,
createdBy: userName,
data: { name: 'all-feature-tags' },
},
{
type: DROP_TAGS,
createdBy: userName,
data: { name: 'all-tags' },
},
{
type: DROP_TAG_TYPES,
createdBy: userName,
data: { name: 'all-tag-types' },
},
]);
}
await this.importTagTypes(
tagTypes,
keepExisting,
oldTagTypes,
userName,
);
await this.importTags(tags, keepExisting, oldTags, userName);
await this.importFeatureTags(
featureTags,
keepExisting,
oldFeatureTags,
userName,
);
}
compareFeatureTags: (old: IFeatureTag, tag: IFeatureTag) => boolean = (
old,
tag,
) =>
old.featureName === tag.featureName &&
old.tagValue === tag.tagValue &&
old.tagType === tag.tagType;
async importFeatureTags(
featureTags: IFeatureTag[],
keepExisting: boolean,
oldFeatureTags: IFeatureTag[],
userName: string,
): Promise<void> {
const featureTagsToInsert = featureTags.filter(tag =>
keepExisting
? !oldFeatureTags.some(old => this.compareFeatureTags(old, tag))
: true,
);
if (featureTagsToInsert.length > 0) {
const importedFeatureTags = await this.featureTagStore.importFeatureTags(
featureTagsToInsert,
);
const importedFeatureTagEvents = importedFeatureTags.map(tag => ({
type: FEATURE_TAG_IMPORT,
createdBy: userName,
data: tag,
}));
await this.eventStore.batchStore(importedFeatureTagEvents);
}
}
compareTags = (old: ITag, tag: ITag): boolean =>
old.type === tag.type && old.value === tag.value;
async importTags(
tags: ITag[],
keepExisting: boolean,
oldTags: ITag[],
userName: string,
): Promise<void> {
const tagsToInsert = tags.filter(tag =>
keepExisting
? !oldTags.some(old => this.compareTags(old, tag))
: true,
);
if (tagsToInsert.length > 0) {
const importedTags = await this.tagStore.bulkImport(tagsToInsert);
const importedTagEvents = importedTags.map(tag => ({
type: TAG_IMPORT,
createdBy: userName,
data: tag,
}));
await this.eventStore.batchStore(importedTagEvents);
}
}
async importTagTypes(
tagTypes: ITagType[],
keepExisting: boolean,
oldTagTypes: ITagType[] = [],
userName: string,
): Promise<void> {
const tagTypesToInsert = tagTypes.filter(tagType =>
keepExisting
? !oldTagTypes.some(t => t.name === tagType.name)
: true,
);
if (tagTypesToInsert.length > 0) {
const importedTagTypes = await this.tagTypeStore.bulkImport(
tagTypesToInsert,
);
const importedTagTypeEvents = importedTagTypes.map(tagType => ({
type: TAG_TYPE_IMPORT,
createdBy: userName,
data: tagType,
}));
await this.eventStore.batchStore(importedTagTypeEvents);
}
}
async export({
includeFeatureToggles = true,
includeStrategies = true,
includeProjects = true,
includeTags = true,
includeEnvironments = true,
}): Promise<{
features: FeatureToggle[];
strategies: IStrategy[];
version: number;
projects: IProject[];
tagTypes: ITagType[];
tags: ITag[];
featureTags: IFeatureTag[];
featureStrategies: IFeatureStrategy[];
environments: IEnvironment[];
featureEnvironments: IFeatureEnvironment[];
}> {
return Promise.all([
includeFeatureToggles
? this.toggleStore.getFeatures()
: Promise.resolve([]),
includeStrategies
? this.strategyStore.getEditableStrategies()
: Promise.resolve([]),
this.projectStore && includeProjects
? this.projectStore.getAll()
: Promise.resolve([]),
includeTags ? this.tagTypeStore.getAll() : Promise.resolve([]),
includeTags ? this.tagStore.getAll() : Promise.resolve([]),
includeTags
? this.featureTagStore.getAllFeatureTags()
: Promise.resolve([]),
includeFeatureToggles
? this.featureStrategiesStore.getAll()
: Promise.resolve([]),
includeEnvironments
? this.environmentStore.getAll()
: Promise.resolve([]),
includeFeatureToggles
? this.featureStrategiesStore.getAllFeatureEnvironments()
: Promise.resolve([]),
]).then(
([
features,
strategies,
projects,
tagTypes,
tags,
featureTags,
featureStrategies,
environments,
featureEnvironments,
]) => ({
version: 2,
features,
strategies,
projects,
tagTypes,
tags,
featureTags,
featureStrategies,
environments,
featureEnvironments,
}),
);
}
}
module.exports = StateService;