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:
parent
18e63d5ea3
commit
adface17c7
52
src/lib/openapi/index.test.ts
Normal file
52
src/lib/openapi/index.test.ts
Normal 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',
|
||||||
|
);
|
||||||
|
});
|
@ -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'),
|
|
||||||
),
|
|
||||||
},
|
},
|
||||||
};
|
};
|
||||||
};
|
};
|
||||||
|
@ -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",
|
||||||
|
}
|
||||||
|
`;
|
3
src/lib/openapi/spec/empty-response.ts
Normal file
3
src/lib/openapi/spec/empty-response.ts
Normal file
@ -0,0 +1,3 @@
|
|||||||
|
export const emptyResponse = {
|
||||||
|
description: 'emptyResponse',
|
||||||
|
};
|
@ -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>;
|
|
@ -15,6 +15,9 @@ export const environmentSchema = {
|
|||||||
enabled: {
|
enabled: {
|
||||||
type: 'boolean',
|
type: 'boolean',
|
||||||
},
|
},
|
||||||
|
protected: {
|
||||||
|
type: 'boolean',
|
||||||
|
},
|
||||||
sortOrder: {
|
sortOrder: {
|
||||||
type: 'number',
|
type: 'number',
|
||||||
},
|
},
|
||||||
|
27
src/lib/openapi/spec/environments-schema.ts
Normal file
27
src/lib/openapi/spec/environments-schema.ts
Normal 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>;
|
19
src/lib/openapi/spec/sort-order-schema.test.ts
Normal file
19
src/lib/openapi/spec/sort-order-schema.test.ts
Normal 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();
|
||||||
|
});
|
12
src/lib/openapi/spec/sort-order-schema.ts
Normal file
12
src/lib/openapi/spec/sort-order-schema.ts
Normal 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>;
|
@ -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;
|
||||||
|
@ -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);
|
|
||||||
}
|
|
||||||
}
|
|
169
src/lib/routes/admin-api/environments.ts
Normal file
169
src/lib/routes/admin-api/environments.ts
Normal 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),
|
||||||
|
);
|
||||||
|
}
|
||||||
|
}
|
@ -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 },
|
||||||
}),
|
}),
|
||||||
],
|
],
|
||||||
});
|
});
|
||||||
|
@ -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 {
|
||||||
|
@ -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 },
|
||||||
}),
|
}),
|
||||||
],
|
],
|
||||||
});
|
});
|
||||||
|
@ -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 },
|
||||||
}),
|
}),
|
||||||
],
|
],
|
||||||
});
|
});
|
||||||
|
@ -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);
|
||||||
|
@ -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 [
|
||||||
|
Loading…
Reference in New Issue
Block a user