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

feat: filter features by type (#5160)

This commit is contained in:
Mateusz Kwasniewski 2023-10-26 15:29:30 +02:00 committed by GitHub
parent 05f4c22f7c
commit 0c8d0704f3
No known key found for this signature in database
GPG Key ID: 4AEE18F83AFDEB23
7 changed files with 70 additions and 28 deletions

View File

@ -71,10 +71,13 @@ export default class FeatureSearchController extends Controller {
res: Response, res: Response,
): Promise<void> { ): Promise<void> {
if (this.config.flagResolver.isEnabled('featureSearchAPI')) { if (this.config.flagResolver.isEnabled('featureSearchAPI')) {
const { query = '', projectId = '' } = req.query; const { query, projectId, type } = req.query;
const userId = req.user.id;
const features = await this.featureSearchService.search({ const features = await this.featureSearchService.search({
query, query,
projectId, projectId,
type,
userId,
}); });
res.json({ features }); res.json({ features });
} else { } else {

View File

@ -4,7 +4,7 @@ import {
IUnleashConfig, IUnleashConfig,
IUnleashStores, IUnleashStores,
} from '../../types'; } from '../../types';
import { FeatureSearchQueryParameters } from '../../openapi/spec/feature-search-query-parameters'; import { IFeatureSearchParams } from '../feature-toggle/types/feature-toggle-strategies-store-type';
export class FeatureSearchService { export class FeatureSearchService {
private featureStrategiesStore: IFeatureStrategiesStore; private featureStrategiesStore: IFeatureStrategiesStore;
@ -19,10 +19,12 @@ export class FeatureSearchService {
this.logger = getLogger('services/feature-search-service.ts'); this.logger = getLogger('services/feature-search-service.ts');
} }
async search(params: FeatureSearchQueryParameters) { async search(params: IFeatureSearchParams) {
const features = await this.featureStrategiesStore.searchFeatures({ const features = await this.featureStrategiesStore.searchFeatures({
projectId: params.projectId, projectId: params.projectId,
queryString: params.query, query: params.query,
userId: params.userId,
type: params.type,
}); });
return features; return features;

View File

@ -35,7 +35,7 @@ beforeEach(async () => {
}); });
const searchFeatures = async ( const searchFeatures = async (
{ query, projectId = 'default' }: Partial<FeatureSearchQueryParameters>, { query = '', projectId = 'default' }: FeatureSearchQueryParameters,
expectedCode = 200, expectedCode = 200,
) => { ) => {
return app.request return app.request
@ -43,11 +43,18 @@ const searchFeatures = async (
.expect(expectedCode); .expect(expectedCode);
}; };
const filterFeaturesByType = async (types: string[], expectedCode = 200) => {
const typeParams = types.map((type) => `type[]=${type}`).join('&');
return app.request
.get(`/api/admin/search/features?${typeParams}`)
.expect(expectedCode);
};
const searchFeaturesWithoutQueryParams = async (expectedCode = 200) => { const searchFeaturesWithoutQueryParams = async (expectedCode = 200) => {
return app.request.get(`/api/admin/search/features`).expect(expectedCode); return app.request.get(`/api/admin/search/features`).expect(expectedCode);
}; };
test('should return matching features by name', async () => { test('should search matching features by name', 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_feat_c'); await app.createFeature('my_feat_c');
@ -59,7 +66,21 @@ test('should return matching features by name', async () => {
}); });
}); });
test('should return matching features by tag', async () => { test('should filter features by type', async () => {
await app.createFeature({ name: 'my_feature_a', type: 'release' });
await app.createFeature({ name: 'my_feature_b', type: 'experimental' });
const { body } = await filterFeaturesByType([
'experimental',
'kill-switch',
]);
expect(body).toMatchObject({
features: [{ name: 'my_feature_b' }],
});
});
test('should search matching features by tag', 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.addTag('my_feature_a', { type: 'simple', value: 'my_tag' }); await app.addTag('my_feature_a', { type: 'simple', value: 'my_tag' });
@ -90,7 +111,7 @@ test('should return empty features', async () => {
expect(body).toMatchObject({ features: [] }); expect(body).toMatchObject({ features: [] });
}); });
test('should not return features from another project', async () => { test('should not search features from another project', 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');

View File

@ -8,7 +8,10 @@ import {
FeatureToggle, FeatureToggle,
} from '../../../types/model'; } from '../../../types/model';
import NotFoundError from '../../../error/notfound-error'; import NotFoundError from '../../../error/notfound-error';
import { IFeatureStrategiesStore } from '../types/feature-toggle-strategies-store-type'; import {
IFeatureSearchParams,
IFeatureStrategiesStore,
} from '../types/feature-toggle-strategies-store-type';
import { IFeatureProjectUserParams } from '../feature-toggle-controller'; import { IFeatureProjectUserParams } from '../feature-toggle-controller';
interface ProjectEnvironment { interface ProjectEnvironment {
@ -322,11 +325,7 @@ export default class FakeFeatureStrategiesStore
return Promise.resolve([]); return Promise.resolve([]);
} }
searchFeatures(params: { searchFeatures(params: IFeatureSearchParams): Promise<IFeatureOverview[]> {
projectId: string;
userId?: number | undefined;
queryString: string;
}): Promise<IFeatureOverview[]> {
return Promise.resolve([]); return Promise.resolve([]);
} }

View File

@ -25,6 +25,7 @@ import { ensureStringValue, mapValues } from '../../util';
import { IFeatureProjectUserParams } from './feature-toggle-controller'; import { IFeatureProjectUserParams } from './feature-toggle-controller';
import { Db } from '../../db/db'; import { Db } from '../../db/db';
import Raw = Knex.Raw; import Raw = Knex.Raw;
import { IFeatureSearchParams } from './types/feature-toggle-strategies-store-type';
const COLUMNS = [ const COLUMNS = [
'id', 'id',
@ -518,10 +519,9 @@ class FeatureStrategiesStore implements IFeatureStrategiesStore {
async searchFeatures({ async searchFeatures({
projectId, projectId,
userId, userId,
queryString, query: queryString,
}: { projectId: string; userId?: number; queryString: string }): Promise< type,
IFeatureOverview[] }: IFeatureSearchParams): Promise<IFeatureOverview[]> {
> {
let query = this.db('features'); let query = this.db('features');
if (projectId) { if (projectId) {
query = query.where({ project: projectId }); query = query.where({ project: projectId });
@ -542,6 +542,9 @@ class FeatureStrategiesStore implements IFeatureStrategiesStore {
.whereILike('features.name', `%${queryString}%`) .whereILike('features.name', `%${queryString}%`)
.orWhereIn('features.name', tagQuery); .orWhereIn('features.name', tagQuery);
} }
if (type) {
query = query.whereIn('features.type', type);
}
query = query query = query
.modify(FeatureToggleStore.filterByArchived, false) .modify(FeatureToggleStore.filterByArchived, false)
.leftJoin( .leftJoin(

View File

@ -19,6 +19,14 @@ export interface FeatureConfigurationClient {
variants: IVariant[]; variants: IVariant[];
dependencies?: IDependency[]; dependencies?: IDependency[];
} }
export interface IFeatureSearchParams {
userId: number;
query?: string;
projectId?: string;
type?: string[];
}
export interface IFeatureStrategiesStore export interface IFeatureStrategiesStore
extends Store<IFeatureStrategy, string> { extends Store<IFeatureStrategy, string> {
createStrategyFeatureEnv( createStrategyFeatureEnv(
@ -46,11 +54,7 @@ export interface IFeatureStrategiesStore
getFeatureOverview( getFeatureOverview(
params: IFeatureProjectUserParams, params: IFeatureProjectUserParams,
): Promise<IFeatureOverview[]>; ): Promise<IFeatureOverview[]>;
searchFeatures(params: { searchFeatures(params: IFeatureSearchParams): Promise<IFeatureOverview[]>;
projectId: string;
userId?: number;
queryString: string;
}): Promise<IFeatureOverview[]>;
getStrategyById(id: string): Promise<IFeatureStrategy>; getStrategyById(id: string): Promise<IFeatureStrategy>;
updateStrategy( updateStrategy(
id: string, id: string,

View File

@ -4,7 +4,6 @@ export const featureSearchQueryParameters = [
{ {
name: 'query', name: 'query',
schema: { schema: {
default: '',
type: 'string', type: 'string',
example: 'feature_a', example: 'feature_a',
}, },
@ -14,15 +13,26 @@ export const featureSearchQueryParameters = [
{ {
name: 'projectId', name: 'projectId',
schema: { schema: {
default: '',
type: 'string', type: 'string',
example: 'default', example: 'default',
}, },
description: 'Id of the project where search is performed', description: 'Id of the project where search and filter is performed',
in: 'query',
},
{
name: 'type',
schema: {
type: 'array',
items: {
type: 'string',
example: 'release',
},
},
description: 'The list of feature types to filter by',
in: 'query', in: 'query',
}, },
] as const; ] as const;
export type FeatureSearchQueryParameters = FromQueryParams< export type FeatureSearchQueryParameters = Partial<
typeof featureSearchQueryParameters FromQueryParams<typeof featureSearchQueryParameters>
>; >;