mirror of
				https://github.com/Unleash/unleash.git
				synced 2025-10-27 11:02:16 +01:00 
			
		
		
		
	feat: filter features by type (#5160)
This commit is contained in:
		
							parent
							
								
									05f4c22f7c
								
							
						
					
					
						commit
						0c8d0704f3
					
				| @ -71,10 +71,13 @@ export default class FeatureSearchController extends Controller { | ||||
|         res: Response, | ||||
|     ): Promise<void> { | ||||
|         if (this.config.flagResolver.isEnabled('featureSearchAPI')) { | ||||
|             const { query = '', projectId = '' } = req.query; | ||||
|             const { query, projectId, type } = req.query; | ||||
|             const userId = req.user.id; | ||||
|             const features = await this.featureSearchService.search({ | ||||
|                 query, | ||||
|                 projectId, | ||||
|                 type, | ||||
|                 userId, | ||||
|             }); | ||||
|             res.json({ features }); | ||||
|         } else { | ||||
|  | ||||
| @ -4,7 +4,7 @@ import { | ||||
|     IUnleashConfig, | ||||
|     IUnleashStores, | ||||
| } from '../../types'; | ||||
| import { FeatureSearchQueryParameters } from '../../openapi/spec/feature-search-query-parameters'; | ||||
| import { IFeatureSearchParams } from '../feature-toggle/types/feature-toggle-strategies-store-type'; | ||||
| 
 | ||||
| export class FeatureSearchService { | ||||
|     private featureStrategiesStore: IFeatureStrategiesStore; | ||||
| @ -19,10 +19,12 @@ export class FeatureSearchService { | ||||
|         this.logger = getLogger('services/feature-search-service.ts'); | ||||
|     } | ||||
| 
 | ||||
|     async search(params: FeatureSearchQueryParameters) { | ||||
|     async search(params: IFeatureSearchParams) { | ||||
|         const features = await this.featureStrategiesStore.searchFeatures({ | ||||
|             projectId: params.projectId, | ||||
|             queryString: params.query, | ||||
|             query: params.query, | ||||
|             userId: params.userId, | ||||
|             type: params.type, | ||||
|         }); | ||||
| 
 | ||||
|         return features; | ||||
|  | ||||
| @ -35,7 +35,7 @@ beforeEach(async () => { | ||||
| }); | ||||
| 
 | ||||
| const searchFeatures = async ( | ||||
|     { query, projectId = 'default' }: Partial<FeatureSearchQueryParameters>, | ||||
|     { query = '', projectId = 'default' }: FeatureSearchQueryParameters, | ||||
|     expectedCode = 200, | ||||
| ) => { | ||||
|     return app.request | ||||
| @ -43,11 +43,18 @@ const searchFeatures = async ( | ||||
|         .expect(expectedCode); | ||||
| }; | ||||
| 
 | ||||
| const filterFeaturesByType = async (types: string[], expectedCode = 200) => { | ||||
|     const typeParams = types.map((type) => `type[]=${type}`).join('&'); | ||||
|     return app.request | ||||
|         .get(`/api/admin/search/features?${typeParams}`) | ||||
|         .expect(expectedCode); | ||||
| }; | ||||
| 
 | ||||
| const searchFeaturesWithoutQueryParams = async (expectedCode = 200) => { | ||||
|     return app.request.get(`/api/admin/search/features`).expect(expectedCode); | ||||
| }; | ||||
| 
 | ||||
| test('should return matching features by name', async () => { | ||||
| test('should search matching features by name', async () => { | ||||
|     await app.createFeature('my_feature_a'); | ||||
|     await app.createFeature('my_feature_b'); | ||||
|     await app.createFeature('my_feat_c'); | ||||
| @ -59,7 +66,21 @@ test('should return matching features by name', async () => { | ||||
|     }); | ||||
| }); | ||||
| 
 | ||||
| test('should return matching features by tag', async () => { | ||||
| test('should filter features by type', async () => { | ||||
|     await app.createFeature({ name: 'my_feature_a', type: 'release' }); | ||||
|     await app.createFeature({ name: 'my_feature_b', type: 'experimental' }); | ||||
| 
 | ||||
|     const { body } = await filterFeaturesByType([ | ||||
|         'experimental', | ||||
|         'kill-switch', | ||||
|     ]); | ||||
| 
 | ||||
|     expect(body).toMatchObject({ | ||||
|         features: [{ name: 'my_feature_b' }], | ||||
|     }); | ||||
| }); | ||||
| 
 | ||||
| test('should search matching 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' }); | ||||
| @ -90,7 +111,7 @@ test('should return empty features', async () => { | ||||
|     expect(body).toMatchObject({ features: [] }); | ||||
| }); | ||||
| 
 | ||||
| test('should not return features from another project', async () => { | ||||
| test('should not search features from another project', async () => { | ||||
|     await app.createFeature('my_feature_a'); | ||||
|     await app.createFeature('my_feature_b'); | ||||
| 
 | ||||
|  | ||||
| @ -8,7 +8,10 @@ import { | ||||
|     FeatureToggle, | ||||
| } from '../../../types/model'; | ||||
| import NotFoundError from '../../../error/notfound-error'; | ||||
| import { IFeatureStrategiesStore } from '../types/feature-toggle-strategies-store-type'; | ||||
| import { | ||||
|     IFeatureSearchParams, | ||||
|     IFeatureStrategiesStore, | ||||
| } from '../types/feature-toggle-strategies-store-type'; | ||||
| import { IFeatureProjectUserParams } from '../feature-toggle-controller'; | ||||
| 
 | ||||
| interface ProjectEnvironment { | ||||
| @ -322,11 +325,7 @@ export default class FakeFeatureStrategiesStore | ||||
|         return Promise.resolve([]); | ||||
|     } | ||||
| 
 | ||||
|     searchFeatures(params: { | ||||
|         projectId: string; | ||||
|         userId?: number | undefined; | ||||
|         queryString: string; | ||||
|     }): Promise<IFeatureOverview[]> { | ||||
|     searchFeatures(params: IFeatureSearchParams): Promise<IFeatureOverview[]> { | ||||
|         return Promise.resolve([]); | ||||
|     } | ||||
| 
 | ||||
|  | ||||
| @ -25,6 +25,7 @@ import { ensureStringValue, mapValues } from '../../util'; | ||||
| import { IFeatureProjectUserParams } from './feature-toggle-controller'; | ||||
| import { Db } from '../../db/db'; | ||||
| import Raw = Knex.Raw; | ||||
| import { IFeatureSearchParams } from './types/feature-toggle-strategies-store-type'; | ||||
| 
 | ||||
| const COLUMNS = [ | ||||
|     'id', | ||||
| @ -518,10 +519,9 @@ class FeatureStrategiesStore implements IFeatureStrategiesStore { | ||||
|     async searchFeatures({ | ||||
|         projectId, | ||||
|         userId, | ||||
|         queryString, | ||||
|     }: { projectId: string; userId?: number; queryString: string }): Promise< | ||||
|         IFeatureOverview[] | ||||
|     > { | ||||
|         query: queryString, | ||||
|         type, | ||||
|     }: IFeatureSearchParams): Promise<IFeatureOverview[]> { | ||||
|         let query = this.db('features'); | ||||
|         if (projectId) { | ||||
|             query = query.where({ project: projectId }); | ||||
| @ -542,6 +542,9 @@ class FeatureStrategiesStore implements IFeatureStrategiesStore { | ||||
|                 .whereILike('features.name', `%${queryString}%`) | ||||
|                 .orWhereIn('features.name', tagQuery); | ||||
|         } | ||||
|         if (type) { | ||||
|             query = query.whereIn('features.type', type); | ||||
|         } | ||||
|         query = query | ||||
|             .modify(FeatureToggleStore.filterByArchived, false) | ||||
|             .leftJoin( | ||||
|  | ||||
| @ -19,6 +19,14 @@ export interface FeatureConfigurationClient { | ||||
|     variants: IVariant[]; | ||||
|     dependencies?: IDependency[]; | ||||
| } | ||||
| 
 | ||||
| export interface IFeatureSearchParams { | ||||
|     userId: number; | ||||
|     query?: string; | ||||
|     projectId?: string; | ||||
|     type?: string[]; | ||||
| } | ||||
| 
 | ||||
| export interface IFeatureStrategiesStore | ||||
|     extends Store<IFeatureStrategy, string> { | ||||
|     createStrategyFeatureEnv( | ||||
| @ -46,11 +54,7 @@ export interface IFeatureStrategiesStore | ||||
|     getFeatureOverview( | ||||
|         params: IFeatureProjectUserParams, | ||||
|     ): Promise<IFeatureOverview[]>; | ||||
|     searchFeatures(params: { | ||||
|         projectId: string; | ||||
|         userId?: number; | ||||
|         queryString: string; | ||||
|     }): Promise<IFeatureOverview[]>; | ||||
|     searchFeatures(params: IFeatureSearchParams): Promise<IFeatureOverview[]>; | ||||
|     getStrategyById(id: string): Promise<IFeatureStrategy>; | ||||
|     updateStrategy( | ||||
|         id: string, | ||||
|  | ||||
| @ -4,7 +4,6 @@ export const featureSearchQueryParameters = [ | ||||
|     { | ||||
|         name: 'query', | ||||
|         schema: { | ||||
|             default: '', | ||||
|             type: 'string', | ||||
|             example: 'feature_a', | ||||
|         }, | ||||
| @ -14,15 +13,26 @@ export const featureSearchQueryParameters = [ | ||||
|     { | ||||
|         name: 'projectId', | ||||
|         schema: { | ||||
|             default: '', | ||||
|             type: 'string', | ||||
|             example: 'default', | ||||
|         }, | ||||
|         description: 'Id of the project where search is performed', | ||||
|         description: 'Id of the project where search and filter is performed', | ||||
|         in: 'query', | ||||
|     }, | ||||
|     { | ||||
|         name: 'type', | ||||
|         schema: { | ||||
|             type: 'array', | ||||
|             items: { | ||||
|                 type: 'string', | ||||
|                 example: 'release', | ||||
|             }, | ||||
|         }, | ||||
|         description: 'The list of feature types to filter by', | ||||
|         in: 'query', | ||||
|     }, | ||||
| ] as const; | ||||
| 
 | ||||
| export type FeatureSearchQueryParameters = FromQueryParams< | ||||
|     typeof featureSearchQueryParameters | ||||
| export type FeatureSearchQueryParameters = Partial< | ||||
|     FromQueryParams<typeof featureSearchQueryParameters> | ||||
| >; | ||||
|  | ||||
		Loading…
	
		Reference in New Issue
	
	Block a user