@trapi/swagger
Version:
Generate Swagger files from a decorator APIs.
517 lines • 21.7 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.
*/
var __importDefault = (this && this.__importDefault) || function (mod) {
return (mod && mod.__esModule) ? mod : { "default": mod };
};
Object.defineProperty(exports, "__esModule", { value: true });
exports.V2Generator = void 0;
const metadata_1 = require("@trapi/metadata");
const node_path_1 = __importDefault(require("node:path"));
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 V2Generator extends abstract_1.AbstractSpecGenerator {
async build() {
if (typeof this.spec !== 'undefined') {
return this.spec;
}
let spec = {
definitions: this.buildSchemasForReferenceTypes(),
info: this.buildInfo(),
paths: this.buildPaths(),
swagger: '2.0',
};
spec.securityDefinitions = this.config.securityDefinitions ?
V2Generator.translateSecurityDefinitions(this.config.securityDefinitions) :
{};
if (this.config.consumes) {
spec.consumes = this.config.consumes;
}
if (this.config.produces) {
spec.produces = this.config.produces;
}
if (this.config.servers &&
this.config.servers.length > 0) {
const url = new node_url_1.URL(this.config.servers[0].url, 'http://localhost:3000/');
spec.host = url.host;
if (url.pathname) {
spec.basePath = url.pathname;
}
}
if (this.config.specificationExtra) {
spec = (0, smob_1.merge)(spec, this.config.specificationExtra);
}
this.spec = spec;
await this.save();
return spec;
}
static translateSecurityDefinitions(securityDefinitions) {
const definitions = {};
const keys = Object.keys(securityDefinitions);
for (let i = 0; i < keys.length; i++) {
const securityDefinition = securityDefinitions[keys[i]];
switch (securityDefinition.type) {
case 'http':
if (securityDefinition.schema === 'basic') {
definitions[keys[i]] = {
type: 'basic',
};
}
break;
case 'apiKey':
definitions[keys[i]] = securityDefinition;
break;
case 'oauth2':
if (securityDefinition.flows.implicit) {
definitions[`${keys[i]}Implicit`] = {
type: 'oauth2',
flow: 'implicit',
authorizationUrl: securityDefinition.flows.implicit.authorizationUrl,
scopes: securityDefinition.flows.implicit.scopes,
};
}
if (securityDefinition.flows.password) {
definitions[`${keys[i]}Implicit`] = {
type: 'oauth2',
flow: 'password',
tokenUrl: securityDefinition.flows.password.tokenUrl,
scopes: securityDefinition.flows.password.scopes,
};
}
if (securityDefinition.flows.authorizationCode) {
definitions[`${keys[i]}AccessCode`] = {
type: 'oauth2',
flow: 'accessCode',
tokenUrl: securityDefinition.flows.authorizationCode.tokenUrl,
authorizationUrl: securityDefinition.flows.authorizationCode.authorizationUrl,
scopes: securityDefinition.flows.authorizationCode.scopes,
};
}
if (securityDefinition.flows.clientCredentials) {
definitions[`${keys[i]}Application`] = {
type: 'oauth2',
flow: 'application',
tokenUrl: securityDefinition.flows.clientCredentials.tokenUrl,
scopes: securityDefinition.flows.clientCredentials.scopes,
};
}
break;
}
}
return definitions;
}
buildSchemaForRefObject(referenceType) {
const required = referenceType.properties
.filter((p) => p.required && !this.isUndefinedProperty(p))
.map((p) => p.name);
const output = {
description: referenceType.description,
properties: this.buildProperties(referenceType.properties),
required: required && required.length > 0 ? Array.from(new Set(required)) : undefined,
type: schema_1.DataTypeName.OBJECT,
};
if (referenceType.additionalProperties) {
output.additionalProperties = true;
}
if (referenceType.example) {
output.example = referenceType.example;
}
return output;
}
buildSchemaForRefEnum(referenceType) {
const output = {
description: referenceType.description,
enum: referenceType.members,
type: this.decideEnumType(referenceType.members),
};
if (referenceType.memberNames !== undefined && referenceType.members.length === referenceType.memberNames.length) {
output['x-enum-varnames'] = referenceType.memberNames;
}
return output;
}
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),
};
}
/*
Path & Parameter ( + utils)
*/
buildPaths() {
const output = {};
const unique = (input) => [...new Set(input)];
this.metadata.controllers.forEach((controller) => {
controller.methods.forEach((method) => {
let fullPath = node_path_1.default.posix.join('/', (controller.path ? controller.path : ''), method.path);
fullPath = (0, utils_1.normalizePathParameters)(fullPath);
method.consumes = unique([...controller.consumes, ...method.consumes]);
method.produces = unique([...controller.produces, ...method.produces]);
method.tags = unique([...controller.tags, ...method.tags]);
method.security = method.security || controller.security;
// todo: unique for objects
method.responses = unique([...controller.responses, ...method.responses]);
output[fullPath] = output[fullPath] || {};
output[fullPath][method.method] = this.buildMethod(method);
});
});
return output;
}
buildMethod(method) {
const output = this.buildOperation(method);
output.consumes = this.buildMethodConsumes(method);
output.description = method.description;
if (method.summary) {
output.summary = method.summary;
}
if (method.deprecated) {
output.deprecated = method.deprecated;
}
if (method.tags.length) {
output.tags = method.tags;
}
if (method.security) {
output.security = method.security;
}
const parameters = this.groupParameters(method.parameters);
output.parameters = [
...(parameters[metadata_1.ParameterSource.PATH] || []),
...(parameters[metadata_1.ParameterSource.QUERY_PROP] || []),
...(parameters[metadata_1.ParameterSource.HEADER] || []),
...(parameters[metadata_1.ParameterSource.FORM_DATA] || []),
].map((p) => this.buildParameter(p));
// ignore ParameterSource.QUERY!
// ------------------------------------------------------
const bodyParameters = (parameters[metadata_1.ParameterSource.BODY] || []);
if (bodyParameters.length > 1) {
throw new Error('Only one body parameter allowed per controller method.');
}
const bodyParameter = bodyParameters.length > 0 ?
this.buildParameter(bodyParameters[0]) :
undefined;
const bodyPropParams = parameters[metadata_1.ParameterSource.BODY_PROP] || [];
if (bodyPropParams.length > 0) {
const schema = {
type: schema_1.DataTypeName.OBJECT,
title: 'Body',
properties: {},
};
const required = [];
for (let i = 0; i < bodyPropParams.length; i++) {
const bodyProp = this.getSchemaForType(bodyPropParams[i].type);
bodyProp.default = bodyPropParams[i].default;
bodyProp.description = bodyPropParams[i].description;
bodyProp.example = bodyPropParams[i].examples;
if (bodyProp.required) {
required.push(bodyPropParams[i].name);
}
schema.properties[bodyPropParams[i].name] = bodyProp;
}
if (bodyParameter &&
bodyParameter.in === schema_1.ParameterSourceV2.BODY) {
if (bodyParameter.schema.type === schema_1.DataTypeName.OBJECT) {
bodyParameter.schema.properties = {
...(bodyParameter.schema.properties || {}),
...schema.properties,
};
bodyParameter.schema.required = [
...(bodyParameter.schema.required || []),
...required,
];
}
else {
bodyParameter.schema = schema;
}
output.parameters.push(bodyParameter);
}
else {
const parameter = {
in: schema_1.ParameterSourceV2.BODY,
name: 'body',
schema,
};
if (required.length) {
parameter.schema.required = required;
}
output.parameters.push(parameter);
}
}
else if (bodyParameter) {
output.parameters.push(bodyParameter);
}
for (let i = 0; i < method.extensions.length; i++) {
output[method.extensions[i].key] = method.extensions[i].value;
}
return output;
}
transformParameterSource(source) {
if (source === metadata_1.ParameterSource.BODY) {
return schema_1.ParameterSourceV2.BODY;
}
if (source === metadata_1.ParameterSource.FORM_DATA) {
return schema_1.ParameterSourceV2.FORM_DATA;
}
if (source === metadata_1.ParameterSource.HEADER) {
return schema_1.ParameterSourceV2.HEADER;
}
if (source === metadata_1.ParameterSource.PATH) {
return schema_1.ParameterSourceV2.PATH;
}
if (source === metadata_1.ParameterSource.QUERY || source === metadata_1.ParameterSource.QUERY_PROP) {
return schema_1.ParameterSourceV2.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 = {
description: input.description,
in: sourceIn,
name: input.name,
required: input.required,
};
if (input.in !== metadata_1.ParameterSource.BODY &&
(0, metadata_1.isRefEnumType)(input.type)) {
input.type = {
typeName: metadata_1.TypeName.ENUM,
members: input.type.members,
};
}
const parameterType = this.getSchemaForType(input.type);
if (parameter.in !== schema_1.ParameterSourceV2.BODY &&
parameterType.format) {
parameter.format = parameterType.format;
}
// collectionFormat, might be valid for all parameters (if value != multi)
if ((parameter.in === schema_1.ParameterSourceV2.FORM_DATA || parameter.in === schema_1.ParameterSourceV2.QUERY) &&
(input.type.typeName === metadata_1.TypeName.ARRAY || parameterType.type === schema_1.DataTypeName.ARRAY)) {
parameter.collectionFormat = input.collectionFormat || this.config.collectionFormat || 'multi';
}
if (parameter.in === schema_1.ParameterSourceV2.BODY) {
if ((input.type.typeName === metadata_1.TypeName.ARRAY || parameterType.type === schema_1.DataTypeName.ARRAY)) {
parameter.schema = {
items: parameterType.items,
type: schema_1.DataTypeName.ARRAY,
};
}
else if (input.type.typeName === metadata_1.TypeName.ANY) {
parameter.schema = { type: schema_1.DataTypeName.OBJECT };
}
else {
parameter.schema = parameterType;
}
parameter.schema = {
...parameter.schema,
...this.transformValidators(input.validators),
};
return parameter;
}
// todo: this is eventually illegal
(0, smob_1.merge)(parameter, this.transformValidators(input.validators));
if (input.type.typeName === metadata_1.TypeName.ANY) {
parameter.type = schema_1.DataTypeName.STRING;
}
else if (parameterType.type) {
parameter.type = parameterType.type;
}
if (parameterType.items) {
parameter.items = parameterType.items;
}
if (parameterType.enum) {
parameter.enum = parameterType.enum;
}
if (typeof input.default !== 'undefined') {
parameter.default = input.default;
}
return parameter;
}
buildMethodConsumes(method) {
if (method.consumes &&
method.consumes.length > 0) {
return method.consumes;
}
if (this.hasFileParams(method)) {
return ['multipart/form-data'];
}
if (this.hasFormParams(method)) {
return ['application/x-www-form-urlencoded'];
}
if (this.supportsBodyParameters(method.method)) {
return ['application/json'];
}
return [];
}
hasFileParams(method) {
return method.parameters.some((p) => (p.in === metadata_1.ParameterSource.FORM_DATA && p.type.typeName === 'file'));
}
hasFormParams(method) {
return method.parameters.some((p) => (p.in === metadata_1.ParameterSource.FORM_DATA));
}
supportsBodyParameters(method) {
return ['post', 'put', 'patch'].some((m) => m === method);
}
/*
Swagger Type ( + utils)
*/
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)),
'x-nullable': nullable,
};
}
getSchemaForIntersectionType(type) {
// tslint:disable-next-line:no-shadowed-variable
const properties = type.members.reduce((acc, type) => {
if ((0, metadata_1.isRefObjectType)(type)) {
const refType = this.metadata.referenceTypes[type.refName];
const props = refType &&
refType.properties &&
refType.properties.reduce((pAcc, prop) => ({
...pAcc,
[prop.name]: this.getSchemaForType(prop.type),
}), {});
return { ...acc, ...props };
}
return { ...acc };
}, {});
return { type: schema_1.DataTypeName.OBJECT, properties };
}
getSchemaForReferenceType(referenceType) {
return { $ref: `#/definitions/${referenceType.refName}` };
}
getSchemaForUnionType(type) {
const members = [];
const enumTypeMember = { typeName: metadata_1.TypeName.ENUM, members: [] };
for (let i = 0; i < type.members.length; i++) {
const member = type.members[i];
if ((0, metadata_1.isEnumType)(member)) {
enumTypeMember.members.push(...member.members);
}
if (!(0, metadata_1.isAnyType)(member) &&
!(0, metadata_1.isUndefinedType)(member) &&
!(0, metadata_1.isEnumType)(member)) {
members.push(member);
}
}
if (members.length === 0 &&
enumTypeMember.members.length > 0) {
return this.getSchemaForEnumType(enumTypeMember);
}
const isNullEnum = enumTypeMember.members.every((member) => member === null);
if (members.length === 1) {
if (isNullEnum) {
const memberType = this.getSchemaForType(members[0]);
if (memberType.$ref) {
return memberType;
}
memberType['x-nullable'] = true;
return memberType;
}
if (enumTypeMember.members.length === 0) {
return this.getSchemaForType(members[0]);
}
}
return { type: schema_1.DataTypeName.OBJECT, ...(isNullEnum ? { 'x-nullable': true } : {}) };
}
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 (!(0, utils_1.hasOwnProperty)(swaggerType, '$ref') || !swaggerType.$ref) {
swaggerType.description = property.description;
}
if (property.deprecated) {
swaggerType['x-deprecated'] = true;
}
if (property.extensions) {
for (let i = 0; i < property.extensions.length; i++) {
swaggerType[property.extensions[i].key] = property.extensions[i].value;
}
}
const extensions = this.transformExtensions(property.extensions);
const validators = this.transformValidators(property.validators);
output[property.name] = {
...swaggerType,
...validators,
...extensions,
};
});
return output;
}
buildOperation(method) {
const operation = {
operationId: this.getOperationId(method.name),
consumes: method.consumes || [],
produces: method.produces || [],
responses: {},
security: method.security || [],
};
const produces = [];
method.responses.forEach((res) => {
operation.responses[res.status] = {
description: res.description,
};
if (res.schema &&
!(0, metadata_1.isVoidType)(res.schema)) {
if (res.produces) {
produces.push(...res.produces);
}
else if ((0, metadata_1.isBinaryType)(res.schema)) {
produces.push('application/octet-stream');
}
operation.responses[res.status].schema = this.getSchemaForType(res.schema);
}
if (res.examples &&
res.examples.length > 0) {
const example = res.examples[0];
if (example.value) {
operation.responses[res.status].examples = { 'application/json': example.value };
}
}
});
if (operation.consumes.length === 0) {
const hasBody = method.parameters
.some((parameter) => parameter.in === metadata_1.ParameterSource.BODY || parameter.in === metadata_1.ParameterSource.BODY_PROP);
if (hasBody) {
operation.consumes.push('application/json');
}
const hasFormData = method.parameters
.some((parameter) => parameter.in === metadata_1.ParameterSource.FORM_DATA);
if (hasFormData) {
operation.consumes.push('multipart/form-data');
}
}
if (operation.produces.length === 0 &&
produces.length > 0) {
operation.produces = [...new Set(produces)];
}
if (operation.produces.length === 0) {
operation.produces = ['application/json'];
}
return operation;
}
}
exports.V2Generator = V2Generator;
//# sourceMappingURL=module.js.map