mirror of
https://github.com/Unleash/unleash.git
synced 2024-11-01 19:07:38 +01:00
005e5b1d15
## About the changes When exporting v3, for variants backward compatibility, we need to find one featureEnvironment and fetch variants from there. In cases where the default environment is disabled (therefore does not get variants per environment when added), it can be still be selected for the export process. Therefore variants don't appear in the feature when they should be there. An e2e test that fails with the previous implementation was added to validate the behavior This comes from our support ticket 404
846 lines
28 KiB
TypeScript
846 lines
28 KiB
TypeScript
import { stateSchema } from './state-schema';
|
|
import {
|
|
DROP_ENVIRONMENTS,
|
|
DROP_FEATURE_TAGS,
|
|
DROP_FEATURES,
|
|
DROP_PROJECTS,
|
|
DROP_STRATEGIES,
|
|
DROP_TAG_TYPES,
|
|
DROP_TAGS,
|
|
ENVIRONMENT_IMPORT,
|
|
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 { IUnleashConfig } from '../types/option';
|
|
import {
|
|
FeatureToggle,
|
|
IEnvironment,
|
|
IFeatureEnvironment,
|
|
IFeatureStrategy,
|
|
IImportData,
|
|
IImportFile,
|
|
IProject,
|
|
ISegment,
|
|
IStrategyConfig,
|
|
ITag,
|
|
} from '../types/model';
|
|
import { Logger } from '../logger';
|
|
import {
|
|
IFeatureTag,
|
|
IFeatureTagStore,
|
|
} from '../types/stores/feature-tag-store';
|
|
import { IProjectStore } from '../types/stores/project-store';
|
|
import { ITagType, ITagTypeStore } from '../types/stores/tag-type-store';
|
|
import { ITagStore } from '../types/stores/tag-store';
|
|
import { IEventStore } from '../types/stores/event-store';
|
|
import { IStrategy, IStrategyStore } from '../types/stores/strategy-store';
|
|
import { IFeatureToggleStore } from '../types/stores/feature-toggle-store';
|
|
import { IFeatureStrategiesStore } from '../types/stores/feature-strategies-store';
|
|
import { IEnvironmentStore } from '../types/stores/environment-store';
|
|
import { IFeatureEnvironmentStore } from '../types/stores/feature-environment-store';
|
|
import { IUnleashStores } from '../types/stores';
|
|
import { DEFAULT_ENV } from '../util/constants';
|
|
import { GLOBAL_ENV } from '../types/environment';
|
|
import { ISegmentStore } from '../types/stores/segment-store';
|
|
import { PartialSome } from '../types/partial';
|
|
import { IFlagResolver } from 'lib/types';
|
|
|
|
export interface IBackupOption {
|
|
includeFeatureToggles: boolean;
|
|
includeStrategies: boolean;
|
|
includeProjects: boolean;
|
|
includeTags: boolean;
|
|
}
|
|
|
|
interface IExportIncludeOptions {
|
|
includeFeatureToggles?: boolean;
|
|
includeStrategies?: boolean;
|
|
includeProjects?: boolean;
|
|
includeTags?: boolean;
|
|
includeEnvironments?: boolean;
|
|
includeSegments?: boolean;
|
|
}
|
|
|
|
export default class StateService {
|
|
private logger: Logger;
|
|
|
|
private toggleStore: IFeatureToggleStore;
|
|
|
|
private featureStrategiesStore: IFeatureStrategiesStore;
|
|
|
|
private strategyStore: IStrategyStore;
|
|
|
|
private eventStore: IEventStore;
|
|
|
|
private tagStore: ITagStore;
|
|
|
|
private tagTypeStore: ITagTypeStore;
|
|
|
|
private projectStore: IProjectStore;
|
|
|
|
private featureEnvironmentStore: IFeatureEnvironmentStore;
|
|
|
|
private featureTagStore: IFeatureTagStore;
|
|
|
|
private environmentStore: IEnvironmentStore;
|
|
|
|
private segmentStore: ISegmentStore;
|
|
|
|
private flagResolver: IFlagResolver;
|
|
|
|
constructor(
|
|
stores: IUnleashStores,
|
|
{
|
|
getLogger,
|
|
flagResolver,
|
|
}: Pick<IUnleashConfig, 'getLogger' | 'flagResolver'>,
|
|
) {
|
|
this.eventStore = stores.eventStore;
|
|
this.toggleStore = stores.featureToggleStore;
|
|
this.strategyStore = stores.strategyStore;
|
|
this.tagStore = stores.tagStore;
|
|
this.featureStrategiesStore = stores.featureStrategiesStore;
|
|
this.featureEnvironmentStore = stores.featureEnvironmentStore;
|
|
this.tagTypeStore = stores.tagTypeStore;
|
|
this.projectStore = stores.projectStore;
|
|
this.featureTagStore = stores.featureTagStore;
|
|
this.environmentStore = stores.environmentStore;
|
|
this.segmentStore = stores.segmentStore;
|
|
this.flagResolver = flagResolver;
|
|
this.logger = getLogger('services/state-service.js');
|
|
}
|
|
|
|
async importFile({
|
|
file,
|
|
dropBeforeImport = false,
|
|
userName = 'import-user',
|
|
keepExisting = true,
|
|
}: IImportFile): Promise<void> {
|
|
return readFile(file)
|
|
.then((data) => parseFile(file, data))
|
|
.then((data) =>
|
|
this.import({
|
|
data,
|
|
userName,
|
|
dropBeforeImport,
|
|
keepExisting,
|
|
}),
|
|
);
|
|
}
|
|
|
|
// eslint-disable-next-line @typescript-eslint/explicit-module-boundary-types
|
|
replaceGlobalEnvWithDefaultEnv(data: any) {
|
|
data.environments?.forEach((e) => {
|
|
if (e.name === GLOBAL_ENV) {
|
|
e.name = DEFAULT_ENV;
|
|
}
|
|
});
|
|
data.featureEnvironments?.forEach((fe) => {
|
|
if (fe.environment === GLOBAL_ENV) {
|
|
// eslint-disable-next-line no-param-reassign
|
|
fe.environment = DEFAULT_ENV;
|
|
}
|
|
});
|
|
data.featureStrategies?.forEach((fs) => {
|
|
if (fs.environment === GLOBAL_ENV) {
|
|
// eslint-disable-next-line no-param-reassign
|
|
fs.environment = DEFAULT_ENV;
|
|
}
|
|
});
|
|
}
|
|
|
|
// eslint-disable-next-line @typescript-eslint/explicit-module-boundary-types
|
|
moveVariantsToFeatureEnvironments(data: any) {
|
|
data.featureEnvironments?.forEach((featureEnvironment) => {
|
|
let feature = data.features?.find(
|
|
(f) => f.name === featureEnvironment.featureName,
|
|
);
|
|
if (feature) {
|
|
featureEnvironment.variants = feature.variants || [];
|
|
}
|
|
});
|
|
}
|
|
|
|
async import({
|
|
data,
|
|
userName = 'importUser',
|
|
dropBeforeImport = false,
|
|
keepExisting = true,
|
|
}: IImportData): Promise<void> {
|
|
if (data.version === 2) {
|
|
this.replaceGlobalEnvWithDefaultEnv(data);
|
|
}
|
|
if (!data.version || data.version < 4) {
|
|
this.moveVariantsToFeatureEnvironments(data);
|
|
}
|
|
const importData = await stateSchema.validateAsync(data);
|
|
|
|
let importedEnvironments: IEnvironment[] = [];
|
|
if (importData.environments) {
|
|
importedEnvironments = await this.importEnvironments({
|
|
environments: data.environments,
|
|
userName,
|
|
dropBeforeImport,
|
|
keepExisting,
|
|
});
|
|
}
|
|
|
|
if (importData.projects) {
|
|
await this.importProjects({
|
|
projects: data.projects,
|
|
importedEnvironments,
|
|
userName,
|
|
dropBeforeImport,
|
|
keepExisting,
|
|
});
|
|
}
|
|
|
|
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,
|
|
featureEnvironments,
|
|
});
|
|
|
|
if (featureEnvironments) {
|
|
await this.importFeatureEnvironments({
|
|
featureEnvironments,
|
|
});
|
|
}
|
|
|
|
await this.importFeatureStrategies({
|
|
featureStrategies,
|
|
dropBeforeImport,
|
|
keepExisting,
|
|
});
|
|
}
|
|
|
|
if (importData.strategies) {
|
|
await this.importStrategies({
|
|
strategies: data.strategies,
|
|
userName,
|
|
dropBeforeImport,
|
|
keepExisting,
|
|
});
|
|
}
|
|
|
|
if (importData.tagTypes && importData.tags) {
|
|
await this.importTagData({
|
|
tagTypes: data.tagTypes,
|
|
tags: data.tags,
|
|
featureTags:
|
|
(data.featureTags || [])
|
|
.filter((t) =>
|
|
(data.features || []).some(
|
|
(f) => f.name === t.featureName,
|
|
),
|
|
)
|
|
.map((t) => ({
|
|
featureName: t.featureName,
|
|
tagValue: t.tagValue || t.value,
|
|
tagType: t.tagType || t.type,
|
|
})) || [],
|
|
userName,
|
|
dropBeforeImport,
|
|
keepExisting,
|
|
});
|
|
}
|
|
|
|
if (importData.segments) {
|
|
await this.importSegments(
|
|
data.segments,
|
|
userName,
|
|
dropBeforeImport,
|
|
);
|
|
}
|
|
|
|
if (importData.featureStrategySegments) {
|
|
await this.importFeatureStrategySegments(
|
|
data.featureStrategySegments,
|
|
);
|
|
}
|
|
}
|
|
|
|
// eslint-disable-next-line @typescript-eslint/explicit-module-boundary-types
|
|
enabledIn(feature: string, env) {
|
|
const config = {};
|
|
env.filter((e) => e.featureName === feature).forEach((e) => {
|
|
config[e.environment] = e.enabled || false;
|
|
});
|
|
return config;
|
|
}
|
|
|
|
// eslint-disable-next-line @typescript-eslint/explicit-module-boundary-types
|
|
async importFeatureEnvironments({ featureEnvironments }): Promise<void> {
|
|
await Promise.all(
|
|
featureEnvironments
|
|
.filter(async (env) => {
|
|
await this.environmentStore.exists(env.environment);
|
|
})
|
|
.map(async (featureEnvironment) =>
|
|
this.featureEnvironmentStore.addFeatureEnvironment(
|
|
featureEnvironment,
|
|
),
|
|
),
|
|
);
|
|
}
|
|
|
|
// eslint-disable-next-line @typescript-eslint/explicit-module-boundary-types
|
|
async importFeatureStrategies({
|
|
featureStrategies,
|
|
dropBeforeImport,
|
|
keepExisting,
|
|
}): Promise<void> {
|
|
const oldFeatureStrategies = dropBeforeImport
|
|
? []
|
|
: await this.featureStrategiesStore.getAll();
|
|
if (dropBeforeImport) {
|
|
this.logger.info(
|
|
'Dropping existing strategies for feature toggles',
|
|
);
|
|
await this.featureStrategiesStore.deleteAll();
|
|
}
|
|
const strategiesToImport = keepExisting
|
|
? featureStrategies.filter(
|
|
(s) => !oldFeatureStrategies.some((o) => o.id === s.id),
|
|
)
|
|
: featureStrategies;
|
|
await Promise.all(
|
|
strategiesToImport.map((featureStrategy) =>
|
|
this.featureStrategiesStore.createStrategyFeatureEnv(
|
|
featureStrategy,
|
|
),
|
|
),
|
|
);
|
|
}
|
|
|
|
// eslint-disable-next-line @typescript-eslint/explicit-module-boundary-types
|
|
async convertLegacyFeatures({
|
|
features,
|
|
}): Promise<{ features; featureStrategies; featureEnvironments }> {
|
|
const strategies = features.flatMap((f) =>
|
|
f.strategies.map((strategy: IStrategyConfig) => ({
|
|
featureName: f.name,
|
|
projectId: f.project,
|
|
constraints: strategy.constraints || [],
|
|
parameters: strategy.parameters || {},
|
|
environment: DEFAULT_ENV,
|
|
strategyName: strategy.name,
|
|
})),
|
|
);
|
|
const newFeatures = features;
|
|
const featureEnvironments = features.map((feature) => ({
|
|
featureName: feature.name,
|
|
environment: DEFAULT_ENV,
|
|
enabled: feature.enabled,
|
|
variants: feature.variants || [],
|
|
}));
|
|
return {
|
|
features: newFeatures,
|
|
featureStrategies: strategies,
|
|
featureEnvironments,
|
|
};
|
|
}
|
|
|
|
// eslint-disable-next-line @typescript-eslint/explicit-module-boundary-types
|
|
async importFeatures({
|
|
features,
|
|
userName,
|
|
dropBeforeImport,
|
|
keepExisting,
|
|
featureEnvironments,
|
|
}): Promise<void> {
|
|
this.logger.info(`Importing ${features.length} feature toggles`);
|
|
const oldToggles = dropBeforeImport
|
|
? []
|
|
: await this.toggleStore.getAll();
|
|
|
|
if (dropBeforeImport) {
|
|
this.logger.info('Dropping existing feature toggles');
|
|
await this.toggleStore.deleteAll();
|
|
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(async (feature) => {
|
|
await this.toggleStore.create(feature.project, feature);
|
|
await this.featureEnvironmentStore.connectFeatureToEnvironmentsForProject(
|
|
feature.name,
|
|
feature.project,
|
|
this.enabledIn(feature.name, featureEnvironments),
|
|
);
|
|
await this.eventStore.store({
|
|
type: FEATURE_IMPORT,
|
|
createdBy: userName,
|
|
data: feature,
|
|
});
|
|
}),
|
|
);
|
|
}
|
|
|
|
// eslint-disable-next-line @typescript-eslint/explicit-module-boundary-types
|
|
async importStrategies({
|
|
strategies,
|
|
userName,
|
|
dropBeforeImport,
|
|
keepExisting,
|
|
}): Promise<void> {
|
|
this.logger.info(`Importing ${strategies.length} strategies`);
|
|
const oldStrategies = dropBeforeImport
|
|
? []
|
|
: await this.strategyStore.getAll();
|
|
|
|
if (dropBeforeImport) {
|
|
this.logger.info('Dropping existing strategies');
|
|
await this.strategyStore.dropCustomStrategies();
|
|
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,
|
|
});
|
|
}),
|
|
),
|
|
);
|
|
}
|
|
|
|
// eslint-disable-next-line @typescript-eslint/explicit-module-boundary-types
|
|
async importEnvironments({
|
|
environments,
|
|
userName,
|
|
dropBeforeImport,
|
|
keepExisting,
|
|
}): Promise<IEnvironment[]> {
|
|
this.logger.info(`Import ${environments.length} projects`);
|
|
const oldEnvs = dropBeforeImport
|
|
? []
|
|
: await this.environmentStore.getAll();
|
|
if (dropBeforeImport) {
|
|
this.logger.info('Dropping existing environments');
|
|
await this.environmentStore.deleteAll();
|
|
await this.eventStore.store({
|
|
type: DROP_ENVIRONMENTS,
|
|
createdBy: userName,
|
|
data: { name: 'all-environments' },
|
|
});
|
|
}
|
|
const envsImport = environments.filter((env) =>
|
|
keepExisting ? !oldEnvs.some((old) => old.name === env.name) : true,
|
|
);
|
|
let importedEnvs = [];
|
|
if (envsImport.length > 0) {
|
|
importedEnvs = await this.environmentStore.importEnvironments(
|
|
envsImport,
|
|
);
|
|
const importedEnvironmentEvents = importedEnvs.map((env) => ({
|
|
type: ENVIRONMENT_IMPORT,
|
|
createdBy: userName,
|
|
data: env,
|
|
}));
|
|
await this.eventStore.batchStore(importedEnvironmentEvents);
|
|
}
|
|
return importedEnvs;
|
|
}
|
|
|
|
// eslint-disable-next-line @typescript-eslint/explicit-module-boundary-types
|
|
async importProjects({
|
|
projects,
|
|
importedEnvironments,
|
|
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.deleteAll();
|
|
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,
|
|
importedEnvironments,
|
|
);
|
|
const importedProjectEvents = importedProjects.map((project) => ({
|
|
type: PROJECT_IMPORT,
|
|
createdBy: userName,
|
|
data: project,
|
|
}));
|
|
await this.eventStore.batchStore(importedProjectEvents);
|
|
}
|
|
}
|
|
|
|
// eslint-disable-next-line @typescript-eslint/explicit-module-boundary-types
|
|
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.getAll();
|
|
if (dropBeforeImport) {
|
|
this.logger.info(
|
|
'Dropping all existing featuretags, tags and tagtypes',
|
|
);
|
|
await this.featureTagStore.deleteAll();
|
|
await this.tagStore.deleteAll();
|
|
await this.tagTypeStore.deleteAll();
|
|
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[] = [], // eslint-disable-line
|
|
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 importSegments(
|
|
segments: PartialSome<ISegment, 'id'>[],
|
|
userName: string,
|
|
dropBeforeImport: boolean,
|
|
): Promise<void> {
|
|
if (dropBeforeImport) {
|
|
await this.segmentStore.deleteAll();
|
|
}
|
|
|
|
await Promise.all(
|
|
segments.map((segment) =>
|
|
this.segmentStore.create(segment, { username: userName }),
|
|
),
|
|
);
|
|
}
|
|
|
|
async importFeatureStrategySegments(
|
|
featureStrategySegments: {
|
|
featureStrategyId: string;
|
|
segmentId: number;
|
|
}[],
|
|
): Promise<void> {
|
|
await Promise.all(
|
|
featureStrategySegments.map(({ featureStrategyId, segmentId }) =>
|
|
this.segmentStore.addToStrategy(segmentId, featureStrategyId),
|
|
),
|
|
);
|
|
}
|
|
|
|
async export(opts: IExportIncludeOptions): Promise<{
|
|
features: FeatureToggle[];
|
|
strategies: IStrategy[];
|
|
version: number;
|
|
projects: IProject[];
|
|
tagTypes: ITagType[];
|
|
tags: ITag[];
|
|
featureTags: IFeatureTag[];
|
|
featureStrategies: IFeatureStrategy[];
|
|
environments: IEnvironment[];
|
|
featureEnvironments: IFeatureEnvironment[];
|
|
}> {
|
|
if (this.flagResolver.isEnabled('variantsPerEnvironment')) {
|
|
return this.exportV4(opts);
|
|
}
|
|
// adapt v4 to v3. We need includeEnvironments set to true to filter the
|
|
// best environment from where we'll pick variants (cause now they are stored
|
|
// per environment despite being displayed as if they belong to the feature)
|
|
const v4 = await this.exportV4({ ...opts, includeEnvironments: true });
|
|
// undefined defaults to true
|
|
if (opts.includeFeatureToggles !== false) {
|
|
const enabledEnvironments = v4.environments.filter(
|
|
(env) => env.enabled !== false,
|
|
);
|
|
|
|
const featureAndEnvs = v4.featureEnvironments.map((fE) => {
|
|
const envDetails = enabledEnvironments.find(
|
|
(env) => fE.environment === env.name,
|
|
);
|
|
return { ...fE, ...envDetails, active: fE.enabled };
|
|
});
|
|
v4.features = v4.features.map((f) => {
|
|
const variants = featureAndEnvs
|
|
.sort((e1, e2) => {
|
|
if (e1.active !== e2.active) {
|
|
return e1.active ? -1 : 1;
|
|
}
|
|
if (
|
|
e1.type !== 'production' ||
|
|
e2.type !== 'production'
|
|
) {
|
|
if (e1.type === 'production') {
|
|
return -1;
|
|
} else if (e2.type === 'production') {
|
|
return 1;
|
|
}
|
|
}
|
|
return e1.sortOrder - e2.sortOrder;
|
|
})
|
|
.find((fe) => fe.featureName === f.name)?.variants;
|
|
return { ...f, variants };
|
|
});
|
|
v4.featureEnvironments = v4.featureEnvironments.map((fe) => {
|
|
delete fe.variants;
|
|
return fe;
|
|
});
|
|
}
|
|
// only if explicitly set to false (i.e. undefined defaults to true)
|
|
if (opts.includeEnvironments === false) {
|
|
delete v4.environments;
|
|
} else {
|
|
if (v4.environments.length === 0) {
|
|
throw Error('Unable to export without enabled environments');
|
|
}
|
|
}
|
|
v4.version = 3;
|
|
return v4;
|
|
}
|
|
|
|
async exportV4({
|
|
includeFeatureToggles = true,
|
|
includeStrategies = true,
|
|
includeProjects = true,
|
|
includeTags = true,
|
|
includeEnvironments = true,
|
|
includeSegments = true,
|
|
}: IExportIncludeOptions): 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.getAll({ archived: false })
|
|
: 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 && includeFeatureToggles
|
|
? this.featureTagStore.getAll()
|
|
: Promise.resolve([]),
|
|
includeFeatureToggles
|
|
? this.featureStrategiesStore.getAll()
|
|
: Promise.resolve([]),
|
|
includeEnvironments
|
|
? this.environmentStore.getAll()
|
|
: Promise.resolve([]),
|
|
includeFeatureToggles
|
|
? this.featureEnvironmentStore.getAll()
|
|
: Promise.resolve([]),
|
|
includeSegments ? this.segmentStore.getAll() : Promise.resolve([]),
|
|
includeSegments
|
|
? this.segmentStore.getAllFeatureStrategySegments()
|
|
: Promise.resolve([]),
|
|
]).then(
|
|
([
|
|
features,
|
|
strategies,
|
|
projects,
|
|
tagTypes,
|
|
tags,
|
|
featureTags,
|
|
featureStrategies,
|
|
environments,
|
|
featureEnvironments,
|
|
segments,
|
|
featureStrategySegments,
|
|
]) => ({
|
|
version: 4,
|
|
features,
|
|
strategies,
|
|
projects,
|
|
tagTypes,
|
|
tags,
|
|
featureTags,
|
|
featureStrategies: featureStrategies.filter((fS) =>
|
|
features.some((f) => fS.featureName === f.name),
|
|
),
|
|
environments,
|
|
featureEnvironments: featureEnvironments.filter((fE) =>
|
|
features.some((f) => fE.featureName === f.name),
|
|
),
|
|
segments,
|
|
featureStrategySegments,
|
|
}),
|
|
);
|
|
}
|
|
}
|
|
|
|
module.exports = StateService;
|