2021-11-24 13:08:04 +01:00
import FeatureToggleService from '../../../services/feature-toggle-service' ;
import { Logger } from '../../../logger' ;
import Controller from '../../controller' ;
import { IUnleashConfig } from '../../../types/option' ;
import { IUnleashServices } from '../../../types' ;
import { Request , Response } from 'express' ;
import { Operation } from 'fast-json-patch' ;
2022-11-22 10:54:04 +01:00
import {
NONE ,
UPDATE_FEATURE_ENVIRONMENT_VARIANTS ,
UPDATE_FEATURE_VARIANTS ,
} from '../../../types/permissions' ;
2023-01-20 10:30:20 +01:00
import { IVariant , WeightType } from '../../../types/model' ;
2021-12-16 11:07:19 +01:00
import { extractUsername } from '../../../util/extract-user' ;
import { IAuthRequest } from '../../unleash-types' ;
2022-06-03 13:16:59 +02:00
import { FeatureVariantsSchema } from '../../../openapi/spec/feature-variants-schema' ;
2022-07-01 08:06:33 +02:00
import { createRequestSchema } from '../../../openapi/util/create-request-schema' ;
import { createResponseSchema } from '../../../openapi/util/create-response-schema' ;
2023-01-20 10:30:20 +01:00
import { AccessService } from '../../../services' ;
import { BadDataError , NoAccessError } from '../../../../lib/error' ;
import { User } from 'lib/server-impl' ;
import { PushVariantsSchema } from 'lib/openapi/spec/push-variants-schema' ;
2021-11-24 13:08:04 +01:00
const PREFIX = '/:projectId/features/:featureName/variants' ;
2022-11-21 10:37:16 +01:00
const ENV_PREFIX =
'/:projectId/features/:featureName/environments/:environment/variants' ;
interface FeatureEnvironmentParams extends FeatureParams {
environment : string ;
}
2021-11-24 13:08:04 +01:00
interface FeatureParams extends ProjectParam {
featureName : string ;
}
interface ProjectParam {
projectId : string ;
}
export default class VariantsController extends Controller {
private logger : Logger ;
private featureService : FeatureToggleService ;
2023-01-20 10:30:20 +01:00
private accessService : AccessService ;
2021-11-24 13:08:04 +01:00
constructor (
config : IUnleashConfig ,
{
featureToggleService ,
2022-06-03 13:16:59 +02:00
openApiService ,
2023-01-20 10:30:20 +01:00
accessService ,
} : Pick <
IUnleashServices ,
'featureToggleService' | 'openApiService' | 'accessService'
> ,
2021-11-24 13:08:04 +01:00
) {
super ( config ) ;
this . logger = config . getLogger ( 'admin-api/project/variants.ts' ) ;
this . featureService = featureToggleService ;
2023-01-20 10:30:20 +01:00
this . accessService = accessService ;
2022-06-03 13:16:59 +02:00
this . route ( {
method : 'get' ,
path : PREFIX ,
permission : NONE ,
handler : this.getVariants ,
middleware : [
openApiService . validPath ( {
2023-02-24 15:25:03 +01:00
summary : 'Retrieve variants for a feature (deprecated) ' ,
description :
'(deprecated from 4.21) Retrieve the variants for the specified feature. From Unleash 4.21 onwards, this endpoint will attempt to choose a [production-type environment](https://docs.getunleash.io/reference/environments) as the source of truth. If more than one production environment is found, the first one will be used.' ,
deprecated : true ,
2022-08-12 11:37:57 +02:00
tags : [ 'Features' ] ,
2022-06-03 13:16:59 +02:00
operationId : 'getFeatureVariants' ,
2022-06-08 08:01:14 +02:00
responses : {
200 : createResponseSchema ( 'featureVariantsSchema' ) ,
} ,
2022-06-03 13:16:59 +02:00
} ) ,
] ,
} ) ;
this . route ( {
method : 'patch' ,
path : PREFIX ,
permission : UPDATE_FEATURE_VARIANTS ,
handler : this.patchVariants ,
middleware : [
openApiService . validPath ( {
2023-02-24 15:25:03 +01:00
summary :
"Apply a patch to a feature's variants (in all environments)." ,
description : ` Apply a list of patches patch to the specified feature's variants. The patch objects should conform to the [JSON-patch format (RFC 6902)](https://www.rfc-editor.org/rfc/rfc6902).
⚠ ️ * * Warning * * : This method is not atomic . If something fails in the middle of applying the patch , you can be left with a half - applied patch . We recommend that you instead [ patch variants on a per - environment basis ] ( / d o c s / r e f e r e n c e / a p i / u n l e a s h / p a t c h - e n v i r o n m e n t s - f e a t u r e - v a r i a n t s . a p i . m d x ) , w h i c h * * i s * * a n a t o m i c o p e r a t i o n . ` ,
2022-08-12 11:37:57 +02:00
tags : [ 'Features' ] ,
2022-06-03 13:16:59 +02:00
operationId : 'patchFeatureVariants' ,
2022-06-08 08:01:14 +02:00
requestBody : createRequestSchema ( 'patchesSchema' ) ,
responses : {
200 : createResponseSchema ( 'featureVariantsSchema' ) ,
} ,
2022-06-03 13:16:59 +02:00
} ) ,
] ,
} ) ;
this . route ( {
method : 'put' ,
path : PREFIX ,
permission : UPDATE_FEATURE_VARIANTS ,
handler : this.overwriteVariants ,
middleware : [
openApiService . validPath ( {
2023-02-24 15:25:03 +01:00
summary :
'Create (overwrite) variants for a feature toggle in all environments' ,
description : ` This overwrites the current variants for the feature specified in the :featureName parameter in all environments.
The backend will validate the input for the following invariants
* If there are variants , there needs to be at least one variant with \ ` weightType: variable \`
* The sum of the weights of variants with \ ` weightType: fix \` must be strictly less than 1000 (< 1000)
The backend will also distribute remaining weight up to 1000 after adding the variants with \ ` weightType: fix \` together amongst the variants of \` weightType: variable \` ` ,
2022-08-12 11:37:57 +02:00
tags : [ 'Features' ] ,
2022-06-03 13:16:59 +02:00
operationId : 'overwriteFeatureVariants' ,
2022-06-08 08:01:14 +02:00
requestBody : createRequestSchema ( 'variantsSchema' ) ,
responses : {
200 : createResponseSchema ( 'featureVariantsSchema' ) ,
} ,
2022-06-03 13:16:59 +02:00
} ) ,
] ,
} ) ;
2022-11-21 10:37:16 +01:00
this . route ( {
method : 'get' ,
path : ENV_PREFIX ,
permission : NONE ,
handler : this.getVariantsOnEnv ,
middleware : [
openApiService . validPath ( {
2023-02-24 15:25:03 +01:00
summary : 'Get variants for a feature in an environment' ,
description : ` Returns the variants for a feature in a specific environment. If the feature has no variants it will return an empty array of variants ` ,
2022-11-21 10:37:16 +01:00
tags : [ 'Features' ] ,
operationId : 'getEnvironmentFeatureVariants' ,
responses : {
200 : createResponseSchema ( 'featureVariantsSchema' ) ,
} ,
} ) ,
] ,
} ) ;
this . route ( {
method : 'patch' ,
path : ENV_PREFIX ,
2022-11-22 10:54:04 +01:00
permission : UPDATE_FEATURE_ENVIRONMENT_VARIANTS ,
2022-11-21 10:37:16 +01:00
handler : this.patchVariantsOnEnv ,
middleware : [
openApiService . validPath ( {
2023-02-24 15:25:03 +01:00
summary : "Patch a feature's variants in an environment" ,
description : ` Apply a list of patches to the features environments in the specified environment. The patch objects should conform to the [JSON-patch format (RFC 6902)](https://www.rfc-editor.org/rfc/rfc6902). ` ,
2022-11-21 10:37:16 +01:00
tags : [ 'Features' ] ,
operationId : 'patchEnvironmentsFeatureVariants' ,
requestBody : createRequestSchema ( 'patchesSchema' ) ,
responses : {
200 : createResponseSchema ( 'featureVariantsSchema' ) ,
} ,
} ) ,
] ,
} ) ;
this . route ( {
method : 'put' ,
path : ENV_PREFIX ,
2022-11-22 10:54:04 +01:00
permission : UPDATE_FEATURE_ENVIRONMENT_VARIANTS ,
2022-11-21 10:37:16 +01:00
handler : this.overwriteVariantsOnEnv ,
middleware : [
openApiService . validPath ( {
2023-02-24 15:25:03 +01:00
summary :
'Create (overwrite) variants for a feature in an environment' ,
description : ` This overwrites the current variants for the feature toggle in the :featureName parameter for the :environment parameter.
The backend will validate the input for the following invariants :
* If there are variants , there needs to be at least one variant with \ ` weightType: variable \`
* The sum of the weights of variants with \ ` weightType: fix \` must be strictly less than 1000 (< 1000)
The backend will also distribute remaining weight up to 1000 after adding the variants with \ ` weightType: fix \` together amongst the variants of \` weightType: variable \` ` ,
2022-11-21 10:37:16 +01:00
tags : [ 'Features' ] ,
operationId : 'overwriteEnvironmentFeatureVariants' ,
requestBody : createRequestSchema ( 'variantsSchema' ) ,
responses : {
200 : createResponseSchema ( 'featureVariantsSchema' ) ,
} ,
} ) ,
] ,
} ) ;
2023-01-20 10:30:20 +01:00
this . route ( {
method : 'put' ,
path : ` ${ PREFIX } -batch ` ,
permission : NONE ,
handler : this.pushVariantsToEnvironments ,
middleware : [
openApiService . validPath ( {
tags : [ 'Features' ] ,
operationId : 'overwriteFeatureVariantsOnEnvironments' ,
requestBody : createRequestSchema ( 'pushVariantsSchema' ) ,
responses : {
200 : createResponseSchema ( 'featureVariantsSchema' ) ,
} ,
} ) ,
] ,
} ) ;
2021-11-24 13:08:04 +01:00
}
2022-12-06 10:47:54 +01:00
/ * *
* @deprecated - Variants should be fetched from featureService . getVariantsForEnv ( since variants are now ; since 4.18 , connected to environments )
* @param req
* @param res
* /
2021-11-24 13:08:04 +01:00
async getVariants (
req : Request < FeatureParams , any , any , any > ,
2022-06-03 13:16:59 +02:00
res : Response < FeatureVariantsSchema > ,
2021-11-24 13:08:04 +01:00
) : Promise < void > {
const { featureName } = req . params ;
const variants = await this . featureService . getVariants ( featureName ) ;
2022-06-08 08:01:14 +02:00
res . status ( 200 ) . json ( { version : 1 , variants : variants || [ ] } ) ;
2021-11-24 13:08:04 +01:00
}
async patchVariants (
2021-12-16 11:07:19 +01:00
req : IAuthRequest < FeatureParams , any , Operation [ ] > ,
2022-06-03 13:16:59 +02:00
res : Response < FeatureVariantsSchema > ,
2021-11-24 13:08:04 +01:00
) : Promise < void > {
2021-12-16 11:07:19 +01:00
const { projectId , featureName } = req . params ;
2021-11-24 13:08:04 +01:00
const updatedFeature = await this . featureService . updateVariants (
featureName ,
2021-12-16 11:07:19 +01:00
projectId ,
2021-11-24 13:08:04 +01:00
req . body ,
2023-01-24 10:43:10 +01:00
req . user ,
2021-11-24 13:08:04 +01:00
) ;
res . status ( 200 ) . json ( {
2022-06-08 08:01:14 +02:00
version : 1 ,
2021-11-24 13:08:04 +01:00
variants : updatedFeature.variants ,
} ) ;
}
async overwriteVariants (
2021-12-16 11:07:19 +01:00
req : IAuthRequest < FeatureParams , any , IVariant [ ] , any > ,
2022-06-03 13:16:59 +02:00
res : Response < FeatureVariantsSchema > ,
2021-11-24 13:08:04 +01:00
) : Promise < void > {
2021-12-16 11:07:19 +01:00
const { projectId , featureName } = req . params ;
const userName = extractUsername ( req ) ;
2021-11-24 13:08:04 +01:00
const updatedFeature = await this . featureService . saveVariants (
featureName ,
2021-12-16 11:07:19 +01:00
projectId ,
2021-11-24 13:08:04 +01:00
req . body ,
2021-12-16 11:07:19 +01:00
userName ,
2021-11-24 13:08:04 +01:00
) ;
res . status ( 200 ) . json ( {
2022-06-08 08:01:14 +02:00
version : 1 ,
2021-11-24 13:08:04 +01:00
variants : updatedFeature.variants ,
} ) ;
}
2022-11-21 10:37:16 +01:00
2023-01-20 10:30:20 +01:00
async pushVariantsToEnvironments (
req : IAuthRequest <
FeatureEnvironmentParams ,
any ,
PushVariantsSchema ,
any
> ,
res : Response < FeatureVariantsSchema > ,
) : Promise < void > {
const { projectId , featureName } = req . params ;
const { environments , variants } = req . body ;
if ( environments === undefined || environments . length === 0 ) {
throw new BadDataError ( 'No environments provided' ) ;
}
await this . checkAccess (
req . user ,
projectId ,
environments ,
UPDATE_FEATURE_ENVIRONMENT_VARIANTS ,
) ;
const variantsWithDefaults = variants . map ( ( variant ) = > ( {
weightType : WeightType.VARIABLE ,
stickiness : 'default' ,
. . . variant ,
} ) ) ;
2023-01-24 10:43:10 +01:00
await this . featureService . crProtectedSetVariantsOnEnvs (
2023-01-20 10:30:20 +01:00
projectId ,
featureName ,
environments ,
variantsWithDefaults ,
2023-01-24 10:43:10 +01:00
req . user ,
2023-01-20 10:30:20 +01:00
) ;
res . status ( 200 ) . json ( {
version : 1 ,
variants : variantsWithDefaults ,
} ) ;
}
async checkAccess (
user : User ,
projectId : string ,
environments : string [ ] ,
permission : string ,
) : Promise < void > {
for ( const environment of environments ) {
if (
! ( await this . accessService . hasPermission (
user ,
permission ,
projectId ,
environment ,
) )
) {
throw new NoAccessError (
UPDATE_FEATURE_ENVIRONMENT_VARIANTS ,
environment ,
) ;
}
}
}
2022-11-21 10:37:16 +01:00
async getVariantsOnEnv (
req : Request < FeatureEnvironmentParams , any , any , any > ,
res : Response < FeatureVariantsSchema > ,
) : Promise < void > {
const { featureName , environment } = req . params ;
const variants = await this . featureService . getVariantsForEnv (
featureName ,
environment ,
) ;
res . status ( 200 ) . json ( { version : 1 , variants : variants || [ ] } ) ;
}
async patchVariantsOnEnv (
req : IAuthRequest < FeatureEnvironmentParams , any , Operation [ ] > ,
res : Response < FeatureVariantsSchema > ,
) : Promise < void > {
const { projectId , featureName , environment } = req . params ;
const variants = await this . featureService . updateVariantsOnEnv (
featureName ,
projectId ,
environment ,
req . body ,
2023-01-24 10:43:10 +01:00
req . user ,
2022-11-21 10:37:16 +01:00
) ;
res . status ( 200 ) . json ( {
version : 1 ,
variants ,
} ) ;
}
async overwriteVariantsOnEnv (
req : IAuthRequest < FeatureEnvironmentParams , any , IVariant [ ] , any > ,
res : Response < FeatureVariantsSchema > ,
) : Promise < void > {
2022-11-22 09:57:12 +01:00
const { featureName , environment , projectId } = req . params ;
2023-01-24 10:43:10 +01:00
const variants = await this . featureService . crProtectedSaveVariantsOnEnv (
2022-11-22 09:57:12 +01:00
projectId ,
2022-11-21 10:37:16 +01:00
featureName ,
environment ,
req . body ,
2023-01-24 10:43:10 +01:00
req . user ,
2022-11-21 10:37:16 +01:00
) ;
res . status ( 200 ) . json ( {
version : 1 ,
variants : variants ,
} ) ;
}
2021-11-24 13:08:04 +01:00
}