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..029a3538e886 100644 --- a/src/lib/openapi/index.ts +++ b/src/lib/openapi/index.ts @@ -169,6 +169,7 @@ import { validateArchiveFeaturesSchema, searchFeaturesSchema, featureTypeCountSchema, + featureSearchResponseSchema, } from './spec'; import { IServerOption } from '../types'; import { mapValues, omitKeys } from '../util'; @@ -401,6 +402,7 @@ export const schemas: UnleashSchemas = { searchFeaturesSchema, featureTypeCountSchema, projectOverviewSchema, + featureSearchResponseSchema, }; // Remove JSONSchema keys that would result in an invalid OpenAPI spec. diff --git a/src/lib/openapi/spec/feature-search-response-schema.ts b/src/lib/openapi/spec/feature-search-response-schema.ts new file mode 100644 index 000000000000..96d21cb9104e --- /dev/null +++ b/src/lib/openapi/spec/feature-search-response-schema.ts @@ -0,0 +1,190 @@ +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 featureSearchResponseSchema = { + $id: '#/components/schemas/featureSearchResponseSchema', + 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 FeatureSearchResponseSchema = FromSchema< + typeof featureSearchResponseSchema +>; diff --git a/src/lib/openapi/spec/index.ts b/src/lib/openapi/spec/index.ts index cdd1b2d4d284..1ca36013b83a 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-schema'; diff --git a/src/lib/openapi/spec/search-features-schema.ts b/src/lib/openapi/spec/search-features-schema.ts index 30c93605e94d..60edf6ef5550 100644 --- a/src/lib/openapi/spec/search-features-schema.ts +++ b/src/lib/openapi/spec/search-features-schema.ts @@ -3,11 +3,11 @@ 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'; import { tagSchema } from './tag-schema'; +import { featureSearchResponseSchema } from './feature-search-response-schema'; export const searchFeaturesSchema = { $id: '#/components/schemas/searchFeaturesSchema', @@ -19,10 +19,10 @@ export const searchFeaturesSchema = { features: { type: 'array', items: { - $ref: '#/components/schemas/featureSchema', + $ref: '#/components/schemas/featureSearchResponseSchema', }, 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 +33,7 @@ export const searchFeaturesSchema = { }, components: { schemas: { - featureSchema, + featureSearchResponseSchema, constraintSchema, featureEnvironmentSchema, featureStrategySchema,