Skip to content

Commit

Permalink
feat: add openapi validation for search (#5541)
Browse files Browse the repository at this point in the history
  • Loading branch information
sjaanus authored Dec 5, 2023
1 parent e341a58 commit 6f497e6
Show file tree
Hide file tree
Showing 5 changed files with 210 additions and 6 deletions.
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

0 comments on commit 6f497e6

Please sign in to comment.