From bb31511b44a100295fdf2e2bca553278f1911570 Mon Sep 17 00:00:00 2001 From: sjaanus Date: Tue, 5 Dec 2023 10:11:56 +0200 Subject: [PATCH 1/3] feat: add openapi validation for search --- .../feature-search-controller.ts | 15 +- src/lib/openapi/index.ts | 2 + .../openapi/spec/feature-search-response.ts | 188 ++++++++++++++++++ src/lib/openapi/spec/index.ts | 1 + .../openapi/spec/search-features-schema.ts | 7 +- 5 files changed, 208 insertions(+), 5 deletions(-) create mode 100644 src/lib/openapi/spec/feature-search-response.ts diff --git a/src/lib/features/feature-search/feature-search-controller.ts b/src/lib/features/feature-search/feature-search-controller.ts index ed2daf30b445..1b3ec3579548 100644 --- a/src/lib/features/feature-search/feature-search-controller.ts +++ b/src/lib/features/feature-search/feature-search-controller.ts @@ -6,9 +6,15 @@ import { IUnleashConfig, IUnleashServices, NONE, + serializeDates, } from '../../types'; import { Logger } from '../../logger'; -import { createResponseSchema, getStandardResponses } from '../../openapi'; +import { + createResponseSchema, + getStandardResponses, + projectOverviewSchema, + searchFeaturesSchema, +} from '../../openapi'; import { IAuthRequest } from '../../routes/unleash-types'; import { InvalidOperationError } from '../../error'; import { @@ -122,7 +128,12 @@ export default class FeatureSearchController extends Controller { favoritesFirst: normalizedFavoritesFirst, }); - res.json({ features, total }); + this.openApiService.respondWithValidation( + 200, + res, + searchFeaturesSchema.$id, + serializeDates({ features, total }), + ); } else { throw new InvalidOperationError( 'Feature Search API is not enabled', diff --git a/src/lib/openapi/index.ts b/src/lib/openapi/index.ts index e0b90cb7924b..24b43a437f3c 100644 --- a/src/lib/openapi/index.ts +++ b/src/lib/openapi/index.ts @@ -169,6 +169,7 @@ import { validateArchiveFeaturesSchema, searchFeaturesSchema, featureTypeCountSchema, + featureSearchResponse, } from './spec'; import { IServerOption } from '../types'; import { mapValues, omitKeys } from '../util'; @@ -401,6 +402,7 @@ export const schemas: UnleashSchemas = { searchFeaturesSchema, featureTypeCountSchema, projectOverviewSchema, + featureSearchResponse, }; // Remove JSONSchema keys that would result in an invalid OpenAPI spec. diff --git a/src/lib/openapi/spec/feature-search-response.ts b/src/lib/openapi/spec/feature-search-response.ts new file mode 100644 index 000000000000..8bac20dcac77 --- /dev/null +++ b/src/lib/openapi/spec/feature-search-response.ts @@ -0,0 +1,188 @@ +import { FromSchema } from 'json-schema-to-ts'; +import { variantSchema } from './variant-schema'; +import { constraintSchema } from './constraint-schema'; +import { overrideSchema } from './override-schema'; +import { parametersSchema } from './parameters-schema'; +import { featureStrategySchema } from './feature-strategy-schema'; +import { tagSchema } from './tag-schema'; +import { featureEnvironmentSchema } from './feature-environment-schema'; +import { strategyVariantSchema } from './strategy-variant-schema'; + +export const featureSearchResponse = { + $id: '#/components/schemas/featureSearchResponse', + type: 'object', + additionalProperties: false, + required: ['name'], + description: 'A feature toggle definition', + properties: { + name: { + type: 'string', + example: 'disable-comments', + description: 'Unique feature name', + }, + type: { + type: 'string', + example: 'kill-switch', + description: + 'Type of the toggle 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', + }, + archived: { + type: 'boolean', + example: true, + description: '`true` if the feature is archived', + }, + project: { + type: 'string', + example: 'dx-squad', + description: 'Name of the project the feature belongs to', + }, + enabled: { + type: 'boolean', + example: true, + description: '`true` if the feature is enabled, otherwise `false`.', + }, + 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', + nullable: true, + example: '2023-01-28T15:21:39.975Z', + description: 'The date the feature was created', + }, + archivedAt: { + type: 'string', + format: 'date-time', + nullable: true, + 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 is deprecated, use the one in featureEnvironmentSchema', + }, + environments: { + type: 'array', + items: { + $ref: '#/components/schemas/featureEnvironmentSchema', + }, + description: + 'The list of environments where the feature can be used', + }, + segments: { + type: 'array', + description: 'The list of segments the feature is enabled for.', + example: ['pro-users', 'main-segment'], + items: { + type: 'string', + }, + }, + variants: { + type: 'array', + items: { + $ref: '#/components/schemas/variantSchema', + }, + description: 'The list of feature variants', + deprecated: true, + }, + strategies: { + type: 'array', + items: { + type: 'object', + }, + description: 'This is a legacy field that will be deprecated', + deprecated: true, + }, + tags: { + type: 'array', + items: { + $ref: '#/components/schemas/tagSchema', + }, + nullable: true, + description: 'The list of feature tags', + }, + children: { + type: 'array', + description: + 'The list of child feature names. This is an experimental field and may change.', + items: { + type: 'string', + example: 'some-feature', + }, + }, + dependencies: { + type: 'array', + items: { + type: 'object', + additionalProperties: false, + required: ['feature'], + properties: { + feature: { + description: 'The name of the parent feature', + type: 'string', + example: 'some-feature', + }, + enabled: { + description: + 'Whether the parent feature is enabled or not', + type: 'boolean', + example: true, + }, + variants: { + description: + 'The list of variants the parent feature should resolve to. Only valid when feature is enabled.', + type: 'array', + items: { + example: 'some-feature-blue-variant', + type: 'string', + }, + }, + }, + }, + description: + 'The list of parent dependencies. This is an experimental field and may change.', + }, + }, + components: { + schemas: { + constraintSchema, + featureEnvironmentSchema, + featureStrategySchema, + strategyVariantSchema, + overrideSchema, + parametersSchema, + variantSchema, + tagSchema, + }, + }, +} as const; + +export type FeatureSearchResponse = FromSchema; diff --git a/src/lib/openapi/spec/index.ts b/src/lib/openapi/spec/index.ts index cdd1b2d4d284..9c05ba4f471d 100644 --- a/src/lib/openapi/spec/index.ts +++ b/src/lib/openapi/spec/index.ts @@ -170,3 +170,4 @@ export * from './validate-archive-features-schema'; export * from './search-features-schema'; export * from './feature-search-query-parameters'; export * from './feature-type-count-schema'; +export * from './feature-search-response'; diff --git a/src/lib/openapi/spec/search-features-schema.ts b/src/lib/openapi/spec/search-features-schema.ts index 30c93605e94d..f690bf524114 100644 --- a/src/lib/openapi/spec/search-features-schema.ts +++ b/src/lib/openapi/spec/search-features-schema.ts @@ -8,6 +8,7 @@ import { constraintSchema } from './constraint-schema'; import { featureEnvironmentSchema } from './feature-environment-schema'; import { strategyVariantSchema } from './strategy-variant-schema'; import { tagSchema } from './tag-schema'; +import { featureSearchResponse } from './feature-search-response'; export const searchFeaturesSchema = { $id: '#/components/schemas/searchFeaturesSchema', @@ -19,10 +20,10 @@ export const searchFeaturesSchema = { features: { type: 'array', items: { - $ref: '#/components/schemas/featureSchema', + $ref: '#/components/schemas/featureSearchResponse', }, description: - 'The full list of features in this project (excluding archived features)', + 'The full list of features in this project matching search and filter criteria.', }, total: { type: 'number', @@ -33,7 +34,7 @@ export const searchFeaturesSchema = { }, components: { schemas: { - featureSchema, + featureSearchResponse, constraintSchema, featureEnvironmentSchema, featureStrategySchema, From 71328f6355af6582ea15e15f4e66bffd19750d5f Mon Sep 17 00:00:00 2001 From: sjaanus Date: Tue, 5 Dec 2023 10:43:59 +0200 Subject: [PATCH 2/3] Fix schema --- src/lib/openapi/index.ts | 4 ++-- ...arch-response.ts => feature-search-response-schema.ts} | 8 +++++--- src/lib/openapi/spec/index.ts | 2 +- src/lib/openapi/spec/search-features-schema.ts | 6 +++--- 4 files changed, 11 insertions(+), 9 deletions(-) rename src/lib/openapi/spec/{feature-search-response.ts => feature-search-response-schema.ts} (96%) diff --git a/src/lib/openapi/index.ts b/src/lib/openapi/index.ts index 24b43a437f3c..029a3538e886 100644 --- a/src/lib/openapi/index.ts +++ b/src/lib/openapi/index.ts @@ -169,7 +169,7 @@ import { validateArchiveFeaturesSchema, searchFeaturesSchema, featureTypeCountSchema, - featureSearchResponse, + featureSearchResponseSchema, } from './spec'; import { IServerOption } from '../types'; import { mapValues, omitKeys } from '../util'; @@ -402,7 +402,7 @@ export const schemas: UnleashSchemas = { searchFeaturesSchema, featureTypeCountSchema, projectOverviewSchema, - featureSearchResponse, + featureSearchResponseSchema, }; // Remove JSONSchema keys that would result in an invalid OpenAPI spec. diff --git a/src/lib/openapi/spec/feature-search-response.ts b/src/lib/openapi/spec/feature-search-response-schema.ts similarity index 96% rename from src/lib/openapi/spec/feature-search-response.ts rename to src/lib/openapi/spec/feature-search-response-schema.ts index 8bac20dcac77..96d21cb9104e 100644 --- a/src/lib/openapi/spec/feature-search-response.ts +++ b/src/lib/openapi/spec/feature-search-response-schema.ts @@ -8,8 +8,8 @@ import { tagSchema } from './tag-schema'; import { featureEnvironmentSchema } from './feature-environment-schema'; import { strategyVariantSchema } from './strategy-variant-schema'; -export const featureSearchResponse = { - $id: '#/components/schemas/featureSearchResponse', +export const featureSearchResponseSchema = { + $id: '#/components/schemas/featureSearchResponseSchema', type: 'object', additionalProperties: false, required: ['name'], @@ -185,4 +185,6 @@ export const featureSearchResponse = { }, } as const; -export type FeatureSearchResponse = FromSchema; +export type FeatureSearchResponseSchema = FromSchema< + typeof featureSearchResponseSchema +>; diff --git a/src/lib/openapi/spec/index.ts b/src/lib/openapi/spec/index.ts index 9c05ba4f471d..1ca36013b83a 100644 --- a/src/lib/openapi/spec/index.ts +++ b/src/lib/openapi/spec/index.ts @@ -170,4 +170,4 @@ export * from './validate-archive-features-schema'; export * from './search-features-schema'; export * from './feature-search-query-parameters'; export * from './feature-type-count-schema'; -export * from './feature-search-response'; +export * from './feature-search-response-schema'; diff --git a/src/lib/openapi/spec/search-features-schema.ts b/src/lib/openapi/spec/search-features-schema.ts index f690bf524114..1f02666935af 100644 --- a/src/lib/openapi/spec/search-features-schema.ts +++ b/src/lib/openapi/spec/search-features-schema.ts @@ -8,7 +8,7 @@ import { constraintSchema } from './constraint-schema'; import { featureEnvironmentSchema } from './feature-environment-schema'; import { strategyVariantSchema } from './strategy-variant-schema'; import { tagSchema } from './tag-schema'; -import { featureSearchResponse } from './feature-search-response'; +import { featureSearchResponseSchema } from './feature-search-response-schema'; export const searchFeaturesSchema = { $id: '#/components/schemas/searchFeaturesSchema', @@ -20,7 +20,7 @@ export const searchFeaturesSchema = { features: { type: 'array', items: { - $ref: '#/components/schemas/featureSearchResponse', + $ref: '#/components/schemas/featureSearchResponseSchema', }, description: 'The full list of features in this project matching search and filter criteria.', @@ -34,7 +34,7 @@ export const searchFeaturesSchema = { }, components: { schemas: { - featureSearchResponse, + featureSearchResponse: featureSearchResponseSchema, constraintSchema, featureEnvironmentSchema, featureStrategySchema, From 2fadec70466407127a69601aae6833a8c68b194a Mon Sep 17 00:00:00 2001 From: sjaanus Date: Tue, 5 Dec 2023 11:21:15 +0200 Subject: [PATCH 3/3] Fix --- src/lib/openapi/spec/search-features-schema.ts | 3 +-- 1 file changed, 1 insertion(+), 2 deletions(-) diff --git a/src/lib/openapi/spec/search-features-schema.ts b/src/lib/openapi/spec/search-features-schema.ts index 1f02666935af..60edf6ef5550 100644 --- a/src/lib/openapi/spec/search-features-schema.ts +++ b/src/lib/openapi/spec/search-features-schema.ts @@ -3,7 +3,6 @@ import { parametersSchema } from './parameters-schema'; import { variantSchema } from './variant-schema'; import { overrideSchema } from './override-schema'; import { featureStrategySchema } from './feature-strategy-schema'; -import { featureSchema } from './feature-schema'; import { constraintSchema } from './constraint-schema'; import { featureEnvironmentSchema } from './feature-environment-schema'; import { strategyVariantSchema } from './strategy-variant-schema'; @@ -34,7 +33,7 @@ export const searchFeaturesSchema = { }, components: { schemas: { - featureSearchResponse: featureSearchResponseSchema, + featureSearchResponseSchema, constraintSchema, featureEnvironmentSchema, featureStrategySchema,