From f91c8a338aa47ca57fc45ef981d1af7462336d66 Mon Sep 17 00:00:00 2001 From: Tymoteusz Czech <2625371+Tymek@users.noreply.github.com> Date: Fri, 14 Jul 2023 16:48:35 +0200 Subject: [PATCH] fix: feature OpenAPI endpoints - project related (#4212) Update OpenAPI for `/api/admin/projects/{projectId}/features/` and related endpoints --------- Co-authored-by: Thomas Heartman --- src/lib/openapi/meta-schema-rules.test.ts | 17 ---- src/lib/openapi/spec/clone-feature-schema.ts | 6 ++ src/lib/openapi/spec/create-feature-schema.ts | 12 +++ .../spec/create-strategy-variant-schema.ts | 3 +- .../spec/environments-project-schema.ts | 3 + src/lib/openapi/spec/environments-schema.ts | 4 + .../spec/feature-environment-schema.ts | 3 + src/lib/openapi/spec/feature-schema.test.ts | 2 +- .../openapi/spec/feature-variants-schema.ts | 4 + src/lib/openapi/spec/features-schema.ts | 4 + src/lib/openapi/spec/patches-schema.ts | 1 + src/lib/openapi/spec/proxy-feature-schema.ts | 2 +- .../spec/set-strategy-sort-order-schema.ts | 6 ++ src/lib/openapi/spec/strategies-schema.ts | 3 + src/lib/openapi/spec/update-feature-schema.ts | 29 +++--- src/lib/openapi/spec/variant-schema.ts | 5 +- src/lib/routes/admin-api/feature.ts | 4 +- .../admin-api/project/project-features.ts | 99 ++++++++++++++----- src/lib/routes/admin-api/project/variants.ts | 42 +++++--- src/lib/services/feature-toggle-service.ts | 1 + src/lib/types/model.ts | 2 +- src/test/e2e/api/admin/playground.e2e.test.ts | 2 +- .../api/admin/project/features.e2e.test.ts | 2 +- .../api/admin/project/variants.e2e.test.ts | 24 ++--- .../feature-toggle-service-v2.e2e.test.ts | 2 +- 25 files changed, 188 insertions(+), 94 deletions(-) diff --git a/src/lib/openapi/meta-schema-rules.test.ts b/src/lib/openapi/meta-schema-rules.test.ts index 9966e32d10..4ca051b562 100644 --- a/src/lib/openapi/meta-schema-rules.test.ts +++ b/src/lib/openapi/meta-schema-rules.test.ts @@ -90,15 +90,8 @@ const metaRules: Rule[] = [ }, }, knownExceptions: [ - 'cloneFeatureSchema', - 'createFeatureSchema', 'createInvitedUserSchema', - 'environmentsSchema', - 'environmentsProjectSchema', - 'featureEnvironmentSchema', - 'featuresSchema', 'featureStrategySegmentSchema', - 'featureVariantsSchema', 'groupSchema', 'groupsSchema', 'groupUserModelSchema', @@ -112,9 +105,7 @@ const metaRules: Rule[] = [ 'sdkContextSchema', 'setUiConfigSchema', 'stateSchema', - 'strategiesSchema', 'uiConfigSchema', - 'updateFeatureSchema', 'upsertContextFieldSchema', 'upsertStrategySchema', 'usersGroupsBaseSchema', @@ -137,31 +128,23 @@ const metaRules: Rule[] = [ 'adminFeaturesQuerySchema', 'applicationSchema', 'applicationsSchema', - 'cloneFeatureSchema', 'createFeatureSchema', 'createInvitedUserSchema', 'dateSchema', - 'environmentsSchema', - 'featuresSchema', 'featureStrategySegmentSchema', - 'featureVariantsSchema', 'groupSchema', 'groupsSchema', 'groupUserModelSchema', 'maintenanceSchema', 'toggleMaintenanceSchema', - 'patchesSchema', 'patchSchema', 'playgroundSegmentSchema', 'playgroundStrategySchema', 'pushVariantsSchema', 'resetPasswordSchema', - 'setStrategySortOrderSchema', 'setUiConfigSchema', 'sortOrderSchema', - 'strategiesSchema', 'uiConfigSchema', - 'updateFeatureSchema', 'upsertContextFieldSchema', 'upsertStrategySchema', 'usersGroupsBaseSchema', diff --git a/src/lib/openapi/spec/clone-feature-schema.ts b/src/lib/openapi/spec/clone-feature-schema.ts index be85d77cc8..f555d41f9e 100644 --- a/src/lib/openapi/spec/clone-feature-schema.ts +++ b/src/lib/openapi/spec/clone-feature-schema.ts @@ -4,12 +4,18 @@ export const cloneFeatureSchema = { $id: '#/components/schemas/cloneFeatureSchema', type: 'object', required: ['name'], + description: 'Copy of a feature with a new name', properties: { name: { type: 'string', + description: 'The name of the new feature', + example: 'new-feature', }, replaceGroupId: { type: 'boolean', + example: true, + description: + 'Whether to use the new feature name as its group ID or not. Group ID is used for calculating [stickiness](https://docs.getunleash.io/reference/stickiness#calculation). Defaults to true.', }, }, components: {}, diff --git a/src/lib/openapi/spec/create-feature-schema.ts b/src/lib/openapi/spec/create-feature-schema.ts index b3a4356da2..96e5be61a8 100644 --- a/src/lib/openapi/spec/create-feature-schema.ts +++ b/src/lib/openapi/spec/create-feature-schema.ts @@ -7,15 +7,27 @@ export const createFeatureSchema = { properties: { name: { type: 'string', + example: 'disable-comments', + description: 'Unique feature name', }, type: { type: 'string', + example: 'release', + description: + "The feature toggle's [type](https://docs.getunleash.io/reference/feature-toggle-types). One of experiment, kill-switch, release, operational, or permission", }, description: { type: 'string', + nullable: true, + example: + 'Controls disabling of the comments section in case of an incident', + description: 'Detailed description of the feature', }, impressionData: { type: 'boolean', + example: false, + description: + '`true` if the impression data collection is enabled for the feature, otherwise `false`.', }, }, components: {}, diff --git a/src/lib/openapi/spec/create-strategy-variant-schema.ts b/src/lib/openapi/spec/create-strategy-variant-schema.ts index 17e7318310..2a93a77847 100644 --- a/src/lib/openapi/spec/create-strategy-variant-schema.ts +++ b/src/lib/openapi/spec/create-strategy-variant-schema.ts @@ -26,6 +26,7 @@ export const createStrategyVariantSchema = { 'Set to `fix` if this variant must have exactly the weight allocated to it. If the type is `variable`, the weight will adjust so that the total weight of all variants adds up to 1000. Refer to the [variant weight documentation](https://docs.getunleash.io/reference/feature-toggle-variants#variant-weight).', type: 'string', example: 'fix', + enum: ['variable', 'fix'], }, stickiness: { type: 'string', @@ -48,7 +49,7 @@ export const createStrategyVariantSchema = { type: 'string', }, }, - example: { type: 'json', value: '{color: red}' }, + example: { type: 'json', value: '{"color": "red"}' }, }, }, components: {}, diff --git a/src/lib/openapi/spec/environments-project-schema.ts b/src/lib/openapi/spec/environments-project-schema.ts index 66747f67de..1f8d38966b 100644 --- a/src/lib/openapi/spec/environments-project-schema.ts +++ b/src/lib/openapi/spec/environments-project-schema.ts @@ -10,12 +10,15 @@ export const environmentsProjectSchema = { properties: { version: { type: 'integer', + example: 1, + description: 'Version of the environments schema', }, environments: { type: 'array', items: { $ref: '#/components/schemas/environmentProjectSchema', }, + description: 'List of environments', }, }, components: { diff --git a/src/lib/openapi/spec/environments-schema.ts b/src/lib/openapi/spec/environments-schema.ts index 6f26ce9cc3..cce7e9bbef 100644 --- a/src/lib/openapi/spec/environments-schema.ts +++ b/src/lib/openapi/spec/environments-schema.ts @@ -6,15 +6,19 @@ export const environmentsSchema = { type: 'object', additionalProperties: false, required: ['version', 'environments'], + description: 'A versioned list of environments', properties: { version: { type: 'integer', + example: 1, + description: 'Version of the environments schema', }, environments: { type: 'array', items: { $ref: '#/components/schemas/environmentSchema', }, + description: 'List of environments', }, }, components: { diff --git a/src/lib/openapi/spec/feature-environment-schema.ts b/src/lib/openapi/spec/feature-environment-schema.ts index c80c9c5d7a..edc1ea6acf 100644 --- a/src/lib/openapi/spec/feature-environment-schema.ts +++ b/src/lib/openapi/spec/feature-environment-schema.ts @@ -20,9 +20,12 @@ export const featureEnvironmentSchema = { featureName: { type: 'string', example: 'disable-comments', + description: 'The name of the feature', }, environment: { type: 'string', + example: 'development', + description: 'The name of the environment', }, type: { type: 'string', diff --git a/src/lib/openapi/spec/feature-schema.test.ts b/src/lib/openapi/spec/feature-schema.test.ts index 2836c7ea2c..191bf8d28f 100644 --- a/src/lib/openapi/spec/feature-schema.test.ts +++ b/src/lib/openapi/spec/feature-schema.test.ts @@ -84,7 +84,7 @@ test('featureSchema variant override values must be an array', () => { { name: 'a', weight: 1, - weightType: 'a', + weightType: 'fix', stickiness: 'a', overrides: [{ contextName: 'a', values: 'b' }], payload: { type: 'a', value: 'b' }, diff --git a/src/lib/openapi/spec/feature-variants-schema.ts b/src/lib/openapi/spec/feature-variants-schema.ts index 608f45135c..5b30d44f5d 100644 --- a/src/lib/openapi/spec/feature-variants-schema.ts +++ b/src/lib/openapi/spec/feature-variants-schema.ts @@ -7,15 +7,19 @@ export const featureVariantsSchema = { type: 'object', additionalProperties: false, required: ['version', 'variants'], + description: 'A versioned collection of feature toggle variants.', properties: { version: { type: 'integer', + example: 1, + description: 'The version of the feature variants schema.', }, variants: { type: 'array', items: { $ref: '#/components/schemas/variantSchema', }, + description: 'All variants defined for a specific feature toggle.', }, }, components: { diff --git a/src/lib/openapi/spec/features-schema.ts b/src/lib/openapi/spec/features-schema.ts index 67416896e1..75c95b7961 100644 --- a/src/lib/openapi/spec/features-schema.ts +++ b/src/lib/openapi/spec/features-schema.ts @@ -14,15 +14,19 @@ export const featuresSchema = { type: 'object', additionalProperties: false, required: ['version', 'features'], + description: 'A list of features', + deprecated: true, properties: { version: { type: 'integer', + description: "The version of the feature's schema", }, features: { type: 'array', items: { $ref: '#/components/schemas/featureSchema', }, + description: 'A list of features', }, }, components: { diff --git a/src/lib/openapi/spec/patches-schema.ts b/src/lib/openapi/spec/patches-schema.ts index b0fa8314ff..8069cd2bdd 100644 --- a/src/lib/openapi/spec/patches-schema.ts +++ b/src/lib/openapi/spec/patches-schema.ts @@ -4,6 +4,7 @@ import { patchSchema } from './patch-schema'; export const patchesSchema = { $id: '#/components/schemas/patchesSchema', type: 'array', + description: 'A list of patches', items: { $ref: '#/components/schemas/patchSchema', }, diff --git a/src/lib/openapi/spec/proxy-feature-schema.ts b/src/lib/openapi/spec/proxy-feature-schema.ts index fedc9ed3a0..c8979b62fc 100644 --- a/src/lib/openapi/spec/proxy-feature-schema.ts +++ b/src/lib/openapi/spec/proxy-feature-schema.ts @@ -46,7 +46,7 @@ export const proxyFeatureSchema = { additionalProperties: false, required: ['type', 'value'], description: 'Extra data configured for this variant', - example: { type: 'json', value: '{color: red}' }, + example: { type: 'json', value: '{"color": "red"}' }, properties: { type: { type: 'string', diff --git a/src/lib/openapi/spec/set-strategy-sort-order-schema.ts b/src/lib/openapi/spec/set-strategy-sort-order-schema.ts index e2574e81f9..28b19bfb3a 100644 --- a/src/lib/openapi/spec/set-strategy-sort-order-schema.ts +++ b/src/lib/openapi/spec/set-strategy-sort-order-schema.ts @@ -3,16 +3,22 @@ import { FromSchema } from 'json-schema-to-ts'; export const setStrategySortOrderSchema = { $id: '#/components/schemas/setStrategySortOrderSchema', type: 'array', + description: 'An array of strategies with their new sort order', items: { type: 'object', additionalProperties: false, required: ['id', 'sortOrder'], + description: 'A strategy with its new sort order', properties: { id: { type: 'string', + example: '9c40958a-daac-400e-98fb-3bb438567008', + description: 'The ID of the strategy', }, sortOrder: { type: 'number', + example: 1, + description: 'The new sort order of the strategy', }, }, }, diff --git a/src/lib/openapi/spec/strategies-schema.ts b/src/lib/openapi/spec/strategies-schema.ts index bd829bf1e1..232c94cf2d 100644 --- a/src/lib/openapi/spec/strategies-schema.ts +++ b/src/lib/openapi/spec/strategies-schema.ts @@ -6,17 +6,20 @@ export const strategiesSchema = { type: 'object', additionalProperties: false, required: ['version', 'strategies'], + description: 'List of strategies', properties: { version: { type: 'integer', enum: [1], example: 1, + description: 'Version of the strategies schema', }, strategies: { type: 'array', items: { $ref: '#/components/schemas/strategySchema', }, + description: 'List of strategies', }, }, components: { diff --git a/src/lib/openapi/spec/update-feature-schema.ts b/src/lib/openapi/spec/update-feature-schema.ts index 26a8aab5a9..4fe02d5275 100644 --- a/src/lib/openapi/spec/update-feature-schema.ts +++ b/src/lib/openapi/spec/update-feature-schema.ts @@ -4,35 +4,36 @@ import { constraintSchema } from './constraint-schema'; export const updateFeatureSchema = { $id: '#/components/schemas/updateFeatureSchema', type: 'object', - required: ['name'], + description: 'Data used for updating a feature toggle', properties: { - name: { - type: 'string', - }, description: { type: 'string', + example: + 'Controls disabling of the comments section in case of an incident', + description: 'Detailed description of the feature', }, type: { type: 'string', + example: 'kill-switch', + description: + 'Type of the toggle e.g. experiment, kill-switch, release, operational, permission', }, stale: { type: 'boolean', + example: true, + description: '`true` if the feature is archived', }, archived: { type: 'boolean', - }, - createdAt: { - type: 'string', - format: 'date-time', + example: true, + description: + 'If `true` the feature toggle will be moved to the [archive](https://docs.getunleash.io/reference/archived-toggles) with a property `archivedAt` set to current time', }, impressionData: { type: 'boolean', - }, - constraints: { - type: 'array', - items: { - $ref: '#/components/schemas/constraintSchema', - }, + example: false, + description: + '`true` if the impression data collection is enabled for the feature', }, }, components: { diff --git a/src/lib/openapi/spec/variant-schema.ts b/src/lib/openapi/spec/variant-schema.ts index 8052d82177..0dd3335490 100644 --- a/src/lib/openapi/spec/variant-schema.ts +++ b/src/lib/openapi/spec/variant-schema.ts @@ -25,7 +25,8 @@ export const variantSchema = { description: 'Set to fix if this variant must have exactly the weight allocated to it. If the type is variable, the weight will adjust so that the total weight of all variants adds up to 1000', type: 'string', - example: 'fix', + example: 'variable', + enum: ['variable', 'fix'], }, stickiness: { type: 'string', @@ -48,7 +49,7 @@ export const variantSchema = { type: 'string', }, }, - example: { type: 'json', value: '{color: red}' }, + example: { type: 'json', value: '{"color": "red"}' }, }, overrides: { description: `Overrides assigning specific variants to specific users. The weighting system automatically assigns users to specific groups for you, but any overrides in this list will take precedence.`, diff --git a/src/lib/routes/admin-api/feature.ts b/src/lib/routes/admin-api/feature.ts index eb2d3f2b35..02b624e220 100644 --- a/src/lib/routes/admin-api/feature.ts +++ b/src/lib/routes/admin-api/feature.ts @@ -69,7 +69,7 @@ class FeatureController extends Controller { 200: createResponseSchema('featuresSchema'), ...getStandardResponses(401, 403), }, - summary: 'Get all features (deprecated)', + summary: 'Get all feature toggles (deprecated)', description: 'Gets all feature toggles with their full configuration. This endpoint is **deprecated**. You should use the project-based endpoint instead (`/api/admin/projects//features`).', deprecated: true, @@ -86,7 +86,7 @@ class FeatureController extends Controller { openApiService.validPath({ tags: ['Features'], operationId: 'validateFeature', - summary: 'Validate feature name', + summary: 'Validate a feature toggle name.', requestBody: createRequestSchema('validateFeatureSchema'), description: 'Validates a feature toggle name: checks whether the name is URL-friendly and whether a feature with the given name already exists. Returns 200 if the feature name is compliant and unused.', diff --git a/src/lib/routes/admin-api/project/project-features.ts b/src/lib/routes/admin-api/project/project-features.ts index f342330225..1cb9cd9973 100644 --- a/src/lib/routes/admin-api/project/project-features.ts +++ b/src/lib/routes/admin-api/project/project-features.ts @@ -52,6 +52,7 @@ import { TransactionCreator, UnleashTransaction, } from '../../../db/transaction'; +import { BadDataError } from '../../../error'; interface FeatureStrategyParams { projectId: string; @@ -152,7 +153,7 @@ export default class ProjectFeaturesController extends Controller { handler: this.getFeatureEnvironment, middleware: [ openApiService.validPath({ - summary: 'Get a feature environment.', + summary: 'Get a feature environment', description: 'Information about the enablement status and strategies for a feature toggle in specified environment.', tags: ['Features'], @@ -173,7 +174,7 @@ export default class ProjectFeaturesController extends Controller { permission: UPDATE_FEATURE_ENVIRONMENT, middleware: [ openApiService.validPath({ - summary: 'Disable a feature toggle.', + summary: 'Disable a feature toggle', description: 'Disable a feature toggle in the specified environment.', tags: ['Features'], @@ -194,7 +195,7 @@ export default class ProjectFeaturesController extends Controller { permission: UPDATE_FEATURE_ENVIRONMENT, middleware: [ openApiService.validPath({ - summary: 'Enable a feature toggle.', + summary: 'Enable a feature toggle', description: 'Enable a feature toggle in the specified environment.', tags: ['Features'], @@ -215,9 +216,9 @@ export default class ProjectFeaturesController extends Controller { middleware: [ openApiService.validPath({ tags: ['Features'], + summary: 'Bulk enable a list of features', description: 'This endpoint enables multiple feature toggles.', - summary: 'Bulk enable a list of features.', operationId: 'bulkToggleFeaturesEnvironmentOn', requestBody: createRequestSchema( 'bulkToggleFeaturesSchema', @@ -238,9 +239,9 @@ export default class ProjectFeaturesController extends Controller { middleware: [ openApiService.validPath({ tags: ['Features'], + summary: 'Bulk disable a list of features', description: 'This endpoint disables multiple feature toggles.', - summary: 'Bulk disabled a list of features.', operationId: 'bulkToggleFeaturesEnvironmentOff', requestBody: createRequestSchema( 'bulkToggleFeaturesSchema', @@ -261,7 +262,7 @@ export default class ProjectFeaturesController extends Controller { middleware: [ openApiService.validPath({ tags: ['Features'], - summary: 'Get feature toggle strategies.', + summary: 'Get feature toggle strategies', operationId: 'getFeatureStrategies', description: 'Get strategies defined for a feature toggle in the specified environment.', @@ -281,7 +282,7 @@ export default class ProjectFeaturesController extends Controller { middleware: [ openApiService.validPath({ tags: ['Features'], - summary: 'Add a strategy to a feature toggle.', + summary: 'Add a strategy to a feature toggle', description: 'Add a strategy to a feature toggle in the specified environment.', operationId: 'addFeatureStrategy', @@ -304,12 +305,13 @@ export default class ProjectFeaturesController extends Controller { middleware: [ openApiService.validPath({ tags: ['Features'], - summary: 'Get a strategy configuration.', + summary: 'Get a strategy configuration', description: 'Get a strategy configuration for an environment in a feature toggle.', operationId: 'getFeatureStrategy', responses: { 200: createResponseSchema('featureStrategySchema'), + ...getStandardResponses(401, 403, 404), }, }), ], @@ -323,14 +325,14 @@ export default class ProjectFeaturesController extends Controller { middleware: [ openApiService.validPath({ tags: ['Features'], - summary: 'Set the order of strategies on the list.', + summary: 'Set the order of strategies on the list', operationId: 'setStrategySortOrder', requestBody: createRequestSchema( 'setStrategySortOrderSchema', ), responses: { 200: emptyResponse, - ...getStandardResponses(401, 403), + ...getStandardResponses(400, 401, 403), }, }), ], @@ -344,7 +346,7 @@ export default class ProjectFeaturesController extends Controller { middleware: [ openApiService.validPath({ tags: ['Features'], - summary: 'Update a strategy.', + summary: 'Update a strategy', description: 'Replace strategy configuration for a feature toggle in the specified environment.', operationId: 'updateFeatureStrategy', @@ -353,6 +355,7 @@ export default class ProjectFeaturesController extends Controller { ), responses: { 200: createResponseSchema('featureStrategySchema'), + ...getStandardResponses(400, 401, 403, 404, 415), }, }), ], @@ -366,13 +369,14 @@ export default class ProjectFeaturesController extends Controller { middleware: [ openApiService.validPath({ tags: ['Features'], - summary: 'Change specific properties of a strategy.', + summary: 'Change specific properties of a strategy', description: 'Change specific properties of a strategy configuration in a feature toggle.', operationId: 'patchFeatureStrategy', requestBody: createRequestSchema('patchesSchema'), responses: { 200: createResponseSchema('featureStrategySchema'), + ...getStandardResponses(400, 401, 403, 404, 415), }, }), ], @@ -387,7 +391,7 @@ export default class ProjectFeaturesController extends Controller { middleware: [ openApiService.validPath({ tags: ['Features'], - summary: 'Delete a strategy from a feature toggle.', + summary: 'Delete a strategy from a feature toggle', description: 'Delete a strategy configuration from a feature toggle in the specified environment.', operationId: 'deleteFeatureStrategy', @@ -406,9 +410,15 @@ export default class ProjectFeaturesController extends Controller { permission: NONE, middleware: [ openApiService.validPath({ + summary: 'Get all features in a project', + description: + 'A list of all features for the specified project.', tags: ['Features'], operationId: 'getFeatures', - responses: { 200: createResponseSchema('featuresSchema') }, + responses: { + 200: createResponseSchema('featuresSchema'), + ...getStandardResponses(400, 401, 403), + }, }), ], }); @@ -420,10 +430,16 @@ export default class ProjectFeaturesController extends Controller { permission: CREATE_FEATURE, middleware: [ openApiService.validPath({ + summary: 'Add a new feature toggle', + description: + 'Create a new feature toggle in a specified project.', tags: ['Features'], operationId: 'createFeature', requestBody: createRequestSchema('createFeatureSchema'), - responses: { 200: createResponseSchema('featureSchema') }, + responses: { + 200: createResponseSchema('featureSchema'), + ...getStandardResponses(401, 403, 404, 415), + }, }), ], }); @@ -436,10 +452,16 @@ export default class ProjectFeaturesController extends Controller { permission: CREATE_FEATURE, middleware: [ openApiService.validPath({ + summary: 'Clone a feature toggle', + description: + 'Creates a copy of the specified feature toggle. The copy can be created in any project.', tags: ['Features'], operationId: 'cloneFeature', requestBody: createRequestSchema('cloneFeatureSchema'), - responses: { 200: createResponseSchema('featureSchema') }, + responses: { + 200: createResponseSchema('featureSchema'), + ...getStandardResponses(401, 403, 404, 415), + }, }), ], }); @@ -453,16 +475,16 @@ export default class ProjectFeaturesController extends Controller { openApiService.validPath({ operationId: 'getFeature', tags: ['Features'], + summary: 'Get a feature', description: 'This endpoint returns the information about the requested feature if the feature belongs to the specified project.', - summary: 'Get a feature.', responses: { 200: createResponseSchema('featureSchema'), 403: { description: 'You either do not have the required permissions or used an invalid URL.', }, - ...getStandardResponses(401, 404), + ...getStandardResponses(401, 403, 404), }, }), ], @@ -477,8 +499,14 @@ export default class ProjectFeaturesController extends Controller { openApiService.validPath({ tags: ['Features'], operationId: 'updateFeature', + summary: 'Update a feature toggle', + description: + 'Updates the specified feature if the feature belongs to the specified project. Only the provided properties are updated; any feature properties left out of the request body are left untouched.', requestBody: createRequestSchema('updateFeatureSchema'), - responses: { 200: createResponseSchema('featureSchema') }, + responses: { + 200: createResponseSchema('featureSchema'), + ...getStandardResponses(401, 403, 404, 415), + }, }), ], }); @@ -492,8 +520,14 @@ export default class ProjectFeaturesController extends Controller { openApiService.validPath({ tags: ['Features'], operationId: 'patchFeature', + summary: 'Modify a feature toggle', + description: + 'Change specific properties of a feature toggle.', requestBody: createRequestSchema('patchesSchema'), - responses: { 200: createResponseSchema('featureSchema') }, + responses: { + 200: createResponseSchema('featureSchema'), + ...getStandardResponses(401, 403, 404, 415), + }, }), ], }); @@ -508,16 +542,16 @@ export default class ProjectFeaturesController extends Controller { openApiService.validPath({ tags: ['Features'], operationId: 'archiveFeature', + summary: 'Archive a feature toggle', description: 'This endpoint archives the specified feature if the feature belongs to the specified project.', - summary: 'Archive a feature.', responses: { 202: emptyResponse, 403: { description: 'You either do not have the required permissions or used an invalid URL.', }, - ...getStandardResponses(401, 404), + ...getStandardResponses(401, 403, 404), }, }), ], @@ -552,8 +586,14 @@ export default class ProjectFeaturesController extends Controller { openApiService.validPath({ tags: ['Tags'], operationId: 'addTagToFeatures', + summary: 'Adds a tag to the specified features', + description: + 'Add a tag to a list of features. Create tags if needed.', requestBody: createRequestSchema('tagsBulkAddSchema'), - responses: { 200: emptyResponse }, + responses: { + 200: emptyResponse, + ...getStandardResponses(401, 403, 404, 415), + }, }), ], }); @@ -640,7 +680,10 @@ export default class ProjectFeaturesController extends Controller { const userName = extractUsername(req); const created = await this.featureService.createFeatureToggle( projectId, - req.body, + { + ...req.body, + description: req.body.description || undefined, + }, userName, ); @@ -680,9 +723,15 @@ export default class ProjectFeaturesController extends Controller { const { projectId, featureName } = req.params; const { createdAt, ...data } = req.body; const userName = extractUsername(req); + if (data.name && data.name !== featureName) { + throw new BadDataError('Cannot change name of feature toggle'); + } const created = await this.featureService.updateFeatureToggle( projectId, - data, + { + ...data, + name: featureName, + }, userName, featureName, ); diff --git a/src/lib/routes/admin-api/project/variants.ts b/src/lib/routes/admin-api/project/variants.ts index f076fae2b1..2f62494f39 100644 --- a/src/lib/routes/admin-api/project/variants.ts +++ b/src/lib/routes/admin-api/project/variants.ts @@ -20,6 +20,7 @@ import { AccessService } from '../../../services'; import { BadDataError, PermissionError } from '../../../../lib/error'; import { User } from 'lib/server-impl'; import { PushVariantsSchema } from 'lib/openapi/spec/push-variants-schema'; +import { getStandardResponses } from '../../../openapi'; const PREFIX = '/:projectId/features/:featureName/variants'; const ENV_PREFIX = @@ -73,6 +74,7 @@ export default class VariantsController extends Controller { operationId: 'getFeatureVariants', responses: { 200: createResponseSchema('featureVariantsSchema'), + ...getStandardResponses(401, 403, 404), }, }), ], @@ -87,13 +89,14 @@ export default class VariantsController extends Controller { summary: "Apply a patch to a feature's variants (in all environments).", description: `Apply a list of patches patch to the specified feature's variants. The patch objects should conform to the [JSON-patch format (RFC 6902)](https://www.rfc-editor.org/rfc/rfc6902). - - ⚠️ **Warning**: This method is not atomic. If something fails in the middle of applying the patch, you can be left with a half-applied patch. We recommend that you instead [patch variants on a per-environment basis](/docs/reference/api/unleash/patch-environments-feature-variants.api.mdx), which **is** an atomic operation.`, + +⚠️ **Warning**: This method is not atomic. If something fails in the middle of applying the patch, you can be left with a half-applied patch. We recommend that you instead [patch variants on a per-environment basis](/docs/reference/api/unleash/patch-environments-feature-variants.api.mdx), which **is** an atomic operation.`, tags: ['Features'], operationId: 'patchFeatureVariants', requestBody: createRequestSchema('patchesSchema'), responses: { 200: createResponseSchema('featureVariantsSchema'), + ...getStandardResponses(400, 401, 403, 404), }, }), ], @@ -109,17 +112,18 @@ export default class VariantsController extends Controller { 'Create (overwrite) variants for a feature toggle in all environments', description: `This overwrites the current variants for the feature specified in the :featureName parameter in all environments. - The backend will validate the input for the following invariants +The backend will validate the input for the following invariants - * If there are variants, there needs to be at least one variant with \`weightType: variable\` - * The sum of the weights of variants with \`weightType: fix\` must be strictly less than 1000 (< 1000) +* If there are variants, there needs to be at least one variant with \`weightType: variable\` +* The sum of the weights of variants with \`weightType: fix\` must be strictly less than 1000 (< 1000) - The backend will also distribute remaining weight up to 1000 after adding the variants with \`weightType: fix\` together amongst the variants of \`weightType: variable\``, +The backend will also distribute remaining weight up to 1000 after adding the variants with \`weightType: fix\` together amongst the variants of \`weightType: variable\``, tags: ['Features'], operationId: 'overwriteFeatureVariants', requestBody: createRequestSchema('variantsSchema'), responses: { 200: createResponseSchema('featureVariantsSchema'), + ...getStandardResponses(400, 401, 403, 404), }, }), ], @@ -137,6 +141,7 @@ export default class VariantsController extends Controller { operationId: 'getEnvironmentFeatureVariants', responses: { 200: createResponseSchema('featureVariantsSchema'), + ...getStandardResponses(401, 403, 404), }, }), ], @@ -155,6 +160,7 @@ export default class VariantsController extends Controller { requestBody: createRequestSchema('patchesSchema'), responses: { 200: createResponseSchema('featureVariantsSchema'), + ...getStandardResponses(400, 401, 403, 404), }, }), ], @@ -169,18 +175,19 @@ export default class VariantsController extends Controller { summary: 'Create (overwrite) variants for a feature in an environment', description: `This overwrites the current variants for the feature toggle in the :featureName parameter for the :environment parameter. - - The backend will validate the input for the following invariants: - - * If there are variants, there needs to be at least one variant with \`weightType: variable\` - * The sum of the weights of variants with \`weightType: fix\` must be strictly less than 1000 (< 1000) - The backend will also distribute remaining weight up to 1000 after adding the variants with \`weightType: fix\` together amongst the variants of \`weightType: variable\``, +The backend will validate the input for the following invariants: + +* If there are variants, there needs to be at least one variant with \`weightType: variable\` +* The sum of the weights of variants with \`weightType: fix\` must be strictly less than 1000 (< 1000) + +The backend will also distribute remaining weight up to 1000 after adding the variants with \`weightType: fix\` together amongst the variants of \`weightType: variable\``, tags: ['Features'], operationId: 'overwriteEnvironmentFeatureVariants', requestBody: createRequestSchema('variantsSchema'), responses: { 200: createResponseSchema('featureVariantsSchema'), + ...getStandardResponses(400, 401, 403), }, }), ], @@ -194,9 +201,14 @@ export default class VariantsController extends Controller { openApiService.validPath({ tags: ['Features'], operationId: 'overwriteFeatureVariantsOnEnvironments', + summary: + 'Create (overwrite) variants for a feature toggle in multiple environments', + description: + 'This overwrites the current variants for the feature toggle in the :featureName parameter for the :environment parameter.', requestBody: createRequestSchema('pushVariantsSchema'), responses: { 200: createResponseSchema('featureVariantsSchema'), + ...getStandardResponses(400, 401, 403), }, }), ], @@ -230,7 +242,7 @@ export default class VariantsController extends Controller { ); res.status(200).json({ version: 1, - variants: updatedFeature.variants, + variants: updatedFeature.variants || [], }); } @@ -248,7 +260,7 @@ export default class VariantsController extends Controller { ); res.status(200).json({ version: 1, - variants: updatedFeature.variants, + variants: updatedFeature.variants || [], }); } @@ -275,7 +287,7 @@ export default class VariantsController extends Controller { UPDATE_FEATURE_ENVIRONMENT_VARIANTS, ); - const variantsWithDefaults = variants.map((variant) => ({ + const variantsWithDefaults = (variants || []).map((variant) => ({ weightType: WeightType.VARIABLE, stickiness: 'default', ...variant, diff --git a/src/lib/services/feature-toggle-service.ts b/src/lib/services/feature-toggle-service.ts index 987542c13b..57eb4891a1 100644 --- a/src/lib/services/feature-toggle-service.ts +++ b/src/lib/services/feature-toggle-service.ts @@ -1127,6 +1127,7 @@ class FeatureToggleService { segments: [], title: strategy.title, disabled: strategy.disabled, + // FIXME: Should we return sortOrder here, or adjust OpenAPI? }; if (segments && segments.length > 0) { diff --git a/src/lib/types/model.ts b/src/lib/types/model.ts index e13a18100c..a7448988c2 100644 --- a/src/lib/types/model.ts +++ b/src/lib/types/model.ts @@ -120,7 +120,7 @@ export interface IFeatureEnvironment { export interface IVariant { name: string; weight: number; - weightType: string; + weightType: 'variable' | 'fix'; payload?: { type: string; value: string; diff --git a/src/test/e2e/api/admin/playground.e2e.test.ts b/src/test/e2e/api/admin/playground.e2e.test.ts index d2c9fa2943..f6ac7afa12 100644 --- a/src/test/e2e/api/admin/playground.e2e.test.ts +++ b/src/test/e2e/api/admin/playground.e2e.test.ts @@ -663,7 +663,7 @@ describe('Playground API E2E', () => { { name: 'a', weight: 1000, - weightType: 'variable', + weightType: 'variable' as const, stickiness: 'default', overrides: [], }, diff --git a/src/test/e2e/api/admin/project/features.e2e.test.ts b/src/test/e2e/api/admin/project/features.e2e.test.ts index 441c138a30..6db66a3fc2 100644 --- a/src/test/e2e/api/admin/project/features.e2e.test.ts +++ b/src/test/e2e/api/admin/project/features.e2e.test.ts @@ -2370,7 +2370,7 @@ test('should handle strategy variants', async () => { const variant = { name: 'variantName', weight: 1, - weightType: 'variable', + weightType: 'variable' as const, stickiness: 'default', }; const updatedVariant1 = { diff --git a/src/test/e2e/api/admin/project/variants.e2e.test.ts b/src/test/e2e/api/admin/project/variants.e2e.test.ts index 6f7762d08e..be72d6df39 100644 --- a/src/test/e2e/api/admin/project/variants.e2e.test.ts +++ b/src/test/e2e/api/admin/project/variants.e2e.test.ts @@ -179,7 +179,7 @@ test('Can patch variants for a feature patches all environments independently', path: '/1', value: { name: addedVariantName, - weightType: 'fix', + weightType: WeightType.FIX, weight: 50, }, }, @@ -509,7 +509,7 @@ test('PUTing an invalid variant throws 400 exception', async () => { .expect((res) => { expect(res.body.details).toHaveLength(1); expect(res.body.details[0].description).toMatch( - /.*weightType" must be one of/, + /.*weightType property should be equal to one of the allowed values/, ); }); }); @@ -1002,13 +1002,13 @@ test('PUT endpoint validates uniqueness of variant names', async () => { .send([ { name: 'variant1', - weightType: 'variable', + weightType: WeightType.VARIABLE, weight: 500, stickiness: 'default', }, { name: 'variant1', - weightType: 'variable', + weightType: WeightType.VARIABLE, weight: 500, stickiness: 'default', }, @@ -1038,25 +1038,25 @@ test('Variants should be sorted by their name when PUT', async () => { .send([ { name: 'zvariant', - weightType: 'variable', + weightType: WeightType.VARIABLE, weight: 500, stickiness: 'default', }, { name: 'variant-a', - weightType: 'variable', + weightType: WeightType.VARIABLE, weight: 500, stickiness: 'default', }, { name: 'g-variant', - weightType: 'variable', + weightType: WeightType.VARIABLE, weight: 500, stickiness: 'default', }, { name: 'variant-g', - weightType: 'variable', + weightType: WeightType.VARIABLE, weight: 500, stickiness: 'default', }, @@ -1086,13 +1086,13 @@ test('Variants should be sorted by name when PATCHed as well', async () => { const observer = jsonpatch.observe(variants); variants.push({ name: 'g-variant', - weightType: 'variable', + weightType: WeightType.VARIABLE, weight: 500, stickiness: 'default', }); variants.push({ name: 'a-variant', - weightType: 'variable', + weightType: WeightType.VARIABLE, weight: 500, stickiness: 'default', }); @@ -1107,13 +1107,13 @@ test('Variants should be sorted by name when PATCHed as well', async () => { }); variants.push({ name: '00-variant', - weightType: 'variable', + weightType: WeightType.VARIABLE, weight: 500, stickiness: 'default', }); variants.push({ name: 'z-variant', - weightType: 'variable', + weightType: WeightType.VARIABLE, weight: 500, stickiness: 'default', }); diff --git a/src/test/e2e/services/feature-toggle-service-v2.e2e.test.ts b/src/test/e2e/services/feature-toggle-service-v2.e2e.test.ts index f47e0aba3d..5c0dfcbe04 100644 --- a/src/test/e2e/services/feature-toggle-service-v2.e2e.test.ts +++ b/src/test/e2e/services/feature-toggle-service-v2.e2e.test.ts @@ -564,7 +564,7 @@ test('If CRs are protected for any environment in the project stops bulk update { name: 'cr-enabled-2', weight: 500, - weightType: 'fix', + weightType: 'fix' as const, stickiness: 'default', }, ];