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.
		
		
		
		
		
			
		
			
				
					
					
						
							388 lines
						
					
					
						
							13 KiB
						
					
					
				
			
		
		
	
	
							388 lines
						
					
					
						
							13 KiB
						
					
					
				| 'use strict'
 | |
| 
 | |
| const { readPackageJson, formatParamUrl, resolveLocalRef } = require('../../util/common')
 | |
| const { xResponseDescription, xConsume } = require('../../constants')
 | |
| const { rawRequired } = require('../../symbols')
 | |
| 
 | |
| function prepareDefaultOptions (opts) {
 | |
|   const openapi = opts.openapi
 | |
|   const info = openapi.info || null
 | |
|   const servers = openapi.servers || null
 | |
|   const components = openapi.components || null
 | |
|   const security = openapi.security || null
 | |
|   const tags = openapi.tags || null
 | |
|   const externalDocs = openapi.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.openapi)) {
 | |
|     if (key.startsWith('x-')) {
 | |
|       extensions.push([key, value])
 | |
|     }
 | |
|   }
 | |
| 
 | |
|   return {
 | |
|     info,
 | |
|     servers,
 | |
|     components,
 | |
|     security,
 | |
|     tags,
 | |
|     externalDocs,
 | |
|     stripBasePath,
 | |
|     transform,
 | |
|     hiddenTag,
 | |
|     extensions,
 | |
|     hideUntagged
 | |
|   }
 | |
| }
 | |
| 
 | |
| function prepareOpenapiObject (opts) {
 | |
|   const pkg = readPackageJson()
 | |
|   const openapiObject = {
 | |
|     openapi: '3.0.3',
 | |
|     info: {
 | |
|       version: pkg.version || '1.0.0',
 | |
|       title: pkg.name || ''
 | |
|     },
 | |
|     components: { schemas: {} },
 | |
|     paths: {}
 | |
|   }
 | |
| 
 | |
|   if (opts.info) openapiObject.info = opts.info
 | |
|   if (opts.servers) openapiObject.servers = opts.servers
 | |
|   if (opts.components) openapiObject.components = Object.assign({}, opts.components, { schemas: Object.assign({}, opts.components.schemas) })
 | |
|   if (opts.security) openapiObject.security = opts.security
 | |
|   if (opts.tags) openapiObject.tags = opts.tags
 | |
|   if (opts.externalDocs) openapiObject.externalDocs = opts.externalDocs
 | |
| 
 | |
|   for (const [key, value] of opts.extensions) {
 | |
|     // "x-" extension can not be typed
 | |
|     openapiObject[key] = value
 | |
|   }
 | |
| 
 | |
|   return openapiObject
 | |
| }
 | |
| 
 | |
| function normalizeUrl (url, servers, stripBasePath) {
 | |
|   if (!stripBasePath) return formatParamUrl(url)
 | |
|   servers = Array.isArray(servers) ? servers : []
 | |
|   servers.forEach(function (server) {
 | |
|     const basePath = new URL(server.url).pathname
 | |
|     if (url.startsWith(basePath) && basePath !== '/') {
 | |
|       url = url.replace(basePath, '')
 | |
|     }
 | |
|   })
 | |
|   return formatParamUrl(url)
 | |
| }
 | |
| 
 | |
| function transformDefsToComponents (jsonSchema) {
 | |
|   if (typeof jsonSchema === 'object' && jsonSchema !== null) {
 | |
|     // Handle patternProperties, that is not part of OpenAPI definitions
 | |
|     if (jsonSchema.patternProperties) {
 | |
|       jsonSchema.additionalProperties = Object.values(jsonSchema.patternProperties)[0]
 | |
|       delete jsonSchema.patternProperties
 | |
|     } else if (jsonSchema.const) {
 | |
|       // OAS 3.1 supports `const` but it is not supported by `swagger-ui`
 | |
|       // https://swagger.io/docs/specification/data-models/keywords/
 | |
|       jsonSchema.enum = [jsonSchema.const]
 | |
|       delete jsonSchema.const
 | |
|     }
 | |
| 
 | |
|     Object.keys(jsonSchema).forEach(function (key) {
 | |
|       if (key === 'properties') {
 | |
|         Object.keys(jsonSchema[key]).forEach(function (prop) {
 | |
|           jsonSchema[key][prop] = transformDefsToComponents(jsonSchema[key][prop])
 | |
|         })
 | |
|       } else if (key === '$ref') {
 | |
|         jsonSchema[key] = jsonSchema[key].replace('definitions', 'components/schemas')
 | |
|       } else if (key === 'examples' && Array.isArray(jsonSchema[key]) && (jsonSchema[key].length > 1)) {
 | |
|         jsonSchema.examples = convertExamplesArrayToObject(jsonSchema.examples)
 | |
|       } else if (key === 'examples' && Array.isArray(jsonSchema[key]) && (jsonSchema[key].length === 1)) {
 | |
|         jsonSchema.example = jsonSchema[key][0]
 | |
|         delete jsonSchema[key]
 | |
|       } else if (key === '$id' || key === '$schema') {
 | |
|         delete jsonSchema[key]
 | |
|       } else {
 | |
|         jsonSchema[key] = transformDefsToComponents(jsonSchema[key])
 | |
|       }
 | |
|     })
 | |
|   }
 | |
|   return jsonSchema
 | |
| }
 | |
| 
 | |
| function convertExamplesArrayToObject (examples) {
 | |
|   return examples.reduce((examplesObject, example, index) => {
 | |
|     if (typeof example === 'object') {
 | |
|       examplesObject['example' + (index + 1)] = { value: example }
 | |
|     } else {
 | |
|       examplesObject[example] = { value: example }
 | |
|     }
 | |
| 
 | |
|     return examplesObject
 | |
|   }, {})
 | |
| }
 | |
| 
 | |
| // For supported keys read:
 | |
| // https://swagger.io/docs/specification/describing-parameters/
 | |
| function plainJsonObjectToOpenapi3 (container, jsonSchema, externalSchemas, securityIgnores = []) {
 | |
|   const obj = transformDefsToComponents(resolveLocalRef(jsonSchema, externalSchemas))
 | |
|   let toOpenapiProp
 | |
|   switch (container) {
 | |
|     case 'cookie':
 | |
|     case 'query':
 | |
|       toOpenapiProp = function (propertyName, jsonSchemaElement) {
 | |
|         const result = {
 | |
|           in: container,
 | |
|           name: propertyName,
 | |
|           required: jsonSchemaElement.required
 | |
|         }
 | |
|         // complex serialization in query or cookie, eg. JSON
 | |
|         // https://swagger.io/docs/specification/describing-parameters/#schema-vs-content
 | |
|         if (jsonSchemaElement[xConsume]) {
 | |
|           result.content = {
 | |
|             [jsonSchemaElement[xConsume]]: {
 | |
|               schema: {
 | |
|                 ...jsonSchemaElement,
 | |
|                 required: jsonSchemaElement[rawRequired]
 | |
|               }
 | |
|             }
 | |
|           }
 | |
| 
 | |
|           delete result.content[jsonSchemaElement[xConsume]].schema[xConsume]
 | |
|         } else {
 | |
|           result.schema = jsonSchemaElement
 | |
|         }
 | |
|         // description should be optional
 | |
|         if (jsonSchemaElement.description) result.description = jsonSchemaElement.description
 | |
|         // optionally add serialization format style
 | |
|         if (jsonSchema.style) result.style = jsonSchema.style
 | |
|         if (jsonSchema.explode != null) result.explode = jsonSchema.explode
 | |
|         return result
 | |
|       }
 | |
|       break
 | |
|     case 'path':
 | |
|       toOpenapiProp = function (propertyName, jsonSchemaElement) {
 | |
|         const result = {
 | |
|           in: container,
 | |
|           name: propertyName,
 | |
|           required: true,
 | |
|           schema: jsonSchemaElement
 | |
|         }
 | |
|         // description should be optional
 | |
|         if (jsonSchemaElement.description) result.description = jsonSchemaElement.description
 | |
|         return result
 | |
|       }
 | |
|       break
 | |
|     case 'header':
 | |
|       toOpenapiProp = function (propertyName, jsonSchemaElement) {
 | |
|         return {
 | |
|           in: 'header',
 | |
|           name: propertyName,
 | |
|           required: jsonSchemaElement.required,
 | |
|           description: jsonSchemaElement.description,
 | |
|           schema: {
 | |
|             type: jsonSchemaElement.type
 | |
|           }
 | |
|         }
 | |
|       }
 | |
|       break
 | |
|   }
 | |
| 
 | |
|   return Object.keys(obj)
 | |
|     .filter((propKey) => (!securityIgnores.includes(propKey)))
 | |
|     .map((propKey) => {
 | |
|       const jsonSchema = toOpenapiProp(propKey, obj[propKey])
 | |
|       if (jsonSchema.schema) {
 | |
|         // it is needed as required in schema is invalid prop - delete only if needed
 | |
|         if (typeof jsonSchema.schema.required !== 'undefined') delete jsonSchema.schema.required
 | |
|         // it is needed as description in schema is invalid prop - delete only if needed
 | |
|         if (typeof jsonSchema.schema.description !== 'undefined') delete jsonSchema.schema.description
 | |
|       }
 | |
|       return jsonSchema
 | |
|     })
 | |
| }
 | |
| 
 | |
| function resolveBodyParams (body, schema, consumes, ref) {
 | |
|   const resolved = transformDefsToComponents(ref.resolve(schema))
 | |
|   if ((Array.isArray(consumes) && consumes.length === 0) || typeof consumes === 'undefined') {
 | |
|     consumes = ['application/json']
 | |
|   }
 | |
| 
 | |
|   consumes.forEach((consume) => {
 | |
|     body.content[consume] = {
 | |
|       schema: resolved
 | |
|     }
 | |
|   })
 | |
|   if (resolved && resolved.required && resolved.required.length) {
 | |
|     body.required = true
 | |
|   }
 | |
| }
 | |
| 
 | |
| function resolveCommonParams (container, parameters, schema, ref, sharedSchemas, securityIgnores) {
 | |
|   const schemasPath = '#/components/schemas/'
 | |
|   let resolved = transformDefsToComponents(ref.resolve(schema))
 | |
| 
 | |
|   // if the resolved definition is in global schema
 | |
|   if (resolved.$ref && resolved.$ref.startsWith(schemasPath)) {
 | |
|     const parts = resolved.$ref.split(schemasPath)
 | |
|     const pathParts = parts[1].split('/')
 | |
|     resolved = pathParts.reduce((resolved, pathPart) => resolved[pathPart], ref.definitions().definitions)
 | |
|   }
 | |
| 
 | |
|   const arr = plainJsonObjectToOpenapi3(container, resolved, sharedSchemas, securityIgnores)
 | |
|   arr.forEach(swaggerSchema => parameters.push(swaggerSchema))
 | |
| }
 | |
| 
 | |
| // https://swagger.io/docs/specification/describing-responses/
 | |
| function resolveResponse (fastifyResponseJson, produces, 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 = transformDefsToComponents(ref.resolve(rawJsonSchema))
 | |
| 
 | |
|     /**
 | |
|      * 2xx require to be all upper-case
 | |
|      * converts statusCode to upper case only when it is not "default"
 | |
|      */
 | |
|     if (statusCode !== 'default') {
 | |
|       statusCode = statusCode.toUpperCase()
 | |
|     }
 | |
| 
 | |
|     const response = {
 | |
|       description: resolved[xResponseDescription] || rawJsonSchema.description || 'Default Response'
 | |
|     }
 | |
| 
 | |
|     // add headers when there are any.
 | |
|     if (rawJsonSchema.headers) {
 | |
|       response.headers = {}
 | |
|       Object.keys(rawJsonSchema.headers).forEach(function (key) {
 | |
|         const header = {
 | |
|           schema: rawJsonSchema.headers[key]
 | |
|         }
 | |
| 
 | |
|         if (rawJsonSchema.headers[key].description) {
 | |
|           header.description = rawJsonSchema.headers[key].description
 | |
|           // remove invalid field
 | |
|           delete header.schema.description
 | |
|         }
 | |
| 
 | |
|         response.headers[key] = header
 | |
|       })
 | |
|       // remove invalid field
 | |
|       delete resolved.headers
 | |
|     }
 | |
| 
 | |
|     // add schema when type is not 'null'
 | |
|     if (rawJsonSchema.type !== 'null') {
 | |
|       const content = {}
 | |
| 
 | |
|       if ((Array.isArray(produces) && produces.length === 0) || typeof produces === 'undefined') {
 | |
|         produces = ['application/json']
 | |
|       }
 | |
| 
 | |
|       delete resolved[xResponseDescription]
 | |
|       produces.forEach((produce) => {
 | |
|         content[produce] = {
 | |
|           schema: resolved
 | |
|         }
 | |
|       })
 | |
| 
 | |
|       response.content = content
 | |
|     }
 | |
| 
 | |
|     responsesContainer[statusCode] = response
 | |
|   })
 | |
| 
 | |
|   return responsesContainer
 | |
| }
 | |
| 
 | |
| function prepareOpenapiMethod (schema, ref, openapiObject) {
 | |
|   const openapiMethod = {}
 | |
|   const parameters = []
 | |
| 
 | |
|   // Parse out the security prop keys to ignore
 | |
|   const securityIgnores = [
 | |
|     ...(openapiObject && openapiObject.security ? openapiObject.security : []),
 | |
|     ...(schema && schema.security ? schema.security : [])
 | |
|   ]
 | |
|     .reduce((acc, securitySchemeGroup) => {
 | |
|       Object.keys(securitySchemeGroup).forEach((securitySchemeLabel) => {
 | |
|         const { name, in: category } = openapiObject.components.securitySchemes[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) openapiMethod.operationId = schema.operationId
 | |
|     if (schema.summary) openapiMethod.summary = schema.summary
 | |
|     if (schema.tags) openapiMethod.tags = schema.tags
 | |
|     if (schema.description) openapiMethod.description = schema.description
 | |
|     if (schema.externalDocs) openapiMethod.externalDocs = schema.externalDocs
 | |
|     if (schema.querystring) resolveCommonParams('query', parameters, schema.querystring, ref, openapiObject.definitions, securityIgnores.query)
 | |
|     if (schema.body) {
 | |
|       openapiMethod.requestBody = { content: {} }
 | |
|       resolveBodyParams(openapiMethod.requestBody, schema.body, schema.consumes, ref)
 | |
|     }
 | |
|     if (schema.params) resolveCommonParams('path', parameters, schema.params, ref, openapiObject.definitions)
 | |
|     if (schema.headers) resolveCommonParams('header', parameters, schema.headers, ref, openapiObject.definitions, securityIgnores.header)
 | |
|     // TODO: need to documentation, we treat it same as the querystring
 | |
|     // fastify do not support cookies schema in first place
 | |
|     if (schema.cookies) resolveCommonParams('cookie', parameters, schema.cookies, ref, openapiObject.definitions, securityIgnores.cookie)
 | |
|     if (parameters.length > 0) openapiMethod.parameters = parameters
 | |
|     if (schema.deprecated) openapiMethod.deprecated = schema.deprecated
 | |
|     if (schema.security) openapiMethod.security = schema.security
 | |
|     if (schema.servers) openapiMethod.servers = schema.servers
 | |
|     for (const key of Object.keys(schema)) {
 | |
|       if (key.startsWith('x-')) {
 | |
|         openapiMethod[key] = schema[key]
 | |
|       }
 | |
|     }
 | |
|   }
 | |
| 
 | |
|   openapiMethod.responses = resolveResponse(schema ? schema.response : null, schema ? schema.produces : null, ref)
 | |
| 
 | |
|   return openapiMethod
 | |
| }
 | |
| 
 | |
| function prepareOpenapiSchemas (schemas, ref) {
 | |
|   return Object.entries(schemas)
 | |
|     .reduce((res, [name, schema]) => {
 | |
|       const _ = { ...schema }
 | |
|       const resolved = transformDefsToComponents(ref.resolve(_, { externalSchemas: [schemas] }))
 | |
| 
 | |
|       // 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
 | |
|       // definitions are added by resolve but they are replace by components.schemas
 | |
|       delete resolved.$id
 | |
|       delete resolved.definitions
 | |
| 
 | |
|       res[name] = resolved
 | |
|       return res
 | |
|     }, {})
 | |
| }
 | |
| 
 | |
| module.exports = {
 | |
|   prepareDefaultOptions,
 | |
|   prepareOpenapiObject,
 | |
|   prepareOpenapiMethod,
 | |
|   prepareOpenapiSchemas,
 | |
|   normalizeUrl
 | |
| }
 |