diff --git a/frontend/src/component/project/Project/Import/configure/ImportExplanation.tsx b/frontend/src/component/project/Project/Import/configure/ImportExplanation.tsx
index 350c291ac2..c114bf81c9 100644
--- a/frontend/src/component/project/Project/Import/configure/ImportExplanation.tsx
+++ b/frontend/src/component/project/Project/Import/configure/ImportExplanation.tsx
@@ -30,6 +30,8 @@ export const ImportExplanation: FC = () => (
variants
tags
feature flag status
+ feature dependencies
+ feature links
Exceptions?
diff --git a/src/lib/features/export-import-toggles/createExportImportService.ts b/src/lib/features/export-import-toggles/createExportImportService.ts
index 128cd1fbe5..00bf9325cb 100644
--- a/src/lib/features/export-import-toggles/createExportImportService.ts
+++ b/src/lib/features/export-import-toggles/createExportImportService.ts
@@ -51,6 +51,10 @@ import {
} from '../context/createContextService';
import { FakeFeatureLinksReadModel } from '../feature-links/fake-feature-links-read-model';
import { FeatureLinksReadModel } from '../feature-links/feature-links-read-model';
+import {
+ createFakeFeatureLinkService,
+ createFeatureLinkService,
+} from '../feature-links/createFeatureLinkService';
export const createFakeExportImportTogglesService = (
config: IUnleashConfig,
@@ -98,6 +102,9 @@ export const createFakeExportImportTogglesService = (
const featureLinksReadModel = new FakeFeatureLinksReadModel();
+ const featureLinkService =
+ createFakeFeatureLinkService(config).featureLinkService;
+
return new ExportImportService(
{
importTogglesStore,
@@ -118,6 +125,7 @@ export const createFakeExportImportTogglesService = (
strategyService,
tagTypeService,
dependentFeaturesService,
+ featureLinkService,
},
dependentFeaturesReadModel,
segmentReadModel,
@@ -191,6 +199,8 @@ export const deferredExportImportTogglesService = (
const featureLinksReadModel = new FeatureLinksReadModel(db, eventBus);
+ const featureLinkService = createFeatureLinkService(config)(db);
+
return new ExportImportService(
{
importTogglesStore,
@@ -211,6 +221,7 @@ export const deferredExportImportTogglesService = (
strategyService,
tagTypeService,
dependentFeaturesService,
+ featureLinkService,
},
dependentFeaturesReadModel,
segmentReadModel,
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 59ef481f2f..5a5cfdb956 100644
--- a/src/lib/features/export-import-toggles/export-import-service.ts
+++ b/src/lib/features/export-import-toggles/export-import-service.ts
@@ -59,6 +59,7 @@ import groupBy from 'lodash.groupby';
import { allSettledWithRejection } from '../../util/allSettledWithRejection';
import type { ISegmentReadModel } from '../segment/segment-read-model-type';
import { readFile } from '../../util/read-file';
+import type FeatureLinkService from '../feature-links/feature-link-service';
export type IImportService = {
validate(
@@ -131,6 +132,8 @@ export default class ExportImportService
private featureLinksReadModel: IFeatureLinksReadModel;
+ private featureLinkService: FeatureLinkService;
+
constructor(
stores: Pick<
IUnleashStores,
@@ -155,6 +158,7 @@ export default class ExportImportService
tagTypeService,
featureTagService,
dependentFeaturesService,
+ featureLinkService,
}: Pick<
IUnleashServices,
| 'featureToggleService'
@@ -165,6 +169,7 @@ export default class ExportImportService
| 'tagTypeService'
| 'featureTagService'
| 'dependentFeaturesService'
+ | 'featureLinkService'
>,
dependentFeaturesReadModel: IDependentFeaturesReadModel,
segmentReadModel: ISegmentReadModel,
@@ -186,6 +191,7 @@ export default class ExportImportService
this.tagTypeService = tagTypeService;
this.featureTagService = featureTagService;
this.dependentFeaturesService = dependentFeaturesService;
+ this.featureLinkService = featureLinkService;
this.importPermissionsService = new ImportPermissionsService(
this.importTogglesStore,
this.accessService,
@@ -297,6 +303,9 @@ export default class ExportImportService
await this.importTagTypes(dto, auditUser);
await this.importTags(dto, auditUser);
await this.importContextFields(dto, auditUser);
+ if (this.flagResolver.isEnabled('featureLinks')) {
+ await this.importLinks(dto, auditUser);
+ }
}
async import(
@@ -355,6 +364,27 @@ export default class ExportImportService
await this.importDependencies(dto, user, auditUser);
}
+ private async importLinks(dto: ImportTogglesSchema, auditUser: IAuditUser) {
+ await this.importTogglesStore.deleteLinksForFeatures(
+ (dto.data.links ?? []).map((featureLink) => featureLink.feature),
+ );
+
+ const links = dto.data.links || [];
+ for (const featureLink of links) {
+ for (const link of featureLink.links) {
+ await this.featureLinkService.createLink(
+ dto.project,
+ {
+ featureName: featureLink.feature,
+ url: link.url,
+ title: link.title || undefined,
+ },
+ auditUser,
+ );
+ }
+ }
+ }
+
private async importDependencies(
dto: ImportTogglesSchema,
user: IUser,
diff --git a/src/lib/features/export-import-toggles/export-import.e2e.test.ts b/src/lib/features/export-import-toggles/export-import.e2e.test.ts
index cf5fd95fa9..69c3894649 100644
--- a/src/lib/features/export-import-toggles/export-import.e2e.test.ts
+++ b/src/lib/features/export-import-toggles/export-import.e2e.test.ts
@@ -10,6 +10,7 @@ import {
type IContextFieldStore,
type IEnvironmentStore,
type IEventStore,
+ type IFeatureLinkStore,
type IFeatureToggleStore,
type IProjectStore,
type ISegment,
@@ -35,6 +36,7 @@ let contextFieldStore: IContextFieldStore;
let projectStore: IProjectStore;
let toggleStore: IFeatureToggleStore;
let tagStore: ITagStore;
+let featureLinkStore: IFeatureLinkStore;
const defaultStrategy: IStrategyConfig = {
name: 'default',
@@ -179,6 +181,7 @@ beforeAll(async () => {
contextFieldStore = db.stores.contextFieldStore;
toggleStore = db.stores.featureToggleStore;
tagStore = db.stores.tagStore;
+ featureLinkStore = db.stores.featureLinkStore;
});
beforeEach(async () => {
@@ -187,6 +190,7 @@ beforeEach(async () => {
await projectStore.deleteAll();
await environmentStore.deleteAll();
await tagStore.deleteAll();
+ await featureLinkStore.deleteAll();
await contextFieldStore.deleteAll();
await app.createContextField({ name: 'appName' });
@@ -835,6 +839,15 @@ test('import features to existing project and environment', async () => {
],
},
],
+ links: [
+ {
+ feature: exportedFeature.name,
+ links: [
+ { url: 'http://example1.com', title: 'link title 1' },
+ { url: 'http://example2.com' },
+ ],
+ },
+ ],
},
});
@@ -857,6 +870,10 @@ test('import features to existing project and environment', async () => {
feature: anotherExportedFeature.name,
},
],
+ links: [
+ { title: 'link title 1', url: 'http://example1.com' },
+ { title: null, url: 'http://example2.com' },
+ ],
});
const { body: importedFeatureEnvironment } =
diff --git a/src/lib/features/export-import-toggles/import-toggles-store-type.ts b/src/lib/features/export-import-toggles/import-toggles-store-type.ts
index 60cd0afaf3..dd460a60d9 100644
--- a/src/lib/features/export-import-toggles/import-toggles-store-type.ts
+++ b/src/lib/features/export-import-toggles/import-toggles-store-type.ts
@@ -27,7 +27,8 @@ export interface IImportTogglesStore {
project: string,
): Promise;
- deleteTagsForFeatures(tags: string[]): Promise;
+ deleteTagsForFeatures(features: string[]): Promise;
+ deleteLinksForFeatures(features: string[]): Promise;
strategiesExistForFeatures(
featureNames: string[],
diff --git a/src/lib/features/export-import-toggles/import-toggles-store.ts b/src/lib/features/export-import-toggles/import-toggles-store.ts
index 31744cac80..10bc8e30f0 100644
--- a/src/lib/features/export-import-toggles/import-toggles-store.ts
+++ b/src/lib/features/export-import-toggles/import-toggles-store.ts
@@ -9,6 +9,7 @@ const T = {
features: 'features',
featureTag: 'feature_tag',
projectSettings: 'project_settings',
+ links: 'feature_link',
};
export class ImportTogglesStore implements IImportTogglesStore {
private db: Db;
@@ -124,4 +125,8 @@ export class ImportTogglesStore implements IImportTogglesStore {
async deleteTagsForFeatures(features: string[]): Promise {
return this.db(T.featureTag).whereIn('feature_name', features).del();
}
+
+ async deleteLinksForFeatures(features: string[]): Promise {
+ return this.db(T.links).whereIn('feature_name', features).del();
+ }
}
diff --git a/src/lib/features/frontend-api/frontend-api-controller.ts b/src/lib/features/frontend-api/frontend-api-controller.ts
index bc91ca083d..e45d7a56ef 100644
--- a/src/lib/features/frontend-api/frontend-api-controller.ts
+++ b/src/lib/features/frontend-api/frontend-api-controller.ts
@@ -22,7 +22,6 @@ import type { Context } from 'unleash-client';
import { enrichContextWithIp } from './index';
import { corsOriginMiddleware } from '../../middleware';
import NotImplementedError from '../../error/not-implemented-error';
-import NotFoundError from '../../error/notfound-error';
import rateLimit from 'express-rate-limit';
import { minutesToMilliseconds } from 'date-fns';
import metricsHelper from '../../util/metrics-helper';
diff --git a/src/lib/openapi/spec/import-toggles-schema.ts b/src/lib/openapi/spec/import-toggles-schema.ts
index b75915393c..a9420274da 100644
--- a/src/lib/openapi/spec/import-toggles-schema.ts
+++ b/src/lib/openapi/spec/import-toggles-schema.ts
@@ -16,6 +16,8 @@ import { featureEnvironmentSchema } from './feature-environment-schema';
import { strategyVariantSchema } from './strategy-variant-schema';
import { featureDependenciesSchema } from './feature-dependencies-schema';
import { dependentFeatureSchema } from './dependent-feature-schema';
+import { featureLinksSchema } from './feature-links-schema';
+import { featureLinkSchema } from './feature-link-schema';
export const importTogglesSchema = {
$id: '#/components/schemas/importTogglesSchema',
@@ -60,6 +62,8 @@ export const importTogglesSchema = {
tagTypeSchema,
featureDependenciesSchema,
dependentFeatureSchema,
+ featureLinksSchema,
+ featureLinkSchema,
},
},
} as const;
diff --git a/src/lib/services/index.ts b/src/lib/services/index.ts
index 3f65976751..4568fa395d 100644
--- a/src/lib/services/index.ts
+++ b/src/lib/services/index.ts
@@ -444,6 +444,8 @@ export const createServices = (
createFakeFeatureLinkService(config).featureLinkService,
);
+ const featureLinkService = transactionalFeatureLinkService;
+
return {
transactionalAccessService,
accessService,
@@ -514,6 +516,7 @@ export const createServices = (
uniqueConnectionService,
featureLifecycleReadModel,
transactionalFeatureLinkService,
+ featureLinkService,
unknownFlagsService,
};
};
diff --git a/src/lib/types/services.ts b/src/lib/types/services.ts
index 9009f1c2ec..54c6963c55 100644
--- a/src/lib/types/services.ts
+++ b/src/lib/types/services.ts
@@ -136,5 +136,6 @@ export interface IUnleashServices {
uniqueConnectionService: UniqueConnectionService;
featureLifecycleReadModel: IFeatureLifecycleReadModel;
transactionalFeatureLinkService: WithTransactional;
+ featureLinkService: FeatureLinkService;
unknownFlagsService: UnknownFlagsService;
}