/* 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 FeatureToggleServiceV2 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: FeatureToggleServiceV2; 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 { 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 { 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 { const name = req.params.featureName; const feature = await this.service.getFeatureToggleLegacy(name); res.json(feature).end(); } async listTags(req: Request, res: Response): Promise { const tags = await this.tagService.listTags(req.params.featureName); res.json({ version, tags }); } async addTag(req: IAuthRequest, res: Response): Promise { 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 { 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 { const { name } = req.body; await this.service.validateName(name); res.status(200).end(); } async createToggle(req: IAuthRequest, res: Response): Promise { 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, ); 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 { 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 { 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 { 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 { 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 { 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 { 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 { const { featureName } = req.params; const userName = extractUsername(req); await this.service.archiveToggle(featureName, userName); res.status(200).end(); } } export default FeatureController;