mirror of
https://github.com/Unleash/unleash.git
synced 2025-02-04 00:18:01 +01:00
feat: clone feature toggle API (#1006)
This commit is contained in:
parent
b4b222f4c9
commit
3612884501
@ -45,6 +45,7 @@ interface StrategyUpdateBody {
|
||||
|
||||
const PATH = '/:projectId/features';
|
||||
const PATH_FEATURE = `${PATH}/:featureName`;
|
||||
const PATH_FEATURE_CLONE = `${PATH_FEATURE}/clone`;
|
||||
const PATH_ENV = `${PATH_FEATURE}/environments/:environment`;
|
||||
const PATH_STRATEGIES = `${PATH_ENV}/strategies`;
|
||||
const PATH_STRATEGY = `${PATH_STRATEGIES}/:strategyId`;
|
||||
@ -82,6 +83,8 @@ export default class ProjectFeaturesController extends Controller {
|
||||
this.get(PATH, this.getFeatures);
|
||||
this.post(PATH, this.createFeature, CREATE_FEATURE);
|
||||
|
||||
this.post(PATH_FEATURE_CLONE, this.cloneFeature, CREATE_FEATURE);
|
||||
|
||||
this.get(PATH_FEATURE, this.getFeature);
|
||||
this.put(PATH_FEATURE, this.updateFeature);
|
||||
this.patch(PATH_FEATURE, this.patchFeature);
|
||||
@ -99,8 +102,30 @@ export default class ProjectFeaturesController extends Controller {
|
||||
res.json({ version: 1, features });
|
||||
}
|
||||
|
||||
async cloneFeature(
|
||||
req: IAuthRequest<
|
||||
FeatureParams,
|
||||
any,
|
||||
{ name: string; replaceGroupId: boolean },
|
||||
any
|
||||
>,
|
||||
res: Response,
|
||||
): Promise<void> {
|
||||
const { projectId, featureName } = req.params;
|
||||
const { name, replaceGroupId } = req.body;
|
||||
const userName = extractUsername(req);
|
||||
const created = await this.featureService.cloneFeatureToggle(
|
||||
featureName,
|
||||
projectId,
|
||||
name,
|
||||
replaceGroupId,
|
||||
userName,
|
||||
);
|
||||
res.status(201).json(created);
|
||||
}
|
||||
|
||||
async createFeature(
|
||||
req: IAuthRequest<ProjectParam, any, FeatureToggleDTO, any>,
|
||||
req: IAuthRequest<FeatureParams, any, FeatureToggleDTO, any>,
|
||||
res: Response,
|
||||
): Promise<void> {
|
||||
const { projectId } = req.params;
|
||||
|
@ -35,6 +35,7 @@ import {
|
||||
FeatureToggleDTO,
|
||||
FeatureToggleWithEnvironment,
|
||||
FeatureToggleWithEnvironmentLegacy,
|
||||
IEnvironmentDetail,
|
||||
IFeatureEnvironmentInfo,
|
||||
IFeatureOverview,
|
||||
IFeatureStrategy,
|
||||
@ -372,6 +373,57 @@ class FeatureToggleServiceV2 {
|
||||
throw new NotFoundError(`Project with id ${projectId} does not exist`);
|
||||
}
|
||||
|
||||
async cloneFeatureToggle(
|
||||
featureName: string,
|
||||
projectId: string,
|
||||
newFeatureName: string,
|
||||
replaceGroupId: boolean = true,
|
||||
userName: string,
|
||||
): Promise<FeatureToggle> {
|
||||
this.logger.info(
|
||||
`${userName} clones feature toggle ${featureName} to ${newFeatureName}`,
|
||||
);
|
||||
await this.validateName(newFeatureName);
|
||||
|
||||
const cToggle =
|
||||
await this.featureStrategiesStore.getFeatureToggleWithEnvs(
|
||||
featureName,
|
||||
);
|
||||
|
||||
const newToggle = { ...cToggle, name: newFeatureName };
|
||||
|
||||
// Create feature toggle
|
||||
const created = await this.createFeatureToggle(
|
||||
projectId,
|
||||
newToggle,
|
||||
userName,
|
||||
);
|
||||
|
||||
const createStrategies = [];
|
||||
newToggle.environments.forEach((e: IEnvironmentDetail) =>
|
||||
e.strategies.forEach((s: IStrategyConfig) => {
|
||||
if (replaceGroupId && s.parameters.hasOwnProperty('groupId')) {
|
||||
//@ts-ignore
|
||||
s.parameters.groupId = newFeatureName;
|
||||
}
|
||||
delete s.id;
|
||||
createStrategies.push(
|
||||
this.createStrategy(
|
||||
s,
|
||||
projectId,
|
||||
newFeatureName,
|
||||
userName,
|
||||
e.name,
|
||||
),
|
||||
);
|
||||
}),
|
||||
);
|
||||
|
||||
// Create strategies
|
||||
await Promise.allSettled(createStrategies);
|
||||
return created;
|
||||
}
|
||||
|
||||
async updateFeatureToggle(
|
||||
projectId: string,
|
||||
updatedFeature: FeatureToggleDTO,
|
||||
|
@ -13,7 +13,7 @@ export interface IStrategyConfig {
|
||||
id?: string;
|
||||
name: string;
|
||||
constraints: IConstraint[];
|
||||
parameters: Object;
|
||||
parameters: object;
|
||||
sortOrder?: number;
|
||||
}
|
||||
export interface IFeatureStrategy {
|
||||
@ -37,6 +37,7 @@ export interface FeatureToggleDTO {
|
||||
variants?: IVariant[];
|
||||
createdAt?: Date;
|
||||
}
|
||||
|
||||
export interface FeatureToggle extends FeatureToggleDTO {
|
||||
project: string;
|
||||
lastSeenAt?: Date;
|
||||
|
@ -1303,3 +1303,132 @@ test('Deleting strategy for feature environment should not disable that environm
|
||||
expect(res.body.enabled).toBeTruthy();
|
||||
});
|
||||
});
|
||||
|
||||
test('should clone feature toggle without strategies', async () => {
|
||||
const envName = 'some-env-3';
|
||||
const featureName = 'feature.toggle.base';
|
||||
const cloneName = 'feature.toggle.clone';
|
||||
const type = 'eExperiment';
|
||||
const description = 'Lorem ipsum...';
|
||||
|
||||
// Create environment
|
||||
await db.stores.environmentStore.create({
|
||||
name: envName,
|
||||
type: 'production',
|
||||
});
|
||||
// Connect environment to project
|
||||
await app.request
|
||||
.post('/api/admin/projects/default/environments')
|
||||
.send({
|
||||
environment: envName,
|
||||
})
|
||||
.expect(200);
|
||||
|
||||
await app.request
|
||||
.post('/api/admin/projects/default/features')
|
||||
.send({ name: featureName, description, type })
|
||||
.expect(201);
|
||||
await app.request
|
||||
.post(`/api/admin/projects/default/features/${featureName}/clone`)
|
||||
.send({ name: cloneName, replaceGroupId: false })
|
||||
.expect(201);
|
||||
await app.request
|
||||
.get(`/api/admin/projects/default/features/${cloneName}`)
|
||||
.expect(200)
|
||||
.expect((res) => {
|
||||
expect(res.body.name).toBe(cloneName);
|
||||
expect(res.body.type).toBe(type);
|
||||
expect(res.body.project).toBe('default');
|
||||
expect(res.body.description).toBe(description);
|
||||
});
|
||||
});
|
||||
|
||||
test('should clone feature toggle WITH strategies', async () => {
|
||||
const envName = 'some-env-4';
|
||||
const featureName = 'feature.toggle.base.2';
|
||||
const cloneName = 'feature.toggle.clone.2';
|
||||
const type = 'eExperiment';
|
||||
const description = 'Lorem ipsum...';
|
||||
|
||||
// Create environment
|
||||
await db.stores.environmentStore.create({
|
||||
name: envName,
|
||||
type: 'production',
|
||||
});
|
||||
// Connect environment to project
|
||||
await app.request
|
||||
.post('/api/admin/projects/default/environments')
|
||||
.send({
|
||||
environment: envName,
|
||||
})
|
||||
.expect(200);
|
||||
|
||||
await app.request
|
||||
.post('/api/admin/projects/default/features')
|
||||
.send({ name: featureName, description, type })
|
||||
.expect(201);
|
||||
await app.request
|
||||
.post(
|
||||
`/api/admin/projects/default/features/${featureName}/environments/${envName}/strategies`,
|
||||
)
|
||||
.send({
|
||||
name: 'flexibleRollout',
|
||||
parameters: {
|
||||
groupId: featureName,
|
||||
},
|
||||
})
|
||||
.expect(200);
|
||||
|
||||
await app.request
|
||||
.post(`/api/admin/projects/default/features/${featureName}/clone`)
|
||||
.send({ name: cloneName })
|
||||
.expect(201);
|
||||
await app.request
|
||||
.get(`/api/admin/projects/default/features/${cloneName}`)
|
||||
.expect(200)
|
||||
.expect((res) => {
|
||||
expect(res.body.name).toBe(cloneName);
|
||||
expect(res.body.type).toBe(type);
|
||||
expect(res.body.project).toBe('default');
|
||||
expect(res.body.description).toBe(description);
|
||||
|
||||
const env = res.body.environments.find((e) => e.name === envName);
|
||||
expect(env.strategies).toHaveLength(1);
|
||||
expect(env.strategies[0].parameters.groupId).toBe(cloneName);
|
||||
});
|
||||
});
|
||||
|
||||
test('should clone feature toggle without replacing groupId', async () => {
|
||||
const envName = 'default';
|
||||
const featureName = 'feature.toggle.base.3';
|
||||
const cloneName = 'feature.toggle.clone.3';
|
||||
|
||||
await app.request
|
||||
.post('/api/admin/projects/default/features')
|
||||
.send({ name: featureName })
|
||||
.expect(201);
|
||||
await app.request
|
||||
.post(
|
||||
`/api/admin/projects/default/features/${featureName}/environments/${envName}/strategies`,
|
||||
)
|
||||
.send({
|
||||
name: 'flexibleRollout',
|
||||
parameters: {
|
||||
groupId: featureName,
|
||||
},
|
||||
})
|
||||
.expect(200);
|
||||
|
||||
await app.request
|
||||
.post(`/api/admin/projects/default/features/${featureName}/clone`)
|
||||
.send({ name: cloneName, replaceGroupId: false })
|
||||
.expect(201);
|
||||
await app.request
|
||||
.get(`/api/admin/projects/default/features/${cloneName}`)
|
||||
.expect(200)
|
||||
.expect((res) => {
|
||||
const env = res.body.environments.find((e) => e.name === envName);
|
||||
expect(env.strategies).toHaveLength(1);
|
||||
expect(env.strategies[0].parameters.groupId).toBe(featureName);
|
||||
});
|
||||
});
|
@ -308,6 +308,58 @@ Some fields is not possible to change via this endpoint:
|
||||
- lastSeen
|
||||
|
||||
|
||||
### Clone Feature Toggle {#create-toggle}
|
||||
|
||||
`http://localhost:4242/api/admin/projects/:projectId/features/:featureName/clone`
|
||||
|
||||
This endpoint will accept HTTP POST request to clone an existing feature toggle with all strategies and variants. It is not possible to clone archived feature toggles. The newly created feature toggle will be disabled for all environments.
|
||||
|
||||
**Example Query**
|
||||
|
||||
```sh
|
||||
echo '{ "name": "newName" }' | http POST http://localhost:4242/api/admin/projects/default/features/Demo/clone Authorization:$KEY`
|
||||
```
|
||||
|
||||
|
||||
**Example response:**
|
||||
|
||||
```json
|
||||
HTTP/1.1 201 Created
|
||||
Access-Control-Allow-Origin: *
|
||||
Connection: keep-alive
|
||||
Content-Length: 260
|
||||
Content-Type: application/json; charset=utf-8
|
||||
Date: Wed, 06 Oct 2021 20:04:39 GMT
|
||||
ETag: W/"104-joC/gdjtJ29jZMxj91lIzR42Pmo"
|
||||
Keep-Alive: timeout=60
|
||||
Vary: Accept-Encoding
|
||||
|
||||
{
|
||||
"createdAt": "2021-09-29T10:22:28.523Z",
|
||||
"description": "Some useful description",
|
||||
"lastSeenAt": null,
|
||||
"name": "DemoNew",
|
||||
"project": "default",
|
||||
"stale": false,
|
||||
"type": "release",
|
||||
"variants": [
|
||||
{
|
||||
"name": "blue",
|
||||
"overrides": [],
|
||||
"stickiness": "default",
|
||||
"weight": 1000,
|
||||
"weightType": "variable"
|
||||
}
|
||||
]
|
||||
}
|
||||
|
||||
```
|
||||
|
||||
Possible Errors:
|
||||
|
||||
- _409 Conflict_ - A toggle with that name already exists
|
||||
|
||||
|
||||
### Archive Feature Toggle {#archive-toggle}
|
||||
|
||||
`http://localhost:4242/api/admin/projects/:projectId/features/:featureName`
|
||||
|
Loading…
Reference in New Issue
Block a user