diff --git a/src/lib/openapi/index.test.ts b/src/lib/openapi/index.test.ts new file mode 100644 index 0000000000..f30709d0f3 --- /dev/null +++ b/src/lib/openapi/index.test.ts @@ -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', + ); +}); diff --git a/src/lib/openapi/index.ts b/src/lib/openapi/index.ts index 855f034eff..3fef56e82c 100644 --- a/src/lib/openapi/index.ts +++ b/src/lib/openapi/index.ts @@ -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 { 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 = ( + schema: T, +): OpenAPIV3.SchemaObject => { + return omitKeys(schema, '$id', 'components'); +}; + export const createOpenApiSchema = ( serverUrl?: string, ): Omit => { @@ -135,9 +150,7 @@ export const createOpenApiSchema = ( name: 'Authorization', }, }, - schemas: mapValues(schemas, (schema) => - omitKeys(schema, '$id', 'components'), - ), + schemas: mapValues(schemas, removeJsonSchemaProps), }, }; }; diff --git a/src/lib/openapi/spec/__snapshots__/sort-order-schema.test.ts.snap b/src/lib/openapi/spec/__snapshots__/sort-order-schema.test.ts.snap new file mode 100644 index 0000000000..3f6fbd77eb --- /dev/null +++ b/src/lib/openapi/spec/__snapshots__/sort-order-schema.test.ts.snap @@ -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", +} +`; diff --git a/src/lib/openapi/spec/empty-response.ts b/src/lib/openapi/spec/empty-response.ts new file mode 100644 index 0000000000..37879aba23 --- /dev/null +++ b/src/lib/openapi/spec/empty-response.ts @@ -0,0 +1,3 @@ +export const emptyResponse = { + description: 'emptyResponse', +}; diff --git a/src/lib/openapi/spec/empty-schema.ts b/src/lib/openapi/spec/empty-schema.ts deleted file mode 100644 index f3fc5cf171..0000000000 --- a/src/lib/openapi/spec/empty-schema.ts +++ /dev/null @@ -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; diff --git a/src/lib/openapi/spec/environment-schema.ts b/src/lib/openapi/spec/environment-schema.ts index befed054dc..3fec0e4b83 100644 --- a/src/lib/openapi/spec/environment-schema.ts +++ b/src/lib/openapi/spec/environment-schema.ts @@ -15,6 +15,9 @@ export const environmentSchema = { enabled: { type: 'boolean', }, + protected: { + type: 'boolean', + }, sortOrder: { type: 'number', }, diff --git a/src/lib/openapi/spec/environments-schema.ts b/src/lib/openapi/spec/environments-schema.ts new file mode 100644 index 0000000000..6f26ce9cc3 --- /dev/null +++ b/src/lib/openapi/spec/environments-schema.ts @@ -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; diff --git a/src/lib/openapi/spec/sort-order-schema.test.ts b/src/lib/openapi/spec/sort-order-schema.test.ts new file mode 100644 index 0000000000..b72df33a8e --- /dev/null +++ b/src/lib/openapi/spec/sort-order-schema.test.ts @@ -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(); +}); diff --git a/src/lib/openapi/spec/sort-order-schema.ts b/src/lib/openapi/spec/sort-order-schema.ts new file mode 100644 index 0000000000..d809a731bc --- /dev/null +++ b/src/lib/openapi/spec/sort-order-schema.ts @@ -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; diff --git a/src/lib/routes/admin-api/api-token-controller.ts b/src/lib/routes/admin-api/api-token.ts similarity index 100% rename from src/lib/routes/admin-api/api-token-controller.ts rename to src/lib/routes/admin-api/api-token.ts diff --git a/src/lib/routes/admin-api/archive.ts b/src/lib/routes/admin-api/archive.ts index 53682ad196..e698ba4784 100644 --- a/src/lib/routes/admin-api/archive.ts +++ b/src/lib/routes/admin-api/archive.ts @@ -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, + res: Response, ): Promise { 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, + res: Response, ): Promise { const userName = extractUsername(req); const { featureName } = req.params; diff --git a/src/lib/routes/admin-api/bootstrap-controller.ts b/src/lib/routes/admin-api/bootstrap.ts similarity index 100% rename from src/lib/routes/admin-api/bootstrap-controller.ts rename to src/lib/routes/admin-api/bootstrap.ts diff --git a/src/lib/routes/admin-api/environments-controller.ts b/src/lib/routes/admin-api/environments-controller.ts deleted file mode 100644 index 9f5085677b..0000000000 --- a/src/lib/routes/admin-api/environments-controller.ts +++ /dev/null @@ -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, - ) { - 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 { - const environments = await this.service.getAll(); - res.status(200).json({ version: 1, environments }); - } - - async updateSortOrder( - req: Request, - res: Response, - ): Promise { - await this.service.updateSortOrder(req.body); - res.status(200).end(); - } - - async toggleEnvironmentOn( - req: Request, - res: Response, - ): Promise { - const { name } = req.params; - await this.service.toggleEnvironment(name, true); - res.status(204).end(); - } - - async toggleEnvironmentOff( - req: Request, - res: Response, - ): Promise { - const { name } = req.params; - await this.service.toggleEnvironment(name, false); - res.status(204).end(); - } - - async getEnv( - req: Request, - res: Response, - ): Promise { - const { name } = req.params; - - const env = await this.service.get(name); - res.status(200).json(env); - } -} diff --git a/src/lib/routes/admin-api/environments.ts b/src/lib/routes/admin-api/environments.ts new file mode 100644 index 0000000000..d1c785f264 --- /dev/null +++ b/src/lib/routes/admin-api/environments.ts @@ -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, + ) { + 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, + ): Promise { + this.openApiService.respondWithValidation( + 200, + res, + environmentsSchema.$id, + { version: 1, environments: await this.service.getAll() }, + ); + } + + async updateSortOrder( + req: Request, + res: Response, + ): Promise { + await this.service.updateSortOrder(req.body); + res.status(200).end(); + } + + async toggleEnvironmentOn( + req: Request, + res: Response, + ): Promise { + const { name } = req.params; + await this.service.toggleEnvironment(name, true); + res.status(204).end(); + } + + async toggleEnvironmentOff( + req: Request, + res: Response, + ): Promise { + const { name } = req.params; + await this.service.toggleEnvironment(name, false); + res.status(204).end(); + } + + async getEnvironment( + req: Request, + res: Response, + ): Promise { + this.openApiService.respondWithValidation( + 200, + res, + environmentSchema.$id, + await this.service.get(req.params.name), + ); + } +} diff --git a/src/lib/routes/admin-api/feature.ts b/src/lib/routes/admin-api/feature.ts index 93524ae78f..320d2f75eb 100644 --- a/src/lib/routes/admin-api/feature.ts +++ b/src/lib/routes/admin-api/feature.ts @@ -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 }, }), ], }); diff --git a/src/lib/routes/admin-api/index.ts b/src/lib/routes/admin-api/index.ts index 69493f7de8..45a6679679 100644 --- a/src/lib/routes/admin-api/index.ts +++ b/src/lib/routes/admin-api/index.ts @@ -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 { diff --git a/src/lib/routes/admin-api/project/environments.ts b/src/lib/routes/admin-api/project/environments.ts index c7336b21b3..f54ef1a6d7 100644 --- a/src/lib/routes/admin-api/project/environments.ts +++ b/src/lib/routes/admin-api/project/environments.ts @@ -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 }, }), ], }); diff --git a/src/lib/routes/admin-api/project/features.ts b/src/lib/routes/admin-api/project/features.ts index bb9ccda64c..52528a957a 100644 --- a/src/lib/routes/admin-api/project/features.ts +++ b/src/lib/routes/admin-api/project/features.ts @@ -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 }, }), ], }); diff --git a/src/lib/routes/admin-api/user-feedback-controller.ts b/src/lib/routes/admin-api/user-feedback.ts similarity index 100% rename from src/lib/routes/admin-api/user-feedback-controller.ts rename to src/lib/routes/admin-api/user-feedback.ts diff --git a/src/lib/routes/admin-api/user-splash-controller.ts b/src/lib/routes/admin-api/user-splash.ts similarity index 100% rename from src/lib/routes/admin-api/user-splash-controller.ts rename to src/lib/routes/admin-api/user-splash.ts diff --git a/src/lib/services/openapi-service.ts b/src/lib/services/openapi-service.ts index d477d88950..56d1011343 100644 --- a/src/lib/services/openapi-service.ts +++ b/src/lib/services/openapi-service.ts @@ -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(schemas: { - [name: string]: { $id: string; components: T }; - }): void { + registerCustomSchemas( + schemas: Record, + ): 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); diff --git a/src/test/e2e/api/openapi/__snapshots__/openapi.e2e.test.ts.snap b/src/test/e2e/api/openapi/__snapshots__/openapi.e2e.test.ts.snap index c7ffc6ef38..00164a9947 100644 --- a/src/test/e2e/api/openapi/__snapshots__/openapi.e2e.test.ts.snap +++ b/src/test/e2e/api/openapi/__snapshots__/openapi.e2e.test.ts.snap @@ -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 [