mirror of
https://github.com/Unleash/unleash.git
synced 2025-07-26 13:48:33 +02:00
refactor: move segment limits to env vars (#1642)
* refactor: improve env var helpers * refactor: remove unused segments client API * refactor: remove experimental segment flags * refactor: move segment limits to env vars * refactor: add segment limits to UIConfig response * refactor: fix type name casing
This commit is contained in:
parent
1e5859425f
commit
224b9cb229
@ -64,6 +64,7 @@ Object {
|
|||||||
"file": undefined,
|
"file": undefined,
|
||||||
"keepExisting": false,
|
"keepExisting": false,
|
||||||
},
|
},
|
||||||
|
"inlineSegmentConstraints": true,
|
||||||
"listen": Object {
|
"listen": Object {
|
||||||
"host": undefined,
|
"host": undefined,
|
||||||
"port": 4242,
|
"port": 4242,
|
||||||
@ -71,6 +72,7 @@ Object {
|
|||||||
"preHook": undefined,
|
"preHook": undefined,
|
||||||
"preRouterHook": undefined,
|
"preRouterHook": undefined,
|
||||||
"secureHeaders": false,
|
"secureHeaders": false,
|
||||||
|
"segmentValuesLimit": 100,
|
||||||
"server": Object {
|
"server": Object {
|
||||||
"baseUriPath": "",
|
"baseUriPath": "",
|
||||||
"cdnPrefix": undefined,
|
"cdnPrefix": undefined,
|
||||||
@ -90,6 +92,7 @@ Object {
|
|||||||
"db": true,
|
"db": true,
|
||||||
"ttlHours": 48,
|
"ttlHours": 48,
|
||||||
},
|
},
|
||||||
|
"strategySegmentsLimit": 5,
|
||||||
"ui": Object {
|
"ui": Object {
|
||||||
"flags": Object {
|
"flags": Object {
|
||||||
"E": true,
|
"E": true,
|
||||||
|
@ -28,6 +28,12 @@ import {
|
|||||||
mapLegacyToken,
|
mapLegacyToken,
|
||||||
validateApiToken,
|
validateApiToken,
|
||||||
} from './types/models/api-token';
|
} from './types/models/api-token';
|
||||||
|
import { parseEnvVarBoolean, parseEnvVarNumber } from './util/env';
|
||||||
|
import { IExperimentalOptions } from './experimental';
|
||||||
|
import {
|
||||||
|
DEFAULT_SEGMENT_VALUES_LIMIT,
|
||||||
|
DEFAULT_STRATEGY_SEGMENTS_LIMIT,
|
||||||
|
} from './util/segments';
|
||||||
|
|
||||||
const safeToUpper = (s: string) => (s ? s.toUpperCase() : s);
|
const safeToUpper = (s: string) => (s ? s.toUpperCase() : s);
|
||||||
|
|
||||||
@ -38,33 +44,12 @@ export function authTypeFromString(
|
|||||||
return IAuthType[safeToUpper(s)] || defaultType;
|
return IAuthType[safeToUpper(s)] || defaultType;
|
||||||
}
|
}
|
||||||
|
|
||||||
function safeNumber(envVar, defaultVal): number {
|
|
||||||
if (envVar) {
|
|
||||||
try {
|
|
||||||
return Number.parseInt(envVar, 10);
|
|
||||||
} catch (err) {
|
|
||||||
return defaultVal;
|
|
||||||
}
|
|
||||||
} else {
|
|
||||||
return defaultVal;
|
|
||||||
}
|
|
||||||
}
|
|
||||||
|
|
||||||
function safeBoolean(envVar: string, defaultVal: boolean): boolean {
|
|
||||||
if (envVar) {
|
|
||||||
return envVar === 'true' || envVar === '1' || envVar === 't';
|
|
||||||
}
|
|
||||||
return defaultVal;
|
|
||||||
}
|
|
||||||
|
|
||||||
function mergeAll<T>(objects: Partial<T>[]): T {
|
function mergeAll<T>(objects: Partial<T>[]): T {
|
||||||
return merge.all<T>(objects.filter((i) => i));
|
return merge.all<T>(objects.filter((i) => i));
|
||||||
}
|
}
|
||||||
|
|
||||||
function loadExperimental(options: IUnleashOptions): any {
|
function loadExperimental(options: IUnleashOptions): IExperimentalOptions {
|
||||||
const experimental = options.experimental || {};
|
return options.experimental || {};
|
||||||
|
|
||||||
return experimental;
|
|
||||||
}
|
}
|
||||||
|
|
||||||
function loadUI(options: IUnleashOptions): IUIConfig {
|
function loadUI(options: IUnleashOptions): IUIConfig {
|
||||||
@ -81,7 +66,7 @@ const defaultDbOptions: IDBOption = {
|
|||||||
user: process.env.DATABASE_USERNAME,
|
user: process.env.DATABASE_USERNAME,
|
||||||
password: process.env.DATABASE_PASSWORD,
|
password: process.env.DATABASE_PASSWORD,
|
||||||
host: process.env.DATABASE_HOST,
|
host: process.env.DATABASE_HOST,
|
||||||
port: safeNumber(process.env.DATABASE_PORT, 5432),
|
port: parseEnvVarNumber(process.env.DATABASE_PORT, 5432),
|
||||||
database: process.env.DATABASE_NAME || 'unleash',
|
database: process.env.DATABASE_NAME || 'unleash',
|
||||||
ssl:
|
ssl:
|
||||||
process.env.DATABASE_SSL != null
|
process.env.DATABASE_SSL != null
|
||||||
@ -91,9 +76,9 @@ const defaultDbOptions: IDBOption = {
|
|||||||
version: process.env.DATABASE_VERSION,
|
version: process.env.DATABASE_VERSION,
|
||||||
acquireConnectionTimeout: secondsToMilliseconds(30),
|
acquireConnectionTimeout: secondsToMilliseconds(30),
|
||||||
pool: {
|
pool: {
|
||||||
min: safeNumber(process.env.DATABASE_POOL_MIN, 0),
|
min: parseEnvVarNumber(process.env.DATABASE_POOL_MIN, 0),
|
||||||
max: safeNumber(process.env.DATABASE_POOL_MAX, 4),
|
max: parseEnvVarNumber(process.env.DATABASE_POOL_MAX, 4),
|
||||||
idleTimeoutMillis: safeNumber(
|
idleTimeoutMillis: parseEnvVarNumber(
|
||||||
process.env.DATABASE_POOL_IDLE_TIMEOUT_MS,
|
process.env.DATABASE_POOL_IDLE_TIMEOUT_MS,
|
||||||
secondsToMilliseconds(30),
|
secondsToMilliseconds(30),
|
||||||
),
|
),
|
||||||
@ -105,14 +90,14 @@ const defaultDbOptions: IDBOption = {
|
|||||||
};
|
};
|
||||||
|
|
||||||
const defaultSessionOption: ISessionOption = {
|
const defaultSessionOption: ISessionOption = {
|
||||||
ttlHours: safeNumber(process.env.SESSION_TTL_HOURS, 48),
|
ttlHours: parseEnvVarNumber(process.env.SESSION_TTL_HOURS, 48),
|
||||||
db: true,
|
db: true,
|
||||||
};
|
};
|
||||||
|
|
||||||
const defaultServerOption: IServerOption = {
|
const defaultServerOption: IServerOption = {
|
||||||
pipe: undefined,
|
pipe: undefined,
|
||||||
host: process.env.HTTP_HOST,
|
host: process.env.HTTP_HOST,
|
||||||
port: safeNumber(process.env.HTTP_PORT || process.env.PORT, 4242),
|
port: parseEnvVarNumber(process.env.HTTP_PORT || process.env.PORT, 4242),
|
||||||
baseUriPath: formatBaseUri(process.env.BASE_URI_PATH),
|
baseUriPath: formatBaseUri(process.env.BASE_URI_PATH),
|
||||||
cdnPrefix: process.env.CDN_PREFIX,
|
cdnPrefix: process.env.CDN_PREFIX,
|
||||||
unleashUrl: process.env.UNLEASH_URL || 'http://localhost:4242',
|
unleashUrl: process.env.UNLEASH_URL || 'http://localhost:4242',
|
||||||
@ -120,11 +105,11 @@ const defaultServerOption: IServerOption = {
|
|||||||
keepAliveTimeout: minutesToMilliseconds(1),
|
keepAliveTimeout: minutesToMilliseconds(1),
|
||||||
headersTimeout: secondsToMilliseconds(61),
|
headersTimeout: secondsToMilliseconds(61),
|
||||||
enableRequestLogger: false,
|
enableRequestLogger: false,
|
||||||
gracefulShutdownEnable: safeBoolean(
|
gracefulShutdownEnable: parseEnvVarBoolean(
|
||||||
process.env.GRACEFUL_SHUTDOWN_ENABLE,
|
process.env.GRACEFUL_SHUTDOWN_ENABLE,
|
||||||
true,
|
true,
|
||||||
),
|
),
|
||||||
gracefulShutdownTimeout: safeNumber(
|
gracefulShutdownTimeout: parseEnvVarNumber(
|
||||||
process.env.GRACEFUL_SHUTDOWN_TIMEOUT,
|
process.env.GRACEFUL_SHUTDOWN_TIMEOUT,
|
||||||
secondsToMilliseconds(1),
|
secondsToMilliseconds(1),
|
||||||
),
|
),
|
||||||
@ -133,11 +118,11 @@ const defaultServerOption: IServerOption = {
|
|||||||
|
|
||||||
const defaultVersionOption: IVersionOption = {
|
const defaultVersionOption: IVersionOption = {
|
||||||
url: process.env.UNLEASH_VERSION_URL || 'https://version.unleash.run',
|
url: process.env.UNLEASH_VERSION_URL || 'https://version.unleash.run',
|
||||||
enable: safeBoolean(process.env.CHECK_VERSION, true),
|
enable: parseEnvVarBoolean(process.env.CHECK_VERSION, true),
|
||||||
};
|
};
|
||||||
|
|
||||||
const defaultAuthentication: IAuthOption = {
|
const defaultAuthentication: IAuthOption = {
|
||||||
enableApiToken: safeBoolean(process.env.AUTH_ENABLE_API_TOKEN, true),
|
enableApiToken: parseEnvVarBoolean(process.env.AUTH_ENABLE_API_TOKEN, true),
|
||||||
type: authTypeFromString(process.env.AUTH_TYPE),
|
type: authTypeFromString(process.env.AUTH_TYPE),
|
||||||
customAuthHandler: defaultCustomAuthDenyAll,
|
customAuthHandler: defaultCustomAuthDenyAll,
|
||||||
createAdminUser: true,
|
createAdminUser: true,
|
||||||
@ -146,14 +131,17 @@ const defaultAuthentication: IAuthOption = {
|
|||||||
|
|
||||||
const defaultImport: IImportOption = {
|
const defaultImport: IImportOption = {
|
||||||
file: process.env.IMPORT_FILE,
|
file: process.env.IMPORT_FILE,
|
||||||
dropBeforeImport: safeBoolean(process.env.IMPORT_DROP_BEFORE_IMPORT, false),
|
dropBeforeImport: parseEnvVarBoolean(
|
||||||
keepExisting: safeBoolean(process.env.IMPORT_KEEP_EXISTING, false),
|
process.env.IMPORT_DROP_BEFORE_IMPORT,
|
||||||
|
false,
|
||||||
|
),
|
||||||
|
keepExisting: parseEnvVarBoolean(process.env.IMPORT_KEEP_EXISTING, false),
|
||||||
};
|
};
|
||||||
|
|
||||||
const defaultEmail: IEmailOption = {
|
const defaultEmail: IEmailOption = {
|
||||||
host: process.env.EMAIL_HOST,
|
host: process.env.EMAIL_HOST,
|
||||||
secure: safeBoolean(process.env.EMAIL_SECURE, false),
|
secure: parseEnvVarBoolean(process.env.EMAIL_SECURE, false),
|
||||||
port: safeNumber(process.env.EMAIL_PORT, 587),
|
port: parseEnvVarNumber(process.env.EMAIL_PORT, 587),
|
||||||
sender: process.env.EMAIL_SENDER || 'noreply@unleash-hosted.com',
|
sender: process.env.EMAIL_SENDER || 'noreply@unleash-hosted.com',
|
||||||
smtpuser: process.env.EMAIL_USER,
|
smtpuser: process.env.EMAIL_USER,
|
||||||
smtppass: process.env.EMAIL_PASSWORD,
|
smtppass: process.env.EMAIL_PASSWORD,
|
||||||
@ -342,19 +330,35 @@ export function createConfig(options: IUnleashOptions): IUnleashConfig {
|
|||||||
}
|
}
|
||||||
|
|
||||||
const secureHeaders =
|
const secureHeaders =
|
||||||
options.secureHeaders || safeBoolean(process.env.SECURE_HEADERS, false);
|
options.secureHeaders ||
|
||||||
|
parseEnvVarBoolean(process.env.SECURE_HEADERS, false);
|
||||||
|
|
||||||
const enableOAS =
|
const enableOAS =
|
||||||
options.enableOAS || safeBoolean(process.env.ENABLE_OAS, false);
|
options.enableOAS || parseEnvVarBoolean(process.env.ENABLE_OAS, false);
|
||||||
|
|
||||||
const disableLegacyFeaturesApi =
|
const disableLegacyFeaturesApi =
|
||||||
options.disableLegacyFeaturesApi ||
|
options.disableLegacyFeaturesApi ||
|
||||||
safeBoolean(process.env.DISABLE_LEGACY_FEATURES_API, false);
|
parseEnvVarBoolean(process.env.DISABLE_LEGACY_FEATURES_API, false);
|
||||||
|
|
||||||
const additionalCspAllowedDomains: ICspDomainConfig =
|
const additionalCspAllowedDomains: ICspDomainConfig =
|
||||||
parseCspConfig(options.additionalCspAllowedDomains) ||
|
parseCspConfig(options.additionalCspAllowedDomains) ||
|
||||||
parseCspEnvironmentVariables();
|
parseCspEnvironmentVariables();
|
||||||
|
|
||||||
|
const inlineSegmentConstraints =
|
||||||
|
typeof options.inlineSegmentConstraints === 'boolean'
|
||||||
|
? options.inlineSegmentConstraints
|
||||||
|
: true;
|
||||||
|
|
||||||
|
const segmentValuesLimit = parseEnvVarNumber(
|
||||||
|
process.env.UNLEASH_SEGMENT_VALUES_LIMIT,
|
||||||
|
DEFAULT_SEGMENT_VALUES_LIMIT,
|
||||||
|
);
|
||||||
|
|
||||||
|
const strategySegmentsLimit = parseEnvVarNumber(
|
||||||
|
process.env.UNLEASH_STRATEGY_SEGMENTS_LIMIT,
|
||||||
|
DEFAULT_STRATEGY_SEGMENTS_LIMIT,
|
||||||
|
);
|
||||||
|
|
||||||
return {
|
return {
|
||||||
db,
|
db,
|
||||||
session,
|
session,
|
||||||
@ -377,6 +381,9 @@ export function createConfig(options: IUnleashOptions): IUnleashConfig {
|
|||||||
eventBus: new EventEmitter(),
|
eventBus: new EventEmitter(),
|
||||||
environmentEnableOverrides,
|
environmentEnableOverrides,
|
||||||
additionalCspAllowedDomains,
|
additionalCspAllowedDomains,
|
||||||
|
inlineSegmentConstraints,
|
||||||
|
segmentValuesLimit,
|
||||||
|
strategySegmentsLimit,
|
||||||
};
|
};
|
||||||
}
|
}
|
||||||
|
|
||||||
|
@ -10,7 +10,6 @@ import {
|
|||||||
import { IFeatureToggleClientStore } from '../types/stores/feature-toggle-client-store';
|
import { IFeatureToggleClientStore } from '../types/stores/feature-toggle-client-store';
|
||||||
import { DEFAULT_ENV } from '../util/constants';
|
import { DEFAULT_ENV } from '../util/constants';
|
||||||
import { PartialDeep } from '../types/partial';
|
import { PartialDeep } from '../types/partial';
|
||||||
import { IExperimentalOptions } from '../experimental';
|
|
||||||
import EventEmitter from 'events';
|
import EventEmitter from 'events';
|
||||||
|
|
||||||
export interface FeaturesTable {
|
export interface FeaturesTable {
|
||||||
@ -31,7 +30,7 @@ export default class FeatureToggleClientStore
|
|||||||
|
|
||||||
private logger: Logger;
|
private logger: Logger;
|
||||||
|
|
||||||
private experimental: IExperimentalOptions;
|
private inlineSegmentConstraints: boolean;
|
||||||
|
|
||||||
private timer: Function;
|
private timer: Function;
|
||||||
|
|
||||||
@ -39,11 +38,11 @@ export default class FeatureToggleClientStore
|
|||||||
db: Knex,
|
db: Knex,
|
||||||
eventBus: EventEmitter,
|
eventBus: EventEmitter,
|
||||||
getLogger: LogProvider,
|
getLogger: LogProvider,
|
||||||
experimental: IExperimentalOptions,
|
inlineSegmentConstraints: boolean,
|
||||||
) {
|
) {
|
||||||
this.db = db;
|
this.db = db;
|
||||||
this.logger = getLogger('feature-toggle-client-store.ts');
|
this.logger = getLogger('feature-toggle-client-store.ts');
|
||||||
this.experimental = experimental;
|
this.inlineSegmentConstraints = inlineSegmentConstraints;
|
||||||
this.timer = (action) =>
|
this.timer = (action) =>
|
||||||
metricsHelper.wrapTimer(eventBus, DB_TIME, {
|
metricsHelper.wrapTimer(eventBus, DB_TIME, {
|
||||||
store: 'feature-toggle',
|
store: 'feature-toggle',
|
||||||
@ -59,9 +58,6 @@ export default class FeatureToggleClientStore
|
|||||||
const environment = featureQuery?.environment || DEFAULT_ENV;
|
const environment = featureQuery?.environment || DEFAULT_ENV;
|
||||||
const stopTimer = this.timer('getFeatureAdmin');
|
const stopTimer = this.timer('getFeatureAdmin');
|
||||||
|
|
||||||
const { inlineSegmentConstraints = false } =
|
|
||||||
this.experimental?.segments ?? {};
|
|
||||||
|
|
||||||
let selectColumns = [
|
let selectColumns = [
|
||||||
'features.name as name',
|
'features.name as name',
|
||||||
'features.description as description',
|
'features.description as description',
|
||||||
@ -143,9 +139,9 @@ export default class FeatureToggleClientStore
|
|||||||
FeatureToggleClientStore.rowToStrategy(r),
|
FeatureToggleClientStore.rowToStrategy(r),
|
||||||
);
|
);
|
||||||
}
|
}
|
||||||
if (inlineSegmentConstraints && r.segment_id) {
|
if (this.inlineSegmentConstraints && r.segment_id) {
|
||||||
this.addSegmentToStrategy(feature, r);
|
this.addSegmentToStrategy(feature, r);
|
||||||
} else if (!inlineSegmentConstraints && r.segment_id) {
|
} else if (!this.inlineSegmentConstraints && r.segment_id) {
|
||||||
this.addSegmentIdsToStrategy(feature, r);
|
this.addSegmentIdsToStrategy(feature, r);
|
||||||
}
|
}
|
||||||
feature.impressionData = r.impression_data;
|
feature.impressionData = r.impression_data;
|
||||||
|
@ -70,7 +70,7 @@ export const createStores = (
|
|||||||
db,
|
db,
|
||||||
eventBus,
|
eventBus,
|
||||||
getLogger,
|
getLogger,
|
||||||
config.experimental,
|
config.inlineSegmentConstraints,
|
||||||
),
|
),
|
||||||
environmentStore: new EnvironmentStore(db, eventBus, getLogger),
|
environmentStore: new EnvironmentStore(db, eventBus, getLogger),
|
||||||
featureTagStore: new FeatureTagStore(db, eventBus, getLogger),
|
featureTagStore: new FeatureTagStore(db, eventBus, getLogger),
|
||||||
|
@ -1,24 +1,9 @@
|
|||||||
export interface IExperimentalOptions {
|
export interface IExperimentalOptions {
|
||||||
metricsV2?: IExperimentalToggle;
|
metricsV2?: IExperimentalToggle;
|
||||||
clientFeatureMemoize?: IExperimentalToggle;
|
clientFeatureMemoize?: IExperimentalToggle;
|
||||||
segments?: IExperimentalSegments;
|
|
||||||
anonymiseEventLog?: boolean;
|
anonymiseEventLog?: boolean;
|
||||||
}
|
}
|
||||||
|
|
||||||
export interface IExperimentalToggle {
|
export interface IExperimentalToggle {
|
||||||
enabled: boolean;
|
enabled: boolean;
|
||||||
}
|
}
|
||||||
|
|
||||||
export interface IExperimentalSegments {
|
|
||||||
enableSegmentsClientApi: boolean;
|
|
||||||
enableSegmentsAdminApi: boolean;
|
|
||||||
inlineSegmentConstraints: boolean;
|
|
||||||
}
|
|
||||||
|
|
||||||
export const experimentalSegmentsConfig = (): IExperimentalSegments => {
|
|
||||||
return {
|
|
||||||
enableSegmentsAdminApi: true,
|
|
||||||
enableSegmentsClientApi: true,
|
|
||||||
inlineSegmentConstraints: true,
|
|
||||||
};
|
|
||||||
};
|
|
||||||
|
@ -4,6 +4,10 @@ import { createTestConfig } from '../../../test/config/test-config';
|
|||||||
import createStores from '../../../test/fixtures/store';
|
import createStores from '../../../test/fixtures/store';
|
||||||
import getApp from '../../app';
|
import getApp from '../../app';
|
||||||
import { createServices } from '../../services';
|
import { createServices } from '../../services';
|
||||||
|
import {
|
||||||
|
DEFAULT_SEGMENT_VALUES_LIMIT,
|
||||||
|
DEFAULT_STRATEGY_SEGMENTS_LIMIT,
|
||||||
|
} from '../../util/segments';
|
||||||
|
|
||||||
const uiConfig = {
|
const uiConfig = {
|
||||||
headerBackground: 'red',
|
headerBackground: 'red',
|
||||||
@ -46,14 +50,15 @@ beforeEach(async () => {
|
|||||||
afterEach(() => {
|
afterEach(() => {
|
||||||
destroy();
|
destroy();
|
||||||
});
|
});
|
||||||
test('should get ui config', () => {
|
|
||||||
expect.assertions(2);
|
test('should get ui config', async () => {
|
||||||
return request
|
const { body } = await request
|
||||||
.get(`${base}/api/admin/ui-config`)
|
.get(`${base}/api/admin/ui-config`)
|
||||||
.expect('Content-Type', /json/)
|
.expect('Content-Type', /json/)
|
||||||
.expect(200)
|
.expect(200);
|
||||||
.expect((res) => {
|
|
||||||
expect(res.body.slogan === 'hello').toBe(true);
|
expect(body.slogan).toEqual('hello');
|
||||||
expect(res.body.headerBackground === 'red').toBe(true);
|
expect(body.headerBackground).toEqual('red');
|
||||||
});
|
expect(body.segmentValuesLimit).toEqual(DEFAULT_SEGMENT_VALUES_LIMIT);
|
||||||
|
expect(body.strategySegmentsLimit).toEqual(DEFAULT_STRATEGY_SEGMENTS_LIMIT);
|
||||||
});
|
});
|
||||||
|
@ -1,22 +1,35 @@
|
|||||||
import { Request, Response } from 'express';
|
import { Request, Response } from 'express';
|
||||||
import { IUnleashServices } from '../../types/services';
|
import { IUnleashServices } from '../../types/services';
|
||||||
import { IAuthType, IUnleashConfig } from '../../types/option';
|
import { IAuthType, IUIConfig, IUnleashConfig } from '../../types/option';
|
||||||
import version from '../../util/version';
|
import version from '../../util/version';
|
||||||
|
|
||||||
import Controller from '../controller';
|
import Controller from '../controller';
|
||||||
import VersionService from '../../services/version-service';
|
import VersionService, { IVersionHolder } from '../../services/version-service';
|
||||||
import SettingService from '../../services/setting-service';
|
import SettingService from '../../services/setting-service';
|
||||||
import {
|
import {
|
||||||
simpleAuthKey,
|
simpleAuthKey,
|
||||||
SimpleAuthSettings,
|
SimpleAuthSettings,
|
||||||
} from '../../types/settings/simple-auth-settings';
|
} from '../../types/settings/simple-auth-settings';
|
||||||
|
|
||||||
|
interface IUIConfigResponse extends IUIConfig {
|
||||||
|
version: string;
|
||||||
|
unleashUrl: string;
|
||||||
|
baseUriPath: string;
|
||||||
|
authenticationType?: IAuthType;
|
||||||
|
versionInfo: IVersionHolder;
|
||||||
|
disablePasswordAuth: boolean;
|
||||||
|
segmentValuesLimit: number;
|
||||||
|
strategySegmentsLimit: number;
|
||||||
|
}
|
||||||
|
|
||||||
class ConfigController extends Controller {
|
class ConfigController extends Controller {
|
||||||
private versionService: VersionService;
|
private versionService: VersionService;
|
||||||
|
|
||||||
private settingService: SettingService;
|
private settingService: SettingService;
|
||||||
|
|
||||||
private uiConfig: any;
|
private uiConfig: Omit<
|
||||||
|
IUIConfigResponse,
|
||||||
|
'versionInfo' | 'disablePasswordAuth'
|
||||||
|
>;
|
||||||
|
|
||||||
constructor(
|
constructor(
|
||||||
config: IUnleashConfig,
|
config: IUnleashConfig,
|
||||||
@ -32,15 +45,20 @@ class ConfigController extends Controller {
|
|||||||
config.authentication && config.authentication.type;
|
config.authentication && config.authentication.type;
|
||||||
this.uiConfig = {
|
this.uiConfig = {
|
||||||
...config.ui,
|
...config.ui,
|
||||||
authenticationType,
|
version,
|
||||||
unleashUrl: config.server.unleashUrl,
|
unleashUrl: config.server.unleashUrl,
|
||||||
baseUriPath: config.server.baseUriPath,
|
baseUriPath: config.server.baseUriPath,
|
||||||
version,
|
authenticationType,
|
||||||
|
segmentValuesLimit: config.segmentValuesLimit,
|
||||||
|
strategySegmentsLimit: config.strategySegmentsLimit,
|
||||||
};
|
};
|
||||||
this.get('/', this.getUIConfig);
|
this.get('/', this.getUIConfig);
|
||||||
}
|
}
|
||||||
|
|
||||||
async getUIConfig(req: Request, res: Response): Promise<void> {
|
async getUIConfig(
|
||||||
|
req: Request,
|
||||||
|
res: Response<IUIConfigResponse>,
|
||||||
|
): Promise<void> {
|
||||||
const config = this.uiConfig;
|
const config = this.uiConfig;
|
||||||
const simpleAuthSettings =
|
const simpleAuthSettings =
|
||||||
await this.settingService.get<SimpleAuthSettings>(simpleAuthKey);
|
await this.settingService.get<SimpleAuthSettings>(simpleAuthKey);
|
||||||
|
@ -46,10 +46,10 @@ export default class FeatureController extends Controller {
|
|||||||
this.featureToggleServiceV2 = featureToggleServiceV2;
|
this.featureToggleServiceV2 = featureToggleServiceV2;
|
||||||
this.segmentService = segmentService;
|
this.segmentService = segmentService;
|
||||||
this.logger = config.getLogger('client-api/feature.js');
|
this.logger = config.getLogger('client-api/feature.js');
|
||||||
|
this.useGlobalSegments = !this.config.inlineSegmentConstraints;
|
||||||
|
|
||||||
this.get('/', this.getAll);
|
this.get('/', this.getAll);
|
||||||
this.get('/:featureName', this.getFeatureToggle);
|
this.get('/:featureName', this.getFeatureToggle);
|
||||||
this.useGlobalSegments =
|
|
||||||
experimental && !experimental?.segments?.inlineSegmentConstraints;
|
|
||||||
|
|
||||||
if (experimental && experimental.clientFeatureMemoize) {
|
if (experimental && experimental.clientFeatureMemoize) {
|
||||||
// @ts-ignore
|
// @ts-ignore
|
||||||
|
@ -5,7 +5,6 @@ import MetricsController from './metrics';
|
|||||||
import RegisterController from './register';
|
import RegisterController from './register';
|
||||||
import { IUnleashConfig } from '../../types/option';
|
import { IUnleashConfig } from '../../types/option';
|
||||||
import { IUnleashServices } from '../../types';
|
import { IUnleashServices } from '../../types';
|
||||||
import { SegmentsController } from './segments';
|
|
||||||
|
|
||||||
const apiDef = require('./api-def.json');
|
const apiDef = require('./api-def.json');
|
||||||
|
|
||||||
@ -17,13 +16,6 @@ export default class ClientApi extends Controller {
|
|||||||
this.use('/features', new FeatureController(services, config).router);
|
this.use('/features', new FeatureController(services, config).router);
|
||||||
this.use('/metrics', new MetricsController(services, config).router);
|
this.use('/metrics', new MetricsController(services, config).router);
|
||||||
this.use('/register', new RegisterController(services, config).router);
|
this.use('/register', new RegisterController(services, config).router);
|
||||||
|
|
||||||
if (config.experimental?.segments?.enableSegmentsClientApi) {
|
|
||||||
this.use(
|
|
||||||
'/segments',
|
|
||||||
new SegmentsController(services, config).router,
|
|
||||||
);
|
|
||||||
}
|
|
||||||
}
|
}
|
||||||
|
|
||||||
index(req: Request, res: Response): void {
|
index(req: Request, res: Response): void {
|
||||||
|
@ -1,29 +0,0 @@
|
|||||||
import { Response } from 'express';
|
|
||||||
import Controller from '../controller';
|
|
||||||
import { IUnleashConfig } from '../../types/option';
|
|
||||||
import { IUnleashServices } from '../../types';
|
|
||||||
import { Logger } from '../../logger';
|
|
||||||
import { SegmentService } from '../../services/segment-service';
|
|
||||||
import { IAuthRequest } from '../unleash-types';
|
|
||||||
|
|
||||||
export class SegmentsController extends Controller {
|
|
||||||
private logger: Logger;
|
|
||||||
|
|
||||||
private segmentService: SegmentService;
|
|
||||||
|
|
||||||
constructor(
|
|
||||||
{ segmentService }: Pick<IUnleashServices, 'segmentService'>,
|
|
||||||
config: IUnleashConfig,
|
|
||||||
) {
|
|
||||||
super(config);
|
|
||||||
this.logger = config.getLogger('/client-api/segments.ts');
|
|
||||||
this.segmentService = segmentService;
|
|
||||||
|
|
||||||
this.get('/active', this.getActive);
|
|
||||||
}
|
|
||||||
|
|
||||||
async getActive(req: IAuthRequest, res: Response): Promise<void> {
|
|
||||||
const segments = await this.segmentService.getActive();
|
|
||||||
res.json({ segments });
|
|
||||||
}
|
|
||||||
}
|
|
@ -14,10 +14,6 @@ import {
|
|||||||
import User from '../types/user';
|
import User from '../types/user';
|
||||||
import { IFeatureStrategiesStore } from '../types/stores/feature-strategies-store';
|
import { IFeatureStrategiesStore } from '../types/stores/feature-strategies-store';
|
||||||
import BadDataError from '../error/bad-data-error';
|
import BadDataError from '../error/bad-data-error';
|
||||||
import {
|
|
||||||
SEGMENT_VALUES_LIMIT,
|
|
||||||
STRATEGY_SEGMENTS_LIMIT,
|
|
||||||
} from '../util/segments';
|
|
||||||
|
|
||||||
export class SegmentService {
|
export class SegmentService {
|
||||||
private logger: Logger;
|
private logger: Logger;
|
||||||
@ -28,6 +24,8 @@ export class SegmentService {
|
|||||||
|
|
||||||
private eventStore: IEventStore;
|
private eventStore: IEventStore;
|
||||||
|
|
||||||
|
private config: IUnleashConfig;
|
||||||
|
|
||||||
constructor(
|
constructor(
|
||||||
{
|
{
|
||||||
segmentStore,
|
segmentStore,
|
||||||
@ -37,12 +35,13 @@ export class SegmentService {
|
|||||||
IUnleashStores,
|
IUnleashStores,
|
||||||
'segmentStore' | 'featureStrategiesStore' | 'eventStore'
|
'segmentStore' | 'featureStrategiesStore' | 'eventStore'
|
||||||
>,
|
>,
|
||||||
{ getLogger }: Pick<IUnleashConfig, 'getLogger'>,
|
config: IUnleashConfig,
|
||||||
) {
|
) {
|
||||||
this.segmentStore = segmentStore;
|
this.segmentStore = segmentStore;
|
||||||
this.featureStrategiesStore = featureStrategiesStore;
|
this.featureStrategiesStore = featureStrategiesStore;
|
||||||
this.eventStore = eventStore;
|
this.eventStore = eventStore;
|
||||||
this.logger = getLogger('services/segment-service.ts');
|
this.logger = config.getLogger('services/segment-service.ts');
|
||||||
|
this.config = config;
|
||||||
}
|
}
|
||||||
|
|
||||||
async get(id: number): Promise<ISegment> {
|
async get(id: number): Promise<ISegment> {
|
||||||
@ -69,7 +68,7 @@ export class SegmentService {
|
|||||||
|
|
||||||
async create(data: unknown, user: User): Promise<void> {
|
async create(data: unknown, user: User): Promise<void> {
|
||||||
const input = await segmentSchema.validateAsync(data);
|
const input = await segmentSchema.validateAsync(data);
|
||||||
SegmentService.validateSegmentValuesLimit(input);
|
this.validateSegmentValuesLimit(input);
|
||||||
await this.validateName(input.name);
|
await this.validateName(input.name);
|
||||||
|
|
||||||
const segment = await this.segmentStore.create(input, user);
|
const segment = await this.segmentStore.create(input, user);
|
||||||
@ -83,7 +82,7 @@ export class SegmentService {
|
|||||||
|
|
||||||
async update(id: number, data: unknown, user: User): Promise<void> {
|
async update(id: number, data: unknown, user: User): Promise<void> {
|
||||||
const input = await segmentSchema.validateAsync(data);
|
const input = await segmentSchema.validateAsync(data);
|
||||||
SegmentService.validateSegmentValuesLimit(input);
|
this.validateSegmentValuesLimit(input);
|
||||||
const preData = await this.segmentStore.get(id);
|
const preData = await this.segmentStore.get(id);
|
||||||
|
|
||||||
if (preData.name !== input.name) {
|
if (preData.name !== input.name) {
|
||||||
@ -134,31 +133,28 @@ export class SegmentService {
|
|||||||
private async validateStrategySegmentLimit(
|
private async validateStrategySegmentLimit(
|
||||||
strategyId: string,
|
strategyId: string,
|
||||||
): Promise<void> {
|
): Promise<void> {
|
||||||
const limit = STRATEGY_SEGMENTS_LIMIT;
|
const { strategySegmentsLimit } = this.config;
|
||||||
|
|
||||||
if (typeof limit === 'undefined') {
|
if (
|
||||||
return;
|
(await this.getByStrategy(strategyId)).length >=
|
||||||
}
|
strategySegmentsLimit
|
||||||
|
) {
|
||||||
if ((await this.getByStrategy(strategyId)).length >= limit) {
|
|
||||||
throw new BadDataError(
|
throw new BadDataError(
|
||||||
`Strategies may not have more than ${limit} segments`,
|
`Strategies may not have more than ${strategySegmentsLimit} segments`,
|
||||||
);
|
);
|
||||||
}
|
}
|
||||||
}
|
}
|
||||||
|
|
||||||
private static validateSegmentValuesLimit(
|
private validateSegmentValuesLimit(segment: Omit<ISegment, 'id'>): void {
|
||||||
segment: Omit<ISegment, 'id'>,
|
const { segmentValuesLimit } = this.config;
|
||||||
): void {
|
|
||||||
const limit = SEGMENT_VALUES_LIMIT;
|
|
||||||
|
|
||||||
const valuesCount = segment.constraints
|
const valuesCount = segment.constraints
|
||||||
.flatMap((constraint) => constraint.values?.length ?? 0)
|
.flatMap((constraint) => constraint.values?.length ?? 0)
|
||||||
.reduce((acc, length) => acc + length, 0);
|
.reduce((acc, length) => acc + length, 0);
|
||||||
|
|
||||||
if (valuesCount > limit) {
|
if (valuesCount > segmentValuesLimit) {
|
||||||
throw new BadDataError(
|
throw new BadDataError(
|
||||||
`Segments may not have more than ${limit} values`,
|
`Segments may not have more than ${segmentValuesLimit} values`,
|
||||||
);
|
);
|
||||||
}
|
}
|
||||||
}
|
}
|
||||||
|
@ -104,6 +104,7 @@ export interface IUnleashOptions {
|
|||||||
eventHook?: EventHook;
|
eventHook?: EventHook;
|
||||||
enterpriseVersion?: string;
|
enterpriseVersion?: string;
|
||||||
disableLegacyFeaturesApi?: boolean;
|
disableLegacyFeaturesApi?: boolean;
|
||||||
|
inlineSegmentConstraints?: boolean;
|
||||||
}
|
}
|
||||||
|
|
||||||
export interface IEmailOption {
|
export interface IEmailOption {
|
||||||
@ -176,4 +177,7 @@ export interface IUnleashConfig {
|
|||||||
eventBus: EventEmitter;
|
eventBus: EventEmitter;
|
||||||
disableLegacyFeaturesApi?: boolean;
|
disableLegacyFeaturesApi?: boolean;
|
||||||
environmentEnableOverrides?: string[];
|
environmentEnableOverrides?: string[];
|
||||||
|
inlineSegmentConstraints: boolean;
|
||||||
|
segmentValuesLimit: number;
|
||||||
|
strategySegmentsLimit: number;
|
||||||
}
|
}
|
||||||
|
25
src/lib/util/env.test.ts
Normal file
25
src/lib/util/env.test.ts
Normal file
@ -0,0 +1,25 @@
|
|||||||
|
import { parseEnvVarBoolean, parseEnvVarNumber } from './env';
|
||||||
|
|
||||||
|
test('parseEnvVarNumber', () => {
|
||||||
|
expect(parseEnvVarNumber('', 1)).toEqual(1);
|
||||||
|
expect(parseEnvVarNumber('', 2)).toEqual(2);
|
||||||
|
expect(parseEnvVarNumber(' ', 1)).toEqual(1);
|
||||||
|
expect(parseEnvVarNumber('a', 1)).toEqual(1);
|
||||||
|
expect(parseEnvVarNumber('1', 1)).toEqual(1);
|
||||||
|
expect(parseEnvVarNumber('2', 1)).toEqual(2);
|
||||||
|
expect(parseEnvVarNumber('2e2', 1)).toEqual(2);
|
||||||
|
expect(parseEnvVarNumber('-1', 1)).toEqual(-1);
|
||||||
|
});
|
||||||
|
|
||||||
|
test('parseEnvVarBoolean', () => {
|
||||||
|
expect(parseEnvVarBoolean('', true)).toEqual(true);
|
||||||
|
expect(parseEnvVarBoolean('', false)).toEqual(false);
|
||||||
|
expect(parseEnvVarBoolean(' ', false)).toEqual(false);
|
||||||
|
expect(parseEnvVarBoolean('1', false)).toEqual(true);
|
||||||
|
expect(parseEnvVarBoolean('2', false)).toEqual(false);
|
||||||
|
expect(parseEnvVarBoolean('t', false)).toEqual(true);
|
||||||
|
expect(parseEnvVarBoolean('f', false)).toEqual(false);
|
||||||
|
expect(parseEnvVarBoolean('true', false)).toEqual(true);
|
||||||
|
expect(parseEnvVarBoolean('false', false)).toEqual(false);
|
||||||
|
expect(parseEnvVarBoolean('test', false)).toEqual(false);
|
||||||
|
});
|
20
src/lib/util/env.ts
Normal file
20
src/lib/util/env.ts
Normal file
@ -0,0 +1,20 @@
|
|||||||
|
export function parseEnvVarNumber(envVar: string, defaultVal: number): number {
|
||||||
|
const parsed = Number.parseInt(envVar, 10);
|
||||||
|
|
||||||
|
if (Number.isNaN(parsed)) {
|
||||||
|
return defaultVal;
|
||||||
|
}
|
||||||
|
|
||||||
|
return parsed;
|
||||||
|
}
|
||||||
|
|
||||||
|
export function parseEnvVarBoolean(
|
||||||
|
envVar: string,
|
||||||
|
defaultVal: boolean,
|
||||||
|
): boolean {
|
||||||
|
if (envVar) {
|
||||||
|
return envVar === 'true' || envVar === '1' || envVar === 't';
|
||||||
|
}
|
||||||
|
|
||||||
|
return defaultVal;
|
||||||
|
}
|
@ -1,2 +1,2 @@
|
|||||||
export const SEGMENT_VALUES_LIMIT = 100;
|
export const DEFAULT_SEGMENT_VALUES_LIMIT = 100;
|
||||||
export const STRATEGY_SEGMENTS_LIMIT = 5;
|
export const DEFAULT_STRATEGY_SEGMENTS_LIMIT = 5;
|
||||||
|
@ -2,7 +2,6 @@ import { start } from './lib/server-impl';
|
|||||||
import { createConfig } from './lib/create-config';
|
import { createConfig } from './lib/create-config';
|
||||||
import { LogLevel } from './lib/logger';
|
import { LogLevel } from './lib/logger';
|
||||||
import { ApiTokenType } from './lib/types/models/api-token';
|
import { ApiTokenType } from './lib/types/models/api-token';
|
||||||
import { experimentalSegmentsConfig } from './lib/experimental';
|
|
||||||
|
|
||||||
process.nextTick(async () => {
|
process.nextTick(async () => {
|
||||||
try {
|
try {
|
||||||
@ -33,7 +32,6 @@ process.nextTick(async () => {
|
|||||||
},
|
},
|
||||||
experimental: {
|
experimental: {
|
||||||
metricsV2: { enabled: true },
|
metricsV2: { enabled: true },
|
||||||
segments: experimentalSegmentsConfig(),
|
|
||||||
anonymiseEventLog: false,
|
anonymiseEventLog: false,
|
||||||
},
|
},
|
||||||
authentication: {
|
authentication: {
|
||||||
|
@ -5,9 +5,7 @@ import {
|
|||||||
IUnleashOptions,
|
IUnleashOptions,
|
||||||
} from '../../lib/types/option';
|
} from '../../lib/types/option';
|
||||||
import getLogger from '../fixtures/no-logger';
|
import getLogger from '../fixtures/no-logger';
|
||||||
|
|
||||||
import { createConfig } from '../../lib/create-config';
|
import { createConfig } from '../../lib/create-config';
|
||||||
import { experimentalSegmentsConfig } from '../../lib/experimental';
|
|
||||||
|
|
||||||
function mergeAll<T>(objects: Partial<T>[]): T {
|
function mergeAll<T>(objects: Partial<T>[]): T {
|
||||||
return merge.all<T>(objects.filter((i) => i));
|
return merge.all<T>(objects.filter((i) => i));
|
||||||
@ -19,7 +17,6 @@ export function createTestConfig(config?: IUnleashOptions): IUnleashConfig {
|
|||||||
authentication: { type: IAuthType.NONE, createAdminUser: false },
|
authentication: { type: IAuthType.NONE, createAdminUser: false },
|
||||||
server: { secret: 'really-secret' },
|
server: { secret: 'really-secret' },
|
||||||
session: { db: false },
|
session: { db: false },
|
||||||
experimental: { segments: experimentalSegmentsConfig() },
|
|
||||||
versionCheck: { enable: false },
|
versionCheck: { enable: false },
|
||||||
enableOAS: true,
|
enableOAS: true,
|
||||||
};
|
};
|
||||||
|
@ -96,21 +96,9 @@ const createTestSegments = async (): Promise<void> => {
|
|||||||
};
|
};
|
||||||
|
|
||||||
beforeAll(async () => {
|
beforeAll(async () => {
|
||||||
const experimentalConfig = {
|
const config = { inlineSegmentConstraints: false };
|
||||||
segments: {
|
db = await dbInit('global_segments', getLogger, config);
|
||||||
enableSegmentsAdminApi: true,
|
app = await setupAppWithCustomConfig(db.stores, config);
|
||||||
enableSegmentsClientApi: true,
|
|
||||||
inlineSegmentConstraints: false,
|
|
||||||
},
|
|
||||||
};
|
|
||||||
|
|
||||||
db = await dbInit('global_segments', getLogger, {
|
|
||||||
experimental: experimentalConfig,
|
|
||||||
});
|
|
||||||
|
|
||||||
app = await setupAppWithCustomConfig(db.stores, {
|
|
||||||
experimental: experimentalConfig,
|
|
||||||
});
|
|
||||||
});
|
});
|
||||||
|
|
||||||
afterAll(async () => {
|
afterAll(async () => {
|
||||||
@ -143,6 +131,8 @@ test('should send all segments that are in use by feature', async () => {
|
|||||||
|
|
||||||
const clientFeatures = await fetchClientResponse();
|
const clientFeatures = await fetchClientResponse();
|
||||||
const globalSegments = clientFeatures.segments;
|
const globalSegments = clientFeatures.segments;
|
||||||
|
expect(globalSegments).toHaveLength(2);
|
||||||
|
|
||||||
const globalSegmentIds = globalSegments.map((segment) => segment.id);
|
const globalSegmentIds = globalSegments.map((segment) => segment.id);
|
||||||
const allSegmentIds = clientFeatures.features
|
const allSegmentIds = clientFeatures.features
|
||||||
.map((feat) => feat.strategies.map((strategy) => strategy.segments))
|
.map((feat) => feat.strategies.map((strategy) => strategy.segments))
|
||||||
@ -150,6 +140,5 @@ test('should send all segments that are in use by feature', async () => {
|
|||||||
.flat()
|
.flat()
|
||||||
.filter((x) => !!x);
|
.filter((x) => !!x);
|
||||||
const toggleSegmentIds = [...new Set(allSegmentIds)];
|
const toggleSegmentIds = [...new Set(allSegmentIds)];
|
||||||
|
|
||||||
expect(globalSegmentIds).toEqual(toggleSegmentIds);
|
expect(globalSegmentIds).toEqual(toggleSegmentIds);
|
||||||
});
|
});
|
||||||
|
@ -1,7 +1,6 @@
|
|||||||
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 { IUnleashTest, setupApp } from '../../helpers/test-helper';
|
import { IUnleashTest, setupApp } from '../../helpers/test-helper';
|
||||||
import { collectIds } from '../../../../lib/util/collect-ids';
|
|
||||||
import {
|
import {
|
||||||
IConstraint,
|
IConstraint,
|
||||||
IFeatureToggleClient,
|
IFeatureToggleClient,
|
||||||
@ -10,8 +9,8 @@ import {
|
|||||||
import { randomId } from '../../../../lib/util/random-id';
|
import { randomId } from '../../../../lib/util/random-id';
|
||||||
import User from '../../../../lib/types/user';
|
import User from '../../../../lib/types/user';
|
||||||
import {
|
import {
|
||||||
SEGMENT_VALUES_LIMIT,
|
DEFAULT_SEGMENT_VALUES_LIMIT,
|
||||||
STRATEGY_SEGMENTS_LIMIT,
|
DEFAULT_STRATEGY_SEGMENTS_LIMIT,
|
||||||
} from '../../../../lib/util/segments';
|
} from '../../../../lib/util/segments';
|
||||||
|
|
||||||
let db: ITestDb;
|
let db: ITestDb;
|
||||||
@ -45,13 +44,6 @@ const fetchGlobalSegments = (): Promise<ISegment[] | undefined> => {
|
|||||||
.then((res) => res.body.segments);
|
.then((res) => res.body.segments);
|
||||||
};
|
};
|
||||||
|
|
||||||
const fetchClientSegmentsActive = (): Promise<ISegment[]> => {
|
|
||||||
return app.request
|
|
||||||
.get('/api/client/segments/active')
|
|
||||||
.expect(200)
|
|
||||||
.then((res) => res.body.segments);
|
|
||||||
};
|
|
||||||
|
|
||||||
const createSegment = (postData: object): Promise<unknown> => {
|
const createSegment = (postData: object): Promise<unknown> => {
|
||||||
const user = { email: 'test@example.com' } as User;
|
const user = { email: 'test@example.com' } as User;
|
||||||
return app.services.segmentService.create(postData, user);
|
return app.services.segmentService.create(postData, user);
|
||||||
@ -141,52 +133,28 @@ test('should add segments to features as constraints', async () => {
|
|||||||
expect(uniqueValues.length).toEqual(3);
|
expect(uniqueValues.length).toEqual(3);
|
||||||
});
|
});
|
||||||
|
|
||||||
test('should list active segments', async () => {
|
|
||||||
const constraints = mockConstraints();
|
|
||||||
await createSegment({ name: 'S1', constraints });
|
|
||||||
await createSegment({ name: 'S2', constraints });
|
|
||||||
await createSegment({ name: 'S3', constraints });
|
|
||||||
await createFeatureToggle(mockFeatureToggle());
|
|
||||||
await createFeatureToggle(mockFeatureToggle());
|
|
||||||
await createFeatureToggle(mockFeatureToggle());
|
|
||||||
const [feature1, feature2] = await fetchFeatures();
|
|
||||||
const [segment1, segment2] = await fetchSegments();
|
|
||||||
|
|
||||||
await addSegmentToStrategy(segment1.id, feature1.strategies[0].id);
|
|
||||||
await addSegmentToStrategy(segment2.id, feature1.strategies[0].id);
|
|
||||||
await addSegmentToStrategy(segment2.id, feature2.strategies[0].id);
|
|
||||||
|
|
||||||
const clientSegments = await fetchClientSegmentsActive();
|
|
||||||
|
|
||||||
expect(collectIds(clientSegments)).toEqual(
|
|
||||||
collectIds([segment1, segment2]),
|
|
||||||
);
|
|
||||||
});
|
|
||||||
|
|
||||||
test('should validate segment constraint values limit', async () => {
|
test('should validate segment constraint values limit', async () => {
|
||||||
const limit = SEGMENT_VALUES_LIMIT;
|
|
||||||
|
|
||||||
const constraints: IConstraint[] = [
|
const constraints: IConstraint[] = [
|
||||||
{
|
{
|
||||||
contextName: randomId(),
|
contextName: randomId(),
|
||||||
operator: 'IN',
|
operator: 'IN',
|
||||||
values: mockConstraintValues(limit + 1),
|
values: mockConstraintValues(DEFAULT_SEGMENT_VALUES_LIMIT + 1),
|
||||||
},
|
},
|
||||||
];
|
];
|
||||||
|
|
||||||
await expect(
|
await expect(
|
||||||
createSegment({ name: randomId(), constraints }),
|
createSegment({ name: randomId(), constraints }),
|
||||||
).rejects.toThrow(`Segments may not have more than ${limit} values`);
|
).rejects.toThrow(
|
||||||
|
`Segments may not have more than ${DEFAULT_SEGMENT_VALUES_LIMIT} values`,
|
||||||
|
);
|
||||||
});
|
});
|
||||||
|
|
||||||
test('should validate segment constraint values limit with multiple constraints', async () => {
|
test('should validate segment constraint values limit with multiple constraints', async () => {
|
||||||
const limit = SEGMENT_VALUES_LIMIT;
|
|
||||||
|
|
||||||
const constraints: IConstraint[] = [
|
const constraints: IConstraint[] = [
|
||||||
{
|
{
|
||||||
contextName: randomId(),
|
contextName: randomId(),
|
||||||
operator: 'IN',
|
operator: 'IN',
|
||||||
values: mockConstraintValues(limit),
|
values: mockConstraintValues(DEFAULT_SEGMENT_VALUES_LIMIT),
|
||||||
},
|
},
|
||||||
{
|
{
|
||||||
contextName: randomId(),
|
contextName: randomId(),
|
||||||
@ -197,12 +165,12 @@ test('should validate segment constraint values limit with multiple constraints'
|
|||||||
|
|
||||||
await expect(
|
await expect(
|
||||||
createSegment({ name: randomId(), constraints }),
|
createSegment({ name: randomId(), constraints }),
|
||||||
).rejects.toThrow(`Segments may not have more than ${limit} values`);
|
).rejects.toThrow(
|
||||||
|
`Segments may not have more than ${DEFAULT_SEGMENT_VALUES_LIMIT} values`,
|
||||||
|
);
|
||||||
});
|
});
|
||||||
|
|
||||||
test('should validate feature strategy segment limit', async () => {
|
test('should validate feature strategy segment limit', async () => {
|
||||||
const limit = STRATEGY_SEGMENTS_LIMIT;
|
|
||||||
|
|
||||||
await createSegment({ name: 'S1', constraints: [] });
|
await createSegment({ name: 'S1', constraints: [] });
|
||||||
await createSegment({ name: 'S2', constraints: [] });
|
await createSegment({ name: 'S2', constraints: [] });
|
||||||
await createSegment({ name: 'S3', constraints: [] });
|
await createSegment({ name: 'S3', constraints: [] });
|
||||||
@ -221,7 +189,9 @@ test('should validate feature strategy segment limit', async () => {
|
|||||||
|
|
||||||
await expect(
|
await expect(
|
||||||
addSegmentToStrategy(segments[5].id, feature1.strategies[0].id),
|
addSegmentToStrategy(segments[5].id, feature1.strategies[0].id),
|
||||||
).rejects.toThrow(`Strategies may not have more than ${limit} segments`);
|
).rejects.toThrow(
|
||||||
|
`Strategies may not have more than ${DEFAULT_STRATEGY_SEGMENTS_LIMIT} segments`,
|
||||||
|
);
|
||||||
});
|
});
|
||||||
|
|
||||||
test('should not return segments in base of toggle response if inline is enabled', async () => {
|
test('should not return segments in base of toggle response if inline is enabled', async () => {
|
||||||
|
Loading…
Reference in New Issue
Block a user