1
0
mirror of https://github.com/Unleash/unleash.git synced 2025-02-14 00:19:16 +01:00

fix: Should not remove variants when updating feature toggle metadata (#1234)

This commit is contained in:
Ivar Conradi Østhus 2022-01-06 10:23:52 +01:00 committed by GitHub
parent e409e9e751
commit 2b59a4219a
No known key found for this signature in database
GPG Key ID: 4AEE18F83AFDEB23
10 changed files with 148 additions and 82 deletions

View File

@ -23,7 +23,7 @@ export interface FeaturesTable {
description: string; description: string;
type: string; type: string;
stale: boolean; stale: boolean;
variants: string; variants?: string;
project: string; project: string;
last_seen_at?: Date; last_seen_at?: Date;
created_at?: Date; created_at?: Date;
@ -187,13 +187,6 @@ export default class FeatureToggleStore implements IFeatureToggleStore {
project, project,
archived: data.archived || false, archived: data.archived || false,
stale: data.stale, stale: data.stale,
variants: data.variants
? JSON.stringify(
data.variants.sort((a, b) =>
a.name.localeCompare(b.name),
),
)
: JSON.stringify([]),
created_at: data.createdAt, created_at: data.createdAt,
}; };
if (!row.created_at) { if (!row.created_at) {

View File

@ -141,9 +141,9 @@ class FeatureController extends Controller {
const toggle = req.body; const toggle = req.body;
const validatedToggle = await featureSchema.validateAsync(toggle); const validatedToggle = await featureSchema.validateAsync(toggle);
const { enabled } = validatedToggle; const { enabled, project, name, variants = [] } = validatedToggle;
const createdFeature = await this.service.createFeatureToggle( const createdFeature = await this.service.createFeatureToggle(
validatedToggle.project, project,
validatedToggle, validatedToggle,
userName, userName,
true, true,
@ -153,8 +153,8 @@ class FeatureController extends Controller {
this.service.createStrategy( this.service.createStrategy(
s, s,
{ {
projectId: createdFeature.project, projectId: project,
featureName: createdFeature.name, featureName: name,
environment: DEFAULT_ENV, environment: DEFAULT_ENV,
}, },
userName, userName,
@ -162,15 +162,17 @@ class FeatureController extends Controller {
), ),
); );
await this.service.updateEnabled( await this.service.updateEnabled(
createdFeature.project, project,
createdFeature.name, name,
DEFAULT_ENV, DEFAULT_ENV,
enabled, enabled,
userName, userName,
); );
await this.service.saveVariants(name, project, variants, userName);
res.status(201).json({ res.status(201).json({
...createdFeature, ...createdFeature,
variants,
enabled, enabled,
strategies, strategies,
}); });
@ -183,7 +185,7 @@ class FeatureController extends Controller {
updatedFeature.name = featureName; updatedFeature.name = featureName;
const projectId = await this.service.getProjectId(updatedFeature.name); const projectId = await this.service.getProjectId(featureName);
const value = await featureSchema.validateAsync(updatedFeature); const value = await featureSchema.validateAsync(updatedFeature);
await this.service.updateFeatureToggle(projectId, value, userName); await this.service.updateFeatureToggle(projectId, value, userName);
@ -203,11 +205,17 @@ class FeatureController extends Controller {
} }
await this.service.updateEnabled( await this.service.updateEnabled(
projectId, projectId,
updatedFeature.name, featureName,
DEFAULT_ENV, DEFAULT_ENV,
updatedFeature.enabled, updatedFeature.enabled,
userName, userName,
); );
await this.service.saveVariants(
featureName,
projectId,
value.variants || [],
userName,
);
const feature = await this.service.storeFeatureUpdatedEventLegacy( const feature = await this.service.storeFeatureUpdatedEventLegacy(
featureName, featureName,

View File

@ -101,7 +101,7 @@ class FeatureToggleService {
>, >,
{ getLogger }: Pick<IUnleashConfig, 'getLogger'>, { getLogger }: Pick<IUnleashConfig, 'getLogger'>,
) { ) {
this.logger = getLogger('services/feature-toggle-service-v2.ts'); this.logger = getLogger('services/feature-toggle-service.ts');
this.featureStrategiesStore = featureStrategiesStore; this.featureStrategiesStore = featureStrategiesStore;
this.featureToggleStore = featureToggleStore; this.featureToggleStore = featureToggleStore;
this.featureToggleClientStore = featureToggleClientStore; this.featureToggleClientStore = featureToggleClientStore;

View File

@ -324,17 +324,20 @@ export default class StateService {
features features
.filter(filterExisting(keepExisting, oldToggles)) .filter(filterExisting(keepExisting, oldToggles))
.filter(filterEqual(oldToggles)) .filter(filterEqual(oldToggles))
.map((feature) => .map(async (feature) => {
this.toggleStore const { name, project, variants = [] } = feature;
.create(feature.project, feature) await this.toggleStore.create(feature.project, feature);
.then(() => { await this.toggleStore.saveVariants(
this.eventStore.store({ project,
name,
variants,
);
await this.eventStore.store({
type: FEATURE_IMPORT, type: FEATURE_IMPORT,
createdBy: userName, createdBy: userName,
data: feature, data: feature,
}); });
}), }),
),
); );
} }

View File

@ -37,7 +37,6 @@ export interface FeatureToggleDTO {
type?: string; type?: string;
stale?: boolean; stale?: boolean;
archived?: boolean; archived?: boolean;
variants?: IVariant[];
createdAt?: Date; createdAt?: Date;
} }
@ -45,6 +44,7 @@ export interface FeatureToggle extends FeatureToggleDTO {
project: string; project: string;
lastSeenAt?: Date; lastSeenAt?: Date;
createdAt?: Date; createdAt?: Date;
variants?: IVariant[];
} }
export interface IFeatureToggleClient { export interface IFeatureToggleClient {

View File

@ -537,6 +537,46 @@ test('Should patch feature toggle', async () => {
expect(updateForOurToggle.data.type).toBe('kill-switch'); expect(updateForOurToggle.data.type).toBe('kill-switch');
}); });
test('Should patch feature toggle and not remove variants', async () => {
const url = '/api/admin/projects/default/features';
const name = 'new.toggle.variants';
await app.request
.post(url)
.send({ name, description: 'some', type: 'release' })
.expect(201);
await app.request
.put(`${url}/${name}/variants`)
.send([
{
name: 'variant1',
weightType: 'variable',
weight: 500,
stickiness: 'default',
},
{
name: 'variant2',
weightType: 'variable',
weight: 500,
stickiness: 'default',
},
])
.expect(200);
await app.request
.patch(`${url}/${name}`)
.send([
{ op: 'replace', path: '/description', value: 'New desc' },
{ op: 'replace', path: '/type', value: 'kill-switch' },
])
.expect(200);
const { body: toggle } = await app.request.get(`${url}/${name}`);
expect(toggle.name).toBe(name);
expect(toggle.description).toBe('New desc');
expect(toggle.type).toBe('kill-switch');
expect(toggle.variants).toHaveLength(2);
});
test('Patching feature toggles to stale should trigger FEATURE_STALE_ON event', async () => { test('Patching feature toggles to stale should trigger FEATURE_STALE_ON event', async () => {
const url = '/api/admin/projects/default/features'; const url = '/api/admin/projects/default/features';
const name = 'toggle.stale.on.patch'; const name = 'toggle.stale.on.patch';

View File

@ -20,17 +20,15 @@ afterAll(async () => {
test('Can get variants for a feature', async () => { test('Can get variants for a feature', async () => {
const featureName = 'feature-variants'; const featureName = 'feature-variants';
const variantName = 'fancy-variant'; const variantName = 'fancy-variant';
await db.stores.featureToggleStore.create('default', { await db.stores.featureToggleStore.create('default', { name: featureName });
name: featureName, await db.stores.featureToggleStore.saveVariants('default', featureName, [
variants: [
{ {
name: variantName, name: variantName,
stickiness: 'default', stickiness: 'default',
weight: 1000, weight: 1000,
weightType: WeightType.VARIABLE, weightType: WeightType.VARIABLE,
}, },
], ]);
});
await app.request await app.request
.get(`/api/admin/projects/default/features/${featureName}/variants`) .get(`/api/admin/projects/default/features/${featureName}/variants`)
.expect(200) .expect(200)
@ -90,8 +88,12 @@ test('Can patch variants for a feature and get a response of new variant', async
await db.stores.featureToggleStore.create('default', { await db.stores.featureToggleStore.create('default', {
name: featureName, name: featureName,
variants,
}); });
await db.stores.featureToggleStore.saveVariants(
'default',
featureName,
variants,
);
const observer = jsonpatch.observe(variants); const observer = jsonpatch.observe(variants);
variants[0].name = expectedVariantName; variants[0].name = expectedVariantName;
@ -123,8 +125,12 @@ test('Can add variant for a feature', async () => {
await db.stores.featureToggleStore.create('default', { await db.stores.featureToggleStore.create('default', {
name: featureName, name: featureName,
variants,
}); });
await db.stores.featureToggleStore.saveVariants(
'default',
featureName,
variants,
);
const observer = jsonpatch.observe(variants); const observer = jsonpatch.observe(variants);
variants.push({ variants.push({
@ -167,8 +173,12 @@ test('Can remove variant for a feature', async () => {
await db.stores.featureToggleStore.create('default', { await db.stores.featureToggleStore.create('default', {
name: featureName, name: featureName,
variants,
}); });
await db.stores.featureToggleStore.saveVariants(
'default',
featureName,
variants,
);
const observer = jsonpatch.observe(variants); const observer = jsonpatch.observe(variants);
variants.pop(); variants.pop();
@ -200,8 +210,12 @@ test('PUT overwrites current variant on feature', async () => {
]; ];
await db.stores.featureToggleStore.create('default', { await db.stores.featureToggleStore.create('default', {
name: featureName, name: featureName,
variants,
}); });
await db.stores.featureToggleStore.saveVariants(
'default',
featureName,
variants,
);
const newVariants: IVariant[] = [ const newVariants: IVariant[] = [
{ {
@ -646,18 +660,24 @@ test('If sum of fixed variant weight equals 1000 variable variants gets weight 0
test('PATCH endpoint validates uniqueness of variant names', async () => { test('PATCH endpoint validates uniqueness of variant names', async () => {
const featureName = 'variants-uniqueness-names'; const featureName = 'variants-uniqueness-names';
await db.stores.featureToggleStore.create('default', { const variants = [
name: featureName,
variants: [
{ {
name: 'variant1', name: 'variant1',
weight: 1000, weight: 1000,
weightType: WeightType.VARIABLE, weightType: WeightType.VARIABLE,
stickiness: 'default', stickiness: 'default',
}, },
], ];
await db.stores.featureToggleStore.create('default', {
name: featureName,
}); });
await db.stores.featureToggleStore.saveVariants(
'default',
featureName,
variants,
);
const newVariants: IVariant[] = []; const newVariants: IVariant[] = [];
const observer = jsonpatch.observe(newVariants); const observer = jsonpatch.observe(newVariants);
@ -689,7 +709,6 @@ test('PUT endpoint validates uniqueness of variant names', async () => {
const featureName = 'variants-put-uniqueness-names'; const featureName = 'variants-put-uniqueness-names';
await db.stores.featureToggleStore.create('default', { await db.stores.featureToggleStore.create('default', {
name: featureName, name: featureName,
variants: [],
}); });
await app.request await app.request
.put(`/api/admin/projects/default/features/${featureName}/variants`) .put(`/api/admin/projects/default/features/${featureName}/variants`)

View File

@ -32,8 +32,6 @@ test('exports strategies and features as json by default', async () => {
}); });
test('exports strategies and features as yaml', async () => { test('exports strategies and features as yaml', async () => {
expect.assertions(0);
return app.request return app.request
.get('/api/admin/state/export?format=yaml') .get('/api/admin/state/export?format=yaml')
.expect('Content-Type', /yaml/) .expect('Content-Type', /yaml/)
@ -41,8 +39,6 @@ test('exports strategies and features as yaml', async () => {
}); });
test('exports only features as yaml', async () => { test('exports only features as yaml', async () => {
expect.assertions(0);
return app.request return app.request
.get('/api/admin/state/export?format=yaml&featureToggles=1') .get('/api/admin/state/export?format=yaml&featureToggles=1')
.expect('Content-Type', /yaml/) .expect('Content-Type', /yaml/)
@ -50,8 +46,6 @@ test('exports only features as yaml', async () => {
}); });
test('exports strategies and features as attachment', async () => { test('exports strategies and features as attachment', async () => {
expect.assertions(0);
return app.request return app.request
.get('/api/admin/state/export?download=1') .get('/api/admin/state/export?download=1')
.expect('Content-Type', /json/) .expect('Content-Type', /json/)
@ -60,17 +54,26 @@ test('exports strategies and features as attachment', async () => {
}); });
test('imports strategies and features', async () => { test('imports strategies and features', async () => {
expect.assertions(0);
return app.request return app.request
.post('/api/admin/state/import') .post('/api/admin/state/import')
.send(importData) .send(importData)
.expect(202); .expect(202);
}); });
test('does not not accept gibberish', async () => { test('imports features with variants', async () => {
expect.assertions(0); await app.request
.post('/api/admin/state/import')
.send(importData)
.expect(202);
const { body } = await app.request.get(
'/api/admin/projects/default/features/feature.with.variants',
);
expect(body.variants).toHaveLength(2);
});
test('does not not accept gibberish', async () => {
return app.request return app.request
.post('/api/admin/state/import') .post('/api/admin/state/import')
.send({ features: 'nonsense' }) .send({ features: 'nonsense' })
@ -78,8 +81,6 @@ test('does not not accept gibberish', async () => {
}); });
test('imports strategies and features from json file', async () => { test('imports strategies and features from json file', async () => {
expect.assertions(0);
return app.request return app.request
.post('/api/admin/state/import') .post('/api/admin/state/import')
.attach('file', 'src/test/examples/import.json') .attach('file', 'src/test/examples/import.json')
@ -87,8 +88,6 @@ test('imports strategies and features from json file', async () => {
}); });
test('imports strategies and features from yaml file', async () => { test('imports strategies and features from yaml file', async () => {
expect.assertions(0);
return app.request return app.request
.post('/api/admin/state/import') .post('/api/admin/state/import')
.attach('file', 'src/test/examples/import.yml') .attach('file', 'src/test/examples/import.yml')

View File

@ -77,7 +77,13 @@ beforeAll(async () => {
{ {
name: 'feature.with.variants', name: 'feature.with.variants',
description: 'A feature toggle with variants', description: 'A feature toggle with variants',
variants: [ },
'test',
);
await app.services.featureToggleServiceV2.saveVariants(
'feature.with.variants',
'default',
[
{ {
name: 'control', name: 'control',
weight: 50, weight: 50,
@ -87,12 +93,11 @@ beforeAll(async () => {
{ {
name: 'new', name: 'new',
weight: 50, weight: 50,
weightType: 'fix', weightType: 'variable',
stickiness: 'default', stickiness: 'default',
}, },
], ],
}, 'ivar',
'test',
); );
}); });

View File

@ -49,7 +49,6 @@ test('Can connect environment to project', async () => {
type: 'release', type: 'release',
description: '', description: '',
stale: false, stale: false,
variants: [],
}); });
await service.addEnvironmentToProject('test-connection', 'default'); await service.addEnvironmentToProject('test-connection', 'default');
const overview = await stores.featureStrategiesStore.getFeatureOverview( const overview = await stores.featureStrategiesStore.getFeatureOverview(