diff --git a/src/lib/features/feature-search/feature-search-controller.ts b/src/lib/features/feature-search/feature-search-controller.ts index a8a034fadafa..ed2daf30b445 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, + createdAt, state, status, offset, @@ -112,6 +113,7 @@ export default class FeatureSearchController extends Controller { tag, segment, state, + createdAt, 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 b4e8206ebd5f..c7f5ab822237 100644 --- a/src/lib/features/feature-search/feature-search-service.ts +++ b/src/lib/features/feature-search/feature-search-service.ts @@ -43,7 +43,7 @@ export class FeatureSearchService { parseOperatorValue = (field: string, value: string): IQueryParam | null => { const pattern = - /^(IS|IS_NOT|IS_ANY_OF|IS_NOT_ANY_OF|INCLUDE|DO_NOT_INCLUDE|INCLUDE_ALL_OF|INCLUDE_ANY_OF|EXCLUDE_IF_ANY_OF|EXCLUDE_ALL):(.+)$/; + /^(IS|IS_NOT|IS_ANY_OF|IS_NOT_ANY_OF|INCLUDE|DO_NOT_INCLUDE|INCLUDE_ALL_OF|INCLUDE_ANY_OF|EXCLUDE_IF_ANY_OF|EXCLUDE_ALL|IS_BEFORE|IS_ON_OR_AFTER):(.+)$/; const match = value.match(pattern); if (match) { @@ -70,6 +70,14 @@ export class FeatureSearchService { } } + if (params.createdAt) { + const parsed = this.parseOperatorValue( + 'features.created_at', + params.createdAt, + ); + if (parsed) queryParams.push(parsed); + } + ['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 81f31d4431c6..e2820ab93bd6 100644 --- a/src/lib/features/feature-search/feature.search.e2e.test.ts +++ b/src/lib/features/feature-search/feature.search.e2e.test.ts @@ -126,6 +126,15 @@ const filterFeaturesByState = async (state: string, expectedCode = 200) => { .expect(expectedCode); }; +const filterFeaturesByCreated = async ( + createdAt: string, + expectedCode = 200, +) => { + return app.request + .get(`/api/admin/search/features?createdAt=${createdAt}`) + .expect(expectedCode); +}; + const filterFeaturesByEnvironmentStatus = async ( environmentStatuses: string[], expectedCode = 200, @@ -796,3 +805,28 @@ test('should search features by state with operators', async () => { features: [], }); }); + +test('should search features by created date with operators', async () => { + await app.createFeature({ + name: 'my_feature_a', + createdAt: '2023-01-27T15:21:39.975Z', + }); + await app.createFeature({ + name: 'my_feature_b', + createdAt: '2023-01-29T15:21:39.975Z', + }); + + const { body } = await filterFeaturesByCreated( + 'IS_BEFORE:2023-01-28T15:21:39.975Z', + ); + expect(body).toMatchObject({ + features: [{ name: 'my_feature_a' }], + }); + + const { body: afterBody } = await filterFeaturesByCreated( + 'IS_ON_OR_AFTER:2023-01-28T15:21:39.975Z', + ); + expect(afterBody).toMatchObject({ + features: [{ name: 'my_feature_b' }], + }); +}); 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 d41a57a8bb0c..fcf9551e7c0f 100644 --- a/src/lib/features/feature-toggle/feature-toggle-strategies-store.ts +++ b/src/lib/features/feature-toggle/feature-toggle-strategies-store.ts @@ -1131,6 +1131,12 @@ const applyGenericQueryParams = ( case 'IS_NOT_ANY_OF': query.whereNotIn(param.field, param.values); break; + case 'IS_BEFORE': + query.where(param.field, '<', param.values[0]); + break; + case 'IS_ON_OR_AFTER': + query.where(param.field, '>=', param.values[0]); + break; } }); }; 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 b2db9c69bb43..ee7691e01f56 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; + createdAt?: string; state?: string; type?: string[]; tag?: string; @@ -47,7 +48,9 @@ export type IQueryOperator = | 'INCLUDE_ALL_OF' | 'INCLUDE_ANY_OF' | 'EXCLUDE_IF_ANY_OF' - | 'EXCLUDE_ALL'; + | 'EXCLUDE_ALL' + | 'IS_BEFORE' + | 'IS_ON_OR_AFTER'; export interface IQueryParam { field: string; @@ -60,53 +63,71 @@ export interface IFeatureStrategiesStore createStrategyFeatureEnv( strategyConfig: Omit, ): Promise; + removeAllStrategiesForFeatureEnv( featureName: string, environment: string, ): Promise; + getStrategiesForFeatureEnv( projectId: string, featureName: string, environment: string, ): Promise; + getFeatureToggleWithEnvs( featureName: string, userId?: number, archived?: boolean, ): Promise; + getFeatureToggleWithVariantEnvs( featureName: string, userId?: number, archived?, ): Promise; + getFeatureOverview( params: IFeatureProjectUserParams, ): Promise; + searchFeatures( params: IFeatureSearchParams, queryParams: IQueryParam[], - ): Promise<{ features: IFeatureOverview[]; total: number }>; + ): Promise<{ + features: IFeatureOverview[]; + total: number; + }>; + getStrategyById(id: string): Promise; + updateStrategy( id: string, updates: Partial, ): Promise; + deleteConfigurationsForProjectAndEnvironment( projectId: String, environment: String, ): Promise; + setProjectForStrategiesBelongingToFeature( featureName: string, newProjectId: string, ): Promise; + getStrategiesBySegment(segmentId: number): Promise; + getStrategiesByContextField( contextFieldName: string, ): Promise; + updateSortOrder(id: string, sortOrder: number): Promise; + getAllByFeatures( features: string[], environment?: string, ): Promise; + getCustomStrategiesInUseCount(): Promise; } diff --git a/src/lib/openapi/spec/feature-search-query-parameters.ts b/src/lib/openapi/spec/feature-search-query-parameters.ts index 883d99601dd6..442c487894f0 100644 --- a/src/lib/openapi/spec/feature-search-query-parameters.ts +++ b/src/lib/openapi/spec/feature-search-query-parameters.ts @@ -18,7 +18,8 @@ export const featureSearchQueryParameters = [ pattern: '^(IS|IS_NOT|IS_ANY_OF|IS_NOT_ANY_OF):(.*?)(,([a-zA-Z0-9_]+))*$', }, - description: 'Id of the project where search and filter is performed', + description: + 'Id of the project where search and filter is performed. The project id can be specified with an operator. The supported operators are IS, IS_NOT, IS_ANY_OF, IS_NOT_ANY_OF.', in: 'query', }, { @@ -29,7 +30,8 @@ export const featureSearchQueryParameters = [ pattern: '^(IS|IS_NOT|IS_ANY_OF|IS_NOT_ANY_OF):(.*?)(,([a-zA-Z0-9_]+))*$', }, - description: 'The state of the feature active/stale', + description: + 'The state of the feature active/stale. The state can be specified with an operator. The supported operators are IS, IS_NOT, IS_ANY_OF, IS_NOT_ANY_OF.', in: 'query', }, { @@ -64,7 +66,8 @@ export const featureSearchQueryParameters = [ '^(INCLUDE|DO_NOT_INCLUDE|INCLUDE_ALL_OF|INCLUDE_ANY_OF|EXCLUDE_IF_ANY_OF|EXCLUDE_ALL):(.*?)(,([a-zA-Z0-9_]+))*$', example: 'INCLUDE:pro-users', }, - description: 'The list of segments with operators to filter by.', + description: + 'The list of segments with operators to filter by. The segment valid operators are INCLUDE, DO_NOT_INCLUDE, INCLUDE_ALL_OF, INCLUDE_ANY_OF, EXCLUDE_IF_ANY_OF, EXCLUDE_ALL.', in: 'query', }, { @@ -130,6 +133,17 @@ export const featureSearchQueryParameters = [ 'The flag to indicate if the favorite features should be returned first. By default it is set to false.', in: 'query', }, + { + name: 'createdAt', + schema: { + type: 'string', + example: 'IS_ON_OR_AFTER:2023-01-28T15:21:39.975Z', + pattern: '^(IS_BEFORE|IS_ON_OR_AFTER):(.*?)(,([a-zA-Z0-9_]+))*$', + }, + description: + 'The date the feature was created. The date can be specified with an operator. The supported operators are IS_BEFORE, IS_ON_OR_AFTER.', + in: 'query', + }, ] as const; export type FeatureSearchQueryParameters = Partial<