1
0
mirror of https://github.com/Unleash/unleash.git synced 2025-07-26 13:48:33 +02:00

feat: Import feature links (#9958)

This commit is contained in:
Mateusz Kwasniewski 2025-05-12 13:59:18 +02:00 committed by GitHub
parent d4d6e658ff
commit d175a5705a
No known key found for this signature in database
GPG Key ID: B5690EEEBB952194
10 changed files with 75 additions and 2 deletions

View File

@ -30,6 +30,8 @@ export const ImportExplanation: FC = () => (
<li>variants</li>
<li>tags</li>
<li>feature flag status</li>
<li>feature dependencies</li>
<li>feature links</li>
</ul>
</ImportExplanationDescription>
<ImportExplanationHeader>Exceptions?</ImportExplanationHeader>

View File

@ -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,

View File

@ -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,

View File

@ -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 } =

View File

@ -27,7 +27,8 @@ export interface IImportTogglesStore {
project: string,
): Promise<ProjectFeaturesLimit>;
deleteTagsForFeatures(tags: string[]): Promise<void>;
deleteTagsForFeatures(features: string[]): Promise<void>;
deleteLinksForFeatures(features: string[]): Promise<void>;
strategiesExistForFeatures(
featureNames: string[],

View File

@ -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<void> {
return this.db(T.featureTag).whereIn('feature_name', features).del();
}
async deleteLinksForFeatures(features: string[]): Promise<void> {
return this.db(T.links).whereIn('feature_name', features).del();
}
}

View File

@ -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';

View File

@ -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;

View File

@ -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,
};
};

View File

@ -136,5 +136,6 @@ export interface IUnleashServices {
uniqueConnectionService: UniqueConnectionService;
featureLifecycleReadModel: IFeatureLifecycleReadModel;
transactionalFeatureLinkService: WithTransactional<FeatureLinkService>;
featureLinkService: FeatureLinkService;
unknownFlagsService: UnknownFlagsService;
}