mirror of
https://github.com/Unleash/unleash.git
synced 2025-01-20 00:08:02 +01:00
feat: Persist strategy variants (#4236)
This commit is contained in:
parent
00ca1d7186
commit
87b46ea734
@ -84,6 +84,7 @@ test('counts custom strategies in use', async () => {
|
|||||||
environment: 'default',
|
environment: 'default',
|
||||||
parameters: {},
|
parameters: {},
|
||||||
constraints: [],
|
constraints: [],
|
||||||
|
variants: [],
|
||||||
});
|
});
|
||||||
|
|
||||||
// Act
|
// Act
|
||||||
|
@ -15,6 +15,7 @@ import {
|
|||||||
IFeatureToggleClient,
|
IFeatureToggleClient,
|
||||||
IFlagResolver,
|
IFlagResolver,
|
||||||
IStrategyConfig,
|
IStrategyConfig,
|
||||||
|
IStrategyVariant,
|
||||||
ITag,
|
ITag,
|
||||||
PartialDeep,
|
PartialDeep,
|
||||||
PartialSome,
|
PartialSome,
|
||||||
@ -34,6 +35,7 @@ const COLUMNS = [
|
|||||||
'title',
|
'title',
|
||||||
'parameters',
|
'parameters',
|
||||||
'constraints',
|
'constraints',
|
||||||
|
'variants',
|
||||||
'created_at',
|
'created_at',
|
||||||
'disabled',
|
'disabled',
|
||||||
];
|
];
|
||||||
@ -62,6 +64,7 @@ interface IFeatureStrategiesTable {
|
|||||||
strategy_name: string;
|
strategy_name: string;
|
||||||
parameters: object;
|
parameters: object;
|
||||||
constraints: string;
|
constraints: string;
|
||||||
|
variants: string;
|
||||||
sort_order: number;
|
sort_order: number;
|
||||||
created_at?: Date;
|
created_at?: Date;
|
||||||
disabled?: boolean | null;
|
disabled?: boolean | null;
|
||||||
@ -84,6 +87,7 @@ function mapRow(row: IFeatureStrategiesTable): IFeatureStrategy {
|
|||||||
title: row.title,
|
title: row.title,
|
||||||
parameters: mapValues(row.parameters || {}, ensureStringValue),
|
parameters: mapValues(row.parameters || {}, ensureStringValue),
|
||||||
constraints: (row.constraints as unknown as IConstraint[]) || [],
|
constraints: (row.constraints as unknown as IConstraint[]) || [],
|
||||||
|
variants: (row.variants as unknown as IStrategyVariant[]) || [],
|
||||||
createdAt: row.created_at,
|
createdAt: row.created_at,
|
||||||
sortOrder: row.sort_order,
|
sortOrder: row.sort_order,
|
||||||
disabled: row.disabled,
|
disabled: row.disabled,
|
||||||
@ -100,6 +104,7 @@ function mapInput(input: IFeatureStrategy): IFeatureStrategiesTable {
|
|||||||
title: input.title,
|
title: input.title,
|
||||||
parameters: input.parameters,
|
parameters: input.parameters,
|
||||||
constraints: JSON.stringify(input.constraints || []),
|
constraints: JSON.stringify(input.constraints || []),
|
||||||
|
variants: JSON.stringify(input.variants || []),
|
||||||
created_at: input.createdAt,
|
created_at: input.createdAt,
|
||||||
sort_order: input.sortOrder,
|
sort_order: input.sortOrder,
|
||||||
disabled: input.disabled,
|
disabled: input.disabled,
|
||||||
@ -110,6 +115,7 @@ interface StrategyUpdate {
|
|||||||
strategy_name: string;
|
strategy_name: string;
|
||||||
parameters: object;
|
parameters: object;
|
||||||
constraints: string;
|
constraints: string;
|
||||||
|
variants: string;
|
||||||
title?: string;
|
title?: string;
|
||||||
disabled?: boolean;
|
disabled?: boolean;
|
||||||
}
|
}
|
||||||
@ -131,6 +137,7 @@ function mapStrategyUpdate(
|
|||||||
update.disabled = input.disabled;
|
update.disabled = input.disabled;
|
||||||
}
|
}
|
||||||
update.constraints = JSON.stringify(input.constraints || []);
|
update.constraints = JSON.stringify(input.constraints || []);
|
||||||
|
update.variants = JSON.stringify(input.variants || []);
|
||||||
return update;
|
return update;
|
||||||
}
|
}
|
||||||
|
|
||||||
@ -599,6 +606,7 @@ class FeatureStrategiesStore implements IFeatureStrategiesStore {
|
|||||||
const strategy = {
|
const strategy = {
|
||||||
name: r.strategy_name,
|
name: r.strategy_name,
|
||||||
constraints: r.constraints || [],
|
constraints: r.constraints || [],
|
||||||
|
variants: r.strategy_variants || [],
|
||||||
parameters: r.parameters,
|
parameters: r.parameters,
|
||||||
sortOrder: r.sort_order,
|
sortOrder: r.sort_order,
|
||||||
id: r.strategy_id,
|
id: r.strategy_id,
|
||||||
|
@ -439,6 +439,14 @@ class FeatureToggleService {
|
|||||||
strategyConfig.parameters.stickiness = 'default';
|
strategyConfig.parameters.stickiness = 'default';
|
||||||
}
|
}
|
||||||
|
|
||||||
|
if (strategyConfig.variants && strategyConfig.variants.length > 0) {
|
||||||
|
await variantsArraySchema.validateAsync(strategyConfig.variants);
|
||||||
|
const fixedVariants = this.fixVariantWeights(
|
||||||
|
strategyConfig.variants,
|
||||||
|
);
|
||||||
|
strategyConfig.variants = fixedVariants;
|
||||||
|
}
|
||||||
|
|
||||||
try {
|
try {
|
||||||
const newFeatureStrategy =
|
const newFeatureStrategy =
|
||||||
await this.featureStrategiesStore.createStrategyFeatureEnv({
|
await this.featureStrategiesStore.createStrategyFeatureEnv({
|
||||||
@ -446,6 +454,7 @@ class FeatureToggleService {
|
|||||||
title: strategyConfig.title,
|
title: strategyConfig.title,
|
||||||
disabled: strategyConfig.disabled,
|
disabled: strategyConfig.disabled,
|
||||||
constraints: strategyConfig.constraints || [],
|
constraints: strategyConfig.constraints || [],
|
||||||
|
variants: strategyConfig.variants || [],
|
||||||
parameters: strategyConfig.parameters || {},
|
parameters: strategyConfig.parameters || {},
|
||||||
sortOrder: strategyConfig.sortOrder,
|
sortOrder: strategyConfig.sortOrder,
|
||||||
projectId,
|
projectId,
|
||||||
@ -562,6 +571,12 @@ class FeatureToggleService {
|
|||||||
);
|
);
|
||||||
}
|
}
|
||||||
|
|
||||||
|
if (updates.variants && updates.variants.length > 0) {
|
||||||
|
await variantsArraySchema.validateAsync(updates.variants);
|
||||||
|
const fixedVariants = this.fixVariantWeights(updates.variants);
|
||||||
|
updates.variants = fixedVariants;
|
||||||
|
}
|
||||||
|
|
||||||
const strategy = await this.featureStrategiesStore.updateStrategy(
|
const strategy = await this.featureStrategiesStore.updateStrategy(
|
||||||
id,
|
id,
|
||||||
updates,
|
updates,
|
||||||
@ -756,6 +771,7 @@ class FeatureToggleService {
|
|||||||
name: strat.strategyName,
|
name: strat.strategyName,
|
||||||
constraints: strat.constraints,
|
constraints: strat.constraints,
|
||||||
parameters: strat.parameters,
|
parameters: strat.parameters,
|
||||||
|
variants: strat.variants,
|
||||||
title: strat.title,
|
title: strat.title,
|
||||||
disabled: strat.disabled,
|
disabled: strat.disabled,
|
||||||
sortOrder: strat.sortOrder,
|
sortOrder: strat.sortOrder,
|
||||||
|
@ -26,6 +26,7 @@ export interface IStrategyConfig {
|
|||||||
name: string;
|
name: string;
|
||||||
featureName?: string;
|
featureName?: string;
|
||||||
constraints?: IConstraint[];
|
constraints?: IConstraint[];
|
||||||
|
variants?: IStrategyVariant[];
|
||||||
segments?: number[];
|
segments?: number[];
|
||||||
parameters?: { [key: string]: string };
|
parameters?: { [key: string]: string };
|
||||||
sortOrder?: number;
|
sortOrder?: number;
|
||||||
@ -41,6 +42,7 @@ export interface IFeatureStrategy {
|
|||||||
parameters: { [key: string]: string };
|
parameters: { [key: string]: string };
|
||||||
sortOrder?: number;
|
sortOrder?: number;
|
||||||
constraints: IConstraint[];
|
constraints: IConstraint[];
|
||||||
|
variants?: IStrategyVariant[];
|
||||||
createdAt?: Date;
|
createdAt?: Date;
|
||||||
segments?: number[];
|
segments?: number[];
|
||||||
title?: string | null;
|
title?: string | null;
|
||||||
@ -130,6 +132,8 @@ export interface IVariant {
|
|||||||
}[];
|
}[];
|
||||||
}
|
}
|
||||||
|
|
||||||
|
export type IStrategyVariant = Omit<IVariant, 'overrides'>;
|
||||||
|
|
||||||
export interface IEnvironment {
|
export interface IEnvironment {
|
||||||
name: string;
|
name: string;
|
||||||
type: string;
|
type: string;
|
||||||
|
@ -2350,6 +2350,80 @@ test('Can create toggle with impression data on different project', async () =>
|
|||||||
});
|
});
|
||||||
});
|
});
|
||||||
|
|
||||||
|
test('should handle strategy variants', async () => {
|
||||||
|
const feature = { name: uuidv4(), impressionData: false };
|
||||||
|
await app.createFeature(feature.name);
|
||||||
|
|
||||||
|
const strategyWithInvalidVariant = {
|
||||||
|
name: uuidv4(),
|
||||||
|
constraints: [],
|
||||||
|
variants: [
|
||||||
|
{
|
||||||
|
name: 'invalidVariant',
|
||||||
|
weight: 1000,
|
||||||
|
stickiness: 'default',
|
||||||
|
weightType: 'fix',
|
||||||
|
},
|
||||||
|
], // it should be variable
|
||||||
|
};
|
||||||
|
|
||||||
|
const variant = {
|
||||||
|
name: 'variantName',
|
||||||
|
weight: 1,
|
||||||
|
weightType: 'variable',
|
||||||
|
stickiness: 'default',
|
||||||
|
};
|
||||||
|
const updatedVariant1 = {
|
||||||
|
name: 'updatedVariant1',
|
||||||
|
weight: 500,
|
||||||
|
weightType: 'variable',
|
||||||
|
stickiness: 'default',
|
||||||
|
};
|
||||||
|
const updatedVariant2 = {
|
||||||
|
name: 'updatedVariant2',
|
||||||
|
weight: 500,
|
||||||
|
weightType: 'variable',
|
||||||
|
stickiness: 'default',
|
||||||
|
};
|
||||||
|
const strategyWithValidVariant = {
|
||||||
|
name: uuidv4(),
|
||||||
|
constraints: [],
|
||||||
|
variants: [variant],
|
||||||
|
};
|
||||||
|
|
||||||
|
const featureStrategiesPath = `/api/admin/projects/default/features/${feature.name}/environments/default/strategies`;
|
||||||
|
|
||||||
|
await app.request
|
||||||
|
.post(featureStrategiesPath)
|
||||||
|
.send(strategyWithInvalidVariant)
|
||||||
|
.expect(400);
|
||||||
|
|
||||||
|
await createStrategy(feature.name, strategyWithValidVariant);
|
||||||
|
|
||||||
|
const { body: strategies } = await app.request.get(featureStrategiesPath);
|
||||||
|
|
||||||
|
expect(strategies).toMatchObject([
|
||||||
|
{
|
||||||
|
variants: [{ ...variant, weight: 1000 }], // weight was fixed
|
||||||
|
},
|
||||||
|
]);
|
||||||
|
|
||||||
|
await updateStrategy(feature.name, strategies[0].id, {
|
||||||
|
...strategies[0],
|
||||||
|
variants: [updatedVariant1, updatedVariant2],
|
||||||
|
});
|
||||||
|
|
||||||
|
const { body: updatedStrategies } = await app.request.get(
|
||||||
|
featureStrategiesPath,
|
||||||
|
);
|
||||||
|
|
||||||
|
expect(updatedStrategies).toMatchObject([
|
||||||
|
{
|
||||||
|
variants: [updatedVariant1, updatedVariant2],
|
||||||
|
},
|
||||||
|
]);
|
||||||
|
});
|
||||||
|
|
||||||
test('should reject invalid constraint values for multi-valued constraints', async () => {
|
test('should reject invalid constraint values for multi-valued constraints', async () => {
|
||||||
const project = await db.stores.projectStore.create({
|
const project = await db.stores.projectStore.create({
|
||||||
id: uuidv4(),
|
id: uuidv4(),
|
||||||
|
Loading…
Reference in New Issue
Block a user