1
0
mirror of https://github.com/Unleash/unleash.git synced 2025-05-03 01:18:43 +02:00

feat: filter by tags (#5163)

This commit is contained in:
Mateusz Kwasniewski 2023-10-26 17:20:57 +02:00 committed by GitHub
parent 66cc526855
commit 46d7cb236d
No known key found for this signature in database
GPG Key ID: 4AEE18F83AFDEB23
6 changed files with 61 additions and 1 deletions

View File

@ -3,6 +3,7 @@ import Controller from '../../routes/controller';
import { FeatureSearchService, OpenApiService } from '../../services'; import { FeatureSearchService, OpenApiService } from '../../services';
import { import {
IFlagResolver, IFlagResolver,
ITag,
IUnleashConfig, IUnleashConfig,
IUnleashServices, IUnleashServices,
NONE, NONE,
@ -71,13 +72,17 @@ export default class FeatureSearchController extends Controller {
res: Response, res: Response,
): Promise<void> { ): Promise<void> {
if (this.config.flagResolver.isEnabled('featureSearchAPI')) { 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 userId = req.user.id;
const normalizedTag = tag
?.map((tag) => tag.split(':'))
.filter((tag) => tag.length === 2);
const features = await this.featureSearchService.search({ const features = await this.featureSearchService.search({
query, query,
projectId, projectId,
type, type,
userId, userId,
tag: normalizedTag,
}); });
res.json({ features }); res.json({ features });
} else { } else {

View File

@ -25,6 +25,7 @@ export class FeatureSearchService {
query: params.query, query: params.query,
userId: params.userId, userId: params.userId,
type: params.type, type: params.type,
tag: params.tag,
}); });
return features; return features;

View File

@ -50,6 +50,13 @@ const filterFeaturesByType = async (types: string[], expectedCode = 200) => {
.expect(expectedCode); .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) => { const searchFeaturesWithoutQueryParams = async (expectedCode = 200) => {
return app.request.get(`/api/admin/search/features`).expect(expectedCode); 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 () => { test('should search matching features by tag', async () => {
await app.createFeature('my_feature_a'); await app.createFeature('my_feature_a');
await app.createFeature('my_feature_b'); await app.createFeature('my_feature_b');

View File

@ -521,6 +521,7 @@ class FeatureStrategiesStore implements IFeatureStrategiesStore {
userId, userId,
query: queryString, query: queryString,
type, type,
tag,
}: IFeatureSearchParams): Promise<IFeatureOverview[]> { }: IFeatureSearchParams): Promise<IFeatureOverview[]> {
let query = this.db('features'); let query = this.db('features');
if (projectId) { if (projectId) {
@ -542,6 +543,13 @@ class FeatureStrategiesStore implements IFeatureStrategiesStore {
.whereILike('features.name', `%${queryString}%`) .whereILike('features.name', `%${queryString}%`)
.orWhereIn('features.name', tagQuery); .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) { if (type) {
query = query.whereIn('features.type', type); query = query.whereIn('features.type', type);
} }

View File

@ -4,6 +4,7 @@ import {
IFeatureOverview, IFeatureOverview,
IFeatureStrategy, IFeatureStrategy,
IStrategyConfig, IStrategyConfig,
ITag,
IVariant, IVariant,
} from '../../../types/model'; } from '../../../types/model';
import { Store } from '../../../types/stores/store'; import { Store } from '../../../types/stores/store';
@ -25,6 +26,7 @@ export interface IFeatureSearchParams {
query?: string; query?: string;
projectId?: string; projectId?: string;
type?: string[]; type?: string[];
tag?: string[][];
} }
export interface IFeatureStrategiesStore export interface IFeatureStrategiesStore

View File

@ -31,6 +31,19 @@ export const featureSearchQueryParameters = [
description: 'The list of feature types to filter by', description: 'The list of feature types to filter by',
in: 'query', 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; ] as const;
export type FeatureSearchQueryParameters = Partial< export type FeatureSearchQueryParameters = Partial<