mirror of
https://github.com/Unleash/unleash.git
synced 2025-06-27 01:19:00 +02:00
feat: export feature links (#9954)
This commit is contained in:
parent
1ccd201a25
commit
5708acb5b7
@ -49,6 +49,8 @@ import {
|
||||
createContextService,
|
||||
createFakeContextService,
|
||||
} from '../context/createContextService';
|
||||
import { FakeFeatureLinksReadModel } from '../feature-links/fake-feature-links-read-model';
|
||||
import { FeatureLinksReadModel } from '../feature-links/feature-links-read-model';
|
||||
|
||||
export const createFakeExportImportTogglesService = (
|
||||
config: IUnleashConfig,
|
||||
@ -94,6 +96,8 @@ export const createFakeExportImportTogglesService = (
|
||||
|
||||
const dependentFeaturesService = createFakeDependentFeaturesService(config);
|
||||
|
||||
const featureLinksReadModel = new FakeFeatureLinksReadModel();
|
||||
|
||||
return new ExportImportService(
|
||||
{
|
||||
importTogglesStore,
|
||||
@ -117,6 +121,7 @@ export const createFakeExportImportTogglesService = (
|
||||
},
|
||||
dependentFeaturesReadModel,
|
||||
segmentReadModel,
|
||||
featureLinksReadModel,
|
||||
);
|
||||
};
|
||||
|
||||
@ -184,6 +189,8 @@ export const deferredExportImportTogglesService = (
|
||||
const dependentFeaturesService =
|
||||
createDependentFeaturesService(config)(db);
|
||||
|
||||
const featureLinksReadModel = new FeatureLinksReadModel(db, eventBus);
|
||||
|
||||
return new ExportImportService(
|
||||
{
|
||||
importTogglesStore,
|
||||
@ -207,6 +214,7 @@ export const deferredExportImportTogglesService = (
|
||||
},
|
||||
dependentFeaturesReadModel,
|
||||
segmentReadModel,
|
||||
featureLinksReadModel,
|
||||
);
|
||||
};
|
||||
};
|
||||
|
@ -21,6 +21,7 @@ import {
|
||||
type IUnleashStores,
|
||||
type IVariant,
|
||||
type WithRequired,
|
||||
type IFeatureLinksReadModel,
|
||||
} from '../../types';
|
||||
import type {
|
||||
ExportQuerySchema,
|
||||
@ -128,6 +129,8 @@ export default class ExportImportService
|
||||
|
||||
private dependentFeaturesService: DependentFeaturesService;
|
||||
|
||||
private featureLinksReadModel: IFeatureLinksReadModel;
|
||||
|
||||
constructor(
|
||||
stores: Pick<
|
||||
IUnleashStores,
|
||||
@ -165,6 +168,7 @@ export default class ExportImportService
|
||||
>,
|
||||
dependentFeaturesReadModel: IDependentFeaturesReadModel,
|
||||
segmentReadModel: ISegmentReadModel,
|
||||
featureLinksReadModel: IFeatureLinksReadModel,
|
||||
) {
|
||||
this.toggleStore = stores.featureToggleStore;
|
||||
this.importTogglesStore = stores.importTogglesStore;
|
||||
@ -190,6 +194,7 @@ export default class ExportImportService
|
||||
);
|
||||
this.dependentFeaturesReadModel = dependentFeaturesReadModel;
|
||||
this.segmentReadModel = segmentReadModel;
|
||||
this.featureLinksReadModel = featureLinksReadModel;
|
||||
this.logger = getLogger('services/state-service.js');
|
||||
}
|
||||
|
||||
@ -872,6 +877,7 @@ export default class ExportImportService
|
||||
segments,
|
||||
tagTypes,
|
||||
featureDependencies,
|
||||
featureLinks,
|
||||
] = await Promise.all([
|
||||
this.toggleStore.getAllByNames(featureNames),
|
||||
await this.featureEnvironmentStore.getAllByFeatures(
|
||||
@ -888,6 +894,9 @@ export default class ExportImportService
|
||||
this.segmentReadModel.getAll(),
|
||||
this.tagTypeStore.getAll(),
|
||||
this.dependentFeaturesReadModel.getDependencies(featureNames),
|
||||
this.flagResolver.isEnabled('featureLinks')
|
||||
? this.featureLinksReadModel.getLinks(...featureNames)
|
||||
: Promise.resolve([]),
|
||||
]);
|
||||
this.addSegmentsToStrategies(featureStrategies, strategySegments);
|
||||
const filteredContextFields = contextFields
|
||||
@ -929,7 +938,6 @@ export default class ExportImportService
|
||||
featureDependencies,
|
||||
'feature',
|
||||
);
|
||||
|
||||
const mappedFeatureDependencies = Object.entries(
|
||||
groupedFeatureDependencies,
|
||||
).map(([feature, dependencies]) => ({
|
||||
@ -937,6 +945,18 @@ export default class ExportImportService
|
||||
dependencies: dependencies.map((d) => d.dependency),
|
||||
}));
|
||||
|
||||
const groupedFeatureLinks = groupBy(featureLinks, 'feature');
|
||||
const mappedFeatureLinks = Object.entries(groupedFeatureLinks).map(
|
||||
([feature, links]) => ({
|
||||
feature,
|
||||
links: links.map((link) => ({
|
||||
id: link.id,
|
||||
url: link.url,
|
||||
title: link.title,
|
||||
})),
|
||||
}),
|
||||
);
|
||||
|
||||
const result = {
|
||||
features: features.map((item) => {
|
||||
const { createdAt, archivedAt, lastSeenAt, ...rest } = item;
|
||||
@ -978,6 +998,7 @@ export default class ExportImportService
|
||||
}),
|
||||
tagTypes: filteredTagTypes,
|
||||
dependencies: mappedFeatureDependencies,
|
||||
links: mappedFeatureLinks,
|
||||
};
|
||||
await this.eventService.storeEvent(
|
||||
new FeaturesExportedEvent({ data: result, auditUser }),
|
||||
|
@ -112,6 +112,17 @@ const createVariants = async (feature: string, variants: IVariant[]) => {
|
||||
);
|
||||
};
|
||||
|
||||
const addLink = async (
|
||||
feature: string,
|
||||
link: { url: string; title: string },
|
||||
) => {
|
||||
await app.services.transactionalFeatureLinkService.createLink(
|
||||
DEFAULT_ENV,
|
||||
{ ...link, featureName: feature },
|
||||
TEST_AUDIT_USER,
|
||||
);
|
||||
};
|
||||
|
||||
const createProjects = async (
|
||||
projects: string[] = [DEFAULT_PROJECT],
|
||||
featureLimit = 2,
|
||||
@ -155,7 +166,9 @@ beforeAll(async () => {
|
||||
db.stores,
|
||||
{
|
||||
experimental: {
|
||||
flags: {},
|
||||
flags: {
|
||||
featureLinks: true,
|
||||
},
|
||||
},
|
||||
},
|
||||
db.rawDatabase,
|
||||
@ -290,6 +303,15 @@ test('exports features', async () => {
|
||||
);
|
||||
|
||||
await app.addDependency(defaultFeatureName, 'second_feature');
|
||||
await addLink(defaultFeatureName, {
|
||||
url: 'http://example1.com',
|
||||
title: 'link title 1',
|
||||
});
|
||||
await addLink(defaultFeatureName, {
|
||||
url: 'http://example2.com',
|
||||
title: 'link title 2',
|
||||
});
|
||||
|
||||
const { body } = await app.request
|
||||
.post('/api/admin/features-batch/export')
|
||||
.send({
|
||||
@ -331,6 +353,15 @@ test('exports features', async () => {
|
||||
],
|
||||
},
|
||||
],
|
||||
links: [
|
||||
{
|
||||
feature: defaultFeatureName,
|
||||
links: [
|
||||
{ url: 'http://example1.com', title: 'link title 1' },
|
||||
{ url: 'http://example2.com', title: 'link title 2' },
|
||||
],
|
||||
},
|
||||
],
|
||||
});
|
||||
});
|
||||
|
||||
|
@ -8,7 +8,7 @@ export class FakeFeatureLinksReadModel implements IFeatureLinksReadModel {
|
||||
return [];
|
||||
}
|
||||
|
||||
async getLinks(feature: string): Promise<IFeatureLink[]> {
|
||||
async getLinks(...features: string[]): Promise<IFeatureLink[]> {
|
||||
return [];
|
||||
}
|
||||
}
|
||||
|
@ -2,9 +2,10 @@ export interface IFeatureLink {
|
||||
id: string;
|
||||
url: string;
|
||||
title: string | null;
|
||||
feature: string;
|
||||
}
|
||||
|
||||
export interface IFeatureLinksReadModel {
|
||||
getLinks(feature: string): Promise<IFeatureLink[]>;
|
||||
getLinks(...features: string[]): Promise<IFeatureLink[]>;
|
||||
getTopDomains(): Promise<{ domain: string; count: number }[]>;
|
||||
}
|
||||
|
@ -52,16 +52,17 @@ export class FeatureLinksReadModel implements IFeatureLinksReadModel {
|
||||
}));
|
||||
}
|
||||
|
||||
async getLinks(feature: string): Promise<IFeatureLink[]> {
|
||||
async getLinks(...features: string[]): Promise<IFeatureLink[]> {
|
||||
const links = await this.db
|
||||
.from('feature_link')
|
||||
.where('feature_name', feature)
|
||||
.whereIn('feature_name', features)
|
||||
.orderBy('id', 'asc');
|
||||
|
||||
return links.map((link) => ({
|
||||
id: link.id,
|
||||
url: link.url,
|
||||
title: link.title,
|
||||
feature: link.feature_name,
|
||||
}));
|
||||
}
|
||||
}
|
||||
|
@ -1109,7 +1109,11 @@ class FeatureToggleService {
|
||||
dependencies,
|
||||
children,
|
||||
lifecycle,
|
||||
links,
|
||||
links: links.map((link) => ({
|
||||
id: link.id,
|
||||
url: link.url,
|
||||
title: link.title,
|
||||
})),
|
||||
collaborators: { users: collaborators },
|
||||
};
|
||||
} else {
|
||||
|
@ -15,6 +15,8 @@ import { strategyVariantSchema } from './strategy-variant-schema';
|
||||
import { featureDependenciesSchema } from './feature-dependencies-schema';
|
||||
import { dependentFeatureSchema } from './dependent-feature-schema';
|
||||
import { tagSchema } from './tag-schema';
|
||||
import { featureLinkSchema } from './feature-link-schema';
|
||||
import { featureLinksSchema } from './feature-links-schema';
|
||||
|
||||
export const exportResultSchema = {
|
||||
$id: '#/components/schemas/exportResultSchema',
|
||||
@ -177,6 +179,13 @@ export const exportResultSchema = {
|
||||
$ref: '#/components/schemas/featureDependenciesSchema',
|
||||
},
|
||||
},
|
||||
links: {
|
||||
type: 'array',
|
||||
description: 'A list of links for features in `features` list.',
|
||||
items: {
|
||||
$ref: '#/components/schemas/featureLinksSchema',
|
||||
},
|
||||
},
|
||||
},
|
||||
components: {
|
||||
schemas: {
|
||||
@ -196,6 +205,8 @@ export const exportResultSchema = {
|
||||
featureDependenciesSchema,
|
||||
dependentFeatureSchema,
|
||||
tagSchema,
|
||||
featureLinksSchema,
|
||||
featureLinkSchema,
|
||||
},
|
||||
},
|
||||
} as const;
|
||||
|
31
src/lib/openapi/spec/feature-links-schema.ts
Normal file
31
src/lib/openapi/spec/feature-links-schema.ts
Normal file
@ -0,0 +1,31 @@
|
||||
import type { FromSchema } from 'json-schema-to-ts';
|
||||
import { featureLinkSchema } from './feature-link-schema';
|
||||
|
||||
export const featureLinksSchema = {
|
||||
$id: '#/components/schemas/featureLinksSchema',
|
||||
type: 'object',
|
||||
description: 'A list of links for a feature',
|
||||
required: ['feature', 'links'],
|
||||
additionalProperties: false,
|
||||
properties: {
|
||||
feature: {
|
||||
type: 'string',
|
||||
description: 'The name of the child feature.',
|
||||
example: 'child_feature',
|
||||
},
|
||||
links: {
|
||||
type: 'array',
|
||||
description: 'List of feature links',
|
||||
items: {
|
||||
$ref: '#/components/schemas/featureLinkSchema',
|
||||
},
|
||||
},
|
||||
},
|
||||
components: {
|
||||
schemas: {
|
||||
featureLinkSchema,
|
||||
},
|
||||
},
|
||||
} as const;
|
||||
|
||||
export type FeatureLinksSchema = FromSchema<typeof featureLinksSchema>;
|
@ -86,6 +86,7 @@ export * from './feature-lifecycle-completed-schema';
|
||||
export * from './feature-lifecycle-count-schema';
|
||||
export * from './feature-lifecycle-schema';
|
||||
export * from './feature-link-schema';
|
||||
export * from './feature-links-schema';
|
||||
export * from './feature-metrics-schema';
|
||||
export * from './feature-schema';
|
||||
export * from './feature-search-environment-schema';
|
||||
|
@ -124,7 +124,7 @@ export interface FeatureToggleView extends FeatureToggleWithEnvironment {
|
||||
children: string[];
|
||||
lifecycle: IFeatureLifecycleStage | undefined;
|
||||
collaborators?: { users: Collaborator[] };
|
||||
links: IFeatureLink[];
|
||||
links: Omit<IFeatureLink, 'feature'>[];
|
||||
}
|
||||
|
||||
export interface IEnvironmentDetail extends IEnvironmentBase {
|
||||
|
Loading…
Reference in New Issue
Block a user