From f3f3a59e5ecaf09a08363ecb933567d8f2f53ece Mon Sep 17 00:00:00 2001 From: sjaanus Date: Tue, 10 Jan 2023 15:59:02 +0200 Subject: [PATCH] Import export (#2865) --- src/lib/routes/admin-api/export-import.ts | 59 +++++++++++++ src/lib/routes/admin-api/index.ts | 5 ++ src/lib/services/export-import-service.ts | 87 +++++++++++++++++++ src/lib/services/index.ts | 4 + src/lib/types/services.ts | 2 + .../e2e/api/admin/export-import.e2e.test.ts | 72 +++++++++++++++ 6 files changed, 229 insertions(+) create mode 100644 src/lib/routes/admin-api/export-import.ts create mode 100644 src/lib/services/export-import-service.ts create mode 100644 src/test/e2e/api/admin/export-import.e2e.test.ts diff --git a/src/lib/routes/admin-api/export-import.ts b/src/lib/routes/admin-api/export-import.ts new file mode 100644 index 0000000000..d2b36b38f1 --- /dev/null +++ b/src/lib/routes/admin-api/export-import.ts @@ -0,0 +1,59 @@ +import { Request, Response } from 'express'; +import Controller from '../controller'; +import { NONE } from '../../types/permissions'; +import { IUnleashConfig } from '../../types/option'; +import { IUnleashServices } from '../../types/services'; +import { Logger } from '../../logger'; +import { OpenApiService } from '../../services/openapi-service'; +import ExportImportService, { + IExportQuery, +} from 'lib/services/export-import-service'; + +class ExportImportController extends Controller { + private logger: Logger; + + private exportImportService: ExportImportService; + + private openApiService: OpenApiService; + + constructor( + config: IUnleashConfig, + { + exportImportService, + openApiService, + }: Pick, + ) { + super(config); + this.logger = config.getLogger('/admin-api/export-import.ts'); + this.exportImportService = exportImportService; + this.openApiService = openApiService; + this.route({ + method: 'post', + path: '/export', + permission: NONE, + handler: this.export, + // middleware: [ + // this.openApiService.validPath({ + // tags: ['Import/Export'], + // operationId: 'export', + // responses: { + // 200: createResponseSchema('stateSchema'), + // }, + // parameters: + // exportQueryParameters as unknown as OpenAPIV3.ParameterObject[], + // }), + // ], + }); + } + + async export( + req: Request, + res: Response, + ): Promise { + const query = req.body; + const data = await this.exportImportService.export(query); + + res.json(data); + } +} +export default ExportImportController; diff --git a/src/lib/routes/admin-api/index.ts b/src/lib/routes/admin-api/index.ts index 04b479c149..f272c7c2f3 100644 --- a/src/lib/routes/admin-api/index.ts +++ b/src/lib/routes/admin-api/index.ts @@ -28,6 +28,7 @@ import { PublicSignupController } from './public-signup'; import InstanceAdminController from './instance-admin'; import FavoritesController from './favorites'; import MaintenanceController from './maintenance'; +import ExportImportController from './export-import'; class AdminApi extends Controller { constructor(config: IUnleashConfig, services: IUnleashServices) { @@ -77,6 +78,10 @@ class AdminApi extends Controller { new ContextController(config, services).router, ); this.app.use('/state', new StateController(config, services).router); + this.app.use( + '/features-batch', + new ExportImportController(config, services).router, + ); this.app.use('/tags', new TagController(config, services).router); this.app.use( '/tag-types', diff --git a/src/lib/services/export-import-service.ts b/src/lib/services/export-import-service.ts new file mode 100644 index 0000000000..04e2c1ced2 --- /dev/null +++ b/src/lib/services/export-import-service.ts @@ -0,0 +1,87 @@ +import { IUnleashConfig } from '../types/option'; +import { FeatureToggle, ITag } from '../types/model'; +import { Logger } from '../logger'; +import { IFeatureTagStore } from '../types/stores/feature-tag-store'; +import { IProjectStore } from '../types/stores/project-store'; +import { ITagTypeStore } from '../types/stores/tag-type-store'; +import { ITagStore } from '../types/stores/tag-store'; +import { IEventStore } from '../types/stores/event-store'; +import { 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 { ISegmentStore } from '../types/stores/segment-store'; +import { IFlagResolver } from 'lib/types'; +import { IContextFieldDto } from '../types/stores/context-field-store'; + +export interface IExportQuery { + features: string[]; + environment: string; +} + +export interface IExportData { + features: FeatureToggle[]; + tags?: ITag[]; + contextFields?: IContextFieldDto[]; +} + +export default class ExportImportService { + 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, + ) { + 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 export(query: IExportQuery): Promise { + const features = ( + await this.toggleStore.getAll({ archived: false }) + ).filter((toggle) => query.features.includes(toggle.name)); + return { features: features }; + } +} + +module.exports = ExportImportService; diff --git a/src/lib/services/index.ts b/src/lib/services/index.ts index e77922ef4e..de5c7eb663 100644 --- a/src/lib/services/index.ts +++ b/src/lib/services/index.ts @@ -39,6 +39,7 @@ import { LastSeenService } from './client-metrics/last-seen-service'; import { InstanceStatsService } from './instance-stats-service'; import { FavoritesService } from './favorites-service'; import MaintenanceService from './maintenance-service'; +import ExportImportService from './export-import-service'; export const createServices = ( stores: IUnleashStores, @@ -60,6 +61,7 @@ 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); @@ -176,6 +178,7 @@ export const createServices = ( instanceStatsService, favoritesService, maintenanceService, + exportImportService, }; }; @@ -218,4 +221,5 @@ export { LastSeenService, InstanceStatsService, FavoritesService, + ExportImportService, }; diff --git a/src/lib/types/services.ts b/src/lib/types/services.ts index 99a111504b..d35f8640bf 100644 --- a/src/lib/types/services.ts +++ b/src/lib/types/services.ts @@ -37,6 +37,7 @@ import { LastSeenService } from '../services/client-metrics/last-seen-service'; import { InstanceStatsService } from '../services/instance-stats-service'; import { FavoritesService } from '../services'; import MaintenanceService from '../services/maintenance-service'; +import ExportImportService from 'lib/services/export-import-service'; export interface IUnleashServices { accessService: AccessService; @@ -79,4 +80,5 @@ export interface IUnleashServices { instanceStatsService: InstanceStatsService; favoritesService: FavoritesService; maintenanceService: MaintenanceService; + exportImportService: ExportImportService; } diff --git a/src/test/e2e/api/admin/export-import.e2e.test.ts b/src/test/e2e/api/admin/export-import.e2e.test.ts new file mode 100644 index 0000000000..3ae2715929 --- /dev/null +++ b/src/test/e2e/api/admin/export-import.e2e.test.ts @@ -0,0 +1,72 @@ +import { IUnleashTest, setupApp } from '../../helpers/test-helper'; +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 { DEFAULT_ENV } from '../../../../lib/util'; + +let app: IUnleashTest; +let db: ITestDb; +let eventStore: IEventStore; + +const createToggle = async ( + toggle: FeatureToggleDTO, + strategy?: Omit, + projectId: string = 'default', + username: string = 'test', +) => { + await app.services.featureToggleServiceV2.createFeatureToggle( + projectId, + toggle, + username, + ); + if (strategy) { + await app.services.featureToggleServiceV2.createStrategy( + strategy, + { projectId, featureName: toggle.name, environment: DEFAULT_ENV }, + username, + ); + } +}; + +beforeAll(async () => { + db = await dbInit('export_import_api_serial', getLogger); + app = await setupApp(db.stores); + eventStore = db.stores.eventStore; +}); + +beforeEach(async () => { + await eventStore.deleteAll(); +}); + +afterAll(async () => { + await app.destroy(); + await db.destroy(); +}); + +afterEach(() => { + db.stores.featureToggleStore.deleteAll(); +}); + +test('exports features', async () => { + await createToggle({ + name: 'first_feature', + description: 'the #1 feature', + }); + const { body } = await app.request + .post('/api/admin/features-batch/export') + .send({ + features: ['first_feature'], + environment: 'default', + }) + .set('Content-Type', 'application/json') + .expect(200); + + expect(body).toMatchObject({ + features: [ + { + name: 'first_feature', + }, + ], + }); +});