mirror of
https://github.com/Unleash/unleash.git
synced 2025-08-04 13:48:56 +02:00
feat: Import feature links (#9958)
This commit is contained in:
parent
d4d6e658ff
commit
d175a5705a
@ -30,6 +30,8 @@ export const ImportExplanation: FC = () => (
|
|||||||
<li>variants</li>
|
<li>variants</li>
|
||||||
<li>tags</li>
|
<li>tags</li>
|
||||||
<li>feature flag status</li>
|
<li>feature flag status</li>
|
||||||
|
<li>feature dependencies</li>
|
||||||
|
<li>feature links</li>
|
||||||
</ul>
|
</ul>
|
||||||
</ImportExplanationDescription>
|
</ImportExplanationDescription>
|
||||||
<ImportExplanationHeader>Exceptions?</ImportExplanationHeader>
|
<ImportExplanationHeader>Exceptions?</ImportExplanationHeader>
|
||||||
|
@ -51,6 +51,10 @@ import {
|
|||||||
} from '../context/createContextService';
|
} from '../context/createContextService';
|
||||||
import { FakeFeatureLinksReadModel } from '../feature-links/fake-feature-links-read-model';
|
import { FakeFeatureLinksReadModel } from '../feature-links/fake-feature-links-read-model';
|
||||||
import { FeatureLinksReadModel } from '../feature-links/feature-links-read-model';
|
import { FeatureLinksReadModel } from '../feature-links/feature-links-read-model';
|
||||||
|
import {
|
||||||
|
createFakeFeatureLinkService,
|
||||||
|
createFeatureLinkService,
|
||||||
|
} from '../feature-links/createFeatureLinkService';
|
||||||
|
|
||||||
export const createFakeExportImportTogglesService = (
|
export const createFakeExportImportTogglesService = (
|
||||||
config: IUnleashConfig,
|
config: IUnleashConfig,
|
||||||
@ -98,6 +102,9 @@ export const createFakeExportImportTogglesService = (
|
|||||||
|
|
||||||
const featureLinksReadModel = new FakeFeatureLinksReadModel();
|
const featureLinksReadModel = new FakeFeatureLinksReadModel();
|
||||||
|
|
||||||
|
const featureLinkService =
|
||||||
|
createFakeFeatureLinkService(config).featureLinkService;
|
||||||
|
|
||||||
return new ExportImportService(
|
return new ExportImportService(
|
||||||
{
|
{
|
||||||
importTogglesStore,
|
importTogglesStore,
|
||||||
@ -118,6 +125,7 @@ export const createFakeExportImportTogglesService = (
|
|||||||
strategyService,
|
strategyService,
|
||||||
tagTypeService,
|
tagTypeService,
|
||||||
dependentFeaturesService,
|
dependentFeaturesService,
|
||||||
|
featureLinkService,
|
||||||
},
|
},
|
||||||
dependentFeaturesReadModel,
|
dependentFeaturesReadModel,
|
||||||
segmentReadModel,
|
segmentReadModel,
|
||||||
@ -191,6 +199,8 @@ export const deferredExportImportTogglesService = (
|
|||||||
|
|
||||||
const featureLinksReadModel = new FeatureLinksReadModel(db, eventBus);
|
const featureLinksReadModel = new FeatureLinksReadModel(db, eventBus);
|
||||||
|
|
||||||
|
const featureLinkService = createFeatureLinkService(config)(db);
|
||||||
|
|
||||||
return new ExportImportService(
|
return new ExportImportService(
|
||||||
{
|
{
|
||||||
importTogglesStore,
|
importTogglesStore,
|
||||||
@ -211,6 +221,7 @@ export const deferredExportImportTogglesService = (
|
|||||||
strategyService,
|
strategyService,
|
||||||
tagTypeService,
|
tagTypeService,
|
||||||
dependentFeaturesService,
|
dependentFeaturesService,
|
||||||
|
featureLinkService,
|
||||||
},
|
},
|
||||||
dependentFeaturesReadModel,
|
dependentFeaturesReadModel,
|
||||||
segmentReadModel,
|
segmentReadModel,
|
||||||
|
@ -59,6 +59,7 @@ import groupBy from 'lodash.groupby';
|
|||||||
import { allSettledWithRejection } from '../../util/allSettledWithRejection';
|
import { allSettledWithRejection } from '../../util/allSettledWithRejection';
|
||||||
import type { ISegmentReadModel } from '../segment/segment-read-model-type';
|
import type { ISegmentReadModel } from '../segment/segment-read-model-type';
|
||||||
import { readFile } from '../../util/read-file';
|
import { readFile } from '../../util/read-file';
|
||||||
|
import type FeatureLinkService from '../feature-links/feature-link-service';
|
||||||
|
|
||||||
export type IImportService = {
|
export type IImportService = {
|
||||||
validate(
|
validate(
|
||||||
@ -131,6 +132,8 @@ export default class ExportImportService
|
|||||||
|
|
||||||
private featureLinksReadModel: IFeatureLinksReadModel;
|
private featureLinksReadModel: IFeatureLinksReadModel;
|
||||||
|
|
||||||
|
private featureLinkService: FeatureLinkService;
|
||||||
|
|
||||||
constructor(
|
constructor(
|
||||||
stores: Pick<
|
stores: Pick<
|
||||||
IUnleashStores,
|
IUnleashStores,
|
||||||
@ -155,6 +158,7 @@ export default class ExportImportService
|
|||||||
tagTypeService,
|
tagTypeService,
|
||||||
featureTagService,
|
featureTagService,
|
||||||
dependentFeaturesService,
|
dependentFeaturesService,
|
||||||
|
featureLinkService,
|
||||||
}: Pick<
|
}: Pick<
|
||||||
IUnleashServices,
|
IUnleashServices,
|
||||||
| 'featureToggleService'
|
| 'featureToggleService'
|
||||||
@ -165,6 +169,7 @@ export default class ExportImportService
|
|||||||
| 'tagTypeService'
|
| 'tagTypeService'
|
||||||
| 'featureTagService'
|
| 'featureTagService'
|
||||||
| 'dependentFeaturesService'
|
| 'dependentFeaturesService'
|
||||||
|
| 'featureLinkService'
|
||||||
>,
|
>,
|
||||||
dependentFeaturesReadModel: IDependentFeaturesReadModel,
|
dependentFeaturesReadModel: IDependentFeaturesReadModel,
|
||||||
segmentReadModel: ISegmentReadModel,
|
segmentReadModel: ISegmentReadModel,
|
||||||
@ -186,6 +191,7 @@ export default class ExportImportService
|
|||||||
this.tagTypeService = tagTypeService;
|
this.tagTypeService = tagTypeService;
|
||||||
this.featureTagService = featureTagService;
|
this.featureTagService = featureTagService;
|
||||||
this.dependentFeaturesService = dependentFeaturesService;
|
this.dependentFeaturesService = dependentFeaturesService;
|
||||||
|
this.featureLinkService = featureLinkService;
|
||||||
this.importPermissionsService = new ImportPermissionsService(
|
this.importPermissionsService = new ImportPermissionsService(
|
||||||
this.importTogglesStore,
|
this.importTogglesStore,
|
||||||
this.accessService,
|
this.accessService,
|
||||||
@ -297,6 +303,9 @@ export default class ExportImportService
|
|||||||
await this.importTagTypes(dto, auditUser);
|
await this.importTagTypes(dto, auditUser);
|
||||||
await this.importTags(dto, auditUser);
|
await this.importTags(dto, auditUser);
|
||||||
await this.importContextFields(dto, auditUser);
|
await this.importContextFields(dto, auditUser);
|
||||||
|
if (this.flagResolver.isEnabled('featureLinks')) {
|
||||||
|
await this.importLinks(dto, auditUser);
|
||||||
|
}
|
||||||
}
|
}
|
||||||
|
|
||||||
async import(
|
async import(
|
||||||
@ -355,6 +364,27 @@ export default class ExportImportService
|
|||||||
await this.importDependencies(dto, user, auditUser);
|
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(
|
private async importDependencies(
|
||||||
dto: ImportTogglesSchema,
|
dto: ImportTogglesSchema,
|
||||||
user: IUser,
|
user: IUser,
|
||||||
|
@ -10,6 +10,7 @@ import {
|
|||||||
type IContextFieldStore,
|
type IContextFieldStore,
|
||||||
type IEnvironmentStore,
|
type IEnvironmentStore,
|
||||||
type IEventStore,
|
type IEventStore,
|
||||||
|
type IFeatureLinkStore,
|
||||||
type IFeatureToggleStore,
|
type IFeatureToggleStore,
|
||||||
type IProjectStore,
|
type IProjectStore,
|
||||||
type ISegment,
|
type ISegment,
|
||||||
@ -35,6 +36,7 @@ let contextFieldStore: IContextFieldStore;
|
|||||||
let projectStore: IProjectStore;
|
let projectStore: IProjectStore;
|
||||||
let toggleStore: IFeatureToggleStore;
|
let toggleStore: IFeatureToggleStore;
|
||||||
let tagStore: ITagStore;
|
let tagStore: ITagStore;
|
||||||
|
let featureLinkStore: IFeatureLinkStore;
|
||||||
|
|
||||||
const defaultStrategy: IStrategyConfig = {
|
const defaultStrategy: IStrategyConfig = {
|
||||||
name: 'default',
|
name: 'default',
|
||||||
@ -179,6 +181,7 @@ beforeAll(async () => {
|
|||||||
contextFieldStore = db.stores.contextFieldStore;
|
contextFieldStore = db.stores.contextFieldStore;
|
||||||
toggleStore = db.stores.featureToggleStore;
|
toggleStore = db.stores.featureToggleStore;
|
||||||
tagStore = db.stores.tagStore;
|
tagStore = db.stores.tagStore;
|
||||||
|
featureLinkStore = db.stores.featureLinkStore;
|
||||||
});
|
});
|
||||||
|
|
||||||
beforeEach(async () => {
|
beforeEach(async () => {
|
||||||
@ -187,6 +190,7 @@ beforeEach(async () => {
|
|||||||
await projectStore.deleteAll();
|
await projectStore.deleteAll();
|
||||||
await environmentStore.deleteAll();
|
await environmentStore.deleteAll();
|
||||||
await tagStore.deleteAll();
|
await tagStore.deleteAll();
|
||||||
|
await featureLinkStore.deleteAll();
|
||||||
|
|
||||||
await contextFieldStore.deleteAll();
|
await contextFieldStore.deleteAll();
|
||||||
await app.createContextField({ name: 'appName' });
|
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,
|
feature: anotherExportedFeature.name,
|
||||||
},
|
},
|
||||||
],
|
],
|
||||||
|
links: [
|
||||||
|
{ title: 'link title 1', url: 'http://example1.com' },
|
||||||
|
{ title: null, url: 'http://example2.com' },
|
||||||
|
],
|
||||||
});
|
});
|
||||||
|
|
||||||
const { body: importedFeatureEnvironment } =
|
const { body: importedFeatureEnvironment } =
|
||||||
|
@ -27,7 +27,8 @@ export interface IImportTogglesStore {
|
|||||||
project: string,
|
project: string,
|
||||||
): Promise<ProjectFeaturesLimit>;
|
): Promise<ProjectFeaturesLimit>;
|
||||||
|
|
||||||
deleteTagsForFeatures(tags: string[]): Promise<void>;
|
deleteTagsForFeatures(features: string[]): Promise<void>;
|
||||||
|
deleteLinksForFeatures(features: string[]): Promise<void>;
|
||||||
|
|
||||||
strategiesExistForFeatures(
|
strategiesExistForFeatures(
|
||||||
featureNames: string[],
|
featureNames: string[],
|
||||||
|
@ -9,6 +9,7 @@ const T = {
|
|||||||
features: 'features',
|
features: 'features',
|
||||||
featureTag: 'feature_tag',
|
featureTag: 'feature_tag',
|
||||||
projectSettings: 'project_settings',
|
projectSettings: 'project_settings',
|
||||||
|
links: 'feature_link',
|
||||||
};
|
};
|
||||||
export class ImportTogglesStore implements IImportTogglesStore {
|
export class ImportTogglesStore implements IImportTogglesStore {
|
||||||
private db: Db;
|
private db: Db;
|
||||||
@ -124,4 +125,8 @@ export class ImportTogglesStore implements IImportTogglesStore {
|
|||||||
async deleteTagsForFeatures(features: string[]): Promise<void> {
|
async deleteTagsForFeatures(features: string[]): Promise<void> {
|
||||||
return this.db(T.featureTag).whereIn('feature_name', features).del();
|
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();
|
||||||
|
}
|
||||||
}
|
}
|
||||||
|
@ -22,7 +22,6 @@ import type { Context } from 'unleash-client';
|
|||||||
import { enrichContextWithIp } from './index';
|
import { enrichContextWithIp } from './index';
|
||||||
import { corsOriginMiddleware } from '../../middleware';
|
import { corsOriginMiddleware } from '../../middleware';
|
||||||
import NotImplementedError from '../../error/not-implemented-error';
|
import NotImplementedError from '../../error/not-implemented-error';
|
||||||
import NotFoundError from '../../error/notfound-error';
|
|
||||||
import rateLimit from 'express-rate-limit';
|
import rateLimit from 'express-rate-limit';
|
||||||
import { minutesToMilliseconds } from 'date-fns';
|
import { minutesToMilliseconds } from 'date-fns';
|
||||||
import metricsHelper from '../../util/metrics-helper';
|
import metricsHelper from '../../util/metrics-helper';
|
||||||
|
@ -16,6 +16,8 @@ import { featureEnvironmentSchema } from './feature-environment-schema';
|
|||||||
import { strategyVariantSchema } from './strategy-variant-schema';
|
import { strategyVariantSchema } from './strategy-variant-schema';
|
||||||
import { featureDependenciesSchema } from './feature-dependencies-schema';
|
import { featureDependenciesSchema } from './feature-dependencies-schema';
|
||||||
import { dependentFeatureSchema } from './dependent-feature-schema';
|
import { dependentFeatureSchema } from './dependent-feature-schema';
|
||||||
|
import { featureLinksSchema } from './feature-links-schema';
|
||||||
|
import { featureLinkSchema } from './feature-link-schema';
|
||||||
|
|
||||||
export const importTogglesSchema = {
|
export const importTogglesSchema = {
|
||||||
$id: '#/components/schemas/importTogglesSchema',
|
$id: '#/components/schemas/importTogglesSchema',
|
||||||
@ -60,6 +62,8 @@ export const importTogglesSchema = {
|
|||||||
tagTypeSchema,
|
tagTypeSchema,
|
||||||
featureDependenciesSchema,
|
featureDependenciesSchema,
|
||||||
dependentFeatureSchema,
|
dependentFeatureSchema,
|
||||||
|
featureLinksSchema,
|
||||||
|
featureLinkSchema,
|
||||||
},
|
},
|
||||||
},
|
},
|
||||||
} as const;
|
} as const;
|
||||||
|
@ -444,6 +444,8 @@ export const createServices = (
|
|||||||
createFakeFeatureLinkService(config).featureLinkService,
|
createFakeFeatureLinkService(config).featureLinkService,
|
||||||
);
|
);
|
||||||
|
|
||||||
|
const featureLinkService = transactionalFeatureLinkService;
|
||||||
|
|
||||||
return {
|
return {
|
||||||
transactionalAccessService,
|
transactionalAccessService,
|
||||||
accessService,
|
accessService,
|
||||||
@ -514,6 +516,7 @@ export const createServices = (
|
|||||||
uniqueConnectionService,
|
uniqueConnectionService,
|
||||||
featureLifecycleReadModel,
|
featureLifecycleReadModel,
|
||||||
transactionalFeatureLinkService,
|
transactionalFeatureLinkService,
|
||||||
|
featureLinkService,
|
||||||
unknownFlagsService,
|
unknownFlagsService,
|
||||||
};
|
};
|
||||||
};
|
};
|
||||||
|
@ -136,5 +136,6 @@ export interface IUnleashServices {
|
|||||||
uniqueConnectionService: UniqueConnectionService;
|
uniqueConnectionService: UniqueConnectionService;
|
||||||
featureLifecycleReadModel: IFeatureLifecycleReadModel;
|
featureLifecycleReadModel: IFeatureLifecycleReadModel;
|
||||||
transactionalFeatureLinkService: WithTransactional<FeatureLinkService>;
|
transactionalFeatureLinkService: WithTransactional<FeatureLinkService>;
|
||||||
|
featureLinkService: FeatureLinkService;
|
||||||
unknownFlagsService: UnknownFlagsService;
|
unknownFlagsService: UnknownFlagsService;
|
||||||
}
|
}
|
||||||
|
Loading…
Reference in New Issue
Block a user