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:
parent
66cc526855
commit
46d7cb236d
@ -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 {
|
||||||
|
@ -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;
|
||||||
|
@ -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');
|
||||||
|
@ -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);
|
||||||
}
|
}
|
||||||
|
@ -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
|
||||||
|
@ -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<
|
||||||
|
Loading…
Reference in New Issue
Block a user