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:
parent
05f4c22f7c
commit
0c8d0704f3
@ -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 {
|
||||||
|
@ -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;
|
||||||
|
@ -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');
|
||||||
|
|
||||||
|
@ -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([]);
|
||||||
}
|
}
|
||||||
|
|
||||||
|
@ -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(
|
||||||
|
@ -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,
|
||||||
|
@ -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>
|
||||||
>;
|
>;
|
||||||
|
Loading…
Reference in New Issue
Block a user