1
0
mirror of https://github.com/Unleash/unleash.git synced 2025-02-09 00:18:00 +01:00

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
This commit is contained in:
Jaanus Sellin 2023-11-22 15:06:07 +02:00 committed by GitHub
parent 5414fa6663
commit 432aed3034
No known key found for this signature in database
GPG Key ID: 4AEE18F83AFDEB23
4 changed files with 56 additions and 90 deletions

View File

@ -84,7 +84,13 @@ export default class FeatureSearchController extends Controller {
favoritesFirst, favoritesFirst,
} = req.query; } = req.query;
const userId = req.user.id; 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 const normalizedStatus = status
?.map((tag) => tag.split(':')) ?.map((tag) => tag.split(':'))
.filter( .filter(
@ -100,7 +106,7 @@ export default class FeatureSearchController extends Controller {
sortOrder === 'asc' || sortOrder === 'desc' ? sortOrder : 'asc'; sortOrder === 'asc' || sortOrder === 'desc' ? sortOrder : 'asc';
const normalizedFavoritesFirst = favoritesFirst === 'true'; const normalizedFavoritesFirst = favoritesFirst === 'true';
const { features, total } = await this.featureSearchService.search({ const { features, total } = await this.featureSearchService.search({
query, queryParams: normalizedQuery,
projectId, projectId,
type, type,
userId, userId,

View File

@ -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 () => { test('should return all feature tags', async () => {
await app.createFeature('my_feature_a'); await app.createFeature('my_feature_a');
await app.addTag('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 }], 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' },
],
});
});

View File

@ -529,7 +529,7 @@ class FeatureStrategiesStore implements IFeatureStrategiesStore {
async searchFeatures({ async searchFeatures({
projectId, projectId,
userId, userId,
query: queryString, queryParams,
type, type,
tag, tag,
status, status,
@ -542,9 +542,6 @@ class FeatureStrategiesStore implements IFeatureStrategiesStore {
features: IFeatureOverview[]; features: IFeatureOverview[];
total: number; total: number;
}> { }> {
const normalizedFullTag = tag?.filter((tag) => tag.length === 2);
const normalizedHalfTag = tag?.filter((tag) => tag.length === 1).flat();
const validatedSortOrder = const validatedSortOrder =
sortOrder === 'asc' || sortOrder === 'desc' ? sortOrder : 'asc'; sortOrder === 'asc' || sortOrder === 'desc' ? sortOrder : 'asc';
@ -554,54 +551,33 @@ class FeatureStrategiesStore implements IFeatureStrategiesStore {
if (projectId) { if (projectId) {
query.where({ project: projectId }); query.where({ project: projectId });
} }
const hasQueryString = Boolean(queryString?.trim()); const hasQueryString = queryParams?.length;
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) { if (hasQueryString) {
tagQuery.whereRaw("(?? || ':' || ??) ILIKE ?", [ const sqlParameters = queryParams.map(
'tag_type', (item) => `%${item}%`,
'tag_value',
`%${queryString}%`,
]);
}
if (hasHalfTag) {
const tagParameters = normalizedHalfTag.map(
(tag) => `%${tag}%`,
); );
const tagQueryParameters = normalizedHalfTag const sqlQueryParameters = sqlParameters
.map(() => '?') .map(() => '?')
.join(','); .join(',');
tagQuery
.orWhereRaw(
`(??) ILIKE ANY (ARRAY[${tagQueryParameters}])`,
['tag_type', ...tagParameters],
)
.orWhereRaw(
`(??) ILIKE ANY (ARRAY[${tagQueryParameters}])`,
['tag_value', ...tagParameters],
);
}
query.where((builder) => { query.where((builder) => {
builder builder
.whereILike('features.name', `%${queryString}%`) .orWhereRaw(
.orWhereILike( `(??) ILIKE ANY (ARRAY[${sqlQueryParameters}])`,
'features.description', ['features.name', ...sqlParameters],
`%${queryString}%`,
) )
.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 const tagQuery = this.db
.from('feature_tag') .from('feature_tag')
.select('feature_name') .select('feature_name')
.whereIn(['tag_type', 'tag_value'], normalizedFullTag); .whereIn(['tag_type', 'tag_value'], tag);
query.whereIn('features.name', tagQuery); query.whereIn('features.name', tagQuery);
} }
if (type) { if (type) {

View File

@ -23,7 +23,7 @@ export interface FeatureConfigurationClient {
export interface IFeatureSearchParams { export interface IFeatureSearchParams {
userId: number; userId: number;
query?: string; queryParams?: string[];
projectId?: string; projectId?: string;
type?: string[]; type?: string[];
tag?: string[][]; tag?: string[][];