1
0
mirror of https://github.com/Unleash/unleash.git synced 2025-04-10 01:16:39 +02:00

chore: Improve openapi documentation for tags (#3496)

## About the changes
1. Create tag should not throw a 500 when bad data is provided
2. Added summary, description and examples to open API endpoints

---------

Co-authored-by: Nuno Góis <github@nunogois.com>
This commit is contained in:
Gastón Fournier 2023-04-12 11:34:32 +02:00 committed by GitHub
parent c1a1a0fdeb
commit 37beaa611f
No known key found for this signature in database
GPG Key ID: 4AEE18F83AFDEB23
6 changed files with 134 additions and 11 deletions

View File

@ -1,5 +1,7 @@
import { FromSchema } from 'json-schema-to-ts';
export const TAG_MIN_LENGTH = 2;
export const TAG_MAX_LENGTH = 50;
export const tagSchema = {
$id: '#/components/schemas/tagSchema',
type: 'object',
@ -8,11 +10,20 @@ export const tagSchema = {
properties: {
value: {
type: 'string',
minLength: TAG_MIN_LENGTH,
maxLength: TAG_MAX_LENGTH,
},
type: {
type: 'string',
minLength: TAG_MIN_LENGTH,
maxLength: TAG_MAX_LENGTH,
default: 'simple',
},
},
example: {
value: 'tag-value',
type: 'simple',
},
components: {},
} as const;

View File

@ -20,6 +20,20 @@ export const updateTagsSchema = {
},
},
},
example: {
addedTags: [
{
value: 'tag-to-add',
type: 'simple',
},
],
removedTags: [
{
value: 'tag-to-remove',
type: 'simple',
},
],
},
components: {
schemas: {
tagSchema,

View File

@ -29,7 +29,10 @@ import {
createResponseSchema,
resourceCreatedResponseSchema,
} from '../../openapi/util/create-response-schema';
import { emptyResponse } from '../../openapi/util/standard-responses';
import {
emptyResponse,
getStandardResponses,
} from '../../openapi/util/standard-responses';
import { UpdateTagsSchema } from '../../openapi/spec/update-tags-schema';
const version = 1;
@ -110,9 +113,15 @@ class FeatureController extends Controller {
permission: NONE,
middleware: [
openApiService.validPath({
summary: 'Get all tags for a feature.',
description:
'Retrieves all the tags for a feature name. If the feature does not exist it returns an empty list.',
tags: ['Features'],
operationId: 'listTags',
responses: { 200: createResponseSchema('tagsSchema') },
responses: {
200: createResponseSchema('tagsSchema'),
...getStandardResponses(401),
},
}),
],
});
@ -124,11 +133,15 @@ class FeatureController extends Controller {
handler: this.addTag,
middleware: [
openApiService.validPath({
summary: 'Adds a tag to a feature.',
description:
'Adds a tag to a feature if the feature and tag type exist in the system. The operation is idempotent, so adding an existing tag will result in a successful response.',
tags: ['Features'],
operationId: 'addTag',
requestBody: createRequestSchema('tagSchema'),
responses: {
201: resourceCreatedResponseSchema('tagSchema'),
...getStandardResponses(400, 401, 403, 404),
},
}),
],
@ -141,11 +154,15 @@ class FeatureController extends Controller {
handler: this.updateTags,
middleware: [
openApiService.validPath({
summary: 'Updates multiple tags for a feature.',
description:
'Receives a list of tags to add and a list of tags to remove that are mandatory but can be empty. All tags under addedTags are first added to the feature and then all tags under removedTags are removed from the feature.',
tags: ['Features'],
operationId: 'updateTags',
requestBody: createRequestSchema('updateTagsSchema'),
responses: {
200: resourceCreatedResponseSchema('tagsSchema'),
...getStandardResponses(400, 401, 403, 404),
},
}),
],
@ -159,9 +176,15 @@ class FeatureController extends Controller {
handler: this.removeTag,
middleware: [
openApiService.validPath({
summary: 'Removes a tag from a feature.',
description:
'Removes a tag from a feature. If the feature exists but the tag does not, it returns a successful response.',
tags: ['Features'],
operationId: 'removeTag',
responses: { 200: emptyResponse },
responses: {
200: emptyResponse,
...getStandardResponses(404),
},
}),
],
});

View File

@ -11,6 +11,7 @@ import {
import { IEventStore } from '../types/stores/event-store';
import { ITagStore } from '../types/stores/tag-store';
import { ITag } from '../types/model';
import { BadDataError, FOREIGN_KEY_VIOLATION } from '../../lib/error';
class FeatureTagService {
private tagStore: ITagStore;
@ -129,12 +130,20 @@ class FeatureTagService {
await this.tagStore.getTag(tag.type, tag.value);
} catch (error) {
if (error instanceof NotFoundError) {
await this.tagStore.createTag(tag);
await this.eventStore.store({
type: TAG_CREATED,
createdBy: userName,
data: tag,
});
try {
await this.tagStore.createTag(tag);
await this.eventStore.store({
type: TAG_CREATED,
createdBy: userName,
data: tag,
});
} catch (err) {
if (err.code === FOREIGN_KEY_VIOLATION) {
throw new BadDataError(
`Tag type '${tag.type}' does not exist`,
);
}
}
}
}
}

View File

@ -1,11 +1,16 @@
import Joi from 'joi';
import { customJoi } from '../routes/util';
import { TAG_MAX_LENGTH, TAG_MIN_LENGTH } from '../../lib/openapi';
export const tagSchema = Joi.object()
.keys({
value: Joi.string().min(2).max(50),
type: customJoi.isUrlFriendly().min(2).max(50).default('simple'),
value: Joi.string().min(TAG_MIN_LENGTH).max(TAG_MAX_LENGTH),
type: customJoi
.isUrlFriendly()
.min(TAG_MIN_LENGTH)
.max(TAG_MAX_LENGTH)
.default('simple'),
})
.options({
allowUnknown: false,

View File

@ -3706,11 +3706,20 @@ Stats are divided into current and previous **windows**.
},
"tagSchema": {
"additionalProperties": false,
"example": {
"type": "simple",
"value": "tag-value",
},
"properties": {
"type": {
"default": "simple",
"maxLength": 50,
"minLength": 2,
"type": "string",
},
"value": {
"maxLength": 50,
"minLength": 2,
"type": "string",
},
},
@ -4019,6 +4028,20 @@ Stats are divided into current and previous **windows**.
},
"updateTagsSchema": {
"additionalProperties": false,
"example": {
"addedTags": [
{
"type": "simple",
"value": "tag-to-add",
},
],
"removedTags": [
{
"type": "simple",
"value": "tag-to-remove",
},
],
},
"properties": {
"addedTags": {
"items": {
@ -5474,6 +5497,7 @@ If the provided project does not exist, the list of events will be empty.",
},
"/api/admin/features/{featureName}/tags": {
"get": {
"description": "Retrieves all the tags for a feature name. If the feature does not exist it returns an empty list.",
"operationId": "listTags",
"parameters": [
{
@ -5496,12 +5520,17 @@ If the provided project does not exist, the list of events will be empty.",
},
"description": "tagsSchema",
},
"401": {
"description": "Authorization information is missing or invalid. Provide a valid API token as the \`authorization\` header, e.g. \`authorization:*.*.my-admin-token\`.",
},
},
"summary": "Get all tags for a feature.",
"tags": [
"Features",
],
},
"post": {
"description": "Adds a tag to a feature if the feature and tag type exist in the system. The operation is idempotent, so adding an existing tag will result in a successful response.",
"operationId": "addTag",
"parameters": [
{
@ -5544,12 +5573,26 @@ If the provided project does not exist, the list of events will be empty.",
},
},
},
"400": {
"description": "The request data does not match what we expect.",
},
"401": {
"description": "Authorization information is missing or invalid. Provide a valid API token as the \`authorization\` header, e.g. \`authorization:*.*.my-admin-token\`.",
},
"403": {
"description": "User credentials are valid but does not have enough privileges to execute this operation",
},
"404": {
"description": "The requested resource was not found.",
},
},
"summary": "Adds a tag to a feature.",
"tags": [
"Features",
],
},
"put": {
"description": "Receives a list of tags to add and a list of tags to remove that are mandatory but can be empty. All tags under addedTags are first added to the feature and then all tags under removedTags are removed from the feature.",
"operationId": "updateTags",
"parameters": [
{
@ -5592,7 +5635,20 @@ If the provided project does not exist, the list of events will be empty.",
},
},
},
"400": {
"description": "The request data does not match what we expect.",
},
"401": {
"description": "Authorization information is missing or invalid. Provide a valid API token as the \`authorization\` header, e.g. \`authorization:*.*.my-admin-token\`.",
},
"403": {
"description": "User credentials are valid but does not have enough privileges to execute this operation",
},
"404": {
"description": "The requested resource was not found.",
},
},
"summary": "Updates multiple tags for a feature.",
"tags": [
"Features",
],
@ -5600,6 +5656,7 @@ If the provided project does not exist, the list of events will be empty.",
},
"/api/admin/features/{featureName}/tags/{type}/{value}": {
"delete": {
"description": "Removes a tag from a feature. If the feature exists but the tag does not, it returns a successful response.",
"operationId": "removeTag",
"parameters": [
{
@ -5631,7 +5688,11 @@ If the provided project does not exist, the list of events will be empty.",
"200": {
"description": "This response has no body.",
},
"404": {
"description": "The requested resource was not found.",
},
},
"summary": "Removes a tag from a feature.",
"tags": [
"Features",
],