1
0
mirror of https://github.com/Unleash/unleash.git synced 2025-01-25 00:07:47 +01:00

feat: created date operators for search (#5513)

1. Added operators for created date
2. Added better descriptions for searchable fields
This commit is contained in:
Jaanus Sellin 2023-11-30 12:00:39 +02:00 committed by GitHub
parent 44d85c0dcd
commit feae69643c
No known key found for this signature in database
GPG Key ID: 4AEE18F83AFDEB23
6 changed files with 91 additions and 6 deletions

View File

@ -77,6 +77,7 @@ export default class FeatureSearchController extends Controller {
type, type,
tag, tag,
segment, segment,
createdAt,
state, state,
status, status,
offset, offset,
@ -112,6 +113,7 @@ export default class FeatureSearchController extends Controller {
tag, tag,
segment, segment,
state, state,
createdAt,
status: normalizedStatus, status: normalizedStatus,
offset: normalizedOffset, offset: normalizedOffset,
limit: normalizedLimit, limit: normalizedLimit,

View File

@ -43,7 +43,7 @@ export class FeatureSearchService {
parseOperatorValue = (field: string, value: string): IQueryParam | null => { parseOperatorValue = (field: string, value: string): IQueryParam | null => {
const pattern = 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); const match = value.match(pattern);
if (match) { 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) => { ['tag', 'segment', 'project'].forEach((field) => {
if (params[field]) { if (params[field]) {
const parsed = this.parseOperatorValue(field, params[field]); const parsed = this.parseOperatorValue(field, params[field]);

View File

@ -126,6 +126,15 @@ const filterFeaturesByState = async (state: string, expectedCode = 200) => {
.expect(expectedCode); .expect(expectedCode);
}; };
const filterFeaturesByCreated = async (
createdAt: string,
expectedCode = 200,
) => {
return app.request
.get(`/api/admin/search/features?createdAt=${createdAt}`)
.expect(expectedCode);
};
const filterFeaturesByEnvironmentStatus = async ( const filterFeaturesByEnvironmentStatus = async (
environmentStatuses: string[], environmentStatuses: string[],
expectedCode = 200, expectedCode = 200,
@ -796,3 +805,28 @@ test('should search features by state with operators', async () => {
features: [], 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' }],
});
});

View File

@ -1131,6 +1131,12 @@ const applyGenericQueryParams = (
case 'IS_NOT_ANY_OF': case 'IS_NOT_ANY_OF':
query.whereNotIn(param.field, param.values); query.whereNotIn(param.field, param.values);
break; 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;
} }
}); });
}; };

View File

@ -26,6 +26,7 @@ export interface IFeatureSearchParams {
searchParams?: string[]; searchParams?: string[];
project?: string; project?: string;
segment?: string; segment?: string;
createdAt?: string;
state?: string; state?: string;
type?: string[]; type?: string[];
tag?: string; tag?: string;
@ -47,7 +48,9 @@ export type IQueryOperator =
| 'INCLUDE_ALL_OF' | 'INCLUDE_ALL_OF'
| 'INCLUDE_ANY_OF' | 'INCLUDE_ANY_OF'
| 'EXCLUDE_IF_ANY_OF' | 'EXCLUDE_IF_ANY_OF'
| 'EXCLUDE_ALL'; | 'EXCLUDE_ALL'
| 'IS_BEFORE'
| 'IS_ON_OR_AFTER';
export interface IQueryParam { export interface IQueryParam {
field: string; field: string;
@ -60,53 +63,71 @@ export interface IFeatureStrategiesStore
createStrategyFeatureEnv( createStrategyFeatureEnv(
strategyConfig: Omit<IFeatureStrategy, 'id' | 'createdAt'>, strategyConfig: Omit<IFeatureStrategy, 'id' | 'createdAt'>,
): Promise<IFeatureStrategy>; ): Promise<IFeatureStrategy>;
removeAllStrategiesForFeatureEnv( removeAllStrategiesForFeatureEnv(
featureName: string, featureName: string,
environment: string, environment: string,
): Promise<void>; ): Promise<void>;
getStrategiesForFeatureEnv( getStrategiesForFeatureEnv(
projectId: string, projectId: string,
featureName: string, featureName: string,
environment: string, environment: string,
): Promise<IFeatureStrategy[]>; ): Promise<IFeatureStrategy[]>;
getFeatureToggleWithEnvs( getFeatureToggleWithEnvs(
featureName: string, featureName: string,
userId?: number, userId?: number,
archived?: boolean, archived?: boolean,
): Promise<FeatureToggleWithEnvironment>; ): Promise<FeatureToggleWithEnvironment>;
getFeatureToggleWithVariantEnvs( getFeatureToggleWithVariantEnvs(
featureName: string, featureName: string,
userId?: number, userId?: number,
archived?, archived?,
): Promise<FeatureToggleWithEnvironment>; ): Promise<FeatureToggleWithEnvironment>;
getFeatureOverview( getFeatureOverview(
params: IFeatureProjectUserParams, params: IFeatureProjectUserParams,
): Promise<IFeatureOverview[]>; ): Promise<IFeatureOverview[]>;
searchFeatures( searchFeatures(
params: IFeatureSearchParams, params: IFeatureSearchParams,
queryParams: IQueryParam[], queryParams: IQueryParam[],
): Promise<{ features: IFeatureOverview[]; total: number }>; ): Promise<{
features: IFeatureOverview[];
total: number;
}>;
getStrategyById(id: string): Promise<IFeatureStrategy>; getStrategyById(id: string): Promise<IFeatureStrategy>;
updateStrategy( updateStrategy(
id: string, id: string,
updates: Partial<IFeatureStrategy>, updates: Partial<IFeatureStrategy>,
): Promise<IFeatureStrategy>; ): Promise<IFeatureStrategy>;
deleteConfigurationsForProjectAndEnvironment( deleteConfigurationsForProjectAndEnvironment(
projectId: String, projectId: String,
environment: String, environment: String,
): Promise<void>; ): Promise<void>;
setProjectForStrategiesBelongingToFeature( setProjectForStrategiesBelongingToFeature(
featureName: string, featureName: string,
newProjectId: string, newProjectId: string,
): Promise<void>; ): Promise<void>;
getStrategiesBySegment(segmentId: number): Promise<IFeatureStrategy[]>; getStrategiesBySegment(segmentId: number): Promise<IFeatureStrategy[]>;
getStrategiesByContextField( getStrategiesByContextField(
contextFieldName: string, contextFieldName: string,
): Promise<IFeatureStrategy[]>; ): Promise<IFeatureStrategy[]>;
updateSortOrder(id: string, sortOrder: number): Promise<void>; updateSortOrder(id: string, sortOrder: number): Promise<void>;
getAllByFeatures( getAllByFeatures(
features: string[], features: string[],
environment?: string, environment?: string,
): Promise<IFeatureStrategy[]>; ): Promise<IFeatureStrategy[]>;
getCustomStrategiesInUseCount(): Promise<number>; getCustomStrategiesInUseCount(): Promise<number>;
} }

View File

@ -18,7 +18,8 @@ export const featureSearchQueryParameters = [
pattern: pattern:
'^(IS|IS_NOT|IS_ANY_OF|IS_NOT_ANY_OF):(.*?)(,([a-zA-Z0-9_]+))*$', '^(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', in: 'query',
}, },
{ {
@ -29,7 +30,8 @@ export const featureSearchQueryParameters = [
pattern: pattern:
'^(IS|IS_NOT|IS_ANY_OF|IS_NOT_ANY_OF):(.*?)(,([a-zA-Z0-9_]+))*$', '^(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', 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_]+))*$', '^(INCLUDE|DO_NOT_INCLUDE|INCLUDE_ALL_OF|INCLUDE_ANY_OF|EXCLUDE_IF_ANY_OF|EXCLUDE_ALL):(.*?)(,([a-zA-Z0-9_]+))*$',
example: 'INCLUDE:pro-users', 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', 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.', 'The flag to indicate if the favorite features should be returned first. By default it is set to false.',
in: 'query', 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; ] as const;
export type FeatureSearchQueryParameters = Partial< export type FeatureSearchQueryParameters = Partial<