2021-07-07 10:46:50 +02:00
import {
2023-04-21 11:09:07 +02:00
CREATE_FEATURE_STRATEGY ,
2023-04-18 08:59:02 +02:00
EnvironmentVariantEvent ,
2021-12-16 11:07:19 +01:00
FEATURE_UPDATED ,
2021-11-12 13:15:51 +01:00
FeatureArchivedEvent ,
FeatureChangeProjectEvent ,
FeatureCreatedEvent ,
FeatureDeletedEvent ,
FeatureEnvironmentEvent ,
FeatureMetadataUpdateEvent ,
FeatureRevivedEvent ,
FeatureStaleEvent ,
FeatureStrategyAddEvent ,
FeatureStrategyRemoveEvent ,
FeatureStrategyUpdateEvent ,
2023-04-21 11:09:07 +02:00
FeatureToggle ,
FeatureToggleDTO ,
FeatureToggleLegacy ,
FeatureToggleWithEnvironment ,
2021-12-16 11:07:19 +01:00
FeatureVariantEvent ,
2023-04-21 11:09:07 +02:00
IConstraint ,
2023-04-18 08:59:02 +02:00
IEventStore ,
2023-04-21 11:09:07 +02:00
IFeatureEnvironmentInfo ,
IFeatureEnvironmentStore ,
IFeatureOverview ,
IFeatureStrategy ,
2023-04-18 08:59:02 +02:00
IFeatureTagStore ,
2023-04-21 11:09:07 +02:00
IFeatureToggleClientStore ,
IFeatureToggleQuery ,
2023-04-18 08:59:02 +02:00
IFeatureToggleStore ,
IFlagResolver ,
IProjectStore ,
2023-04-21 11:09:07 +02:00
ISegment ,
IStrategyConfig ,
2023-04-18 08:59:02 +02:00
IUnleashConfig ,
IUnleashStores ,
2023-04-21 11:09:07 +02:00
IVariant ,
Saved ,
SKIP_CHANGE_REQUEST ,
Unsaved ,
WeightType ,
2023-04-18 08:59:02 +02:00
} from '../types' ;
import { Logger } from '../logger' ;
import BadDataError from '../error/bad-data-error' ;
import NameExistsError from '../error/name-exists-error' ;
import InvalidOperationError from '../error/invalid-operation-error' ;
2023-04-21 11:09:07 +02:00
import { FOREIGN_KEY_VIOLATION , OperationDeniedError } from '../error' ;
2023-04-18 08:59:02 +02:00
import {
constraintSchema ,
featureMetadataSchema ,
nameSchema ,
variantsArraySchema ,
} from '../schema/feature-schema' ;
2021-07-07 10:46:50 +02:00
import NotFoundError from '../error/notfound-error' ;
2021-08-25 13:38:00 +02:00
import {
FeatureConfigurationClient ,
IFeatureStrategiesStore ,
} from '../types/stores/feature-strategies-store' ;
2022-03-04 17:29:42 +01:00
import {
DATE_OPERATORS ,
DEFAULT_ENV ,
NUM_OPERATORS ,
SEMVER_OPERATORS ,
STRING_OPERATORS ,
2023-04-21 11:09:07 +02:00
} from '../util' ;
2021-10-11 11:27:20 +02:00
import { applyPatch , deepClone , Operation } from 'fast-json-patch' ;
2022-03-04 17:29:42 +01:00
import {
validateDate ,
validateLegalValues ,
validateNumber ,
validateSemver ,
validateString ,
} from '../util/validators/constraint-types' ;
import { IContextFieldStore } from 'lib/types/stores/context-field-store' ;
2022-07-26 14:16:30 +02:00
import { SetStrategySortOrderSchema } from 'lib/openapi/spec/set-strategy-sort-order-schema' ;
2023-05-05 13:32:44 +02:00
import {
getDefaultStrategy ,
getProjectDefaultStrategy ,
} from '../util/feature-evaluator/helpers' ;
2022-10-05 23:33:36 +02:00
import { AccessService } from './access-service' ;
import { User } from '../server-impl' ;
import NoAccessError from '../error/no-access-error' ;
2023-03-08 09:07:06 +01:00
import { IFeatureProjectUserParams } from '../routes/admin-api/project/project-features' ;
2023-03-14 09:48:29 +01:00
import { unique } from '../util/unique' ;
2023-03-15 14:58:19 +01:00
import { ISegmentService } from 'lib/segments/segment-service-interface' ;
2023-03-24 14:31:43 +01:00
import { IChangeRequestAccessReadModel } from '../features/change-request-access-service/change-request-access-read-model' ;
2021-07-07 10:46:50 +02:00
2021-11-04 21:24:55 +01:00
interface IFeatureContext {
featureName : string ;
projectId : string ;
}
interface IFeatureStrategyContext extends IFeatureContext {
environment : string ;
}
2022-11-29 16:06:08 +01:00
export interface IGetFeatureParams {
featureName : string ;
archived? : boolean ;
projectId? : string ;
environmentVariants? : boolean ;
userId? : number ;
}
2022-03-04 17:29:42 +01:00
const oneOf = ( values : string [ ] , match : string ) = > {
return values . some ( ( value ) = > value === match ) ;
} ;
2021-11-12 13:15:51 +01:00
class FeatureToggleService {
2021-07-07 10:46:50 +02:00
private logger : Logger ;
2021-08-12 15:04:37 +02:00
private featureStrategiesStore : IFeatureStrategiesStore ;
private featureToggleStore : IFeatureToggleStore ;
2021-07-07 10:46:50 +02:00
2021-09-13 10:23:57 +02:00
private featureToggleClientStore : IFeatureToggleClientStore ;
2021-11-12 13:15:51 +01:00
private tagStore : IFeatureTagStore ;
2021-07-07 10:46:50 +02:00
2021-08-12 15:04:37 +02:00
private featureEnvironmentStore : IFeatureEnvironmentStore ;
2021-07-07 10:46:50 +02:00
2021-08-12 15:04:37 +02:00
private projectStore : IProjectStore ;
2021-07-07 10:46:50 +02:00
2021-08-12 15:04:37 +02:00
private eventStore : IEventStore ;
2021-07-07 10:46:50 +02:00
2022-03-04 17:29:42 +01:00
private contextFieldStore : IContextFieldStore ;
2023-03-15 14:58:19 +01:00
private segmentService : ISegmentService ;
2022-06-08 15:41:02 +02:00
2022-10-05 23:33:36 +02:00
private accessService : AccessService ;
2023-01-24 10:43:10 +01:00
private flagResolver : IFlagResolver ;
2023-03-24 14:31:43 +01:00
private changeRequestAccessReadModel : IChangeRequestAccessReadModel ;
2021-07-07 10:46:50 +02:00
constructor (
{
featureStrategiesStore ,
featureToggleStore ,
2021-09-13 10:23:57 +02:00
featureToggleClientStore ,
2021-07-07 10:46:50 +02:00
projectStore ,
eventStore ,
featureTagStore ,
2021-08-12 15:04:37 +02:00
featureEnvironmentStore ,
2022-03-04 17:29:42 +01:00
contextFieldStore ,
2021-07-07 10:46:50 +02:00
} : Pick <
2021-08-25 13:38:00 +02:00
IUnleashStores ,
| 'featureStrategiesStore'
| 'featureToggleStore'
2021-09-13 10:23:57 +02:00
| 'featureToggleClientStore'
2021-08-25 13:38:00 +02:00
| 'projectStore'
| 'eventStore'
| 'featureTagStore'
| 'featureEnvironmentStore'
2022-03-04 17:29:42 +01:00
| 'contextFieldStore'
2021-07-07 10:46:50 +02:00
> ,
2023-01-24 10:43:10 +01:00
{
getLogger ,
flagResolver ,
} : Pick < IUnleashConfig , ' getLogger ' | ' flagResolver ' > ,
2023-03-15 14:58:19 +01:00
segmentService : ISegmentService ,
2022-10-05 23:33:36 +02:00
accessService : AccessService ,
2023-03-24 14:31:43 +01:00
changeRequestAccessReadModel : IChangeRequestAccessReadModel ,
2021-07-07 10:46:50 +02:00
) {
2022-01-06 10:23:52 +01:00
this . logger = getLogger ( 'services/feature-toggle-service.ts' ) ;
2021-07-07 10:46:50 +02:00
this . featureStrategiesStore = featureStrategiesStore ;
this . featureToggleStore = featureToggleStore ;
2021-09-13 10:23:57 +02:00
this . featureToggleClientStore = featureToggleClientStore ;
2021-11-12 13:15:51 +01:00
this . tagStore = featureTagStore ;
2021-07-07 10:46:50 +02:00
this . projectStore = projectStore ;
this . eventStore = eventStore ;
2021-08-12 15:04:37 +02:00
this . featureEnvironmentStore = featureEnvironmentStore ;
2022-03-04 17:29:42 +01:00
this . contextFieldStore = contextFieldStore ;
2022-06-08 15:41:02 +02:00
this . segmentService = segmentService ;
2022-10-05 23:33:36 +02:00
this . accessService = accessService ;
2023-01-24 10:43:10 +01:00
this . flagResolver = flagResolver ;
2023-03-24 14:31:43 +01:00
this . changeRequestAccessReadModel = changeRequestAccessReadModel ;
2021-07-07 10:46:50 +02:00
}
2023-03-14 09:48:29 +01:00
async validateFeaturesContext (
featureNames : string [ ] ,
projectId : string ,
) : Promise < void > {
const features = await this . featureToggleStore . getAllByNames (
featureNames ,
) ;
const invalidProjects = unique (
features
. map ( ( feature ) = > feature . project )
. filter ( ( project ) = > project !== projectId ) ,
) ;
if ( invalidProjects . length > 0 ) {
throw new InvalidOperationError (
` The operation could not be completed. The features exist, but the provided project ids (" ${ invalidProjects . join (
',' ,
) } ") does not match the project provided in request URL (" $ { projectId } " ) . ` ,
) ;
}
}
2023-03-17 12:43:34 +01:00
async validateFeatureBelongsToProject ( {
2021-11-04 21:24:55 +01:00
featureName ,
projectId ,
} : IFeatureContext ) : Promise < void > {
const id = await this . featureToggleStore . getProjectId ( featureName ) ;
2022-12-02 13:10:39 +01:00
2021-11-04 21:24:55 +01:00
if ( id !== projectId ) {
throw new InvalidOperationError (
Fix: validate that the project is correct when getting feature by project (#2344)
## What
This PR fixes a bug where fetching a feature toggle via the
`/api/admin/projects/:projectId/features/:featureName` endpoint doesn't
validate that the feature belongs to the provided project. The same
thing applies to the archive functionality. This has also been fixed.
In doing so, it also adds corresponding tests to check for edge cases,
updates the 403 error response we use to provide clearer steps for the
user, and adds more error responses to the OpenAPI documentation.
## Why
As mentioned in #2337, it's unexpected that the provided project
shouldn't matter at all, and after discussions internally, it was also
discovered that this was never intended to be the case.
## Discussion points
It might be worth rethinking this for Unleash v5. Why does the features
API need the projects part at all when features are unique across the
entire instance? Would it be worth reverting to a simpler feature API
later or would that introduce issues with regards to how different
projects can have different active environments and so on?
### Further improvements
I have _not_ provided schemas for the error responses for the endpoints
at this time. I considered it, but because it would introduce new schema
code, more tests, etc, I decided to leave it for later. There's a
thorough OpenAPI walkthrough coming up, so I think it makes sense to do
it as part of that work instead. I am happy to be challenged on this,
however, and will implement it if you think it's better.
### Why 403 when the project is wrong?
We could also have used the 404 status code for when the feature exists
but doesn't belong to this project, but this would require more (and
more complex) code. We also already use 403 for cases like this for
post, patch, and put. Finally, the [HTTP spec's section on the 403
status code](https://httpwg.org/specs/rfc9110.html#status.403) says the
following (emphasis mine):
> The 403 (Forbidden) status code indicates that the server
**_understood the request but refuses to fulfill it_**. A server that
wishes to make public why the request has been forbidden can describe
that reason in the response content (if any).
>
> If authentication credentials were provided in the request, the server
considers them insufficient to grant access. The client SHOULD NOT
automatically repeat the request with the same credentials. The client
MAY repeat the request with new or different credentials. However, **_a
request might be forbidden for reasons unrelated to the credentials_**.
As such, I think using 403 makes sense in this case.
---
Closes #2337.
2022-11-08 13:34:01 +01:00
` The operation could not be completed. The feature exists, but the provided project id (" ${ projectId } ") does not match the project that the feature belongs to (" ${ id } "). Try using " ${ id } " in the request URL instead of " ${ projectId } ". ` ,
2021-11-04 21:24:55 +01:00
) ;
}
}
2023-03-17 12:43:34 +01:00
validateUpdatedProperties (
{ featureName , projectId } : IFeatureContext ,
2021-11-04 21:24:55 +01:00
strategy : IFeatureStrategy ,
) : void {
if ( strategy . projectId !== projectId ) {
throw new InvalidOperationError (
'You can not change the projectId for an activation strategy.' ,
) ;
}
if ( strategy . featureName !== featureName ) {
throw new InvalidOperationError (
'You can not change the featureName for an activation strategy.' ,
) ;
}
2023-04-25 11:57:16 +02:00
if (
strategy . parameters &&
'stickiness' in strategy . parameters &&
strategy . parameters . stickiness === ''
) {
throw new InvalidOperationError (
'You can not have an empty string for stickiness.' ,
) ;
}
2021-11-04 21:24:55 +01:00
}
2023-03-17 12:43:34 +01:00
async validateProjectCanAccessSegments (
projectId : string ,
segmentIds? : number [ ] ,
) : Promise < void > {
if ( segmentIds && segmentIds . length > 0 ) {
await Promise . all (
segmentIds . map ( ( segmentId ) = >
this . segmentService . get ( segmentId ) ,
) ,
) . then ( ( segments ) = >
segments . map ( ( segment ) = > {
if ( segment . project && segment . project !== projectId ) {
throw new BadDataError (
` The segment " ${ segment . name } " with id ${ segment . id } does not belong to project " ${ projectId } ". ` ,
) ;
}
} ) ,
) ;
}
}
2022-03-17 12:32:04 +01:00
async validateConstraints (
constraints : IConstraint [ ] ,
) : Promise < IConstraint [ ] > {
2022-03-10 14:43:53 +01:00
const validations = constraints . map ( ( constraint ) = > {
return this . validateConstraint ( constraint ) ;
} ) ;
2022-03-17 12:32:04 +01:00
return Promise . all ( validations ) ;
2022-03-10 14:43:53 +01:00
}
2022-03-17 12:32:04 +01:00
async validateConstraint ( input : IConstraint ) : Promise < IConstraint > {
const constraint = await constraintSchema . validateAsync ( input ) ;
2022-03-04 17:29:42 +01:00
const { operator } = constraint ;
const contextDefinition = await this . contextFieldStore . get (
constraint . contextName ,
) ;
if ( oneOf ( NUM_OPERATORS , operator ) ) {
await validateNumber ( constraint . value ) ;
}
if ( oneOf ( STRING_OPERATORS , operator ) ) {
await validateString ( constraint . values ) ;
}
if ( oneOf ( SEMVER_OPERATORS , operator ) ) {
// Semver library is not asynchronous, so we do not
// need to await here.
validateSemver ( constraint . value ) ;
}
if ( oneOf ( DATE_OPERATORS , operator ) ) {
2022-04-01 11:10:21 +02:00
await validateDate ( constraint . value ) ;
2022-03-04 17:29:42 +01:00
}
if (
2023-03-24 10:43:38 +01:00
contextDefinition &&
contextDefinition . legalValues &&
contextDefinition . legalValues . length > 0
) {
const valuesToValidate = oneOf (
2022-03-04 17:29:42 +01:00
[ . . . DATE_OPERATORS , . . . SEMVER_OPERATORS , . . . NUM_OPERATORS ] ,
operator ,
)
2023-03-24 10:43:38 +01:00
? constraint . value
: constraint . values ;
validateLegalValues (
contextDefinition . legalValues ,
valuesToValidate ,
) ;
2022-03-04 17:29:42 +01:00
}
2022-03-17 12:32:04 +01:00
return constraint ;
2022-03-04 17:29:42 +01:00
}
2021-10-11 11:27:20 +02:00
async patchFeature (
2021-11-12 13:15:51 +01:00
project : string ,
2021-10-11 11:27:20 +02:00
featureName : string ,
2021-11-12 13:15:51 +01:00
createdBy : string ,
2021-10-11 11:27:20 +02:00
operations : Operation [ ] ,
) : Promise < FeatureToggle > {
const featureToggle = await this . getFeatureMetadata ( featureName ) ;
2021-11-25 14:53:58 +01:00
if ( operations . some ( ( op ) = > op . path . indexOf ( '/variants' ) >= 0 ) ) {
throw new OperationDeniedError (
` Changing variants is done via PATCH operation to /api/admin/projects/:project/features/:feature/variants ` ,
) ;
}
2021-10-11 11:27:20 +02:00
const { newDocument } = applyPatch (
deepClone ( featureToggle ) ,
operations ,
) ;
2021-11-12 13:15:51 +01:00
2021-10-11 11:27:20 +02:00
const updated = await this . updateFeatureToggle (
2021-11-12 13:15:51 +01:00
project ,
2021-10-11 11:27:20 +02:00
newDocument ,
2021-11-12 13:15:51 +01:00
createdBy ,
2022-01-21 12:02:05 +01:00
featureName ,
2021-10-11 11:27:20 +02:00
) ;
2021-11-12 13:15:51 +01:00
2021-10-11 11:27:20 +02:00
if ( featureToggle . stale !== newDocument . stale ) {
2021-11-12 13:15:51 +01:00
const tags = await this . tagStore . getAllTagsForFeature ( featureName ) ;
await this . eventStore . store (
new FeatureStaleEvent ( {
stale : newDocument.stale ,
project ,
featureName ,
createdBy ,
tags ,
} ) ,
) ;
2021-10-11 11:27:20 +02:00
}
2021-11-12 13:15:51 +01:00
2021-10-11 11:27:20 +02:00
return updated ;
}
2021-11-12 13:15:51 +01:00
featureStrategyToPublic (
featureStrategy : IFeatureStrategy ,
2022-10-10 14:32:34 +02:00
segments : ISegment [ ] = [ ] ,
2022-05-04 15:16:18 +02:00
) : Saved < IStrategyConfig > {
2021-11-12 13:15:51 +01:00
return {
id : featureStrategy.id ,
name : featureStrategy.strategyName ,
2023-04-18 08:59:02 +02:00
title : featureStrategy.title ,
2023-04-21 11:09:07 +02:00
disabled : featureStrategy.disabled ,
2021-11-12 13:15:51 +01:00
constraints : featureStrategy.constraints || [ ] ,
parameters : featureStrategy.parameters ,
2022-10-10 14:32:34 +02:00
segments : segments.map ( ( segment ) = > segment . id ) ? ? [ ] ,
2021-11-12 13:15:51 +01:00
} ;
}
2022-07-26 14:16:30 +02:00
async updateStrategiesSortOrder (
featureName : string ,
sortOrders : SetStrategySortOrderSchema ,
) : Promise < Saved < any > > {
await Promise . all (
2022-07-28 17:31:41 +02:00
sortOrders . map ( async ( { id , sortOrder } ) = >
this . featureStrategiesStore . updateSortOrder ( id , sortOrder ) ,
) ,
2022-07-26 14:16:30 +02:00
) ;
}
2021-07-07 10:46:50 +02:00
async createStrategy (
2022-05-04 15:16:18 +02:00
strategyConfig : Unsaved < IStrategyConfig > ,
2021-11-04 21:24:55 +01:00
context : IFeatureStrategyContext ,
2021-11-12 13:15:51 +01:00
createdBy : string ,
2022-12-05 15:38:17 +01:00
user? : User ,
2022-11-18 09:29:26 +01:00
) : Promise < Saved < IStrategyConfig > > {
await this . stopWhenChangeRequestsEnabled (
context . projectId ,
context . environment ,
2022-12-05 15:38:17 +01:00
user ,
2022-11-18 09:29:26 +01:00
) ;
return this . unprotectedCreateStrategy (
strategyConfig ,
context ,
createdBy ,
) ;
}
async unprotectedCreateStrategy (
strategyConfig : Unsaved < IStrategyConfig > ,
context : IFeatureStrategyContext ,
createdBy : string ,
2022-05-04 15:16:18 +02:00
) : Promise < Saved < IStrategyConfig > > {
2021-11-04 21:24:55 +01:00
const { featureName , projectId , environment } = context ;
2023-03-17 12:43:34 +01:00
await this . validateFeatureBelongsToProject ( context ) ;
await this . validateProjectCanAccessSegments (
projectId ,
strategyConfig . segments ,
) ;
2022-03-10 14:43:53 +01:00
2023-03-15 14:58:19 +01:00
if (
strategyConfig . constraints &&
strategyConfig . constraints . length > 0
) {
2022-03-17 12:32:04 +01:00
strategyConfig . constraints = await this . validateConstraints (
strategyConfig . constraints ,
) ;
2022-03-10 14:43:53 +01:00
}
2023-04-25 11:57:16 +02:00
if (
strategyConfig . parameters &&
'stickiness' in strategyConfig . parameters &&
strategyConfig . parameters . stickiness === ''
) {
strategyConfig . parameters . stickiness = 'default' ;
}
2021-07-07 10:46:50 +02:00
try {
2021-08-25 13:38:00 +02:00
const newFeatureStrategy =
2021-09-13 10:23:57 +02:00
await this . featureStrategiesStore . createStrategyFeatureEnv ( {
2021-07-07 10:46:50 +02:00
strategyName : strategyConfig.name ,
2023-04-18 08:59:02 +02:00
title : strategyConfig.title ,
2023-04-21 11:09:07 +02:00
disabled : strategyConfig.disabled ,
2023-03-24 10:43:38 +01:00
constraints : strategyConfig.constraints || [ ] ,
parameters : strategyConfig.parameters || { } ,
2021-09-17 15:11:17 +02:00
sortOrder : strategyConfig.sortOrder ,
2021-09-13 10:23:57 +02:00
projectId ,
2021-07-07 10:46:50 +02:00
featureName ,
environment ,
2021-08-25 13:38:00 +02:00
} ) ;
2021-11-12 13:15:51 +01:00
2022-11-16 15:35:39 +01:00
if (
strategyConfig . segments &&
Array . isArray ( strategyConfig . segments )
) {
await this . segmentService . updateStrategySegments (
newFeatureStrategy . id ,
strategyConfig . segments ,
) ;
}
2021-11-12 13:15:51 +01:00
const tags = await this . tagStore . getAllTagsForFeature ( featureName ) ;
2022-10-10 14:32:34 +02:00
const segments = await this . segmentService . getByStrategy (
newFeatureStrategy . id ,
2023-03-24 10:43:38 +01:00
) ;
2022-10-10 14:32:34 +02:00
const strategy = this . featureStrategyToPublic (
newFeatureStrategy ,
segments ,
) ;
2021-11-12 13:15:51 +01:00
await this . eventStore . store (
new FeatureStrategyAddEvent ( {
project : projectId ,
featureName ,
createdBy ,
environment ,
data : strategy ,
tags ,
} ) ,
) ;
return strategy ;
2021-07-07 10:46:50 +02:00
} catch ( e ) {
if ( e . code === FOREIGN_KEY_VIOLATION ) {
throw new BadDataError (
'You have not added the current environment to the project' ,
) ;
}
throw e ;
}
}
2021-12-16 11:07:19 +01:00
2021-07-07 10:46:50 +02:00
/ * *
2021-09-13 10:23:57 +02:00
* PUT / api / admin / projects / : projectId / features / : featureName / strategies / : strategyId ?
2021-07-07 10:46:50 +02:00
* {
*
* }
* @param id
* @param updates
2021-11-04 21:24:55 +01:00
* @param context - Which context does this strategy live in ( projectId , featureName , environment )
* @param userName - Human readable id of the user performing the update
2023-04-21 11:09:07 +02:00
* @param user - Optional User object performing the action
2021-07-07 10:46:50 +02:00
* /
async updateStrategy (
id : string ,
updates : Partial < IFeatureStrategy > ,
2021-11-04 21:24:55 +01:00
context : IFeatureStrategyContext ,
userName : string ,
2022-12-05 15:38:17 +01:00
user? : User ,
2022-11-18 09:29:26 +01:00
) : Promise < Saved < IStrategyConfig > > {
await this . stopWhenChangeRequestsEnabled (
context . projectId ,
context . environment ,
2022-12-05 15:38:17 +01:00
user ,
2022-11-18 09:29:26 +01:00
) ;
return this . unprotectedUpdateStrategy ( id , updates , context , userName ) ;
}
async unprotectedUpdateStrategy (
id : string ,
updates : Partial < IFeatureStrategy > ,
context : IFeatureStrategyContext ,
userName : string ,
2022-05-04 15:16:18 +02:00
) : Promise < Saved < IStrategyConfig > > {
2021-11-12 13:15:51 +01:00
const { projectId , environment , featureName } = context ;
2021-09-13 10:23:57 +02:00
const existingStrategy = await this . featureStrategiesStore . get ( id ) ;
2023-03-17 12:43:34 +01:00
this . validateUpdatedProperties ( context , existingStrategy ) ;
await this . validateProjectCanAccessSegments (
projectId ,
updates . segments ,
) ;
2021-11-04 21:24:55 +01:00
2021-09-13 10:23:57 +02:00
if ( existingStrategy . id === id ) {
2023-03-15 14:58:19 +01:00
if ( updates . constraints && updates . constraints . length > 0 ) {
2022-03-17 12:32:04 +01:00
updates . constraints = await this . validateConstraints (
updates . constraints ,
) ;
}
2021-09-13 10:23:57 +02:00
const strategy = await this . featureStrategiesStore . updateStrategy (
id ,
updates ,
) ;
2021-11-12 13:15:51 +01:00
2022-11-16 15:35:39 +01:00
if ( updates . segments && Array . isArray ( updates . segments ) ) {
await this . segmentService . updateStrategySegments (
strategy . id ,
updates . segments ,
) ;
}
2022-10-10 14:32:34 +02:00
const segments = await this . segmentService . getByStrategy (
strategy . id ,
2023-03-24 10:43:38 +01:00
) ;
2022-10-10 14:32:34 +02:00
2021-11-12 13:15:51 +01:00
// Store event!
const tags = await this . tagStore . getAllTagsForFeature ( featureName ) ;
2022-10-10 14:32:34 +02:00
const data = this . featureStrategyToPublic ( strategy , segments ) ;
const preData = this . featureStrategyToPublic (
existingStrategy ,
segments ,
) ;
2021-11-12 13:15:51 +01:00
await this . eventStore . store (
new FeatureStrategyUpdateEvent ( {
project : projectId ,
featureName ,
environment ,
createdBy : userName ,
data ,
preData ,
tags ,
} ) ,
) ;
2021-09-20 12:13:38 +02:00
return data ;
2021-09-13 10:23:57 +02:00
}
throw new NotFoundError ( ` Could not find strategy with id ${ id } ` ) ;
}
async updateStrategyParameter (
id : string ,
name : string ,
value : string | number ,
2021-11-04 21:24:55 +01:00
context : IFeatureStrategyContext ,
2021-09-20 12:13:38 +02:00
userName : string ,
2022-06-23 08:10:20 +02:00
) : Promise < Saved < IStrategyConfig > > {
2021-11-12 13:15:51 +01:00
const { projectId , environment , featureName } = context ;
2021-11-04 21:24:55 +01:00
2021-09-13 10:23:57 +02:00
const existingStrategy = await this . featureStrategiesStore . get ( id ) ;
2023-03-17 12:43:34 +01:00
this . validateUpdatedProperties ( context , existingStrategy ) ;
2021-11-04 21:24:55 +01:00
2021-09-13 10:23:57 +02:00
if ( existingStrategy . id === id ) {
2022-05-04 15:16:18 +02:00
existingStrategy . parameters [ name ] = String ( value ) ;
2021-09-13 10:23:57 +02:00
const strategy = await this . featureStrategiesStore . updateStrategy (
id ,
existingStrategy ,
) ;
2021-11-12 13:15:51 +01:00
const tags = await this . tagStore . getAllTagsForFeature ( featureName ) ;
2022-10-10 14:32:34 +02:00
const segments = await this . segmentService . getByStrategy (
strategy . id ,
2023-03-24 10:43:38 +01:00
) ;
2022-10-10 14:32:34 +02:00
const data = this . featureStrategyToPublic ( strategy , segments ) ;
const preData = this . featureStrategyToPublic (
existingStrategy ,
segments ,
) ;
2021-11-12 13:15:51 +01:00
await this . eventStore . store (
new FeatureStrategyUpdateEvent ( {
featureName ,
project : projectId ,
environment ,
createdBy : userName ,
data ,
preData ,
tags ,
} ) ,
) ;
2021-09-20 12:13:38 +02:00
return data ;
2021-07-07 10:46:50 +02:00
}
throw new NotFoundError ( ` Could not find strategy with id ${ id } ` ) ;
}
2021-09-13 10:23:57 +02:00
/ * *
2021-10-07 10:22:20 +02:00
* DELETE / api / admin / projects / : projectId / features / : featureName / environments / : environmentName / strategies / : strategyId
2021-09-13 10:23:57 +02:00
* {
*
* }
2021-10-07 10:22:20 +02:00
* @param id - strategy id
2021-11-04 21:24:55 +01:00
* @param context - Which context does this strategy live in ( projectId , featureName , environment )
Complete open api schemas for project features controller (#1563)
* Completed OpenAPI Schemas for ProjectFeatures Controller
Completed OpenAPI Schemas for Feature Controller (tags)
* Completed OpenAPI Schemas for ProjectFeatures Controller
Completed OpenAPI Schemas for Feature Controller (tags)
* bug fix
* bug fix
* fix merge conflicts, some refactoring
* fix merge conflicts, some refactoring
* fix merge conflicts, some refactoring
* added emptyResponse, patch feature operation schemas and request
* added emptyResponse, patch feature operation schemas and request
* patch strategy
* patch strategy
* update strategy
* update strategy
* fix pr comment
* fix pr comments
* improvements
* added operationId to schema for better generation
* fix pr comment
* fix pr comment
* fix pr comment
* improvements to generated and dynamic types
* improvements to generated and dynamic types
* improvements to generated and dynamic types
* Update response types to use inferred types
* Update addTag response status to 201
* refactor: move schema ref destructuring into createSchemaObject
* made serialize date handle deep objects
* made serialize date handle deep objects
* add `name` to IFeatureStrategy nad fix tests
* fix pr comments
* fix pr comments
* Add types to IAuthRequest
* Sync StrategySchema for FE and BE - into the rabbit hole
* Sync model with OAS spec
* Completed OpenAPI Schemas for ProjectFeatures Controller
Completed OpenAPI Schemas for Feature Controller (tags)
* Completed OpenAPI Schemas for ProjectFeatures Controller
Completed OpenAPI Schemas for Feature Controller (tags)
* bug fix
* bug fix
* fix merge conflicts, some refactoring
* fix merge conflicts, some refactoring
* fix merge conflicts, some refactoring
* added emptyResponse, patch feature operation schemas and request
* added emptyResponse, patch feature operation schemas and request
* patch strategy
* patch strategy
* update strategy
* update strategy
* fix pr comment
* fix pr comments
* improvements
* added operationId to schema for better generation
* fix pr comment
* fix pr comment
* fix pr comment
* improvements to generated and dynamic types
* improvements to generated and dynamic types
* improvements to generated and dynamic types
* Update response types to use inferred types
* Update addTag response status to 201
* refactor: move schema ref destructuring into createSchemaObject
* made serialize date handle deep objects
* made serialize date handle deep objects
* add `name` to IFeatureStrategy nad fix tests
* fix pr comments
* fix pr comments
* Add types to IAuthRequest
* Sync StrategySchema for FE and BE - into the rabbit hole
* Sync model with OAS spec
* Completed OpenAPI Schemas for ProjectFeatures Controller
Completed OpenAPI Schemas for Feature Controller (tags)
* Completed OpenAPI Schemas for ProjectFeatures Controller
Completed OpenAPI Schemas for Feature Controller (tags)
* bug fix
* bug fix
* fix merge conflicts, some refactoring
* fix merge conflicts, some refactoring
* fix merge conflicts, some refactoring
* added emptyResponse, patch feature operation schemas and request
* added emptyResponse, patch feature operation schemas and request
* patch strategy
* patch strategy
* update strategy
* update strategy
* fix pr comment
* fix pr comments
* improvements
* added operationId to schema for better generation
* fix pr comment
* fix pr comment
* fix pr comment
* improvements to generated and dynamic types
* improvements to generated and dynamic types
* improvements to generated and dynamic types
* Update response types to use inferred types
* Update addTag response status to 201
* refactor: move schema ref destructuring into createSchemaObject
* made serialize date handle deep objects
* made serialize date handle deep objects
* add `name` to IFeatureStrategy nad fix tests
* fix pr comments
* fix pr comments
* Add types to IAuthRequest
* Sync StrategySchema for FE and BE - into the rabbit hole
* Sync model with OAS spec
* Completed OpenAPI Schemas for ProjectFeatures Controller
Completed OpenAPI Schemas for Feature Controller (tags)
* Completed OpenAPI Schemas for ProjectFeatures Controller
Completed OpenAPI Schemas for Feature Controller (tags)
* bug fix
* bug fix
* fix merge conflicts, some refactoring
* fix merge conflicts, some refactoring
* fix merge conflicts, some refactoring
* added emptyResponse, patch feature operation schemas and request
* added emptyResponse, patch feature operation schemas and request
* patch strategy
* patch strategy
* update strategy
* update strategy
* fix pr comment
* fix pr comments
* improvements
* added operationId to schema for better generation
* fix pr comment
* fix pr comment
* fix pr comment
* improvements to generated and dynamic types
* improvements to generated and dynamic types
* improvements to generated and dynamic types
* Update response types to use inferred types
* Update addTag response status to 201
* refactor: move schema ref destructuring into createSchemaObject
* made serialize date handle deep objects
* made serialize date handle deep objects
* add `name` to IFeatureStrategy nad fix tests
* fix pr comments
* fix pr comments
* Add types to IAuthRequest
* Sync StrategySchema for FE and BE - into the rabbit hole
* Sync model with OAS spec
* revert
* revert
* revert
* revert
* revert
* mapper
* revert
* revert
* revert
* remove serialize-dates.ts
* remove serialize-dates.ts
* remove serialize-dates.ts
* remove serialize-dates.ts
* remove serialize-dates.ts
* revert
* revert
* add mappers
* add mappers
* fix pr comments
* ignore report.json
* ignore report.json
* Route permission required
Co-authored-by: olav <mail@olav.io>
2022-05-18 15:17:09 +02:00
* @param createdBy - Which user does this strategy belong to
2023-04-18 08:59:02 +02:00
* @param user
2021-09-13 10:23:57 +02:00
* /
2021-09-20 12:13:38 +02:00
async deleteStrategy (
id : string ,
2021-11-04 21:24:55 +01:00
context : IFeatureStrategyContext ,
2021-11-12 13:15:51 +01:00
createdBy : string ,
2022-12-05 15:38:17 +01:00
user? : User ,
2022-11-18 09:29:26 +01:00
) : Promise < void > {
await this . stopWhenChangeRequestsEnabled (
context . projectId ,
context . environment ,
2022-12-05 15:38:17 +01:00
user ,
2022-11-18 09:29:26 +01:00
) ;
return this . unprotectedDeleteStrategy ( id , context , createdBy ) ;
}
async unprotectedDeleteStrategy (
id : string ,
context : IFeatureStrategyContext ,
createdBy : string ,
2021-09-20 12:13:38 +02:00
) : Promise < void > {
2021-11-04 21:24:55 +01:00
const existingStrategy = await this . featureStrategiesStore . get ( id ) ;
const { featureName , projectId , environment } = context ;
2023-03-17 12:43:34 +01:00
this . validateUpdatedProperties ( context , existingStrategy ) ;
2021-11-04 21:24:55 +01:00
2021-09-20 12:13:38 +02:00
await this . featureStrategiesStore . delete ( id ) ;
2021-11-12 13:15:51 +01:00
2023-05-08 10:42:26 +02:00
const featureStrategies =
await this . featureStrategiesStore . getStrategiesForFeatureEnv (
projectId ,
featureName ,
environment ,
) ;
const hasOnlyDisabledStrategies = featureStrategies . every (
( strategy ) = > strategy . disabled ,
) ;
if ( hasOnlyDisabledStrategies ) {
// Disable the feature in the environment if it only has disabled strategies
2023-05-09 13:18:21 +02:00
await this . unprotectedUpdateEnabled (
2023-05-08 10:42:26 +02:00
projectId ,
featureName ,
environment ,
false ,
createdBy ,
) ;
}
2021-11-12 13:15:51 +01:00
const tags = await this . tagStore . getAllTagsForFeature ( featureName ) ;
const preData = this . featureStrategyToPublic ( existingStrategy ) ;
await this . eventStore . store (
new FeatureStrategyRemoveEvent ( {
featureName ,
project : projectId ,
environment ,
createdBy ,
preData ,
tags ,
} ) ,
) ;
2021-11-04 21:24:55 +01:00
2021-10-06 09:25:34 +02:00
// If there are no strategies left for environment disable it
await this . featureEnvironmentStore . disableEnvironmentIfNoStrategies (
featureName ,
environment ,
) ;
2021-09-13 10:23:57 +02:00
}
2021-07-07 10:46:50 +02:00
async getStrategiesForEnvironment (
2021-09-13 10:23:57 +02:00
project : string ,
2021-07-07 10:46:50 +02:00
featureName : string ,
2021-09-24 13:55:00 +02:00
environment : string = DEFAULT_ENV ,
2022-06-23 08:10:20 +02:00
) : Promise < Saved < IStrategyConfig > [ ] > {
2022-10-10 14:32:34 +02:00
this . logger . debug ( 'getStrategiesForEnvironment' ) ;
2021-08-12 15:04:37 +02:00
const hasEnv = await this . featureEnvironmentStore . featureHasEnvironment (
2021-07-07 10:46:50 +02:00
environment ,
featureName ,
) ;
if ( hasEnv ) {
2021-08-25 13:38:00 +02:00
const featureStrategies =
2021-09-13 10:23:57 +02:00
await this . featureStrategiesStore . getStrategiesForFeatureEnv (
project ,
2021-08-25 13:38:00 +02:00
featureName ,
environment ,
) ;
2023-03-24 10:43:38 +01:00
const result : Saved < IStrategyConfig > [ ] = [ ] ;
2022-10-10 14:32:34 +02:00
for ( const strat of featureStrategies ) {
const segments =
( await this . segmentService . getByStrategy ( strat . id ) ) . map (
( segment ) = > segment . id ,
2023-03-24 10:43:38 +01:00
) ? ? [ ] ;
2022-10-10 14:32:34 +02:00
result . push ( {
id : strat.id ,
name : strat.strategyName ,
constraints : strat.constraints ,
parameters : strat.parameters ,
2023-04-21 11:09:07 +02:00
title : strat.title ,
disabled : strat.disabled ,
2022-10-10 14:32:34 +02:00
sortOrder : strat.sortOrder ,
segments ,
} ) ;
}
return result ;
2021-07-07 10:46:50 +02:00
}
throw new NotFoundError (
` Feature ${ featureName } does not have environment ${ environment } ` ,
) ;
}
/ * *
2021-09-13 10:23:57 +02:00
* GET / api / admin / projects / : project / features / : featureName
2021-07-07 10:46:50 +02:00
* @param featureName
* @param archived - return archived or non archived toggles
Fix: validate that the project is correct when getting feature by project (#2344)
## What
This PR fixes a bug where fetching a feature toggle via the
`/api/admin/projects/:projectId/features/:featureName` endpoint doesn't
validate that the feature belongs to the provided project. The same
thing applies to the archive functionality. This has also been fixed.
In doing so, it also adds corresponding tests to check for edge cases,
updates the 403 error response we use to provide clearer steps for the
user, and adds more error responses to the OpenAPI documentation.
## Why
As mentioned in #2337, it's unexpected that the provided project
shouldn't matter at all, and after discussions internally, it was also
discovered that this was never intended to be the case.
## Discussion points
It might be worth rethinking this for Unleash v5. Why does the features
API need the projects part at all when features are unique across the
entire instance? Would it be worth reverting to a simpler feature API
later or would that introduce issues with regards to how different
projects can have different active environments and so on?
### Further improvements
I have _not_ provided schemas for the error responses for the endpoints
at this time. I considered it, but because it would introduce new schema
code, more tests, etc, I decided to leave it for later. There's a
thorough OpenAPI walkthrough coming up, so I think it makes sense to do
it as part of that work instead. I am happy to be challenged on this,
however, and will implement it if you think it's better.
### Why 403 when the project is wrong?
We could also have used the 404 status code for when the feature exists
but doesn't belong to this project, but this would require more (and
more complex) code. We also already use 403 for cases like this for
post, patch, and put. Finally, the [HTTP spec's section on the 403
status code](https://httpwg.org/specs/rfc9110.html#status.403) says the
following (emphasis mine):
> The 403 (Forbidden) status code indicates that the server
**_understood the request but refuses to fulfill it_**. A server that
wishes to make public why the request has been forbidden can describe
that reason in the response content (if any).
>
> If authentication credentials were provided in the request, the server
considers them insufficient to grant access. The client SHOULD NOT
automatically repeat the request with the same credentials. The client
MAY repeat the request with new or different credentials. However, **_a
request might be forbidden for reasons unrelated to the credentials_**.
As such, I think using 403 makes sense in this case.
---
Closes #2337.
2022-11-08 13:34:01 +01:00
* @param projectId - provide if you ' re requesting the feature in the context of a specific project .
2023-04-18 08:59:02 +02:00
* @param userId
2021-07-07 10:46:50 +02:00
* /
2022-11-29 16:06:08 +01:00
async getFeature ( {
featureName ,
archived ,
projectId ,
environmentVariants ,
userId ,
} : IGetFeatureParams ) : Promise < FeatureToggleWithEnvironment > {
2022-11-21 10:37:16 +01:00
if ( projectId ) {
2023-03-17 12:43:34 +01:00
await this . validateFeatureBelongsToProject ( {
featureName ,
projectId ,
} ) ;
2022-11-21 10:37:16 +01:00
}
if ( environmentVariants ) {
return this . featureStrategiesStore . getFeatureToggleWithVariantEnvs (
featureName ,
2022-11-29 16:06:08 +01:00
userId ,
2022-11-21 10:37:16 +01:00
archived ,
) ;
} else {
return this . featureStrategiesStore . getFeatureToggleWithEnvs (
Fix: validate that the project is correct when getting feature by project (#2344)
## What
This PR fixes a bug where fetching a feature toggle via the
`/api/admin/projects/:projectId/features/:featureName` endpoint doesn't
validate that the feature belongs to the provided project. The same
thing applies to the archive functionality. This has also been fixed.
In doing so, it also adds corresponding tests to check for edge cases,
updates the 403 error response we use to provide clearer steps for the
user, and adds more error responses to the OpenAPI documentation.
## Why
As mentioned in #2337, it's unexpected that the provided project
shouldn't matter at all, and after discussions internally, it was also
discovered that this was never intended to be the case.
## Discussion points
It might be worth rethinking this for Unleash v5. Why does the features
API need the projects part at all when features are unique across the
entire instance? Would it be worth reverting to a simpler feature API
later or would that introduce issues with regards to how different
projects can have different active environments and so on?
### Further improvements
I have _not_ provided schemas for the error responses for the endpoints
at this time. I considered it, but because it would introduce new schema
code, more tests, etc, I decided to leave it for later. There's a
thorough OpenAPI walkthrough coming up, so I think it makes sense to do
it as part of that work instead. I am happy to be challenged on this,
however, and will implement it if you think it's better.
### Why 403 when the project is wrong?
We could also have used the 404 status code for when the feature exists
but doesn't belong to this project, but this would require more (and
more complex) code. We also already use 403 for cases like this for
post, patch, and put. Finally, the [HTTP spec's section on the 403
status code](https://httpwg.org/specs/rfc9110.html#status.403) says the
following (emphasis mine):
> The 403 (Forbidden) status code indicates that the server
**_understood the request but refuses to fulfill it_**. A server that
wishes to make public why the request has been forbidden can describe
that reason in the response content (if any).
>
> If authentication credentials were provided in the request, the server
considers them insufficient to grant access. The client SHOULD NOT
automatically repeat the request with the same credentials. The client
MAY repeat the request with new or different credentials. However, **_a
request might be forbidden for reasons unrelated to the credentials_**.
As such, I think using 403 makes sense in this case.
---
Closes #2337.
2022-11-08 13:34:01 +01:00
featureName ,
2022-11-29 16:06:08 +01:00
userId ,
Fix: validate that the project is correct when getting feature by project (#2344)
## What
This PR fixes a bug where fetching a feature toggle via the
`/api/admin/projects/:projectId/features/:featureName` endpoint doesn't
validate that the feature belongs to the provided project. The same
thing applies to the archive functionality. This has also been fixed.
In doing so, it also adds corresponding tests to check for edge cases,
updates the 403 error response we use to provide clearer steps for the
user, and adds more error responses to the OpenAPI documentation.
## Why
As mentioned in #2337, it's unexpected that the provided project
shouldn't matter at all, and after discussions internally, it was also
discovered that this was never intended to be the case.
## Discussion points
It might be worth rethinking this for Unleash v5. Why does the features
API need the projects part at all when features are unique across the
entire instance? Would it be worth reverting to a simpler feature API
later or would that introduce issues with regards to how different
projects can have different active environments and so on?
### Further improvements
I have _not_ provided schemas for the error responses for the endpoints
at this time. I considered it, but because it would introduce new schema
code, more tests, etc, I decided to leave it for later. There's a
thorough OpenAPI walkthrough coming up, so I think it makes sense to do
it as part of that work instead. I am happy to be challenged on this,
however, and will implement it if you think it's better.
### Why 403 when the project is wrong?
We could also have used the 404 status code for when the feature exists
but doesn't belong to this project, but this would require more (and
more complex) code. We also already use 403 for cases like this for
post, patch, and put. Finally, the [HTTP spec's section on the 403
status code](https://httpwg.org/specs/rfc9110.html#status.403) says the
following (emphasis mine):
> The 403 (Forbidden) status code indicates that the server
**_understood the request but refuses to fulfill it_**. A server that
wishes to make public why the request has been forbidden can describe
that reason in the response content (if any).
>
> If authentication credentials were provided in the request, the server
considers them insufficient to grant access. The client SHOULD NOT
automatically repeat the request with the same credentials. The client
MAY repeat the request with new or different credentials. However, **_a
request might be forbidden for reasons unrelated to the credentials_**.
As such, I think using 403 makes sense in this case.
---
Closes #2337.
2022-11-08 13:34:01 +01:00
archived ,
) ;
}
2021-07-07 10:46:50 +02:00
}
2021-11-24 13:08:04 +01:00
/ * *
* GET / api / admin / projects / : project / features / : featureName / variants
2022-12-06 10:47:54 +01:00
* @deprecated - Variants should be fetched from FeatureEnvironmentStore ( since variants are now ; since 4.18 , connected to environments )
2021-11-24 13:08:04 +01:00
* @param featureName
* @return The list of variants
* /
async getVariants ( featureName : string ) : Promise < IVariant [ ] > {
return this . featureToggleStore . getVariants ( featureName ) ;
}
2022-11-21 10:37:16 +01:00
async getVariantsForEnv (
featureName : string ,
environment : string ,
) : Promise < IVariant [ ] > {
const featureEnvironment = await this . featureEnvironmentStore . get ( {
featureName ,
environment ,
} ) ;
return featureEnvironment . variants || [ ] ;
}
2021-09-13 10:23:57 +02:00
async getFeatureMetadata ( featureName : string ) : Promise < FeatureToggle > {
return this . featureToggleStore . get ( featureName ) ;
}
2021-07-07 10:46:50 +02:00
async getClientFeatures (
query? : IFeatureToggleQuery ,
feat(#1873/playground): Return detailed information on feature toggle evaluation (#1839)
* Feat: return reasons why a feature evaluated to true or false
Note: this is very rough and just straight ripped from the nodejs
client. It will need a lot of work, but is a good place to start
* Feat: add suggested shape for new payload
* Chore: minor cleanup
* Wip: make server compile again
* Remove unused schema ref
* Export new schemas
* Chore: fix some tests to use sub property
* Fix: fix some tests
* Refactor: rename some variables, uncomment some stuff
* Add segments type to bootstrap options
* Add segments capability to offline feature evaluator
* Fix function calls after turning params into an option abject
* Feat: test strategy order, etc
* Feat: add test to check that all strats are returned correctly
* Feat: allow you to include strategy ids in clients
* Wip: hook up segments in the offline client.
Note: compared to regular clients, they still fail
* Feat: add segments validation
* Fix: fix test case invariant.
* Chore: revert to returning only `boolean` from strategies.
This _should_ make it work with custom strategies too 🤞
* Feat: make more properties of the returned feature required
* Wip: add some comments and unfinished tests for edge cases
* Feat: add `isEnabledInCurrentEnvironment` prop
* Feat: consider more strategy failure cases
* Feat: test that isenabledinenvironment matches expectations
* Feat: add unknown strategies
* Fix: fix property access typo
* Feat: add unknown strategy for fallback purposes
* Feat: test edge case: all unknown strategies
* Feat: add custom strategy to arbitrary
* Feat: test that features can be true, even if not enabled in env
* Chore: add some comments
* Wip: fix sdk tests
* Remove comments, improve test logging
* Feat: add descriptions and examples to playground feature schema
* Switch `examples` for `example`
* Update schemas with descriptions and examples
* Fix: update snapshot
* Fix: openapi example
* Fix: merge issues
* Fix: fix issue where feature evaluation state was wrong
* Chore: update openapi spec
* Fix: fix broken offline client tests
* Refactor: move schemas into separate files
* Refactor: remove "reason" for incomplete evaluation.
The only instances where evaluation is incomplete is when we don't
know what the strategy is.
* Refactor: move unleash node client into test and dev dependencies
* Wip: further removal of stuff
* Chore: remove a bunch of code that we don't use
* Chore: remove comment
* Chore: remove unused code
* Fix: fix some prettier errors
* Type parameters in strategies to avoid `any`
* Fix: remove commented out code
* Feat: make `id` required on playground strategies
* Chore: remove redundant type
* Fix: remove redundant if and fix fallback evaluation
* Refactor: reduce nesting and remove duplication
* Fix: remove unused helper function
* Refactor: type `parameters` as `unknown`
* Chore: remove redundant comment
* Refactor: move constraint code into a separate file
* Refactor: rename `unleash` -> `feature-evaluator`
* Rename class `Unleash` -> `FeatureEvaluator`
* Refactor: remove this.ready and sync logic from feature evaluator
* Refactor: remove unused code, rename config type
* Refactor: remove event emission from the Unleash client
* Remove unlistened-for events in feature evaluator
* Refactor: make offline client synchronous; remove code
* Fix: update openapi snapshot after adding required strategy ids
* Feat: change `strategies` format.
This commit changes the format of a playground feature's `strategies`
properties from a list of strategies to an object with properties
`result` and `data`. It looks a bit like this:
```ts
type Strategies = {
result: boolean | "unknown",
data: Strategy[]
}
```
The reason is that this allows us to avoid the breaking change that
was previously suggested in the PR:
`feature.isEnabled` used to be a straight boolean. Then, when we found
out we couldn't necessarily evaluate all strategies (custom strats are
hard!) we changed it to `boolean | 'unevaluated'`. However, this is
confusing on a few levels as the playground results are no longer the
same as the SDK would be, nor are they strictly boolean anymore.
This change reverts the `isEnabled` functionality to what it was
before (so it's always a mirror of what the SDK would show).
The equivalent of `feature.isEnabled === 'unevaluated'` now becomes
`feature.isEnabled && strategy.result === 'unknown'`.
* Fix: Fold long string descriptions over multiple lines.
* Fix: update snapshot after adding line breaks to descriptions
2022-08-04 15:41:52 +02:00
includeIds? : boolean ,
2023-04-21 11:09:07 +02:00
includeDisabledStrategies? : boolean ,
2021-07-07 10:46:50 +02:00
) : Promise < FeatureConfigurationClient [ ] > {
2023-03-09 14:45:03 +01:00
const result = await this . featureToggleClientStore . getClient (
2023-03-24 10:43:38 +01:00
query || { } ,
2023-03-09 14:45:03 +01:00
includeIds ,
2023-04-21 11:09:07 +02:00
includeDisabledStrategies ,
2023-03-09 14:45:03 +01:00
) ;
2023-03-09 16:20:12 +01:00
if ( this . flagResolver . isEnabled ( 'cleanClientApi' ) ) {
return result . map (
( {
name ,
type ,
enabled ,
project ,
stale ,
strategies ,
variants ,
description ,
createdAt ,
lastSeenAt ,
impressionData ,
} ) = > ( {
name ,
type ,
enabled ,
project ,
stale ,
strategies ,
variants ,
description ,
createdAt ,
lastSeenAt ,
impressionData ,
} ) ,
) ;
} else {
return result ;
}
2021-07-07 10:46:50 +02:00
}
/ * *
2022-11-30 13:44:38 +01:00
* @deprecated Legacy !
2021-09-13 10:23:57 +02:00
*
2021-07-07 10:46:50 +02:00
* Used to retrieve metadata of all feature toggles defined in Unleash .
* @param query - Allow you to limit search based on criteria such as project , tags , namePrefix . See @IFeatureToggleQuery
* @param archived - Return archived or active toggles
* @returns
* /
async getFeatureToggles (
query? : IFeatureToggleQuery ,
2022-11-29 16:06:08 +01:00
userId? : number ,
2021-08-12 15:04:37 +02:00
archived : boolean = false ,
2021-07-07 10:46:50 +02:00
) : Promise < FeatureToggle [ ] > {
2022-11-29 16:06:08 +01:00
return this . featureToggleClientStore . getAdmin ( {
featureQuery : query ,
userId ,
archived ,
} ) ;
2021-09-13 10:23:57 +02:00
}
async getFeatureOverview (
2022-11-29 16:06:08 +01:00
params : IFeatureProjectUserParams ,
2021-09-13 10:23:57 +02:00
) : Promise < IFeatureOverview [ ] > {
2022-11-29 16:06:08 +01:00
return this . featureStrategiesStore . getFeatureOverview ( params ) ;
2021-07-07 10:46:50 +02:00
}
async getFeatureToggle (
featureName : string ,
) : Promise < FeatureToggleWithEnvironment > {
2021-09-13 10:23:57 +02:00
return this . featureStrategiesStore . getFeatureToggleWithEnvs (
2021-08-25 13:38:00 +02:00
featureName ,
) ;
2021-07-07 10:46:50 +02:00
}
async createFeatureToggle (
projectId : string ,
value : FeatureToggleDTO ,
2021-11-12 13:15:51 +01:00
createdBy : string ,
2021-11-25 14:53:58 +01:00
isValidated : boolean = false ,
2021-07-07 10:46:50 +02:00
) : Promise < FeatureToggle > {
2021-11-12 13:15:51 +01:00
this . logger . info ( ` ${ createdBy } creates feature toggle ${ value . name } ` ) ;
2021-07-07 10:46:50 +02:00
await this . validateName ( value . name ) ;
2021-08-12 15:04:37 +02:00
const exists = await this . projectStore . hasProject ( projectId ) ;
if ( exists ) {
2021-11-25 14:53:58 +01:00
let featureData ;
if ( isValidated ) {
featureData = value ;
} else {
featureData = await featureMetadataSchema . validateAsync ( value ) ;
}
2021-11-12 13:15:51 +01:00
const featureName = featureData . name ;
2021-09-13 10:23:57 +02:00
const createdToggle = await this . featureToggleStore . create (
2021-08-12 15:04:37 +02:00
projectId ,
featureData ,
) ;
2021-09-13 10:23:57 +02:00
await this . featureEnvironmentStore . connectFeatureToEnvironmentsForProject (
2021-11-12 13:15:51 +01:00
featureName ,
2021-08-12 15:04:37 +02:00
projectId ,
) ;
2021-08-27 10:15:56 +02:00
2022-11-21 10:37:16 +01:00
if ( value . variants && value . variants . length > 0 ) {
const environments =
await this . featureEnvironmentStore . getEnvironmentsForFeature (
featureName ,
) ;
2023-01-20 10:30:20 +01:00
await this . featureEnvironmentStore . setVariantsToFeatureEnvironments (
featureName ,
environments . map ( ( env ) = > env . environment ) ,
value . variants ,
) ;
2022-11-21 10:37:16 +01:00
}
2021-11-12 13:15:51 +01:00
const tags = await this . tagStore . getAllTagsForFeature ( featureName ) ;
2021-08-27 10:15:56 +02:00
2021-11-12 13:15:51 +01:00
await this . eventStore . store (
new FeatureCreatedEvent ( {
featureName ,
createdBy ,
project : projectId ,
data : createdToggle ,
tags ,
} ) ,
) ;
2021-07-07 10:46:50 +02:00
2021-08-12 15:04:37 +02:00
return createdToggle ;
}
throw new NotFoundError ( ` Project with id ${ projectId } does not exist ` ) ;
2021-07-07 10:46:50 +02:00
}
2021-10-08 09:37:27 +02:00
async cloneFeatureToggle (
featureName : string ,
projectId : string ,
newFeatureName : string ,
2022-04-25 13:14:43 +02:00
replaceGroupId : boolean = true , // eslint-disable-line
2021-10-08 09:37:27 +02:00
userName : string ,
) : Promise < FeatureToggle > {
2023-03-27 13:21:50 +02:00
const changeRequestEnabled =
await this . changeRequestAccessReadModel . isChangeRequestsEnabledForProject (
projectId ,
) ;
if ( changeRequestEnabled ) {
throw new NoAccessError (
` Cloning not allowed. Project ${ projectId } has change requests enabled. ` ,
) ;
}
2021-10-08 09:37:27 +02:00
this . logger . info (
` ${ userName } clones feature toggle ${ featureName } to ${ newFeatureName } ` ,
) ;
await this . validateName ( newFeatureName ) ;
const cToggle =
2022-11-22 16:00:44 +01:00
await this . featureStrategiesStore . getFeatureToggleWithVariantEnvs (
2021-10-08 09:37:27 +02:00
featureName ,
) ;
2022-11-22 16:00:44 +01:00
const newToggle = {
. . . cToggle ,
name : newFeatureName ,
variants : undefined ,
} ;
2021-10-08 09:37:27 +02:00
const created = await this . createFeatureToggle (
projectId ,
newToggle ,
userName ,
) ;
2022-11-22 16:00:44 +01:00
const variantTasks = newToggle . environments . map ( ( e ) = > {
return this . featureEnvironmentStore . addVariantsToFeatureEnvironment (
newToggle . name ,
e . name ,
e . variants ,
) ;
} ) ;
const strategyTasks = newToggle . environments . flatMap ( ( e ) = >
2022-06-08 15:41:02 +02:00
e . strategies . map ( ( s ) = > {
2023-03-24 10:43:38 +01:00
if (
replaceGroupId &&
s . parameters &&
s . parameters . hasOwnProperty ( 'groupId' )
) {
2021-10-08 09:37:27 +02:00
s . parameters . groupId = newFeatureName ;
}
2022-06-08 15:41:02 +02:00
const context = {
projectId ,
featureName : newFeatureName ,
environment : e.name ,
} ;
2022-11-23 14:39:09 +01:00
return this . createStrategy ( s , context , userName ) ;
2021-10-08 09:37:27 +02:00
} ) ,
) ;
2022-11-22 16:00:44 +01:00
await Promise . all ( [ . . . strategyTasks , . . . variantTasks ] ) ;
2021-10-08 09:37:27 +02:00
return created ;
}
2021-07-07 10:46:50 +02:00
async updateFeatureToggle (
projectId : string ,
updatedFeature : FeatureToggleDTO ,
userName : string ,
2022-01-21 12:02:05 +01:00
featureName : string ,
2021-07-07 10:46:50 +02:00
) : Promise < FeatureToggle > {
2023-03-17 12:43:34 +01:00
await this . validateFeatureBelongsToProject ( { featureName , projectId } ) ;
2022-01-21 12:02:05 +01:00
2021-09-13 10:23:57 +02:00
this . logger . info ( ` ${ userName } updates feature toggle ${ featureName } ` ) ;
const featureData = await featureMetadataSchema . validateAsync (
updatedFeature ,
2021-07-07 10:46:50 +02:00
) ;
2021-11-12 13:15:51 +01:00
const preData = await this . featureToggleStore . get ( featureName ) ;
2022-01-21 12:02:05 +01:00
const featureToggle = await this . featureToggleStore . update ( projectId , {
. . . featureData ,
name : featureName ,
} ) ;
2021-11-12 13:15:51 +01:00
const tags = await this . tagStore . getAllTagsForFeature ( featureName ) ;
2021-08-26 13:59:11 +02:00
2021-11-12 13:15:51 +01:00
await this . eventStore . store (
new FeatureMetadataUpdateEvent ( {
createdBy : userName ,
data : featureToggle ,
preData ,
featureName ,
project : projectId ,
tags ,
} ) ,
) ;
2021-07-07 10:46:50 +02:00
return featureToggle ;
}
async getFeatureCountForProject ( projectId : string ) : Promise < number > {
return this . featureToggleStore . count ( {
archived : false ,
project : projectId ,
} ) ;
}
async removeAllStrategiesForEnv (
toggleName : string ,
2021-09-24 13:55:00 +02:00
environment : string = DEFAULT_ENV ,
2021-07-07 10:46:50 +02:00
) : Promise < void > {
2021-09-13 10:23:57 +02:00
await this . featureStrategiesStore . removeAllStrategiesForFeatureEnv (
2021-07-07 10:46:50 +02:00
toggleName ,
environment ,
) ;
}
2022-06-23 08:10:20 +02:00
async getStrategy ( strategyId : string ) : Promise < Saved < IStrategyConfig > > {
2021-07-07 10:46:50 +02:00
const strategy = await this . featureStrategiesStore . getStrategyById (
strategyId ,
) ;
2022-10-10 14:32:34 +02:00
2023-03-24 10:43:38 +01:00
const segments = await this . segmentService . getByStrategy ( strategyId ) ;
2022-10-10 14:32:34 +02:00
let result : Saved < IStrategyConfig > = {
2021-07-07 10:46:50 +02:00
id : strategy.id ,
name : strategy.strategyName ,
constraints : strategy.constraints || [ ] ,
parameters : strategy.parameters ,
2022-10-10 14:32:34 +02:00
segments : [ ] ,
2023-04-18 08:59:02 +02:00
title : strategy.title ,
2023-04-21 11:09:07 +02:00
disabled : strategy.disabled ,
2021-07-07 10:46:50 +02:00
} ;
2022-10-10 14:32:34 +02:00
if ( segments && segments . length > 0 ) {
result = {
. . . result ,
segments : segments.map ( ( segment ) = > segment . id ) ,
} ;
}
return result ;
2021-07-07 10:46:50 +02:00
}
async getEnvironmentInfo (
project : string ,
environment : string ,
featureName : string ,
) : Promise < IFeatureEnvironmentInfo > {
2021-08-25 13:38:00 +02:00
const envMetadata =
await this . featureEnvironmentStore . getEnvironmentMetaData (
environment ,
featureName ,
) ;
const strategies =
2021-09-13 10:23:57 +02:00
await this . featureStrategiesStore . getStrategiesForFeatureEnv (
2021-08-25 13:38:00 +02:00
project ,
featureName ,
environment ,
) ;
2023-04-28 13:59:04 +02:00
const defaultStrategy = await this . projectStore . getDefaultStrategy (
project ,
environment ,
) ;
2021-07-07 10:46:50 +02:00
return {
name : featureName ,
environment ,
enabled : envMetadata.enabled ,
strategies ,
2023-05-08 10:42:26 +02:00
defaultStrategy ,
2021-07-07 10:46:50 +02:00
} ;
}
2021-11-12 13:15:51 +01:00
// todo: store events for this change.
2021-07-07 10:46:50 +02:00
async deleteEnvironment (
projectId : string ,
environment : string ,
) : Promise < void > {
await this . featureStrategiesStore . deleteConfigurationsForProjectAndEnvironment (
projectId ,
environment ,
) ;
2021-08-25 13:38:00 +02:00
await this . projectStore . deleteEnvironmentForProject (
projectId ,
environment ,
) ;
2021-07-07 10:46:50 +02:00
}
/** Validations */
async validateName ( name : string ) : Promise < string > {
await nameSchema . validateAsync ( { name } ) ;
await this . validateUniqueFeatureName ( name ) ;
return name ;
}
async validateUniqueFeatureName ( name : string ) : Promise < void > {
2021-11-12 13:15:51 +01:00
let msg : string ;
2021-07-07 10:46:50 +02:00
try {
2021-09-13 10:23:57 +02:00
const feature = await this . featureToggleStore . get ( name ) ;
2021-07-07 10:46:50 +02:00
msg = feature . archived
? 'An archived toggle with that name already exists'
: 'A toggle with that name already exists' ;
} catch ( error ) {
return ;
}
throw new NameExistsError ( msg ) ;
}
async hasFeature ( name : string ) : Promise < boolean > {
return this . featureToggleStore . exists ( name ) ;
}
async updateStale (
featureName : string ,
isStale : boolean ,
2021-11-12 13:15:51 +01:00
createdBy : string ,
2021-07-07 10:46:50 +02:00
) : Promise < any > {
2021-09-13 10:23:57 +02:00
const feature = await this . featureToggleStore . get ( featureName ) ;
2021-11-12 13:15:51 +01:00
const { project } = feature ;
2021-09-13 10:23:57 +02:00
feature . stale = isStale ;
2021-11-12 13:15:51 +01:00
await this . featureToggleStore . update ( project , feature ) ;
const tags = await this . tagStore . getAllTagsForFeature ( featureName ) ;
await this . eventStore . store (
new FeatureStaleEvent ( {
stale : isStale ,
project ,
featureName ,
createdBy ,
tags ,
} ) ,
2021-07-07 10:46:50 +02:00
) ;
return feature ;
}
Fix: validate that the project is correct when getting feature by project (#2344)
## What
This PR fixes a bug where fetching a feature toggle via the
`/api/admin/projects/:projectId/features/:featureName` endpoint doesn't
validate that the feature belongs to the provided project. The same
thing applies to the archive functionality. This has also been fixed.
In doing so, it also adds corresponding tests to check for edge cases,
updates the 403 error response we use to provide clearer steps for the
user, and adds more error responses to the OpenAPI documentation.
## Why
As mentioned in #2337, it's unexpected that the provided project
shouldn't matter at all, and after discussions internally, it was also
discovered that this was never intended to be the case.
## Discussion points
It might be worth rethinking this for Unleash v5. Why does the features
API need the projects part at all when features are unique across the
entire instance? Would it be worth reverting to a simpler feature API
later or would that introduce issues with regards to how different
projects can have different active environments and so on?
### Further improvements
I have _not_ provided schemas for the error responses for the endpoints
at this time. I considered it, but because it would introduce new schema
code, more tests, etc, I decided to leave it for later. There's a
thorough OpenAPI walkthrough coming up, so I think it makes sense to do
it as part of that work instead. I am happy to be challenged on this,
however, and will implement it if you think it's better.
### Why 403 when the project is wrong?
We could also have used the 404 status code for when the feature exists
but doesn't belong to this project, but this would require more (and
more complex) code. We also already use 403 for cases like this for
post, patch, and put. Finally, the [HTTP spec's section on the 403
status code](https://httpwg.org/specs/rfc9110.html#status.403) says the
following (emphasis mine):
> The 403 (Forbidden) status code indicates that the server
**_understood the request but refuses to fulfill it_**. A server that
wishes to make public why the request has been forbidden can describe
that reason in the response content (if any).
>
> If authentication credentials were provided in the request, the server
considers them insufficient to grant access. The client SHOULD NOT
automatically repeat the request with the same credentials. The client
MAY repeat the request with new or different credentials. However, **_a
request might be forbidden for reasons unrelated to the credentials_**.
As such, I think using 403 makes sense in this case.
---
Closes #2337.
2022-11-08 13:34:01 +01:00
async archiveToggle (
featureName : string ,
createdBy : string ,
projectId? : string ,
) : Promise < void > {
2021-11-12 13:15:51 +01:00
const feature = await this . featureToggleStore . get ( featureName ) ;
Fix: validate that the project is correct when getting feature by project (#2344)
## What
This PR fixes a bug where fetching a feature toggle via the
`/api/admin/projects/:projectId/features/:featureName` endpoint doesn't
validate that the feature belongs to the provided project. The same
thing applies to the archive functionality. This has also been fixed.
In doing so, it also adds corresponding tests to check for edge cases,
updates the 403 error response we use to provide clearer steps for the
user, and adds more error responses to the OpenAPI documentation.
## Why
As mentioned in #2337, it's unexpected that the provided project
shouldn't matter at all, and after discussions internally, it was also
discovered that this was never intended to be the case.
## Discussion points
It might be worth rethinking this for Unleash v5. Why does the features
API need the projects part at all when features are unique across the
entire instance? Would it be worth reverting to a simpler feature API
later or would that introduce issues with regards to how different
projects can have different active environments and so on?
### Further improvements
I have _not_ provided schemas for the error responses for the endpoints
at this time. I considered it, but because it would introduce new schema
code, more tests, etc, I decided to leave it for later. There's a
thorough OpenAPI walkthrough coming up, so I think it makes sense to do
it as part of that work instead. I am happy to be challenged on this,
however, and will implement it if you think it's better.
### Why 403 when the project is wrong?
We could also have used the 404 status code for when the feature exists
but doesn't belong to this project, but this would require more (and
more complex) code. We also already use 403 for cases like this for
post, patch, and put. Finally, the [HTTP spec's section on the 403
status code](https://httpwg.org/specs/rfc9110.html#status.403) says the
following (emphasis mine):
> The 403 (Forbidden) status code indicates that the server
**_understood the request but refuses to fulfill it_**. A server that
wishes to make public why the request has been forbidden can describe
that reason in the response content (if any).
>
> If authentication credentials were provided in the request, the server
considers them insufficient to grant access. The client SHOULD NOT
automatically repeat the request with the same credentials. The client
MAY repeat the request with new or different credentials. However, **_a
request might be forbidden for reasons unrelated to the credentials_**.
As such, I think using 403 makes sense in this case.
---
Closes #2337.
2022-11-08 13:34:01 +01:00
if ( projectId ) {
2023-03-17 12:43:34 +01:00
await this . validateFeatureBelongsToProject ( {
featureName ,
projectId ,
} ) ;
Fix: validate that the project is correct when getting feature by project (#2344)
## What
This PR fixes a bug where fetching a feature toggle via the
`/api/admin/projects/:projectId/features/:featureName` endpoint doesn't
validate that the feature belongs to the provided project. The same
thing applies to the archive functionality. This has also been fixed.
In doing so, it also adds corresponding tests to check for edge cases,
updates the 403 error response we use to provide clearer steps for the
user, and adds more error responses to the OpenAPI documentation.
## Why
As mentioned in #2337, it's unexpected that the provided project
shouldn't matter at all, and after discussions internally, it was also
discovered that this was never intended to be the case.
## Discussion points
It might be worth rethinking this for Unleash v5. Why does the features
API need the projects part at all when features are unique across the
entire instance? Would it be worth reverting to a simpler feature API
later or would that introduce issues with regards to how different
projects can have different active environments and so on?
### Further improvements
I have _not_ provided schemas for the error responses for the endpoints
at this time. I considered it, but because it would introduce new schema
code, more tests, etc, I decided to leave it for later. There's a
thorough OpenAPI walkthrough coming up, so I think it makes sense to do
it as part of that work instead. I am happy to be challenged on this,
however, and will implement it if you think it's better.
### Why 403 when the project is wrong?
We could also have used the 404 status code for when the feature exists
but doesn't belong to this project, but this would require more (and
more complex) code. We also already use 403 for cases like this for
post, patch, and put. Finally, the [HTTP spec's section on the 403
status code](https://httpwg.org/specs/rfc9110.html#status.403) says the
following (emphasis mine):
> The 403 (Forbidden) status code indicates that the server
**_understood the request but refuses to fulfill it_**. A server that
wishes to make public why the request has been forbidden can describe
that reason in the response content (if any).
>
> If authentication credentials were provided in the request, the server
considers them insufficient to grant access. The client SHOULD NOT
automatically repeat the request with the same credentials. The client
MAY repeat the request with new or different credentials. However, **_a
request might be forbidden for reasons unrelated to the credentials_**.
As such, I think using 403 makes sense in this case.
---
Closes #2337.
2022-11-08 13:34:01 +01:00
}
2021-11-12 13:15:51 +01:00
await this . featureToggleStore . archive ( featureName ) ;
const tags = await this . tagStore . getAllTagsForFeature ( featureName ) ;
await this . eventStore . store (
new FeatureArchivedEvent ( {
featureName ,
createdBy ,
project : feature.project ,
tags ,
} ) ,
) ;
2021-07-07 10:46:50 +02:00
}
2023-03-14 09:48:29 +01:00
async archiveToggles (
featureNames : string [ ] ,
createdBy : string ,
projectId : string ,
) : Promise < void > {
await this . validateFeaturesContext ( featureNames , projectId ) ;
const features = await this . featureToggleStore . getAllByNames (
featureNames ,
) ;
await this . featureToggleStore . batchArchive ( featureNames ) ;
2023-03-15 14:08:08 +01:00
const tags = await this . tagStore . getAllByFeatures ( featureNames ) ;
2023-03-14 09:48:29 +01:00
await this . eventStore . batchStore (
features . map (
( feature ) = >
new FeatureArchivedEvent ( {
featureName : feature.name ,
createdBy ,
project : feature.project ,
2023-03-15 14:08:08 +01:00
tags : tags
. filter ( ( tag ) = > tag . featureName === feature . name )
. map ( ( tag ) = > ( {
value : tag.tagValue ,
type : tag . tagType ,
} ) ) ,
2023-03-14 09:48:29 +01:00
} ) ,
) ,
) ;
}
2023-03-15 07:37:06 +01:00
async setToggleStaleness (
featureNames : string [ ] ,
stale : boolean ,
createdBy : string ,
projectId : string ,
) : Promise < void > {
await this . validateFeaturesContext ( featureNames , projectId ) ;
const features = await this . featureToggleStore . getAllByNames (
featureNames ,
) ;
const relevantFeatures = features . filter (
( feature ) = > feature . stale !== stale ,
) ;
2023-03-15 14:08:08 +01:00
const relevantFeatureNames = relevantFeatures . map (
( feature ) = > feature . name ,
2023-03-15 07:37:06 +01:00
) ;
2023-03-15 14:08:08 +01:00
await this . featureToggleStore . batchStale ( relevantFeatureNames , stale ) ;
const tags = await this . tagStore . getAllByFeatures ( relevantFeatureNames ) ;
2023-03-15 07:37:06 +01:00
await this . eventStore . batchStore (
relevantFeatures . map (
( feature ) = >
new FeatureStaleEvent ( {
stale : stale ,
project : projectId ,
featureName : feature.name ,
createdBy ,
2023-03-15 14:08:08 +01:00
tags : tags
. filter ( ( tag ) = > tag . featureName === feature . name )
. map ( ( tag ) = > ( {
value : tag.tagValue ,
type : tag . tagType ,
} ) ) ,
2023-03-15 07:37:06 +01:00
} ) ,
) ,
) ;
}
2021-07-07 10:46:50 +02:00
async updateEnabled (
2021-11-12 13:15:51 +01:00
project : string ,
2021-07-07 10:46:50 +02:00
featureName : string ,
environment : string ,
enabled : boolean ,
2021-11-12 13:15:51 +01:00
createdBy : string ,
2022-10-05 23:33:36 +02:00
user? : User ,
2023-05-08 10:42:26 +02:00
shouldActivateDisabledStrategies = false ,
2022-11-18 09:29:26 +01:00
) : Promise < FeatureToggle > {
2022-12-05 15:38:17 +01:00
await this . stopWhenChangeRequestsEnabled ( project , environment , user ) ;
2022-12-05 12:39:13 +01:00
if ( enabled ) {
await this . stopWhenCannotCreateStrategies (
project ,
environment ,
featureName ,
user ,
) ;
}
2022-11-18 09:29:26 +01:00
return this . unprotectedUpdateEnabled (
project ,
featureName ,
environment ,
enabled ,
createdBy ,
2023-05-08 10:42:26 +02:00
shouldActivateDisabledStrategies ,
2022-11-18 09:29:26 +01:00
) ;
}
async unprotectedUpdateEnabled (
project : string ,
featureName : string ,
environment : string ,
enabled : boolean ,
createdBy : string ,
2023-05-08 13:56:42 +02:00
shouldActivateDisabledStrategies = false ,
2021-07-07 10:46:50 +02:00
) : Promise < FeatureToggle > {
2021-08-25 13:38:00 +02:00
const hasEnvironment =
await this . featureEnvironmentStore . featureHasEnvironment (
2021-07-07 10:46:50 +02:00
environment ,
featureName ,
) ;
2021-09-13 10:23:57 +02:00
2022-12-05 12:39:13 +01:00
if ( ! hasEnvironment ) {
throw new NotFoundError (
` Could not find environment ${ environment } for feature: ${ featureName } ` ,
) ;
}
2021-10-21 22:33:50 +02:00
2022-12-05 12:39:13 +01:00
if ( enabled ) {
const strategies = await this . getStrategiesForEnvironment (
project ,
featureName ,
environment ,
) ;
2023-05-08 10:42:26 +02:00
const hasDisabledStrategies = strategies . some (
( strategy ) = > strategy . disabled ,
) ;
2023-05-05 13:32:44 +02:00
2023-05-08 10:42:26 +02:00
if (
2023-05-05 13:32:44 +02:00
this . flagResolver . isEnabled ( 'strategyImprovements' ) &&
2023-05-08 10:42:26 +02:00
hasDisabledStrategies &&
shouldActivateDisabledStrategies
) {
strategies . map ( async ( strategy ) = > {
return this . updateStrategy (
strategy . id ,
{ disabled : false } ,
{
environment ,
projectId : project ,
featureName ,
} ,
createdBy ,
) ;
} ) ;
}
const hasOnlyDisabledStrategies = strategies . every (
( strategy ) = > strategy . disabled ,
) ;
const shouldCreate =
hasOnlyDisabledStrategies && ! shouldActivateDisabledStrategies ;
if ( strategies . length === 0 || shouldCreate ) {
const projectEnvironmentDefaultStrategy =
await this . projectStore . getDefaultStrategy (
project ,
environment ,
) ;
const strategy =
this . flagResolver . isEnabled ( 'strategyImprovements' ) &&
projectEnvironmentDefaultStrategy != null
? getProjectDefaultStrategy (
projectEnvironmentDefaultStrategy ,
featureName ,
)
: getDefaultStrategy ( featureName ) ;
2022-12-05 12:39:13 +01:00
await this . unprotectedCreateStrategy (
2023-05-05 13:32:44 +02:00
strategy ,
2022-12-05 12:39:13 +01:00
{
2021-11-12 13:15:51 +01:00
environment ,
2022-12-05 12:39:13 +01:00
projectId : project ,
featureName ,
} ,
createdBy ,
2021-11-12 13:15:51 +01:00
) ;
2021-10-21 22:33:50 +02:00
}
2021-07-07 10:46:50 +02:00
}
2022-12-05 12:39:13 +01:00
const updatedEnvironmentStatus =
await this . featureEnvironmentStore . setEnvironmentEnabledStatus (
environment ,
featureName ,
enabled ,
) ;
const feature = await this . featureToggleStore . get ( featureName ) ;
2022-11-14 14:05:26 +01:00
2022-12-05 12:39:13 +01:00
if ( updatedEnvironmentStatus > 0 ) {
const tags = await this . tagStore . getAllTagsForFeature ( featureName ) ;
await this . eventStore . store (
new FeatureEnvironmentEvent ( {
enabled ,
project ,
featureName ,
environment ,
createdBy ,
tags ,
} ) ,
) ;
}
return feature ;
2021-07-07 10:46:50 +02:00
}
2021-11-12 13:15:51 +01:00
// @deprecated
2021-10-21 20:53:39 +02:00
async storeFeatureUpdatedEventLegacy (
featureName : string ,
2021-11-12 13:15:51 +01:00
createdBy : string ,
2021-10-21 20:53:39 +02:00
) : Promise < FeatureToggleLegacy > {
2021-11-12 13:15:51 +01:00
const tags = await this . tagStore . getAllTagsForFeature ( featureName ) ;
2021-10-21 20:53:39 +02:00
const feature = await this . getFeatureToggleLegacy ( featureName ) ;
2021-11-12 13:15:51 +01:00
// Legacy event. Will not be used from v4.3.
// We do not include 'preData' on purpose.
2021-10-21 20:53:39 +02:00
await this . eventStore . store ( {
type : FEATURE_UPDATED ,
2021-11-12 13:15:51 +01:00
createdBy ,
featureName ,
2021-10-21 20:53:39 +02:00
data : feature ,
tags ,
project : feature.project ,
} ) ;
return feature ;
}
2021-07-07 10:46:50 +02:00
// @deprecated
async toggle (
2021-09-13 10:23:57 +02:00
projectId : string ,
2021-07-07 10:46:50 +02:00
featureName : string ,
environment : string ,
userName : string ,
) : Promise < FeatureToggle > {
2021-09-13 10:23:57 +02:00
await this . featureToggleStore . get ( featureName ) ;
2021-08-25 13:38:00 +02:00
const isEnabled =
await this . featureEnvironmentStore . isEnvironmentEnabled (
featureName ,
environment ,
) ;
2021-07-07 10:46:50 +02:00
return this . updateEnabled (
2021-09-13 10:23:57 +02:00
projectId ,
2021-07-07 10:46:50 +02:00
featureName ,
environment ,
! isEnabled ,
userName ,
) ;
}
2021-11-12 13:15:51 +01:00
// @deprecated
2021-09-13 10:23:57 +02:00
async getFeatureToggleLegacy (
featureName : string ,
2021-10-21 20:53:39 +02:00
) : Promise < FeatureToggleLegacy > {
2021-09-13 10:23:57 +02:00
const feature =
await this . featureStrategiesStore . getFeatureToggleWithEnvs (
featureName ,
) ;
2021-10-21 20:53:39 +02:00
const { environments , . . . legacyFeature } = feature ;
const defaultEnv = environments . find ( ( e ) = > e . name === DEFAULT_ENV ) ;
2021-09-24 13:55:00 +02:00
const strategies = defaultEnv ? . strategies || [ ] ;
const enabled = defaultEnv ? . enabled || false ;
2021-10-21 20:53:39 +02:00
return { . . . legacyFeature , enabled , strategies } ;
2021-08-26 13:59:11 +02:00
}
2021-10-21 21:06:56 +02:00
async changeProject (
2021-07-07 10:46:50 +02:00
featureName : string ,
2021-10-21 21:06:56 +02:00
newProject : string ,
2021-11-12 13:15:51 +01:00
createdBy : string ,
2021-10-21 21:06:56 +02:00
) : Promise < void > {
2023-03-28 11:28:22 +02:00
const changeRequestEnabled =
await this . changeRequestAccessReadModel . isChangeRequestsEnabledForProject (
newProject ,
) ;
if ( changeRequestEnabled ) {
throw new NoAccessError (
` Changing project not allowed. Project ${ newProject } has change requests enabled. ` ,
) ;
}
2021-09-13 10:23:57 +02:00
const feature = await this . featureToggleStore . get ( featureName ) ;
2021-10-21 21:06:56 +02:00
const oldProject = feature . project ;
feature . project = newProject ;
await this . featureToggleStore . update ( newProject , feature ) ;
2021-11-12 13:15:51 +01:00
const tags = await this . tagStore . getAllTagsForFeature ( featureName ) ;
await this . eventStore . store (
new FeatureChangeProjectEvent ( {
createdBy ,
2021-10-21 21:06:56 +02:00
oldProject ,
newProject ,
2021-11-12 13:15:51 +01:00
featureName ,
tags ,
} ) ,
) ;
2021-07-07 10:46:50 +02:00
}
async getArchivedFeatures ( ) : Promise < FeatureToggle [ ] > {
2022-11-29 16:06:08 +01:00
return this . getFeatureToggles ( { } , undefined , true ) ;
2021-07-07 10:46:50 +02:00
}
2021-11-12 13:15:51 +01:00
// TODO: add project id.
async deleteFeature ( featureName : string , createdBy : string ) : Promise < void > {
const toggle = await this . featureToggleStore . get ( featureName ) ;
const tags = await this . tagStore . getAllTagsForFeature ( featureName ) ;
2021-08-12 15:04:37 +02:00
await this . featureToggleStore . delete ( featureName ) ;
2021-11-12 13:15:51 +01:00
await this . eventStore . store (
new FeatureDeletedEvent ( {
2021-08-12 15:04:37 +02:00
featureName ,
2021-11-12 13:15:51 +01:00
project : toggle.project ,
createdBy ,
preData : toggle ,
tags ,
} ) ,
) ;
2021-07-07 10:46:50 +02:00
}
2023-03-15 14:08:08 +01:00
async deleteFeatures (
featureNames : string [ ] ,
projectId : string ,
createdBy : string ,
) : Promise < void > {
await this . validateFeaturesContext ( featureNames , projectId ) ;
const features = await this . featureToggleStore . getAllByNames (
featureNames ,
) ;
const eligibleFeatures = features . filter (
( toggle ) = > toggle . archivedAt !== null ,
) ;
const eligibleFeatureNames = eligibleFeatures . map (
( toggle ) = > toggle . name ,
) ;
const tags = await this . tagStore . getAllByFeatures ( eligibleFeatureNames ) ;
await this . featureToggleStore . batchDelete ( eligibleFeatureNames ) ;
await this . eventStore . batchStore (
eligibleFeatures . map (
( feature ) = >
new FeatureDeletedEvent ( {
featureName : feature.name ,
createdBy ,
project : feature.project ,
preData : feature ,
tags : tags
. filter ( ( tag ) = > tag . featureName === feature . name )
. map ( ( tag ) = > ( {
value : tag.tagValue ,
type : tag . tagType ,
} ) ) ,
} ) ,
) ,
) ;
}
2023-03-16 08:51:18 +01:00
async reviveFeatures (
featureNames : string [ ] ,
projectId : string ,
createdBy : string ,
) : Promise < void > {
await this . validateFeaturesContext ( featureNames , projectId ) ;
const features = await this . featureToggleStore . getAllByNames (
featureNames ,
) ;
const eligibleFeatures = features . filter (
( toggle ) = > toggle . archivedAt !== null ,
) ;
const eligibleFeatureNames = eligibleFeatures . map (
( toggle ) = > toggle . name ,
) ;
const tags = await this . tagStore . getAllByFeatures ( eligibleFeatureNames ) ;
await this . featureToggleStore . batchRevive ( eligibleFeatureNames ) ;
await this . eventStore . batchStore (
eligibleFeatures . map (
( feature ) = >
new FeatureRevivedEvent ( {
featureName : feature.name ,
createdBy ,
project : feature.project ,
tags : tags
. filter ( ( tag ) = > tag . featureName === feature . name )
. map ( ( tag ) = > ( {
value : tag.tagValue ,
type : tag . tagType ,
} ) ) ,
} ) ,
) ,
) ;
}
2021-11-12 13:15:51 +01:00
// TODO: add project id.
async reviveToggle ( featureName : string , createdBy : string ) : Promise < void > {
const toggle = await this . featureToggleStore . revive ( featureName ) ;
const tags = await this . tagStore . getAllTagsForFeature ( featureName ) ;
await this . eventStore . store (
new FeatureRevivedEvent ( {
createdBy ,
featureName ,
project : toggle.project ,
tags ,
} ) ,
2021-08-25 13:38:00 +02:00
) ;
2021-07-07 10:46:50 +02:00
}
2021-08-25 13:38:00 +02:00
async getMetadataForAllFeatures (
archived : boolean ,
) : Promise < FeatureToggle [ ] > {
2021-09-13 10:23:57 +02:00
return this . featureToggleStore . getAll ( { archived } ) ;
2021-07-07 10:46:50 +02:00
}
2022-05-04 08:45:29 +02:00
async getMetadataForAllFeaturesByProjectId (
archived : boolean ,
project : string ,
) : Promise < FeatureToggle [ ] > {
return this . featureToggleStore . getAll ( { archived , project } ) ;
}
2021-08-12 15:04:37 +02:00
async getProjectId ( name : string ) : Promise < string > {
2021-09-13 10:23:57 +02:00
return this . featureToggleStore . getProjectId ( name ) ;
2021-08-12 15:04:37 +02:00
}
2021-10-19 09:49:43 +02:00
async updateFeatureStrategyProject (
featureName : string ,
newProjectId : string ,
) : Promise < void > {
await this . featureStrategiesStore . setProjectForStrategiesBelongingToFeature (
featureName ,
newProjectId ,
) ;
}
2021-11-24 13:08:04 +01:00
async updateVariants (
featureName : string ,
2021-12-16 11:07:19 +01:00
project : string ,
2021-11-24 13:08:04 +01:00
newVariants : Operation [ ] ,
2023-01-24 10:43:10 +01:00
user : User ,
2021-11-24 13:08:04 +01:00
) : Promise < FeatureToggle > {
2022-12-06 10:47:54 +01:00
const ft =
await this . featureStrategiesStore . getFeatureToggleWithVariantEnvs (
featureName ,
) ;
const promises = ft . environments . map ( ( env ) = >
this . updateVariantsOnEnv (
featureName ,
project ,
env . name ,
newVariants ,
2023-01-24 10:43:10 +01:00
user ,
2022-12-06 10:47:54 +01:00
) . then ( ( resultingVariants ) = > ( env . variants = resultingVariants ) ) ,
) ;
await Promise . all ( promises ) ;
ft . variants = ft . environments [ 0 ] . variants ;
return ft ;
2021-11-24 13:08:04 +01:00
}
2022-11-21 10:37:16 +01:00
async updateVariantsOnEnv (
featureName : string ,
project : string ,
environment : string ,
newVariants : Operation [ ] ,
2023-01-24 10:43:10 +01:00
user : User ,
2022-11-21 10:37:16 +01:00
) : Promise < IVariant [ ] > {
const oldVariants = await this . getVariantsForEnv (
featureName ,
environment ,
) ;
const { newDocument } = await applyPatch ( oldVariants , newVariants ) ;
2023-01-24 10:43:10 +01:00
return this . crProtectedSaveVariantsOnEnv (
2022-11-22 09:57:12 +01:00
project ,
2022-11-21 10:37:16 +01:00
featureName ,
environment ,
newDocument ,
2023-01-24 10:43:10 +01:00
user ,
2022-12-06 10:47:54 +01:00
oldVariants ,
2022-11-21 10:37:16 +01:00
) ;
}
2021-11-24 13:08:04 +01:00
async saveVariants (
featureName : string ,
2021-12-16 11:07:19 +01:00
project : string ,
2021-11-24 13:08:04 +01:00
newVariants : IVariant [ ] ,
2021-12-16 11:07:19 +01:00
createdBy : string ,
2021-11-24 13:08:04 +01:00
) : Promise < FeatureToggle > {
await variantsArraySchema . validateAsync ( newVariants ) ;
const fixedVariants = this . fixVariantWeights ( newVariants ) ;
2021-12-16 11:07:19 +01:00
const oldVariants = await this . featureToggleStore . getVariants (
featureName ,
) ;
const featureToggle = await this . featureToggleStore . saveVariants (
project ,
featureName ,
fixedVariants ,
) ;
const tags = await this . tagStore . getAllTagsForFeature ( featureName ) ;
await this . eventStore . store (
new FeatureVariantEvent ( {
project ,
featureName ,
createdBy ,
tags ,
oldVariants ,
Complete open api schemas for project features controller (#1563)
* Completed OpenAPI Schemas for ProjectFeatures Controller
Completed OpenAPI Schemas for Feature Controller (tags)
* Completed OpenAPI Schemas for ProjectFeatures Controller
Completed OpenAPI Schemas for Feature Controller (tags)
* bug fix
* bug fix
* fix merge conflicts, some refactoring
* fix merge conflicts, some refactoring
* fix merge conflicts, some refactoring
* added emptyResponse, patch feature operation schemas and request
* added emptyResponse, patch feature operation schemas and request
* patch strategy
* patch strategy
* update strategy
* update strategy
* fix pr comment
* fix pr comments
* improvements
* added operationId to schema for better generation
* fix pr comment
* fix pr comment
* fix pr comment
* improvements to generated and dynamic types
* improvements to generated and dynamic types
* improvements to generated and dynamic types
* Update response types to use inferred types
* Update addTag response status to 201
* refactor: move schema ref destructuring into createSchemaObject
* made serialize date handle deep objects
* made serialize date handle deep objects
* add `name` to IFeatureStrategy nad fix tests
* fix pr comments
* fix pr comments
* Add types to IAuthRequest
* Sync StrategySchema for FE and BE - into the rabbit hole
* Sync model with OAS spec
* Completed OpenAPI Schemas for ProjectFeatures Controller
Completed OpenAPI Schemas for Feature Controller (tags)
* Completed OpenAPI Schemas for ProjectFeatures Controller
Completed OpenAPI Schemas for Feature Controller (tags)
* bug fix
* bug fix
* fix merge conflicts, some refactoring
* fix merge conflicts, some refactoring
* fix merge conflicts, some refactoring
* added emptyResponse, patch feature operation schemas and request
* added emptyResponse, patch feature operation schemas and request
* patch strategy
* patch strategy
* update strategy
* update strategy
* fix pr comment
* fix pr comments
* improvements
* added operationId to schema for better generation
* fix pr comment
* fix pr comment
* fix pr comment
* improvements to generated and dynamic types
* improvements to generated and dynamic types
* improvements to generated and dynamic types
* Update response types to use inferred types
* Update addTag response status to 201
* refactor: move schema ref destructuring into createSchemaObject
* made serialize date handle deep objects
* made serialize date handle deep objects
* add `name` to IFeatureStrategy nad fix tests
* fix pr comments
* fix pr comments
* Add types to IAuthRequest
* Sync StrategySchema for FE and BE - into the rabbit hole
* Sync model with OAS spec
* Completed OpenAPI Schemas for ProjectFeatures Controller
Completed OpenAPI Schemas for Feature Controller (tags)
* Completed OpenAPI Schemas for ProjectFeatures Controller
Completed OpenAPI Schemas for Feature Controller (tags)
* bug fix
* bug fix
* fix merge conflicts, some refactoring
* fix merge conflicts, some refactoring
* fix merge conflicts, some refactoring
* added emptyResponse, patch feature operation schemas and request
* added emptyResponse, patch feature operation schemas and request
* patch strategy
* patch strategy
* update strategy
* update strategy
* fix pr comment
* fix pr comments
* improvements
* added operationId to schema for better generation
* fix pr comment
* fix pr comment
* fix pr comment
* improvements to generated and dynamic types
* improvements to generated and dynamic types
* improvements to generated and dynamic types
* Update response types to use inferred types
* Update addTag response status to 201
* refactor: move schema ref destructuring into createSchemaObject
* made serialize date handle deep objects
* made serialize date handle deep objects
* add `name` to IFeatureStrategy nad fix tests
* fix pr comments
* fix pr comments
* Add types to IAuthRequest
* Sync StrategySchema for FE and BE - into the rabbit hole
* Sync model with OAS spec
* Completed OpenAPI Schemas for ProjectFeatures Controller
Completed OpenAPI Schemas for Feature Controller (tags)
* Completed OpenAPI Schemas for ProjectFeatures Controller
Completed OpenAPI Schemas for Feature Controller (tags)
* bug fix
* bug fix
* fix merge conflicts, some refactoring
* fix merge conflicts, some refactoring
* fix merge conflicts, some refactoring
* added emptyResponse, patch feature operation schemas and request
* added emptyResponse, patch feature operation schemas and request
* patch strategy
* patch strategy
* update strategy
* update strategy
* fix pr comment
* fix pr comments
* improvements
* added operationId to schema for better generation
* fix pr comment
* fix pr comment
* fix pr comment
* improvements to generated and dynamic types
* improvements to generated and dynamic types
* improvements to generated and dynamic types
* Update response types to use inferred types
* Update addTag response status to 201
* refactor: move schema ref destructuring into createSchemaObject
* made serialize date handle deep objects
* made serialize date handle deep objects
* add `name` to IFeatureStrategy nad fix tests
* fix pr comments
* fix pr comments
* Add types to IAuthRequest
* Sync StrategySchema for FE and BE - into the rabbit hole
* Sync model with OAS spec
* revert
* revert
* revert
* revert
* revert
* mapper
* revert
* revert
* revert
* remove serialize-dates.ts
* remove serialize-dates.ts
* remove serialize-dates.ts
* remove serialize-dates.ts
* remove serialize-dates.ts
* revert
* revert
* add mappers
* add mappers
* fix pr comments
* ignore report.json
* ignore report.json
* Route permission required
Co-authored-by: olav <mail@olav.io>
2022-05-18 15:17:09 +02:00
newVariants : featureToggle.variants as IVariant [ ] ,
2021-12-16 11:07:19 +01:00
} ) ,
) ;
return featureToggle ;
2021-11-24 13:08:04 +01:00
}
2022-11-21 10:37:16 +01:00
async saveVariantsOnEnv (
2022-11-22 09:57:12 +01:00
projectId : string ,
2022-11-21 10:37:16 +01:00
featureName : string ,
environment : string ,
newVariants : IVariant [ ] ,
fix: make sure we have a user in event store (#3072)
## About the changes
Spotted some issues in logs:
```json
{
"level":"warn",
"message":"Failed to store \"feature-environment-variants-updated\" event: error: insert into \"events\" (\"created_by\", \"data\", \"environment\", \"feature_name\", \"pre_data\", \"project\", \"tags\", \"type\") values (DEFAULT, $1, $2, $3, $4, $5, $6, $7) returning \"id\", \"type\", \"created_by\", \"created_at\", \"data\", \"pre_data\", \"tags\", \"feature_name\", \"project\", \"environment\" - null value in column \"created_by\" violates not-null constraint",
"name":"lib/db/event-store.ts"
}
```
In all other events we're doing the following:
https://github.com/Unleash/unleash/blob/b7fdcd36c08fcd9461fef930767606962e73d59c/src/lib/services/segment-service.ts#L80
So this is just mimicking that to quickly release a patch, but I'll look
into a safer (type-checked) solution so this problem does not happen
again
2023-02-09 11:01:39 +01:00
user : User ,
2022-12-06 10:47:54 +01:00
oldVariants? : IVariant [ ] ,
2022-11-21 10:37:16 +01:00
) : Promise < IVariant [ ] > {
await variantsArraySchema . validateAsync ( newVariants ) ;
const fixedVariants = this . fixVariantWeights ( newVariants ) ;
2022-12-06 10:47:54 +01:00
const theOldVariants : IVariant [ ] =
oldVariants ||
(
await this . featureEnvironmentStore . get ( {
featureName ,
environment ,
} )
2023-03-24 10:43:38 +01:00
) . variants ||
[ ] ;
2022-11-21 10:37:16 +01:00
await this . eventStore . store (
new EnvironmentVariantEvent ( {
featureName ,
environment ,
2022-11-22 09:57:12 +01:00
project : projectId ,
2023-02-21 15:11:39 +01:00
createdBy : user ,
2022-12-06 10:47:54 +01:00
oldVariants : theOldVariants ,
2022-11-21 10:37:16 +01:00
newVariants : fixedVariants ,
} ) ,
) ;
2023-01-20 10:30:20 +01:00
await this . featureEnvironmentStore . setVariantsToFeatureEnvironments (
2022-11-21 10:37:16 +01:00
featureName ,
2023-01-20 10:30:20 +01:00
[ environment ] ,
fixedVariants ,
) ;
return fixedVariants ;
}
2023-01-24 10:43:10 +01:00
async crProtectedSaveVariantsOnEnv (
projectId : string ,
featureName : string ,
environment : string ,
newVariants : IVariant [ ] ,
user : User ,
oldVariants? : IVariant [ ] ,
) : Promise < IVariant [ ] > {
if ( this . flagResolver . isEnabled ( 'crOnVariants' ) ) {
await this . stopWhenChangeRequestsEnabled (
projectId ,
environment ,
user ,
) ;
}
return this . saveVariantsOnEnv (
projectId ,
featureName ,
environment ,
newVariants ,
fix: make sure we have a user in event store (#3072)
## About the changes
Spotted some issues in logs:
```json
{
"level":"warn",
"message":"Failed to store \"feature-environment-variants-updated\" event: error: insert into \"events\" (\"created_by\", \"data\", \"environment\", \"feature_name\", \"pre_data\", \"project\", \"tags\", \"type\") values (DEFAULT, $1, $2, $3, $4, $5, $6, $7) returning \"id\", \"type\", \"created_by\", \"created_at\", \"data\", \"pre_data\", \"tags\", \"feature_name\", \"project\", \"environment\" - null value in column \"created_by\" violates not-null constraint",
"name":"lib/db/event-store.ts"
}
```
In all other events we're doing the following:
https://github.com/Unleash/unleash/blob/b7fdcd36c08fcd9461fef930767606962e73d59c/src/lib/services/segment-service.ts#L80
So this is just mimicking that to quickly release a patch, but I'll look
into a safer (type-checked) solution so this problem does not happen
again
2023-02-09 11:01:39 +01:00
user ,
2023-01-24 10:43:10 +01:00
oldVariants ,
) ;
}
async crProtectedSetVariantsOnEnvs (
projectId : string ,
featureName : string ,
environments : string [ ] ,
newVariants : IVariant [ ] ,
user : User ,
) : Promise < IVariant [ ] > {
if ( this . flagResolver . isEnabled ( 'crOnVariants' ) ) {
for ( const env of environments ) {
await this . stopWhenChangeRequestsEnabled ( projectId , env ) ;
}
}
return this . setVariantsOnEnvs (
projectId ,
featureName ,
environments ,
newVariants ,
fix: make sure we have a user in event store (#3072)
## About the changes
Spotted some issues in logs:
```json
{
"level":"warn",
"message":"Failed to store \"feature-environment-variants-updated\" event: error: insert into \"events\" (\"created_by\", \"data\", \"environment\", \"feature_name\", \"pre_data\", \"project\", \"tags\", \"type\") values (DEFAULT, $1, $2, $3, $4, $5, $6, $7) returning \"id\", \"type\", \"created_by\", \"created_at\", \"data\", \"pre_data\", \"tags\", \"feature_name\", \"project\", \"environment\" - null value in column \"created_by\" violates not-null constraint",
"name":"lib/db/event-store.ts"
}
```
In all other events we're doing the following:
https://github.com/Unleash/unleash/blob/b7fdcd36c08fcd9461fef930767606962e73d59c/src/lib/services/segment-service.ts#L80
So this is just mimicking that to quickly release a patch, but I'll look
into a safer (type-checked) solution so this problem does not happen
again
2023-02-09 11:01:39 +01:00
user ,
2023-01-24 10:43:10 +01:00
) ;
}
2023-01-20 10:30:20 +01:00
async setVariantsOnEnvs (
projectId : string ,
featureName : string ,
environments : string [ ] ,
newVariants : IVariant [ ] ,
fix: make sure we have a user in event store (#3072)
## About the changes
Spotted some issues in logs:
```json
{
"level":"warn",
"message":"Failed to store \"feature-environment-variants-updated\" event: error: insert into \"events\" (\"created_by\", \"data\", \"environment\", \"feature_name\", \"pre_data\", \"project\", \"tags\", \"type\") values (DEFAULT, $1, $2, $3, $4, $5, $6, $7) returning \"id\", \"type\", \"created_by\", \"created_at\", \"data\", \"pre_data\", \"tags\", \"feature_name\", \"project\", \"environment\" - null value in column \"created_by\" violates not-null constraint",
"name":"lib/db/event-store.ts"
}
```
In all other events we're doing the following:
https://github.com/Unleash/unleash/blob/b7fdcd36c08fcd9461fef930767606962e73d59c/src/lib/services/segment-service.ts#L80
So this is just mimicking that to quickly release a patch, but I'll look
into a safer (type-checked) solution so this problem does not happen
again
2023-02-09 11:01:39 +01:00
user : User ,
2023-01-20 10:30:20 +01:00
) : Promise < IVariant [ ] > {
await variantsArraySchema . validateAsync ( newVariants ) ;
const fixedVariants = this . fixVariantWeights ( newVariants ) ;
const oldVariants : { [ env : string ] : IVariant [ ] } = environments . reduce (
async ( result , environment ) = > {
result [ environment ] = await this . featureEnvironmentStore . get ( {
featureName ,
environment ,
} ) ;
return result ;
} ,
{ } ,
) ;
await this . eventStore . batchStore (
environments . map (
( environment ) = >
new EnvironmentVariantEvent ( {
featureName ,
environment ,
project : projectId ,
2023-02-21 15:11:39 +01:00
createdBy : user ,
2023-01-20 10:30:20 +01:00
oldVariants : oldVariants [ environment ] ,
newVariants : fixedVariants ,
} ) ,
) ,
) ;
await this . featureEnvironmentStore . setVariantsToFeatureEnvironments (
featureName ,
environments ,
2022-11-21 10:37:16 +01:00
fixedVariants ,
) ;
return fixedVariants ;
}
2021-11-24 13:08:04 +01:00
fixVariantWeights ( variants : IVariant [ ] ) : IVariant [ ] {
let variableVariants = variants . filter ( ( x ) = > {
return x . weightType === WeightType . VARIABLE ;
} ) ;
if ( variants . length > 0 && variableVariants . length === 0 ) {
throw new BadDataError (
'There must be at least one "variable" variant' ,
) ;
}
let fixedVariants = variants . filter ( ( x ) = > {
return x . weightType === WeightType . FIX ;
} ) ;
let fixedWeights = fixedVariants . reduce ( ( a , v ) = > a + v . weight , 0 ) ;
if ( fixedWeights > 1000 ) {
throw new BadDataError (
'The traffic distribution total must equal 100%' ,
) ;
}
let averageWeight = Math . floor (
( 1000 - fixedWeights ) / variableVariants . length ,
) ;
let remainder = ( 1000 - fixedWeights ) % variableVariants . length ;
variableVariants = variableVariants . map ( ( x ) = > {
x . weight = averageWeight ;
if ( remainder > 0 ) {
x . weight += 1 ;
remainder -- ;
}
return x ;
} ) ;
2022-11-21 10:37:16 +01:00
return variableVariants
. concat ( fixedVariants )
. sort ( ( a , b ) = > a . name . localeCompare ( b . name ) ) ;
2021-11-24 13:08:04 +01:00
}
2022-11-18 09:29:26 +01:00
private async stopWhenChangeRequestsEnabled (
project : string ,
environment : string ,
2022-12-05 15:38:17 +01:00
user? : User ,
2022-11-18 09:29:26 +01:00
) {
2023-03-24 14:31:43 +01:00
const canBypass =
await this . changeRequestAccessReadModel . canBypassChangeRequest (
project ,
environment ,
user ,
) ;
if ( ! canBypass ) {
2022-12-05 15:38:17 +01:00
throw new NoAccessError ( SKIP_CHANGE_REQUEST ) ;
2022-11-18 09:29:26 +01:00
}
}
2022-12-05 12:39:13 +01:00
private async stopWhenCannotCreateStrategies (
project : string ,
environment : string ,
featureName : string ,
2023-03-27 13:21:50 +02:00
user? : User ,
2022-12-05 12:39:13 +01:00
) {
const hasEnvironment =
await this . featureEnvironmentStore . featureHasEnvironment (
environment ,
featureName ,
) ;
if ( hasEnvironment ) {
const strategies = await this . getStrategiesForEnvironment (
project ,
featureName ,
environment ,
) ;
if ( strategies . length === 0 ) {
const canAddStrategies =
user &&
( await this . accessService . hasPermission (
user ,
CREATE_FEATURE_STRATEGY ,
project ,
environment ,
) ) ;
if ( ! canAddStrategies ) {
throw new NoAccessError ( CREATE_FEATURE_STRATEGY ) ;
}
}
}
}
2021-07-07 10:46:50 +02:00
}
2021-11-12 13:15:51 +01:00
export default FeatureToggleService ;