mirror of
https://github.com/Unleash/unleash.git
synced 2025-07-21 13:47:39 +02:00
feat: feature search basic functionality (#5150)
This commit is contained in:
parent
3ee250ee7d
commit
de540e09f3
@ -11,11 +11,10 @@ import { Logger } from '../../logger';
|
|||||||
import { createResponseSchema, getStandardResponses } from '../../openapi';
|
import { createResponseSchema, getStandardResponses } from '../../openapi';
|
||||||
import { IAuthRequest } from '../../routes/unleash-types';
|
import { IAuthRequest } from '../../routes/unleash-types';
|
||||||
import { InvalidOperationError } from '../../error';
|
import { InvalidOperationError } from '../../error';
|
||||||
|
import {
|
||||||
interface ISearchQueryParams {
|
FeatureSearchQueryParameters,
|
||||||
query: string;
|
featureSearchQueryParameters,
|
||||||
tags: string[];
|
} from '../../openapi/spec/feature-search-query-parameters';
|
||||||
}
|
|
||||||
|
|
||||||
const PATH = '/features';
|
const PATH = '/features';
|
||||||
|
|
||||||
@ -56,6 +55,8 @@ export default class FeatureSearchController extends Controller {
|
|||||||
summary: 'Search and filter features',
|
summary: 'Search and filter features',
|
||||||
description: 'Search and filter by selected fields.',
|
description: 'Search and filter by selected fields.',
|
||||||
operationId: 'searchFeatures',
|
operationId: 'searchFeatures',
|
||||||
|
// TODO: fix the type
|
||||||
|
parameters: featureSearchQueryParameters as any,
|
||||||
responses: {
|
responses: {
|
||||||
200: createResponseSchema('searchFeaturesSchema'),
|
200: createResponseSchema('searchFeaturesSchema'),
|
||||||
...getStandardResponses(401, 403, 404),
|
...getStandardResponses(401, 403, 404),
|
||||||
@ -66,16 +67,11 @@ export default class FeatureSearchController extends Controller {
|
|||||||
}
|
}
|
||||||
|
|
||||||
async searchFeatures(
|
async searchFeatures(
|
||||||
req: IAuthRequest<any, any, any, ISearchQueryParams>,
|
req: IAuthRequest<any, any, any, FeatureSearchQueryParameters>,
|
||||||
res: Response,
|
res: Response,
|
||||||
): Promise<void> {
|
): Promise<void> {
|
||||||
const { query, tags } = req.query;
|
|
||||||
|
|
||||||
if (this.config.flagResolver.isEnabled('featureSearchAPI')) {
|
if (this.config.flagResolver.isEnabled('featureSearchAPI')) {
|
||||||
const features = await this.featureSearchService.search(
|
const features = await this.featureSearchService.search(req.query);
|
||||||
query,
|
|
||||||
tags,
|
|
||||||
);
|
|
||||||
res.json({ features });
|
res.json({ features });
|
||||||
} else {
|
} else {
|
||||||
throw new InvalidOperationError(
|
throw new InvalidOperationError(
|
||||||
|
@ -4,6 +4,7 @@ import {
|
|||||||
IUnleashConfig,
|
IUnleashConfig,
|
||||||
IUnleashStores,
|
IUnleashStores,
|
||||||
} from '../../types';
|
} from '../../types';
|
||||||
|
import { FeatureSearchQueryParameters } from '../../openapi/spec/feature-search-query-parameters';
|
||||||
|
|
||||||
export class FeatureSearchService {
|
export class FeatureSearchService {
|
||||||
private featureStrategiesStore: IFeatureStrategiesStore;
|
private featureStrategiesStore: IFeatureStrategiesStore;
|
||||||
@ -18,12 +19,12 @@ export class FeatureSearchService {
|
|||||||
this.logger = getLogger('services/feature-search-service.ts');
|
this.logger = getLogger('services/feature-search-service.ts');
|
||||||
}
|
}
|
||||||
|
|
||||||
async search(query: string, tags: string[]) {
|
async search(params: FeatureSearchQueryParameters) {
|
||||||
const features = await this.featureStrategiesStore.getFeatureOverview({
|
const features = await this.featureStrategiesStore.getFeatureOverview({
|
||||||
projectId: 'default',
|
projectId: params.projectId,
|
||||||
|
namePrefix: params.query,
|
||||||
});
|
});
|
||||||
|
|
||||||
return features;
|
return features;
|
||||||
// Search for features
|
|
||||||
}
|
}
|
||||||
}
|
}
|
||||||
|
@ -4,6 +4,7 @@ import {
|
|||||||
setupAppWithCustomConfig,
|
setupAppWithCustomConfig,
|
||||||
} from '../../../test/e2e/helpers/test-helper';
|
} from '../../../test/e2e/helpers/test-helper';
|
||||||
import getLogger from '../../../test/fixtures/no-logger';
|
import getLogger from '../../../test/fixtures/no-logger';
|
||||||
|
import { FeatureSearchQueryParameters } from '../../openapi/spec/feature-search-query-parameters';
|
||||||
|
|
||||||
let app: IUnleashTest;
|
let app: IUnleashTest;
|
||||||
let db: ITestDb;
|
let db: ITestDb;
|
||||||
@ -29,13 +30,44 @@ afterAll(async () => {
|
|||||||
await db.destroy();
|
await db.destroy();
|
||||||
});
|
});
|
||||||
|
|
||||||
beforeEach(async () => {});
|
beforeEach(async () => {
|
||||||
|
await db.stores.featureToggleStore.deleteAll();
|
||||||
|
});
|
||||||
|
|
||||||
const searchFeatures = async (expectedCode = 200) => {
|
const searchFeatures = async (
|
||||||
return app.request.get(`/api/admin/search/features`).expect(expectedCode);
|
{ query, projectId = 'default' }: Partial<FeatureSearchQueryParameters>,
|
||||||
|
expectedCode = 200,
|
||||||
|
) => {
|
||||||
|
return app.request
|
||||||
|
.get(`/api/admin/search/features?query=${query}&projectId=${projectId}`)
|
||||||
|
.expect(expectedCode);
|
||||||
};
|
};
|
||||||
|
|
||||||
test('should return empty features', async () => {
|
test('should return matching features', async () => {
|
||||||
const { body } = await searchFeatures();
|
await app.createFeature('my_feature_a');
|
||||||
expect(body).toStrictEqual({ features: [] });
|
await app.createFeature('my_feature_b');
|
||||||
|
await app.createFeature('my_feat_c');
|
||||||
|
|
||||||
|
const { body } = await searchFeatures({ query: 'my_feature' });
|
||||||
|
|
||||||
|
expect(body).toMatchObject({
|
||||||
|
features: [{ name: 'my_feature_a' }, { name: 'my_feature_b' }],
|
||||||
|
});
|
||||||
|
});
|
||||||
|
|
||||||
|
test('should return empty features', async () => {
|
||||||
|
const { body } = await searchFeatures({ query: '' });
|
||||||
|
expect(body).toMatchObject({ features: [] });
|
||||||
|
});
|
||||||
|
|
||||||
|
test('should not return features from another project', async () => {
|
||||||
|
await app.createFeature('my_feature_a');
|
||||||
|
await app.createFeature('my_feature_b');
|
||||||
|
|
||||||
|
const { body } = await searchFeatures({
|
||||||
|
query: '',
|
||||||
|
projectId: 'another_project',
|
||||||
|
});
|
||||||
|
|
||||||
|
expect(body).toMatchObject({ features: [] });
|
||||||
});
|
});
|
||||||
|
28
src/lib/openapi/spec/feature-search-query-parameters.ts
Normal file
28
src/lib/openapi/spec/feature-search-query-parameters.ts
Normal file
@ -0,0 +1,28 @@
|
|||||||
|
import { FromQueryParams } from '../util/from-query-params';
|
||||||
|
|
||||||
|
export const featureSearchQueryParameters = [
|
||||||
|
{
|
||||||
|
name: 'query',
|
||||||
|
schema: {
|
||||||
|
default: '',
|
||||||
|
type: 'string' as const,
|
||||||
|
example: 'feature_a',
|
||||||
|
},
|
||||||
|
description: 'The search query for the feature or tag',
|
||||||
|
in: 'query',
|
||||||
|
},
|
||||||
|
{
|
||||||
|
name: 'projectId',
|
||||||
|
schema: {
|
||||||
|
default: '',
|
||||||
|
type: 'string' as const,
|
||||||
|
example: 'default',
|
||||||
|
},
|
||||||
|
description: 'Id of the project where search is performed',
|
||||||
|
in: 'query',
|
||||||
|
},
|
||||||
|
] as const;
|
||||||
|
|
||||||
|
export type FeatureSearchQueryParameters = FromQueryParams<
|
||||||
|
typeof featureSearchQueryParameters
|
||||||
|
>;
|
@ -167,3 +167,4 @@ export * from './feature-dependencies-schema';
|
|||||||
export * from './dependencies-exist-schema';
|
export * from './dependencies-exist-schema';
|
||||||
export * from './validate-archive-features-schema';
|
export * from './validate-archive-features-schema';
|
||||||
export * from './search-features-schema';
|
export * from './search-features-schema';
|
||||||
|
export * from './feature-search-query-parameters';
|
||||||
|
Loading…
Reference in New Issue
Block a user