UNPKG

@trapi/swagger

Version:

Generate Swagger files from a decorator APIs.

500 lines 19.8 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. */ 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