Skip to content
New issue

Have a question about this project? Sign up for a free GitHub account to open an issue and contact its maintainers and the community.

By clicking “Sign up for GitHub”, you agree to our terms of service and privacy statement. We’ll occasionally send you account related emails.

Already on GitHub? Sign in to your account

feat: add openapi validation for search #5541

Merged
merged 4 commits into from
Dec 5, 2023
Merged
Show file tree
Hide file tree
Changes from all commits
Commits
File filter

Filter by extension

Filter by extension

Conversations
Failed to load comments.
Loading
Jump to
Jump to file
Failed to load files.
Loading
Diff view
Diff view
15 changes: 13 additions & 2 deletions src/lib/features/feature-search/feature-search-controller.ts
Original file line number Diff line number Diff line change
Expand Up @@ -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 {
Expand Down Expand Up @@ -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',
Expand Down
2 changes: 2 additions & 0 deletions src/lib/openapi/index.ts
Original file line number Diff line number Diff line change
Expand Up @@ -169,6 +169,7 @@ import {
validateArchiveFeaturesSchema,
searchFeaturesSchema,
featureTypeCountSchema,
featureSearchResponseSchema,
} from './spec';
import { IServerOption } from '../types';
import { mapValues, omitKeys } from '../util';
Expand Down Expand Up @@ -401,6 +402,7 @@ export const schemas: UnleashSchemas = {
searchFeaturesSchema,
featureTypeCountSchema,
projectOverviewSchema,
featureSearchResponseSchema,
};

// Remove JSONSchema keys that would result in an invalid OpenAPI spec.
Expand Down
190 changes: 190 additions & 0 deletions src/lib/openapi/spec/feature-search-response-schema.ts
Original file line number Diff line number Diff line change
@@ -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
>;
1 change: 1 addition & 0 deletions src/lib/openapi/spec/index.ts
Original file line number Diff line number Diff line change
Expand Up @@ -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';
8 changes: 4 additions & 4 deletions src/lib/openapi/spec/search-features-schema.ts
Original file line number Diff line number Diff line change
Expand Up @@ -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',
Expand All @@ -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',
Expand All @@ -33,7 +33,7 @@ export const searchFeaturesSchema = {
},
components: {
schemas: {
featureSchema,
featureSearchResponseSchema,
constraintSchema,
featureEnvironmentSchema,
featureStrategySchema,
Expand Down