mirror of
https://github.com/Unleash/unleash.git
synced 2025-02-04 00:18:01 +01:00
5cdb3f665a
* task: Ban changes to variants through feature After adding the new `/variants` endpoint for features we now have a way to access control adding/modifying variants, so the /:featureName endpoint should no longer allow editing/adding variants. This removes variants as a known field from the featureMetadata schema and tells joi to stripUnknown, thus making sure we never include variants in the initial creation or future update calls. For the old features v1 API we allow it to declare that it has already validated the data coming with its own schema, so we should use the data we get from it. Thus keeping the old v1 functionality intact Co-authored-by: Simon Hornby <simon@getunleash.ai>
299 lines
9.7 KiB
TypeScript
299 lines
9.7 KiB
TypeScript
/* eslint-disable @typescript-eslint/explicit-module-boundary-types */
|
|
import { Request, Response } from 'express';
|
|
|
|
import Controller from '../controller';
|
|
|
|
import { extractUsername } from '../../util/extract-user';
|
|
import {
|
|
UPDATE_FEATURE,
|
|
DELETE_FEATURE,
|
|
CREATE_FEATURE,
|
|
} from '../../types/permissions';
|
|
import { IUnleashConfig } from '../../types/option';
|
|
import { IUnleashServices } from '../../types/services';
|
|
import FeatureToggleService from '../../services/feature-toggle-service';
|
|
import { featureSchema, querySchema } from '../../schema/feature-schema';
|
|
import { IFeatureToggleQuery } from '../../types/model';
|
|
import FeatureTagService from '../../services/feature-tag-service';
|
|
import { IAuthRequest } from '../unleash-types';
|
|
import { DEFAULT_ENV } from '../../util/constants';
|
|
|
|
const version = 1;
|
|
|
|
class FeatureController extends Controller {
|
|
private tagService: FeatureTagService;
|
|
|
|
private service: FeatureToggleService;
|
|
|
|
constructor(
|
|
config: IUnleashConfig,
|
|
{
|
|
featureTagService,
|
|
featureToggleServiceV2,
|
|
}: Pick<
|
|
IUnleashServices,
|
|
'featureTagService' | 'featureToggleServiceV2'
|
|
>,
|
|
) {
|
|
super(config);
|
|
this.tagService = featureTagService;
|
|
this.service = featureToggleServiceV2;
|
|
|
|
this.get('/', this.getAllToggles);
|
|
this.post('/', this.createToggle, CREATE_FEATURE);
|
|
this.get('/:featureName', this.getToggle);
|
|
this.put('/:featureName', this.updateToggle, UPDATE_FEATURE);
|
|
this.delete('/:featureName', this.archiveToggle, DELETE_FEATURE);
|
|
this.post('/validate', this.validate);
|
|
this.post('/:featureName/toggle', this.toggle, UPDATE_FEATURE);
|
|
this.post('/:featureName/toggle/on', this.toggleOn, UPDATE_FEATURE);
|
|
this.post('/:featureName/toggle/off', this.toggleOff, UPDATE_FEATURE);
|
|
this.post('/:featureName/stale/on', this.staleOn, UPDATE_FEATURE);
|
|
this.post('/:featureName/stale/off', this.staleOff, UPDATE_FEATURE);
|
|
this.get('/:featureName/tags', this.listTags);
|
|
this.post('/:featureName/tags', this.addTag, UPDATE_FEATURE);
|
|
this.delete(
|
|
'/:featureName/tags/:type/:value',
|
|
this.removeTag,
|
|
UPDATE_FEATURE,
|
|
);
|
|
}
|
|
|
|
// eslint-disable-next-line @typescript-eslint/explicit-module-boundary-types
|
|
paramToArray(param: any) {
|
|
if (!param) {
|
|
return param;
|
|
}
|
|
return Array.isArray(param) ? param : [param];
|
|
}
|
|
|
|
async prepQuery({
|
|
tag,
|
|
project,
|
|
namePrefix,
|
|
}: any): Promise<IFeatureToggleQuery> {
|
|
if (!tag && !project && !namePrefix) {
|
|
return null;
|
|
}
|
|
const tagQuery = this.paramToArray(tag);
|
|
const projectQuery = this.paramToArray(project);
|
|
const query = await querySchema.validateAsync({
|
|
tag: tagQuery,
|
|
project: projectQuery,
|
|
namePrefix,
|
|
});
|
|
if (query.tag) {
|
|
query.tag = query.tag.map((q) => q.split(':'));
|
|
}
|
|
return query;
|
|
}
|
|
|
|
async getAllToggles(req: Request, res: Response): Promise<void> {
|
|
const query = await this.prepQuery(req.query);
|
|
const features = await this.service.getFeatureToggles(query);
|
|
|
|
res.json({ version, features });
|
|
}
|
|
|
|
async getToggle(
|
|
req: Request<{ featureName: string }, any, any, any>,
|
|
res: Response,
|
|
): Promise<void> {
|
|
const name = req.params.featureName;
|
|
const feature = await this.service.getFeatureToggleLegacy(name);
|
|
res.json(feature).end();
|
|
}
|
|
|
|
async listTags(req: Request, res: Response): Promise<void> {
|
|
const tags = await this.tagService.listTags(req.params.featureName);
|
|
res.json({ version, tags });
|
|
}
|
|
|
|
async addTag(req: IAuthRequest, res: Response): Promise<void> {
|
|
const { featureName } = req.params;
|
|
const userName = extractUsername(req);
|
|
const tag = await this.tagService.addTag(
|
|
featureName,
|
|
req.body,
|
|
userName,
|
|
);
|
|
res.status(201).json(tag);
|
|
}
|
|
|
|
// TODO
|
|
async removeTag(req: IAuthRequest, res: Response): Promise<void> {
|
|
const { featureName, type, value } = req.params;
|
|
const userName = extractUsername(req);
|
|
await this.tagService.removeTag(featureName, { type, value }, userName);
|
|
res.status(200).end();
|
|
}
|
|
|
|
async validate(req: Request, res: Response): Promise<void> {
|
|
const { name } = req.body;
|
|
|
|
await this.service.validateName(name);
|
|
res.status(200).end();
|
|
}
|
|
|
|
async createToggle(req: IAuthRequest, res: Response): Promise<void> {
|
|
const userName = extractUsername(req);
|
|
const toggle = req.body;
|
|
|
|
const validatedToggle = await featureSchema.validateAsync(toggle);
|
|
const { enabled } = validatedToggle;
|
|
const createdFeature = await this.service.createFeatureToggle(
|
|
validatedToggle.project,
|
|
validatedToggle,
|
|
userName,
|
|
true,
|
|
);
|
|
const strategies = await Promise.all(
|
|
toggle.strategies.map(async (s) =>
|
|
this.service.createStrategy(
|
|
s,
|
|
{
|
|
projectId: createdFeature.project,
|
|
featureName: createdFeature.name,
|
|
environment: DEFAULT_ENV,
|
|
},
|
|
userName,
|
|
),
|
|
),
|
|
);
|
|
await this.service.updateEnabled(
|
|
createdFeature.project,
|
|
createdFeature.name,
|
|
DEFAULT_ENV,
|
|
enabled,
|
|
userName,
|
|
);
|
|
|
|
res.status(201).json({
|
|
...createdFeature,
|
|
enabled,
|
|
strategies,
|
|
});
|
|
}
|
|
|
|
async updateToggle(req: IAuthRequest, res: Response): Promise<void> {
|
|
const { featureName } = req.params;
|
|
const userName = extractUsername(req);
|
|
const updatedFeature = req.body;
|
|
|
|
updatedFeature.name = featureName;
|
|
|
|
const projectId = await this.service.getProjectId(updatedFeature.name);
|
|
const value = await featureSchema.validateAsync(updatedFeature);
|
|
|
|
await this.service.updateFeatureToggle(projectId, value, userName);
|
|
|
|
await this.service.removeAllStrategiesForEnv(featureName);
|
|
|
|
if (updatedFeature.strategies) {
|
|
await Promise.all(
|
|
updatedFeature.strategies.map(async (s) =>
|
|
this.service.createStrategy(
|
|
s,
|
|
{ projectId, featureName, environment: DEFAULT_ENV },
|
|
userName,
|
|
),
|
|
),
|
|
);
|
|
}
|
|
await this.service.updateEnabled(
|
|
projectId,
|
|
updatedFeature.name,
|
|
DEFAULT_ENV,
|
|
updatedFeature.enabled,
|
|
userName,
|
|
);
|
|
|
|
const feature = await this.service.storeFeatureUpdatedEventLegacy(
|
|
featureName,
|
|
userName,
|
|
);
|
|
|
|
res.status(200).json(feature);
|
|
}
|
|
|
|
// TODO: remove?
|
|
// Kept to keep backward compatibility
|
|
async toggle(req: IAuthRequest, res: Response): Promise<void> {
|
|
const userName = extractUsername(req);
|
|
const { featureName } = req.params;
|
|
const projectId = await this.service.getProjectId(featureName);
|
|
const feature = await this.service.toggle(
|
|
projectId,
|
|
featureName,
|
|
DEFAULT_ENV,
|
|
userName,
|
|
);
|
|
await this.service.storeFeatureUpdatedEventLegacy(
|
|
featureName,
|
|
userName,
|
|
);
|
|
res.status(200).json(feature);
|
|
}
|
|
|
|
async toggleOn(req: IAuthRequest, res: Response): Promise<void> {
|
|
const { featureName } = req.params;
|
|
const userName = extractUsername(req);
|
|
const projectId = await this.service.getProjectId(featureName);
|
|
const feature = await this.service.updateEnabled(
|
|
projectId,
|
|
featureName,
|
|
DEFAULT_ENV,
|
|
true,
|
|
userName,
|
|
);
|
|
await this.service.storeFeatureUpdatedEventLegacy(
|
|
featureName,
|
|
userName,
|
|
);
|
|
res.json(feature);
|
|
}
|
|
|
|
async toggleOff(req: IAuthRequest, res: Response): Promise<void> {
|
|
const { featureName } = req.params;
|
|
const userName = extractUsername(req);
|
|
const projectId = await this.service.getProjectId(featureName);
|
|
const feature = await this.service.updateEnabled(
|
|
projectId,
|
|
featureName,
|
|
DEFAULT_ENV,
|
|
false,
|
|
userName,
|
|
);
|
|
await this.service.storeFeatureUpdatedEventLegacy(
|
|
featureName,
|
|
userName,
|
|
);
|
|
res.json(feature);
|
|
}
|
|
|
|
async staleOn(req: IAuthRequest, res: Response): Promise<void> {
|
|
const { featureName } = req.params;
|
|
const userName = extractUsername(req);
|
|
await this.service.updateStale(featureName, true, userName);
|
|
const feature = await this.service.getFeatureToggleLegacy(featureName);
|
|
res.json(feature);
|
|
}
|
|
|
|
async staleOff(req: IAuthRequest, res: Response): Promise<void> {
|
|
const { featureName } = req.params;
|
|
const userName = extractUsername(req);
|
|
await this.service.updateStale(featureName, false, userName);
|
|
const feature = await this.service.getFeatureToggleLegacy(featureName);
|
|
res.json(feature);
|
|
}
|
|
|
|
async archiveToggle(req: IAuthRequest, res: Response): Promise<void> {
|
|
const { featureName } = req.params;
|
|
const userName = extractUsername(req);
|
|
|
|
await this.service.archiveToggle(featureName, userName);
|
|
res.status(200).end();
|
|
}
|
|
}
|
|
export default FeatureController;
|