From 80ba3647a69ac455684c341b85e6085568f16848 Mon Sep 17 00:00:00 2001 From: David Leek Date: Fri, 31 May 2024 13:21:41 +0200 Subject: [PATCH] feat: file import (#7219) --- .../__snapshots__/create-config.test.ts.snap | 4 +- src/lib/create-config.ts | 7 +-- .../export-import-service.ts | 53 +++++++++++++++++-- .../import-file-reader.ts | 8 +++ src/lib/server-impl.ts | 6 ++- src/lib/types/option.ts | 4 +- 6 files changed, 67 insertions(+), 15 deletions(-) create mode 100644 src/lib/features/export-import-toggles/import-file-reader.ts diff --git a/src/lib/__snapshots__/create-config.test.ts.snap b/src/lib/__snapshots__/create-config.test.ts.snap index 1893ab606e..4a068ecae1 100644 --- a/src/lib/__snapshots__/create-config.test.ts.snap +++ b/src/lib/__snapshots__/create-config.test.ts.snap @@ -164,9 +164,9 @@ exports[`should create default config 1`] = ` ], "getLogger": [Function], "import": { - "dropBeforeImport": false, + "environment": "development", "file": undefined, - "keepExisting": false, + "project": "default", }, "inlineSegmentConstraints": true, "isEnterprise": false, diff --git a/src/lib/create-config.ts b/src/lib/create-config.ts index b635df8969..2929f048ef 100644 --- a/src/lib/create-config.ts +++ b/src/lib/create-config.ts @@ -333,11 +333,8 @@ const defaultAuthentication: IAuthOption = { const defaultImport: WithOptional = { file: process.env.IMPORT_FILE, - dropBeforeImport: parseEnvVarBoolean( - process.env.IMPORT_DROP_BEFORE_IMPORT, - false, - ), - keepExisting: parseEnvVarBoolean(process.env.IMPORT_KEEP_EXISTING, false), + project: process.env.IMPORT_PROJECT ?? 'default', + environment: process.env.IMPORT_ENVIRONMENT ?? 'development', }; const defaultEmail: IEmailOption = { diff --git a/src/lib/features/export-import-toggles/export-import-service.ts b/src/lib/features/export-import-toggles/export-import-service.ts index eed0322525..64679c1447 100644 --- a/src/lib/features/export-import-toggles/export-import-service.ts +++ b/src/lib/features/export-import-toggles/export-import-service.ts @@ -5,6 +5,8 @@ import type { IFeatureStrategiesStore } from '../feature-toggle/types/feature-to import { FeaturesExportedEvent, FeaturesImportedEvent, + SYSTEM_USER, + SYSTEM_USER_AUDIT, type FeatureToggleDTO, type IAuditUser, type IContextFieldStore, @@ -55,6 +57,7 @@ import type { IDependentFeaturesReadModel } from '../dependent-features/dependen import groupBy from 'lodash.groupby'; import { allSettledWithRejection } from '../../util/allSettledWithRejection'; import type { ISegmentReadModel } from '../segment/segment-read-model-type'; +import { readFile } from './import-file-reader'; export type IImportService = { validate( @@ -67,6 +70,12 @@ export type IImportService = { user: IUser, auditUser: IAuditUser, ): Promise; + + importFromFile( + file: string, + project: string, + environment: string, + ): Promise; }; export type IExportService = { @@ -264,6 +273,16 @@ export default class ExportImportService ]); } + async fileImportVerify(dto: ImportTogglesSchema): Promise { + await allSettledWithRejection([ + this.verifyStrategies(dto), + this.verifyContextFields(dto), + this.verifyFeatures(dto), + this.verifySegments(dto), + this.verifyDependencies(dto), + ]); + } + async importFeatureData( dto: ImportTogglesSchema, auditUser: IAuditUser, @@ -281,16 +300,40 @@ export default class ExportImportService auditUser: IAuditUser, ): Promise { const cleanedDto = await this.cleanData(dto); - await this.importVerify(cleanedDto, user); + await this.processImport(cleanedDto, user, auditUser); + } - await this.importFeatureData(cleanedDto, auditUser); + async importFromFile( + file: string, + project: string, + environment: string, + ): Promise { + const content = await readFile(file); + const data = JSON.parse(content); + const dto = { + project, + environment, + data, + }; + const cleanedDto = await this.cleanData(dto); - await this.importEnvironmentData(cleanedDto, user, auditUser); + await this.fileImportVerify(cleanedDto); + await this.processImport(cleanedDto, SYSTEM_USER, SYSTEM_USER_AUDIT); + } + + private async processImport( + dto: ImportTogglesSchema, + user: IUser, + auditUser: IAuditUser, + ) { + await this.importFeatureData(dto, auditUser); + + await this.importEnvironmentData(dto, user, auditUser); await this.eventService.storeEvent( new FeaturesImportedEvent({ - project: cleanedDto.project, - environment: cleanedDto.environment, + project: dto.project, + environment: dto.environment, auditUser, }), ); diff --git a/src/lib/features/export-import-toggles/import-file-reader.ts b/src/lib/features/export-import-toggles/import-file-reader.ts new file mode 100644 index 0000000000..1b1c14f187 --- /dev/null +++ b/src/lib/features/export-import-toggles/import-file-reader.ts @@ -0,0 +1,8 @@ +import * as fs from 'fs'; + +export const readFile: (file: string) => Promise = async (file) => + new Promise((resolve, reject) => + fs.readFile(file, (err, v) => + err ? reject(err) : resolve(v.toString('utf-8')), + ), + ); diff --git a/src/lib/server-impl.ts b/src/lib/server-impl.ts index 79a236e077..4a596e17e5 100644 --- a/src/lib/server-impl.ts +++ b/src/lib/server-impl.ts @@ -97,7 +97,11 @@ async function createApp( }; if (config.import.file) { - // TODO: stateservice was here + await services.importService.importFromFile( + config.import.file, + config.import.project, + config.import.environment, + ); } if ( diff --git a/src/lib/types/option.ts b/src/lib/types/option.ts index e46b2c227c..b8d499ad45 100644 --- a/src/lib/types/option.ts +++ b/src/lib/types/option.ts @@ -81,8 +81,8 @@ export interface IAuthOption { export interface IImportOption { file: string; - keepExisting: boolean; - dropBeforeImport: boolean; + project: string; + environment: string; } export interface IServerOption {