From 432aed30340c8add25040231bb56fe491fd00d78 Mon Sep 17 00:00:00 2001 From: Jaanus Sellin Date: Wed, 22 Nov 2023 15:06:07 +0200 Subject: [PATCH] feat: support multiple terms in search, remove tag support in search (#5395) 1. Removing tag support in search 2. Adding multi keyword support for search --- .../feature-search-controller.ts | 10 ++- .../feature-search/feature.search.e2e.test.ts | 72 ++++++++----------- .../feature-toggle-strategies-store.ts | 62 +++++----------- .../feature-toggle-strategies-store-type.ts | 2 +- 4 files changed, 56 insertions(+), 90 deletions(-) diff --git a/src/lib/features/feature-search/feature-search-controller.ts b/src/lib/features/feature-search/feature-search-controller.ts index b63b44bac2..4ec8b741b9 100644 --- a/src/lib/features/feature-search/feature-search-controller.ts +++ b/src/lib/features/feature-search/feature-search-controller.ts @@ -84,7 +84,13 @@ export default class FeatureSearchController extends Controller { favoritesFirst, } = req.query; const userId = req.user.id; - const normalizedTag = tag?.map((tag) => tag.split(':')); + const normalizedQuery = query + ?.split(',') + .map((query) => query.trim()) + .filter((query) => query); + const normalizedTag = tag + ?.map((tag) => tag.split(':')) + .filter((tag) => tag.length === 2); const normalizedStatus = status ?.map((tag) => tag.split(':')) .filter( @@ -100,7 +106,7 @@ export default class FeatureSearchController extends Controller { sortOrder === 'asc' || sortOrder === 'desc' ? sortOrder : 'asc'; const normalizedFavoritesFirst = favoritesFirst === 'true'; const { features, total } = await this.featureSearchService.search({ - query, + queryParams: normalizedQuery, projectId, type, userId, 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 2097c89f97..25ee75a693 100644 --- a/src/lib/features/feature-search/feature.search.e2e.test.ts +++ b/src/lib/features/feature-search/feature.search.e2e.test.ts @@ -210,50 +210,6 @@ test('should filter features by environment status', async () => { }); }); -test('should filter by partial 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', - }); - - const { body } = await filterFeaturesByTag(['simple']); - - expect(body).toMatchObject({ - features: [{ name: 'my_feature_a' }], - }); -}); - -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', - }); - - const { body: fullMatch } = await searchFeatures({ - query: 'simple:my_tag', - }); - const { body: tagTypeMatch } = await searchFeatures({ query: 'simple' }); - const { body: tagValueMatch } = await searchFeatures({ query: 'my_tag' }); - const { body: partialTagMatch } = await searchFeatures({ query: 'e:m' }); - - expect(fullMatch).toMatchObject({ - features: [{ name: 'my_feature_a' }], - }); - expect(tagTypeMatch).toMatchObject({ - features: [{ name: 'my_feature_a' }], - }); - expect(tagValueMatch).toMatchObject({ - features: [{ name: 'my_feature_a' }], - }); - expect(partialTagMatch).toMatchObject({ - features: [{ name: 'my_feature_a' }], - }); -}); - test('should return all feature tags', async () => { await app.createFeature('my_feature_a'); await app.addTag('my_feature_a', { @@ -500,3 +456,31 @@ test('should search features by description', async () => { features: [{ name: 'my_feature_b', description }], }); }); + +test('should support multiple search values', async () => { + const description = 'secretdescription'; + await app.createFeature('my_feature_a'); + await app.createFeature({ name: 'my_feature_b', description }); + await app.createFeature('my_feature_c'); + + const { body } = await searchFeatures({ + query: 'descr,c', + }); + expect(body).toMatchObject({ + features: [ + { name: 'my_feature_b', description }, + { name: 'my_feature_c' }, + ], + }); + + const { body: emptyQuery } = await searchFeatures({ + query: ' , ', + }); + expect(emptyQuery).toMatchObject({ + features: [ + { name: 'my_feature_a' }, + { name: 'my_feature_b' }, + { name: 'my_feature_c' }, + ], + }); +}); 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 e234d0e2a2..5cb03de1ec 100644 --- a/src/lib/features/feature-toggle/feature-toggle-strategies-store.ts +++ b/src/lib/features/feature-toggle/feature-toggle-strategies-store.ts @@ -529,7 +529,7 @@ class FeatureStrategiesStore implements IFeatureStrategiesStore { async searchFeatures({ projectId, userId, - query: queryString, + queryParams, type, tag, status, @@ -542,9 +542,6 @@ class FeatureStrategiesStore implements IFeatureStrategiesStore { features: IFeatureOverview[]; total: number; }> { - const normalizedFullTag = tag?.filter((tag) => tag.length === 2); - const normalizedHalfTag = tag?.filter((tag) => tag.length === 1).flat(); - const validatedSortOrder = sortOrder === 'asc' || sortOrder === 'desc' ? sortOrder : 'asc'; @@ -554,54 +551,33 @@ class FeatureStrategiesStore implements IFeatureStrategiesStore { if (projectId) { query.where({ project: projectId }); } - const hasQueryString = Boolean(queryString?.trim()); - const hasHalfTag = - normalizedHalfTag && normalizedHalfTag.length > 0; - if (hasQueryString || hasHalfTag) { - const tagQuery = this.db - .from('feature_tag') - .select('feature_name'); - // todo: we can run a cheaper query when no colon is detected - if (hasQueryString) { - tagQuery.whereRaw("(?? || ':' || ??) ILIKE ?", [ - 'tag_type', - 'tag_value', - `%${queryString}%`, - ]); - } - if (hasHalfTag) { - const tagParameters = normalizedHalfTag.map( - (tag) => `%${tag}%`, - ); - const tagQueryParameters = normalizedHalfTag - .map(() => '?') - .join(','); - tagQuery - .orWhereRaw( - `(??) ILIKE ANY (ARRAY[${tagQueryParameters}])`, - ['tag_type', ...tagParameters], - ) - .orWhereRaw( - `(??) ILIKE ANY (ARRAY[${tagQueryParameters}])`, - ['tag_value', ...tagParameters], - ); - } + const hasQueryString = queryParams?.length; + + if (hasQueryString) { + const sqlParameters = queryParams.map( + (item) => `%${item}%`, + ); + const sqlQueryParameters = sqlParameters + .map(() => '?') + .join(','); query.where((builder) => { builder - .whereILike('features.name', `%${queryString}%`) - .orWhereILike( - 'features.description', - `%${queryString}%`, + .orWhereRaw( + `(??) ILIKE ANY (ARRAY[${sqlQueryParameters}])`, + ['features.name', ...sqlParameters], ) - .orWhereIn('features.name', tagQuery); + .orWhereRaw( + `(??) ILIKE ANY (ARRAY[${sqlQueryParameters}])`, + ['features.description', ...sqlParameters], + ); }); } - if (normalizedFullTag && normalizedFullTag.length > 0) { + if (tag && tag.length > 0) { const tagQuery = this.db .from('feature_tag') .select('feature_name') - .whereIn(['tag_type', 'tag_value'], normalizedFullTag); + .whereIn(['tag_type', 'tag_value'], tag); query.whereIn('features.name', tagQuery); } if (type) { 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 331c9b40c5..29e4f8aaec 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 @@ -23,7 +23,7 @@ export interface FeatureConfigurationClient { export interface IFeatureSearchParams { userId: number; - query?: string; + queryParams?: string[]; projectId?: string; type?: string[]; tag?: string[][];