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