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