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 { 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