1
0
mirror of https://github.com/Unleash/unleash.git synced 2025-01-25 00:07:47 +01:00

feat: add openapi validation for search (#5541)

This commit is contained in:
Jaanus Sellin 2023-12-05 11:25:56 +02:00 committed by GitHub
parent e341a58364
commit 6f497e6708
No known key found for this signature in database
GPG Key ID: 4AEE18F83AFDEB23
5 changed files with 210 additions and 6 deletions

View File

@ -6,9 +6,15 @@ import {
IUnleashConfig, IUnleashConfig,
IUnleashServices, IUnleashServices,
NONE, NONE,
serializeDates,
} from '../../types'; } from '../../types';
import { Logger } from '../../logger'; import { Logger } from '../../logger';
import { createResponseSchema, getStandardResponses } from '../../openapi'; import {
createResponseSchema,
getStandardResponses,
projectOverviewSchema,
searchFeaturesSchema,
} from '../../openapi';
import { IAuthRequest } from '../../routes/unleash-types'; import { IAuthRequest } from '../../routes/unleash-types';
import { InvalidOperationError } from '../../error'; import { InvalidOperationError } from '../../error';
import { import {
@ -122,7 +128,12 @@ export default class FeatureSearchController extends Controller {
favoritesFirst: normalizedFavoritesFirst, favoritesFirst: normalizedFavoritesFirst,
}); });
res.json({ features, total }); this.openApiService.respondWithValidation(
200,
res,
searchFeaturesSchema.$id,
serializeDates({ features, total }),
);
} else { } else {
throw new InvalidOperationError( throw new InvalidOperationError(
'Feature Search API is not enabled', 'Feature Search API is not enabled',

View File

@ -169,6 +169,7 @@ import {
validateArchiveFeaturesSchema, validateArchiveFeaturesSchema,
searchFeaturesSchema, searchFeaturesSchema,
featureTypeCountSchema, featureTypeCountSchema,
featureSearchResponseSchema,
} from './spec'; } from './spec';
import { IServerOption } from '../types'; import { IServerOption } from '../types';
import { mapValues, omitKeys } from '../util'; import { mapValues, omitKeys } from '../util';
@ -401,6 +402,7 @@ export const schemas: UnleashSchemas = {
searchFeaturesSchema, searchFeaturesSchema,
featureTypeCountSchema, featureTypeCountSchema,
projectOverviewSchema, projectOverviewSchema,
featureSearchResponseSchema,
}; };
// Remove JSONSchema keys that would result in an invalid OpenAPI spec. // Remove JSONSchema keys that would result in an invalid OpenAPI spec.

View File

@ -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
>;

View File

@ -170,3 +170,4 @@ export * from './validate-archive-features-schema';
export * from './search-features-schema'; export * from './search-features-schema';
export * from './feature-search-query-parameters'; export * from './feature-search-query-parameters';
export * from './feature-type-count-schema'; export * from './feature-type-count-schema';
export * from './feature-search-response-schema';

View File

@ -3,11 +3,11 @@ import { parametersSchema } from './parameters-schema';
import { variantSchema } from './variant-schema'; import { variantSchema } from './variant-schema';
import { overrideSchema } from './override-schema'; import { overrideSchema } from './override-schema';
import { featureStrategySchema } from './feature-strategy-schema'; import { featureStrategySchema } from './feature-strategy-schema';
import { featureSchema } from './feature-schema';
import { constraintSchema } from './constraint-schema'; import { constraintSchema } from './constraint-schema';
import { featureEnvironmentSchema } from './feature-environment-schema'; import { featureEnvironmentSchema } from './feature-environment-schema';
import { strategyVariantSchema } from './strategy-variant-schema'; import { strategyVariantSchema } from './strategy-variant-schema';
import { tagSchema } from './tag-schema'; import { tagSchema } from './tag-schema';
import { featureSearchResponseSchema } from './feature-search-response-schema';
export const searchFeaturesSchema = { export const searchFeaturesSchema = {
$id: '#/components/schemas/searchFeaturesSchema', $id: '#/components/schemas/searchFeaturesSchema',
@ -19,10 +19,10 @@ export const searchFeaturesSchema = {
features: { features: {
type: 'array', type: 'array',
items: { items: {
$ref: '#/components/schemas/featureSchema', $ref: '#/components/schemas/featureSearchResponseSchema',
}, },
description: 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: { total: {
type: 'number', type: 'number',
@ -33,7 +33,7 @@ export const searchFeaturesSchema = {
}, },
components: { components: {
schemas: { schemas: {
featureSchema, featureSearchResponseSchema,
constraintSchema, constraintSchema,
featureEnvironmentSchema, featureEnvironmentSchema,
featureStrategySchema, featureStrategySchema,