Skip to content

Commit

Permalink
feat: import feature strategies (#2885)
Browse files Browse the repository at this point in the history
  • Loading branch information
kwasniew committed Jan 12, 2023
1 parent b12962e commit 5569101
Show file tree
Hide file tree
Showing 2 changed files with 190 additions and 29 deletions.
65 changes: 54 additions & 11 deletions src/lib/services/export-import-service.ts
@@ -1,5 +1,10 @@
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 { IFeatureTagStore } from '../types/stores/feature-tag-store';
import { IProjectStore } from '../types/stores/project-store';
Expand All @@ -26,15 +31,16 @@ export interface IExportQuery {

export interface IImportDTO {
data: IExportData;
project?: string;
environment?: string;
project: string;
environment: string;
}

export interface IExportData {
features: FeatureToggle[];
tags?: ITag[];
contextFields?: IContextFieldDto[];
featureStrategies: IFeatureStrategy[];
featureEnvironments: IFeatureEnvironment[];
}

export default class ExportImportService {
Expand Down Expand Up @@ -93,25 +99,62 @@ export default class ExportImportService {
}

async export(query: ExportQuerySchema): Promise<IExportData> {
const features = await this.toggleStore.getAllByNames(query.features);
const featureStrategies =
await this.featureStrategiesStore.getAllByFeatures(
query.features,
query.environment,
);
return { features, featureStrategies };
const [features, featureEnvironments, featureStrategies] =
await Promise.all([
this.toggleStore.getAllByNames(query.features),
(
await this.featureEnvironmentStore.getAll({
environment: query.environment,
})
).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> {
await Promise.all(
dto.data.features.map((feature) =>
this.featureToggleService.createFeatureToggle(
dto.project || feature.project,
dto.project,
feature,
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,
),
),
);
}
}

Expand Down
154 changes: 136 additions & 18 deletions src/test/e2e/api/admin/export-import.e2e.test.ts
Expand Up @@ -5,14 +5,26 @@ import {
import dbInit, { ITestDb } from '../../helpers/database-init';
import getLogger from '../../../fixtures/no-logger';
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 { IImportDTO } from '../../../../lib/services/export-import-service';

let app: IUnleashTest;
let db: ITestDb;
let eventStore: IEventStore;
let environmentStore: IEnvironmentStore;
let projectStore: IProjectStore;
let toggleStore: IFeatureToggleStore;

const defaultStrategy = {
const defaultStrategy: IStrategyConfig = {
name: 'default',
parameters: {},
constraints: [],
Expand All @@ -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 () => {
db = await dbInit('export_import_api_serial', getLogger);
app = await setupAppWithCustomConfig(db.stores, {
Expand All @@ -48,22 +78,25 @@ beforeAll(async () => {
},
});
eventStore = db.stores.eventStore;
environmentStore = db.stores.environmentStore;
projectStore = db.stores.projectStore;
toggleStore = db.stores.featureToggleStore;
});

beforeEach(async () => {
await eventStore.deleteAll();
await toggleStore.deleteAll();
await projectStore.deleteAll();
await environmentStore.deleteAll();
});

afterAll(async () => {
await app.destroy();
await db.destroy();
});

afterEach(async () => {
await db.stores.featureToggleStore.deleteAll();
});

test('exports features', async () => {
await createProject('default', 'default');
const strategy = {
name: 'default',
parameters: { rollout: '100', stickiness: 'default' },
Expand Down Expand Up @@ -106,10 +139,19 @@ test('exports features', async () => {
},
],
featureStrategies: [resultStrategy],
featureEnvironments: [
{
enabled: false,
environment: 'default',
featureName: 'first_feature',
variants: [],
},
],
});
});

test('returns all features, when no feature was defined', async () => {
await createProject('default', 'default');
await createToggle({
name: 'first_feature',
description: 'the #1 feature',
Expand All @@ -130,24 +172,100 @@ test('returns all features, when no feature was defined', async () => {
expect(body.features).toHaveLength(2);
});

test('import features', async () => {
const feature: FeatureToggle = { project: 'ignore', name: 'first_feature' };
test('import features to existing project and environment', async () => {
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
.post('/api/admin/features-batch/import')
.send({
data: { features: [feature] },
project: 'default',
environment: 'custom_environment',
})
.send(importPayload)
.set('Content-Type', 'application/json')
.expect(201);

const { body } = await app.request
.get('/api/admin/features/first_feature')
const { body: importedFeature } = await app.request
.get(`/api/admin/features/${feature}`)
.expect(200);

expect(body).toMatchObject({
expect(importedFeature).toMatchObject({
name: 'first_feature',
project: 'default',
project: project,
variants,
});

const { body: importedFeatureEnvironment } = await app.request
.get(
`/api/admin/projects/${project}/features/${feature}/environments/${environment}`,
)
.expect(200);

expect(importedFeatureEnvironment).toMatchObject({
name: feature,
environment,
enabled: true,
strategies: [
{
featureName: feature,
projectId: project,
environment: environment,
parameters: {},
constraints: [],
sortOrder: 9999,
name: 'default',
},
],
});
});

0 comments on commit 5569101

Please sign in to comment.