mirror of
https://github.com/Unleash/unleash.git
synced 2025-01-20 00:08:02 +01:00
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
This commit is contained in:
parent
7b7a2a706c
commit
f34d187cd9
@ -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,
|
||||
|
@ -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,
|
||||
|
@ -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<IUnleashStores, 'clientFeatureToggleStore'>,
|
||||
{ getLogger }: Pick<IUnleashConfig, 'getLogger' | 'flagResolver'>,
|
||||
) {
|
||||
this.logger = getLogger('services/client-feature-toggle-service.ts');
|
||||
this.clientFeatureToggleStore = clientFeatureToggleStore;
|
||||
}
|
||||
|
||||
async getClientFeatures(
|
||||
query?: IFeatureToggleQuery,
|
||||
): Promise<FeatureConfigurationClient[]> {
|
||||
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,
|
||||
}),
|
||||
);
|
||||
}
|
||||
}
|
@ -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 {
|
@ -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}`);
|
@ -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;
|
||||
};
|
@ -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();
|
||||
}
|
||||
}
|
@ -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');
|
||||
});
|
||||
});
|
@ -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');
|
||||
});
|
||||
});
|
@ -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(
|
@ -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,
|
||||
|
@ -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<IFeatureToggleQuery>): Promise<number> {
|
||||
async count(query: Partial<IFeatureToggleStoreQuery>): Promise<number> {
|
||||
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<IFeatureToggleQuery>) {
|
||||
private getFilterQuery(query: Partial<IFeatureToggleStoreQuery>) {
|
||||
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<IFeatureToggleQuery>): Promise<FeatureToggle[]> {
|
||||
async getBy(
|
||||
query: Partial<IFeatureToggleStoreQuery>,
|
||||
): Promise<FeatureToggle[]> {
|
||||
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<FeatureToggle[]> {
|
||||
return this.features.filter((f) => f.archived !== archived);
|
||||
}
|
||||
|
||||
async update(
|
||||
project: string,
|
||||
data: FeatureToggleDTO,
|
||||
|
@ -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<FeatureConfigurationClient[]> {
|
||||
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<FeatureConfigurationClient[]> {
|
||||
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<FeatureToggle[]> {
|
||||
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 =
|
||||
|
@ -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<IFeatureToggleClient>,
|
||||
row: Record<string, any>,
|
||||
): boolean => {
|
||||
return (
|
||||
row.strategy_id &&
|
||||
!feature.strategies?.find((s) => s?.id === row.strategy_id)
|
||||
);
|
||||
};
|
||||
|
||||
const isNewTag = (
|
||||
feature: PartialDeep<IFeatureToggleClient>,
|
||||
row: Record<string, any>,
|
||||
): 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<IFeatureToggleClient>,
|
||||
row: Record<string, any>,
|
||||
) => {
|
||||
feature.strategies
|
||||
?.find((s) => s?.id === row.strategy_id)
|
||||
?.constraints?.push(...row.segment_constraints);
|
||||
};
|
||||
|
||||
const addSegmentIdsToStrategy = (
|
||||
feature: PartialDeep<IFeatureToggleClient>,
|
||||
row: Record<string, any>,
|
||||
) => {
|
||||
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<string, any>): 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<string, any>,
|
||||
row: Record<string, any>,
|
||||
): void => {
|
||||
const tags = feature.tags || [];
|
||||
const newTag = rowToTag(row);
|
||||
feature.tags = [...tags, newTag];
|
||||
};
|
||||
|
||||
const rowToTag = (row: Record<string, any>): 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<FeatureToggle[]> {
|
||||
// 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<any>)[];
|
||||
|
||||
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<IFeatureToggleClient> = 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;
|
||||
|
@ -196,7 +196,7 @@ class FeatureController extends Controller {
|
||||
namePrefix,
|
||||
}: any): Promise<IFeatureToggleQuery> {
|
||||
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<FeaturesSchema>,
|
||||
): Promise<void> {
|
||||
const query = await this.prepQuery(req.query);
|
||||
|
||||
const { user } = req;
|
||||
const features = await this.service.getFeatureToggles(query, user.id);
|
||||
|
||||
|
@ -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: [],
|
||||
});
|
||||
});
|
||||
|
@ -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<FeatureToggle, string> {
|
||||
count(query?: Partial<IFeatureToggleQuery>): Promise<number>;
|
||||
count(query?: Partial<IFeatureToggleStoreQuery>): Promise<number>;
|
||||
setLastSeen(data: LastSeenInput[]): Promise<void>;
|
||||
getProjectId(name: string): Promise<string | undefined>;
|
||||
create(project: string, data: FeatureToggleDTO): Promise<FeatureToggle>;
|
||||
@ -28,8 +29,13 @@ export interface IFeatureToggleStore extends Store<FeatureToggle, string> {
|
||||
batchDelete(featureNames: string[]): Promise<void>;
|
||||
batchRevive(featureNames: string[]): Promise<FeatureToggle[]>;
|
||||
revive(featureName: string): Promise<FeatureToggle>;
|
||||
getAll(query?: Partial<IFeatureToggleQuery>): Promise<FeatureToggle[]>;
|
||||
getAll(query?: Partial<IFeatureToggleStoreQuery>): Promise<FeatureToggle[]>;
|
||||
getAllByNames(names: string[]): Promise<FeatureToggle[]>;
|
||||
getFeatureToggleList(
|
||||
featureQuery?: IFeatureToggleQuery,
|
||||
userId?: number,
|
||||
archived?: boolean,
|
||||
): Promise<FeatureToggle[]>;
|
||||
countByDate(queryModifiers: {
|
||||
archived?: boolean;
|
||||
project?: string;
|
||||
|
@ -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';
|
||||
|
@ -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();
|
||||
});
|
||||
|
||||
|
@ -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,
|
||||
};
|
||||
|
@ -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 = {
|
||||
|
@ -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;
|
||||
}
|
||||
|
@ -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;
|
||||
|
@ -48,6 +48,7 @@ process.nextTick(async () => {
|
||||
dependentFeatures: true,
|
||||
transactionalDecorator: true,
|
||||
useLastSeenRefactor: true,
|
||||
separateAdminClientApi: true,
|
||||
},
|
||||
},
|
||||
authentication: {
|
||||
|
@ -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/)
|
||||
|
@ -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;
|
||||
|
@ -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);
|
||||
});
|
||||
|
4
src/test/fixtures/store.ts
vendored
4
src/test/fixtures/store.ts
vendored
@ -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(),
|
||||
|
Loading…
Reference in New Issue
Block a user