mirror of
https://github.com/Unleash/unleash.git
synced 2025-07-26 13:48:33 +02:00
feat: operators for segments (#5485)
1. Added way to filter segments 2. Refactored some code, so tags and segments use same SQL methods.
This commit is contained in:
parent
75aecfca07
commit
5fd1c16def
@ -73,9 +73,10 @@ export default class FeatureSearchController extends Controller {
|
|||||||
if (this.config.flagResolver.isEnabled('featureSearchAPI')) {
|
if (this.config.flagResolver.isEnabled('featureSearchAPI')) {
|
||||||
const {
|
const {
|
||||||
query,
|
query,
|
||||||
projectId,
|
project,
|
||||||
type,
|
type,
|
||||||
tag,
|
tag,
|
||||||
|
segment,
|
||||||
status,
|
status,
|
||||||
offset,
|
offset,
|
||||||
limit = '50',
|
limit = '50',
|
||||||
@ -104,10 +105,11 @@ export default class FeatureSearchController extends Controller {
|
|||||||
const normalizedFavoritesFirst = favoritesFirst === 'true';
|
const normalizedFavoritesFirst = favoritesFirst === 'true';
|
||||||
const { features, total } = await this.featureSearchService.search({
|
const { features, total } = await this.featureSearchService.search({
|
||||||
searchParams: normalizedQuery,
|
searchParams: normalizedQuery,
|
||||||
projectId,
|
project,
|
||||||
type,
|
type,
|
||||||
userId,
|
userId,
|
||||||
tag,
|
tag,
|
||||||
|
segment,
|
||||||
status: normalizedStatus,
|
status: normalizedStatus,
|
||||||
offset: normalizedOffset,
|
offset: normalizedOffset,
|
||||||
limit: normalizedLimit,
|
limit: normalizedLimit,
|
||||||
|
@ -60,17 +60,10 @@ export class FeatureSearchService {
|
|||||||
convertToQueryParams = (params: IFeatureSearchParams): IQueryParam[] => {
|
convertToQueryParams = (params: IFeatureSearchParams): IQueryParam[] => {
|
||||||
const queryParams: IQueryParam[] = [];
|
const queryParams: IQueryParam[] = [];
|
||||||
|
|
||||||
if (params.projectId) {
|
['tag', 'segment', 'project'].forEach((field) => {
|
||||||
const parsed = this.parseOperatorValue('project', params.projectId);
|
|
||||||
if (parsed) queryParams.push(parsed);
|
|
||||||
}
|
|
||||||
|
|
||||||
['tag'].forEach((field) => {
|
|
||||||
if (params[field]) {
|
if (params[field]) {
|
||||||
params[field].forEach((value) => {
|
const parsed = this.parseOperatorValue(field, params[field]);
|
||||||
const parsed = this.parseOperatorValue(field, value);
|
if (parsed) queryParams.push(parsed);
|
||||||
if (parsed) queryParams.push(parsed);
|
|
||||||
});
|
|
||||||
}
|
}
|
||||||
});
|
});
|
||||||
|
|
||||||
|
@ -34,6 +34,20 @@ beforeAll(async () => {
|
|||||||
email: 'user@getunleash.io',
|
email: 'user@getunleash.io',
|
||||||
})
|
})
|
||||||
.expect(200);
|
.expect(200);
|
||||||
|
|
||||||
|
await stores.environmentStore.create({
|
||||||
|
name: 'development',
|
||||||
|
type: 'development',
|
||||||
|
});
|
||||||
|
|
||||||
|
await app.linkProjectToEnvironment('default', 'development');
|
||||||
|
|
||||||
|
await stores.environmentStore.create({
|
||||||
|
name: 'production',
|
||||||
|
type: 'production',
|
||||||
|
});
|
||||||
|
|
||||||
|
await app.linkProjectToEnvironment('default', 'production');
|
||||||
});
|
});
|
||||||
|
|
||||||
afterAll(async () => {
|
afterAll(async () => {
|
||||||
@ -43,14 +57,15 @@ afterAll(async () => {
|
|||||||
|
|
||||||
beforeEach(async () => {
|
beforeEach(async () => {
|
||||||
await db.stores.featureToggleStore.deleteAll();
|
await db.stores.featureToggleStore.deleteAll();
|
||||||
|
await db.stores.segmentStore.deleteAll();
|
||||||
});
|
});
|
||||||
|
|
||||||
const searchFeatures = async (
|
const searchFeatures = async (
|
||||||
{ query = '', projectId = 'IS:default' }: FeatureSearchQueryParameters,
|
{ query = '', project = 'IS:default' }: FeatureSearchQueryParameters,
|
||||||
expectedCode = 200,
|
expectedCode = 200,
|
||||||
) => {
|
) => {
|
||||||
return app.request
|
return app.request
|
||||||
.get(`/api/admin/search/features?query=${query}&projectId=${projectId}`)
|
.get(`/api/admin/search/features?query=${query}&project=${project}`)
|
||||||
.expect(expectedCode);
|
.expect(expectedCode);
|
||||||
};
|
};
|
||||||
|
|
||||||
@ -58,14 +73,14 @@ const sortFeatures = async (
|
|||||||
{
|
{
|
||||||
sortBy = '',
|
sortBy = '',
|
||||||
sortOrder = '',
|
sortOrder = '',
|
||||||
projectId = 'default',
|
project = 'default',
|
||||||
favoritesFirst = 'false',
|
favoritesFirst = 'false',
|
||||||
}: FeatureSearchQueryParameters,
|
}: FeatureSearchQueryParameters,
|
||||||
expectedCode = 200,
|
expectedCode = 200,
|
||||||
) => {
|
) => {
|
||||||
return app.request
|
return app.request
|
||||||
.get(
|
.get(
|
||||||
`/api/admin/search/features?sortBy=${sortBy}&sortOrder=${sortOrder}&projectId=IS:${projectId}&favoritesFirst=${favoritesFirst}`,
|
`/api/admin/search/features?sortBy=${sortBy}&sortOrder=${sortOrder}&project=IS:${project}&favoritesFirst=${favoritesFirst}`,
|
||||||
)
|
)
|
||||||
.expect(expectedCode);
|
.expect(expectedCode);
|
||||||
};
|
};
|
||||||
@ -73,7 +88,7 @@ const sortFeatures = async (
|
|||||||
const searchFeaturesWithOffset = async (
|
const searchFeaturesWithOffset = async (
|
||||||
{
|
{
|
||||||
query = '',
|
query = '',
|
||||||
projectId = 'default',
|
project = 'default',
|
||||||
offset = '0',
|
offset = '0',
|
||||||
limit = '10',
|
limit = '10',
|
||||||
}: FeatureSearchQueryParameters,
|
}: FeatureSearchQueryParameters,
|
||||||
@ -81,7 +96,7 @@ const searchFeaturesWithOffset = async (
|
|||||||
) => {
|
) => {
|
||||||
return app.request
|
return app.request
|
||||||
.get(
|
.get(
|
||||||
`/api/admin/search/features?query=${query}&projectId=IS:${projectId}&offset=${offset}&limit=${limit}`,
|
`/api/admin/search/features?query=${query}&project=IS:${project}&offset=${offset}&limit=${limit}`,
|
||||||
)
|
)
|
||||||
.expect(expectedCode);
|
.expect(expectedCode);
|
||||||
};
|
};
|
||||||
@ -93,10 +108,15 @@ const filterFeaturesByType = async (types: string[], expectedCode = 200) => {
|
|||||||
.expect(expectedCode);
|
.expect(expectedCode);
|
||||||
};
|
};
|
||||||
|
|
||||||
const filterFeaturesByTag = async (tags: string[], expectedCode = 200) => {
|
const filterFeaturesByTag = async (tag: string, expectedCode = 200) => {
|
||||||
const tagParams = tags.map((tag) => `tag[]=${tag}`).join('&');
|
|
||||||
return app.request
|
return app.request
|
||||||
.get(`/api/admin/search/features?${tagParams}`)
|
.get(`/api/admin/search/features?tag=${tag}`)
|
||||||
|
.expect(expectedCode);
|
||||||
|
};
|
||||||
|
|
||||||
|
const filterFeaturesBySegment = async (segment: string, expectedCode = 200) => {
|
||||||
|
return app.request
|
||||||
|
.get(`/api/admin/search/features?segment=${segment}`)
|
||||||
.expect(expectedCode);
|
.expect(expectedCode);
|
||||||
};
|
};
|
||||||
|
|
||||||
@ -202,31 +222,31 @@ test('should filter features by tag', async () => {
|
|||||||
value: 'my_tag',
|
value: 'my_tag',
|
||||||
});
|
});
|
||||||
|
|
||||||
const { body } = await filterFeaturesByTag(['INCLUDE:simple:my_tag']);
|
const { body } = await filterFeaturesByTag('INCLUDE:simple:my_tag');
|
||||||
|
|
||||||
expect(body).toMatchObject({
|
expect(body).toMatchObject({
|
||||||
features: [{ name: 'my_feature_a' }, { name: 'my_feature_d' }],
|
features: [{ name: 'my_feature_a' }, { name: 'my_feature_d' }],
|
||||||
});
|
});
|
||||||
|
|
||||||
const { body: notIncludeBody } = await filterFeaturesByTag([
|
const { body: notIncludeBody } = await filterFeaturesByTag(
|
||||||
'DO_NOT_INCLUDE:simple:my_tag',
|
'DO_NOT_INCLUDE:simple:my_tag',
|
||||||
]);
|
);
|
||||||
|
|
||||||
expect(notIncludeBody).toMatchObject({
|
expect(notIncludeBody).toMatchObject({
|
||||||
features: [{ name: 'my_feature_b' }, { name: 'my_feature_c' }],
|
features: [{ name: 'my_feature_b' }, { name: 'my_feature_c' }],
|
||||||
});
|
});
|
||||||
|
|
||||||
const { body: includeAllOf } = await filterFeaturesByTag([
|
const { body: includeAllOf } = await filterFeaturesByTag(
|
||||||
'INCLUDE_ALL_OF:simple:my_tag, simple:tag_c',
|
'INCLUDE_ALL_OF:simple:my_tag, simple:tag_c',
|
||||||
]);
|
);
|
||||||
|
|
||||||
expect(includeAllOf).toMatchObject({
|
expect(includeAllOf).toMatchObject({
|
||||||
features: [{ name: 'my_feature_d' }],
|
features: [{ name: 'my_feature_d' }],
|
||||||
});
|
});
|
||||||
|
|
||||||
const { body: includeAnyOf } = await filterFeaturesByTag([
|
const { body: includeAnyOf } = await filterFeaturesByTag(
|
||||||
'INCLUDE_ANY_OF:simple:my_tag, simple:tag_c',
|
'INCLUDE_ANY_OF:simple:my_tag, simple:tag_c',
|
||||||
]);
|
);
|
||||||
|
|
||||||
expect(includeAnyOf).toMatchObject({
|
expect(includeAnyOf).toMatchObject({
|
||||||
features: [
|
features: [
|
||||||
@ -236,17 +256,17 @@ test('should filter features by tag', async () => {
|
|||||||
],
|
],
|
||||||
});
|
});
|
||||||
|
|
||||||
const { body: excludeIfAnyOf } = await filterFeaturesByTag([
|
const { body: excludeIfAnyOf } = await filterFeaturesByTag(
|
||||||
'EXCLUDE_IF_ANY_OF:simple:my_tag, simple:tag_c',
|
'EXCLUDE_IF_ANY_OF:simple:my_tag, simple:tag_c',
|
||||||
]);
|
);
|
||||||
|
|
||||||
expect(excludeIfAnyOf).toMatchObject({
|
expect(excludeIfAnyOf).toMatchObject({
|
||||||
features: [{ name: 'my_feature_b' }],
|
features: [{ name: 'my_feature_b' }],
|
||||||
});
|
});
|
||||||
|
|
||||||
const { body: excludeAll } = await filterFeaturesByTag([
|
const { body: excludeAll } = await filterFeaturesByTag(
|
||||||
'EXCLUDE_ALL:simple:my_tag, simple:tag_c',
|
'EXCLUDE_ALL:simple:my_tag, simple:tag_c',
|
||||||
]);
|
);
|
||||||
|
|
||||||
expect(excludeAll).toMatchObject({
|
expect(excludeAll).toMatchObject({
|
||||||
features: [
|
features: [
|
||||||
@ -316,7 +336,7 @@ test('should not search features from another project', async () => {
|
|||||||
|
|
||||||
const { body } = await searchFeatures({
|
const { body } = await searchFeatures({
|
||||||
query: '',
|
query: '',
|
||||||
projectId: 'IS:another_project',
|
project: 'IS:another_project',
|
||||||
});
|
});
|
||||||
|
|
||||||
expect(body).toMatchObject({ features: [] });
|
expect(body).toMatchObject({ features: [] });
|
||||||
@ -468,13 +488,6 @@ test('should not return duplicate entries when sorting by last seen', async () =
|
|||||||
await app.createFeature('my_feature_a');
|
await app.createFeature('my_feature_a');
|
||||||
await app.createFeature('my_feature_b');
|
await app.createFeature('my_feature_b');
|
||||||
await app.createFeature('my_feature_c');
|
await app.createFeature('my_feature_c');
|
||||||
|
|
||||||
await stores.environmentStore.create({
|
|
||||||
name: 'production',
|
|
||||||
type: 'development',
|
|
||||||
});
|
|
||||||
|
|
||||||
await app.linkProjectToEnvironment('default', 'production');
|
|
||||||
await app.enableFeature('my_feature_a', 'production');
|
await app.enableFeature('my_feature_a', 'production');
|
||||||
await app.enableFeature('my_feature_b', 'production');
|
await app.enableFeature('my_feature_b', 'production');
|
||||||
|
|
||||||
@ -586,28 +599,28 @@ test('should search features by project with operators', async () => {
|
|||||||
});
|
});
|
||||||
|
|
||||||
const { body } = await searchFeatures({
|
const { body } = await searchFeatures({
|
||||||
projectId: 'IS:default',
|
project: 'IS:default',
|
||||||
});
|
});
|
||||||
expect(body).toMatchObject({
|
expect(body).toMatchObject({
|
||||||
features: [{ name: 'my_feature_a' }],
|
features: [{ name: 'my_feature_a' }],
|
||||||
});
|
});
|
||||||
|
|
||||||
const { body: isNotBody } = await searchFeatures({
|
const { body: isNotBody } = await searchFeatures({
|
||||||
projectId: 'IS_NOT:default',
|
project: 'IS_NOT:default',
|
||||||
});
|
});
|
||||||
expect(isNotBody).toMatchObject({
|
expect(isNotBody).toMatchObject({
|
||||||
features: [{ name: 'my_feature_b' }, { name: 'my_feature_c' }],
|
features: [{ name: 'my_feature_b' }, { name: 'my_feature_c' }],
|
||||||
});
|
});
|
||||||
|
|
||||||
const { body: isAnyOfBody } = await searchFeatures({
|
const { body: isAnyOfBody } = await searchFeatures({
|
||||||
projectId: 'IS_ANY_OF:default,project_c',
|
project: 'IS_ANY_OF:default,project_c',
|
||||||
});
|
});
|
||||||
expect(isAnyOfBody).toMatchObject({
|
expect(isAnyOfBody).toMatchObject({
|
||||||
features: [{ name: 'my_feature_a' }, { name: 'my_feature_c' }],
|
features: [{ name: 'my_feature_a' }, { name: 'my_feature_c' }],
|
||||||
});
|
});
|
||||||
|
|
||||||
const { body: isNotAnyBody } = await searchFeatures({
|
const { body: isNotAnyBody } = await searchFeatures({
|
||||||
projectId: 'IS_NOT_ANY_OF:default,project_c',
|
project: 'IS_NOT_ANY_OF:default,project_c',
|
||||||
});
|
});
|
||||||
expect(isNotAnyBody).toMatchObject({
|
expect(isNotAnyBody).toMatchObject({
|
||||||
features: [{ name: 'my_feature_b' }],
|
features: [{ name: 'my_feature_b' }],
|
||||||
@ -620,14 +633,6 @@ test('should return segments in payload with no duplicates/nulls', async () => {
|
|||||||
name: 'my_segment_a',
|
name: 'my_segment_a',
|
||||||
constraints: [],
|
constraints: [],
|
||||||
});
|
});
|
||||||
|
|
||||||
await stores.environmentStore.create({
|
|
||||||
name: 'development',
|
|
||||||
type: 'development',
|
|
||||||
});
|
|
||||||
|
|
||||||
await app.linkProjectToEnvironment('default', 'development');
|
|
||||||
await app.enableFeature('my_feature_a', 'development');
|
|
||||||
await app.addStrategyToFeatureEnv(
|
await app.addStrategyToFeatureEnv(
|
||||||
{
|
{
|
||||||
name: 'default',
|
name: 'default',
|
||||||
@ -636,6 +641,7 @@ test('should return segments in payload with no duplicates/nulls', async () => {
|
|||||||
DEFAULT_ENV,
|
DEFAULT_ENV,
|
||||||
'my_feature_a',
|
'my_feature_a',
|
||||||
);
|
);
|
||||||
|
await app.enableFeature('my_feature_a', 'development');
|
||||||
|
|
||||||
const { body } = await searchFeatures({});
|
const { body } = await searchFeatures({});
|
||||||
|
|
||||||
@ -648,3 +654,104 @@ test('should return segments in payload with no duplicates/nulls', async () => {
|
|||||||
],
|
],
|
||||||
});
|
});
|
||||||
});
|
});
|
||||||
|
|
||||||
|
test('should filter features by segment', async () => {
|
||||||
|
await app.createFeature('my_feature_a');
|
||||||
|
const { body: mySegmentA } = await app.createSegment({
|
||||||
|
name: 'my_segment_a',
|
||||||
|
constraints: [],
|
||||||
|
});
|
||||||
|
await app.addStrategyToFeatureEnv(
|
||||||
|
{
|
||||||
|
name: 'default',
|
||||||
|
segments: [mySegmentA.id],
|
||||||
|
},
|
||||||
|
DEFAULT_ENV,
|
||||||
|
'my_feature_a',
|
||||||
|
);
|
||||||
|
await app.createFeature('my_feature_b');
|
||||||
|
await app.createFeature('my_feature_c');
|
||||||
|
const { body: mySegmentC } = await app.createSegment({
|
||||||
|
name: 'my_segment_c',
|
||||||
|
constraints: [],
|
||||||
|
});
|
||||||
|
await app.addStrategyToFeatureEnv(
|
||||||
|
{
|
||||||
|
name: 'default',
|
||||||
|
segments: [mySegmentC.id],
|
||||||
|
},
|
||||||
|
DEFAULT_ENV,
|
||||||
|
'my_feature_c',
|
||||||
|
);
|
||||||
|
await app.createFeature('my_feature_d');
|
||||||
|
await app.addStrategyToFeatureEnv(
|
||||||
|
{
|
||||||
|
name: 'default',
|
||||||
|
segments: [mySegmentC.id],
|
||||||
|
},
|
||||||
|
DEFAULT_ENV,
|
||||||
|
'my_feature_d',
|
||||||
|
);
|
||||||
|
await app.addStrategyToFeatureEnv(
|
||||||
|
{
|
||||||
|
name: 'default',
|
||||||
|
segments: [mySegmentA.id],
|
||||||
|
},
|
||||||
|
DEFAULT_ENV,
|
||||||
|
'my_feature_d',
|
||||||
|
);
|
||||||
|
|
||||||
|
const { body } = await filterFeaturesBySegment('INCLUDE:my_segment_a');
|
||||||
|
|
||||||
|
expect(body).toMatchObject({
|
||||||
|
features: [{ name: 'my_feature_a' }, { name: 'my_feature_d' }],
|
||||||
|
});
|
||||||
|
|
||||||
|
const { body: notIncludeBody } = await filterFeaturesBySegment(
|
||||||
|
'DO_NOT_INCLUDE:my_segment_a',
|
||||||
|
);
|
||||||
|
|
||||||
|
expect(notIncludeBody).toMatchObject({
|
||||||
|
features: [{ name: 'my_feature_b' }, { name: 'my_feature_c' }],
|
||||||
|
});
|
||||||
|
|
||||||
|
const { body: includeAllOf } = await filterFeaturesBySegment(
|
||||||
|
'INCLUDE_ALL_OF:my_segment_a, my_segment_c',
|
||||||
|
);
|
||||||
|
|
||||||
|
expect(includeAllOf).toMatchObject({
|
||||||
|
features: [{ name: 'my_feature_d' }],
|
||||||
|
});
|
||||||
|
|
||||||
|
const { body: includeAnyOf } = await filterFeaturesBySegment(
|
||||||
|
'INCLUDE_ANY_OF:my_segment_a, my_segment_c',
|
||||||
|
);
|
||||||
|
|
||||||
|
expect(includeAnyOf).toMatchObject({
|
||||||
|
features: [
|
||||||
|
{ name: 'my_feature_a' },
|
||||||
|
{ name: 'my_feature_c' },
|
||||||
|
{ name: 'my_feature_d' },
|
||||||
|
],
|
||||||
|
});
|
||||||
|
|
||||||
|
const { body: excludeIfAnyOf } = await filterFeaturesBySegment(
|
||||||
|
'EXCLUDE_IF_ANY_OF:my_segment_a, my_segment_c',
|
||||||
|
);
|
||||||
|
|
||||||
|
expect(excludeIfAnyOf).toMatchObject({
|
||||||
|
features: [{ name: 'my_feature_b' }],
|
||||||
|
});
|
||||||
|
|
||||||
|
const { body: excludeAll } = await filterFeaturesBySegment(
|
||||||
|
'EXCLUDE_ALL:my_segment_a, my_segment_c',
|
||||||
|
);
|
||||||
|
|
||||||
|
expect(excludeAll).toMatchObject({
|
||||||
|
features: [
|
||||||
|
{ name: 'my_feature_a' },
|
||||||
|
{ name: 'my_feature_b' },
|
||||||
|
{ name: 'my_feature_c' },
|
||||||
|
],
|
||||||
|
});
|
||||||
|
});
|
||||||
|
@ -747,7 +747,7 @@ class FeatureStrategiesStore implements IFeatureStrategiesStore {
|
|||||||
)
|
)
|
||||||
.joinRaw('CROSS JOIN total_features')
|
.joinRaw('CROSS JOIN total_features')
|
||||||
.whereBetween('final_rank', [offset + 1, offset + limit]);
|
.whereBetween('final_rank', [offset + 1, offset + limit]);
|
||||||
|
console.log(finalQuery.toQuery());
|
||||||
const rows = await finalQuery;
|
const rows = await finalQuery;
|
||||||
|
|
||||||
if (rows.length > 0) {
|
if (rows.length > 0) {
|
||||||
@ -1112,11 +1112,26 @@ const applyQueryParams = (
|
|||||||
queryParams: IQueryParam[],
|
queryParams: IQueryParam[],
|
||||||
): void => {
|
): void => {
|
||||||
const tagConditions = queryParams.filter((param) => param.field === 'tag');
|
const tagConditions = queryParams.filter((param) => param.field === 'tag');
|
||||||
|
const segmentConditions = queryParams.filter(
|
||||||
|
(param) => param.field === 'segment',
|
||||||
|
);
|
||||||
const genericConditions = queryParams.filter(
|
const genericConditions = queryParams.filter(
|
||||||
(param) => param.field !== 'tag',
|
(param) => param.field !== 'tag',
|
||||||
);
|
);
|
||||||
applyTagQueryParams(query, tagConditions);
|
|
||||||
applyGenericQueryParams(query, genericConditions);
|
applyGenericQueryParams(query, genericConditions);
|
||||||
|
|
||||||
|
applyMultiQueryParams(
|
||||||
|
query,
|
||||||
|
tagConditions,
|
||||||
|
['tag_type', 'tag_value'],
|
||||||
|
createTagBaseQuery,
|
||||||
|
);
|
||||||
|
applyMultiQueryParams(
|
||||||
|
query,
|
||||||
|
segmentConditions,
|
||||||
|
'segments.name',
|
||||||
|
createSegmentBaseQuery,
|
||||||
|
);
|
||||||
};
|
};
|
||||||
|
|
||||||
const applyGenericQueryParams = (
|
const applyGenericQueryParams = (
|
||||||
@ -1137,41 +1152,54 @@ const applyGenericQueryParams = (
|
|||||||
});
|
});
|
||||||
};
|
};
|
||||||
|
|
||||||
const applyTagQueryParams = (
|
const applyMultiQueryParams = (
|
||||||
query: Knex.QueryBuilder,
|
query: Knex.QueryBuilder,
|
||||||
queryParams: IQueryParam[],
|
queryParams: IQueryParam[],
|
||||||
|
fields: string | string[],
|
||||||
|
createBaseQuery: (
|
||||||
|
values: string[] | string[][],
|
||||||
|
) => (dbSubQuery: Knex.QueryBuilder) => Knex.QueryBuilder,
|
||||||
): void => {
|
): void => {
|
||||||
queryParams.forEach((param) => {
|
queryParams.forEach((param) => {
|
||||||
const tags = param.values.map((val) =>
|
const values = param.values.map((val) =>
|
||||||
val.split(':').map((s) => s.trim()),
|
(Array.isArray(fields) ? val.split(':') : [val]).map((s) =>
|
||||||
|
s.trim(),
|
||||||
|
),
|
||||||
);
|
);
|
||||||
|
|
||||||
const baseTagSubQuery = createTagBaseQuery(tags);
|
const baseSubQuery = createBaseQuery(values);
|
||||||
|
|
||||||
switch (param.operator) {
|
switch (param.operator) {
|
||||||
case 'INCLUDE':
|
case 'INCLUDE':
|
||||||
case 'INCLUDE_ANY_OF':
|
case 'INCLUDE_ANY_OF':
|
||||||
query.whereIn(['tag_type', 'tag_value'], tags);
|
if (Array.isArray(fields)) {
|
||||||
|
query.whereIn(fields, values);
|
||||||
|
} else {
|
||||||
|
query.whereIn(
|
||||||
|
fields,
|
||||||
|
values.map((v) => v[0]),
|
||||||
|
);
|
||||||
|
}
|
||||||
break;
|
break;
|
||||||
|
|
||||||
case 'DO_NOT_INCLUDE':
|
case 'DO_NOT_INCLUDE':
|
||||||
case 'EXCLUDE_IF_ANY_OF':
|
case 'EXCLUDE_IF_ANY_OF':
|
||||||
query.whereNotIn('features.name', baseTagSubQuery);
|
query.whereNotIn('features.name', baseSubQuery);
|
||||||
break;
|
break;
|
||||||
|
|
||||||
case 'INCLUDE_ALL_OF':
|
case 'INCLUDE_ALL_OF':
|
||||||
query.whereIn('features.name', (dbSubQuery) => {
|
query.whereIn('features.name', (dbSubQuery) => {
|
||||||
baseTagSubQuery(dbSubQuery)
|
baseSubQuery(dbSubQuery)
|
||||||
.groupBy('feature_name')
|
.groupBy('feature_name')
|
||||||
.havingRaw('COUNT(*) = ?', [tags.length]);
|
.havingRaw('COUNT(*) = ?', [values.length]);
|
||||||
});
|
});
|
||||||
break;
|
break;
|
||||||
|
|
||||||
case 'EXCLUDE_ALL':
|
case 'EXCLUDE_ALL':
|
||||||
query.whereNotIn('features.name', (dbSubQuery) => {
|
query.whereNotIn('features.name', (dbSubQuery) => {
|
||||||
baseTagSubQuery(dbSubQuery)
|
baseSubQuery(dbSubQuery)
|
||||||
.groupBy('feature_name')
|
.groupBy('feature_name')
|
||||||
.havingRaw('COUNT(*) = ?', [tags.length]);
|
.havingRaw('COUNT(*) = ?', [values.length]);
|
||||||
});
|
});
|
||||||
break;
|
break;
|
||||||
}
|
}
|
||||||
@ -1187,5 +1215,24 @@ const createTagBaseQuery = (tags: string[][]) => {
|
|||||||
};
|
};
|
||||||
};
|
};
|
||||||
|
|
||||||
|
const createSegmentBaseQuery = (segments: string[]) => {
|
||||||
|
return (dbSubQuery: Knex.QueryBuilder): Knex.QueryBuilder => {
|
||||||
|
return dbSubQuery
|
||||||
|
.from('feature_strategies')
|
||||||
|
.leftJoin(
|
||||||
|
'feature_strategy_segment',
|
||||||
|
'feature_strategy_segment.feature_strategy_id',
|
||||||
|
'feature_strategies.id',
|
||||||
|
)
|
||||||
|
.leftJoin(
|
||||||
|
'segments',
|
||||||
|
'feature_strategy_segment.segment_id',
|
||||||
|
'segments.id',
|
||||||
|
)
|
||||||
|
.select('feature_name')
|
||||||
|
.whereIn('name', segments);
|
||||||
|
};
|
||||||
|
};
|
||||||
|
|
||||||
module.exports = FeatureStrategiesStore;
|
module.exports = FeatureStrategiesStore;
|
||||||
export default FeatureStrategiesStore;
|
export default FeatureStrategiesStore;
|
||||||
|
@ -24,9 +24,10 @@ export interface FeatureConfigurationClient {
|
|||||||
export interface IFeatureSearchParams {
|
export interface IFeatureSearchParams {
|
||||||
userId: number;
|
userId: number;
|
||||||
searchParams?: string[];
|
searchParams?: string[];
|
||||||
projectId?: string;
|
project?: string;
|
||||||
|
segment?: string;
|
||||||
type?: string[];
|
type?: string[];
|
||||||
tag?: string[];
|
tag?: string;
|
||||||
status?: string[][];
|
status?: string[][];
|
||||||
offset: number;
|
offset: number;
|
||||||
favoritesFirst?: boolean;
|
favoritesFirst?: boolean;
|
||||||
|
@ -11,7 +11,7 @@ export const featureSearchQueryParameters = [
|
|||||||
in: 'query',
|
in: 'query',
|
||||||
},
|
},
|
||||||
{
|
{
|
||||||
name: 'projectId',
|
name: 'project',
|
||||||
schema: {
|
schema: {
|
||||||
type: 'string',
|
type: 'string',
|
||||||
example: 'IS:default',
|
example: 'IS:default',
|
||||||
@ -36,18 +36,26 @@ export const featureSearchQueryParameters = [
|
|||||||
{
|
{
|
||||||
name: 'tag',
|
name: 'tag',
|
||||||
schema: {
|
schema: {
|
||||||
type: 'array',
|
type: 'string',
|
||||||
items: {
|
pattern:
|
||||||
type: 'string',
|
'^(INCLUDE|DO_NOT_INCLUDE|INCLUDE_ALL_OF|INCLUDE_ANY_OF|EXCLUDE_IF_ANY_OF|EXCLUDE_ALL):(.*?)(,([a-zA-Z0-9_]+))*$',
|
||||||
pattern:
|
example: 'INCLUDE:simple:my_tag',
|
||||||
'^(INCLUDE|DO_NOT_INCLUDE|INCLUDE_ALL_OF|INCLUDE_ANY_OF|EXCLUDE_IF_ANY_OF|EXCLUDE_ALL):(.*?)(,([a-zA-Z0-9_]+))*$',
|
|
||||||
example: 'INCLUDE:simple:my_tag',
|
|
||||||
},
|
|
||||||
},
|
},
|
||||||
description:
|
description:
|
||||||
'The list of feature tags to filter by. Feature tag has to specify a type and a value joined with a colon.',
|
'The list of feature tags to filter by. Feature tag has to specify a type and a value joined with a colon.',
|
||||||
in: 'query',
|
in: 'query',
|
||||||
},
|
},
|
||||||
|
{
|
||||||
|
name: 'segment',
|
||||||
|
schema: {
|
||||||
|
type: 'string',
|
||||||
|
pattern:
|
||||||
|
'^(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.',
|
||||||
|
in: 'query',
|
||||||
|
},
|
||||||
{
|
{
|
||||||
name: 'status',
|
name: 'status',
|
||||||
schema: {
|
schema: {
|
||||||
|
Loading…
Reference in New Issue
Block a user