From f34d187cd99076964a1454c4dbc57f6b3305065a Mon Sep 17 00:00:00 2001 From: Fredrik Strand Oseberg Date: Thu, 12 Oct 2023 13:58:23 +0200 Subject: [PATCH] Refactor/separate client and admin store (#5006) This PR is the first step in separating the client and admin stores. Currently our feature toggle services uses the client store to serve multiple purposes. Admin API uses the feature toggle service to serve both the feature toggle list and playground features, while the client API uses the feature toggle service to serve client features. The admin API can change often and have very different requirements than the client API, which changes infrequently and generally keeps the same stable structure for long periods of time. This architecture is error prone, because when you need to make changes to the admin API, you can very easily affect the client API. I aim to put up a stone wall between the two APIs. Complete separation between the two APIs, at the cost of some duplication. In this PR I have created a feature oriented architecture for client features and disconnected the client API from the feature toggle service. It now goes through it's own service to it's own store. For feature toggle service I have duplicated and replaced the functionality that serves /api/admin/features, I have kept a lot of the ugliness in the code and haven't removed anything in order to avoid breaking changes. Next steps: * Move playground to admin API * Remove client-feature-toggle-store from feature-toggle-service --- .../__snapshots__/create-config.test.ts.snap | 2 + src/lib/db/index.ts | 4 +- .../client-feature-toggle-service.ts | 61 +++++ .../client-feature-toggle-store.ts} | 14 +- .../client-feature-toggle.controller.ts} | 54 ++++- .../createClientFeatureToggleService.ts | 45 ++++ .../fake-client-feature-toggle-store.ts} | 10 +- .../tests/client-feature-toggle.e2e.test.ts} | 92 ++------ .../tests/client-feature-toggles.e2e.test.ts | 109 +++++++++ .../client-feature-toggle-store-type.ts} | 7 +- .../createFeatureToggleService.ts | 10 +- .../fakes/fake-feature-toggle-store.ts | 19 +- .../feature-toggle/feature-toggle-service.ts | 28 ++- .../feature-toggle/feature-toggle-store.ts | 215 +++++++++++++++++- .../feature-toggle-legacy-controller.ts | 3 +- .../tests/feature-toggles.e2e.test.ts | 27 +++ .../types/feature-toggle-store-type.ts | 12 +- src/lib/routes/client-api/index.ts | 2 +- src/lib/routes/client-api/metrics.test.ts | 2 - src/lib/services/index.ts | 11 + src/lib/types/experimental.ts | 8 +- src/lib/types/services.ts | 2 + src/lib/types/stores.ts | 4 +- src/server-dev.ts | 1 + .../client/feature.env.disabled.e2e.test.ts | 9 +- .../client/feature.token.access.e2e.test.ts | 2 +- .../feature-toggle-client-store.e2e.test.ts | 6 +- src/test/fixtures/store.ts | 4 +- 28 files changed, 627 insertions(+), 136 deletions(-) create mode 100644 src/lib/features/client-feature-toggles/client-feature-toggle-service.ts rename src/lib/{db/feature-toggle-client-store.ts => features/client-feature-toggles/client-feature-toggle-store.ts} (97%) rename src/lib/{routes/client-api/feature.ts => features/client-feature-toggles/client-feature-toggle.controller.ts} (84%) create mode 100644 src/lib/features/client-feature-toggles/createClientFeatureToggleService.ts rename src/{test/fixtures/fake-feature-toggle-client-store.ts => lib/features/client-feature-toggles/fakes/fake-client-feature-toggle-store.ts} (90%) rename src/lib/{routes/client-api/feature.test.ts => features/client-feature-toggles/tests/client-feature-toggle.e2e.test.ts} (62%) create mode 100644 src/lib/features/client-feature-toggles/tests/client-feature-toggles.e2e.test.ts rename src/lib/{types/stores/feature-toggle-client-store.ts => features/client-feature-toggles/types/client-feature-toggle-store-type.ts} (69%) 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(),