You can not select more than 25 topics
			Topics must start with a letter or number, can include dashes ('-') and can be up to 35 characters long.
		
		
		
		
		
			
		
			
				
					
					
						
							339 lines
						
					
					
						
							11 KiB
						
					
					
				
			
		
		
	
	
							339 lines
						
					
					
						
							11 KiB
						
					
					
				| 'use strict'
 | |
| 
 | |
| const { readPackageJson, formatParamUrl, resolveLocalRef } = require('../../util/common')
 | |
| const { xResponseDescription, xConsume } = require('../../constants')
 | |
| 
 | |
| function prepareDefaultOptions (opts) {
 | |
|   const swagger = opts.swagger
 | |
|   const info = swagger.info || null
 | |
|   const host = swagger.host || null
 | |
|   const schemes = swagger.schemes || null
 | |
|   const consumes = swagger.consumes || null
 | |
|   const produces = swagger.produces || null
 | |
|   const definitions = swagger.definitions || null
 | |
|   const basePath = swagger.basePath || null
 | |
|   const securityDefinitions = swagger.securityDefinitions || null
 | |
|   const security = swagger.security || null
 | |
|   const tags = swagger.tags || null
 | |
|   const externalDocs = swagger.externalDocs || null
 | |
|   const stripBasePath = opts.stripBasePath
 | |
|   const transform = opts.transform
 | |
|   const hiddenTag = opts.hiddenTag
 | |
|   const hideUntagged = opts.hideUntagged
 | |
|   const extensions = []
 | |
| 
 | |
|   for (const [key, value] of Object.entries(opts.swagger)) {
 | |
|     if (key.startsWith('x-')) {
 | |
|       extensions.push([key, value])
 | |
|     }
 | |
|   }
 | |
| 
 | |
|   return {
 | |
|     info,
 | |
|     host,
 | |
|     schemes,
 | |
|     consumes,
 | |
|     produces,
 | |
|     definitions,
 | |
|     basePath,
 | |
|     securityDefinitions,
 | |
|     security,
 | |
|     tags,
 | |
|     externalDocs,
 | |
|     stripBasePath,
 | |
|     transform,
 | |
|     hiddenTag,
 | |
|     extensions,
 | |
|     hideUntagged
 | |
|   }
 | |
| }
 | |
| 
 | |
| function prepareSwaggerObject (opts) {
 | |
|   const pkg = readPackageJson()
 | |
|   const swaggerObject = {
 | |
|     swagger: '2.0',
 | |
|     info: {
 | |
|       version: pkg.version || '1.0.0',
 | |
|       title: pkg.name || ''
 | |
|     },
 | |
|     definitions: {},
 | |
|     paths: {}
 | |
|   }
 | |
| 
 | |
|   if (opts.info) swaggerObject.info = opts.info
 | |
|   if (opts.host) swaggerObject.host = opts.host
 | |
|   if (opts.schemes) swaggerObject.schemes = opts.schemes
 | |
|   if (opts.basePath) swaggerObject.basePath = opts.basePath
 | |
|   if (opts.consumes) swaggerObject.consumes = opts.consumes
 | |
|   if (opts.produces) swaggerObject.produces = opts.produces
 | |
|   if (opts.definitions) swaggerObject.definitions = opts.definitions
 | |
|   if (opts.securityDefinitions) swaggerObject.securityDefinitions = opts.securityDefinitions
 | |
|   if (opts.security) swaggerObject.security = opts.security
 | |
|   if (opts.tags) swaggerObject.tags = opts.tags
 | |
|   if (opts.externalDocs) swaggerObject.externalDocs = opts.externalDocs
 | |
| 
 | |
|   for (const [key, value] of opts.extensions) {
 | |
|     // "x-" extension can not be typed
 | |
|     swaggerObject[key] = value
 | |
|   }
 | |
| 
 | |
|   return swaggerObject
 | |
| }
 | |
| 
 | |
| function normalizeUrl (url, basePath, stripBasePath) {
 | |
|   let path
 | |
|   if (stripBasePath && url.startsWith(basePath)) {
 | |
|     path = url.replace(basePath, '')
 | |
|   } else {
 | |
|     path = url
 | |
|   }
 | |
|   if (!path.startsWith('/')) {
 | |
|     path = '/' + String(path)
 | |
|   }
 | |
|   return formatParamUrl(path)
 | |
| }
 | |
| 
 | |
| // For supported keys read:
 | |
| // https://swagger.io/docs/specification/2-0/describing-parameters/
 | |
| function plainJsonObjectToSwagger2 (container, jsonSchema, externalSchemas, securityIgnores = []) {
 | |
|   const obj = resolveLocalRef(jsonSchema, externalSchemas)
 | |
|   let toSwaggerProp
 | |
|   switch (container) {
 | |
|     case 'query':
 | |
|       toSwaggerProp = function (propertyName, jsonSchemaElement) {
 | |
|         // complex serialization is not supported by swagger
 | |
|         if (jsonSchemaElement[xConsume]) {
 | |
|           throw new Error('Complex serialization is not supported by Swagger. ' +
 | |
|             'Remove "' + xConsume + '" for "' + propertyName + '" querystring schema or ' +
 | |
|             'change specification to OpenAPI')
 | |
|         }
 | |
|         jsonSchemaElement.in = container
 | |
|         jsonSchemaElement.name = propertyName
 | |
|         return jsonSchemaElement
 | |
|       }
 | |
|       break
 | |
|     case 'formData':
 | |
|       toSwaggerProp = function (propertyName, jsonSchemaElement) {
 | |
|         delete jsonSchemaElement.$id
 | |
|         jsonSchemaElement.in = container
 | |
|         jsonSchemaElement.name = propertyName
 | |
| 
 | |
|         // https://json-schema.org/understanding-json-schema/reference/non_json_data.html#contentencoding
 | |
|         if (jsonSchemaElement.contentEncoding === 'binary') {
 | |
|           delete jsonSchemaElement.contentEncoding // Must be removed
 | |
|           jsonSchemaElement.type = 'file'
 | |
|         }
 | |
| 
 | |
|         return jsonSchemaElement
 | |
|       }
 | |
|       break
 | |
|     case 'path':
 | |
|       toSwaggerProp = function (propertyName, jsonSchemaElement) {
 | |
|         jsonSchemaElement.in = container
 | |
|         jsonSchemaElement.name = propertyName
 | |
|         jsonSchemaElement.required = true
 | |
|         return jsonSchemaElement
 | |
|       }
 | |
|       break
 | |
|     case 'header':
 | |
|       toSwaggerProp = function (propertyName, jsonSchemaElement) {
 | |
|         return {
 | |
|           in: 'header',
 | |
|           name: propertyName,
 | |
|           required: jsonSchemaElement.required,
 | |
|           description: jsonSchemaElement.description,
 | |
|           type: jsonSchemaElement.type
 | |
|         }
 | |
|       }
 | |
|       break
 | |
|   }
 | |
| 
 | |
|   return Object.keys(obj)
 | |
|     .filter((propKey) => (!securityIgnores.includes(propKey)))
 | |
|     .map((propKey) => {
 | |
|       return toSwaggerProp(propKey, obj[propKey])
 | |
|     })
 | |
| }
 | |
| 
 | |
| /*
 | |
| * Map  unsupported JSON schema definitions to Swagger definitions
 | |
| */
 | |
| function replaceUnsupported (jsonSchema) {
 | |
|   if (typeof jsonSchema === 'object' && jsonSchema !== null) {
 | |
|     // Handle patternProperties, that is not part of OpenAPI definitions
 | |
|     if (jsonSchema.patternProperties) {
 | |
|       jsonSchema.additionalProperties = { type: 'string' }
 | |
|       delete jsonSchema.patternProperties
 | |
|     } else if (jsonSchema.const) {
 | |
|       // Handle const, that is not part of OpenAPI definitions
 | |
|       jsonSchema.enum = [jsonSchema.const]
 | |
|       delete jsonSchema.const
 | |
|     }
 | |
| 
 | |
|     Object.keys(jsonSchema).forEach(function (key) {
 | |
|       jsonSchema[key] = replaceUnsupported(jsonSchema[key])
 | |
|     })
 | |
|   }
 | |
| 
 | |
|   return jsonSchema
 | |
| }
 | |
| 
 | |
| function isConsumesFormOnly (schema) {
 | |
|   const consumes = schema.consumes
 | |
|   return (
 | |
|     consumes &&
 | |
|       consumes.length === 1 &&
 | |
|       (consumes[0] === 'application/x-www-form-urlencoded' ||
 | |
|         consumes[0] === 'multipart/form-data')
 | |
|   )
 | |
| }
 | |
| 
 | |
| function resolveBodyParams (parameters, schema, ref) {
 | |
|   const resolved = ref.resolve(schema)
 | |
|   replaceUnsupported(resolved)
 | |
| 
 | |
|   parameters.push({
 | |
|     name: 'body',
 | |
|     in: 'body',
 | |
|     schema: resolved
 | |
|   })
 | |
| }
 | |
| 
 | |
| function resolveCommonParams (container, parameters, schema, ref, sharedSchemas, securityIgnores) {
 | |
|   const resolved = ref.resolve(schema)
 | |
|   const arr = plainJsonObjectToSwagger2(container, resolved, sharedSchemas, securityIgnores)
 | |
|   arr.forEach(swaggerSchema => parameters.push(swaggerSchema))
 | |
| }
 | |
| 
 | |
| // https://swagger.io/docs/specification/2-0/describing-responses/
 | |
| function resolveResponse (fastifyResponseJson, ref) {
 | |
|   // if the user does not provided an out schema
 | |
|   if (!fastifyResponseJson) {
 | |
|     return { 200: { description: 'Default Response' } }
 | |
|   }
 | |
| 
 | |
|   const responsesContainer = {}
 | |
| 
 | |
|   const statusCodes = Object.keys(fastifyResponseJson)
 | |
| 
 | |
|   statusCodes.forEach(statusCode => {
 | |
|     const rawJsonSchema = fastifyResponseJson[statusCode]
 | |
|     const resolved = ref.resolve(rawJsonSchema)
 | |
| 
 | |
|     delete resolved.$schema
 | |
| 
 | |
|     // 2xx is not supported by swagger
 | |
|     const deXXStatusCode = statusCode.toUpperCase().replace('XX', '00')
 | |
|     // conflict when we have both 2xx and 200
 | |
|     if (statusCode.toUpperCase().includes('XX') && statusCodes.includes(deXXStatusCode)) {
 | |
|       return
 | |
|     }
 | |
| 
 | |
|     // converts statusCode to upper case only when it is not "default"
 | |
|     if (statusCode !== 'default') {
 | |
|       statusCode = deXXStatusCode
 | |
|     }
 | |
| 
 | |
|     const response = {
 | |
|       description: rawJsonSchema[xResponseDescription] || rawJsonSchema.description || 'Default Response'
 | |
|     }
 | |
| 
 | |
|     // add headers when there are any.
 | |
|     if (rawJsonSchema.headers) {
 | |
|       response.headers = rawJsonSchema.headers
 | |
|       // remove invalid field
 | |
|       delete resolved.headers
 | |
|     }
 | |
| 
 | |
|     // add schema when type is not 'null'
 | |
|     if (rawJsonSchema.type !== 'null') {
 | |
|       const schema = { ...resolved }
 | |
|       replaceUnsupported(schema)
 | |
|       delete schema[xResponseDescription]
 | |
|       response.schema = schema
 | |
|     }
 | |
| 
 | |
|     responsesContainer[statusCode] = response
 | |
|   })
 | |
| 
 | |
|   return responsesContainer
 | |
| }
 | |
| 
 | |
| function prepareSwaggerMethod (schema, ref, swaggerObject) {
 | |
|   const swaggerMethod = {}
 | |
|   const parameters = []
 | |
| 
 | |
|   // Parse out the security prop keys to ignore
 | |
|   const securityIgnores = [
 | |
|     ...(swaggerObject && swaggerObject.security ? swaggerObject.security : []),
 | |
|     ...(schema && schema.security ? schema.security : [])
 | |
|   ]
 | |
|     .reduce((acc, securitySchemeGroup) => {
 | |
|       Object.keys(securitySchemeGroup).forEach((securitySchemeLabel) => {
 | |
|         const { name, in: category } = swaggerObject.securityDefinitions[securitySchemeLabel]
 | |
|         if (!acc[category]) {
 | |
|           acc[category] = []
 | |
|         }
 | |
|         acc[category].push(name)
 | |
|       })
 | |
|       return acc
 | |
|     }, {})
 | |
| 
 | |
|   // All the data the user can give us, is via the schema object
 | |
|   if (schema) {
 | |
|     if (schema.operationId) swaggerMethod.operationId = schema.operationId
 | |
|     if (schema.summary) swaggerMethod.summary = schema.summary
 | |
|     if (schema.description) swaggerMethod.description = schema.description
 | |
|     if (schema.externalDocs) swaggerMethod.externalDocs = schema.externalDocs
 | |
|     if (schema.tags) swaggerMethod.tags = schema.tags
 | |
|     if (schema.produces) swaggerMethod.produces = schema.produces
 | |
|     if (schema.consumes) swaggerMethod.consumes = schema.consumes
 | |
|     if (schema.querystring) resolveCommonParams('query', parameters, schema.querystring, ref, swaggerObject.definitions, securityIgnores.query)
 | |
|     if (schema.body) {
 | |
|       const isConsumesAllFormOnly = isConsumesFormOnly(schema) || isConsumesFormOnly(swaggerObject)
 | |
|       isConsumesAllFormOnly
 | |
|         ? resolveCommonParams('formData', parameters, schema.body, ref, swaggerObject.definitions)
 | |
|         : resolveBodyParams(parameters, schema.body, ref)
 | |
|     }
 | |
|     if (schema.params) resolveCommonParams('path', parameters, schema.params, ref, swaggerObject.definitions)
 | |
|     if (schema.headers) resolveCommonParams('header', parameters, schema.headers, ref, swaggerObject.definitions, securityIgnores.header)
 | |
|     if (parameters.length > 0) swaggerMethod.parameters = parameters
 | |
|     if (schema.deprecated) swaggerMethod.deprecated = schema.deprecated
 | |
|     if (schema.security) swaggerMethod.security = schema.security
 | |
|     for (const key of Object.keys(schema)) {
 | |
|       if (key.startsWith('x-')) {
 | |
|         swaggerMethod[key] = schema[key]
 | |
|       }
 | |
|     }
 | |
|   }
 | |
| 
 | |
|   swaggerMethod.responses = resolveResponse(schema ? schema.response : null, ref)
 | |
| 
 | |
|   return swaggerMethod
 | |
| }
 | |
| 
 | |
| function prepareSwaggerDefinitions (definitions, ref) {
 | |
|   return Object.entries(definitions)
 | |
|     .reduce((res, [name, definition]) => {
 | |
|       const _ = { ...definition }
 | |
|       const resolved = ref.resolve(_, { externalSchemas: [definitions] })
 | |
| 
 | |
|       // Swagger doesn't accept $id on /definitions schemas.
 | |
|       // The $ids are needed by Ref() to check the URI so we need
 | |
|       // to remove them at the end of the process
 | |
|       delete resolved.$id
 | |
|       delete resolved.definitions
 | |
| 
 | |
|       res[name] = resolved
 | |
|       return res
 | |
|     }, {})
 | |
| }
 | |
| 
 | |
| module.exports = {
 | |
|   prepareDefaultOptions,
 | |
|   prepareSwaggerObject,
 | |
|   prepareSwaggerMethod,
 | |
|   normalizeUrl,
 | |
|   prepareSwaggerDefinitions
 | |
| }
 |