From 3ee250ee7d711b5c16e2b3542fd5d3752f33f336 Mon Sep 17 00:00:00 2001 From: Fredrik Strand Oseberg Date: Wed, 25 Oct 2023 15:18:52 +0200 Subject: [PATCH] feat: add feature search service (#5149) --- .../useFeatureSearch/useFeatureSearch.ts | 58 +++++++++++++++++++ .../createFeatureSearchService.ts | 35 +++++++++++ .../feature-search-controller.ts | 18 ++++-- .../feature-search/feature-search-service.ts | 29 ++++++++++ src/lib/services/index.ts | 11 ++++ src/lib/types/services.ts | 2 + src/server-dev.ts | 1 + 7 files changed, 150 insertions(+), 4 deletions(-) create mode 100644 frontend/src/hooks/api/getters/useFeatureSearch/useFeatureSearch.ts create mode 100644 src/lib/features/feature-search/createFeatureSearchService.ts create mode 100644 src/lib/features/feature-search/feature-search-service.ts diff --git a/frontend/src/hooks/api/getters/useFeatureSearch/useFeatureSearch.ts b/frontend/src/hooks/api/getters/useFeatureSearch/useFeatureSearch.ts new file mode 100644 index 0000000000..ac2b0cc288 --- /dev/null +++ b/frontend/src/hooks/api/getters/useFeatureSearch/useFeatureSearch.ts @@ -0,0 +1,58 @@ +import useSWR, { SWRConfiguration } from 'swr'; +import { useCallback } from 'react'; +import { IFeatureToggleListItem } from 'interfaces/featureToggle'; +import { formatApiPath } from 'utils/formatPath'; +import handleErrorResponses from '../httpErrorResponseHandler'; + +type IFeatureSearchResponse = { features: IFeatureToggleListItem[] }; + +interface IUseFeatureSearchOutput { + features: IFeatureSearchResponse; + loading: boolean; + error: string; + refetch: () => void; +} + +const fallbackFeatures: { features: IFeatureToggleListItem[] } = { + features: [], +}; + +export const useFeatureSearch = ( + options: SWRConfiguration = {}, +): IUseFeatureSearchOutput => { + const { KEY, fetcher } = getFeatureSearchFetcher(); + const { data, error, mutate } = useSWR( + KEY, + fetcher, + options, + ); + + const refetch = useCallback(() => { + mutate(); + }, [mutate]); + + return { + features: data || fallbackFeatures, + loading: !error && !data, + error, + refetch, + }; +}; + +const getFeatureSearchFetcher = () => { + const fetcher = () => { + const path = formatApiPath(`api/admin/search/features`); + return fetch(path, { + method: 'GET', + }) + .then(handleErrorResponses('Feature search')) + .then((res) => res.json()); + }; + + const KEY = `api/admin/search/features`; + + return { + fetcher, + KEY, + }; +}; diff --git a/src/lib/features/feature-search/createFeatureSearchService.ts b/src/lib/features/feature-search/createFeatureSearchService.ts new file mode 100644 index 0000000000..3d8c678329 --- /dev/null +++ b/src/lib/features/feature-search/createFeatureSearchService.ts @@ -0,0 +1,35 @@ +import { Db } from '../../db/db'; +import { IUnleashConfig } from '../../types'; + +import FeatureStrategiesStore from '../feature-toggle/feature-toggle-strategies-store'; +import { FeatureSearchService } from './feature-search-service'; +import FakeFeatureStrategiesStore from '../feature-toggle/fakes/fake-feature-strategies-store'; + +export const createFeatureSearchService = + (config: IUnleashConfig) => (db: Db): FeatureSearchService => { + const { getLogger, eventBus, flagResolver } = config; + const featureStrategiesStore = new FeatureStrategiesStore( + db, + eventBus, + getLogger, + flagResolver, + ); + + return new FeatureSearchService( + { featureStrategiesStore: featureStrategiesStore }, + config, + ); + }; + +export const createFakeFeatureSearchService = ( + config: IUnleashConfig, +): FeatureSearchService => { + const fakeFeatureStrategiesStore = new FakeFeatureStrategiesStore(); + + return new FeatureSearchService( + { + featureStrategiesStore: fakeFeatureStrategiesStore, + }, + config, + ); +}; diff --git a/src/lib/features/feature-search/feature-search-controller.ts b/src/lib/features/feature-search/feature-search-controller.ts index ae80cadb6d..d17ed36c8b 100644 --- a/src/lib/features/feature-search/feature-search-controller.ts +++ b/src/lib/features/feature-search/feature-search-controller.ts @@ -1,6 +1,6 @@ import { Response } from 'express'; import Controller from '../../routes/controller'; -import { OpenApiService } from '../../services'; +import { FeatureSearchService, OpenApiService } from '../../services'; import { IFlagResolver, IUnleashConfig, @@ -19,22 +19,28 @@ interface ISearchQueryParams { const PATH = '/features'; -type FeatureSearchServices = Pick; +type FeatureSearchServices = Pick< + IUnleashServices, + 'openApiService' | 'featureSearchService' +>; export default class FeatureSearchController extends Controller { private openApiService: OpenApiService; private flagResolver: IFlagResolver; + private featureSearchService: FeatureSearchService; + private readonly logger: Logger; constructor( config: IUnleashConfig, - { openApiService }: FeatureSearchServices, + { openApiService, featureSearchService }: FeatureSearchServices, ) { super(config); this.openApiService = openApiService; this.flagResolver = config.flagResolver; + this.featureSearchService = featureSearchService; this.logger = config.getLogger( '/feature-search/feature-search-controller.ts', ); @@ -66,7 +72,11 @@ export default class FeatureSearchController extends Controller { const { query, tags } = req.query; if (this.config.flagResolver.isEnabled('featureSearchAPI')) { - res.json({ features: [] }); + const features = await this.featureSearchService.search( + query, + tags, + ); + res.json({ features }); } else { throw new InvalidOperationError( 'Feature Search API is not enabled', diff --git a/src/lib/features/feature-search/feature-search-service.ts b/src/lib/features/feature-search/feature-search-service.ts new file mode 100644 index 0000000000..b37b4d4cf5 --- /dev/null +++ b/src/lib/features/feature-search/feature-search-service.ts @@ -0,0 +1,29 @@ +import { Logger } from '../../logger'; +import { + IFeatureStrategiesStore, + IUnleashConfig, + IUnleashStores, +} from '../../types'; + +export class FeatureSearchService { + private featureStrategiesStore: IFeatureStrategiesStore; + private logger: Logger; + constructor( + { + featureStrategiesStore, + }: Pick, + { getLogger }: Pick, + ) { + this.featureStrategiesStore = featureStrategiesStore; + this.logger = getLogger('services/feature-search-service.ts'); + } + + async search(query: string, tags: string[]) { + const features = await this.featureStrategiesStore.getFeatureOverview({ + projectId: 'default', + }); + + return features; + // Search for features + } +} diff --git a/src/lib/services/index.ts b/src/lib/services/index.ts index d1823cdde3..c93b132707 100644 --- a/src/lib/services/index.ts +++ b/src/lib/services/index.ts @@ -99,6 +99,11 @@ import { createFakeClientFeatureToggleService, } from '../features/client-feature-toggles/createClientFeatureToggleService'; import { ClientFeatureToggleService } from '../features/client-feature-toggles/client-feature-toggle-service'; +import { + createFeatureSearchService, + createFakeFeatureSearchService, +} from '../features/feature-search/createFeatureSearchService'; +import { FeatureSearchService } from '../features/feature-search/feature-search-service'; // TODO: will be moved to scheduler feature directory export const scheduleServices = async ( @@ -336,6 +341,10 @@ export const createServices = ( : withFakeTransactional(createFakeDependentFeaturesService(config)); const dependentFeaturesService = transactionalDependentFeaturesService; + const featureSearchService = db + ? createFeatureSearchService(config)(db) + : createFakeFeatureSearchService(config); + const featureToggleServiceV2 = new FeatureToggleService( stores, config, @@ -483,6 +492,7 @@ export const createServices = ( dependentFeaturesService, transactionalDependentFeaturesService, clientFeatureToggleService, + featureSearchService, }; }; @@ -528,4 +538,5 @@ export { SchedulerService, DependentFeaturesService, ClientFeatureToggleService, + FeatureSearchService, }; diff --git a/src/lib/types/services.ts b/src/lib/types/services.ts index a226171984..a5c15769c7 100644 --- a/src/lib/types/services.ts +++ b/src/lib/types/services.ts @@ -50,6 +50,7 @@ import { IPrivateProjectChecker } from '../features/private-project/privateProje import { DependentFeaturesService } from '../features/dependent-features/dependent-features-service'; import { WithTransactional } from 'lib/db/transaction'; import { ClientFeatureToggleService } from 'lib/features/client-feature-toggles/client-feature-toggle-service'; +import { FeatureSearchService } from 'lib/features/feature-search/feature-search-service'; export interface IUnleashServices { accessService: AccessService; @@ -107,4 +108,5 @@ export interface IUnleashServices { dependentFeaturesService: DependentFeaturesService; transactionalDependentFeaturesService: WithTransactional; clientFeatureToggleService: ClientFeatureToggleService; + featureSearchService: FeatureSearchService; } diff --git a/src/server-dev.ts b/src/server-dev.ts index 763e99ccc4..10a3db9294 100644 --- a/src/server-dev.ts +++ b/src/server-dev.ts @@ -50,6 +50,7 @@ process.nextTick(async () => { separateAdminClientApi: true, playgroundImprovements: true, featureSwitchRefactor: true, + featureSearchAPI: true, }, }, authentication: {