diff --git a/src/lib/features/feature-search/feature-search-controller.ts b/src/lib/features/feature-search/feature-search-controller.ts index 01d678c90e..bc417e0d47 100644 --- a/src/lib/features/feature-search/feature-search-controller.ts +++ b/src/lib/features/feature-search/feature-search-controller.ts @@ -71,10 +71,13 @@ export default class FeatureSearchController extends Controller { res: Response, ): Promise { 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 { diff --git a/src/lib/features/feature-search/feature-search-service.ts b/src/lib/features/feature-search/feature-search-service.ts index 749efdb1fb..65b39ae856 100644 --- a/src/lib/features/feature-search/feature-search-service.ts +++ b/src/lib/features/feature-search/feature-search-service.ts @@ -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; diff --git a/src/lib/features/feature-search/feature.search.e2e.test.ts b/src/lib/features/feature-search/feature.search.e2e.test.ts index 3d7b3d30c1..f7fd45fbfa 100644 --- a/src/lib/features/feature-search/feature.search.e2e.test.ts +++ b/src/lib/features/feature-search/feature.search.e2e.test.ts @@ -35,7 +35,7 @@ beforeEach(async () => { }); const searchFeatures = async ( - { query, projectId = 'default' }: Partial, + { 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'); diff --git a/src/lib/features/feature-toggle/fakes/fake-feature-strategies-store.ts b/src/lib/features/feature-toggle/fakes/fake-feature-strategies-store.ts index 5de74711a7..a7376b1860 100644 --- a/src/lib/features/feature-toggle/fakes/fake-feature-strategies-store.ts +++ b/src/lib/features/feature-toggle/fakes/fake-feature-strategies-store.ts @@ -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 { + searchFeatures(params: IFeatureSearchParams): Promise { return Promise.resolve([]); } diff --git a/src/lib/features/feature-toggle/feature-toggle-strategies-store.ts b/src/lib/features/feature-toggle/feature-toggle-strategies-store.ts index 417f8e40b0..40d8c9d34b 100644 --- a/src/lib/features/feature-toggle/feature-toggle-strategies-store.ts +++ b/src/lib/features/feature-toggle/feature-toggle-strategies-store.ts @@ -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 { 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( diff --git a/src/lib/features/feature-toggle/types/feature-toggle-strategies-store-type.ts b/src/lib/features/feature-toggle/types/feature-toggle-strategies-store-type.ts index 3df185f9cf..5a960e3ad7 100644 --- a/src/lib/features/feature-toggle/types/feature-toggle-strategies-store-type.ts +++ b/src/lib/features/feature-toggle/types/feature-toggle-strategies-store-type.ts @@ -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 { createStrategyFeatureEnv( @@ -46,11 +54,7 @@ export interface IFeatureStrategiesStore getFeatureOverview( params: IFeatureProjectUserParams, ): Promise; - searchFeatures(params: { - projectId: string; - userId?: number; - queryString: string; - }): Promise; + searchFeatures(params: IFeatureSearchParams): Promise; getStrategyById(id: string): Promise; updateStrategy( id: string, diff --git a/src/lib/openapi/spec/feature-search-query-parameters.ts b/src/lib/openapi/spec/feature-search-query-parameters.ts index 2844d959e2..420399843a 100644 --- a/src/lib/openapi/spec/feature-search-query-parameters.ts +++ b/src/lib/openapi/spec/feature-search-query-parameters.ts @@ -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 >;