From 1f3cc3917e70eba5b2f7963931833c31f61abf65 Mon Sep 17 00:00:00 2001 From: Mateusz Kwasniewski Date: Fri, 23 Aug 2024 12:59:39 +0200 Subject: [PATCH] fix: split features schema into archived and project features (#7973) --- .../archive-feature-toggle-controller.ts | 20 ++-- .../feature-toggle-controller.ts | 21 +--- .../openapi/spec/archived-feature-schema.ts | 102 ++++++++++++++++++ .../openapi/spec/archived-features-schema.ts | 31 ++++++ src/lib/openapi/spec/features-schema.test.ts | 13 --- src/lib/openapi/spec/features-schema.ts | 49 --------- src/lib/openapi/spec/index.ts | 6 +- .../project-feature-environment-schema.ts | 68 ++++++++++++ .../openapi/spec/project-feature-schema.ts | 98 +++++++++++++++++ .../openapi/spec/project-features-schema.ts | 35 ++++++ 10 files changed, 354 insertions(+), 89 deletions(-) create mode 100644 src/lib/openapi/spec/archived-feature-schema.ts create mode 100644 src/lib/openapi/spec/archived-features-schema.ts delete mode 100644 src/lib/openapi/spec/features-schema.test.ts delete mode 100644 src/lib/openapi/spec/features-schema.ts create mode 100644 src/lib/openapi/spec/project-feature-environment-schema.ts create mode 100644 src/lib/openapi/spec/project-feature-schema.ts create mode 100644 src/lib/openapi/spec/project-features-schema.ts diff --git a/src/lib/features/feature-toggle/archive-feature-toggle-controller.ts b/src/lib/features/feature-toggle/archive-feature-toggle-controller.ts index d19ff163bd..7fb5135953 100644 --- a/src/lib/features/feature-toggle/archive-feature-toggle-controller.ts +++ b/src/lib/features/feature-toggle/archive-feature-toggle-controller.ts @@ -9,10 +9,6 @@ import { import { DELETE_FEATURE, NONE, UPDATE_FEATURE } from '../../types/permissions'; import type FeatureToggleService from './feature-toggle-service'; import type { IAuthRequest } from '../../routes/unleash-types'; -import { - featuresSchema, - type FeaturesSchema, -} from '../../openapi/spec/features-schema'; import { serializeDates } from '../../types/serialize-dates'; import type { OpenApiService } from '../../services/openapi-service'; import { createResponseSchema } from '../../openapi/util/create-response-schema'; @@ -24,6 +20,10 @@ import type { TransactionCreator, UnleashTransaction, } from '../../db/transaction'; +import { + archivedFeaturesSchema, + type ArchivedFeaturesSchema, +} from '../../openapi'; export default class ArchiveController extends Controller { private featureService: FeatureToggleService; @@ -67,7 +67,7 @@ export default class ArchiveController extends Controller { 'Retrieve a list of all [archived feature flags](https://docs.getunleash.io/reference/archived-toggles).', operationId: 'getArchivedFeatures', responses: { - 200: createResponseSchema('featuresSchema'), + 200: createResponseSchema('archivedFeaturesSchema'), ...getStandardResponses(401, 403), }, @@ -89,7 +89,7 @@ export default class ArchiveController extends Controller { description: 'Retrieves a list of archived features that belong to the provided project.', responses: { - 200: createResponseSchema('featuresSchema'), + 200: createResponseSchema('archivedFeaturesSchema'), ...getStandardResponses(401, 403), }, @@ -143,7 +143,7 @@ export default class ArchiveController extends Controller { async getArchivedFeatures( req: IAuthRequest, - res: Response, + res: Response, ): Promise { const { user } = req; const features = await this.featureService.getAllArchivedFeatures( @@ -153,14 +153,14 @@ export default class ArchiveController extends Controller { this.openApiService.respondWithValidation( 200, res, - featuresSchema.$id, + archivedFeaturesSchema.$id, { version: 2, features: serializeDates(features) }, ); } async getArchivedFeaturesByProjectId( req: Request<{ projectId: string }, any, any, any>, - res: Response, + res: Response, ): Promise { const { projectId } = req.params; const features = @@ -171,7 +171,7 @@ export default class ArchiveController extends Controller { this.openApiService.respondWithValidation( 200, res, - featuresSchema.$id, + archivedFeaturesSchema.$id, { version: 2, features: serializeDates(features) }, ); } diff --git a/src/lib/features/feature-toggle/feature-toggle-controller.ts b/src/lib/features/feature-toggle/feature-toggle-controller.ts index 58823ee511..fabe2591a5 100644 --- a/src/lib/features/feature-toggle/feature-toggle-controller.ts +++ b/src/lib/features/feature-toggle/feature-toggle-controller.ts @@ -30,12 +30,11 @@ import { type FeatureEnvironmentSchema, featureSchema, type FeatureSchema, - featuresSchema, - type FeaturesSchema, featureStrategySchema, type FeatureStrategySchema, getStandardResponses, - type ParametersSchema, + projectFeaturesSchema, + type ProjectFeaturesSchema, type SetStrategySortOrderSchema, type TagsBulkAddSchema, type TagSchema, @@ -421,7 +420,7 @@ export default class ProjectFeaturesController extends Controller { tags: ['Features'], operationId: 'getFeatures', responses: { - 200: createResponseSchema('featuresSchema'), + 200: createResponseSchema('projectFeaturesSchema'), ...getStandardResponses(400, 401, 403), }, }), @@ -606,7 +605,7 @@ export default class ProjectFeaturesController extends Controller { async getFeatures( req: IAuthRequest, - res: Response, + res: Response, ): Promise { const { projectId } = req.params; const query = await this.prepQuery(req.query, projectId); @@ -617,7 +616,7 @@ export default class ProjectFeaturesController extends Controller { this.openApiService.respondWithValidation( 200, res, - featuresSchema.$id, + projectFeaturesSchema.$id, { version: 2, features: serializeDates(features) }, ); } @@ -1155,14 +1154,4 @@ export default class ProjectFeaturesController extends Controller { ); res.status(200).end(); } - - async getStrategyParameters( - req: Request, - res: Response, - ): Promise { - this.logger.info('Getting strategy parameters'); - const { strategyId } = req.params; - const strategy = await this.featureService.getStrategy(strategyId); - res.status(200).json(strategy.parameters); - } } diff --git a/src/lib/openapi/spec/archived-feature-schema.ts b/src/lib/openapi/spec/archived-feature-schema.ts new file mode 100644 index 0000000000..857209897e --- /dev/null +++ b/src/lib/openapi/spec/archived-feature-schema.ts @@ -0,0 +1,102 @@ +import type { FromSchema } from 'json-schema-to-ts'; + +export const archivedFeatureSchema = { + $id: '#/components/schemas/archivedFeatureSchema', + type: 'object', + additionalProperties: false, + required: ['name', 'project'], + description: 'An archived project feature flag definition', + properties: { + name: { + type: 'string', + example: 'disable-comments', + description: 'Unique feature name', + }, + type: { + type: 'string', + example: 'kill-switch', + description: + 'Type of the flag e.g. experiment, kill-switch, release, operational, permission', + }, + description: { + type: 'string', + nullable: true, + example: + 'Controls disabling of the comments section in case of an incident', + description: 'Detailed description of the feature', + }, + project: { + type: 'string', + example: 'dx-squad', + description: 'Name of the project the feature belongs to', + }, + stale: { + type: 'boolean', + example: false, + description: + '`true` if the feature is stale based on the age and feature type, otherwise `false`.', + }, + impressionData: { + type: 'boolean', + example: false, + description: + '`true` if the impression data collection is enabled for the feature, otherwise `false`.', + }, + createdAt: { + type: 'string', + format: 'date-time', + example: '2023-01-28T15:21:39.975Z', + description: 'The date the feature was created', + }, + archivedAt: { + type: 'string', + format: 'date-time', + example: '2023-01-29T15:21:39.975Z', + description: 'The date the feature was archived', + }, + lastSeenAt: { + type: 'string', + format: 'date-time', + nullable: true, + deprecated: true, + example: '2023-01-28T16:21:39.975Z', + description: + 'The date when metrics where last collected for the feature. This field was deprecated in v5, use the one in featureEnvironmentSchema', + }, + environments: { + type: 'array', + deprecated: true, + description: + 'The list of environments where the feature can be used', + items: { + type: 'object', + properties: { + name: { + type: 'string', + example: 'my-dev-env', + description: 'The name of the environment', + }, + lastSeenAt: { + type: 'string', + format: 'date-time', + nullable: true, + example: '2023-01-28T16:21:39.975Z', + description: + 'The date when metrics where last collected for the feature environment', + }, + enabled: { + type: 'boolean', + example: true, + description: + '`true` if the feature is enabled for the environment, otherwise `false`.', + }, + }, + }, + }, + }, + components: { + schemas: {}, + }, +} as const; + +export type ArchivedFeatureSchema = FromSchema; diff --git a/src/lib/openapi/spec/archived-features-schema.ts b/src/lib/openapi/spec/archived-features-schema.ts new file mode 100644 index 0000000000..fbfffab30c --- /dev/null +++ b/src/lib/openapi/spec/archived-features-schema.ts @@ -0,0 +1,31 @@ +import type { FromSchema } from 'json-schema-to-ts'; +import { archivedFeatureSchema } from './archived-feature-schema'; + +export const archivedFeaturesSchema = { + $id: '#/components/schemas/archivedFeaturesSchema', + type: 'object', + additionalProperties: false, + required: ['version', 'features'], + description: 'A list of archived features', + deprecated: true, + properties: { + version: { + type: 'integer', + description: "The version of the feature's schema", + }, + features: { + type: 'array', + items: { + $ref: '#/components/schemas/archivedFeatureSchema', + }, + description: 'A list of features', + }, + }, + components: { + schemas: { + archivedFeatureSchema, + }, + }, +} as const; + +export type ArchivedFeaturesSchema = FromSchema; diff --git a/src/lib/openapi/spec/features-schema.test.ts b/src/lib/openapi/spec/features-schema.test.ts deleted file mode 100644 index 2534f3e430..0000000000 --- a/src/lib/openapi/spec/features-schema.test.ts +++ /dev/null @@ -1,13 +0,0 @@ -import { validateSchema } from '../validate'; -import type { FeaturesSchema } from './features-schema'; - -test('featuresSchema', () => { - const data: FeaturesSchema = { - version: 1, - features: [], - }; - - expect( - validateSchema('#/components/schemas/featuresSchema', data), - ).toBeUndefined(); -}); diff --git a/src/lib/openapi/spec/features-schema.ts b/src/lib/openapi/spec/features-schema.ts deleted file mode 100644 index afaadd2d3e..0000000000 --- a/src/lib/openapi/spec/features-schema.ts +++ /dev/null @@ -1,49 +0,0 @@ -import type { FromSchema } from 'json-schema-to-ts'; -import { featureSchema } from './feature-schema'; -import { parametersSchema } from './parameters-schema'; -import { variantSchema } from './variant-schema'; -import { overrideSchema } from './override-schema'; -import { constraintSchema } from './constraint-schema'; -import { featureStrategySchema } from './feature-strategy-schema'; -import { environmentSchema } from './environment-schema'; -import { featureEnvironmentSchema } from './feature-environment-schema'; -import { strategyVariantSchema } from './strategy-variant-schema'; -import { tagSchema } from './tag-schema'; - -export const featuresSchema = { - $id: '#/components/schemas/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: { - schemas: { - constraintSchema, - environmentSchema, - featureSchema, - overrideSchema, - featureEnvironmentSchema, - featureStrategySchema, - strategyVariantSchema, - parametersSchema, - variantSchema, - tagSchema, - }, - }, -} as const; - -export type FeaturesSchema = FromSchema; diff --git a/src/lib/openapi/spec/index.ts b/src/lib/openapi/spec/index.ts index c5a9c254eb..a18706ab4e 100644 --- a/src/lib/openapi/spec/index.ts +++ b/src/lib/openapi/spec/index.ts @@ -24,6 +24,8 @@ export * from './application-overview-schema'; export * from './application-schema'; export * from './application-usage-schema'; export * from './applications-schema'; +export * from './archived-feature-schema'; +export * from './archived-features-schema'; export * from './batch-features-schema'; export * from './batch-stale-schema'; export * from './bulk-metrics-schema'; @@ -93,7 +95,6 @@ export * from './feature-type-schema'; export * from './feature-types-schema'; export * from './feature-usage-schema'; export * from './feature-variants-schema'; -export * from './features-schema'; export * from './feedback-create-schema'; export * from './feedback-response-schema'; export * from './feedback-update-schema'; @@ -144,6 +145,9 @@ export * from './project-application-sdk-schema'; export * from './project-applications-schema'; export * from './project-dora-metrics-schema'; export * from './project-environment-schema'; +export * from './project-feature-environment-schema'; +export * from './project-feature-schema'; +export * from './project-features-schema'; export * from './project-flag-creators-schema'; export * from './project-insights-schema'; export * from './project-overview-schema'; diff --git a/src/lib/openapi/spec/project-feature-environment-schema.ts b/src/lib/openapi/spec/project-feature-environment-schema.ts new file mode 100644 index 0000000000..ca36e3d139 --- /dev/null +++ b/src/lib/openapi/spec/project-feature-environment-schema.ts @@ -0,0 +1,68 @@ +import type { FromSchema } from 'json-schema-to-ts'; + +export const projectFeatureEnvironmentSchema = { + $id: '#/components/schemas/projectFeatureEnvironmentSchema', + type: 'object', + additionalProperties: false, + required: [ + 'name', + 'type', + 'enabled', + 'sortOrder', + 'variantCount', + 'lastSeenAt', + ], + description: 'A detailed description of the feature environment', + properties: { + name: { + type: 'string', + example: 'my-dev-env', + description: 'The name of the environment', + }, + type: { + type: 'string', + example: 'development', + description: 'The type of the environment', + }, + enabled: { + type: 'boolean', + example: true, + description: + '`true` if the feature is enabled for the environment, otherwise `false`.', + }, + sortOrder: { + type: 'number', + example: 3, + description: + 'The sort order of the feature environment in the feature environments list', + }, + variantCount: { + type: 'number', + description: 'The number of defined variants', + }, + lastSeenAt: { + type: 'string', + format: 'date-time', + nullable: true, + example: '2023-01-28T16:21:39.975Z', + description: + 'The date when metrics where last collected for the feature environment', + }, + hasStrategies: { + type: 'boolean', + description: 'Whether the feature has any strategies defined.', + }, + hasEnabledStrategies: { + type: 'boolean', + description: + 'Whether the feature has any enabled strategies defined.', + }, + }, + components: { + schemas: {}, + }, +} as const; + +export type ProjectFeatureEnvironmentSchema = FromSchema< + typeof projectFeatureEnvironmentSchema +>; diff --git a/src/lib/openapi/spec/project-feature-schema.ts b/src/lib/openapi/spec/project-feature-schema.ts new file mode 100644 index 0000000000..e477a6bfd3 --- /dev/null +++ b/src/lib/openapi/spec/project-feature-schema.ts @@ -0,0 +1,98 @@ +import type { FromSchema } from 'json-schema-to-ts'; +import { tagSchema } from './tag-schema'; +import { projectFeatureEnvironmentSchema } from './project-feature-environment-schema'; + +export const projectFeatureSchema = { + $id: '#/components/schemas/projectFeatureSchema', + type: 'object', + additionalProperties: false, + required: [ + 'name', + 'type', + 'description', + 'stale', + 'favorite', + 'impressionData', + 'createdAt', + 'lastSeenAt', + 'environments', + ], + description: 'A project feature flag definition', + properties: { + name: { + type: 'string', + example: 'disable-comments', + description: 'Unique feature name', + }, + type: { + type: 'string', + example: 'kill-switch', + description: + 'Type of the flag e.g. experiment, kill-switch, release, operational, permission', + }, + description: { + type: 'string', + nullable: true, + example: + 'Controls disabling of the comments section in case of an incident', + description: 'Detailed description of the feature', + }, + stale: { + type: 'boolean', + example: false, + description: + '`true` if the feature is stale based on the age and feature type, otherwise `false`.', + }, + favorite: { + type: 'boolean', + example: true, + description: + '`true` if the feature was favorited, otherwise `false`.', + }, + impressionData: { + type: 'boolean', + example: false, + description: + '`true` if the impression data collection is enabled for the feature, otherwise `false`.', + }, + createdAt: { + type: 'string', + format: 'date-time', + example: '2023-01-28T15:21:39.975Z', + description: 'The date the feature was created', + }, + lastSeenAt: { + type: 'string', + format: 'date-time', + nullable: true, + deprecated: true, + example: '2023-01-28T16:21:39.975Z', + description: + 'The date and time when metrics where last collected for this flag in any environment. This field was deprecated in v5. You should instead use the `lastSeenAt` property on the individual environments listed under the `environments` property.', + }, + environments: { + type: 'array', + items: { + $ref: '#/components/schemas/projectFeatureEnvironmentSchema', + }, + description: + 'The list of environments where the feature can be used', + }, + tags: { + type: 'array', + items: { + $ref: '#/components/schemas/tagSchema', + }, + nullable: true, + description: 'The list of feature tags', + }, + }, + components: { + schemas: { + projectFeatureEnvironmentSchema, + tagSchema, + }, + }, +} as const; + +export type ProjectFeatureSchema = FromSchema; diff --git a/src/lib/openapi/spec/project-features-schema.ts b/src/lib/openapi/spec/project-features-schema.ts new file mode 100644 index 0000000000..9ff6248ed6 --- /dev/null +++ b/src/lib/openapi/spec/project-features-schema.ts @@ -0,0 +1,35 @@ +import type { FromSchema } from 'json-schema-to-ts'; +import { tagSchema } from './tag-schema'; +import { projectFeatureSchema } from './project-feature-schema'; +import { projectFeatureEnvironmentSchema } from './project-feature-environment-schema'; + +export const projectFeaturesSchema = { + $id: '#/components/schemas/projectFeaturesSchema', + type: 'object', + additionalProperties: false, + required: ['version', 'features'], + description: 'A list of features in a project', + deprecated: true, + properties: { + version: { + type: 'integer', + description: "The version of the feature's schema", + }, + features: { + type: 'array', + items: { + $ref: '#/components/schemas/projectFeatureSchema', + }, + description: 'A list of features', + }, + }, + components: { + schemas: { + projectFeatureSchema, + projectFeatureEnvironmentSchema, + tagSchema, + }, + }, +} as const; + +export type ProjectFeaturesSchema = FromSchema;