1
0
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:
Christopher Kolstad 2022-06-14 09:06:41 +02:00 committed by GitHub
parent 7ba8cd05eb
commit 780bb06dba
No known key found for this signature in database
GPG Key ID: 4AEE18F83AFDEB23
8 changed files with 485 additions and 27 deletions

View File

@ -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,

View 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>;

View 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>;

View 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>;

View 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>;

View File

@ -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);

View File

@ -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')

View File

@ -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",