diff --git a/src/lib/__snapshots__/create-config.test.ts.snap b/src/lib/__snapshots__/create-config.test.ts.snap index b45407c367..de3ceed2bb 100644 --- a/src/lib/__snapshots__/create-config.test.ts.snap +++ b/src/lib/__snapshots__/create-config.test.ts.snap @@ -110,6 +110,7 @@ exports[`should create default config 1`] = ` "privateProjects": false, "proPlanAutoCharge": false, "responseTimeWithAppNameKillSwitch": false, + "separateAdminClientApi": false, "strictSchemaValidation": false, "transactionalDecorator": false, "useLastSeenRefactor": false, @@ -153,6 +154,7 @@ exports[`should create default config 1`] = ` "privateProjects": false, "proPlanAutoCharge": false, "responseTimeWithAppNameKillSwitch": false, + "separateAdminClientApi": false, "strictSchemaValidation": false, "transactionalDecorator": false, "useLastSeenRefactor": false, diff --git a/src/lib/db/index.ts b/src/lib/db/index.ts index 506d8361e5..5d8f6a8d59 100644 --- a/src/lib/db/index.ts +++ b/src/lib/db/index.ts @@ -19,7 +19,7 @@ import { AccessStore } from './access-store'; import { ResetTokenStore } from './reset-token-store'; import UserFeedbackStore from './user-feedback-store'; import FeatureStrategyStore from '../features/feature-toggle/feature-toggle-strategies-store'; -import FeatureToggleClientStore from './feature-toggle-client-store'; +import FeatureToggleClientStore from '../features/client-feature-toggles/client-feature-toggle-store'; import EnvironmentStore from './environment-store'; import FeatureTagStore from './feature-tag-store'; import { FeatureEnvironmentStore } from './feature-environment-store'; @@ -91,7 +91,7 @@ export const createStores = ( getLogger, config.flagResolver, ), - featureToggleClientStore: new FeatureToggleClientStore( + clientFeatureToggleStore: new FeatureToggleClientStore( db, eventBus, getLogger, diff --git a/src/lib/features/client-feature-toggles/client-feature-toggle-service.ts b/src/lib/features/client-feature-toggles/client-feature-toggle-service.ts new file mode 100644 index 0000000000..1ffdf271bf --- /dev/null +++ b/src/lib/features/client-feature-toggles/client-feature-toggle-service.ts @@ -0,0 +1,61 @@ +import { + IFeatureNaming, + IFeatureToggleClientStore, + IFeatureToggleQuery, + IUnleashConfig, + IUnleashStores, +} from '../../types'; + +import { Logger } from '../../logger'; + +import { FeatureConfigurationClient } from '../feature-toggle/types/feature-toggle-strategies-store-type'; + +export class ClientFeatureToggleService { + private logger: Logger; + + private clientFeatureToggleStore: IFeatureToggleClientStore; + + constructor( + { + clientFeatureToggleStore, + }: Pick, + { getLogger }: Pick, + ) { + this.logger = getLogger('services/client-feature-toggle-service.ts'); + this.clientFeatureToggleStore = clientFeatureToggleStore; + } + + async getClientFeatures( + query?: IFeatureToggleQuery, + ): Promise { + const result = await this.clientFeatureToggleStore.getClient( + query || {}, + ); + + return result.map( + ({ + name, + type, + enabled, + project, + stale, + strategies, + variants, + description, + impressionData, + dependencies, + }) => ({ + name, + type, + enabled, + project, + stale, + strategies, + variants, + description, + impressionData, + dependencies, + }), + ); + } +} diff --git a/src/lib/db/feature-toggle-client-store.ts b/src/lib/features/client-feature-toggles/client-feature-toggle-store.ts similarity index 97% rename from src/lib/db/feature-toggle-client-store.ts rename to src/lib/features/client-feature-toggles/client-feature-toggle-store.ts index 8c57afae8f..64f42a6cd4 100644 --- a/src/lib/db/feature-toggle-client-store.ts +++ b/src/lib/features/client-feature-toggles/client-feature-toggle-store.ts @@ -1,7 +1,7 @@ import { Knex } from 'knex'; -import metricsHelper from '../util/metrics-helper'; -import { DB_TIME } from '../metric-events'; -import { Logger, LogProvider } from '../logger'; +import metricsHelper from '../../util/metrics-helper'; +import { DB_TIME } from '../../metric-events'; +import { Logger, LogProvider } from '../../logger'; import { IFeatureToggleClient, IFeatureToggleClientStore, @@ -10,11 +10,11 @@ import { IStrategyConfig, ITag, PartialDeep, -} from '../types'; -import { DEFAULT_ENV, ensureStringValue, mapValues } from '../util'; +} from '../../types'; +import { DEFAULT_ENV, ensureStringValue, mapValues } from '../../util'; import EventEmitter from 'events'; -import FeatureToggleStore from '../features/feature-toggle/feature-toggle-store'; -import { Db } from './db'; +import FeatureToggleStore from '../feature-toggle/feature-toggle-store'; +import { Db } from '../../db/db'; import Raw = Knex.Raw; export interface IGetAllFeatures { diff --git a/src/lib/routes/client-api/feature.ts b/src/lib/features/client-feature-toggles/client-feature-toggle.controller.ts similarity index 84% rename from src/lib/routes/client-api/feature.ts rename to src/lib/features/client-feature-toggles/client-feature-toggle.controller.ts index 8203f13149..9e2b6283e9 100644 --- a/src/lib/routes/client-api/feature.ts +++ b/src/lib/features/client-feature-toggles/client-feature-toggle.controller.ts @@ -2,17 +2,23 @@ import memoizee from 'memoizee'; import { Response } from 'express'; // eslint-disable-next-line import/no-extraneous-dependencies import hashSum from 'hash-sum'; -import Controller from '../controller'; -import { IClientSegment, IUnleashConfig, IUnleashServices } from '../../types'; -import FeatureToggleService from '../../features/feature-toggle/feature-toggle-service'; +import Controller from '../../routes/controller'; +import { + IClientSegment, + IFeatureToggleStore, + IFlagResolver, + IUnleashConfig, + IUnleashServices, +} from '../../types'; +import FeatureToggleService from '../feature-toggle/feature-toggle-service'; import { Logger } from '../../logger'; import { querySchema } from '../../schema/feature-schema'; import { IFeatureToggleQuery } from '../../types/model'; import NotFoundError from '../../error/notfound-error'; -import { IAuthRequest } from '../unleash-types'; +import { IAuthRequest } from '../../routes/unleash-types'; import ApiUser from '../../types/api-user'; import { ALL, isAllProjects } from '../../types/models/api-token'; -import { FeatureConfigurationClient } from '../../features/feature-toggle/types/feature-toggle-strategies-store-type'; +import { FeatureConfigurationClient } from '../feature-toggle/types/feature-toggle-strategies-store-type'; import { ClientSpecService } from '../../services/client-spec-service'; import { OpenApiService } from '../../services/openapi-service'; import { NONE } from '../../types/permissions'; @@ -27,7 +33,8 @@ import { ClientFeaturesSchema, } from '../../openapi/spec/client-features-schema'; import { ISegmentService } from '../../segments/segment-service-interface'; -import ConfigurationRevisionService from '../../features/feature-toggle/configuration-revision-service'; +import ConfigurationRevisionService from '../feature-toggle/configuration-revision-service'; +import { ClientFeatureToggleService } from './client-feature-toggle-service'; const version = 2; @@ -45,7 +52,7 @@ interface IMeta { export default class FeatureController extends Controller { private readonly logger: Logger; - private featureToggleServiceV2: FeatureToggleService; + private clientFeatureToggleService: ClientFeatureToggleService; private segmentService: ISegmentService; @@ -55,6 +62,10 @@ export default class FeatureController extends Controller { private configurationRevisionService: ConfigurationRevisionService; + private featureToggleService: FeatureToggleService; + + private flagResolver: IFlagResolver; + private featuresAndSegments: ( query: IFeatureToggleQuery, etag: string, @@ -62,28 +73,32 @@ export default class FeatureController extends Controller { constructor( { - featureToggleServiceV2, + clientFeatureToggleService, segmentService, clientSpecService, openApiService, configurationRevisionService, + featureToggleService, }: Pick< IUnleashServices, - | 'featureToggleServiceV2' + | 'clientFeatureToggleService' | 'segmentService' | 'clientSpecService' | 'openApiService' | 'configurationRevisionService' + | 'featureToggleService' >, config: IUnleashConfig, ) { super(config); const { clientFeatureCaching } = config; - this.featureToggleServiceV2 = featureToggleServiceV2; + this.clientFeatureToggleService = clientFeatureToggleService; this.segmentService = segmentService; this.clientSpecService = clientSpecService; this.openApiService = openApiService; this.configurationRevisionService = configurationRevisionService; + this.featureToggleService = featureToggleService; + this.flagResolver = config.flagResolver; this.logger = config.getLogger('client-api/feature.js'); this.route({ @@ -146,8 +161,15 @@ export default class FeatureController extends Controller { private async resolveFeaturesAndSegments( query?: IFeatureToggleQuery, ): Promise<[FeatureConfigurationClient[], IClientSegment[]]> { + if (this.flagResolver.isEnabled('separateAdminClientApi')) { + return Promise.all([ + this.clientFeatureToggleService.getClientFeatures(query), + this.segmentService.getActiveForClient(), + ]); + } + return Promise.all([ - this.featureToggleServiceV2.getClientFeatures(query), + this.featureToggleService.getClientFeatures(query), this.segmentService.getActiveForClient(), ]); } @@ -287,7 +309,15 @@ export default class FeatureController extends Controller { const name = req.params.featureName; const featureQuery = await this.resolveQuery(req); const q = { ...featureQuery, namePrefix: name }; - const toggles = await this.featureToggleServiceV2.getClientFeatures(q); + + let toggles = await this.featureToggleService.getClientFeatures(q); + + if (this.flagResolver.isEnabled('separateAdminClientApi')) { + toggles = await this.clientFeatureToggleService.getClientFeatures( + q, + ); + } + const toggle = toggles.find((t) => t.name === name); if (!toggle) { throw new NotFoundError(`Could not find feature toggle ${name}`); diff --git a/src/lib/features/client-feature-toggles/createClientFeatureToggleService.ts b/src/lib/features/client-feature-toggles/createClientFeatureToggleService.ts new file mode 100644 index 0000000000..183fd5c94d --- /dev/null +++ b/src/lib/features/client-feature-toggles/createClientFeatureToggleService.ts @@ -0,0 +1,45 @@ +import FeatureToggleClientStore from '../client-feature-toggles/client-feature-toggle-store'; +import { Db } from '../../db/db'; +import { IUnleashConfig } from '../../types'; +import FakeClientFeatureToggleStore from './fakes/fake-client-feature-toggle-store'; +import { ClientFeatureToggleService } from './client-feature-toggle-service'; + +export const createClientFeatureToggleService = ( + db: Db, + config: IUnleashConfig, +): ClientFeatureToggleService => { + const { getLogger, eventBus, flagResolver } = config; + + const featureToggleClientStore = new FeatureToggleClientStore( + db, + eventBus, + getLogger, + flagResolver, + ); + + const clientFeatureToggleService = new ClientFeatureToggleService( + { + clientFeatureToggleStore: featureToggleClientStore, + }, + { getLogger, flagResolver }, + ); + + return clientFeatureToggleService; +}; + +export const createFakeClientFeatureToggleService = ( + config: IUnleashConfig, +): ClientFeatureToggleService => { + const { getLogger, flagResolver } = config; + + const fakeClientFeatureToggleStore = new FakeClientFeatureToggleStore(); + + const clientFeatureToggleService = new ClientFeatureToggleService( + { + clientFeatureToggleStore: fakeClientFeatureToggleStore, + }, + { getLogger, flagResolver }, + ); + + return clientFeatureToggleService; +}; diff --git a/src/test/fixtures/fake-feature-toggle-client-store.ts b/src/lib/features/client-feature-toggles/fakes/fake-client-feature-toggle-store.ts similarity index 90% rename from src/test/fixtures/fake-feature-toggle-client-store.ts rename to src/lib/features/client-feature-toggles/fakes/fake-client-feature-toggle-store.ts index c2d3e7c713..cb347dfa33 100644 --- a/src/test/fixtures/fake-feature-toggle-client-store.ts +++ b/src/lib/features/client-feature-toggles/fakes/fake-client-feature-toggle-store.ts @@ -2,11 +2,11 @@ import { FeatureToggle, IFeatureToggleClient, IFeatureToggleQuery, -} from '../../lib/types/model'; -import { IFeatureToggleClientStore } from '../../lib/types/stores/feature-toggle-client-store'; -import { IGetAdminFeatures } from '../../lib/db/feature-toggle-client-store'; +} from '../../../types/model'; +import { IFeatureToggleClientStore } from '../types/client-feature-toggle-store-type'; +import { IGetAdminFeatures } from '../client-feature-toggle-store'; -export default class FakeFeatureToggleClientStore +export default class FakeClientFeatureToggleStore implements IFeatureToggleClientStore { featureToggles: FeatureToggle[] = []; @@ -34,6 +34,7 @@ export default class FakeFeatureToggleClientStore } return toggle.archived === archived; }); + const clientRows: IFeatureToggleClient[] = rows.map((t) => ({ ...t, enabled: true, @@ -81,6 +82,7 @@ export default class FakeFeatureToggleClientStore archived: false, ...feature, }); + return Promise.resolve(); } } diff --git a/src/lib/routes/client-api/feature.test.ts b/src/lib/features/client-feature-toggles/tests/client-feature-toggle.e2e.test.ts similarity index 62% rename from src/lib/routes/client-api/feature.test.ts rename to src/lib/features/client-feature-toggles/tests/client-feature-toggle.e2e.test.ts index c4e5324d57..6e98a1a476 100644 --- a/src/lib/routes/client-api/feature.test.ts +++ b/src/lib/features/client-feature-toggles/tests/client-feature-toggle.e2e.test.ts @@ -1,12 +1,14 @@ import supertest from 'supertest'; -import createStores from '../../../test/fixtures/store'; -import getLogger from '../../../test/fixtures/no-logger'; -import getApp from '../../app'; -import { createServices } from '../../services'; -import FeatureController from './feature'; -import { createTestConfig } from '../../../test/config/test-config'; +import createStores from '../../../../test/fixtures/store'; +import getLogger from '../../../../test/fixtures/no-logger'; +import getApp from '../../../app'; +import { createServices } from '../../../services'; +import FeatureController from '../client-feature-toggle.controller'; +import { createTestConfig } from '../../../../test/config/test-config'; import { secondsToMilliseconds } from 'date-fns'; -import { ClientSpecService } from '../../services/client-spec-service'; +import { ClientSpecService } from '../../../services/client-spec-service'; + +let app; async function getSetup() { const base = `/random${Math.round(Math.random() * 1000)}`; @@ -16,12 +18,11 @@ async function getSetup() { }); const services = createServices(stores, config); - const app = await getApp(config, stores, services); + app = await getApp(config, stores, services); return { base, - featureToggleStore: stores.featureToggleStore, - featureToggleClientStore: stores.featureToggleClientStore, + clientFeatureToggleStore: stores.clientFeatureToggleStore, request: supertest(app), destroy: () => { services.versionService.destroy(); @@ -44,7 +45,6 @@ const callGetAll = async (controller: FeatureController) => { let base; let request; let destroy; -let featureToggleClientStore; let flagResolver; @@ -52,7 +52,6 @@ beforeEach(async () => { const setup = await getSetup(); base = setup.base; request = setup.request; - featureToggleClientStore = setup.featureToggleClientStore; destroy = setup.destroy; flagResolver = { isEnabled: () => { @@ -84,7 +83,8 @@ test('if caching is enabled should memoize', async () => { const validPath = jest.fn().mockReturnValue(jest.fn()); const clientSpecService = new ClientSpecService({ getLogger }); const openApiService = { respondWithValidation, validPath }; - const featureToggleServiceV2 = { getClientFeatures }; + const clientFeatureToggleService = { getClientFeatures }; + const featureToggleService = { getClientFeatures }; const segmentService = { getActive, getActiveForClient }; const configurationRevisionService = { getMaxRevisionId: () => 1 }; @@ -94,7 +94,9 @@ test('if caching is enabled should memoize', async () => { // @ts-expect-error due to partial implementation openApiService, // @ts-expect-error due to partial implementation - featureToggleServiceV2, + clientFeatureToggleService, + // @ts-expect-error due to partial implementation + featureToggleService, // @ts-expect-error due to partial implementation segmentService, // @ts-expect-error due to partial implementation @@ -122,8 +124,9 @@ test('if caching is not enabled all calls goes to service', async () => { const respondWithValidation = jest.fn().mockReturnValue({}); const validPath = jest.fn().mockReturnValue(jest.fn()); const clientSpecService = new ClientSpecService({ getLogger }); - const featureToggleServiceV2 = { getClientFeatures }; + const clientFeatureToggleService = { getClientFeatures }; const segmentService = { getActive, getActiveForClient }; + const featureToggleService = { getClientFeatures }; const openApiService = { respondWithValidation, validPath }; const configurationRevisionService = { getMaxRevisionId: () => 1 }; @@ -133,7 +136,9 @@ test('if caching is not enabled all calls goes to service', async () => { // @ts-expect-error due to partial implementation openApiService, // @ts-expect-error due to partial implementation - featureToggleServiceV2, + clientFeatureToggleService, + // @ts-expect-error due to partial implementation + featureToggleService, // @ts-expect-error due to partial implementation segmentService, // @ts-expect-error due to partial implementation @@ -153,58 +158,3 @@ test('if caching is not enabled all calls goes to service', async () => { await callGetAll(controller); expect(getClientFeatures).toHaveBeenCalledTimes(2); }); - -test('fetch single feature', async () => { - expect.assertions(1); - await featureToggleClientStore.createFeature({ - name: 'test_', - strategies: [{ name: 'default' }], - }); - - return request - .get(`${base}/api/client/features/test_`) - .expect('Content-Type', /json/) - .expect(200) - .expect((res) => { - expect(res.body.name === 'test_').toBe(true); - }); -}); - -test('support name prefix', async () => { - expect.assertions(2); - await featureToggleClientStore.createFeature({ name: 'a_test1' }); - await featureToggleClientStore.createFeature({ name: 'a_test2' }); - await featureToggleClientStore.createFeature({ name: 'b_test1' }); - await featureToggleClientStore.createFeature({ name: 'b_test2' }); - - const namePrefix = 'b_'; - - return request - .get(`${base}/api/client/features?namePrefix=${namePrefix}`) - .expect('Content-Type', /json/) - .expect(200) - .expect((res) => { - expect(res.body.features.length).toBe(2); - expect(res.body.features[1].name).toBe('b_test2'); - }); -}); - -test('support filtering on project', async () => { - expect.assertions(2); - await featureToggleClientStore.createFeature({ - name: 'a_test1', - project: 'projecta', - }); - await featureToggleClientStore.createFeature({ - name: 'b_test2', - project: 'projectb', - }); - return request - .get(`${base}/api/client/features?project=projecta`) - .expect('Content-Type', /json/) - .expect(200) - .expect((res) => { - expect(res.body.features).toHaveLength(1); - expect(res.body.features[0].name).toBe('a_test1'); - }); -}); diff --git a/src/lib/features/client-feature-toggles/tests/client-feature-toggles.e2e.test.ts b/src/lib/features/client-feature-toggles/tests/client-feature-toggles.e2e.test.ts new file mode 100644 index 0000000000..b88640e35c --- /dev/null +++ b/src/lib/features/client-feature-toggles/tests/client-feature-toggles.e2e.test.ts @@ -0,0 +1,109 @@ +import { RoleName } from '../../../types/model'; +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'; +import { DEFAULT_ENV } from '../../../util/constants'; + +let app: IUnleashTest; +let db: ITestDb; +let dummyAdmin; + +beforeAll(async () => { + db = await dbInit('client_feature_toggles', getLogger); + app = await setupAppWithCustomConfig( + db.stores, + { + experimental: { + flags: { + strictSchemaValidation: true, + dependentFeatures: true, + }, + }, + }, + db.rawDatabase, + ); + + dummyAdmin = await app.services.userService.createUser({ + name: 'Some Name', + email: 'test@getunleash.io', + rootRole: RoleName.ADMIN, + }); +}); + +afterEach(async () => { + const all = await db.stores.projectStore.getEnvironmentsForProject( + 'default', + ); + await Promise.all( + all + .filter((env) => env.environment !== DEFAULT_ENV) + .map(async (env) => + db.stores.projectStore.deleteEnvironmentForProject( + 'default', + env.environment, + ), + ), + ); +}); + +afterAll(async () => { + await app.destroy(); + await db.destroy(); +}); + +test('should fetch single feature', async () => { + expect.assertions(1); + await app.createFeature('test_', 'default'); + + return app.request + .get(`/api/client/features/test_`) + .expect('Content-Type', /json/) + .expect(200) + .expect((res) => { + expect(res.body.name === 'test_').toBe(true); + }); +}); + +test('should support name prefix', async () => { + expect.assertions(2); + await app.createFeature('a_test1'); + await app.createFeature('a_test2'); + await app.createFeature('b_test1'); + await app.createFeature('b_test2'); + + const namePrefix = 'b_'; + + return app.request + .get(`/api/client/features?namePrefix=${namePrefix}`) + .expect('Content-Type', /json/) + .expect(200) + .expect((res) => { + expect(res.body.features.length).toBe(2); + expect(res.body.features[1].name).toBe('b_test2'); + }); +}); + +test('should support filtering on project', async () => { + expect.assertions(2); + await app.services.projectService.createProject( + { name: 'projectA', id: 'projecta' }, + dummyAdmin, + ); + await app.services.projectService.createProject( + { name: 'projectB', id: 'projectb' }, + dummyAdmin, + ); + await app.createFeature('ab_test1', 'projecta'); + await app.createFeature('bd_test2', 'projectb'); + return app.request + .get(`/api/client/features?project=projecta`) + .expect('Content-Type', /json/) + .expect(200) + .expect((res) => { + expect(res.body.features).toHaveLength(1); + expect(res.body.features[0].name).toBe('ab_test1'); + }); +}); diff --git a/src/lib/types/stores/feature-toggle-client-store.ts b/src/lib/features/client-feature-toggles/types/client-feature-toggle-store-type.ts similarity index 69% rename from src/lib/types/stores/feature-toggle-client-store.ts rename to src/lib/features/client-feature-toggles/types/client-feature-toggle-store-type.ts index 090ec8fa61..0dbb56795f 100644 --- a/src/lib/types/stores/feature-toggle-client-store.ts +++ b/src/lib/features/client-feature-toggles/types/client-feature-toggle-store-type.ts @@ -1,5 +1,8 @@ -import { IFeatureToggleClient, IFeatureToggleQuery } from '../model'; -import { IGetAdminFeatures } from '../../db/feature-toggle-client-store'; +import { + IFeatureToggleClient, + IFeatureToggleQuery, +} from '../../../types/model'; +import { IGetAdminFeatures } from '../client-feature-toggle-store'; export interface IFeatureToggleClientStore { getClient( diff --git a/src/lib/features/feature-toggle/createFeatureToggleService.ts b/src/lib/features/feature-toggle/createFeatureToggleService.ts index 2df14f0cf1..095ff22604 100644 --- a/src/lib/features/feature-toggle/createFeatureToggleService.ts +++ b/src/lib/features/feature-toggle/createFeatureToggleService.ts @@ -6,7 +6,7 @@ import { } from '../../services'; import FeatureStrategiesStore from './feature-toggle-strategies-store'; import FeatureToggleStore from './feature-toggle-store'; -import FeatureToggleClientStore from '../../db/feature-toggle-client-store'; +import FeatureToggleClientStore from '../client-feature-toggles/client-feature-toggle-store'; import ProjectStore from '../../db/project-store'; import { FeatureEnvironmentStore } from '../../db/feature-environment-store'; import ContextFieldStore from '../../db/context-field-store'; @@ -20,7 +20,7 @@ import { IUnleashConfig } from '../../types'; import FakeEventStore from '../../../test/fixtures/fake-event-store'; import FakeFeatureStrategiesStore from './fakes/fake-feature-strategies-store'; import FakeFeatureToggleStore from './fakes/fake-feature-toggle-store'; -import FakeFeatureToggleClientStore from '../../../test/fixtures/fake-feature-toggle-client-store'; +import FakeClientFeatureToggleStore from '../client-feature-toggles/fakes/fake-client-feature-toggle-store'; import FakeProjectStore from '../../../test/fixtures/fake-project-store'; import FakeFeatureEnvironmentStore from '../../../test/fixtures/fake-feature-environment-store'; import FakeContextFieldStore from '../../../test/fixtures/fake-context-field-store'; @@ -125,7 +125,7 @@ export const createFeatureToggleService = ( { featureStrategiesStore, featureToggleStore, - featureToggleClientStore, + clientFeatureToggleStore: featureToggleClientStore, projectStore, featureTagStore, featureEnvironmentStore, @@ -152,7 +152,7 @@ export const createFakeFeatureToggleService = ( const strategyStore = new FakeStrategiesStore(); const featureStrategiesStore = new FakeFeatureStrategiesStore(); const featureToggleStore = new FakeFeatureToggleStore(); - const featureToggleClientStore = new FakeFeatureToggleClientStore(); + const featureToggleClientStore = new FakeClientFeatureToggleStore(); const projectStore = new FakeProjectStore(); const featureEnvironmentStore = new FakeFeatureEnvironmentStore(); const contextFieldStore = new FakeContextFieldStore(); @@ -185,7 +185,7 @@ export const createFakeFeatureToggleService = ( { featureStrategiesStore, featureToggleStore, - featureToggleClientStore, + clientFeatureToggleStore: featureToggleClientStore, projectStore, featureTagStore, featureEnvironmentStore, diff --git a/src/lib/features/feature-toggle/fakes/fake-feature-toggle-store.ts b/src/lib/features/feature-toggle/fakes/fake-feature-toggle-store.ts index 3f4aa478a9..bebdaa7b89 100644 --- a/src/lib/features/feature-toggle/fakes/fake-feature-toggle-store.ts +++ b/src/lib/features/feature-toggle/fakes/fake-feature-toggle-store.ts @@ -1,5 +1,5 @@ import { - IFeatureToggleQuery, + IFeatureToggleStoreQuery, IFeatureToggleStore, } from '../types/feature-toggle-store-type'; import NotFoundError from '../../../error/notfound-error'; @@ -7,6 +7,7 @@ import { FeatureToggle, FeatureToggleDTO, IFeatureEnvironment, + IFeatureToggleQuery, IVariant, } from 'lib/types/model'; import { LastSeenInput } from '../../../services/client-metrics/last-seen/last-seen-service'; @@ -66,7 +67,7 @@ export default class FakeFeatureToggleStore implements IFeatureToggleStore { return features; } - async count(query: Partial): Promise { + async count(query: Partial): Promise { return this.features.filter(this.getFilterQuery(query)).length; } @@ -78,7 +79,7 @@ export default class FakeFeatureToggleStore implements IFeatureToggleStore { return this.get(name).then((f) => f.project); } - private getFilterQuery(query: Partial) { + private getFilterQuery(query: Partial) { return (f) => { let projectMatch = true; if (query.project) { @@ -135,7 +136,9 @@ export default class FakeFeatureToggleStore implements IFeatureToggleStore { return this.get(name); } - async getBy(query: Partial): Promise { + async getBy( + query: Partial, + ): Promise { return this.features.filter(this.getFilterQuery(query)); } @@ -147,6 +150,14 @@ export default class FakeFeatureToggleStore implements IFeatureToggleStore { return this.update(revive.project, revive); } + async getFeatureToggleList( + query?: IFeatureToggleQuery, + userId?: number, + archived: boolean = false, + ): Promise { + return this.features.filter((f) => f.archived !== archived); + } + async update( project: string, data: FeatureToggleDTO, diff --git a/src/lib/features/feature-toggle/feature-toggle-service.ts b/src/lib/features/feature-toggle/feature-toggle-service.ts index 824262a74d..a0372c6a2e 100644 --- a/src/lib/features/feature-toggle/feature-toggle-service.ts +++ b/src/lib/features/feature-toggle/feature-toggle-service.ts @@ -140,7 +140,7 @@ class FeatureToggleService { private featureToggleStore: IFeatureToggleStore; - private featureToggleClientStore: IFeatureToggleClientStore; + private clientFeatureToggleStore: IFeatureToggleClientStore; private tagStore: IFeatureTagStore; @@ -170,7 +170,7 @@ class FeatureToggleService { { featureStrategiesStore, featureToggleStore, - featureToggleClientStore, + clientFeatureToggleStore, projectStore, featureTagStore, featureEnvironmentStore, @@ -180,7 +180,7 @@ class FeatureToggleService { IUnleashStores, | 'featureStrategiesStore' | 'featureToggleStore' - | 'featureToggleClientStore' + | 'clientFeatureToggleStore' | 'projectStore' | 'featureTagStore' | 'featureEnvironmentStore' @@ -203,7 +203,7 @@ class FeatureToggleService { this.featureStrategiesStore = featureStrategiesStore; this.strategyStore = strategyStore; this.featureToggleStore = featureToggleStore; - this.featureToggleClientStore = featureToggleClientStore; + this.clientFeatureToggleStore = clientFeatureToggleStore; this.tagStore = featureTagStore; this.projectStore = projectStore; this.featureEnvironmentStore = featureEnvironmentStore; @@ -1016,7 +1016,7 @@ class FeatureToggleService { async getClientFeatures( query?: IFeatureToggleQuery, ): Promise { - const result = await this.featureToggleClientStore.getClient( + const result = await this.clientFeatureToggleStore.getClient( query || {}, ); return result.map( @@ -1049,7 +1049,7 @@ class FeatureToggleService { async getPlaygroundFeatures( query?: IFeatureToggleQuery, ): Promise { - const result = await this.featureToggleClientStore.getPlayground( + const result = await this.clientFeatureToggleStore.getPlayground( query || {}, ); return result; @@ -1068,11 +1068,19 @@ class FeatureToggleService { userId?: number, archived: boolean = false, ): Promise { - const features = await this.featureToggleClientStore.getAdmin({ + let features = (await this.clientFeatureToggleStore.getAdmin({ featureQuery: query, - userId, - archived, - }); + userId: userId, + archived: false, + })) as FeatureToggle[]; + + if (this.flagResolver.isEnabled('separateAdminClientApi')) { + features = await this.featureToggleStore.getFeatureToggleList( + query, + userId, + archived, + ); + } if (this.flagResolver.isEnabled('privateProjects') && userId) { const projectAccess = diff --git a/src/lib/features/feature-toggle/feature-toggle-store.ts b/src/lib/features/feature-toggle/feature-toggle-store.ts index a066e439d5..a735781947 100644 --- a/src/lib/features/feature-toggle/feature-toggle-store.ts +++ b/src/lib/features/feature-toggle/feature-toggle-store.ts @@ -4,11 +4,23 @@ import metricsHelper from '../../util/metrics-helper'; import { DB_TIME } from '../../metric-events'; import NotFoundError from '../../error/notfound-error'; import { Logger, LogProvider } from '../../logger'; -import { FeatureToggle, FeatureToggleDTO, IVariant } from '../../types/model'; +import { + FeatureToggle, + FeatureToggleDTO, + IFeatureToggleQuery, + IVariant, +} from '../../types/model'; import { IFeatureToggleStore } from './types/feature-toggle-store-type'; import { Db } from '../../db/db'; import { LastSeenInput } from '../../services/client-metrics/last-seen/last-seen-service'; import { NameExistsError } from '../../error'; +import { DEFAULT_ENV, ensureStringValue, mapValues } from '../../../lib/util'; +import { + IFeatureToggleClient, + IStrategyConfig, + ITag, + PartialDeep, +} from '../../../lib/types'; export type EnvironmentFeatureNames = { [key: string]: string[] }; @@ -44,6 +56,81 @@ interface VariantDTO { const TABLE = 'features'; const FEATURE_ENVIRONMENTS_TABLE = 'feature_environments'; +const isUnseenStrategyRow = ( + feature: PartialDeep, + row: Record, +): boolean => { + return ( + row.strategy_id && + !feature.strategies?.find((s) => s?.id === row.strategy_id) + ); +}; + +const isNewTag = ( + feature: PartialDeep, + row: Record, +): boolean => { + return ( + row.tag_type && + row.tag_value && + !feature.tags?.some( + (tag) => tag?.type === row.tag_type && tag?.value === row.tag_value, + ) + ); +}; + +const addSegmentToStrategy = ( + feature: PartialDeep, + row: Record, +) => { + feature.strategies + ?.find((s) => s?.id === row.strategy_id) + ?.constraints?.push(...row.segment_constraints); +}; + +const addSegmentIdsToStrategy = ( + feature: PartialDeep, + row: Record, +) => { + const strategy = feature.strategies?.find((s) => s?.id === row.strategy_id); + if (!strategy) { + return; + } + if (!strategy.segments) { + strategy.segments = []; + } + strategy.segments.push(row.segment_id); +}; + +const rowToStrategy = (row: Record): IStrategyConfig => { + const strategy: IStrategyConfig = { + id: row.strategy_id, + name: row.strategy_name, + title: row.strategy_title, + constraints: row.constraints || [], + parameters: mapValues(row.parameters || {}, ensureStringValue), + sortOrder: row.sort_order, + }; + strategy.variants = row.strategy_variants || []; + return strategy; +}; + +const addTag = ( + feature: Record, + row: Record, +): void => { + const tags = feature.tags || []; + const newTag = rowToTag(row); + feature.tags = [...tags, newTag]; +}; + +const rowToTag = (row: Record): ITag => { + return { + value: row.tag_value, + type: row.tag_type, + }; +}; + export default class FeatureToggleStore implements IFeatureToggleStore { private db: Db; @@ -91,6 +178,132 @@ export default class FeatureToggleStore implements IFeatureToggleStore { .then(this.rowToFeature); } + async getFeatureToggleList( + featureQuery?: IFeatureToggleQuery, + userId?: number, + archived: boolean = false, + ): Promise { + // Handle the admin case first + // Handle the playground case + + const environment = featureQuery?.environment || DEFAULT_ENV; + + const selectColumns = [ + 'features.name as name', + 'features.description as description', + 'features.type as type', + 'features.project as project', + 'features.stale as stale', + 'features.impression_data as impression_data', + 'features.last_seen_at as last_seen_at', + 'features.created_at as created_at', + 'fe.variants as variants', + 'fe.enabled as enabled', + 'fe.environment as environment', + 'fs.id as strategy_id', + 'fs.strategy_name as strategy_name', + 'fs.title as strategy_title', + 'fs.disabled as strategy_disabled', + 'fs.parameters as parameters', + 'fs.constraints as constraints', + 'fs.sort_order as sort_order', + 'fs.variants as strategy_variants', + 'segments.id as segment_id', + 'segments.constraints as segment_constraints', + ] as (string | Knex.Raw)[]; + + let query = this.db('features') + .modify(FeatureToggleStore.filterByArchived, archived) + .leftJoin( + this.db('feature_strategies') + .select('*') + .where({ environment }) + .as('fs'), + 'fs.feature_name', + 'features.name', + ) + .leftJoin( + this.db('feature_environments') + .select( + 'feature_name', + 'enabled', + 'environment', + 'variants', + 'last_seen_at', + ) + .where({ environment }) + .as('fe'), + 'fe.feature_name', + 'features.name', + ) + .leftJoin( + 'feature_strategy_segment as fss', + `fss.feature_strategy_id`, + `fs.id`, + ) + .leftJoin('segments', `segments.id`, `fss.segment_id`) + .leftJoin('dependent_features as df', 'df.child', 'features.name') + .leftJoin('feature_tag as ft', 'ft.feature_name', 'features.name'); + + if (userId) { + query = query.leftJoin(`favorite_features`, function () { + this.on('favorite_features.feature', 'features.name').andOnVal( + 'favorite_features.user_id', + '=', + userId, + ); + }); + selectColumns.push( + this.db.raw( + 'favorite_features.feature is not null as favorite', + ), + ); + } + + query = query.select(selectColumns); + const rows = await query; + + const featureToggles = rows.reduce((acc, r) => { + const feature: PartialDeep = acc[r.name] ?? { + strategies: [], + }; + if (isUnseenStrategyRow(feature, r) && !r.strategy_disabled) { + feature.strategies?.push(rowToStrategy(r)); + } + if (isNewTag(feature, r)) { + addTag(feature, r); + } + if (featureQuery?.inlineSegmentConstraints && r.segment_id) { + addSegmentToStrategy(feature, r); + } else if ( + !featureQuery?.inlineSegmentConstraints && + r.segment_id + ) { + addSegmentIdsToStrategy(feature, r); + } + + feature.impressionData = r.impression_data; + feature.enabled = !!r.enabled; + feature.name = r.name; + feature.description = r.description; + feature.project = r.project; + feature.stale = r.stale; + feature.type = r.type; + feature.lastSeenAt = r.last_seen_at; + feature.variants = r.variants || []; + feature.project = r.project; + + feature.favorite = r.favorite; + feature.lastSeenAt = r.last_seen_at; + feature.createdAt = r.created_at; + + acc[r.name] = feature; + return acc; + }, {}); + + return Object.values(featureToggles); + } + async getAll( query: { archived?: boolean; diff --git a/src/lib/features/feature-toggle/legacy/feature-toggle-legacy-controller.ts b/src/lib/features/feature-toggle/legacy/feature-toggle-legacy-controller.ts index f259a3e5db..8caffa6f90 100644 --- a/src/lib/features/feature-toggle/legacy/feature-toggle-legacy-controller.ts +++ b/src/lib/features/feature-toggle/legacy/feature-toggle-legacy-controller.ts @@ -196,7 +196,7 @@ class FeatureController extends Controller { namePrefix, }: any): Promise { if (!tag && !project && !namePrefix) { - return null; + return {}; } const tagQuery = this.paramToArray(tag); const projectQuery = this.paramToArray(project); @@ -216,6 +216,7 @@ class FeatureController extends Controller { res: Response, ): Promise { const query = await this.prepQuery(req.query); + const { user } = req; const features = await this.service.getFeatureToggles(query, user.id); diff --git a/src/lib/features/feature-toggle/tests/feature-toggles.e2e.test.ts b/src/lib/features/feature-toggle/tests/feature-toggles.e2e.test.ts index 493464d319..fb547c5937 100644 --- a/src/lib/features/feature-toggle/tests/feature-toggles.e2e.test.ts +++ b/src/lib/features/feature-toggle/tests/feature-toggles.e2e.test.ts @@ -3533,3 +3533,30 @@ test('should not be allowed to update with invalid strategy type name', async () 400, ); }); + +test('should return correct data structure for /api/admin/features', async () => { + await app.createFeature('refactor-features'); + + const result = await app.request.get('/api/admin/features').expect(200); + + expect(result.body.features).toBeInstanceOf(Array); + + const feature = result.body.features.find( + (features) => features.name === 'refactor-features', + ); + + expect(feature).toMatchObject({ + impressionData: false, + enabled: false, + name: 'refactor-features', + description: null, + project: 'default', + stale: false, + type: 'release', + lastSeenAt: null, + variants: [], + favorite: false, + createdAt: expect.anything(), + strategies: [], + }); +}); diff --git a/src/lib/features/feature-toggle/types/feature-toggle-store-type.ts b/src/lib/features/feature-toggle/types/feature-toggle-store-type.ts index 7e2a1749f7..c0b546773e 100644 --- a/src/lib/features/feature-toggle/types/feature-toggle-store-type.ts +++ b/src/lib/features/feature-toggle/types/feature-toggle-store-type.ts @@ -1,12 +1,13 @@ import { FeatureToggle, FeatureToggleDTO, + IFeatureToggleQuery, IVariant, } from '../../../types/model'; import { Store } from '../../../types/stores/store'; import { LastSeenInput } from '../../../services/client-metrics/last-seen/last-seen-service'; -export interface IFeatureToggleQuery { +export interface IFeatureToggleStoreQuery { archived: boolean; project: string; stale: boolean; @@ -14,7 +15,7 @@ export interface IFeatureToggleQuery { } export interface IFeatureToggleStore extends Store { - count(query?: Partial): Promise; + count(query?: Partial): Promise; setLastSeen(data: LastSeenInput[]): Promise; getProjectId(name: string): Promise; create(project: string, data: FeatureToggleDTO): Promise; @@ -28,8 +29,13 @@ export interface IFeatureToggleStore extends Store { batchDelete(featureNames: string[]): Promise; batchRevive(featureNames: string[]): Promise; revive(featureName: string): Promise; - getAll(query?: Partial): Promise; + getAll(query?: Partial): Promise; getAllByNames(names: string[]): Promise; + getFeatureToggleList( + featureQuery?: IFeatureToggleQuery, + userId?: number, + archived?: boolean, + ): Promise; countByDate(queryModifiers: { archived?: boolean; project?: string; diff --git a/src/lib/routes/client-api/index.ts b/src/lib/routes/client-api/index.ts index de34084bf6..ce3b46984c 100644 --- a/src/lib/routes/client-api/index.ts +++ b/src/lib/routes/client-api/index.ts @@ -1,5 +1,5 @@ import Controller from '../controller'; -import FeatureController from './feature'; +import FeatureController from '../../features/client-feature-toggles/client-feature-toggle.controller'; import MetricsController from './metrics'; import RegisterController from './register'; import { IUnleashConfig, IUnleashServices } from '../../types'; diff --git a/src/lib/routes/client-api/metrics.test.ts b/src/lib/routes/client-api/metrics.test.ts index dfebb46d3e..c02b7b2157 100644 --- a/src/lib/routes/client-api/metrics.test.ts +++ b/src/lib/routes/client-api/metrics.test.ts @@ -211,8 +211,6 @@ test('should set lastSeen on toggle', async () => { await services.lastSeenService.store(); const toggle = await stores.featureToggleStore.get('toggleLastSeen'); - console.log(toggle); - expect(toggle.lastSeenAt).toBeTruthy(); }); diff --git a/src/lib/services/index.ts b/src/lib/services/index.ts index 3c21322be6..303401a9f7 100644 --- a/src/lib/services/index.ts +++ b/src/lib/services/index.ts @@ -89,6 +89,11 @@ import { createFakeGetProductionChanges, createGetProductionChanges, } from '../features/instance-stats/getProductionChanges'; +import { + createClientFeatureToggleService, + createFakeClientFeatureToggleService, +} from '../features/client-feature-toggles/createClientFeatureToggleService'; +import { ClientFeatureToggleService } from '../features/client-feature-toggles/client-feature-toggle-service'; // TODO: will be moved to scheduler feature directory export const scheduleServices = async ( @@ -322,6 +327,10 @@ export const createServices = ( config, ); + const clientFeatureToggleService = db + ? createClientFeatureToggleService(db, config) + : createFakeClientFeatureToggleService(config); + const proxyService = new ProxyService(config, stores, { featureToggleServiceV2, clientMetricsServiceV2, @@ -413,6 +422,7 @@ export const createServices = ( privateProjectChecker, dependentFeaturesService, transactionalDependentFeaturesService, + clientFeatureToggleService, }; }; @@ -457,4 +467,5 @@ export { FavoritesService, SchedulerService, DependentFeaturesService, + ClientFeatureToggleService, }; diff --git a/src/lib/types/experimental.ts b/src/lib/types/experimental.ts index 307835bbfe..855be38613 100644 --- a/src/lib/types/experimental.ts +++ b/src/lib/types/experimental.ts @@ -35,7 +35,9 @@ export type IFlagKey = | 'disableMetrics' | 'transactionalDecorator' | 'useLastSeenRefactor' - | 'internalMessageBanners'; + | 'internalMessageBanners' + | 'internalMessageBanner' + | 'separateAdminClientApi'; export type IFlags = Partial<{ [key in IFlagKey]: boolean | Variant }>; @@ -167,6 +169,10 @@ const flags: IFlags = { process.env.UNLEASH_EXPERIMENTAL_INTERNAL_MESSAGE_BANNERS, false, ), + separateAdminClientApi: parseEnvVarBoolean( + process.env.UNLEASH_EXPERIMENTAL_SEPARATE_ADMIN_CLIENT_API, + false, + ), }; export const defaultExperimentalOptions: IExperimentalOptions = { diff --git a/src/lib/types/services.ts b/src/lib/types/services.ts index 1aaadcba9e..45d11ba996 100644 --- a/src/lib/types/services.ts +++ b/src/lib/types/services.ts @@ -49,6 +49,7 @@ import EventAnnouncerService from 'lib/services/event-announcer-service'; import { IPrivateProjectChecker } from '../features/private-project/privateProjectCheckerType'; 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'; export interface IUnleashServices { accessService: AccessService; @@ -108,4 +109,5 @@ export interface IUnleashServices { transactionalDependentFeaturesService: ( db: Knex.Transaction, ) => DependentFeaturesService; + clientFeatureToggleService: ClientFeatureToggleService; } diff --git a/src/lib/types/stores.ts b/src/lib/types/stores.ts index e696cc704d..52b8a4a59c 100644 --- a/src/lib/types/stores.ts +++ b/src/lib/types/stores.ts @@ -20,7 +20,7 @@ import { IUserFeedbackStore } from './stores/user-feedback-store'; import { IFeatureEnvironmentStore } from './stores/feature-environment-store'; import { IFeatureStrategiesStore } from '../features/feature-toggle/types/feature-toggle-strategies-store-type'; import { IEnvironmentStore } from './stores/environment-store'; -import { IFeatureToggleClientStore } from './stores/feature-toggle-client-store'; +import { IFeatureToggleClientStore } from '../features/client-feature-toggles/types/client-feature-toggle-store-type'; import { IClientMetricsStoreV2 } from './stores/client-metrics-store-v2'; import { IUserSplashStore } from './stores/user-splash-store'; import { IRoleStore } from './stores/role-store'; @@ -52,7 +52,7 @@ export interface IUnleashStores { featureStrategiesStore: IFeatureStrategiesStore; featureTagStore: IFeatureTagStore; featureToggleStore: IFeatureToggleStore; - featureToggleClientStore: IFeatureToggleClientStore; + clientFeatureToggleStore: IFeatureToggleClientStore; featureTypeStore: IFeatureTypeStore; groupStore: IGroupStore; projectStore: IProjectStore; diff --git a/src/server-dev.ts b/src/server-dev.ts index 96d5ee5ba9..b286e01217 100644 --- a/src/server-dev.ts +++ b/src/server-dev.ts @@ -48,6 +48,7 @@ process.nextTick(async () => { dependentFeatures: true, transactionalDecorator: true, useLastSeenRefactor: true, + separateAdminClientApi: true, }, }, authentication: { diff --git a/src/test/e2e/api/client/feature.env.disabled.e2e.test.ts b/src/test/e2e/api/client/feature.env.disabled.e2e.test.ts index 0eeac5eb03..3f61644528 100644 --- a/src/test/e2e/api/client/feature.env.disabled.e2e.test.ts +++ b/src/test/e2e/api/client/feature.env.disabled.e2e.test.ts @@ -1,4 +1,8 @@ -import { IUnleashTest, setupApp } from '../../helpers/test-helper'; +import { + IUnleashTest, + setupApp, + setupAppWithCustomConfig, +} from '../../helpers/test-helper'; import dbInit, { ITestDb } from '../../helpers/database-init'; import getLogger from '../../../fixtures/no-logger'; import { DEFAULT_ENV } from '../../../../lib/util/constants'; @@ -12,7 +16,7 @@ const projectId = 'default'; beforeAll(async () => { db = await dbInit('feature_env_api_client', getLogger); - app = await setupApp(db.stores); + app = await setupAppWithCustomConfig(db.stores, {}, db.rawDatabase); await app.services.featureToggleServiceV2.createFeatureToggle( projectId, @@ -43,6 +47,7 @@ test('returns feature toggle for default env', async () => { true, 'test', ); + await app.request .get('/api/client/features') .expect('Content-Type', /json/) diff --git a/src/test/e2e/api/client/feature.token.access.e2e.test.ts b/src/test/e2e/api/client/feature.token.access.e2e.test.ts index 670813d986..801b248436 100644 --- a/src/test/e2e/api/client/feature.token.access.e2e.test.ts +++ b/src/test/e2e/api/client/feature.token.access.e2e.test.ts @@ -20,7 +20,7 @@ const feature3 = 'f3.p2.token.access'; beforeAll(async () => { db = await dbInit('feature_api_api_access_client', getLogger); - app = await setupAppWithAuth(db.stores); + app = await setupAppWithAuth(db.stores, {}, db.rawDatabase); apiTokenService = app.services.apiTokenService; const { featureToggleServiceV2, environmentService } = app.services; diff --git a/src/test/e2e/stores/feature-toggle-client-store.e2e.test.ts b/src/test/e2e/stores/feature-toggle-client-store.e2e.test.ts index 5c802f5858..190fd2bc5f 100644 --- a/src/test/e2e/stores/feature-toggle-client-store.e2e.test.ts +++ b/src/test/e2e/stores/feature-toggle-client-store.e2e.test.ts @@ -5,14 +5,14 @@ import { setupApp } from '../helpers/test-helper'; let stores; let app; let db; -let featureToggleClientStore; +let clientFeatureToggleStore; beforeAll(async () => { getLogger.setMuteError(true); db = await dbInit('feature_toggle_client_store_serial', getLogger); app = await setupApp(db.stores); stores = db.stores; - featureToggleClientStore = stores.featureToggleClientStore; + clientFeatureToggleStore = stores.clientFeatureToggleStore; }); afterAll(async () => { @@ -27,6 +27,6 @@ test('should be able to fetch client toggles', async () => { expect(response.status).toBe(202); - const clientToggles = await featureToggleClientStore.getClient(); + const clientToggles = await clientFeatureToggleStore.getClient(); expect(clientToggles).toHaveLength(1); }); diff --git a/src/test/fixtures/store.ts b/src/test/fixtures/store.ts index e692aaa40b..039afedae9 100644 --- a/src/test/fixtures/store.ts +++ b/src/test/fixtures/store.ts @@ -25,7 +25,7 @@ import FakeFeatureEnvironmentStore from './fake-feature-environment-store'; import FakeApiTokenStore from './fake-api-token-store'; import FakeFeatureTypeStore from './fake-feature-type-store'; import FakeResetTokenStore from './fake-reset-token-store'; -import FakeFeatureToggleClientStore from './fake-feature-toggle-client-store'; +import FakeClientFeatureToggleStore from '../../lib/features/client-feature-toggles/fakes/fake-client-feature-toggle-store'; import FakeClientMetricsStoreV2 from './fake-client-metrics-store-v2'; import FakeUserSplashStore from './fake-user-splash-store'; import FakeRoleStore from './fake-role-store'; @@ -52,7 +52,7 @@ const createStores: () => IUnleashStores = () => { clientMetricsStoreV2: new FakeClientMetricsStoreV2(), clientInstanceStore: new FakeClientInstanceStore(), featureToggleStore: new FakeFeatureToggleStore(), - featureToggleClientStore: new FakeFeatureToggleClientStore(), + clientFeatureToggleStore: new FakeClientFeatureToggleStore(), tagStore: new FakeTagStore(), tagTypeStore: new FakeTagTypeStore(), eventStore: new FakeEventStore(),