mirror of
https://github.com/Unleash/unleash.git
synced 2025-02-23 00:22:19 +01:00
feat: import feature strategies (#2885)
This commit is contained in:
parent
b12962e7d2
commit
5569101f30
@ -1,5 +1,10 @@
|
|||||||
import { IUnleashConfig } from '../types/option';
|
import { IUnleashConfig } from '../types/option';
|
||||||
import { FeatureToggle, IFeatureStrategy, ITag } from '../types/model';
|
import {
|
||||||
|
FeatureToggle,
|
||||||
|
IFeatureEnvironment,
|
||||||
|
IFeatureStrategy,
|
||||||
|
ITag,
|
||||||
|
} from '../types/model';
|
||||||
import { Logger } from '../logger';
|
import { Logger } from '../logger';
|
||||||
import { IFeatureTagStore } from '../types/stores/feature-tag-store';
|
import { IFeatureTagStore } from '../types/stores/feature-tag-store';
|
||||||
import { IProjectStore } from '../types/stores/project-store';
|
import { IProjectStore } from '../types/stores/project-store';
|
||||||
@ -26,8 +31,8 @@ export interface IExportQuery {
|
|||||||
|
|
||||||
export interface IImportDTO {
|
export interface IImportDTO {
|
||||||
data: IExportData;
|
data: IExportData;
|
||||||
project?: string;
|
project: string;
|
||||||
environment?: string;
|
environment: string;
|
||||||
}
|
}
|
||||||
|
|
||||||
export interface IExportData {
|
export interface IExportData {
|
||||||
@ -35,6 +40,7 @@ export interface IExportData {
|
|||||||
tags?: ITag[];
|
tags?: ITag[];
|
||||||
contextFields?: IContextFieldDto[];
|
contextFields?: IContextFieldDto[];
|
||||||
featureStrategies: IFeatureStrategy[];
|
featureStrategies: IFeatureStrategy[];
|
||||||
|
featureEnvironments: IFeatureEnvironment[];
|
||||||
}
|
}
|
||||||
|
|
||||||
export default class ExportImportService {
|
export default class ExportImportService {
|
||||||
@ -93,25 +99,62 @@ export default class ExportImportService {
|
|||||||
}
|
}
|
||||||
|
|
||||||
async export(query: ExportQuerySchema): Promise<IExportData> {
|
async export(query: ExportQuerySchema): Promise<IExportData> {
|
||||||
const features = await this.toggleStore.getAllByNames(query.features);
|
const [features, featureEnvironments, featureStrategies] =
|
||||||
const featureStrategies =
|
await Promise.all([
|
||||||
await this.featureStrategiesStore.getAllByFeatures(
|
this.toggleStore.getAllByNames(query.features),
|
||||||
query.features,
|
(
|
||||||
query.environment,
|
await this.featureEnvironmentStore.getAll({
|
||||||
);
|
environment: query.environment,
|
||||||
return { features, featureStrategies };
|
})
|
||||||
|
).filter((item) => query.features.includes(item.featureName)),
|
||||||
|
this.featureStrategiesStore.getAllByFeatures(
|
||||||
|
query.features,
|
||||||
|
query.environment,
|
||||||
|
),
|
||||||
|
]);
|
||||||
|
return { features, featureStrategies, featureEnvironments };
|
||||||
}
|
}
|
||||||
|
|
||||||
async import(dto: IImportDTO, user: User): Promise<void> {
|
async import(dto: IImportDTO, user: User): Promise<void> {
|
||||||
await Promise.all(
|
await Promise.all(
|
||||||
dto.data.features.map((feature) =>
|
dto.data.features.map((feature) =>
|
||||||
this.featureToggleService.createFeatureToggle(
|
this.featureToggleService.createFeatureToggle(
|
||||||
dto.project || feature.project,
|
dto.project,
|
||||||
feature,
|
feature,
|
||||||
user.name,
|
user.name,
|
||||||
),
|
),
|
||||||
),
|
),
|
||||||
);
|
);
|
||||||
|
await Promise.all(
|
||||||
|
dto.data.featureStrategies.map((featureStrategy) =>
|
||||||
|
this.featureToggleService.unprotectedCreateStrategy(
|
||||||
|
{
|
||||||
|
name: featureStrategy.strategyName,
|
||||||
|
constraints: featureStrategy.constraints,
|
||||||
|
parameters: featureStrategy.parameters,
|
||||||
|
segments: featureStrategy.segments,
|
||||||
|
sortOrder: featureStrategy.sortOrder,
|
||||||
|
},
|
||||||
|
{
|
||||||
|
featureName: featureStrategy.featureName,
|
||||||
|
environment: dto.environment,
|
||||||
|
projectId: dto.project,
|
||||||
|
},
|
||||||
|
user.name,
|
||||||
|
),
|
||||||
|
),
|
||||||
|
);
|
||||||
|
await Promise.all(
|
||||||
|
dto.data.featureEnvironments.map((featureEnvironment) =>
|
||||||
|
this.featureToggleService.unprotectedUpdateEnabled(
|
||||||
|
dto.project,
|
||||||
|
featureEnvironment.featureName,
|
||||||
|
dto.environment,
|
||||||
|
featureEnvironment.enabled,
|
||||||
|
user.name,
|
||||||
|
),
|
||||||
|
),
|
||||||
|
);
|
||||||
}
|
}
|
||||||
}
|
}
|
||||||
|
|
||||||
|
@ -5,14 +5,26 @@ import {
|
|||||||
import dbInit, { ITestDb } from '../../helpers/database-init';
|
import dbInit, { ITestDb } from '../../helpers/database-init';
|
||||||
import getLogger from '../../../fixtures/no-logger';
|
import getLogger from '../../../fixtures/no-logger';
|
||||||
import { IEventStore } from 'lib/types/stores/event-store';
|
import { IEventStore } from 'lib/types/stores/event-store';
|
||||||
import { FeatureToggle, FeatureToggleDTO, IStrategyConfig } from 'lib/types';
|
import {
|
||||||
|
FeatureToggle,
|
||||||
|
FeatureToggleDTO,
|
||||||
|
IEnvironmentStore,
|
||||||
|
IFeatureStrategy,
|
||||||
|
IFeatureToggleStore,
|
||||||
|
IProjectStore,
|
||||||
|
IStrategyConfig,
|
||||||
|
} from 'lib/types';
|
||||||
import { DEFAULT_ENV } from '../../../../lib/util';
|
import { DEFAULT_ENV } from '../../../../lib/util';
|
||||||
|
import { IImportDTO } from '../../../../lib/services/export-import-service';
|
||||||
|
|
||||||
let app: IUnleashTest;
|
let app: IUnleashTest;
|
||||||
let db: ITestDb;
|
let db: ITestDb;
|
||||||
let eventStore: IEventStore;
|
let eventStore: IEventStore;
|
||||||
|
let environmentStore: IEnvironmentStore;
|
||||||
|
let projectStore: IProjectStore;
|
||||||
|
let toggleStore: IFeatureToggleStore;
|
||||||
|
|
||||||
const defaultStrategy = {
|
const defaultStrategy: IStrategyConfig = {
|
||||||
name: 'default',
|
name: 'default',
|
||||||
parameters: {},
|
parameters: {},
|
||||||
constraints: [],
|
constraints: [],
|
||||||
@ -38,6 +50,24 @@ const createToggle = async (
|
|||||||
}
|
}
|
||||||
};
|
};
|
||||||
|
|
||||||
|
const createProject = async (project: string, environment: string) => {
|
||||||
|
await db.stores.environmentStore.create({
|
||||||
|
name: environment,
|
||||||
|
type: 'production',
|
||||||
|
});
|
||||||
|
await db.stores.projectStore.create({
|
||||||
|
name: project,
|
||||||
|
description: '',
|
||||||
|
id: project,
|
||||||
|
});
|
||||||
|
await app.request
|
||||||
|
.post(`/api/admin/projects/${project}/environments`)
|
||||||
|
.send({
|
||||||
|
environment,
|
||||||
|
})
|
||||||
|
.expect(200);
|
||||||
|
};
|
||||||
|
|
||||||
beforeAll(async () => {
|
beforeAll(async () => {
|
||||||
db = await dbInit('export_import_api_serial', getLogger);
|
db = await dbInit('export_import_api_serial', getLogger);
|
||||||
app = await setupAppWithCustomConfig(db.stores, {
|
app = await setupAppWithCustomConfig(db.stores, {
|
||||||
@ -48,10 +78,16 @@ beforeAll(async () => {
|
|||||||
},
|
},
|
||||||
});
|
});
|
||||||
eventStore = db.stores.eventStore;
|
eventStore = db.stores.eventStore;
|
||||||
|
environmentStore = db.stores.environmentStore;
|
||||||
|
projectStore = db.stores.projectStore;
|
||||||
|
toggleStore = db.stores.featureToggleStore;
|
||||||
});
|
});
|
||||||
|
|
||||||
beforeEach(async () => {
|
beforeEach(async () => {
|
||||||
await eventStore.deleteAll();
|
await eventStore.deleteAll();
|
||||||
|
await toggleStore.deleteAll();
|
||||||
|
await projectStore.deleteAll();
|
||||||
|
await environmentStore.deleteAll();
|
||||||
});
|
});
|
||||||
|
|
||||||
afterAll(async () => {
|
afterAll(async () => {
|
||||||
@ -59,11 +95,8 @@ afterAll(async () => {
|
|||||||
await db.destroy();
|
await db.destroy();
|
||||||
});
|
});
|
||||||
|
|
||||||
afterEach(async () => {
|
|
||||||
await db.stores.featureToggleStore.deleteAll();
|
|
||||||
});
|
|
||||||
|
|
||||||
test('exports features', async () => {
|
test('exports features', async () => {
|
||||||
|
await createProject('default', 'default');
|
||||||
const strategy = {
|
const strategy = {
|
||||||
name: 'default',
|
name: 'default',
|
||||||
parameters: { rollout: '100', stickiness: 'default' },
|
parameters: { rollout: '100', stickiness: 'default' },
|
||||||
@ -106,10 +139,19 @@ test('exports features', async () => {
|
|||||||
},
|
},
|
||||||
],
|
],
|
||||||
featureStrategies: [resultStrategy],
|
featureStrategies: [resultStrategy],
|
||||||
|
featureEnvironments: [
|
||||||
|
{
|
||||||
|
enabled: false,
|
||||||
|
environment: 'default',
|
||||||
|
featureName: 'first_feature',
|
||||||
|
variants: [],
|
||||||
|
},
|
||||||
|
],
|
||||||
});
|
});
|
||||||
});
|
});
|
||||||
|
|
||||||
test('returns all features, when no feature was defined', async () => {
|
test('returns all features, when no feature was defined', async () => {
|
||||||
|
await createProject('default', 'default');
|
||||||
await createToggle({
|
await createToggle({
|
||||||
name: 'first_feature',
|
name: 'first_feature',
|
||||||
description: 'the #1 feature',
|
description: 'the #1 feature',
|
||||||
@ -130,24 +172,100 @@ test('returns all features, when no feature was defined', async () => {
|
|||||||
expect(body.features).toHaveLength(2);
|
expect(body.features).toHaveLength(2);
|
||||||
});
|
});
|
||||||
|
|
||||||
test('import features', async () => {
|
test('import features to existing project and environment', async () => {
|
||||||
const feature: FeatureToggle = { project: 'ignore', name: 'first_feature' };
|
const feature = 'first_feature';
|
||||||
|
const project = 'new_project';
|
||||||
|
const environment = 'staging';
|
||||||
|
const variants = [
|
||||||
|
{
|
||||||
|
name: 'variantA',
|
||||||
|
weight: 500,
|
||||||
|
payload: {
|
||||||
|
type: 'string',
|
||||||
|
value: 'payloadA',
|
||||||
|
},
|
||||||
|
overrides: [],
|
||||||
|
stickiness: 'default',
|
||||||
|
weightType: 'variable',
|
||||||
|
},
|
||||||
|
{
|
||||||
|
name: 'variantB',
|
||||||
|
weight: 500,
|
||||||
|
payload: {
|
||||||
|
type: 'string',
|
||||||
|
value: 'payloadB',
|
||||||
|
},
|
||||||
|
overrides: [],
|
||||||
|
stickiness: 'default',
|
||||||
|
weightType: 'variable',
|
||||||
|
},
|
||||||
|
];
|
||||||
|
const exportedFeature: FeatureToggle = {
|
||||||
|
project: 'old_project',
|
||||||
|
name: 'first_feature',
|
||||||
|
variants,
|
||||||
|
};
|
||||||
|
const exportedStrategy: IFeatureStrategy = {
|
||||||
|
id: '798cb25a-2abd-47bd-8a95-40ec13472309',
|
||||||
|
featureName: feature,
|
||||||
|
projectId: 'old_project',
|
||||||
|
environment: 'old_environment',
|
||||||
|
strategyName: 'default',
|
||||||
|
parameters: {},
|
||||||
|
constraints: [],
|
||||||
|
};
|
||||||
|
const importPayload: IImportDTO = {
|
||||||
|
data: {
|
||||||
|
features: [exportedFeature],
|
||||||
|
featureStrategies: [exportedStrategy],
|
||||||
|
featureEnvironments: [
|
||||||
|
{
|
||||||
|
enabled: true,
|
||||||
|
featureName: 'first_feature',
|
||||||
|
environment: 'irrelevant',
|
||||||
|
},
|
||||||
|
],
|
||||||
|
},
|
||||||
|
project: project,
|
||||||
|
environment: environment,
|
||||||
|
};
|
||||||
|
await createProject(project, environment);
|
||||||
|
|
||||||
await app.request
|
await app.request
|
||||||
.post('/api/admin/features-batch/import')
|
.post('/api/admin/features-batch/import')
|
||||||
.send({
|
.send(importPayload)
|
||||||
data: { features: [feature] },
|
|
||||||
project: 'default',
|
|
||||||
environment: 'custom_environment',
|
|
||||||
})
|
|
||||||
.set('Content-Type', 'application/json')
|
.set('Content-Type', 'application/json')
|
||||||
.expect(201);
|
.expect(201);
|
||||||
|
|
||||||
const { body } = await app.request
|
const { body: importedFeature } = await app.request
|
||||||
.get('/api/admin/features/first_feature')
|
.get(`/api/admin/features/${feature}`)
|
||||||
|
.expect(200);
|
||||||
|
expect(importedFeature).toMatchObject({
|
||||||
|
name: 'first_feature',
|
||||||
|
project: project,
|
||||||
|
variants,
|
||||||
|
});
|
||||||
|
|
||||||
|
const { body: importedFeatureEnvironment } = await app.request
|
||||||
|
.get(
|
||||||
|
`/api/admin/projects/${project}/features/${feature}/environments/${environment}`,
|
||||||
|
)
|
||||||
.expect(200);
|
.expect(200);
|
||||||
|
|
||||||
expect(body).toMatchObject({
|
expect(importedFeatureEnvironment).toMatchObject({
|
||||||
name: 'first_feature',
|
name: feature,
|
||||||
project: 'default',
|
environment,
|
||||||
|
enabled: true,
|
||||||
|
strategies: [
|
||||||
|
{
|
||||||
|
featureName: feature,
|
||||||
|
projectId: project,
|
||||||
|
environment: environment,
|
||||||
|
parameters: {},
|
||||||
|
constraints: [],
|
||||||
|
sortOrder: 9999,
|
||||||
|
name: 'default',
|
||||||
|
},
|
||||||
|
],
|
||||||
});
|
});
|
||||||
});
|
});
|
||||||
|
Loading…
Reference in New Issue
Block a user