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,
|
"privateProjects": false,
|
||||||
"proPlanAutoCharge": false,
|
"proPlanAutoCharge": false,
|
||||||
"responseTimeWithAppNameKillSwitch": false,
|
"responseTimeWithAppNameKillSwitch": false,
|
||||||
|
"separateAdminClientApi": false,
|
||||||
"strictSchemaValidation": false,
|
"strictSchemaValidation": false,
|
||||||
"transactionalDecorator": false,
|
"transactionalDecorator": false,
|
||||||
"useLastSeenRefactor": false,
|
"useLastSeenRefactor": false,
|
||||||
@ -153,6 +154,7 @@ exports[`should create default config 1`] = `
|
|||||||
"privateProjects": false,
|
"privateProjects": false,
|
||||||
"proPlanAutoCharge": false,
|
"proPlanAutoCharge": false,
|
||||||
"responseTimeWithAppNameKillSwitch": false,
|
"responseTimeWithAppNameKillSwitch": false,
|
||||||
|
"separateAdminClientApi": false,
|
||||||
"strictSchemaValidation": false,
|
"strictSchemaValidation": false,
|
||||||
"transactionalDecorator": false,
|
"transactionalDecorator": false,
|
||||||
"useLastSeenRefactor": false,
|
"useLastSeenRefactor": false,
|
||||||
|
@ -19,7 +19,7 @@ import { AccessStore } from './access-store';
|
|||||||
import { ResetTokenStore } from './reset-token-store';
|
import { ResetTokenStore } from './reset-token-store';
|
||||||
import UserFeedbackStore from './user-feedback-store';
|
import UserFeedbackStore from './user-feedback-store';
|
||||||
import FeatureStrategyStore from '../features/feature-toggle/feature-toggle-strategies-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 EnvironmentStore from './environment-store';
|
||||||
import FeatureTagStore from './feature-tag-store';
|
import FeatureTagStore from './feature-tag-store';
|
||||||
import { FeatureEnvironmentStore } from './feature-environment-store';
|
import { FeatureEnvironmentStore } from './feature-environment-store';
|
||||||
@ -91,7 +91,7 @@ export const createStores = (
|
|||||||
getLogger,
|
getLogger,
|
||||||
config.flagResolver,
|
config.flagResolver,
|
||||||
),
|
),
|
||||||
featureToggleClientStore: new FeatureToggleClientStore(
|
clientFeatureToggleStore: new FeatureToggleClientStore(
|
||||||
db,
|
db,
|
||||||
eventBus,
|
eventBus,
|
||||||
getLogger,
|
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 { Knex } from 'knex';
|
||||||
import metricsHelper from '../util/metrics-helper';
|
import metricsHelper from '../../util/metrics-helper';
|
||||||
import { DB_TIME } from '../metric-events';
|
import { DB_TIME } from '../../metric-events';
|
||||||
import { Logger, LogProvider } from '../logger';
|
import { Logger, LogProvider } from '../../logger';
|
||||||
import {
|
import {
|
||||||
IFeatureToggleClient,
|
IFeatureToggleClient,
|
||||||
IFeatureToggleClientStore,
|
IFeatureToggleClientStore,
|
||||||
@ -10,11 +10,11 @@ import {
|
|||||||
IStrategyConfig,
|
IStrategyConfig,
|
||||||
ITag,
|
ITag,
|
||||||
PartialDeep,
|
PartialDeep,
|
||||||
} from '../types';
|
} from '../../types';
|
||||||
import { DEFAULT_ENV, ensureStringValue, mapValues } from '../util';
|
import { DEFAULT_ENV, ensureStringValue, mapValues } from '../../util';
|
||||||
import EventEmitter from 'events';
|
import EventEmitter from 'events';
|
||||||
import FeatureToggleStore from '../features/feature-toggle/feature-toggle-store';
|
import FeatureToggleStore from '../feature-toggle/feature-toggle-store';
|
||||||
import { Db } from './db';
|
import { Db } from '../../db/db';
|
||||||
import Raw = Knex.Raw;
|
import Raw = Knex.Raw;
|
||||||
|
|
||||||
export interface IGetAllFeatures {
|
export interface IGetAllFeatures {
|
@ -2,17 +2,23 @@ import memoizee from 'memoizee';
|
|||||||
import { Response } from 'express';
|
import { Response } from 'express';
|
||||||
// eslint-disable-next-line import/no-extraneous-dependencies
|
// eslint-disable-next-line import/no-extraneous-dependencies
|
||||||
import hashSum from 'hash-sum';
|
import hashSum from 'hash-sum';
|
||||||
import Controller from '../controller';
|
import Controller from '../../routes/controller';
|
||||||
import { IClientSegment, IUnleashConfig, IUnleashServices } from '../../types';
|
import {
|
||||||
import FeatureToggleService from '../../features/feature-toggle/feature-toggle-service';
|
IClientSegment,
|
||||||
|
IFeatureToggleStore,
|
||||||
|
IFlagResolver,
|
||||||
|
IUnleashConfig,
|
||||||
|
IUnleashServices,
|
||||||
|
} from '../../types';
|
||||||
|
import FeatureToggleService from '../feature-toggle/feature-toggle-service';
|
||||||
import { Logger } from '../../logger';
|
import { Logger } from '../../logger';
|
||||||
import { querySchema } from '../../schema/feature-schema';
|
import { querySchema } from '../../schema/feature-schema';
|
||||||
import { IFeatureToggleQuery } from '../../types/model';
|
import { IFeatureToggleQuery } from '../../types/model';
|
||||||
import NotFoundError from '../../error/notfound-error';
|
import NotFoundError from '../../error/notfound-error';
|
||||||
import { IAuthRequest } from '../unleash-types';
|
import { IAuthRequest } from '../../routes/unleash-types';
|
||||||
import ApiUser from '../../types/api-user';
|
import ApiUser from '../../types/api-user';
|
||||||
import { ALL, isAllProjects } from '../../types/models/api-token';
|
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 { ClientSpecService } from '../../services/client-spec-service';
|
||||||
import { OpenApiService } from '../../services/openapi-service';
|
import { OpenApiService } from '../../services/openapi-service';
|
||||||
import { NONE } from '../../types/permissions';
|
import { NONE } from '../../types/permissions';
|
||||||
@ -27,7 +33,8 @@ import {
|
|||||||
ClientFeaturesSchema,
|
ClientFeaturesSchema,
|
||||||
} from '../../openapi/spec/client-features-schema';
|
} from '../../openapi/spec/client-features-schema';
|
||||||
import { ISegmentService } from '../../segments/segment-service-interface';
|
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;
|
const version = 2;
|
||||||
|
|
||||||
@ -45,7 +52,7 @@ interface IMeta {
|
|||||||
export default class FeatureController extends Controller {
|
export default class FeatureController extends Controller {
|
||||||
private readonly logger: Logger;
|
private readonly logger: Logger;
|
||||||
|
|
||||||
private featureToggleServiceV2: FeatureToggleService;
|
private clientFeatureToggleService: ClientFeatureToggleService;
|
||||||
|
|
||||||
private segmentService: ISegmentService;
|
private segmentService: ISegmentService;
|
||||||
|
|
||||||
@ -55,6 +62,10 @@ export default class FeatureController extends Controller {
|
|||||||
|
|
||||||
private configurationRevisionService: ConfigurationRevisionService;
|
private configurationRevisionService: ConfigurationRevisionService;
|
||||||
|
|
||||||
|
private featureToggleService: FeatureToggleService;
|
||||||
|
|
||||||
|
private flagResolver: IFlagResolver;
|
||||||
|
|
||||||
private featuresAndSegments: (
|
private featuresAndSegments: (
|
||||||
query: IFeatureToggleQuery,
|
query: IFeatureToggleQuery,
|
||||||
etag: string,
|
etag: string,
|
||||||
@ -62,28 +73,32 @@ export default class FeatureController extends Controller {
|
|||||||
|
|
||||||
constructor(
|
constructor(
|
||||||
{
|
{
|
||||||
featureToggleServiceV2,
|
clientFeatureToggleService,
|
||||||
segmentService,
|
segmentService,
|
||||||
clientSpecService,
|
clientSpecService,
|
||||||
openApiService,
|
openApiService,
|
||||||
configurationRevisionService,
|
configurationRevisionService,
|
||||||
|
featureToggleService,
|
||||||
}: Pick<
|
}: Pick<
|
||||||
IUnleashServices,
|
IUnleashServices,
|
||||||
| 'featureToggleServiceV2'
|
| 'clientFeatureToggleService'
|
||||||
| 'segmentService'
|
| 'segmentService'
|
||||||
| 'clientSpecService'
|
| 'clientSpecService'
|
||||||
| 'openApiService'
|
| 'openApiService'
|
||||||
| 'configurationRevisionService'
|
| 'configurationRevisionService'
|
||||||
|
| 'featureToggleService'
|
||||||
>,
|
>,
|
||||||
config: IUnleashConfig,
|
config: IUnleashConfig,
|
||||||
) {
|
) {
|
||||||
super(config);
|
super(config);
|
||||||
const { clientFeatureCaching } = config;
|
const { clientFeatureCaching } = config;
|
||||||
this.featureToggleServiceV2 = featureToggleServiceV2;
|
this.clientFeatureToggleService = clientFeatureToggleService;
|
||||||
this.segmentService = segmentService;
|
this.segmentService = segmentService;
|
||||||
this.clientSpecService = clientSpecService;
|
this.clientSpecService = clientSpecService;
|
||||||
this.openApiService = openApiService;
|
this.openApiService = openApiService;
|
||||||
this.configurationRevisionService = configurationRevisionService;
|
this.configurationRevisionService = configurationRevisionService;
|
||||||
|
this.featureToggleService = featureToggleService;
|
||||||
|
this.flagResolver = config.flagResolver;
|
||||||
this.logger = config.getLogger('client-api/feature.js');
|
this.logger = config.getLogger('client-api/feature.js');
|
||||||
|
|
||||||
this.route({
|
this.route({
|
||||||
@ -146,8 +161,15 @@ export default class FeatureController extends Controller {
|
|||||||
private async resolveFeaturesAndSegments(
|
private async resolveFeaturesAndSegments(
|
||||||
query?: IFeatureToggleQuery,
|
query?: IFeatureToggleQuery,
|
||||||
): Promise<[FeatureConfigurationClient[], IClientSegment[]]> {
|
): Promise<[FeatureConfigurationClient[], IClientSegment[]]> {
|
||||||
|
if (this.flagResolver.isEnabled('separateAdminClientApi')) {
|
||||||
return Promise.all([
|
return Promise.all([
|
||||||
this.featureToggleServiceV2.getClientFeatures(query),
|
this.clientFeatureToggleService.getClientFeatures(query),
|
||||||
|
this.segmentService.getActiveForClient(),
|
||||||
|
]);
|
||||||
|
}
|
||||||
|
|
||||||
|
return Promise.all([
|
||||||
|
this.featureToggleService.getClientFeatures(query),
|
||||||
this.segmentService.getActiveForClient(),
|
this.segmentService.getActiveForClient(),
|
||||||
]);
|
]);
|
||||||
}
|
}
|
||||||
@ -287,7 +309,15 @@ export default class FeatureController extends Controller {
|
|||||||
const name = req.params.featureName;
|
const name = req.params.featureName;
|
||||||
const featureQuery = await this.resolveQuery(req);
|
const featureQuery = await this.resolveQuery(req);
|
||||||
const q = { ...featureQuery, namePrefix: name };
|
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);
|
const toggle = toggles.find((t) => t.name === name);
|
||||||
if (!toggle) {
|
if (!toggle) {
|
||||||
throw new NotFoundError(`Could not find feature toggle ${name}`);
|
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,
|
FeatureToggle,
|
||||||
IFeatureToggleClient,
|
IFeatureToggleClient,
|
||||||
IFeatureToggleQuery,
|
IFeatureToggleQuery,
|
||||||
} from '../../lib/types/model';
|
} from '../../../types/model';
|
||||||
import { IFeatureToggleClientStore } from '../../lib/types/stores/feature-toggle-client-store';
|
import { IFeatureToggleClientStore } from '../types/client-feature-toggle-store-type';
|
||||||
import { IGetAdminFeatures } from '../../lib/db/feature-toggle-client-store';
|
import { IGetAdminFeatures } from '../client-feature-toggle-store';
|
||||||
|
|
||||||
export default class FakeFeatureToggleClientStore
|
export default class FakeClientFeatureToggleStore
|
||||||
implements IFeatureToggleClientStore
|
implements IFeatureToggleClientStore
|
||||||
{
|
{
|
||||||
featureToggles: FeatureToggle[] = [];
|
featureToggles: FeatureToggle[] = [];
|
||||||
@ -34,6 +34,7 @@ export default class FakeFeatureToggleClientStore
|
|||||||
}
|
}
|
||||||
return toggle.archived === archived;
|
return toggle.archived === archived;
|
||||||
});
|
});
|
||||||
|
|
||||||
const clientRows: IFeatureToggleClient[] = rows.map((t) => ({
|
const clientRows: IFeatureToggleClient[] = rows.map((t) => ({
|
||||||
...t,
|
...t,
|
||||||
enabled: true,
|
enabled: true,
|
||||||
@ -81,6 +82,7 @@ export default class FakeFeatureToggleClientStore
|
|||||||
archived: false,
|
archived: false,
|
||||||
...feature,
|
...feature,
|
||||||
});
|
});
|
||||||
|
|
||||||
return Promise.resolve();
|
return Promise.resolve();
|
||||||
}
|
}
|
||||||
}
|
}
|
@ -1,12 +1,14 @@
|
|||||||
import supertest from 'supertest';
|
import supertest from 'supertest';
|
||||||
import createStores from '../../../test/fixtures/store';
|
import createStores from '../../../../test/fixtures/store';
|
||||||
import getLogger from '../../../test/fixtures/no-logger';
|
import getLogger from '../../../../test/fixtures/no-logger';
|
||||||
import getApp from '../../app';
|
import getApp from '../../../app';
|
||||||
import { createServices } from '../../services';
|
import { createServices } from '../../../services';
|
||||||
import FeatureController from './feature';
|
import FeatureController from '../client-feature-toggle.controller';
|
||||||
import { createTestConfig } from '../../../test/config/test-config';
|
import { createTestConfig } from '../../../../test/config/test-config';
|
||||||
import { secondsToMilliseconds } from 'date-fns';
|
import { secondsToMilliseconds } from 'date-fns';
|
||||||
import { ClientSpecService } from '../../services/client-spec-service';
|
import { ClientSpecService } from '../../../services/client-spec-service';
|
||||||
|
|
||||||
|
let app;
|
||||||
|
|
||||||
async function getSetup() {
|
async function getSetup() {
|
||||||
const base = `/random${Math.round(Math.random() * 1000)}`;
|
const base = `/random${Math.round(Math.random() * 1000)}`;
|
||||||
@ -16,12 +18,11 @@ async function getSetup() {
|
|||||||
});
|
});
|
||||||
const services = createServices(stores, config);
|
const services = createServices(stores, config);
|
||||||
|
|
||||||
const app = await getApp(config, stores, services);
|
app = await getApp(config, stores, services);
|
||||||
|
|
||||||
return {
|
return {
|
||||||
base,
|
base,
|
||||||
featureToggleStore: stores.featureToggleStore,
|
clientFeatureToggleStore: stores.clientFeatureToggleStore,
|
||||||
featureToggleClientStore: stores.featureToggleClientStore,
|
|
||||||
request: supertest(app),
|
request: supertest(app),
|
||||||
destroy: () => {
|
destroy: () => {
|
||||||
services.versionService.destroy();
|
services.versionService.destroy();
|
||||||
@ -44,7 +45,6 @@ const callGetAll = async (controller: FeatureController) => {
|
|||||||
let base;
|
let base;
|
||||||
let request;
|
let request;
|
||||||
let destroy;
|
let destroy;
|
||||||
let featureToggleClientStore;
|
|
||||||
|
|
||||||
let flagResolver;
|
let flagResolver;
|
||||||
|
|
||||||
@ -52,7 +52,6 @@ beforeEach(async () => {
|
|||||||
const setup = await getSetup();
|
const setup = await getSetup();
|
||||||
base = setup.base;
|
base = setup.base;
|
||||||
request = setup.request;
|
request = setup.request;
|
||||||
featureToggleClientStore = setup.featureToggleClientStore;
|
|
||||||
destroy = setup.destroy;
|
destroy = setup.destroy;
|
||||||
flagResolver = {
|
flagResolver = {
|
||||||
isEnabled: () => {
|
isEnabled: () => {
|
||||||
@ -84,7 +83,8 @@ test('if caching is enabled should memoize', async () => {
|
|||||||
const validPath = jest.fn().mockReturnValue(jest.fn());
|
const validPath = jest.fn().mockReturnValue(jest.fn());
|
||||||
const clientSpecService = new ClientSpecService({ getLogger });
|
const clientSpecService = new ClientSpecService({ getLogger });
|
||||||
const openApiService = { respondWithValidation, validPath };
|
const openApiService = { respondWithValidation, validPath };
|
||||||
const featureToggleServiceV2 = { getClientFeatures };
|
const clientFeatureToggleService = { getClientFeatures };
|
||||||
|
const featureToggleService = { getClientFeatures };
|
||||||
const segmentService = { getActive, getActiveForClient };
|
const segmentService = { getActive, getActiveForClient };
|
||||||
const configurationRevisionService = { getMaxRevisionId: () => 1 };
|
const configurationRevisionService = { getMaxRevisionId: () => 1 };
|
||||||
|
|
||||||
@ -94,7 +94,9 @@ test('if caching is enabled should memoize', async () => {
|
|||||||
// @ts-expect-error due to partial implementation
|
// @ts-expect-error due to partial implementation
|
||||||
openApiService,
|
openApiService,
|
||||||
// @ts-expect-error due to partial implementation
|
// @ts-expect-error due to partial implementation
|
||||||
featureToggleServiceV2,
|
clientFeatureToggleService,
|
||||||
|
// @ts-expect-error due to partial implementation
|
||||||
|
featureToggleService,
|
||||||
// @ts-expect-error due to partial implementation
|
// @ts-expect-error due to partial implementation
|
||||||
segmentService,
|
segmentService,
|
||||||
// @ts-expect-error due to partial implementation
|
// @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 respondWithValidation = jest.fn().mockReturnValue({});
|
||||||
const validPath = jest.fn().mockReturnValue(jest.fn());
|
const validPath = jest.fn().mockReturnValue(jest.fn());
|
||||||
const clientSpecService = new ClientSpecService({ getLogger });
|
const clientSpecService = new ClientSpecService({ getLogger });
|
||||||
const featureToggleServiceV2 = { getClientFeatures };
|
const clientFeatureToggleService = { getClientFeatures };
|
||||||
const segmentService = { getActive, getActiveForClient };
|
const segmentService = { getActive, getActiveForClient };
|
||||||
|
const featureToggleService = { getClientFeatures };
|
||||||
const openApiService = { respondWithValidation, validPath };
|
const openApiService = { respondWithValidation, validPath };
|
||||||
const configurationRevisionService = { getMaxRevisionId: () => 1 };
|
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
|
// @ts-expect-error due to partial implementation
|
||||||
openApiService,
|
openApiService,
|
||||||
// @ts-expect-error due to partial implementation
|
// @ts-expect-error due to partial implementation
|
||||||
featureToggleServiceV2,
|
clientFeatureToggleService,
|
||||||
|
// @ts-expect-error due to partial implementation
|
||||||
|
featureToggleService,
|
||||||
// @ts-expect-error due to partial implementation
|
// @ts-expect-error due to partial implementation
|
||||||
segmentService,
|
segmentService,
|
||||||
// @ts-expect-error due to partial implementation
|
// @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);
|
await callGetAll(controller);
|
||||||
expect(getClientFeatures).toHaveBeenCalledTimes(2);
|
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 {
|
||||||
import { IGetAdminFeatures } from '../../db/feature-toggle-client-store';
|
IFeatureToggleClient,
|
||||||
|
IFeatureToggleQuery,
|
||||||
|
} from '../../../types/model';
|
||||||
|
import { IGetAdminFeatures } from '../client-feature-toggle-store';
|
||||||
|
|
||||||
export interface IFeatureToggleClientStore {
|
export interface IFeatureToggleClientStore {
|
||||||
getClient(
|
getClient(
|
@ -6,7 +6,7 @@ import {
|
|||||||
} from '../../services';
|
} from '../../services';
|
||||||
import FeatureStrategiesStore from './feature-toggle-strategies-store';
|
import FeatureStrategiesStore from './feature-toggle-strategies-store';
|
||||||
import FeatureToggleStore from './feature-toggle-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 ProjectStore from '../../db/project-store';
|
||||||
import { FeatureEnvironmentStore } from '../../db/feature-environment-store';
|
import { FeatureEnvironmentStore } from '../../db/feature-environment-store';
|
||||||
import ContextFieldStore from '../../db/context-field-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 FakeEventStore from '../../../test/fixtures/fake-event-store';
|
||||||
import FakeFeatureStrategiesStore from './fakes/fake-feature-strategies-store';
|
import FakeFeatureStrategiesStore from './fakes/fake-feature-strategies-store';
|
||||||
import FakeFeatureToggleStore from './fakes/fake-feature-toggle-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 FakeProjectStore from '../../../test/fixtures/fake-project-store';
|
||||||
import FakeFeatureEnvironmentStore from '../../../test/fixtures/fake-feature-environment-store';
|
import FakeFeatureEnvironmentStore from '../../../test/fixtures/fake-feature-environment-store';
|
||||||
import FakeContextFieldStore from '../../../test/fixtures/fake-context-field-store';
|
import FakeContextFieldStore from '../../../test/fixtures/fake-context-field-store';
|
||||||
@ -125,7 +125,7 @@ export const createFeatureToggleService = (
|
|||||||
{
|
{
|
||||||
featureStrategiesStore,
|
featureStrategiesStore,
|
||||||
featureToggleStore,
|
featureToggleStore,
|
||||||
featureToggleClientStore,
|
clientFeatureToggleStore: featureToggleClientStore,
|
||||||
projectStore,
|
projectStore,
|
||||||
featureTagStore,
|
featureTagStore,
|
||||||
featureEnvironmentStore,
|
featureEnvironmentStore,
|
||||||
@ -152,7 +152,7 @@ export const createFakeFeatureToggleService = (
|
|||||||
const strategyStore = new FakeStrategiesStore();
|
const strategyStore = new FakeStrategiesStore();
|
||||||
const featureStrategiesStore = new FakeFeatureStrategiesStore();
|
const featureStrategiesStore = new FakeFeatureStrategiesStore();
|
||||||
const featureToggleStore = new FakeFeatureToggleStore();
|
const featureToggleStore = new FakeFeatureToggleStore();
|
||||||
const featureToggleClientStore = new FakeFeatureToggleClientStore();
|
const featureToggleClientStore = new FakeClientFeatureToggleStore();
|
||||||
const projectStore = new FakeProjectStore();
|
const projectStore = new FakeProjectStore();
|
||||||
const featureEnvironmentStore = new FakeFeatureEnvironmentStore();
|
const featureEnvironmentStore = new FakeFeatureEnvironmentStore();
|
||||||
const contextFieldStore = new FakeContextFieldStore();
|
const contextFieldStore = new FakeContextFieldStore();
|
||||||
@ -185,7 +185,7 @@ export const createFakeFeatureToggleService = (
|
|||||||
{
|
{
|
||||||
featureStrategiesStore,
|
featureStrategiesStore,
|
||||||
featureToggleStore,
|
featureToggleStore,
|
||||||
featureToggleClientStore,
|
clientFeatureToggleStore: featureToggleClientStore,
|
||||||
projectStore,
|
projectStore,
|
||||||
featureTagStore,
|
featureTagStore,
|
||||||
featureEnvironmentStore,
|
featureEnvironmentStore,
|
||||||
|
@ -1,5 +1,5 @@
|
|||||||
import {
|
import {
|
||||||
IFeatureToggleQuery,
|
IFeatureToggleStoreQuery,
|
||||||
IFeatureToggleStore,
|
IFeatureToggleStore,
|
||||||
} from '../types/feature-toggle-store-type';
|
} from '../types/feature-toggle-store-type';
|
||||||
import NotFoundError from '../../../error/notfound-error';
|
import NotFoundError from '../../../error/notfound-error';
|
||||||
@ -7,6 +7,7 @@ import {
|
|||||||
FeatureToggle,
|
FeatureToggle,
|
||||||
FeatureToggleDTO,
|
FeatureToggleDTO,
|
||||||
IFeatureEnvironment,
|
IFeatureEnvironment,
|
||||||
|
IFeatureToggleQuery,
|
||||||
IVariant,
|
IVariant,
|
||||||
} from 'lib/types/model';
|
} from 'lib/types/model';
|
||||||
import { LastSeenInput } from '../../../services/client-metrics/last-seen/last-seen-service';
|
import { LastSeenInput } from '../../../services/client-metrics/last-seen/last-seen-service';
|
||||||
@ -66,7 +67,7 @@ export default class FakeFeatureToggleStore implements IFeatureToggleStore {
|
|||||||
return features;
|
return features;
|
||||||
}
|
}
|
||||||
|
|
||||||
async count(query: Partial<IFeatureToggleQuery>): Promise<number> {
|
async count(query: Partial<IFeatureToggleStoreQuery>): Promise<number> {
|
||||||
return this.features.filter(this.getFilterQuery(query)).length;
|
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);
|
return this.get(name).then((f) => f.project);
|
||||||
}
|
}
|
||||||
|
|
||||||
private getFilterQuery(query: Partial<IFeatureToggleQuery>) {
|
private getFilterQuery(query: Partial<IFeatureToggleStoreQuery>) {
|
||||||
return (f) => {
|
return (f) => {
|
||||||
let projectMatch = true;
|
let projectMatch = true;
|
||||||
if (query.project) {
|
if (query.project) {
|
||||||
@ -135,7 +136,9 @@ export default class FakeFeatureToggleStore implements IFeatureToggleStore {
|
|||||||
return this.get(name);
|
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));
|
return this.features.filter(this.getFilterQuery(query));
|
||||||
}
|
}
|
||||||
|
|
||||||
@ -147,6 +150,14 @@ export default class FakeFeatureToggleStore implements IFeatureToggleStore {
|
|||||||
return this.update(revive.project, revive);
|
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(
|
async update(
|
||||||
project: string,
|
project: string,
|
||||||
data: FeatureToggleDTO,
|
data: FeatureToggleDTO,
|
||||||
|
@ -140,7 +140,7 @@ class FeatureToggleService {
|
|||||||
|
|
||||||
private featureToggleStore: IFeatureToggleStore;
|
private featureToggleStore: IFeatureToggleStore;
|
||||||
|
|
||||||
private featureToggleClientStore: IFeatureToggleClientStore;
|
private clientFeatureToggleStore: IFeatureToggleClientStore;
|
||||||
|
|
||||||
private tagStore: IFeatureTagStore;
|
private tagStore: IFeatureTagStore;
|
||||||
|
|
||||||
@ -170,7 +170,7 @@ class FeatureToggleService {
|
|||||||
{
|
{
|
||||||
featureStrategiesStore,
|
featureStrategiesStore,
|
||||||
featureToggleStore,
|
featureToggleStore,
|
||||||
featureToggleClientStore,
|
clientFeatureToggleStore,
|
||||||
projectStore,
|
projectStore,
|
||||||
featureTagStore,
|
featureTagStore,
|
||||||
featureEnvironmentStore,
|
featureEnvironmentStore,
|
||||||
@ -180,7 +180,7 @@ class FeatureToggleService {
|
|||||||
IUnleashStores,
|
IUnleashStores,
|
||||||
| 'featureStrategiesStore'
|
| 'featureStrategiesStore'
|
||||||
| 'featureToggleStore'
|
| 'featureToggleStore'
|
||||||
| 'featureToggleClientStore'
|
| 'clientFeatureToggleStore'
|
||||||
| 'projectStore'
|
| 'projectStore'
|
||||||
| 'featureTagStore'
|
| 'featureTagStore'
|
||||||
| 'featureEnvironmentStore'
|
| 'featureEnvironmentStore'
|
||||||
@ -203,7 +203,7 @@ class FeatureToggleService {
|
|||||||
this.featureStrategiesStore = featureStrategiesStore;
|
this.featureStrategiesStore = featureStrategiesStore;
|
||||||
this.strategyStore = strategyStore;
|
this.strategyStore = strategyStore;
|
||||||
this.featureToggleStore = featureToggleStore;
|
this.featureToggleStore = featureToggleStore;
|
||||||
this.featureToggleClientStore = featureToggleClientStore;
|
this.clientFeatureToggleStore = clientFeatureToggleStore;
|
||||||
this.tagStore = featureTagStore;
|
this.tagStore = featureTagStore;
|
||||||
this.projectStore = projectStore;
|
this.projectStore = projectStore;
|
||||||
this.featureEnvironmentStore = featureEnvironmentStore;
|
this.featureEnvironmentStore = featureEnvironmentStore;
|
||||||
@ -1016,7 +1016,7 @@ class FeatureToggleService {
|
|||||||
async getClientFeatures(
|
async getClientFeatures(
|
||||||
query?: IFeatureToggleQuery,
|
query?: IFeatureToggleQuery,
|
||||||
): Promise<FeatureConfigurationClient[]> {
|
): Promise<FeatureConfigurationClient[]> {
|
||||||
const result = await this.featureToggleClientStore.getClient(
|
const result = await this.clientFeatureToggleStore.getClient(
|
||||||
query || {},
|
query || {},
|
||||||
);
|
);
|
||||||
return result.map(
|
return result.map(
|
||||||
@ -1049,7 +1049,7 @@ class FeatureToggleService {
|
|||||||
async getPlaygroundFeatures(
|
async getPlaygroundFeatures(
|
||||||
query?: IFeatureToggleQuery,
|
query?: IFeatureToggleQuery,
|
||||||
): Promise<FeatureConfigurationClient[]> {
|
): Promise<FeatureConfigurationClient[]> {
|
||||||
const result = await this.featureToggleClientStore.getPlayground(
|
const result = await this.clientFeatureToggleStore.getPlayground(
|
||||||
query || {},
|
query || {},
|
||||||
);
|
);
|
||||||
return result;
|
return result;
|
||||||
@ -1068,11 +1068,19 @@ class FeatureToggleService {
|
|||||||
userId?: number,
|
userId?: number,
|
||||||
archived: boolean = false,
|
archived: boolean = false,
|
||||||
): Promise<FeatureToggle[]> {
|
): Promise<FeatureToggle[]> {
|
||||||
const features = await this.featureToggleClientStore.getAdmin({
|
let features = (await this.clientFeatureToggleStore.getAdmin({
|
||||||
featureQuery: query,
|
featureQuery: query,
|
||||||
|
userId: userId,
|
||||||
|
archived: false,
|
||||||
|
})) as FeatureToggle[];
|
||||||
|
|
||||||
|
if (this.flagResolver.isEnabled('separateAdminClientApi')) {
|
||||||
|
features = await this.featureToggleStore.getFeatureToggleList(
|
||||||
|
query,
|
||||||
userId,
|
userId,
|
||||||
archived,
|
archived,
|
||||||
});
|
);
|
||||||
|
}
|
||||||
|
|
||||||
if (this.flagResolver.isEnabled('privateProjects') && userId) {
|
if (this.flagResolver.isEnabled('privateProjects') && userId) {
|
||||||
const projectAccess =
|
const projectAccess =
|
||||||
|
@ -4,11 +4,23 @@ import metricsHelper from '../../util/metrics-helper';
|
|||||||
import { DB_TIME } from '../../metric-events';
|
import { DB_TIME } from '../../metric-events';
|
||||||
import NotFoundError from '../../error/notfound-error';
|
import NotFoundError from '../../error/notfound-error';
|
||||||
import { Logger, LogProvider } from '../../logger';
|
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 { IFeatureToggleStore } from './types/feature-toggle-store-type';
|
||||||
import { Db } from '../../db/db';
|
import { Db } from '../../db/db';
|
||||||
import { LastSeenInput } from '../../services/client-metrics/last-seen/last-seen-service';
|
import { LastSeenInput } from '../../services/client-metrics/last-seen/last-seen-service';
|
||||||
import { NameExistsError } from '../../error';
|
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[] };
|
export type EnvironmentFeatureNames = { [key: string]: string[] };
|
||||||
|
|
||||||
@ -44,6 +56,81 @@ interface VariantDTO {
|
|||||||
const TABLE = 'features';
|
const TABLE = 'features';
|
||||||
const FEATURE_ENVIRONMENTS_TABLE = 'feature_environments';
|
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 {
|
export default class FeatureToggleStore implements IFeatureToggleStore {
|
||||||
private db: Db;
|
private db: Db;
|
||||||
|
|
||||||
@ -91,6 +178,132 @@ export default class FeatureToggleStore implements IFeatureToggleStore {
|
|||||||
.then(this.rowToFeature);
|
.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(
|
async getAll(
|
||||||
query: {
|
query: {
|
||||||
archived?: boolean;
|
archived?: boolean;
|
||||||
|
@ -196,7 +196,7 @@ class FeatureController extends Controller {
|
|||||||
namePrefix,
|
namePrefix,
|
||||||
}: any): Promise<IFeatureToggleQuery> {
|
}: any): Promise<IFeatureToggleQuery> {
|
||||||
if (!tag && !project && !namePrefix) {
|
if (!tag && !project && !namePrefix) {
|
||||||
return null;
|
return {};
|
||||||
}
|
}
|
||||||
const tagQuery = this.paramToArray(tag);
|
const tagQuery = this.paramToArray(tag);
|
||||||
const projectQuery = this.paramToArray(project);
|
const projectQuery = this.paramToArray(project);
|
||||||
@ -216,6 +216,7 @@ class FeatureController extends Controller {
|
|||||||
res: Response<FeaturesSchema>,
|
res: Response<FeaturesSchema>,
|
||||||
): Promise<void> {
|
): Promise<void> {
|
||||||
const query = await this.prepQuery(req.query);
|
const query = await this.prepQuery(req.query);
|
||||||
|
|
||||||
const { user } = req;
|
const { user } = req;
|
||||||
const features = await this.service.getFeatureToggles(query, user.id);
|
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,
|
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 {
|
import {
|
||||||
FeatureToggle,
|
FeatureToggle,
|
||||||
FeatureToggleDTO,
|
FeatureToggleDTO,
|
||||||
|
IFeatureToggleQuery,
|
||||||
IVariant,
|
IVariant,
|
||||||
} from '../../../types/model';
|
} from '../../../types/model';
|
||||||
import { Store } from '../../../types/stores/store';
|
import { Store } from '../../../types/stores/store';
|
||||||
import { LastSeenInput } from '../../../services/client-metrics/last-seen/last-seen-service';
|
import { LastSeenInput } from '../../../services/client-metrics/last-seen/last-seen-service';
|
||||||
|
|
||||||
export interface IFeatureToggleQuery {
|
export interface IFeatureToggleStoreQuery {
|
||||||
archived: boolean;
|
archived: boolean;
|
||||||
project: string;
|
project: string;
|
||||||
stale: boolean;
|
stale: boolean;
|
||||||
@ -14,7 +15,7 @@ export interface IFeatureToggleQuery {
|
|||||||
}
|
}
|
||||||
|
|
||||||
export interface IFeatureToggleStore extends Store<FeatureToggle, string> {
|
export interface IFeatureToggleStore extends Store<FeatureToggle, string> {
|
||||||
count(query?: Partial<IFeatureToggleQuery>): Promise<number>;
|
count(query?: Partial<IFeatureToggleStoreQuery>): Promise<number>;
|
||||||
setLastSeen(data: LastSeenInput[]): Promise<void>;
|
setLastSeen(data: LastSeenInput[]): Promise<void>;
|
||||||
getProjectId(name: string): Promise<string | undefined>;
|
getProjectId(name: string): Promise<string | undefined>;
|
||||||
create(project: string, data: FeatureToggleDTO): Promise<FeatureToggle>;
|
create(project: string, data: FeatureToggleDTO): Promise<FeatureToggle>;
|
||||||
@ -28,8 +29,13 @@ export interface IFeatureToggleStore extends Store<FeatureToggle, string> {
|
|||||||
batchDelete(featureNames: string[]): Promise<void>;
|
batchDelete(featureNames: string[]): Promise<void>;
|
||||||
batchRevive(featureNames: string[]): Promise<FeatureToggle[]>;
|
batchRevive(featureNames: string[]): Promise<FeatureToggle[]>;
|
||||||
revive(featureName: 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[]>;
|
getAllByNames(names: string[]): Promise<FeatureToggle[]>;
|
||||||
|
getFeatureToggleList(
|
||||||
|
featureQuery?: IFeatureToggleQuery,
|
||||||
|
userId?: number,
|
||||||
|
archived?: boolean,
|
||||||
|
): Promise<FeatureToggle[]>;
|
||||||
countByDate(queryModifiers: {
|
countByDate(queryModifiers: {
|
||||||
archived?: boolean;
|
archived?: boolean;
|
||||||
project?: string;
|
project?: string;
|
||||||
|
@ -1,5 +1,5 @@
|
|||||||
import Controller from '../controller';
|
import Controller from '../controller';
|
||||||
import FeatureController from './feature';
|
import FeatureController from '../../features/client-feature-toggles/client-feature-toggle.controller';
|
||||||
import MetricsController from './metrics';
|
import MetricsController from './metrics';
|
||||||
import RegisterController from './register';
|
import RegisterController from './register';
|
||||||
import { IUnleashConfig, IUnleashServices } from '../../types';
|
import { IUnleashConfig, IUnleashServices } from '../../types';
|
||||||
|
@ -211,8 +211,6 @@ test('should set lastSeen on toggle', async () => {
|
|||||||
await services.lastSeenService.store();
|
await services.lastSeenService.store();
|
||||||
const toggle = await stores.featureToggleStore.get('toggleLastSeen');
|
const toggle = await stores.featureToggleStore.get('toggleLastSeen');
|
||||||
|
|
||||||
console.log(toggle);
|
|
||||||
|
|
||||||
expect(toggle.lastSeenAt).toBeTruthy();
|
expect(toggle.lastSeenAt).toBeTruthy();
|
||||||
});
|
});
|
||||||
|
|
||||||
|
@ -89,6 +89,11 @@ import {
|
|||||||
createFakeGetProductionChanges,
|
createFakeGetProductionChanges,
|
||||||
createGetProductionChanges,
|
createGetProductionChanges,
|
||||||
} from '../features/instance-stats/getProductionChanges';
|
} 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
|
// TODO: will be moved to scheduler feature directory
|
||||||
export const scheduleServices = async (
|
export const scheduleServices = async (
|
||||||
@ -322,6 +327,10 @@ export const createServices = (
|
|||||||
config,
|
config,
|
||||||
);
|
);
|
||||||
|
|
||||||
|
const clientFeatureToggleService = db
|
||||||
|
? createClientFeatureToggleService(db, config)
|
||||||
|
: createFakeClientFeatureToggleService(config);
|
||||||
|
|
||||||
const proxyService = new ProxyService(config, stores, {
|
const proxyService = new ProxyService(config, stores, {
|
||||||
featureToggleServiceV2,
|
featureToggleServiceV2,
|
||||||
clientMetricsServiceV2,
|
clientMetricsServiceV2,
|
||||||
@ -413,6 +422,7 @@ export const createServices = (
|
|||||||
privateProjectChecker,
|
privateProjectChecker,
|
||||||
dependentFeaturesService,
|
dependentFeaturesService,
|
||||||
transactionalDependentFeaturesService,
|
transactionalDependentFeaturesService,
|
||||||
|
clientFeatureToggleService,
|
||||||
};
|
};
|
||||||
};
|
};
|
||||||
|
|
||||||
@ -457,4 +467,5 @@ export {
|
|||||||
FavoritesService,
|
FavoritesService,
|
||||||
SchedulerService,
|
SchedulerService,
|
||||||
DependentFeaturesService,
|
DependentFeaturesService,
|
||||||
|
ClientFeatureToggleService,
|
||||||
};
|
};
|
||||||
|
@ -35,7 +35,9 @@ export type IFlagKey =
|
|||||||
| 'disableMetrics'
|
| 'disableMetrics'
|
||||||
| 'transactionalDecorator'
|
| 'transactionalDecorator'
|
||||||
| 'useLastSeenRefactor'
|
| 'useLastSeenRefactor'
|
||||||
| 'internalMessageBanners';
|
| 'internalMessageBanners'
|
||||||
|
| 'internalMessageBanner'
|
||||||
|
| 'separateAdminClientApi';
|
||||||
|
|
||||||
export type IFlags = Partial<{ [key in IFlagKey]: boolean | Variant }>;
|
export type IFlags = Partial<{ [key in IFlagKey]: boolean | Variant }>;
|
||||||
|
|
||||||
@ -167,6 +169,10 @@ const flags: IFlags = {
|
|||||||
process.env.UNLEASH_EXPERIMENTAL_INTERNAL_MESSAGE_BANNERS,
|
process.env.UNLEASH_EXPERIMENTAL_INTERNAL_MESSAGE_BANNERS,
|
||||||
false,
|
false,
|
||||||
),
|
),
|
||||||
|
separateAdminClientApi: parseEnvVarBoolean(
|
||||||
|
process.env.UNLEASH_EXPERIMENTAL_SEPARATE_ADMIN_CLIENT_API,
|
||||||
|
false,
|
||||||
|
),
|
||||||
};
|
};
|
||||||
|
|
||||||
export const defaultExperimentalOptions: IExperimentalOptions = {
|
export const defaultExperimentalOptions: IExperimentalOptions = {
|
||||||
|
@ -49,6 +49,7 @@ import EventAnnouncerService from 'lib/services/event-announcer-service';
|
|||||||
import { IPrivateProjectChecker } from '../features/private-project/privateProjectCheckerType';
|
import { IPrivateProjectChecker } from '../features/private-project/privateProjectCheckerType';
|
||||||
import { DependentFeaturesService } from '../features/dependent-features/dependent-features-service';
|
import { DependentFeaturesService } from '../features/dependent-features/dependent-features-service';
|
||||||
import { WithTransactional } from 'lib/db/transaction';
|
import { WithTransactional } from 'lib/db/transaction';
|
||||||
|
import { ClientFeatureToggleService } from 'lib/features/client-feature-toggles/client-feature-toggle-service';
|
||||||
|
|
||||||
export interface IUnleashServices {
|
export interface IUnleashServices {
|
||||||
accessService: AccessService;
|
accessService: AccessService;
|
||||||
@ -108,4 +109,5 @@ export interface IUnleashServices {
|
|||||||
transactionalDependentFeaturesService: (
|
transactionalDependentFeaturesService: (
|
||||||
db: Knex.Transaction,
|
db: Knex.Transaction,
|
||||||
) => DependentFeaturesService;
|
) => DependentFeaturesService;
|
||||||
|
clientFeatureToggleService: ClientFeatureToggleService;
|
||||||
}
|
}
|
||||||
|
@ -20,7 +20,7 @@ import { IUserFeedbackStore } from './stores/user-feedback-store';
|
|||||||
import { IFeatureEnvironmentStore } from './stores/feature-environment-store';
|
import { IFeatureEnvironmentStore } from './stores/feature-environment-store';
|
||||||
import { IFeatureStrategiesStore } from '../features/feature-toggle/types/feature-toggle-strategies-store-type';
|
import { IFeatureStrategiesStore } from '../features/feature-toggle/types/feature-toggle-strategies-store-type';
|
||||||
import { IEnvironmentStore } from './stores/environment-store';
|
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 { IClientMetricsStoreV2 } from './stores/client-metrics-store-v2';
|
||||||
import { IUserSplashStore } from './stores/user-splash-store';
|
import { IUserSplashStore } from './stores/user-splash-store';
|
||||||
import { IRoleStore } from './stores/role-store';
|
import { IRoleStore } from './stores/role-store';
|
||||||
@ -52,7 +52,7 @@ export interface IUnleashStores {
|
|||||||
featureStrategiesStore: IFeatureStrategiesStore;
|
featureStrategiesStore: IFeatureStrategiesStore;
|
||||||
featureTagStore: IFeatureTagStore;
|
featureTagStore: IFeatureTagStore;
|
||||||
featureToggleStore: IFeatureToggleStore;
|
featureToggleStore: IFeatureToggleStore;
|
||||||
featureToggleClientStore: IFeatureToggleClientStore;
|
clientFeatureToggleStore: IFeatureToggleClientStore;
|
||||||
featureTypeStore: IFeatureTypeStore;
|
featureTypeStore: IFeatureTypeStore;
|
||||||
groupStore: IGroupStore;
|
groupStore: IGroupStore;
|
||||||
projectStore: IProjectStore;
|
projectStore: IProjectStore;
|
||||||
|
@ -48,6 +48,7 @@ process.nextTick(async () => {
|
|||||||
dependentFeatures: true,
|
dependentFeatures: true,
|
||||||
transactionalDecorator: true,
|
transactionalDecorator: true,
|
||||||
useLastSeenRefactor: true,
|
useLastSeenRefactor: true,
|
||||||
|
separateAdminClientApi: true,
|
||||||
},
|
},
|
||||||
},
|
},
|
||||||
authentication: {
|
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 dbInit, { ITestDb } from '../../helpers/database-init';
|
||||||
import getLogger from '../../../fixtures/no-logger';
|
import getLogger from '../../../fixtures/no-logger';
|
||||||
import { DEFAULT_ENV } from '../../../../lib/util/constants';
|
import { DEFAULT_ENV } from '../../../../lib/util/constants';
|
||||||
@ -12,7 +16,7 @@ const projectId = 'default';
|
|||||||
|
|
||||||
beforeAll(async () => {
|
beforeAll(async () => {
|
||||||
db = await dbInit('feature_env_api_client', getLogger);
|
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(
|
await app.services.featureToggleServiceV2.createFeatureToggle(
|
||||||
projectId,
|
projectId,
|
||||||
@ -43,6 +47,7 @@ test('returns feature toggle for default env', async () => {
|
|||||||
true,
|
true,
|
||||||
'test',
|
'test',
|
||||||
);
|
);
|
||||||
|
|
||||||
await app.request
|
await app.request
|
||||||
.get('/api/client/features')
|
.get('/api/client/features')
|
||||||
.expect('Content-Type', /json/)
|
.expect('Content-Type', /json/)
|
||||||
|
@ -20,7 +20,7 @@ const feature3 = 'f3.p2.token.access';
|
|||||||
|
|
||||||
beforeAll(async () => {
|
beforeAll(async () => {
|
||||||
db = await dbInit('feature_api_api_access_client', getLogger);
|
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;
|
apiTokenService = app.services.apiTokenService;
|
||||||
|
|
||||||
const { featureToggleServiceV2, environmentService } = app.services;
|
const { featureToggleServiceV2, environmentService } = app.services;
|
||||||
|
@ -5,14 +5,14 @@ import { setupApp } from '../helpers/test-helper';
|
|||||||
let stores;
|
let stores;
|
||||||
let app;
|
let app;
|
||||||
let db;
|
let db;
|
||||||
let featureToggleClientStore;
|
let clientFeatureToggleStore;
|
||||||
|
|
||||||
beforeAll(async () => {
|
beforeAll(async () => {
|
||||||
getLogger.setMuteError(true);
|
getLogger.setMuteError(true);
|
||||||
db = await dbInit('feature_toggle_client_store_serial', getLogger);
|
db = await dbInit('feature_toggle_client_store_serial', getLogger);
|
||||||
app = await setupApp(db.stores);
|
app = await setupApp(db.stores);
|
||||||
stores = db.stores;
|
stores = db.stores;
|
||||||
featureToggleClientStore = stores.featureToggleClientStore;
|
clientFeatureToggleStore = stores.clientFeatureToggleStore;
|
||||||
});
|
});
|
||||||
|
|
||||||
afterAll(async () => {
|
afterAll(async () => {
|
||||||
@ -27,6 +27,6 @@ test('should be able to fetch client toggles', async () => {
|
|||||||
|
|
||||||
expect(response.status).toBe(202);
|
expect(response.status).toBe(202);
|
||||||
|
|
||||||
const clientToggles = await featureToggleClientStore.getClient();
|
const clientToggles = await clientFeatureToggleStore.getClient();
|
||||||
expect(clientToggles).toHaveLength(1);
|
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 FakeApiTokenStore from './fake-api-token-store';
|
||||||
import FakeFeatureTypeStore from './fake-feature-type-store';
|
import FakeFeatureTypeStore from './fake-feature-type-store';
|
||||||
import FakeResetTokenStore from './fake-reset-token-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 FakeClientMetricsStoreV2 from './fake-client-metrics-store-v2';
|
||||||
import FakeUserSplashStore from './fake-user-splash-store';
|
import FakeUserSplashStore from './fake-user-splash-store';
|
||||||
import FakeRoleStore from './fake-role-store';
|
import FakeRoleStore from './fake-role-store';
|
||||||
@ -52,7 +52,7 @@ const createStores: () => IUnleashStores = () => {
|
|||||||
clientMetricsStoreV2: new FakeClientMetricsStoreV2(),
|
clientMetricsStoreV2: new FakeClientMetricsStoreV2(),
|
||||||
clientInstanceStore: new FakeClientInstanceStore(),
|
clientInstanceStore: new FakeClientInstanceStore(),
|
||||||
featureToggleStore: new FakeFeatureToggleStore(),
|
featureToggleStore: new FakeFeatureToggleStore(),
|
||||||
featureToggleClientStore: new FakeFeatureToggleClientStore(),
|
clientFeatureToggleStore: new FakeClientFeatureToggleStore(),
|
||||||
tagStore: new FakeTagStore(),
|
tagStore: new FakeTagStore(),
|
||||||
tagTypeStore: new FakeTagTypeStore(),
|
tagTypeStore: new FakeTagTypeStore(),
|
||||||
eventStore: new FakeEventStore(),
|
eventStore: new FakeEventStore(),
|
||||||
|
Loading…
Reference in New Issue
Block a user