mirror of
https://github.com/Unleash/unleash.git
synced 2024-12-22 19:07:54 +01:00
task: add open-api for tag-types (#1700)
* task: add open-api for tag-types
This commit is contained in:
parent
7ba8cd05eb
commit
780bb06dba
@ -34,6 +34,10 @@ 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 { tagTypeSchema } from './spec/tag-type-schema';
|
||||
import { tagTypesSchema } from './spec/tag-types-schema';
|
||||
import { updateTagTypeSchema } from './spec/update-tag-type-schema';
|
||||
import { validateTagTypeSchema } from './spec/validate-tag-type-schema';
|
||||
|
||||
// All schemas in `openapi/spec` should be listed here.
|
||||
export const schemas = {
|
||||
@ -64,9 +68,13 @@ export const schemas = {
|
||||
strategySchema,
|
||||
tagSchema,
|
||||
tagsSchema,
|
||||
tagTypeSchema,
|
||||
tagTypesSchema,
|
||||
uiConfigSchema,
|
||||
updateFeatureSchema,
|
||||
updateStrategySchema,
|
||||
updateTagTypeSchema,
|
||||
validateTagTypeSchema,
|
||||
variantSchema,
|
||||
variantsSchema,
|
||||
versionSchema,
|
||||
|
22
src/lib/openapi/spec/tag-type-schema.ts
Normal file
22
src/lib/openapi/spec/tag-type-schema.ts
Normal file
@ -0,0 +1,22 @@
|
||||
import { FromSchema } from 'json-schema-to-ts';
|
||||
|
||||
export const tagTypeSchema = {
|
||||
$id: '#/components/schemas/tagTypeSchema',
|
||||
type: 'object',
|
||||
additionalProperties: false,
|
||||
required: ['name'],
|
||||
properties: {
|
||||
name: {
|
||||
type: 'string',
|
||||
},
|
||||
description: {
|
||||
type: 'string',
|
||||
},
|
||||
icon: {
|
||||
type: 'string',
|
||||
},
|
||||
},
|
||||
components: {},
|
||||
} as const;
|
||||
|
||||
export type TagTypeSchema = FromSchema<typeof tagTypeSchema>;
|
27
src/lib/openapi/spec/tag-types-schema.ts
Normal file
27
src/lib/openapi/spec/tag-types-schema.ts
Normal file
@ -0,0 +1,27 @@
|
||||
import { tagTypeSchema } from './tag-type-schema';
|
||||
import { FromSchema } from 'json-schema-to-ts';
|
||||
|
||||
export const tagTypesSchema = {
|
||||
$id: '#/components/schemas/tagTypesSchema',
|
||||
type: 'object',
|
||||
additionalProperties: false,
|
||||
required: ['version', 'tagTypes'],
|
||||
properties: {
|
||||
version: {
|
||||
type: 'integer',
|
||||
},
|
||||
tagTypes: {
|
||||
type: 'array',
|
||||
items: {
|
||||
$ref: '#/components/schemas/tagTypeSchema',
|
||||
},
|
||||
},
|
||||
},
|
||||
components: {
|
||||
schemas: {
|
||||
tagTypeSchema,
|
||||
},
|
||||
},
|
||||
} as const;
|
||||
|
||||
export type TagTypesSchema = FromSchema<typeof tagTypesSchema>;
|
18
src/lib/openapi/spec/update-tag-type-schema.ts
Normal file
18
src/lib/openapi/spec/update-tag-type-schema.ts
Normal file
@ -0,0 +1,18 @@
|
||||
import { FromSchema } from 'json-schema-to-ts';
|
||||
|
||||
export const updateTagTypeSchema = {
|
||||
$id: '#/components/schemas/updateTagTypeSchema',
|
||||
type: 'object',
|
||||
additionalProperties: false,
|
||||
properties: {
|
||||
description: {
|
||||
type: 'string',
|
||||
},
|
||||
icon: {
|
||||
type: 'string',
|
||||
},
|
||||
},
|
||||
components: {},
|
||||
} as const;
|
||||
|
||||
export type UpdateTagTypeSchema = FromSchema<typeof updateTagTypeSchema>;
|
24
src/lib/openapi/spec/validate-tag-type-schema.ts
Normal file
24
src/lib/openapi/spec/validate-tag-type-schema.ts
Normal file
@ -0,0 +1,24 @@
|
||||
import { FromSchema } from 'json-schema-to-ts';
|
||||
import { tagTypeSchema } from './tag-type-schema';
|
||||
|
||||
export const validateTagTypeSchema = {
|
||||
$id: '#/components/schemas/validateTagTypeSchema',
|
||||
type: 'object',
|
||||
additionalProperties: false,
|
||||
required: ['valid', 'tagType'],
|
||||
properties: {
|
||||
valid: {
|
||||
type: 'boolean',
|
||||
},
|
||||
tagType: {
|
||||
$ref: '#/components/schemas/tagTypeSchema',
|
||||
},
|
||||
},
|
||||
components: {
|
||||
schemas: {
|
||||
tagTypeSchema,
|
||||
},
|
||||
},
|
||||
} as const;
|
||||
|
||||
export type ValidateTagTypeSchema = FromSchema<typeof validateTagTypeSchema>;
|
@ -1,13 +1,27 @@
|
||||
import { Request, Response } from 'express';
|
||||
import Controller from '../controller';
|
||||
|
||||
import { DELETE_TAG_TYPE, UPDATE_TAG_TYPE } from '../../types/permissions';
|
||||
import {
|
||||
DELETE_TAG_TYPE,
|
||||
NONE,
|
||||
UPDATE_TAG_TYPE,
|
||||
} from '../../types/permissions';
|
||||
import { extractUsername } from '../../util/extract-user';
|
||||
import { IUnleashConfig } from '../../types/option';
|
||||
import { IUnleashServices } from '../../types/services';
|
||||
import TagTypeService from '../../services/tag-type-service';
|
||||
import { Logger } from '../../logger';
|
||||
import { IAuthRequest } from '../unleash-types';
|
||||
import { createRequestSchema, createResponseSchema } from '../../openapi';
|
||||
import { TagTypesSchema } from '../../openapi/spec/tag-types-schema';
|
||||
import { emptyResponse } from '../../openapi/spec/empty-response';
|
||||
import { ValidateTagTypeSchema } from '../../openapi/spec/validate-tag-type-schema';
|
||||
import {
|
||||
tagTypeSchema,
|
||||
TagTypeSchema,
|
||||
} from '../../openapi/spec/tag-type-schema';
|
||||
import { UpdateTagTypeSchema } from '../../openapi/spec/update-tag-type-schema';
|
||||
import { OpenApiService } from '../../services/openapi-service';
|
||||
|
||||
const version = 1;
|
||||
|
||||
@ -16,32 +30,134 @@ class TagTypeController extends Controller {
|
||||
|
||||
private tagTypeService: TagTypeService;
|
||||
|
||||
private openApiService: OpenApiService;
|
||||
|
||||
constructor(
|
||||
config: IUnleashConfig,
|
||||
{ tagTypeService }: Pick<IUnleashServices, 'tagTypeService'>,
|
||||
{
|
||||
tagTypeService,
|
||||
openApiService,
|
||||
}: Pick<IUnleashServices, 'tagTypeService' | 'openApiService'>,
|
||||
) {
|
||||
super(config);
|
||||
this.logger = config.getLogger('/admin-api/tag-type.js');
|
||||
this.tagTypeService = tagTypeService;
|
||||
this.get('/', this.getTagTypes);
|
||||
this.post('/', this.createTagType, UPDATE_TAG_TYPE);
|
||||
this.post('/validate', this.validate, UPDATE_TAG_TYPE);
|
||||
this.get('/:name', this.getTagType);
|
||||
this.put('/:name', this.updateTagType, UPDATE_TAG_TYPE);
|
||||
this.delete('/:name', this.deleteTagType, DELETE_TAG_TYPE);
|
||||
this.openApiService = openApiService;
|
||||
this.route({
|
||||
method: 'get',
|
||||
path: '',
|
||||
handler: this.getTagTypes,
|
||||
permission: NONE,
|
||||
middleware: [
|
||||
openApiService.validPath({
|
||||
tags: ['admin'],
|
||||
operationId: 'getTagTypes',
|
||||
responses: { 200: createResponseSchema('tagTypesSchema') },
|
||||
}),
|
||||
],
|
||||
});
|
||||
this.route({
|
||||
method: 'post',
|
||||
path: '',
|
||||
handler: this.createTagType,
|
||||
permission: UPDATE_TAG_TYPE,
|
||||
middleware: [
|
||||
openApiService.validPath({
|
||||
tags: ['admin'],
|
||||
operationId: 'createTagType',
|
||||
responses: { 201: createResponseSchema('tagTypeSchema') },
|
||||
requestBody: createRequestSchema('tagTypeSchema'),
|
||||
}),
|
||||
],
|
||||
});
|
||||
this.route({
|
||||
method: 'post',
|
||||
path: '/validate',
|
||||
handler: this.validateTagType,
|
||||
permission: UPDATE_TAG_TYPE,
|
||||
middleware: [
|
||||
openApiService.validPath({
|
||||
tags: ['admin'],
|
||||
operationId: 'validateTagType',
|
||||
responses: {
|
||||
200: createResponseSchema('validateTagTypeSchema'),
|
||||
},
|
||||
requestBody: createRequestSchema('tagTypeSchema'),
|
||||
}),
|
||||
],
|
||||
});
|
||||
this.route({
|
||||
method: 'get',
|
||||
path: '/:name',
|
||||
handler: this.getTagType,
|
||||
permission: NONE,
|
||||
middleware: [
|
||||
openApiService.validPath({
|
||||
tags: ['admin'],
|
||||
operationId: 'getTagType',
|
||||
responses: {
|
||||
200: createResponseSchema('tagTypeSchema'),
|
||||
},
|
||||
}),
|
||||
],
|
||||
});
|
||||
this.route({
|
||||
method: 'put',
|
||||
path: '/:name',
|
||||
handler: this.updateTagType,
|
||||
permission: UPDATE_TAG_TYPE,
|
||||
middleware: [
|
||||
openApiService.validPath({
|
||||
tags: ['admin'],
|
||||
operationId: 'updateTagType',
|
||||
responses: {
|
||||
200: emptyResponse,
|
||||
},
|
||||
requestBody: createRequestSchema('updateTagTypeSchema'),
|
||||
}),
|
||||
],
|
||||
});
|
||||
this.route({
|
||||
method: 'delete',
|
||||
path: '/:name',
|
||||
handler: this.deleteTagType,
|
||||
acceptAnyContentType: true,
|
||||
permission: DELETE_TAG_TYPE,
|
||||
middleware: [
|
||||
openApiService.validPath({
|
||||
tags: ['admin'],
|
||||
operationId: 'deleteTagType',
|
||||
responses: {
|
||||
200: emptyResponse,
|
||||
},
|
||||
}),
|
||||
],
|
||||
});
|
||||
}
|
||||
|
||||
async getTagTypes(req: Request, res: Response): Promise<void> {
|
||||
async getTagTypes(
|
||||
req: Request,
|
||||
res: Response<TagTypesSchema>,
|
||||
): Promise<void> {
|
||||
const tagTypes = await this.tagTypeService.getAll();
|
||||
res.json({ version, tagTypes });
|
||||
}
|
||||
|
||||
async validate(req: Request, res: Response): Promise<void> {
|
||||
async validateTagType(
|
||||
req: Request<unknown, unknown, TagTypeSchema>,
|
||||
res: Response<ValidateTagTypeSchema>,
|
||||
): Promise<void> {
|
||||
await this.tagTypeService.validate(req.body);
|
||||
res.status(200).json({ valid: true, tagType: req.body });
|
||||
this.openApiService.respondWithValidation(200, res, tagTypeSchema.$id, {
|
||||
valid: true,
|
||||
tagType: req.body,
|
||||
});
|
||||
}
|
||||
|
||||
async createTagType(req: IAuthRequest, res: Response): Promise<void> {
|
||||
async createTagType(
|
||||
req: IAuthRequest<unknown, unknown, TagTypeSchema>,
|
||||
res: Response,
|
||||
): Promise<void> {
|
||||
const userName = extractUsername(req);
|
||||
const tagType = await this.tagTypeService.createTagType(
|
||||
req.body,
|
||||
@ -50,7 +166,10 @@ class TagTypeController extends Controller {
|
||||
res.status(201).json(tagType);
|
||||
}
|
||||
|
||||
async updateTagType(req: IAuthRequest, res: Response): Promise<void> {
|
||||
async updateTagType(
|
||||
req: IAuthRequest<{ name: string }, unknown, UpdateTagTypeSchema>,
|
||||
res: Response,
|
||||
): Promise<void> {
|
||||
const { description, icon } = req.body;
|
||||
const { name } = req.params;
|
||||
const userName = extractUsername(req);
|
||||
|
@ -46,12 +46,15 @@ test('querying a tag-type that does not exist yields 404', async () => {
|
||||
});
|
||||
|
||||
test('Can create a new tag type', async () => {
|
||||
await app.request.post('/api/admin/tag-types').send({
|
||||
name: 'slack',
|
||||
description:
|
||||
'Tag your feature toggles with slack channel to post updates for toggle to',
|
||||
icon: 'http://icons.iconarchive.com/icons/papirus-team/papirus-apps/32/slack-icon.png',
|
||||
});
|
||||
await app.request
|
||||
.post('/api/admin/tag-types')
|
||||
.send({
|
||||
name: 'slack',
|
||||
description:
|
||||
'Tag your feature toggles with slack channel to post updates for toggle to',
|
||||
icon: 'http://icons.iconarchive.com/icons/papirus-team/papirus-apps/32/slack-icon.png',
|
||||
})
|
||||
.expect(201);
|
||||
return app.request
|
||||
.get('/api/admin/tag-types/slack')
|
||||
.expect('Content-Type', /json/)
|
||||
@ -97,7 +100,7 @@ test('Can update a tag types description and icon', async () => {
|
||||
expect(res.body.tagType.icon).toBe('$');
|
||||
});
|
||||
});
|
||||
test('Invalid updates gets rejected', async () => {
|
||||
test('Numbers are coerced to strings for icons and descriptions', async () => {
|
||||
await app.request.get('/api/admin/tag-types/simple').expect(200);
|
||||
await app.request
|
||||
.put('/api/admin/tag-types/simple')
|
||||
@ -105,13 +108,7 @@ test('Invalid updates gets rejected', async () => {
|
||||
description: 15125,
|
||||
icon: 125,
|
||||
})
|
||||
.expect(400)
|
||||
.expect((res) => {
|
||||
expect(res.body.details[0].message).toBe(
|
||||
'"description" must be a string',
|
||||
);
|
||||
expect(res.body.details[1].message).toBe('"icon" must be a string');
|
||||
});
|
||||
.expect(200);
|
||||
});
|
||||
|
||||
test('Validation of tag-types returns 200 for valid tag-types', async () => {
|
||||
@ -128,6 +125,21 @@ test('Validation of tag-types returns 200 for valid tag-types', async () => {
|
||||
expect(res.body.valid).toBe(true);
|
||||
});
|
||||
});
|
||||
|
||||
test('Validation of tag types allows numbers for description and icons because of coercion', async () => {
|
||||
await app.request
|
||||
.post('/api/admin/tag-types/validate')
|
||||
.send({
|
||||
name: 'something',
|
||||
description: 1234,
|
||||
icon: 56789,
|
||||
})
|
||||
.set('Content-Type', 'application/json')
|
||||
.expect(200)
|
||||
.expect((res) => {
|
||||
expect(res.body.valid).toBe(true);
|
||||
});
|
||||
});
|
||||
test('Invalid tag-types get refused by validator', async () => {
|
||||
await app.request
|
||||
.post('/api/admin/tag-types/validate')
|
||||
|
@ -712,6 +712,43 @@ Object {
|
||||
],
|
||||
"type": "object",
|
||||
},
|
||||
"tagTypeSchema": Object {
|
||||
"additionalProperties": false,
|
||||
"properties": Object {
|
||||
"description": Object {
|
||||
"type": "string",
|
||||
},
|
||||
"icon": Object {
|
||||
"type": "string",
|
||||
},
|
||||
"name": Object {
|
||||
"type": "string",
|
||||
},
|
||||
},
|
||||
"required": Array [
|
||||
"name",
|
||||
],
|
||||
"type": "object",
|
||||
},
|
||||
"tagTypesSchema": Object {
|
||||
"additionalProperties": false,
|
||||
"properties": Object {
|
||||
"tagTypes": Object {
|
||||
"items": Object {
|
||||
"$ref": "#/components/schemas/tagTypeSchema",
|
||||
},
|
||||
"type": "array",
|
||||
},
|
||||
"version": Object {
|
||||
"type": "integer",
|
||||
},
|
||||
},
|
||||
"required": Array [
|
||||
"version",
|
||||
"tagTypes",
|
||||
],
|
||||
"type": "object",
|
||||
},
|
||||
"tagsSchema": Object {
|
||||
"additionalProperties": false,
|
||||
"properties": Object {
|
||||
@ -857,6 +894,34 @@ Object {
|
||||
"required": Array [],
|
||||
"type": "object",
|
||||
},
|
||||
"updateTagTypeSchema": Object {
|
||||
"additionalProperties": false,
|
||||
"properties": Object {
|
||||
"description": Object {
|
||||
"type": "string",
|
||||
},
|
||||
"icon": Object {
|
||||
"type": "string",
|
||||
},
|
||||
},
|
||||
"type": "object",
|
||||
},
|
||||
"validateTagTypeSchema": Object {
|
||||
"additionalProperties": false,
|
||||
"properties": Object {
|
||||
"tagType": Object {
|
||||
"$ref": "#/components/schemas/tagTypeSchema",
|
||||
},
|
||||
"valid": Object {
|
||||
"type": "boolean",
|
||||
},
|
||||
},
|
||||
"required": Array [
|
||||
"valid",
|
||||
"tagType",
|
||||
],
|
||||
"type": "object",
|
||||
},
|
||||
"variantSchema": Object {
|
||||
"additionalProperties": false,
|
||||
"properties": Object {
|
||||
@ -2414,6 +2479,169 @@ Object {
|
||||
],
|
||||
},
|
||||
},
|
||||
"/api/admin/tag-types": Object {
|
||||
"get": Object {
|
||||
"operationId": "getTagTypes",
|
||||
"responses": Object {
|
||||
"200": Object {
|
||||
"content": Object {
|
||||
"application/json": Object {
|
||||
"schema": Object {
|
||||
"$ref": "#/components/schemas/tagTypesSchema",
|
||||
},
|
||||
},
|
||||
},
|
||||
"description": "tagTypesSchema",
|
||||
},
|
||||
},
|
||||
"tags": Array [
|
||||
"admin",
|
||||
],
|
||||
},
|
||||
"post": Object {
|
||||
"operationId": "createTagType",
|
||||
"requestBody": Object {
|
||||
"content": Object {
|
||||
"application/json": Object {
|
||||
"schema": Object {
|
||||
"$ref": "#/components/schemas/tagTypeSchema",
|
||||
},
|
||||
},
|
||||
},
|
||||
"description": "tagTypeSchema",
|
||||
"required": true,
|
||||
},
|
||||
"responses": Object {
|
||||
"201": Object {
|
||||
"content": Object {
|
||||
"application/json": Object {
|
||||
"schema": Object {
|
||||
"$ref": "#/components/schemas/tagTypeSchema",
|
||||
},
|
||||
},
|
||||
},
|
||||
"description": "tagTypeSchema",
|
||||
},
|
||||
},
|
||||
"tags": Array [
|
||||
"admin",
|
||||
],
|
||||
},
|
||||
},
|
||||
"/api/admin/tag-types/validate": Object {
|
||||
"post": Object {
|
||||
"operationId": "validateTagType",
|
||||
"requestBody": Object {
|
||||
"content": Object {
|
||||
"application/json": Object {
|
||||
"schema": Object {
|
||||
"$ref": "#/components/schemas/tagTypeSchema",
|
||||
},
|
||||
},
|
||||
},
|
||||
"description": "tagTypeSchema",
|
||||
"required": true,
|
||||
},
|
||||
"responses": Object {
|
||||
"200": Object {
|
||||
"content": Object {
|
||||
"application/json": Object {
|
||||
"schema": Object {
|
||||
"$ref": "#/components/schemas/validateTagTypeSchema",
|
||||
},
|
||||
},
|
||||
},
|
||||
"description": "validateTagTypeSchema",
|
||||
},
|
||||
},
|
||||
"tags": Array [
|
||||
"admin",
|
||||
],
|
||||
},
|
||||
},
|
||||
"/api/admin/tag-types/{name}": Object {
|
||||
"delete": Object {
|
||||
"operationId": "deleteTagType",
|
||||
"parameters": Array [
|
||||
Object {
|
||||
"in": "path",
|
||||
"name": "name",
|
||||
"required": true,
|
||||
"schema": Object {
|
||||
"type": "string",
|
||||
},
|
||||
},
|
||||
],
|
||||
"responses": Object {
|
||||
"200": Object {
|
||||
"description": "emptyResponse",
|
||||
},
|
||||
},
|
||||
"tags": Array [
|
||||
"admin",
|
||||
],
|
||||
},
|
||||
"get": Object {
|
||||
"operationId": "getTagType",
|
||||
"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/tagTypeSchema",
|
||||
},
|
||||
},
|
||||
},
|
||||
"description": "tagTypeSchema",
|
||||
},
|
||||
},
|
||||
"tags": Array [
|
||||
"admin",
|
||||
],
|
||||
},
|
||||
"put": Object {
|
||||
"operationId": "updateTagType",
|
||||
"parameters": Array [
|
||||
Object {
|
||||
"in": "path",
|
||||
"name": "name",
|
||||
"required": true,
|
||||
"schema": Object {
|
||||
"type": "string",
|
||||
},
|
||||
},
|
||||
],
|
||||
"requestBody": Object {
|
||||
"content": Object {
|
||||
"application/json": Object {
|
||||
"schema": Object {
|
||||
"$ref": "#/components/schemas/updateTagTypeSchema",
|
||||
},
|
||||
},
|
||||
},
|
||||
"description": "updateTagTypeSchema",
|
||||
"required": true,
|
||||
},
|
||||
"responses": Object {
|
||||
"200": Object {
|
||||
"description": "emptyResponse",
|
||||
},
|
||||
},
|
||||
"tags": Array [
|
||||
"admin",
|
||||
],
|
||||
},
|
||||
},
|
||||
"/api/admin/ui-config": Object {
|
||||
"get": Object {
|
||||
"operationId": "getUIConfig",
|
||||
|
Loading…
Reference in New Issue
Block a user