mirror of
				https://github.com/Unleash/unleash.git
				synced 2025-10-27 11:02:16 +01: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, | ||||
|     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, | ||||
|  | ||||
| @ -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 () => { | ||||
|  | ||||
| @ -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({ | ||||
|  | ||||
| @ -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; | ||||
|  | ||||
							
								
								
									
										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, | ||||
|     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.
 | ||||
|  | ||||
| @ -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'; | ||||
|  | ||||
							
								
								
									
										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: | ||||
|             '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: | ||||
|  | ||||
| @ -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, | ||||
|         ); | ||||
|     } | ||||
| } | ||||
| 
 | ||||
|  | ||||
| @ -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 = { | ||||
|  | ||||
		Loading…
	
		Reference in New Issue
	
	Block a user