From a5288ae0b137747e76908d5742634e606aa02f99 Mon Sep 17 00:00:00 2001 From: Jaanus Sellin Date: Wed, 8 Nov 2023 16:05:22 +0200 Subject: [PATCH] feat: also allow searching partial tags (#5299) --- .../feature-search-controller.ts | 6 ++-- .../feature-search/feature.search.e2e.test.ts | 4 +-- .../feature-toggle-strategies-store.ts | 30 +++++++++++++------ 3 files changed, 25 insertions(+), 15 deletions(-) diff --git a/src/lib/features/feature-search/feature-search-controller.ts b/src/lib/features/feature-search/feature-search-controller.ts index b02c2f7049..98a85cf2b5 100644 --- a/src/lib/features/feature-search/feature-search-controller.ts +++ b/src/lib/features/feature-search/feature-search-controller.ts @@ -83,9 +83,7 @@ export default class FeatureSearchController extends Controller { sortBy, } = req.query; const userId = req.user.id; - const normalizedTag = tag - ?.map((tag) => tag.split(':')) - .filter((tag) => tag.length === 2); + const normalizedTag = tag?.map((tag) => tag.split(':')); const normalizedStatus = status ?.map((tag) => tag.split(':')) .filter( @@ -95,7 +93,7 @@ export default class FeatureSearchController extends Controller { ); const normalizedLimit = Number(limit) > 0 && Number(limit) <= 50 ? Number(limit) : 50; - const normalizedOffset = Number(offset) > 0 ? Number(limit) : 0; + const normalizedOffset = Number(offset) > 0 ? Number(offset) : 0; const normalizedSortBy: string = sortBy ? sortBy : 'createdAt'; const normalizedSortOrder = sortOrder === 'asc' || sortOrder === 'desc' ? sortOrder : 'asc'; 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 9e9c30d9ef..6388c23c66 100644 --- a/src/lib/features/feature-search/feature.search.e2e.test.ts +++ b/src/lib/features/feature-search/feature.search.e2e.test.ts @@ -190,7 +190,7 @@ test('should filter features by environment status', async () => { }); }); -test('filter with invalid tag should ignore filter', 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' }); @@ -198,7 +198,7 @@ test('filter with invalid tag should ignore filter', async () => { const { body } = await filterFeaturesByTag(['simple']); expect(body).toMatchObject({ - features: [{ name: 'my_feature_a' }, { name: 'my_feature_b' }], + features: [{ name: 'my_feature_a' }], }); }); 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 3ebc3c0b6d..0aa473441e 100644 --- a/src/lib/features/feature-toggle/feature-toggle-strategies-store.ts +++ b/src/lib/features/feature-toggle/feature-toggle-strategies-store.ts @@ -26,7 +26,6 @@ import { IFeatureProjectUserParams } from './feature-toggle-controller'; import { Db } from '../../db/db'; import Raw = Knex.Raw; import { IFeatureSearchParams } from './types/feature-toggle-strategies-store-type'; -import { addMilliseconds, format, formatISO, parseISO } from 'date-fns'; const COLUMNS = [ 'id', @@ -547,6 +546,9 @@ 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(); + let environmentCount = 1; if (projectId) { const rows = await this.db('project_environments') @@ -559,17 +561,27 @@ class FeatureStrategiesStore implements IFeatureStrategiesStore { if (projectId) { query = query.where({ project: projectId }); } - - if (queryString?.trim()) { + 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 - const tagQuery = this.db - .from('feature_tag') - .select('feature_name') - .whereRaw("(?? || ':' || ??) LIKE ?", [ + if (hasQueryString) { + tagQuery.whereRaw("(?? || ':' || ??) ILIKE ?", [ 'tag_type', 'tag_value', `%${queryString}%`, ]); + } + if (hasHalfTag) { + const tagParameter = normalizedHalfTag.map((tag) => `%${tag}%`); + tagQuery.orWhereRaw( + `(?? || ':' || ??) ILIKE ANY (ARRAY[${tagParameter + .map(() => '?') + .join(',')}])`, + ['tag_type', 'tag_value', ...tagParameter], + ); + } query = query.where((builder) => { builder @@ -577,11 +589,11 @@ class FeatureStrategiesStore implements IFeatureStrategiesStore { .orWhereIn('features.name', tagQuery); }); } - if (tag && tag.length > 0) { + if (normalizedFullTag && normalizedFullTag.length > 0) { const tagQuery = this.db .from('feature_tag') .select('feature_name') - .whereIn(['tag_type', 'tag_value'], tag); + .whereIn(['tag_type', 'tag_value'], normalizedFullTag); query = query.whereIn('features.name', tagQuery); } if (type) {