@trapi/swagger
Version:
Generate Swagger files from a decorator APIs.
500 lines • 19.8 kB
JavaScript
"use strict";
/*
* Copyright (c) 2021-2023.
* Author Peter Placzek (tada5hi)
* For the full copyright and license information,
* view the LICENSE file that was distributed with this source code.
*/
Object.defineProperty(exports, "__esModule", { value: true });
exports.V3Generator = void 0;
const metadata_1 = require("@trapi/metadata");
const node_url_1 = require("node:url");
const smob_1 = require("smob");
const schema_1 = require("../../schema");
const utils_1 = require("../../utils");
const abstract_1 = require("../abstract");
class V3Generator extends abstract_1.AbstractSpecGenerator {
async build() {
if (typeof this.spec !== 'undefined') {
return this.spec;
}
let spec = {
components: this.buildComponents(),
info: this.buildInfo(),
openapi: '3.1.0',
paths: this.buildPaths(),
servers: this.buildServers(),
tags: [],
};
if (this.config.specificationExtra) {
spec = (0, smob_1.merge)(spec, this.config.specificationExtra);
}
this.spec = spec;
await this.save();
return spec;
}
buildComponents() {
const components = {
examples: {},
headers: {},
parameters: {},
requestBodies: {},
responses: {},
schemas: this.buildSchemasForReferenceTypes((output, referenceType) => {
if (referenceType.deprecated) {
output.deprecated = true;
}
}),
securitySchemes: {},
};
if (this.config.securityDefinitions) {
components.securitySchemes = V3Generator.translateSecurityDefinitions(this.config.securityDefinitions);
}
return components;
}
static translateSecurityDefinitions(securityDefinitions) {
const output = {};
const keys = Object.keys(securityDefinitions);
for (let i = 0; i < keys.length; i++) {
const securityDefinition = securityDefinitions[keys[i]];
switch (securityDefinition.type) {
case 'http':
output[keys[i]] = securityDefinition;
break;
case 'oauth2':
output[keys[i]] = securityDefinition;
break;
case 'apiKey':
output[keys[i]] = securityDefinition;
break;
}
}
return output;
}
buildPaths() {
const output = {};
for (let i = 0; i < this.metadata.controllers.length; i++) {
const controller = this.metadata.controllers[i];
for (let j = 0; j < controller.methods.length; j++) {
const method = controller.methods[j];
if (method.hidden) {
continue;
}
let path = (0, utils_1.removeFinalCharacter)((0, utils_1.removeDuplicateSlashes)(`/${controller.path}/${method.path}`), '/');
path = (0, utils_1.normalizePathParameters)(path);
output[path] = output[path] || {};
output[path][method.method] = this.buildMethod(controller.name, method);
}
}
return output;
}
buildMethod(controllerName, method) {
const output = this.buildOperation(controllerName, method);
output.description = method.description;
output.summary = method.summary;
output.tags = method.tags;
// Use operationId tag otherwise fallback to generate. Warning: This doesn't check uniqueness.
output.operationId = method.operationId || output.operationId;
if (method.deprecated) {
output.deprecated = method.deprecated;
}
if (method.security) {
output.security = method.security;
}
const parameters = this.groupParameters(method.parameters);
output.parameters = [
...(parameters[metadata_1.ParameterSource.QUERY_PROP] || []),
...(parameters[metadata_1.ParameterSource.HEADER] || []),
...(parameters[metadata_1.ParameterSource.PATH] || []),
...(parameters[metadata_1.ParameterSource.COOKIE] || []),
]
.map((p) => this.buildParameter(p));
// ignore ParameterSource.QUERY!
const bodyParams = parameters[metadata_1.ParameterSource.BODY] || [];
const formParams = parameters[metadata_1.ParameterSource.FORM_DATA] || [];
if (bodyParams.length > 1) {
throw new Error('Only one body parameter allowed per controller method.');
}
if (bodyParams.length > 0 && formParams.length > 0) {
throw new Error('Either body parameter or form parameters allowed per controller method - not both.');
}
const bodyPropParams = parameters[metadata_1.ParameterSource.BODY_PROP] || [];
if (bodyPropParams.length > 0) {
if (bodyParams.length === 0) {
bodyParams.push({
in: metadata_1.ParameterSource.BODY,
name: 'body',
description: '',
parameterName: bodyPropParams[0].parameterName || 'body',
required: true,
type: {
typeName: metadata_1.TypeName.NESTED_OBJECT_LITERAL,
properties: [],
},
validators: {},
deprecated: false,
});
}
if ((0, metadata_1.isNestedObjectLiteralType)(bodyParams[0].type)) {
for (let i = 0; i < bodyPropParams.length; i++) {
bodyParams[0].type.properties.push({
default: bodyPropParams[i].default,
validators: bodyPropParams[i].validators,
description: bodyPropParams[i].description,
name: bodyPropParams[i].name,
type: bodyPropParams[i].type,
required: bodyPropParams[i].required,
deprecated: bodyPropParams[i].deprecated,
});
}
}
}
if (bodyParams.length > 0) {
output.requestBody = this.buildRequestBody(bodyParams[0]);
}
else if (formParams.length > 0) {
output.requestBody = this.buildRequestBodyWithFormData(formParams);
}
for (let i = 0; i < method.extensions.length; i++) {
output[method.extensions[i].key] = method.extensions[i].value;
}
return output;
}
buildRequestBodyWithFormData(parameters) {
const required = [];
const properties = {};
const keys = Object.keys(parameters);
for (let i = 0; i < parameters.length; i++) {
const { schema } = this.buildMediaType(parameters[keys[i]]);
properties[parameters[keys[i]].name] = schema;
if (parameters[keys[i]].required) {
required.push(parameters[keys[i]].name);
}
}
return {
required: required.length > 0,
content: {
'multipart/form-data': {
schema: {
type: schema_1.DataTypeName.OBJECT,
properties,
// An empty list required: [] is not valid.
// If all properties are optional, do not specify the required keyword.
...(required && required.length && { required }),
},
},
},
};
}
buildRequestBody(parameter) {
const mediaType = this.buildMediaType(parameter);
return {
description: parameter.description,
required: parameter.required,
content: {
'application/json': mediaType,
},
};
}
buildMediaType(parameter) {
return {
schema: this.getSchemaForType(parameter.type),
examples: this.transformParameterExamples(parameter),
};
}
buildResponses(input) {
const output = {};
for (let i = 0; i < input.length; i++) {
const res = input[i];
const name = res.status || 'default';
output[name] = {
description: res.description,
};
if (res.schema &&
!(0, metadata_1.isVoidType)(res.schema)) {
const examples = {};
if (res.examples &&
res.examples.length > 0) {
for (let i = 0; i < res.examples.length; i++) {
const label = res.examples[i].label || `example${i + 1}`;
examples[label] = {
value: res.examples[i].value,
};
}
}
output[name].content = output[name].content || {};
const contentTypes = res.produces || ['application/json'];
for (let i = 0; i < contentTypes.length; i++) {
output[name].content[contentTypes[i]] = {
schema: this.getSchemaForType(res.schema),
examples,
};
}
}
if (res.headers) {
const headers = {};
if ((0, metadata_1.isRefObjectType)(res.headers)) {
headers[res.headers.refName] = {
schema: this.getSchemaForReferenceType(res.headers),
description: res.headers.description,
};
}
else if ((0, metadata_1.isNestedObjectLiteralType)(res.headers)) {
res.headers.properties.forEach((each) => {
headers[each.name] = {
schema: this.getSchemaForType(each.type),
description: each.description,
required: each.required,
};
});
}
output[res.name].headers = headers;
}
}
return output;
}
buildOperation(controllerName, method) {
const operation = {
operationId: this.getOperationId(method.name),
responses: this.buildResponses(method.responses),
};
if (method.description) {
operation.description = method.description;
}
if (method.security) {
operation.security = method.security;
}
if (method.deprecated) {
operation.deprecated = method.deprecated;
}
return operation;
}
transformParameterSource(source) {
if (source === metadata_1.ParameterSource.COOKIE) {
return schema_1.ParameterSourceV3.COOKIE;
}
if (source === metadata_1.ParameterSource.HEADER) {
return schema_1.ParameterSourceV3.HEADER;
}
if (source === metadata_1.ParameterSource.PATH) {
return schema_1.ParameterSourceV3.PATH;
}
if (source === metadata_1.ParameterSource.QUERY_PROP || source === metadata_1.ParameterSource.QUERY) {
return schema_1.ParameterSourceV3.QUERY;
}
return undefined;
}
buildParameter(input) {
const sourceIn = this.transformParameterSource(input.in);
if (!sourceIn) {
throw new Error(`The parameter source "${input.in}" is not valid for generating a document.`);
}
const parameter = {
allowEmptyValue: false,
deprecated: false,
description: input.description,
in: sourceIn,
name: input.name,
required: input.required,
schema: {
default: input.default,
format: undefined,
...this.transformValidators(input.validators),
},
};
if (input.deprecated) {
parameter.deprecated = true;
}
const parameterType = this.getSchemaForType(input.type);
if (parameterType.format) {
parameter.schema.format = parameterType.format;
}
if (parameterType.$ref) {
parameter.schema = parameterType;
return parameter;
}
if ((0, metadata_1.isAnyType)(input.type)) {
parameter.schema.type = schema_1.DataTypeName.STRING;
}
else {
if (parameterType.type) {
parameter.schema.type = parameterType.type;
}
parameter.schema.items = parameterType.items;
parameter.schema.enum = parameterType.enum;
}
parameter.examples = this.transformParameterExamples(input);
return parameter;
}
transformParameterExamples(parameter) {
const output = {};
if (parameter.examples &&
parameter.examples.length > 0) {
for (let i = 0; i < parameter.examples.length; i++) {
const label = parameter.examples[i].label || `example${i + 1}`;
output[label] = {
value: parameter.examples[i].value,
};
}
}
return output;
}
buildServers() {
const servers = [];
for (let i = 0; i < this.config.servers.length; i++) {
const url = new node_url_1.URL(this.config.servers[i].url, 'http://localhost:3000/');
servers.push({
url: `${url.protocol}//${url.host}${url.pathname || ''}`,
...(this.config.servers[i].description ? { description: this.config.servers[i].description } : {}),
});
}
return servers;
}
buildSchemaForRefObject(input) {
const required = input.properties
.filter((p) => p.required && !this.isUndefinedProperty(p))
.map((p) => p.name);
const schema = {
description: input.description,
properties: this.buildProperties(input.properties),
required: required && required.length > 0 ? Array.from(new Set(required)) : undefined,
type: 'object',
};
if (input.additionalProperties) {
schema.additionalProperties = this.getSchemaForType(input.additionalProperties);
}
if (input.example) {
schema.example = input.example;
}
return schema;
}
buildSchemaForRefEnum(referenceType) {
const type = this.decideEnumType(referenceType.members);
const typesUsed = this.determineTypesUsedInEnum(referenceType.members);
if (typesUsed.length === 1) {
const schema = {
description: referenceType.description,
enum: referenceType.members,
type,
};
if (typeof referenceType.memberNames !== 'undefined' &&
referenceType.members.length === referenceType.memberNames.length) {
schema['x-enum-varnames'] = referenceType.memberNames;
}
return schema;
}
const schema = {
description: referenceType.description,
anyOf: [],
};
for (let i = 0; i < typesUsed.length; i++) {
schema.anyOf.push({
type: typesUsed[i],
// eslint-disable-next-line valid-typeof
enum: referenceType.members.filter((e) => typeof e === typesUsed[i]),
});
}
return schema;
}
buildSchemaForRefAlias(referenceType) {
const swaggerType = this.getSchemaForType(referenceType.type);
const format = referenceType.format;
return {
...swaggerType,
default: referenceType.default || swaggerType.default,
example: referenceType.example,
format: format || swaggerType.format,
description: referenceType.description,
...this.transformValidators(referenceType.validators),
};
}
buildProperties(properties) {
const output = {};
properties.forEach((property) => {
const swaggerType = this.getSchemaForType(property.type);
swaggerType.description = property.description;
swaggerType.example = property.example;
swaggerType.format = property.format || swaggerType.format;
if (!swaggerType.$ref) {
swaggerType.default = property.default;
}
if (property.deprecated) {
swaggerType.deprecated = true;
}
const extensions = this.transformExtensions(property.extensions);
const validators = this.transformValidators(property.validators);
output[property.name] = {
...swaggerType,
...validators,
...extensions,
};
});
return output;
}
getSchemaForIntersectionType(type) {
return { allOf: type.members.map((x) => this.getSchemaForType(x)) };
}
getSchemaForEnumType(enumType) {
const type = this.decideEnumType(enumType.members);
const nullable = !!enumType.members.includes(null);
return {
type,
enum: enumType.members.map((member) => (0, utils_1.transformValueTo)(type, member)),
nullable,
};
}
getSchemaForReferenceType(referenceType) {
return {
$ref: `#/components/schemas/${referenceType.refName}`,
};
}
getSchemaForUnionType(type) {
const members = [];
let nullable = false;
const enumMembers = {};
for (let i = 0; i < type.members.length; i++) {
const member = type.members[i];
if ((0, metadata_1.isEnumType)(member)) {
for (let j = 0; j < member.members.length; j++) {
const memberChild = member.members[j];
if (memberChild === null || memberChild === undefined) {
nullable = true;
continue;
}
const typeOf = typeof memberChild;
if (typeOf === 'string' || typeOf === 'number' || typeOf === 'boolean') {
enumMembers[typeOf] = enumMembers[typeOf] || [];
enumMembers[typeOf].push(memberChild);
}
}
}
if (!(0, metadata_1.isAnyType)(member) &&
!(0, metadata_1.isUndefinedType)(member) &&
!(0, metadata_1.isEnumType)(member)) {
members.push(member);
}
}
const schemas = [];
for (let i = 0; i < members.length; i++) {
schemas.push(this.getSchemaForType(members[i]));
}
const enumMembersKeys = Object.keys(enumMembers);
for (let i = 0; i < enumMembersKeys.length; i++) {
const enumType = {
typeName: 'enum',
members: enumMembers[enumMembersKeys[i]],
};
schemas.push(this.getSchemaForEnumType(enumType));
}
if (schemas.length === 1) {
const schema = schemas[0];
if (schema.$ref) {
return { allOf: [schema], nullable };
}
return { ...schema, nullable };
}
return { anyOf: schemas, ...(nullable ? { nullable } : {}) };
}
}
exports.V3Generator = V3Generator;
//# sourceMappingURL=module.js.map