UNPKG

@trapi/swagger

Version:

Generate Swagger files from a decorator APIs.

517 lines 21.7 kB
"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