mirror of
				https://github.com/Unleash/unleash.git
				synced 2025-10-27 11:02:16 +01: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