From afdcd45042e583b87f4d38fe964441b2193cc9c2 Mon Sep 17 00:00:00 2001 From: Mateusz Kwasniewski Date: Wed, 11 Jan 2023 15:19:16 +0100 Subject: [PATCH] feat: first skeleton of the batch import (#2868) First skeleton of batch import: * injecting feature toggle service because I want to reuse logic and not just the store --- src/lib/routes/admin-api/export-import.ts | 19 +++++++++++++ src/lib/services/export-import-service.ts | 28 ++++++++++++++++++- src/lib/services/index.ts | 4 ++- .../e2e/api/admin/export-import.e2e.test.ts | 24 +++++++++++++++- 4 files changed, 72 insertions(+), 3 deletions(-) diff --git a/src/lib/routes/admin-api/export-import.ts b/src/lib/routes/admin-api/export-import.ts index cfc088b93e..cde5d40ef5 100644 --- a/src/lib/routes/admin-api/export-import.ts +++ b/src/lib/routes/admin-api/export-import.ts @@ -7,8 +7,10 @@ import { Logger } from '../../logger'; import { OpenApiService } from '../../services/openapi-service'; import ExportImportService, { IExportQuery, + IImportDTO, } from 'lib/services/export-import-service'; import { InvalidOperationError } from '../../error'; +import { IAuthRequest } from '../unleash-types'; class ExportImportController extends Controller { private logger: Logger; @@ -45,6 +47,12 @@ class ExportImportController extends Controller { // }), // ], }); + this.route({ + method: 'post', + path: '/import', + permission: NONE, + handler: this.importData, + }); } async export( @@ -65,5 +73,16 @@ class ExportImportController extends Controller { ); } } + + async importData( + req: IAuthRequest, + res: Response, + ): Promise { + const dto = req.body; + const user = req.user; + await this.exportImportService.import(dto, user); + + res.status(201).end(); + } } export default ExportImportController; diff --git a/src/lib/services/export-import-service.ts b/src/lib/services/export-import-service.ts index 04e2c1ced2..d11f57413f 100644 --- a/src/lib/services/export-import-service.ts +++ b/src/lib/services/export-import-service.ts @@ -13,14 +13,22 @@ import { IEnvironmentStore } from '../types/stores/environment-store'; import { IFeatureEnvironmentStore } from '../types/stores/feature-environment-store'; import { IUnleashStores } from '../types/stores'; import { ISegmentStore } from '../types/stores/segment-store'; -import { IFlagResolver } from 'lib/types'; +import { IFlagResolver, IUnleashServices } from 'lib/types'; import { IContextFieldDto } from '../types/stores/context-field-store'; +import FeatureToggleService from './feature-toggle-service'; +import User from 'lib/types/user'; export interface IExportQuery { features: string[]; environment: string; } +export interface IImportDTO { + data: IExportData; + project?: string; + environment?: string; +} + export interface IExportData { features: FeatureToggle[]; tags?: ITag[]; @@ -54,12 +62,17 @@ export default class ExportImportService { private flagResolver: IFlagResolver; + private featureToggleService: FeatureToggleService; + constructor( stores: IUnleashStores, { getLogger, flagResolver, }: Pick, + { + featureToggleService, + }: Pick, ) { this.eventStore = stores.eventStore; this.toggleStore = stores.featureToggleStore; @@ -73,6 +86,7 @@ export default class ExportImportService { this.environmentStore = stores.environmentStore; this.segmentStore = stores.segmentStore; this.flagResolver = flagResolver; + this.featureToggleService = featureToggleService; this.logger = getLogger('services/state-service.js'); } @@ -82,6 +96,18 @@ export default class ExportImportService { ).filter((toggle) => query.features.includes(toggle.name)); return { features: features }; } + + async import(dto: IImportDTO, user: User): Promise { + await Promise.all( + dto.data.features.map((feature) => + this.featureToggleService.createFeatureToggle( + dto.project || feature.project, + feature, + user.name, + ), + ), + ); + } } module.exports = ExportImportService; diff --git a/src/lib/services/index.ts b/src/lib/services/index.ts index de5c7eb663..5f29b5254b 100644 --- a/src/lib/services/index.ts +++ b/src/lib/services/index.ts @@ -61,7 +61,6 @@ export const createServices = ( const featureTypeService = new FeatureTypeService(stores, config); const resetTokenService = new ResetTokenService(stores, config); const stateService = new StateService(stores, config); - const exportImportService = new ExportImportService(stores, config); const strategyService = new StrategyService(stores, config); const tagService = new TagService(stores, config); const tagTypeService = new TagTypeService(stores, config); @@ -85,6 +84,9 @@ export const createServices = ( segmentService, accessService, ); + const exportImportService = new ExportImportService(stores, config, { + featureToggleService: featureToggleServiceV2, + }); const environmentService = new EnvironmentService(stores, config); const featureTagService = new FeatureTagService(stores, config); const favoritesService = new FavoritesService(stores, config); diff --git a/src/test/e2e/api/admin/export-import.e2e.test.ts b/src/test/e2e/api/admin/export-import.e2e.test.ts index dcff784ca3..ceefdf1a58 100644 --- a/src/test/e2e/api/admin/export-import.e2e.test.ts +++ b/src/test/e2e/api/admin/export-import.e2e.test.ts @@ -5,7 +5,7 @@ import { import dbInit, { ITestDb } from '../../helpers/database-init'; import getLogger from '../../../fixtures/no-logger'; import { IEventStore } from 'lib/types/stores/event-store'; -import { FeatureToggleDTO, IStrategyConfig } from 'lib/types'; +import { FeatureToggle, FeatureToggleDTO, IStrategyConfig } from 'lib/types'; import { DEFAULT_ENV } from '../../../../lib/util'; let app: IUnleashTest; @@ -79,3 +79,25 @@ test('exports features', async () => { ], }); }); + +test('import features', async () => { + const feature: FeatureToggle = { project: 'ignore', name: 'first_feature' }; + await app.request + .post('/api/admin/features-batch/import') + .send({ + data: { features: [feature] }, + project: 'default', + environment: 'custom_environment', + }) + .set('Content-Type', 'application/json') + .expect(201); + + const { body } = await app.request + .get('/api/admin/features/first_feature') + .expect(200); + + expect(body).toMatchObject({ + name: 'first_feature', + project: 'default', + }); +});