1
0
mirror of https://github.com/Unleash/unleash.git synced 2025-04-01 01:18:10 +02: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 { constraintSchema } from './spec/constraint-schema';
import { createFeatureSchema } from './spec/create-feature-schema'; import { createFeatureSchema } from './spec/create-feature-schema';
import { createStrategySchema } from './spec/create-strategy-schema'; import { createStrategySchema } from './spec/create-strategy-schema';
import { emptySchema } from './spec/empty-schema';
import { environmentSchema } from './spec/environment-schema'; import { environmentSchema } from './spec/environment-schema';
import { featureEnvironmentSchema } from './spec/feature-environment-schema'; import { featureEnvironmentSchema } from './spec/feature-environment-schema';
import { featureSchema } from './spec/feature-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 { variantSchema } from './spec/variant-schema';
import { variantsSchema } from './spec/variants-schema'; import { variantsSchema } from './spec/variants-schema';
import { versionSchema } from './spec/version-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". // Schemas must have $id property on the form "#/components/schemas/mySchema".
export type SchemaId = typeof schemas[keyof typeof schemas]['$id']; 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. // Schemas must list all $ref schemas in "components", including nested schemas.
export type SchemaRef = typeof schemas[keyof typeof schemas]['components']; 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 export interface AdminApiOperation
extends Omit<OpenAPIV3.OperationObject, 'tags'> { extends Omit<OpenAPIV3.OperationObject, 'tags'> {
operationId: string; operationId: string;
@ -56,8 +63,8 @@ export const schemas = {
constraintSchema, constraintSchema,
createFeatureSchema, createFeatureSchema,
createStrategySchema, createStrategySchema,
emptySchema,
environmentSchema, environmentSchema,
environmentsSchema,
featureEnvironmentSchema, featureEnvironmentSchema,
featureSchema, featureSchema,
featureStrategySchema, featureStrategySchema,
@ -74,6 +81,7 @@ export const schemas = {
projectEnvironmentSchema, projectEnvironmentSchema,
projectSchema, projectSchema,
projectsSchema, projectsSchema,
sortOrderSchema,
strategySchema, strategySchema,
tagSchema, tagSchema,
tagsSchema, 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 = ( export const createOpenApiSchema = (
serverUrl?: string, serverUrl?: string,
): Omit<OpenAPIV3.Document, 'paths'> => { ): Omit<OpenAPIV3.Document, 'paths'> => {
@ -135,9 +150,7 @@ export const createOpenApiSchema = (
name: 'Authorization', name: 'Authorization',
}, },
}, },
schemas: mapValues(schemas, (schema) => schemas: mapValues(schemas, removeJsonSchemaProps),
omitKeys(schema, '$id', 'components'),
),
}, },
}; };
}; };

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: { enabled: {
type: 'boolean', type: 'boolean',
}, },
protected: {
type: 'boolean',
},
sortOrder: { sortOrder: {
type: 'number', 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 { serializeDates } from '../../types/serialize-dates';
import { OpenApiService } from '../../services/openapi-service'; import { OpenApiService } from '../../services/openapi-service';
import { createResponseSchema } from '../../openapi'; import { createResponseSchema } from '../../openapi';
import { EmptySchema } from '../../openapi/spec/empty-schema'; import { emptyResponse } from '../../openapi/spec/empty-response';
export default class ArchiveController extends Controller { export default class ArchiveController extends Controller {
private readonly logger: Logger; private readonly logger: Logger;
@ -75,7 +75,7 @@ export default class ArchiveController extends Controller {
openApiService.validPath({ openApiService.validPath({
tags: ['admin'], tags: ['admin'],
operationId: 'deleteFeature', operationId: 'deleteFeature',
responses: { 200: createResponseSchema('emptySchema') }, responses: { 200: emptyResponse },
}), }),
], ],
}); });
@ -90,7 +90,7 @@ export default class ArchiveController extends Controller {
openApiService.validPath({ openApiService.validPath({
tags: ['admin'], tags: ['admin'],
operationId: 'reviveFeature', operationId: 'reviveFeature',
responses: { 200: createResponseSchema('emptySchema') }, responses: { 200: emptyResponse },
}), }),
], ],
}); });
@ -131,7 +131,7 @@ export default class ArchiveController extends Controller {
async deleteFeature( async deleteFeature(
req: IAuthRequest<{ featureName: string }>, req: IAuthRequest<{ featureName: string }>,
res: Response<EmptySchema>, res: Response<void>,
): Promise<void> { ): Promise<void> {
const { featureName } = req.params; const { featureName } = req.params;
const user = extractUsername(req); const user = extractUsername(req);
@ -141,7 +141,7 @@ export default class ArchiveController extends Controller {
async reviveFeature( async reviveFeature(
req: IAuthRequest<{ featureName: string }>, req: IAuthRequest<{ featureName: string }>,
res: Response<EmptySchema>, res: Response<void>,
): Promise<void> { ): Promise<void> {
const userName = extractUsername(req); const userName = extractUsername(req);
const { featureName } = req.params; 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 { serializeDates } from '../../types/serialize-dates';
import { OpenApiService } from '../../services/openapi-service'; import { OpenApiService } from '../../services/openapi-service';
import { createRequestSchema, createResponseSchema } from '../../openapi'; import { createRequestSchema, createResponseSchema } from '../../openapi';
import { emptyResponse } from '../../openapi/spec/empty-response';
const version = 1; const version = 1;
@ -92,7 +93,7 @@ class FeatureController extends Controller {
openApiService.validPath({ openApiService.validPath({
tags: ['admin'], tags: ['admin'],
operationId: 'validateFeature', operationId: 'validateFeature',
responses: { 200: createResponseSchema('emptySchema') }, responses: { 200: emptyResponse },
}), }),
], ],
}); });
@ -136,7 +137,7 @@ class FeatureController extends Controller {
openApiService.validPath({ openApiService.validPath({
tags: ['admin'], tags: ['admin'],
operationId: 'removeTag', operationId: 'removeTag',
responses: { 200: createResponseSchema('emptySchema') }, responses: { 200: emptyResponse },
}), }),
], ],
}); });

View File

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

View File

@ -5,8 +5,9 @@ import { IUnleashServices } from '../../../types/services';
import { Logger } from '../../../logger'; import { Logger } from '../../../logger';
import EnvironmentService from '../../../services/environment-service'; import EnvironmentService from '../../../services/environment-service';
import { UPDATE_PROJECT } from '../../../types/permissions'; import { UPDATE_PROJECT } from '../../../types/permissions';
import { createRequestSchema, createResponseSchema } from '../../../openapi'; import { createRequestSchema } from '../../../openapi';
import { ProjectEnvironmentSchema } from '../../../openapi/spec/project-environment-schema'; import { ProjectEnvironmentSchema } from '../../../openapi/spec/project-environment-schema';
import { emptyResponse } from '../../../openapi/spec/empty-response';
const PREFIX = '/:projectId/environments'; const PREFIX = '/:projectId/environments';
@ -44,7 +45,7 @@ export default class EnvironmentsController extends Controller {
requestBody: createRequestSchema( requestBody: createRequestSchema(
'projectEnvironmentSchema', 'projectEnvironmentSchema',
), ),
responses: { 200: createResponseSchema('emptySchema') }, responses: { 200: emptyResponse },
}), }),
], ],
}); });
@ -59,7 +60,7 @@ export default class EnvironmentsController extends Controller {
openApiService.validPath({ openApiService.validPath({
tags: ['admin'], tags: ['admin'],
operationId: 'removeEnvironmentFromProject', 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 { OpenApiService } from '../../../services/openapi-service';
import { createRequestSchema, createResponseSchema } from '../../../openapi'; import { createRequestSchema, createResponseSchema } from '../../../openapi';
import { FeatureEnvironmentSchema } from '../../../openapi/spec/feature-environment-schema'; import { FeatureEnvironmentSchema } from '../../../openapi/spec/feature-environment-schema';
import { emptyResponse } from '../../../openapi/spec/empty-response';
interface FeatureStrategyParams { interface FeatureStrategyParams {
projectId: string; projectId: string;
@ -216,7 +217,7 @@ export default class ProjectFeaturesController extends Controller {
openApiService.validPath({ openApiService.validPath({
operationId: 'deleteStrategy', operationId: 'deleteStrategy',
tags: ['admin'], tags: ['admin'],
responses: { 200: createResponseSchema('emptySchema') }, responses: { 200: emptyResponse },
}), }),
], ],
}); });
@ -322,7 +323,7 @@ export default class ProjectFeaturesController extends Controller {
openApiService.validPath({ openApiService.validPath({
tags: ['admin'], tags: ['admin'],
operationId: 'archiveFeature', operationId: 'archiveFeature',
responses: { 200: createResponseSchema('emptySchema') }, responses: { 200: emptyResponse },
}), }),
], ],
}); });

View File

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

View File

@ -157,9 +157,6 @@ Object {
], ],
"type": "object", "type": "object",
}, },
"emptySchema": Object {
"description": "emptySchema",
},
"environmentSchema": Object { "environmentSchema": Object {
"additionalProperties": false, "additionalProperties": false,
"properties": Object { "properties": Object {
@ -169,6 +166,9 @@ Object {
"name": Object { "name": Object {
"type": "string", "type": "string",
}, },
"protected": Object {
"type": "boolean",
},
"sortOrder": Object { "sortOrder": Object {
"type": "number", "type": "number",
}, },
@ -183,6 +183,25 @@ Object {
], ],
"type": "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 { "featureEnvironmentSchema": Object {
"additionalProperties": false, "additionalProperties": false,
"properties": Object { "properties": Object {
@ -624,6 +643,12 @@ Object {
], ],
"type": "object", "type": "object",
}, },
"sortOrderSchema": Object {
"additionalProperties": Object {
"type": "number",
},
"type": "object",
},
"strategySchema": Object { "strategySchema": Object {
"additionalProperties": false, "additionalProperties": false,
"properties": Object { "properties": Object {
@ -986,14 +1011,7 @@ Object {
], ],
"responses": Object { "responses": Object {
"200": Object { "200": Object {
"content": Object { "description": "emptyResponse",
"application/json": Object {
"schema": Object {
"$ref": "#/components/schemas/emptySchema",
},
},
},
"description": "emptySchema",
}, },
}, },
"tags": Array [ "tags": Array [
@ -1016,14 +1034,7 @@ Object {
], ],
"responses": Object { "responses": Object {
"200": Object { "200": Object {
"content": Object { "description": "emptyResponse",
"application/json": Object {
"schema": Object {
"$ref": "#/components/schemas/emptySchema",
},
},
},
"description": "emptySchema",
}, },
}, },
"tags": Array [ "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 { "/api/admin/feature-types": Object {
"get": Object { "get": Object {
"operationId": "getAllFeatureTypes", "operationId": "getAllFeatureTypes",
@ -1104,14 +1228,7 @@ Object {
"operationId": "validateFeature", "operationId": "validateFeature",
"responses": Object { "responses": Object {
"200": Object { "200": Object {
"content": Object { "description": "emptyResponse",
"application/json": Object {
"schema": Object {
"$ref": "#/components/schemas/emptySchema",
},
},
},
"description": "emptySchema",
}, },
}, },
"tags": Array [ "tags": Array [
@ -1219,14 +1336,7 @@ Object {
], ],
"responses": Object { "responses": Object {
"200": Object { "200": Object {
"content": Object { "description": "emptyResponse",
"application/json": Object {
"schema": Object {
"$ref": "#/components/schemas/emptySchema",
},
},
},
"description": "emptySchema",
}, },
}, },
"tags": Array [ "tags": Array [
@ -1310,14 +1420,7 @@ Object {
}, },
"responses": Object { "responses": Object {
"200": Object { "200": Object {
"content": Object { "description": "emptyResponse",
"application/json": Object {
"schema": Object {
"$ref": "#/components/schemas/emptySchema",
},
},
},
"description": "emptySchema",
}, },
}, },
"tags": Array [ "tags": Array [
@ -1348,14 +1451,7 @@ Object {
], ],
"responses": Object { "responses": Object {
"200": Object { "200": Object {
"content": Object { "description": "emptyResponse",
"application/json": Object {
"schema": Object {
"$ref": "#/components/schemas/emptySchema",
},
},
},
"description": "emptySchema",
}, },
}, },
"tags": Array [ "tags": Array [
@ -1455,14 +1551,7 @@ Object {
], ],
"responses": Object { "responses": Object {
"200": Object { "200": Object {
"content": Object { "description": "emptyResponse",
"application/json": Object {
"schema": Object {
"$ref": "#/components/schemas/emptySchema",
},
},
},
"description": "emptySchema",
}, },
}, },
"tags": Array [ "tags": Array [
@ -1927,14 +2016,7 @@ Object {
], ],
"responses": Object { "responses": Object {
"200": Object { "200": Object {
"content": Object { "description": "emptyResponse",
"application/json": Object {
"schema": Object {
"$ref": "#/components/schemas/emptySchema",
},
},
},
"description": "emptySchema",
}, },
}, },
"tags": Array [ "tags": Array [