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

feat: feature search stub (#5143)

This commit is contained in:
Mateusz Kwasniewski 2023-10-25 10:50:59 +02:00 committed by GitHub
parent 26dcc70e85
commit 705ca1514e
No known key found for this signature in database
GPG Key ID: 4AEE18F83AFDEB23
12 changed files with 181 additions and 58 deletions

View File

@ -69,54 +69,6 @@ exports[`should create default config 1`] = `
"_maxListeners": undefined,
Symbol(kCapture): false,
},
"experimental": {
"externalResolver": {
"getVariant": [Function],
"isEnabled": [Function],
},
"flags": {
"accessOverview": false,
"anonymiseEventLog": false,
"banners": false,
"caseInsensitiveInOperators": false,
"customRootRolesKillSwitch": false,
"datadogJsonTemplate": false,
"demo": false,
"dependentFeatures": false,
"disableBulkToggle": false,
"disableEnvsOnRevive": false,
"disableMetrics": false,
"disableNotifications": false,
"doraMetrics": false,
"embedProxy": true,
"embedProxyFrontend": true,
"featureNamingPattern": false,
"featureSwitchRefactor": false,
"featuresExportImport": true,
"filterInvalidClientMetrics": false,
"googleAuthEnabled": false,
"lastSeenByEnvironment": false,
"maintenanceMode": false,
"messageBanner": {
"enabled": false,
"name": "message-banner",
"payload": {
"type": "json",
"value": "",
},
},
"migrationLock": true,
"personalAccessTokensKillSwitch": false,
"playgroundImprovements": false,
"privateProjects": false,
"proPlanAutoCharge": false,
"responseTimeWithAppNameKillSwitch": false,
"separateAdminClientApi": false,
"strictSchemaValidation": false,
"useLastSeenRefactor": false,
"variantTypeNumber": false,
},
},
"flagResolver": FlagResolver {
"experiments": {
"accessOverview": false,
@ -135,6 +87,7 @@ exports[`should create default config 1`] = `
"embedProxy": true,
"embedProxyFrontend": true,
"featureNamingPattern": false,
"featureSearchAPI": false,
"featureSwitchRefactor": false,
"featuresExportImport": true,
"filterInvalidClientMetrics": false,

View File

@ -15,7 +15,8 @@ test('should create default config', async () => {
},
});
expect(config).toMatchSnapshot();
const { experimental, ...configWithoutExperimental } = config;
expect(configWithoutExperimental).toMatchSnapshot();
});
test('should add initApiToken for admin token from options', async () => {

View File

@ -20,11 +20,7 @@ import {
import { IAuthRequest } from '../../routes/unleash-types';
import { InvalidOperationError } from '../../error';
import { DependentFeaturesService } from './dependent-features-service';
import {
TransactionCreator,
UnleashTransaction,
WithTransactional,
} from '../../db/transaction';
import { WithTransactional } from '../../db/transaction';
interface ProjectParams {
projectId: string;
@ -72,7 +68,7 @@ export default class DependentFeaturesController extends Controller {
this.openApiService = openApiService;
this.flagResolver = config.flagResolver;
this.logger = config.getLogger(
'/dependent-features/dependent-feature-service.ts',
'/dependent-features/dependent-features-controller.ts',
);
this.route({

View File

@ -11,9 +11,7 @@ import {
FEATURE_DEPENDENCY_ADDED,
FEATURE_DEPENDENCY_REMOVED,
IEventStore,
IUser,
} from '../../types';
import { ProjectService } from '../../services';
let app: IUnleashTest;
let db: ITestDb;

View File

@ -0,0 +1,76 @@
import { Response } from 'express';
import Controller from '../../routes/controller';
import { OpenApiService } from '../../services';
import {
IFlagResolver,
IUnleashConfig,
IUnleashServices,
NONE,
} from '../../types';
import { Logger } from '../../logger';
import { createResponseSchema, getStandardResponses } from '../../openapi';
import { IAuthRequest } from '../../routes/unleash-types';
import { InvalidOperationError } from '../../error';
interface ISearchQueryParams {
query: string;
tags: string[];
}
const PATH = '/features';
type FeatureSearchServices = Pick<IUnleashServices, 'openApiService'>;
export default class FeatureSearchController extends Controller {
private openApiService: OpenApiService;
private flagResolver: IFlagResolver;
private readonly logger: Logger;
constructor(
config: IUnleashConfig,
{ openApiService }: FeatureSearchServices,
) {
super(config);
this.openApiService = openApiService;
this.flagResolver = config.flagResolver;
this.logger = config.getLogger(
'/feature-search/feature-search-controller.ts',
);
this.route({
method: 'get',
path: PATH,
handler: this.searchFeatures,
permission: NONE,
middleware: [
openApiService.validPath({
tags: ['Search'],
summary: 'Search and filter features',
description: 'Search and filter by selected fields.',
operationId: 'searchFeatures',
responses: {
200: createResponseSchema('searchFeaturesSchema'),
...getStandardResponses(401, 403, 404),
},
}),
],
});
}
async searchFeatures(
req: IAuthRequest<any, any, any, ISearchQueryParams>,
res: Response,
): Promise<void> {
const { query, tags } = req.query;
if (this.config.flagResolver.isEnabled('featureSearchAPI')) {
res.json({ features: [] });
} else {
throw new InvalidOperationError(
'Feature Search API is not enabled',
);
}
}
}

View File

@ -0,0 +1,41 @@
import dbInit, { ITestDb } from '../../../test/e2e/helpers/database-init';
import {
IUnleashTest,
setupAppWithCustomConfig,
} from '../../../test/e2e/helpers/test-helper';
import getLogger from '../../../test/fixtures/no-logger';
let app: IUnleashTest;
let db: ITestDb;
beforeAll(async () => {
db = await dbInit('feature_search', getLogger);
app = await setupAppWithCustomConfig(
db.stores,
{
experimental: {
flags: {
strictSchemaValidation: true,
featureSearchAPI: true,
},
},
},
db.rawDatabase,
);
});
afterAll(async () => {
await app.destroy();
await db.destroy();
});
beforeEach(async () => {});
const searchFeatures = async (expectedCode = 200) => {
return app.request.get(`/api/admin/search/features`).expect(expectedCode);
};
test('should return empty features', async () => {
const { body } = await searchFeatures();
expect(body).toStrictEqual({ features: [] });
});

View File

@ -166,6 +166,7 @@ import {
parentFeatureOptionsSchema,
dependenciesExistSchema,
validateArchiveFeaturesSchema,
searchFeaturesSchema,
} from './spec';
import { IServerOption } from '../types';
import { mapValues, omitKeys } from '../util';
@ -395,6 +396,7 @@ export const schemas: UnleashSchemas = {
featureDependenciesSchema,
dependenciesExistSchema,
validateArchiveFeaturesSchema,
searchFeaturesSchema,
};
// Remove JSONSchema keys that would result in an invalid OpenAPI spec.

View File

@ -166,3 +166,4 @@ export * from './parent-feature-options-schema';
export * from './feature-dependencies-schema';
export * from './dependencies-exist-schema';
export * from './validate-archive-features-schema';
export * from './search-features-schema';

View File

@ -0,0 +1,43 @@
import { FromSchema } from 'json-schema-to-ts';
import { parametersSchema } from './parameters-schema';
import { variantSchema } from './variant-schema';
import { overrideSchema } from './override-schema';
import { featureStrategySchema } from './feature-strategy-schema';
import { featureSchema } from './feature-schema';
import { constraintSchema } from './constraint-schema';
import { featureEnvironmentSchema } from './feature-environment-schema';
import { strategyVariantSchema } from './strategy-variant-schema';
import { tagSchema } from './tag-schema';
export const searchFeaturesSchema = {
$id: '#/components/schemas/searchFeaturesSchema',
type: 'object',
additionalProperties: false,
required: ['features'],
description: 'A list of features matching search and filter criteria.',
properties: {
features: {
type: 'array',
items: {
$ref: '#/components/schemas/featureSchema',
},
description:
'The full list of features in this project (excluding archived features)',
},
},
components: {
schemas: {
featureSchema,
constraintSchema,
featureEnvironmentSchema,
featureStrategySchema,
strategyVariantSchema,
overrideSchema,
parametersSchema,
variantSchema,
tagSchema,
},
},
} as const;
export type SearchFeaturesSchema = FromSchema<typeof searchFeaturesSchema>;

View File

@ -113,6 +113,7 @@ const OPENAPI_TAGS = [
description:
'Create, update, and delete [Unleash Public Signup tokens](https://docs.getunleash.io/reference/public-signup-tokens).',
},
{ name: 'Search', description: 'Search for features.' },
{
name: 'Segments',
description:

View File

@ -33,6 +33,7 @@ import { createKnexTransactionStarter } from '../../db/transaction';
import { Db } from '../../db/db';
import ExportImportController from '../../features/export-import-toggles/export-import-controller';
import { SegmentsController } from '../../features/segment/segment-controller';
import FeatureSearchController from '../../features/feature-search/feature-search-controller';
class AdminApi extends Controller {
constructor(config: IUnleashConfig, services: IUnleashServices, db: Db) {
@ -147,6 +148,11 @@ class AdminApi extends Controller {
'/telemetry',
new TelemetryController(config, services).router,
);
this.app.use(
'/search',
new FeatureSearchController(config, services).router,
);
}
}

View File

@ -36,7 +36,8 @@ export type IFlagKey =
| 'separateAdminClientApi'
| 'disableEnvsOnRevive'
| 'playgroundImprovements'
| 'featureSwitchRefactor';
| 'featureSwitchRefactor'
| 'featureSearchAPI';
export type IFlags = Partial<{ [key in IFlagKey]: boolean | Variant }>;
@ -172,6 +173,10 @@ const flags: IFlags = {
process.env.UNLEASH_EXPERIMENTAL_FEATURE_SWITCH_REFACTOR,
false,
),
featureSearchAPI: parseEnvVarBoolean(
process.env.UNLEASH_EXPERIMENTAL_FEATURE_SEARCH_API,
false,
),
};
export const defaultExperimentalOptions: IExperimentalOptions = {