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:
parent
26dcc70e85
commit
705ca1514e
@ -69,54 +69,6 @@ exports[`should create default config 1`] = `
|
|||||||
"_maxListeners": undefined,
|
"_maxListeners": undefined,
|
||||||
Symbol(kCapture): false,
|
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 {
|
"flagResolver": FlagResolver {
|
||||||
"experiments": {
|
"experiments": {
|
||||||
"accessOverview": false,
|
"accessOverview": false,
|
||||||
@ -135,6 +87,7 @@ exports[`should create default config 1`] = `
|
|||||||
"embedProxy": true,
|
"embedProxy": true,
|
||||||
"embedProxyFrontend": true,
|
"embedProxyFrontend": true,
|
||||||
"featureNamingPattern": false,
|
"featureNamingPattern": false,
|
||||||
|
"featureSearchAPI": false,
|
||||||
"featureSwitchRefactor": false,
|
"featureSwitchRefactor": false,
|
||||||
"featuresExportImport": true,
|
"featuresExportImport": true,
|
||||||
"filterInvalidClientMetrics": false,
|
"filterInvalidClientMetrics": false,
|
||||||
|
@ -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 () => {
|
test('should add initApiToken for admin token from options', async () => {
|
||||||
|
@ -20,11 +20,7 @@ import {
|
|||||||
import { IAuthRequest } from '../../routes/unleash-types';
|
import { IAuthRequest } from '../../routes/unleash-types';
|
||||||
import { InvalidOperationError } from '../../error';
|
import { InvalidOperationError } from '../../error';
|
||||||
import { DependentFeaturesService } from './dependent-features-service';
|
import { DependentFeaturesService } from './dependent-features-service';
|
||||||
import {
|
import { WithTransactional } from '../../db/transaction';
|
||||||
TransactionCreator,
|
|
||||||
UnleashTransaction,
|
|
||||||
WithTransactional,
|
|
||||||
} from '../../db/transaction';
|
|
||||||
|
|
||||||
interface ProjectParams {
|
interface ProjectParams {
|
||||||
projectId: string;
|
projectId: string;
|
||||||
@ -72,7 +68,7 @@ export default class DependentFeaturesController extends Controller {
|
|||||||
this.openApiService = openApiService;
|
this.openApiService = openApiService;
|
||||||
this.flagResolver = config.flagResolver;
|
this.flagResolver = config.flagResolver;
|
||||||
this.logger = config.getLogger(
|
this.logger = config.getLogger(
|
||||||
'/dependent-features/dependent-feature-service.ts',
|
'/dependent-features/dependent-features-controller.ts',
|
||||||
);
|
);
|
||||||
|
|
||||||
this.route({
|
this.route({
|
||||||
|
@ -11,9 +11,7 @@ import {
|
|||||||
FEATURE_DEPENDENCY_ADDED,
|
FEATURE_DEPENDENCY_ADDED,
|
||||||
FEATURE_DEPENDENCY_REMOVED,
|
FEATURE_DEPENDENCY_REMOVED,
|
||||||
IEventStore,
|
IEventStore,
|
||||||
IUser,
|
|
||||||
} from '../../types';
|
} from '../../types';
|
||||||
import { ProjectService } from '../../services';
|
|
||||||
|
|
||||||
let app: IUnleashTest;
|
let app: IUnleashTest;
|
||||||
let db: ITestDb;
|
let db: ITestDb;
|
||||||
|
76
src/lib/features/feature-search/feature-search-controller.ts
Normal file
76
src/lib/features/feature-search/feature-search-controller.ts
Normal 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',
|
||||||
|
);
|
||||||
|
}
|
||||||
|
}
|
||||||
|
}
|
41
src/lib/features/feature-search/feature.search.e2e.test.ts
Normal file
41
src/lib/features/feature-search/feature.search.e2e.test.ts
Normal 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: [] });
|
||||||
|
});
|
@ -166,6 +166,7 @@ import {
|
|||||||
parentFeatureOptionsSchema,
|
parentFeatureOptionsSchema,
|
||||||
dependenciesExistSchema,
|
dependenciesExistSchema,
|
||||||
validateArchiveFeaturesSchema,
|
validateArchiveFeaturesSchema,
|
||||||
|
searchFeaturesSchema,
|
||||||
} from './spec';
|
} from './spec';
|
||||||
import { IServerOption } from '../types';
|
import { IServerOption } from '../types';
|
||||||
import { mapValues, omitKeys } from '../util';
|
import { mapValues, omitKeys } from '../util';
|
||||||
@ -395,6 +396,7 @@ export const schemas: UnleashSchemas = {
|
|||||||
featureDependenciesSchema,
|
featureDependenciesSchema,
|
||||||
dependenciesExistSchema,
|
dependenciesExistSchema,
|
||||||
validateArchiveFeaturesSchema,
|
validateArchiveFeaturesSchema,
|
||||||
|
searchFeaturesSchema,
|
||||||
};
|
};
|
||||||
|
|
||||||
// Remove JSONSchema keys that would result in an invalid OpenAPI spec.
|
// Remove JSONSchema keys that would result in an invalid OpenAPI spec.
|
||||||
|
@ -166,3 +166,4 @@ export * from './parent-feature-options-schema';
|
|||||||
export * from './feature-dependencies-schema';
|
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';
|
||||||
|
43
src/lib/openapi/spec/search-features-schema.ts
Normal file
43
src/lib/openapi/spec/search-features-schema.ts
Normal 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>;
|
@ -113,6 +113,7 @@ const OPENAPI_TAGS = [
|
|||||||
description:
|
description:
|
||||||
'Create, update, and delete [Unleash Public Signup tokens](https://docs.getunleash.io/reference/public-signup-tokens).',
|
'Create, update, and delete [Unleash Public Signup tokens](https://docs.getunleash.io/reference/public-signup-tokens).',
|
||||||
},
|
},
|
||||||
|
{ name: 'Search', description: 'Search for features.' },
|
||||||
{
|
{
|
||||||
name: 'Segments',
|
name: 'Segments',
|
||||||
description:
|
description:
|
||||||
|
@ -33,6 +33,7 @@ import { createKnexTransactionStarter } from '../../db/transaction';
|
|||||||
import { Db } from '../../db/db';
|
import { Db } from '../../db/db';
|
||||||
import ExportImportController from '../../features/export-import-toggles/export-import-controller';
|
import ExportImportController from '../../features/export-import-toggles/export-import-controller';
|
||||||
import { SegmentsController } from '../../features/segment/segment-controller';
|
import { SegmentsController } from '../../features/segment/segment-controller';
|
||||||
|
import FeatureSearchController from '../../features/feature-search/feature-search-controller';
|
||||||
|
|
||||||
class AdminApi extends Controller {
|
class AdminApi extends Controller {
|
||||||
constructor(config: IUnleashConfig, services: IUnleashServices, db: Db) {
|
constructor(config: IUnleashConfig, services: IUnleashServices, db: Db) {
|
||||||
@ -147,6 +148,11 @@ class AdminApi extends Controller {
|
|||||||
'/telemetry',
|
'/telemetry',
|
||||||
new TelemetryController(config, services).router,
|
new TelemetryController(config, services).router,
|
||||||
);
|
);
|
||||||
|
|
||||||
|
this.app.use(
|
||||||
|
'/search',
|
||||||
|
new FeatureSearchController(config, services).router,
|
||||||
|
);
|
||||||
}
|
}
|
||||||
}
|
}
|
||||||
|
|
||||||
|
@ -36,7 +36,8 @@ export type IFlagKey =
|
|||||||
| 'separateAdminClientApi'
|
| 'separateAdminClientApi'
|
||||||
| 'disableEnvsOnRevive'
|
| 'disableEnvsOnRevive'
|
||||||
| 'playgroundImprovements'
|
| 'playgroundImprovements'
|
||||||
| 'featureSwitchRefactor';
|
| 'featureSwitchRefactor'
|
||||||
|
| 'featureSearchAPI';
|
||||||
|
|
||||||
export type IFlags = Partial<{ [key in IFlagKey]: boolean | Variant }>;
|
export type IFlags = Partial<{ [key in IFlagKey]: boolean | Variant }>;
|
||||||
|
|
||||||
@ -172,6 +173,10 @@ const flags: IFlags = {
|
|||||||
process.env.UNLEASH_EXPERIMENTAL_FEATURE_SWITCH_REFACTOR,
|
process.env.UNLEASH_EXPERIMENTAL_FEATURE_SWITCH_REFACTOR,
|
||||||
false,
|
false,
|
||||||
),
|
),
|
||||||
|
featureSearchAPI: parseEnvVarBoolean(
|
||||||
|
process.env.UNLEASH_EXPERIMENTAL_FEATURE_SEARCH_API,
|
||||||
|
false,
|
||||||
|
),
|
||||||
};
|
};
|
||||||
|
|
||||||
export const defaultExperimentalOptions: IExperimentalOptions = {
|
export const defaultExperimentalOptions: IExperimentalOptions = {
|
||||||
|
Loading…
Reference in New Issue
Block a user