diff --git a/src/lib/features/feature-search/feature-search-controller.ts b/src/lib/features/feature-search/feature-search-controller.ts index 9fa153c93b..a8a034fada 100644 --- a/src/lib/features/feature-search/feature-search-controller.ts +++ b/src/lib/features/feature-search/feature-search-controller.ts @@ -77,6 +77,7 @@ export default class FeatureSearchController extends Controller { type, tag, segment, + state, status, offset, limit = '50', @@ -110,6 +111,7 @@ export default class FeatureSearchController extends Controller { userId, tag, segment, + state, status: normalizedStatus, offset: normalizedOffset, limit: normalizedLimit, diff --git a/src/lib/features/feature-search/feature-search-service.ts b/src/lib/features/feature-search/feature-search-service.ts index 8900579b01..b4e8206ebd 100644 --- a/src/lib/features/feature-search/feature-search-service.ts +++ b/src/lib/features/feature-search/feature-search-service.ts @@ -60,6 +60,16 @@ export class FeatureSearchService { convertToQueryParams = (params: IFeatureSearchParams): IQueryParam[] => { const queryParams: IQueryParam[] = []; + if (params.state) { + const parsedState = this.parseOperatorValue('stale', params.state); + if (parsedState) { + parsedState.values = parsedState.values.map((value) => + value === 'active' ? 'false' : 'true', + ); + queryParams.push(parsedState); + } + } + ['tag', 'segment', 'project'].forEach((field) => { if (params[field]) { const parsed = this.parseOperatorValue(field, params[field]); 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 c0724ec22d..81f31d4431 100644 --- a/src/lib/features/feature-search/feature.search.e2e.test.ts +++ b/src/lib/features/feature-search/feature.search.e2e.test.ts @@ -120,6 +120,12 @@ const filterFeaturesBySegment = async (segment: string, expectedCode = 200) => { .expect(expectedCode); }; +const filterFeaturesByState = async (state: string, expectedCode = 200) => { + return app.request + .get(`/api/admin/search/features?state=${state}`) + .expect(expectedCode); +}; + const filterFeaturesByEnvironmentStatus = async ( environmentStatuses: string[], expectedCode = 200, @@ -756,3 +762,37 @@ test('should filter features by segment', async () => { ], }); }); + +test('should search features by state with operators', async () => { + await app.createFeature({ name: 'my_feature_a', stale: false }); + await app.createFeature({ name: 'my_feature_b', stale: true }); + await app.createFeature({ name: 'my_feature_c', stale: true }); + + const { body } = await filterFeaturesByState('IS:active'); + expect(body).toMatchObject({ + features: [{ name: 'my_feature_a' }], + }); + + const { body: isNotBody } = await filterFeaturesByState('IS_NOT:active'); + expect(isNotBody).toMatchObject({ + features: [{ name: 'my_feature_b' }, { name: 'my_feature_c' }], + }); + + const { body: isAnyOfBody } = await filterFeaturesByState( + 'IS_ANY_OF:active, stale', + ); + expect(isAnyOfBody).toMatchObject({ + features: [ + { name: 'my_feature_a' }, + { name: 'my_feature_b' }, + { name: 'my_feature_c' }, + ], + }); + + const { body: isNotAnyBody } = await filterFeaturesByState( + 'IS_NOT_ANY_OF:active, stale', + ); + expect(isNotAnyBody).toMatchObject({ + features: [], + }); +}); 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 33db87140d..49360a9b1b 100644 --- a/src/lib/features/feature-toggle/feature-toggle-strategies-store.ts +++ b/src/lib/features/feature-toggle/feature-toggle-strategies-store.ts @@ -748,7 +748,7 @@ class FeatureStrategiesStore implements IFeatureStrategiesStore { ) .joinRaw('CROSS JOIN total_features') .whereBetween('final_rank', [offset + 1, offset + limit]); - console.log(finalQuery.toQuery()); + const rows = await finalQuery; if (rows.length > 0) { 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 20fc81730a..b2db9c69bb 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 @@ -26,6 +26,7 @@ export interface IFeatureSearchParams { searchParams?: string[]; project?: string; segment?: string; + state?: string; type?: string[]; tag?: string; status?: string[][]; diff --git a/src/lib/openapi/spec/feature-search-query-parameters.ts b/src/lib/openapi/spec/feature-search-query-parameters.ts index 9115c45f79..883d99601d 100644 --- a/src/lib/openapi/spec/feature-search-query-parameters.ts +++ b/src/lib/openapi/spec/feature-search-query-parameters.ts @@ -21,6 +21,17 @@ export const featureSearchQueryParameters = [ description: 'Id of the project where search and filter is performed', in: 'query', }, + { + name: 'state', + schema: { + type: 'string', + example: 'IS:active', + pattern: + '^(IS|IS_NOT|IS_ANY_OF|IS_NOT_ANY_OF):(.*?)(,([a-zA-Z0-9_]+))*$', + }, + description: 'The state of the feature active/stale', + in: 'query', + }, { name: 'type', schema: { diff --git a/src/test/e2e/helpers/test-helper.ts b/src/test/e2e/helpers/test-helper.ts index 580e2c0f7b..8b0da3c515 100644 --- a/src/test/e2e/helpers/test-helper.ts +++ b/src/test/e2e/helpers/test-helper.ts @@ -6,7 +6,11 @@ import { createTestConfig } from '../../config/test-config'; import { IAuthType, IUnleashConfig } from '../../../lib/types/option'; import { createServices } from '../../../lib/services'; import sessionDb from '../../../lib/middleware/session-db'; -import { DEFAULT_PROJECT, IUnleashStores } from '../../../lib/types'; +import { + DEFAULT_PROJECT, + FeatureToggleDTO, + IUnleashStores, +} from '../../../lib/types'; import { IUnleashServices } from '../../../lib/types/services'; import { Db } from '../../../lib/db/db'; import { IContextFieldDto } from 'lib/types/stores/context-field-store'; @@ -121,7 +125,7 @@ function httpApis( return request.post(url).send(postData).expect(expectStatusCode); }, createFeature: ( - feature: string | CreateFeatureSchema, + feature: string | FeatureToggleDTO, project: string = DEFAULT_PROJECT, expectedResponseCode: number = 201, ) => {