mirror of
				https://github.com/Unleash/unleash.git
				synced 2025-10-27 11:02:16 +01:00 
			
		
		
		
	feat: filter by tags (#5163)
This commit is contained in:
		
							parent
							
								
									66cc526855
								
							
						
					
					
						commit
						46d7cb236d
					
				@ -3,6 +3,7 @@ import Controller from '../../routes/controller';
 | 
			
		||||
import { FeatureSearchService, OpenApiService } from '../../services';
 | 
			
		||||
import {
 | 
			
		||||
    IFlagResolver,
 | 
			
		||||
    ITag,
 | 
			
		||||
    IUnleashConfig,
 | 
			
		||||
    IUnleashServices,
 | 
			
		||||
    NONE,
 | 
			
		||||
@ -71,13 +72,17 @@ export default class FeatureSearchController extends Controller {
 | 
			
		||||
        res: Response,
 | 
			
		||||
    ): Promise<void> {
 | 
			
		||||
        if (this.config.flagResolver.isEnabled('featureSearchAPI')) {
 | 
			
		||||
            const { query, projectId, type } = req.query;
 | 
			
		||||
            const { query, projectId, type, tag } = req.query;
 | 
			
		||||
            const userId = req.user.id;
 | 
			
		||||
            const normalizedTag = tag
 | 
			
		||||
                ?.map((tag) => tag.split(':'))
 | 
			
		||||
                .filter((tag) => tag.length === 2);
 | 
			
		||||
            const features = await this.featureSearchService.search({
 | 
			
		||||
                query,
 | 
			
		||||
                projectId,
 | 
			
		||||
                type,
 | 
			
		||||
                userId,
 | 
			
		||||
                tag: normalizedTag,
 | 
			
		||||
            });
 | 
			
		||||
            res.json({ features });
 | 
			
		||||
        } else {
 | 
			
		||||
 | 
			
		||||
@ -25,6 +25,7 @@ export class FeatureSearchService {
 | 
			
		||||
            query: params.query,
 | 
			
		||||
            userId: params.userId,
 | 
			
		||||
            type: params.type,
 | 
			
		||||
            tag: params.tag,
 | 
			
		||||
        });
 | 
			
		||||
 | 
			
		||||
        return features;
 | 
			
		||||
 | 
			
		||||
@ -50,6 +50,13 @@ const filterFeaturesByType = async (types: string[], expectedCode = 200) => {
 | 
			
		||||
        .expect(expectedCode);
 | 
			
		||||
};
 | 
			
		||||
 | 
			
		||||
const filterFeaturesByTag = async (tags: string[], expectedCode = 200) => {
 | 
			
		||||
    const tagParams = tags.map((tag) => `tag[]=${tag}`).join('&');
 | 
			
		||||
    return app.request
 | 
			
		||||
        .get(`/api/admin/search/features?${tagParams}`)
 | 
			
		||||
        .expect(expectedCode);
 | 
			
		||||
};
 | 
			
		||||
 | 
			
		||||
const searchFeaturesWithoutQueryParams = async (expectedCode = 200) => {
 | 
			
		||||
    return app.request.get(`/api/admin/search/features`).expect(expectedCode);
 | 
			
		||||
};
 | 
			
		||||
@ -80,6 +87,30 @@ test('should filter features by type', async () => {
 | 
			
		||||
    });
 | 
			
		||||
});
 | 
			
		||||
 | 
			
		||||
test('should filter features by tag', async () => {
 | 
			
		||||
    await app.createFeature('my_feature_a');
 | 
			
		||||
    await app.createFeature('my_feature_b');
 | 
			
		||||
    await app.addTag('my_feature_a', { type: 'simple', value: 'my_tag' });
 | 
			
		||||
 | 
			
		||||
    const { body } = await filterFeaturesByTag(['simple:my_tag']);
 | 
			
		||||
 | 
			
		||||
    expect(body).toMatchObject({
 | 
			
		||||
        features: [{ name: 'my_feature_a' }],
 | 
			
		||||
    });
 | 
			
		||||
});
 | 
			
		||||
 | 
			
		||||
test('filter with invalid tag should ignore filter', async () => {
 | 
			
		||||
    await app.createFeature('my_feature_a');
 | 
			
		||||
    await app.createFeature('my_feature_b');
 | 
			
		||||
    await app.addTag('my_feature_a', { type: 'simple', value: 'my_tag' });
 | 
			
		||||
 | 
			
		||||
    const { body } = await filterFeaturesByTag(['simple']);
 | 
			
		||||
 | 
			
		||||
    expect(body).toMatchObject({
 | 
			
		||||
        features: [{ name: 'my_feature_a' }, { name: 'my_feature_b' }],
 | 
			
		||||
    });
 | 
			
		||||
});
 | 
			
		||||
 | 
			
		||||
test('should search matching features by tag', async () => {
 | 
			
		||||
    await app.createFeature('my_feature_a');
 | 
			
		||||
    await app.createFeature('my_feature_b');
 | 
			
		||||
 | 
			
		||||
@ -521,6 +521,7 @@ class FeatureStrategiesStore implements IFeatureStrategiesStore {
 | 
			
		||||
        userId,
 | 
			
		||||
        query: queryString,
 | 
			
		||||
        type,
 | 
			
		||||
        tag,
 | 
			
		||||
    }: IFeatureSearchParams): Promise<IFeatureOverview[]> {
 | 
			
		||||
        let query = this.db('features');
 | 
			
		||||
        if (projectId) {
 | 
			
		||||
@ -542,6 +543,13 @@ class FeatureStrategiesStore implements IFeatureStrategiesStore {
 | 
			
		||||
                .whereILike('features.name', `%${queryString}%`)
 | 
			
		||||
                .orWhereIn('features.name', tagQuery);
 | 
			
		||||
        }
 | 
			
		||||
        if (tag && tag.length > 0) {
 | 
			
		||||
            const tagQuery = this.db
 | 
			
		||||
                .from('feature_tag')
 | 
			
		||||
                .select('feature_name')
 | 
			
		||||
                .whereIn(['tag_type', 'tag_value'], tag);
 | 
			
		||||
            query = query.whereIn('features.name', tagQuery);
 | 
			
		||||
        }
 | 
			
		||||
        if (type) {
 | 
			
		||||
            query = query.whereIn('features.type', type);
 | 
			
		||||
        }
 | 
			
		||||
 | 
			
		||||
@ -4,6 +4,7 @@ import {
 | 
			
		||||
    IFeatureOverview,
 | 
			
		||||
    IFeatureStrategy,
 | 
			
		||||
    IStrategyConfig,
 | 
			
		||||
    ITag,
 | 
			
		||||
    IVariant,
 | 
			
		||||
} from '../../../types/model';
 | 
			
		||||
import { Store } from '../../../types/stores/store';
 | 
			
		||||
@ -25,6 +26,7 @@ export interface IFeatureSearchParams {
 | 
			
		||||
    query?: string;
 | 
			
		||||
    projectId?: string;
 | 
			
		||||
    type?: string[];
 | 
			
		||||
    tag?: string[][];
 | 
			
		||||
}
 | 
			
		||||
 | 
			
		||||
export interface IFeatureStrategiesStore
 | 
			
		||||
 | 
			
		||||
@ -31,6 +31,19 @@ export const featureSearchQueryParameters = [
 | 
			
		||||
        description: 'The list of feature types to filter by',
 | 
			
		||||
        in: 'query',
 | 
			
		||||
    },
 | 
			
		||||
    {
 | 
			
		||||
        name: 'tag',
 | 
			
		||||
        schema: {
 | 
			
		||||
            type: 'array',
 | 
			
		||||
            items: {
 | 
			
		||||
                type: 'string',
 | 
			
		||||
                example: 'simple:my_tag',
 | 
			
		||||
            },
 | 
			
		||||
        },
 | 
			
		||||
        description:
 | 
			
		||||
            'The list of feature tags to filter by. Feature tag has to specify type and value joined with a colon.',
 | 
			
		||||
        in: 'query',
 | 
			
		||||
    },
 | 
			
		||||
] as const;
 | 
			
		||||
 | 
			
		||||
export type FeatureSearchQueryParameters = Partial<
 | 
			
		||||
 | 
			
		||||
		Loading…
	
		Reference in New Issue
	
	Block a user