mirror of
https://github.com/Unleash/unleash.git
synced 2025-02-14 00:19:16 +01:00
Fix PATCH variants (old endpoint) when variants per environment are enabled (#2591)
## About the changes This PR addresses some issues when working with variants after migrating to variants per environment. **Problem:** since PATCH `/api/admin/projects/default/features/${featureName}/variants` does not take into account `featureEnvironments`, when variantsPerEnvironment gets enabled, this method will override the variants in other environments (i.e. not doing a patch). This method has to be maintained because of backward compatibility but it has to be adapted to deal with variants per environment https://linear.app/unleash/issue/2-476/when-using-patch-for-variants-without-environments-it-wipes-out Co-authored-by: Nuno Góis <github@nunogois.com>
This commit is contained in:
parent
a15157465c
commit
aa20d2d418
@ -303,7 +303,15 @@ class FeatureStrategiesStore implements IFeatureStrategiesStore {
|
|||||||
if (withEnvironmentVariants) {
|
if (withEnvironmentVariants) {
|
||||||
env.variants = variants;
|
env.variants = variants;
|
||||||
}
|
}
|
||||||
acc.variants = variants;
|
|
||||||
|
// this code sets variants at the feature level (should be deprecated with variants per environment)
|
||||||
|
const currentVariants = new Map(
|
||||||
|
acc.variants?.map((v) => [v.name, v]),
|
||||||
|
);
|
||||||
|
variants.forEach((variant) => {
|
||||||
|
currentVariants.set(variant.name, variant);
|
||||||
|
});
|
||||||
|
acc.variants = Array.from(currentVariants.values());
|
||||||
|
|
||||||
env.enabled = r.enabled;
|
env.enabled = r.enabled;
|
||||||
env.type = r.environment_type;
|
env.type = r.environment_type;
|
||||||
|
@ -143,6 +143,11 @@ export default class VariantsController extends Controller {
|
|||||||
});
|
});
|
||||||
}
|
}
|
||||||
|
|
||||||
|
/**
|
||||||
|
* @deprecated - Variants should be fetched from featureService.getVariantsForEnv (since variants are now; since 4.18, connected to environments)
|
||||||
|
* @param req
|
||||||
|
* @param res
|
||||||
|
*/
|
||||||
async getVariants(
|
async getVariants(
|
||||||
req: Request<FeatureParams, any, any, any>,
|
req: Request<FeatureParams, any, any, any>,
|
||||||
res: Response<FeatureVariantsSchema>,
|
res: Response<FeatureVariantsSchema>,
|
||||||
@ -158,7 +163,6 @@ export default class VariantsController extends Controller {
|
|||||||
): Promise<void> {
|
): Promise<void> {
|
||||||
const { projectId, featureName } = req.params;
|
const { projectId, featureName } = req.params;
|
||||||
const userName = extractUsername(req);
|
const userName = extractUsername(req);
|
||||||
|
|
||||||
const updatedFeature = await this.featureService.updateVariants(
|
const updatedFeature = await this.featureService.updateVariants(
|
||||||
featureName,
|
featureName,
|
||||||
projectId,
|
projectId,
|
||||||
|
@ -655,6 +655,7 @@ class FeatureToggleService {
|
|||||||
|
|
||||||
/**
|
/**
|
||||||
* GET /api/admin/projects/:project/features/:featureName/variants
|
* GET /api/admin/projects/:project/features/:featureName/variants
|
||||||
|
* @deprecated - Variants should be fetched from FeatureEnvironmentStore (since variants are now; since 4.18, connected to environments)
|
||||||
* @param featureName
|
* @param featureName
|
||||||
* @return The list of variants
|
* @return The list of variants
|
||||||
*/
|
*/
|
||||||
@ -1250,9 +1251,22 @@ class FeatureToggleService {
|
|||||||
newVariants: Operation[],
|
newVariants: Operation[],
|
||||||
createdBy: string,
|
createdBy: string,
|
||||||
): Promise<FeatureToggle> {
|
): Promise<FeatureToggle> {
|
||||||
const oldVariants = await this.getVariants(featureName);
|
const ft =
|
||||||
const { newDocument } = applyPatch(oldVariants, newVariants);
|
await this.featureStrategiesStore.getFeatureToggleWithVariantEnvs(
|
||||||
return this.saveVariants(featureName, project, newDocument, createdBy);
|
featureName,
|
||||||
|
);
|
||||||
|
const promises = ft.environments.map((env) =>
|
||||||
|
this.updateVariantsOnEnv(
|
||||||
|
featureName,
|
||||||
|
project,
|
||||||
|
env.name,
|
||||||
|
newVariants,
|
||||||
|
createdBy,
|
||||||
|
).then((resultingVariants) => (env.variants = resultingVariants)),
|
||||||
|
);
|
||||||
|
await Promise.all(promises);
|
||||||
|
ft.variants = ft.environments[0].variants;
|
||||||
|
return ft;
|
||||||
}
|
}
|
||||||
|
|
||||||
async updateVariantsOnEnv(
|
async updateVariantsOnEnv(
|
||||||
@ -1273,6 +1287,7 @@ class FeatureToggleService {
|
|||||||
environment,
|
environment,
|
||||||
newDocument,
|
newDocument,
|
||||||
createdBy,
|
createdBy,
|
||||||
|
oldVariants,
|
||||||
);
|
);
|
||||||
}
|
}
|
||||||
|
|
||||||
@ -1312,15 +1327,18 @@ class FeatureToggleService {
|
|||||||
environment: string,
|
environment: string,
|
||||||
newVariants: IVariant[],
|
newVariants: IVariant[],
|
||||||
createdBy: string,
|
createdBy: string,
|
||||||
|
oldVariants?: IVariant[],
|
||||||
): Promise<IVariant[]> {
|
): Promise<IVariant[]> {
|
||||||
await variantsArraySchema.validateAsync(newVariants);
|
await variantsArraySchema.validateAsync(newVariants);
|
||||||
const fixedVariants = this.fixVariantWeights(newVariants);
|
const fixedVariants = this.fixVariantWeights(newVariants);
|
||||||
const oldVariants = (
|
const theOldVariants: IVariant[] =
|
||||||
await this.featureEnvironmentStore.get({
|
oldVariants ||
|
||||||
featureName,
|
(
|
||||||
environment,
|
await this.featureEnvironmentStore.get({
|
||||||
})
|
featureName,
|
||||||
).variants;
|
environment,
|
||||||
|
})
|
||||||
|
).variants;
|
||||||
|
|
||||||
await this.eventStore.store(
|
await this.eventStore.store(
|
||||||
new EnvironmentVariantEvent({
|
new EnvironmentVariantEvent({
|
||||||
@ -1328,7 +1346,7 @@ class FeatureToggleService {
|
|||||||
environment,
|
environment,
|
||||||
project: projectId,
|
project: projectId,
|
||||||
createdBy,
|
createdBy,
|
||||||
oldVariants,
|
oldVariants: theOldVariants,
|
||||||
newVariants: fixedVariants,
|
newVariants: fixedVariants,
|
||||||
}),
|
}),
|
||||||
);
|
);
|
||||||
|
@ -120,6 +120,104 @@ test('Can patch variants for a feature and get a response of new variant', async
|
|||||||
});
|
});
|
||||||
});
|
});
|
||||||
|
|
||||||
|
test('Can patch variants for a feature patches all environments independently', async () => {
|
||||||
|
const featureName = 'feature-to-patch';
|
||||||
|
const addedVariantName = 'patched-variant-name';
|
||||||
|
const variants = (name: string) => [
|
||||||
|
{
|
||||||
|
name,
|
||||||
|
stickiness: 'default',
|
||||||
|
weight: 1000,
|
||||||
|
weightType: WeightType.VARIABLE,
|
||||||
|
},
|
||||||
|
];
|
||||||
|
|
||||||
|
await db.stores.environmentStore.create({
|
||||||
|
name: 'development',
|
||||||
|
type: 'development',
|
||||||
|
});
|
||||||
|
await db.stores.environmentStore.create({
|
||||||
|
name: 'production',
|
||||||
|
type: 'production',
|
||||||
|
});
|
||||||
|
await db.stores.featureToggleStore.create('default', {
|
||||||
|
name: featureName,
|
||||||
|
});
|
||||||
|
await db.stores.featureEnvironmentStore.addEnvironmentToFeature(
|
||||||
|
featureName,
|
||||||
|
'development',
|
||||||
|
true,
|
||||||
|
);
|
||||||
|
await db.stores.featureEnvironmentStore.addEnvironmentToFeature(
|
||||||
|
featureName,
|
||||||
|
'production',
|
||||||
|
true,
|
||||||
|
);
|
||||||
|
await db.stores.featureEnvironmentStore.addVariantsToFeatureEnvironment(
|
||||||
|
featureName,
|
||||||
|
'development',
|
||||||
|
variants('dev-variant'),
|
||||||
|
);
|
||||||
|
await db.stores.featureEnvironmentStore.addVariantsToFeatureEnvironment(
|
||||||
|
featureName,
|
||||||
|
'production',
|
||||||
|
variants('prod-variant'),
|
||||||
|
);
|
||||||
|
|
||||||
|
const patch = [
|
||||||
|
{
|
||||||
|
op: 'add',
|
||||||
|
path: '/1',
|
||||||
|
value: {
|
||||||
|
name: addedVariantName,
|
||||||
|
weightType: 'fix',
|
||||||
|
weight: 50,
|
||||||
|
},
|
||||||
|
},
|
||||||
|
];
|
||||||
|
|
||||||
|
await app.request
|
||||||
|
.patch(`/api/admin/projects/default/features/${featureName}/variants`)
|
||||||
|
.send(patch)
|
||||||
|
.expect(200)
|
||||||
|
.expect((res) => {
|
||||||
|
expect(res.body.version).toBe(1);
|
||||||
|
expect(res.body.variants).toHaveLength(2);
|
||||||
|
// it picks variants from the first environment (sorted by name)
|
||||||
|
expect(res.body.variants[0].name).toBe('dev-variant');
|
||||||
|
expect(res.body.variants[1].name).toBe(addedVariantName);
|
||||||
|
});
|
||||||
|
|
||||||
|
await app.request
|
||||||
|
.get(
|
||||||
|
`/api/admin/projects/default/features/${featureName}?variantEnvironments=true`,
|
||||||
|
)
|
||||||
|
.expect((res) => {
|
||||||
|
const environments = res.body.environments;
|
||||||
|
expect(environments).toHaveLength(2);
|
||||||
|
const developmentVariants = environments.find(
|
||||||
|
(x) => x.name === 'development',
|
||||||
|
).variants;
|
||||||
|
const productionVariants = environments.find(
|
||||||
|
(x) => x.name === 'production',
|
||||||
|
).variants;
|
||||||
|
expect(developmentVariants).toHaveLength(2);
|
||||||
|
expect(productionVariants).toHaveLength(2);
|
||||||
|
expect(
|
||||||
|
developmentVariants.find((x) => x.name === addedVariantName),
|
||||||
|
).toBeTruthy();
|
||||||
|
expect(
|
||||||
|
productionVariants.find((x) => x.name === addedVariantName),
|
||||||
|
).toBeTruthy();
|
||||||
|
expect(
|
||||||
|
developmentVariants.find((x) => x.name === 'dev-variant'),
|
||||||
|
).toBeTruthy();
|
||||||
|
expect(
|
||||||
|
productionVariants.find((x) => x.name === 'prod-variant'),
|
||||||
|
).toBeTruthy();
|
||||||
|
});
|
||||||
|
});
|
||||||
|
|
||||||
test('Can add variant for a feature', async () => {
|
test('Can add variant for a feature', async () => {
|
||||||
const featureName = 'feature-variants-patch-add';
|
const featureName = 'feature-variants-patch-add';
|
||||||
const variantName = 'fancy-variant-patch';
|
const variantName = 'fancy-variant-patch';
|
||||||
@ -315,6 +413,11 @@ test('Invalid variant in PATCH also throws 400 exception', async () => {
|
|||||||
await db.stores.featureToggleStore.create('default', {
|
await db.stores.featureToggleStore.create('default', {
|
||||||
name: featureName,
|
name: featureName,
|
||||||
});
|
});
|
||||||
|
await db.stores.featureEnvironmentStore.addEnvironmentToFeature(
|
||||||
|
featureName,
|
||||||
|
'default',
|
||||||
|
true,
|
||||||
|
);
|
||||||
|
|
||||||
const invalidPatch = `[{
|
const invalidPatch = `[{
|
||||||
"op": "add",
|
"op": "add",
|
||||||
@ -439,6 +542,11 @@ test('PATCHING with no variable variants fails with 400', async () => {
|
|||||||
await db.stores.featureToggleStore.create('default', {
|
await db.stores.featureToggleStore.create('default', {
|
||||||
name: featureName,
|
name: featureName,
|
||||||
});
|
});
|
||||||
|
await db.stores.featureEnvironmentStore.addEnvironmentToFeature(
|
||||||
|
featureName,
|
||||||
|
'default',
|
||||||
|
true,
|
||||||
|
);
|
||||||
|
|
||||||
const newVariants: IVariant[] = [];
|
const newVariants: IVariant[] = [];
|
||||||
|
|
||||||
|
Loading…
Reference in New Issue
Block a user