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:
parent
e409e9e751
commit
2b59a4219a
@ -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) {
|
||||||
|
@ -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,
|
||||||
|
@ -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;
|
||||||
|
@ -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,
|
||||||
});
|
});
|
||||||
}),
|
}),
|
||||||
),
|
|
||||||
);
|
);
|
||||||
}
|
}
|
||||||
|
|
||||||
|
@ -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 {
|
||||||
|
@ -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';
|
||||||
|
@ -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`)
|
||||||
|
@ -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')
|
||||||
|
@ -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',
|
|
||||||
);
|
);
|
||||||
});
|
});
|
||||||
|
|
||||||
|
@ -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(
|
||||||
|
Loading…
Reference in New Issue
Block a user