1
0
mirror of https://github.com/Unleash/unleash.git synced 2024-12-22 19:07:54 +01:00

refactor: add OpenAPI schema to environments controller (#1682)

* refactor: normalize controller file names

* refactor: throw invalid responses in dev mode

* refactor: add OpenAPI schema to environments controller

* refactor: improve JSON schema prop removal code

* refactor: fix empty response specs
This commit is contained in:
olav 2022-06-10 10:04:56 +02:00 committed by GitHub
parent 18e63d5ea3
commit adface17c7
No known key found for this signature in database
GPG Key ID: 4AEE18F83AFDEB23
22 changed files with 504 additions and 182 deletions

View File

@ -0,0 +1,52 @@
import {
createOpenApiSchema,
createRequestSchema,
createResponseSchema,
removeJsonSchemaProps,
} from './index';
test('createRequestSchema', () => {
expect(createRequestSchema('schemaName')).toMatchInlineSnapshot(`
Object {
"content": Object {
"application/json": Object {
"schema": Object {
"$ref": "#/components/schemas/schemaName",
},
},
},
"description": "schemaName",
"required": true,
}
`);
});
test('createResponseSchema', () => {
expect(createResponseSchema('schemaName')).toMatchInlineSnapshot(`
Object {
"content": Object {
"application/json": Object {
"schema": Object {
"$ref": "#/components/schemas/schemaName",
},
},
},
"description": "schemaName",
}
`);
});
test('removeJsonSchemaProps', () => {
expect(removeJsonSchemaProps({ a: 'b', $id: 'c', components: {} }))
.toMatchInlineSnapshot(`
Object {
"a": "b",
}
`);
});
test('createOpenApiSchema url', () => {
expect(createOpenApiSchema('https://example.com').servers[0].url).toEqual(
'https://example.com',
);
});

View File

@ -3,7 +3,6 @@ import { cloneFeatureSchema } from './spec/clone-feature-schema';
import { constraintSchema } from './spec/constraint-schema';
import { createFeatureSchema } from './spec/create-feature-schema';
import { createStrategySchema } from './spec/create-strategy-schema';
import { emptySchema } from './spec/empty-schema';
import { environmentSchema } from './spec/environment-schema';
import { featureEnvironmentSchema } from './spec/feature-environment-schema';
import { featureSchema } from './spec/feature-schema';
@ -32,6 +31,8 @@ import { updateStrategySchema } from './spec/update-strategy-schema';
import { variantSchema } from './spec/variant-schema';
import { variantsSchema } from './spec/variants-schema';
import { versionSchema } from './spec/version-schema';
import { environmentsSchema } from './spec/environments-schema';
import { sortOrderSchema } from './spec/sort-order-schema';
// Schemas must have $id property on the form "#/components/schemas/mySchema".
export type SchemaId = typeof schemas[keyof typeof schemas]['$id'];
@ -39,6 +40,12 @@ export type SchemaId = typeof schemas[keyof typeof schemas]['$id'];
// Schemas must list all $ref schemas in "components", including nested schemas.
export type SchemaRef = typeof schemas[keyof typeof schemas]['components'];
// JSON schema properties that should not be included in the OpenAPI spec.
export interface JsonSchemaProps {
$id: string;
components: object;
}
export interface AdminApiOperation
extends Omit<OpenAPIV3.OperationObject, 'tags'> {
operationId: string;
@ -56,8 +63,8 @@ export const schemas = {
constraintSchema,
createFeatureSchema,
createStrategySchema,
emptySchema,
environmentSchema,
environmentsSchema,
featureEnvironmentSchema,
featureSchema,
featureStrategySchema,
@ -74,6 +81,7 @@ export const schemas = {
projectEnvironmentSchema,
projectSchema,
projectsSchema,
sortOrderSchema,
strategySchema,
tagSchema,
tagsSchema,
@ -116,6 +124,13 @@ export const createResponseSchema = (
};
};
// Remove JSONSchema keys that would result in an invalid OpenAPI spec.
export const removeJsonSchemaProps = <T extends JsonSchemaProps>(
schema: T,
): OpenAPIV3.SchemaObject => {
return omitKeys(schema, '$id', 'components');
};
export const createOpenApiSchema = (
serverUrl?: string,
): Omit<OpenAPIV3.Document, 'paths'> => {
@ -135,9 +150,7 @@ export const createOpenApiSchema = (
name: 'Authorization',
},
},
schemas: mapValues(schemas, (schema) =>
omitKeys(schema, '$id', 'components'),
),
schemas: mapValues(schemas, removeJsonSchemaProps),
},
};
};

View File

@ -0,0 +1,21 @@
// Jest Snapshot v1, https://goo.gl/fbAQLP
exports[`sortOrderSchema invalid value type 1`] = `
Object {
"data": Object {
"a": "1",
},
"errors": Array [
Object {
"instancePath": "/a",
"keyword": "type",
"message": "must be number",
"params": Object {
"type": "number",
},
"schemaPath": "#/additionalProperties/type",
},
],
"schema": "#/components/schemas/sortOrderSchema",
}
`;

View File

@ -0,0 +1,3 @@
export const emptyResponse = {
description: 'emptyResponse',
};

View File

@ -1,9 +0,0 @@
import { FromSchema } from 'json-schema-to-ts';
export const emptySchema = {
$id: '#/components/schemas/emptySchema',
description: 'emptySchema',
components: {},
} as const;
export type EmptySchema = FromSchema<typeof emptySchema>;

View File

@ -15,6 +15,9 @@ export const environmentSchema = {
enabled: {
type: 'boolean',
},
protected: {
type: 'boolean',
},
sortOrder: {
type: 'number',
},

View File

@ -0,0 +1,27 @@
import { FromSchema } from 'json-schema-to-ts';
import { environmentSchema } from './environment-schema';
export const environmentsSchema = {
$id: '#/components/schemas/environmentsSchema',
type: 'object',
additionalProperties: false,
required: ['version', 'environments'],
properties: {
version: {
type: 'integer',
},
environments: {
type: 'array',
items: {
$ref: '#/components/schemas/environmentSchema',
},
},
},
components: {
schemas: {
environmentSchema,
},
},
} as const;
export type EnvironmentsSchema = FromSchema<typeof environmentsSchema>;

View File

@ -0,0 +1,19 @@
import { validateSchema } from '../validate';
import { SortOrderSchema } from './sort-order-schema';
test('sortOrderSchema', () => {
const data: SortOrderSchema = {
a: 1,
b: 2,
};
expect(
validateSchema('#/components/schemas/sortOrderSchema', data),
).toBeUndefined();
});
test('sortOrderSchema invalid value type', () => {
expect(
validateSchema('#/components/schemas/sortOrderSchema', { a: '1' }),
).toMatchSnapshot();
});

View File

@ -0,0 +1,12 @@
import { FromSchema } from 'json-schema-to-ts';
export const sortOrderSchema = {
$id: '#/components/schemas/sortOrderSchema',
type: 'object',
additionalProperties: {
type: 'number',
},
components: {},
} as const;
export type SortOrderSchema = FromSchema<typeof sortOrderSchema>;

View File

@ -14,7 +14,7 @@ import {
import { serializeDates } from '../../types/serialize-dates';
import { OpenApiService } from '../../services/openapi-service';
import { createResponseSchema } from '../../openapi';
import { EmptySchema } from '../../openapi/spec/empty-schema';
import { emptyResponse } from '../../openapi/spec/empty-response';
export default class ArchiveController extends Controller {
private readonly logger: Logger;
@ -75,7 +75,7 @@ export default class ArchiveController extends Controller {
openApiService.validPath({
tags: ['admin'],
operationId: 'deleteFeature',
responses: { 200: createResponseSchema('emptySchema') },
responses: { 200: emptyResponse },
}),
],
});
@ -90,7 +90,7 @@ export default class ArchiveController extends Controller {
openApiService.validPath({
tags: ['admin'],
operationId: 'reviveFeature',
responses: { 200: createResponseSchema('emptySchema') },
responses: { 200: emptyResponse },
}),
],
});
@ -131,7 +131,7 @@ export default class ArchiveController extends Controller {
async deleteFeature(
req: IAuthRequest<{ featureName: string }>,
res: Response<EmptySchema>,
res: Response<void>,
): Promise<void> {
const { featureName } = req.params;
const user = extractUsername(req);
@ -141,7 +141,7 @@ export default class ArchiveController extends Controller {
async reviveFeature(
req: IAuthRequest<{ featureName: string }>,
res: Response<EmptySchema>,
res: Response<void>,
): Promise<void> {
const userName = extractUsername(req);
const { featureName } = req.params;

View File

@ -1,73 +0,0 @@
import { Request, Response } from 'express';
import Controller from '../controller';
import { IUnleashServices } from '../../types/services';
import { IUnleashConfig } from '../../types/option';
import { ISortOrder } from '../../types/model';
import EnvironmentService from '../../services/environment-service';
import { Logger } from '../../logger';
import { ADMIN } from '../../types/permissions';
interface EnvironmentParam {
name: string;
}
export class EnvironmentsController extends Controller {
private logger: Logger;
private service: EnvironmentService;
constructor(
config: IUnleashConfig,
{ environmentService }: Pick<IUnleashServices, 'environmentService'>,
) {
super(config);
this.logger = config.getLogger('admin-api/environments-controller.ts');
this.service = environmentService;
this.get('/', this.getAll);
this.put('/sort-order', this.updateSortOrder, ADMIN);
this.get('/:name', this.getEnv);
this.post('/:name/on', this.toggleEnvironmentOn, ADMIN);
this.post('/:name/off', this.toggleEnvironmentOff, ADMIN);
}
async getAll(req: Request, res: Response): Promise<void> {
const environments = await this.service.getAll();
res.status(200).json({ version: 1, environments });
}
async updateSortOrder(
req: Request<any, any, ISortOrder, any>,
res: Response,
): Promise<void> {
await this.service.updateSortOrder(req.body);
res.status(200).end();
}
async toggleEnvironmentOn(
req: Request<EnvironmentParam, any, any, any>,
res: Response,
): Promise<void> {
const { name } = req.params;
await this.service.toggleEnvironment(name, true);
res.status(204).end();
}
async toggleEnvironmentOff(
req: Request<EnvironmentParam, any, any, any>,
res: Response,
): Promise<void> {
const { name } = req.params;
await this.service.toggleEnvironment(name, false);
res.status(204).end();
}
async getEnv(
req: Request<EnvironmentParam, any, any, any>,
res: Response,
): Promise<void> {
const { name } = req.params;
const env = await this.service.get(name);
res.status(200).json(env);
}
}

View File

@ -0,0 +1,169 @@
import { Request, Response } from 'express';
import Controller from '../controller';
import { IUnleashServices } from '../../types/services';
import { IUnleashConfig } from '../../types/option';
import EnvironmentService from '../../services/environment-service';
import { Logger } from '../../logger';
import { ADMIN, NONE } from '../../types/permissions';
import { OpenApiService } from '../../services/openapi-service';
import { createRequestSchema, createResponseSchema } from '../../openapi';
import {
environmentsSchema,
EnvironmentsSchema,
} from '../../openapi/spec/environments-schema';
import {
environmentSchema,
EnvironmentSchema,
} from '../../openapi/spec/environment-schema';
import { SortOrderSchema } from '../../openapi/spec/sort-order-schema';
import { emptyResponse } from '../../openapi/spec/empty-response';
interface EnvironmentParam {
name: string;
}
export class EnvironmentsController extends Controller {
private logger: Logger;
private openApiService: OpenApiService;
private service: EnvironmentService;
constructor(
config: IUnleashConfig,
{
environmentService,
openApiService,
}: Pick<IUnleashServices, 'environmentService' | 'openApiService'>,
) {
super(config);
this.logger = config.getLogger('admin-api/environments-controller.ts');
this.openApiService = openApiService;
this.service = environmentService;
this.route({
method: 'get',
path: '',
handler: this.getAllEnvironments,
permission: NONE,
middleware: [
openApiService.validPath({
tags: ['admin'],
operationId: 'getAllEnvironments',
responses: { 200: emptyResponse },
}),
],
});
this.route({
method: 'get',
path: '/:name',
handler: this.getEnvironment,
permission: NONE,
middleware: [
openApiService.validPath({
tags: ['admin'],
operationId: 'getEnvironment',
responses: {
200: createResponseSchema('environmentSchema'),
},
}),
],
});
this.route({
method: 'put',
path: '/sort-order',
handler: this.updateSortOrder,
permission: ADMIN,
middleware: [
openApiService.validPath({
tags: ['admin'],
operationId: 'updateSortOrder',
requestBody: createRequestSchema('sortOrderSchema'),
responses: { 200: emptyResponse },
}),
],
});
this.route({
method: 'post',
path: '/:name/on',
acceptAnyContentType: true,
handler: this.toggleEnvironmentOn,
permission: ADMIN,
middleware: [
openApiService.validPath({
tags: ['admin'],
operationId: 'toggleEnvironmentOn',
responses: { 200: emptyResponse },
}),
],
});
this.route({
method: 'post',
path: '/:name/off',
acceptAnyContentType: true,
handler: this.toggleEnvironmentOff,
permission: ADMIN,
middleware: [
openApiService.validPath({
tags: ['admin'],
operationId: 'toggleEnvironmentOff',
responses: { 200: emptyResponse },
}),
],
});
}
async getAllEnvironments(
req: Request,
res: Response<EnvironmentsSchema>,
): Promise<void> {
this.openApiService.respondWithValidation(
200,
res,
environmentsSchema.$id,
{ version: 1, environments: await this.service.getAll() },
);
}
async updateSortOrder(
req: Request<unknown, unknown, SortOrderSchema>,
res: Response,
): Promise<void> {
await this.service.updateSortOrder(req.body);
res.status(200).end();
}
async toggleEnvironmentOn(
req: Request<EnvironmentParam>,
res: Response,
): Promise<void> {
const { name } = req.params;
await this.service.toggleEnvironment(name, true);
res.status(204).end();
}
async toggleEnvironmentOff(
req: Request<EnvironmentParam>,
res: Response,
): Promise<void> {
const { name } = req.params;
await this.service.toggleEnvironment(name, false);
res.status(204).end();
}
async getEnvironment(
req: Request<EnvironmentParam>,
res: Response<EnvironmentSchema>,
): Promise<void> {
this.openApiService.respondWithValidation(
200,
res,
environmentSchema.$id,
await this.service.get(req.params.name),
);
}
}

View File

@ -25,6 +25,7 @@ import { TagsSchema } from '../../openapi/spec/tags-schema';
import { serializeDates } from '../../types/serialize-dates';
import { OpenApiService } from '../../services/openapi-service';
import { createRequestSchema, createResponseSchema } from '../../openapi';
import { emptyResponse } from '../../openapi/spec/empty-response';
const version = 1;
@ -92,7 +93,7 @@ class FeatureController extends Controller {
openApiService.validPath({
tags: ['admin'],
operationId: 'validateFeature',
responses: { 200: createResponseSchema('emptySchema') },
responses: { 200: emptyResponse },
}),
],
});
@ -136,7 +137,7 @@ class FeatureController extends Controller {
openApiService.validPath({
tags: ['admin'],
operationId: 'removeTag',
responses: { 200: createResponseSchema('emptySchema') },
responses: { 200: emptyResponse },
}),
],
});

View File

@ -12,18 +12,18 @@ import UserController from './user';
import ConfigController from './config';
import ContextController from './context';
import ClientMetricsController from './client-metrics';
import BootstrapController from './bootstrap-controller';
import BootstrapController from './bootstrap';
import StateController from './state';
import TagController from './tag';
import TagTypeController from './tag-type';
import AddonController from './addon';
import ApiTokenController from './api-token-controller';
import ApiTokenController from './api-token';
import UserAdminController from './user-admin';
import EmailController from './email';
import UserFeedbackController from './user-feedback-controller';
import UserSplashController from './user-splash-controller';
import UserFeedbackController from './user-feedback';
import UserSplashController from './user-splash';
import ProjectApi from './project';
import { EnvironmentsController } from './environments-controller';
import { EnvironmentsController } from './environments';
import ConstraintsController from './constraints';
class AdminApi extends Controller {

View File

@ -5,8 +5,9 @@ import { IUnleashServices } from '../../../types/services';
import { Logger } from '../../../logger';
import EnvironmentService from '../../../services/environment-service';
import { UPDATE_PROJECT } from '../../../types/permissions';
import { createRequestSchema, createResponseSchema } from '../../../openapi';
import { createRequestSchema } from '../../../openapi';
import { ProjectEnvironmentSchema } from '../../../openapi/spec/project-environment-schema';
import { emptyResponse } from '../../../openapi/spec/empty-response';
const PREFIX = '/:projectId/environments';
@ -44,7 +45,7 @@ export default class EnvironmentsController extends Controller {
requestBody: createRequestSchema(
'projectEnvironmentSchema',
),
responses: { 200: createResponseSchema('emptySchema') },
responses: { 200: emptyResponse },
}),
],
});
@ -59,7 +60,7 @@ export default class EnvironmentsController extends Controller {
openApiService.validPath({
tags: ['admin'],
operationId: 'removeEnvironmentFromProject',
responses: { 200: createResponseSchema('emptySchema') },
responses: { 200: emptyResponse },
}),
],
});

View File

@ -35,6 +35,7 @@ import { serializeDates } from '../../../types/serialize-dates';
import { OpenApiService } from '../../../services/openapi-service';
import { createRequestSchema, createResponseSchema } from '../../../openapi';
import { FeatureEnvironmentSchema } from '../../../openapi/spec/feature-environment-schema';
import { emptyResponse } from '../../../openapi/spec/empty-response';
interface FeatureStrategyParams {
projectId: string;
@ -216,7 +217,7 @@ export default class ProjectFeaturesController extends Controller {
openApiService.validPath({
operationId: 'deleteStrategy',
tags: ['admin'],
responses: { 200: createResponseSchema('emptySchema') },
responses: { 200: emptyResponse },
}),
],
});
@ -322,7 +323,7 @@ export default class ProjectFeaturesController extends Controller {
openApiService.validPath({
tags: ['admin'],
operationId: 'archiveFeature',
responses: { 200: createResponseSchema('emptySchema') },
responses: { 200: emptyResponse },
}),
],
});

View File

@ -5,11 +5,12 @@ import {
AdminApiOperation,
ClientApiOperation,
createOpenApiSchema,
JsonSchemaProps,
removeJsonSchemaProps,
SchemaId,
} from '../openapi';
import { Logger } from '../logger';
import { validateSchema } from '../openapi/validate';
import { omitKeys } from '../util/omit-keys';
export class OpenApiService {
private readonly config: IUnleashConfig;
@ -43,11 +44,11 @@ export class OpenApiService {
return `${baseUriPath}/docs/openapi`;
}
registerCustomSchemas<T extends object>(schemas: {
[name: string]: { $id: string; components: T };
}): void {
registerCustomSchemas<T extends JsonSchemaProps>(
schemas: Record<string, T>,
): void {
Object.entries(schemas).forEach(([name, schema]) => {
this.api.schema(name, omitKeys(schema, '$id', 'components'));
this.api.schema(name, removeJsonSchemaProps(schema));
});
}
@ -73,12 +74,11 @@ export class OpenApiService {
const errors = validateSchema(schema, data);
if (errors) {
this.logger.warn(
'Invalid response:',
process.env.NODE_ENV === 'development'
? JSON.stringify(errors, null, 2)
: errors,
);
if (process.env.NODE_ENV === 'development') {
throw new Error(JSON.stringify(errors, null, 2));
} else {
this.logger.warn('Invalid response:', errors);
}
}
res.status(status).json(data);

View File

@ -157,9 +157,6 @@ Object {
],
"type": "object",
},
"emptySchema": Object {
"description": "emptySchema",
},
"environmentSchema": Object {
"additionalProperties": false,
"properties": Object {
@ -169,6 +166,9 @@ Object {
"name": Object {
"type": "string",
},
"protected": Object {
"type": "boolean",
},
"sortOrder": Object {
"type": "number",
},
@ -183,6 +183,25 @@ Object {
],
"type": "object",
},
"environmentsSchema": Object {
"additionalProperties": false,
"properties": Object {
"environments": Object {
"items": Object {
"$ref": "#/components/schemas/environmentSchema",
},
"type": "array",
},
"version": Object {
"type": "integer",
},
},
"required": Array [
"version",
"environments",
],
"type": "object",
},
"featureEnvironmentSchema": Object {
"additionalProperties": false,
"properties": Object {
@ -624,6 +643,12 @@ Object {
],
"type": "object",
},
"sortOrderSchema": Object {
"additionalProperties": Object {
"type": "number",
},
"type": "object",
},
"strategySchema": Object {
"additionalProperties": false,
"properties": Object {
@ -986,14 +1011,7 @@ Object {
],
"responses": Object {
"200": Object {
"content": Object {
"application/json": Object {
"schema": Object {
"$ref": "#/components/schemas/emptySchema",
},
},
},
"description": "emptySchema",
"description": "emptyResponse",
},
},
"tags": Array [
@ -1016,14 +1034,7 @@ Object {
],
"responses": Object {
"200": Object {
"content": Object {
"application/json": Object {
"schema": Object {
"$ref": "#/components/schemas/emptySchema",
},
},
},
"description": "emptySchema",
"description": "emptyResponse",
},
},
"tags": Array [
@ -1058,6 +1069,119 @@ Object {
],
},
},
"/api/admin/environments": Object {
"get": Object {
"operationId": "getAllEnvironments",
"responses": Object {
"200": Object {
"description": "emptyResponse",
},
},
"tags": Array [
"admin",
],
},
},
"/api/admin/environments/sort-order": Object {
"put": Object {
"operationId": "updateSortOrder",
"requestBody": Object {
"content": Object {
"application/json": Object {
"schema": Object {
"$ref": "#/components/schemas/sortOrderSchema",
},
},
},
"description": "sortOrderSchema",
"required": true,
},
"responses": Object {
"200": Object {
"description": "emptyResponse",
},
},
"tags": Array [
"admin",
],
},
},
"/api/admin/environments/{name}": Object {
"get": Object {
"operationId": "getEnvironment",
"parameters": Array [
Object {
"in": "path",
"name": "name",
"required": true,
"schema": Object {
"type": "string",
},
},
],
"responses": Object {
"200": Object {
"content": Object {
"application/json": Object {
"schema": Object {
"$ref": "#/components/schemas/environmentSchema",
},
},
},
"description": "environmentSchema",
},
},
"tags": Array [
"admin",
],
},
},
"/api/admin/environments/{name}/off": Object {
"post": Object {
"operationId": "toggleEnvironmentOff",
"parameters": Array [
Object {
"in": "path",
"name": "name",
"required": true,
"schema": Object {
"type": "string",
},
},
],
"responses": Object {
"200": Object {
"description": "emptyResponse",
},
},
"tags": Array [
"admin",
],
},
},
"/api/admin/environments/{name}/on": Object {
"post": Object {
"operationId": "toggleEnvironmentOn",
"parameters": Array [
Object {
"in": "path",
"name": "name",
"required": true,
"schema": Object {
"type": "string",
},
},
],
"responses": Object {
"200": Object {
"description": "emptyResponse",
},
},
"tags": Array [
"admin",
],
},
},
"/api/admin/feature-types": Object {
"get": Object {
"operationId": "getAllFeatureTypes",
@ -1104,14 +1228,7 @@ Object {
"operationId": "validateFeature",
"responses": Object {
"200": Object {
"content": Object {
"application/json": Object {
"schema": Object {
"$ref": "#/components/schemas/emptySchema",
},
},
},
"description": "emptySchema",
"description": "emptyResponse",
},
},
"tags": Array [
@ -1219,14 +1336,7 @@ Object {
],
"responses": Object {
"200": Object {
"content": Object {
"application/json": Object {
"schema": Object {
"$ref": "#/components/schemas/emptySchema",
},
},
},
"description": "emptySchema",
"description": "emptyResponse",
},
},
"tags": Array [
@ -1310,14 +1420,7 @@ Object {
},
"responses": Object {
"200": Object {
"content": Object {
"application/json": Object {
"schema": Object {
"$ref": "#/components/schemas/emptySchema",
},
},
},
"description": "emptySchema",
"description": "emptyResponse",
},
},
"tags": Array [
@ -1348,14 +1451,7 @@ Object {
],
"responses": Object {
"200": Object {
"content": Object {
"application/json": Object {
"schema": Object {
"$ref": "#/components/schemas/emptySchema",
},
},
},
"description": "emptySchema",
"description": "emptyResponse",
},
},
"tags": Array [
@ -1455,14 +1551,7 @@ Object {
],
"responses": Object {
"200": Object {
"content": Object {
"application/json": Object {
"schema": Object {
"$ref": "#/components/schemas/emptySchema",
},
},
},
"description": "emptySchema",
"description": "emptyResponse",
},
},
"tags": Array [
@ -1927,14 +2016,7 @@ Object {
],
"responses": Object {
"200": Object {
"content": Object {
"application/json": Object {
"schema": Object {
"$ref": "#/components/schemas/emptySchema",
},
},
},
"description": "emptySchema",
"description": "emptyResponse",
},
},
"tags": Array [