Projektstart
This commit is contained in:
85
backend/node_modules/@fastify/swagger/lib/spec/openapi/index.js
generated
vendored
Normal file
85
backend/node_modules/@fastify/swagger/lib/spec/openapi/index.js
generated
vendored
Normal file
@@ -0,0 +1,85 @@
|
||||
'use strict'
|
||||
|
||||
const yaml = require('yaml')
|
||||
const { shouldRouteHide } = require('../../util/should-route-hide')
|
||||
const { prepareDefaultOptions, prepareOpenapiObject, prepareOpenapiMethod, prepareOpenapiSchemas, normalizeUrl, resolveServerUrls } = require('./utils')
|
||||
|
||||
module.exports = function (opts, cache, routes, Ref) {
|
||||
let ref
|
||||
|
||||
const defOpts = prepareDefaultOptions(opts)
|
||||
|
||||
return function (opts) {
|
||||
if (opts?.yaml) {
|
||||
if (cache.string) return cache.string
|
||||
} else {
|
||||
if (cache.object) return cache.object
|
||||
}
|
||||
|
||||
// Base Openapi info
|
||||
const openapiObject = prepareOpenapiObject(defOpts)
|
||||
|
||||
ref = Ref()
|
||||
openapiObject.components.schemas = prepareOpenapiSchemas(defOpts, {
|
||||
...openapiObject.components.schemas,
|
||||
...(ref.definitions().definitions)
|
||||
}, ref)
|
||||
|
||||
const serverUrls = resolveServerUrls(defOpts.servers)
|
||||
|
||||
for (const route of routes) {
|
||||
const transformResult = route.config?.swaggerTransform !== undefined
|
||||
? route.config.swaggerTransform
|
||||
? route.config.swaggerTransform({ schema: route.schema, url: route.url, route, openapiObject })
|
||||
: {}
|
||||
: defOpts.transform
|
||||
? defOpts.transform({ schema: route.schema, url: route.url, route, openapiObject })
|
||||
: {}
|
||||
|
||||
const schema = transformResult.schema || route.schema
|
||||
const shouldRouteHideOpts = {
|
||||
hiddenTag: defOpts.hiddenTag,
|
||||
hideUntagged: defOpts.hideUntagged
|
||||
}
|
||||
|
||||
if (shouldRouteHide(schema, shouldRouteHideOpts)) continue
|
||||
|
||||
let url = transformResult.url || route.url
|
||||
url = normalizeUrl(url, serverUrls, defOpts.stripBasePath)
|
||||
|
||||
const openapiRoute = Object.assign({}, openapiObject.paths[url])
|
||||
|
||||
const openapiMethod = prepareOpenapiMethod(defOpts, schema, ref, openapiObject, url)
|
||||
|
||||
if (route.links) {
|
||||
for (const statusCode of Object.keys(route.links)) {
|
||||
if (!openapiMethod.responses[statusCode]) {
|
||||
throw new Error(`missing status code ${statusCode} in route ${route.path}`)
|
||||
}
|
||||
openapiMethod.responses[statusCode].links = route.links[statusCode]
|
||||
}
|
||||
}
|
||||
|
||||
// route.method should be either a String, like 'POST', or an Array of Strings, like ['POST','PUT','PATCH']
|
||||
const methods = typeof route.method === 'string' ? [route.method] : route.method
|
||||
|
||||
for (const method of methods) {
|
||||
openapiRoute[method.toLowerCase()] = openapiMethod
|
||||
}
|
||||
|
||||
openapiObject.paths[url] = openapiRoute
|
||||
}
|
||||
|
||||
const transformObjectResult = defOpts.transformObject
|
||||
? defOpts.transformObject({ openapiObject })
|
||||
: openapiObject
|
||||
|
||||
if (opts?.yaml) {
|
||||
cache.string = yaml.stringify(transformObjectResult, { strict: false })
|
||||
return cache.string
|
||||
}
|
||||
|
||||
cache.object = transformObjectResult
|
||||
return cache.object
|
||||
}
|
||||
}
|
||||
602
backend/node_modules/@fastify/swagger/lib/spec/openapi/utils.js
generated
vendored
Normal file
602
backend/node_modules/@fastify/swagger/lib/spec/openapi/utils.js
generated
vendored
Normal file
@@ -0,0 +1,602 @@
|
||||
'use strict'
|
||||
|
||||
const { readPackageJson } = require('../../util/read-package-json')
|
||||
const { formatParamUrl } = require('../../util/format-param-url')
|
||||
const { resolveLocalRef } = require('../../util/resolve-local-ref')
|
||||
const { resolveSchemaReference } = require('../../util/resolve-schema-reference')
|
||||
const { xResponseDescription, xConsume, xExamples } = require('../../constants')
|
||||
const { rawRequired } = require('../../symbols')
|
||||
const { generateParamsSchema } = require('../../util/generate-params-schema')
|
||||
const { hasParams } = require('../../util/match-params')
|
||||
|
||||
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 transformObject = opts.transformObject
|
||||
const hiddenTag = opts.hiddenTag
|
||||
const hideUntagged = opts.hideUntagged
|
||||
const extensions = []
|
||||
const convertConstToEnum = opts.convertConstToEnum
|
||||
|
||||
for (const [key, value] of Object.entries(opts.openapi)) {
|
||||
if (key.startsWith('x-')) {
|
||||
extensions.push([key, value])
|
||||
}
|
||||
}
|
||||
|
||||
return {
|
||||
...openapi,
|
||||
info,
|
||||
servers,
|
||||
components,
|
||||
security,
|
||||
tags,
|
||||
externalDocs,
|
||||
stripBasePath,
|
||||
transform,
|
||||
transformObject,
|
||||
hiddenTag,
|
||||
extensions,
|
||||
hideUntagged,
|
||||
convertConstToEnum
|
||||
}
|
||||
}
|
||||
|
||||
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.openapi) openapiObject.openapi = opts.openapi
|
||||
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.paths) openapiObject.paths = opts.paths
|
||||
if (opts.webhooks) openapiObject.webhooks = opts.webhooks
|
||||
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, serverUrls, stripBasePath) {
|
||||
if (!stripBasePath) return formatParamUrl(url)
|
||||
serverUrls.forEach(function (serverUrl) {
|
||||
const basePath = serverUrl.startsWith('/') ? serverUrl : new URL(serverUrl).pathname
|
||||
if (url.startsWith(basePath) && basePath !== '/') {
|
||||
url = url.replace(basePath, '')
|
||||
}
|
||||
})
|
||||
return formatParamUrl(url)
|
||||
}
|
||||
|
||||
function resolveServerUrls (servers) {
|
||||
const resolvedUrls = []
|
||||
const findVariablesRegex = /\{([^{}]+)\}/gu // As for OpenAPI v3 spec url variables are named in brackets, e.g. {foo}
|
||||
|
||||
servers = Array.isArray(servers) ? servers : []
|
||||
for (const server of servers) {
|
||||
const originalUrl = server.url
|
||||
const variables = server.variables
|
||||
|
||||
let url = originalUrl
|
||||
const matches = url.matchAll(findVariablesRegex)
|
||||
|
||||
for (const [nameInBrackets, name] of matches) {
|
||||
const value = variables?.[name]?.default
|
||||
|
||||
if (value === undefined) {
|
||||
throw new Error(`Server URL ${originalUrl} could not be resolved. Make sure to provide a default value for each URL variable.`)
|
||||
}
|
||||
|
||||
url = url.replace(nameInBrackets, value)
|
||||
}
|
||||
|
||||
resolvedUrls.push(url)
|
||||
}
|
||||
|
||||
return resolvedUrls
|
||||
}
|
||||
|
||||
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 (opts, container, jsonSchema, externalSchemas, securityIgnores = []) {
|
||||
const obj = convertJsonSchemaToOpenapi3(opts, resolveLocalRef(jsonSchema, externalSchemas))
|
||||
let toOpenapiProp
|
||||
switch (container) {
|
||||
case 'cookie':
|
||||
case 'header':
|
||||
case 'query':
|
||||
toOpenapiProp = function (propertyName, jsonSchemaElement) {
|
||||
let result = {
|
||||
in: container,
|
||||
name: propertyName,
|
||||
required: jsonSchemaElement.required
|
||||
}
|
||||
|
||||
const media = schemaToMedia(jsonSchemaElement)
|
||||
|
||||
// complex serialization in query or cookie, eg. JSON
|
||||
// https://swagger.io/docs/specification/describing-parameters/#schema-vs-content
|
||||
if (jsonSchemaElement[xConsume]) {
|
||||
media.schema.required = jsonSchemaElement[rawRequired]
|
||||
|
||||
result.content = {
|
||||
[jsonSchemaElement[xConsume]]: media
|
||||
}
|
||||
|
||||
delete result.content[jsonSchemaElement[xConsume]].schema[xConsume]
|
||||
} else {
|
||||
result = { ...media, ...result }
|
||||
}
|
||||
// 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
|
||||
if (jsonSchema.allowReserved === true && container === 'query') {
|
||||
result.allowReserved = jsonSchema.allowReserved
|
||||
}
|
||||
return result
|
||||
}
|
||||
break
|
||||
case 'path':
|
||||
toOpenapiProp = function (propertyName, jsonSchemaElement) {
|
||||
const media = schemaToMedia(jsonSchemaElement)
|
||||
|
||||
const result = {
|
||||
...media,
|
||||
in: container,
|
||||
name: propertyName,
|
||||
required: true
|
||||
}
|
||||
|
||||
// description should be optional
|
||||
if (jsonSchemaElement.description) result.description = jsonSchemaElement.description
|
||||
return result
|
||||
}
|
||||
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 (jsonSchema.schema.required !== undefined) delete jsonSchema.schema.required
|
||||
// it is needed as description in schema is invalid prop - delete only if needed
|
||||
if (jsonSchema.schema.description !== undefined) delete jsonSchema.schema.description
|
||||
}
|
||||
return jsonSchema
|
||||
})
|
||||
}
|
||||
|
||||
const schemaTypeToNestedSchemas = {
|
||||
object: (schema) => {
|
||||
return [
|
||||
...Object.values(schema.properties || {}),
|
||||
...Object.values(schema.patternProperties || {}),
|
||||
...Object.values(schema.additionalProperties || {})
|
||||
]
|
||||
},
|
||||
array: (schema) => {
|
||||
return [
|
||||
...(schema.items ? [schema.items] : []),
|
||||
...(schema.contains ? [schema.contains] : [])
|
||||
]
|
||||
}
|
||||
}
|
||||
|
||||
function resolveSchemaExamples (schema) {
|
||||
const example = schema[xExamples] ?? schema.examples?.[0]
|
||||
if (typeof example !== 'undefined') {
|
||||
schema.example = example
|
||||
}
|
||||
delete schema[xExamples]
|
||||
delete schema.examples
|
||||
}
|
||||
|
||||
function resolveSchemaExamplesRecursive (schema) {
|
||||
resolveSchemaExamples(schema)
|
||||
const getNestedSchemas = schemaTypeToNestedSchemas[schema.type]
|
||||
const nestedSchemas = getNestedSchemas?.(schema) ?? []
|
||||
for (const nestedSchema of nestedSchemas) {
|
||||
resolveSchemaExamplesRecursive(nestedSchema)
|
||||
}
|
||||
}
|
||||
|
||||
function schemaToMedia (schema) {
|
||||
const media = { schema }
|
||||
|
||||
if (schema.examples?.length === 1) {
|
||||
media.example = schema.examples[0]
|
||||
delete schema.examples
|
||||
} else if (schema.examples?.length > 1) {
|
||||
media.examples = convertExamplesArrayToObject(schema.examples)
|
||||
// examples is invalid property of media object schema
|
||||
delete schema.examples
|
||||
}
|
||||
|
||||
if (schema[xExamples]) {
|
||||
media.examples = schema[xExamples]
|
||||
delete schema[xExamples]
|
||||
}
|
||||
|
||||
return media
|
||||
}
|
||||
|
||||
function schemaToMediaRecursive (schema) {
|
||||
const media = schemaToMedia(schema)
|
||||
resolveSchemaExamplesRecursive(schema)
|
||||
return media
|
||||
}
|
||||
|
||||
function resolveBodyParams (opts, body, schema, consumes, ref) {
|
||||
const resolved = convertJsonSchemaToOpenapi3(opts, ref.resolve(schema))
|
||||
|
||||
if (resolved.content?.[Object.keys(resolved.content)[0]].schema) {
|
||||
for (const contentType in schema.content) {
|
||||
body.content[contentType] = schemaToMediaRecursive(resolved.content[contentType].schema)
|
||||
}
|
||||
} else {
|
||||
if ((Array.isArray(consumes) && consumes.length === 0) || consumes === undefined) {
|
||||
consumes = ['application/json']
|
||||
}
|
||||
|
||||
const media = schemaToMediaRecursive(resolved)
|
||||
consumes.forEach((consume) => {
|
||||
body.content[consume] = media
|
||||
})
|
||||
|
||||
if (resolved?.required?.length) {
|
||||
body.required = true
|
||||
}
|
||||
|
||||
if (resolved?.description) {
|
||||
body.description = resolved.description
|
||||
}
|
||||
}
|
||||
}
|
||||
|
||||
function resolveCommonParams (opts, container, parameters, schema, ref, sharedSchemas, securityIgnores) {
|
||||
const schemasPath = '#/components/schemas/'
|
||||
let resolved = convertJsonSchemaToOpenapi3(opts, ref.resolve(schema))
|
||||
|
||||
// if the resolved definition is in global schema
|
||||
if (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(opts, container, resolved, { ...sharedSchemas, ...ref.definitions().definitions }, securityIgnores)
|
||||
arr.forEach(swaggerSchema => parameters.push(swaggerSchema))
|
||||
}
|
||||
|
||||
function findReferenceDescription (rawSchema, ref) {
|
||||
const resolved = resolveSchemaReference(rawSchema, ref)
|
||||
return resolved?.description
|
||||
}
|
||||
|
||||
// https://swagger.io/docs/specification/describing-responses/
|
||||
function resolveResponse (opts, 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 = convertJsonSchemaToOpenapi3(opts, 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 ||
|
||||
findReferenceDescription(rawJsonSchema, ref) ||
|
||||
'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') {
|
||||
if (resolved.content?.[Object.keys(resolved.content)[0]].schema) {
|
||||
response.content = resolved.content
|
||||
} else {
|
||||
const content = {}
|
||||
|
||||
if ((Array.isArray(produces) && produces.length === 0) || produces === undefined) {
|
||||
produces = ['application/json']
|
||||
}
|
||||
|
||||
delete resolved[xResponseDescription]
|
||||
|
||||
const media = schemaToMediaRecursive(resolved)
|
||||
|
||||
for (const produce of produces) {
|
||||
content[produce] = media
|
||||
}
|
||||
|
||||
response.content = content
|
||||
}
|
||||
}
|
||||
|
||||
responsesContainer[statusCode] = response
|
||||
})
|
||||
|
||||
return responsesContainer
|
||||
}
|
||||
|
||||
function resolveCallbacks (opts, schema, ref) {
|
||||
const callbacksContainer = {}
|
||||
|
||||
// Iterate over each callback event
|
||||
for (const eventName in schema) {
|
||||
if (!schema[eventName]) {
|
||||
continue
|
||||
}
|
||||
|
||||
// Create an empty object to house the future iterations
|
||||
callbacksContainer[eventName] = {}
|
||||
const eventSchema = schema[eventName]
|
||||
|
||||
// Iterate over each callbackUrl for the event
|
||||
for (const callbackUrl in eventSchema) {
|
||||
if (!callbackUrl || !eventSchema[callbackUrl]) {
|
||||
continue
|
||||
}
|
||||
|
||||
// Create an empty object to house the future iterations
|
||||
callbacksContainer[eventName][callbackUrl] = {}
|
||||
const callbackSchema = eventSchema[callbackUrl]
|
||||
|
||||
// Iterate over each httpMethod for the callbackUrl
|
||||
for (const httpMethodName in callbackSchema) {
|
||||
if (!httpMethodName || !callbackSchema[httpMethodName]) {
|
||||
continue
|
||||
}
|
||||
|
||||
const httpMethodSchema = callbackSchema[httpMethodName]
|
||||
const httpMethodContainer = {}
|
||||
|
||||
if (httpMethodSchema.requestBody) {
|
||||
httpMethodContainer.requestBody = convertJsonSchemaToOpenapi3(
|
||||
opts,
|
||||
ref.resolve(httpMethodSchema.requestBody)
|
||||
)
|
||||
}
|
||||
|
||||
// If a response is not provided, set a 2XX default response
|
||||
httpMethodContainer.responses = httpMethodSchema.responses
|
||||
? convertJsonSchemaToOpenapi3(opts, ref.resolve(httpMethodSchema.responses))
|
||||
: { '2XX': { description: 'Default Response' } }
|
||||
|
||||
// Set the schema at the appropriate location in the response object
|
||||
callbacksContainer[eventName][callbackUrl][httpMethodName] = httpMethodContainer
|
||||
}
|
||||
}
|
||||
}
|
||||
|
||||
return callbacksContainer
|
||||
}
|
||||
|
||||
function prepareOpenapiMethod (opts, schema, ref, openapiObject, url) {
|
||||
const openapiMethod = {}
|
||||
const parameters = []
|
||||
|
||||
// Parse out the security prop keys to ignore
|
||||
const securityIgnores = [
|
||||
...(openapiObject?.security || []),
|
||||
...(schema?.security || [])
|
||||
]
|
||||
.reduce((acc, securitySchemeGroup) => {
|
||||
Object.keys(securitySchemeGroup).forEach((securitySchemeLabel) => {
|
||||
const scheme = openapiObject.components.securitySchemes[securitySchemeLabel]
|
||||
const isBearer = scheme.type === 'http' && scheme.scheme === 'bearer'
|
||||
const category = isBearer ? 'header' : scheme.in
|
||||
const name = isBearer ? 'authorization' : scheme.name
|
||||
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(opts, 'query', parameters, schema.querystring, ref, openapiObject.definitions, securityIgnores.query)
|
||||
if (schema.body) {
|
||||
openapiMethod.requestBody = { content: {} }
|
||||
resolveBodyParams(opts, openapiMethod.requestBody, schema.body, schema.consumes, ref)
|
||||
}
|
||||
if (schema.params) resolveCommonParams(opts, 'path', parameters, schema.params, ref, openapiObject.definitions)
|
||||
if (schema.headers) resolveCommonParams(opts, '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(opts, '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
|
||||
if (schema.callbacks) openapiMethod.callbacks = resolveCallbacks(opts, schema.callbacks, ref)
|
||||
for (const key of Object.keys(schema)) {
|
||||
if (key.startsWith('x-')) {
|
||||
openapiMethod[key] = schema[key]
|
||||
}
|
||||
}
|
||||
}
|
||||
|
||||
// If there is no schema or schema.params, we need to generate them
|
||||
if ((!schema || !schema.params) && hasParams(url)) {
|
||||
const schemaGenerated = generateParamsSchema(url)
|
||||
resolveCommonParams(opts, 'path', parameters, schemaGenerated.params, ref, openapiObject.definitions)
|
||||
openapiMethod.parameters = parameters
|
||||
}
|
||||
|
||||
openapiMethod.responses = resolveResponse(opts, schema ? schema.response : null, schema ? schema.produces : null, ref)
|
||||
|
||||
return openapiMethod
|
||||
}
|
||||
|
||||
function convertJsonSchemaToOpenapi3 (opts, jsonSchema) {
|
||||
if (typeof jsonSchema !== 'object' || jsonSchema === null) {
|
||||
return jsonSchema
|
||||
}
|
||||
|
||||
if (Array.isArray(jsonSchema)) {
|
||||
return jsonSchema.map((s) => convertJsonSchemaToOpenapi3(opts, s))
|
||||
}
|
||||
|
||||
const openapiSchema = { ...jsonSchema }
|
||||
|
||||
if (Object.hasOwn(openapiSchema, '$ref') && Object.keys(openapiSchema).length !== 1) {
|
||||
for (const key of Object.keys(openapiSchema).filter(k => k !== '$ref')) {
|
||||
delete openapiSchema[key]
|
||||
continue
|
||||
}
|
||||
}
|
||||
|
||||
for (const key of Object.keys(openapiSchema)) {
|
||||
const value = openapiSchema[key]
|
||||
|
||||
if (key === '$id' || key === '$schema' || key === 'definitions') {
|
||||
// TODO: this breaks references to the definition properties
|
||||
delete openapiSchema[key]
|
||||
continue
|
||||
}
|
||||
|
||||
if (key === '$ref') {
|
||||
openapiSchema.$ref = value.replace('definitions', 'components/schemas')
|
||||
continue
|
||||
}
|
||||
|
||||
if (opts.convertConstToEnum && key === 'const') {
|
||||
// OAS 3.1 supports `const` but it is not supported by `swagger-ui`
|
||||
// https://swagger.io/docs/specification/data-models/keywords/
|
||||
// TODO: check if enum property already exists
|
||||
// TODO: this breaks references to the const property
|
||||
openapiSchema.enum = [openapiSchema.const]
|
||||
delete openapiSchema.const
|
||||
continue
|
||||
}
|
||||
|
||||
if (key === 'patternProperties') {
|
||||
// TODO: check if additionalProperties property already exists
|
||||
// TODO: this breaks references to the additionalProperties properties
|
||||
// TODO: patternProperties actually allowed in the openapi schema, but should
|
||||
// always start with "x-" prefix
|
||||
const propertyJsonSchema = Object.values(openapiSchema.patternProperties)[0]
|
||||
const propertyOpenapiSchema = convertJsonSchemaToOpenapi3(opts, propertyJsonSchema)
|
||||
openapiSchema.additionalProperties = propertyOpenapiSchema
|
||||
delete openapiSchema.patternProperties
|
||||
continue
|
||||
}
|
||||
|
||||
if (key === 'properties') {
|
||||
openapiSchema[key] = {}
|
||||
for (const propertyName of Object.keys(value)) {
|
||||
const propertyJsonSchema = value[propertyName]
|
||||
const propertyOpenapiSchema = convertJsonSchemaToOpenapi3(opts, propertyJsonSchema)
|
||||
openapiSchema[key][propertyName] = propertyOpenapiSchema
|
||||
}
|
||||
continue
|
||||
}
|
||||
|
||||
openapiSchema[key] = convertJsonSchemaToOpenapi3(opts, value)
|
||||
}
|
||||
|
||||
return openapiSchema
|
||||
}
|
||||
|
||||
function prepareOpenapiSchemas (opts, jsonSchemas, ref) {
|
||||
const openapiSchemas = {}
|
||||
|
||||
for (const schemaName of Object.keys(jsonSchemas)) {
|
||||
const jsonSchema = { ...jsonSchemas[schemaName] }
|
||||
|
||||
const resolvedJsonSchema = ref.resolve(jsonSchema, { externalSchemas: [jsonSchemas] })
|
||||
const openapiSchema = convertJsonSchemaToOpenapi3(opts, resolvedJsonSchema)
|
||||
resolveSchemaExamplesRecursive(openapiSchema)
|
||||
|
||||
openapiSchemas[schemaName] = openapiSchema
|
||||
}
|
||||
return openapiSchemas
|
||||
}
|
||||
|
||||
module.exports = {
|
||||
prepareDefaultOptions,
|
||||
prepareOpenapiObject,
|
||||
prepareOpenapiMethod,
|
||||
prepareOpenapiSchemas,
|
||||
resolveServerUrls,
|
||||
normalizeUrl
|
||||
}
|
||||
77
backend/node_modules/@fastify/swagger/lib/spec/swagger/index.js
generated
vendored
Normal file
77
backend/node_modules/@fastify/swagger/lib/spec/swagger/index.js
generated
vendored
Normal file
@@ -0,0 +1,77 @@
|
||||
'use strict'
|
||||
|
||||
const yaml = require('yaml')
|
||||
const { shouldRouteHide } = require('../../util/should-route-hide')
|
||||
const { prepareDefaultOptions, prepareSwaggerObject, prepareSwaggerMethod, normalizeUrl, prepareSwaggerDefinitions } = require('./utils')
|
||||
|
||||
module.exports = function (opts, cache, routes, Ref) {
|
||||
let ref
|
||||
|
||||
const defOpts = prepareDefaultOptions(opts)
|
||||
|
||||
return function (opts) {
|
||||
if (opts?.yaml) {
|
||||
if (cache.string) return cache.string
|
||||
} else {
|
||||
if (cache.object) return cache.object
|
||||
}
|
||||
|
||||
const swaggerObject = prepareSwaggerObject(defOpts)
|
||||
|
||||
ref = Ref()
|
||||
swaggerObject.definitions = prepareSwaggerDefinitions({
|
||||
...swaggerObject.definitions,
|
||||
...(ref.definitions().definitions)
|
||||
}, ref)
|
||||
|
||||
for (const route of routes) {
|
||||
const transformResult = route.config?.swaggerTransform !== undefined
|
||||
? route.config.swaggerTransform
|
||||
? route.config.swaggerTransform({ schema: route.schema, url: route.url, route, swaggerObject })
|
||||
: {}
|
||||
: defOpts.transform
|
||||
? defOpts.transform({ schema: route.schema, url: route.url, route, swaggerObject })
|
||||
: {}
|
||||
|
||||
const schema = transformResult.schema || route.schema
|
||||
const shouldRouteHideOpts = {
|
||||
hiddenTag: defOpts.hiddenTag,
|
||||
hideUntagged: defOpts.hideUntagged
|
||||
}
|
||||
|
||||
if (shouldRouteHide(schema, shouldRouteHideOpts)) continue
|
||||
|
||||
let url = transformResult.url || route.url
|
||||
url = normalizeUrl(url, defOpts.basePath, defOpts.stripBasePath)
|
||||
|
||||
const swaggerRoute = Object.assign({}, swaggerObject.paths[url])
|
||||
|
||||
const swaggerMethod = prepareSwaggerMethod(schema, ref, swaggerObject, url)
|
||||
|
||||
if (route.links) {
|
||||
throw new Error('Swagger (Open API v2) does not support Links. Upgrade to OpenAPI v3 (see @fastify/swagger readme)')
|
||||
}
|
||||
|
||||
// route.method should be either a String, like 'POST', or an Array of Strings, like ['POST','PUT','PATCH']
|
||||
const methods = typeof route.method === 'string' ? [route.method] : route.method
|
||||
|
||||
for (const method of methods) {
|
||||
swaggerRoute[method.toLowerCase()] = swaggerMethod
|
||||
}
|
||||
|
||||
swaggerObject.paths[url] = swaggerRoute
|
||||
}
|
||||
|
||||
const transformObjectResult = defOpts.transformObject
|
||||
? defOpts.transformObject({ swaggerObject })
|
||||
: swaggerObject
|
||||
|
||||
if (opts?.yaml) {
|
||||
cache.string = yaml.stringify(transformObjectResult, { strict: false })
|
||||
return cache.string
|
||||
}
|
||||
|
||||
cache.object = transformObjectResult
|
||||
return cache.object
|
||||
}
|
||||
}
|
||||
354
backend/node_modules/@fastify/swagger/lib/spec/swagger/utils.js
generated
vendored
Normal file
354
backend/node_modules/@fastify/swagger/lib/spec/swagger/utils.js
generated
vendored
Normal file
@@ -0,0 +1,354 @@
|
||||
'use strict'
|
||||
|
||||
const { readPackageJson } = require('../../util/read-package-json')
|
||||
const { formatParamUrl } = require('../../util/format-param-url')
|
||||
const { resolveLocalRef } = require('../../util/resolve-local-ref')
|
||||
const { resolveSchemaReference } = require('../../util/resolve-schema-reference')
|
||||
const { xResponseDescription, xConsume } = require('../../constants')
|
||||
const { generateParamsSchema } = require('../../util/generate-params-schema')
|
||||
const { hasParams } = require('../../util/match-params')
|
||||
|
||||
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 paths = swagger.paths || 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 transformObject = opts.transformObject
|
||||
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,
|
||||
paths,
|
||||
basePath,
|
||||
securityDefinitions,
|
||||
security,
|
||||
tags,
|
||||
externalDocs,
|
||||
stripBasePath,
|
||||
transform,
|
||||
transformObject,
|
||||
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.paths) swaggerObject.paths = opts.paths
|
||||
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 'header':
|
||||
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/header 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
|
||||
}
|
||||
|
||||
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 !== undefined) {
|
||||
// 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',
|
||||
description: resolved?.description,
|
||||
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))
|
||||
}
|
||||
|
||||
function findReferenceDescription (rawSchema, ref) {
|
||||
const resolved = resolveSchemaReference(rawSchema, ref)
|
||||
return resolved?.description
|
||||
}
|
||||
|
||||
// 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 ||
|
||||
findReferenceDescription(rawJsonSchema, ref) ||
|
||||
'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, url) {
|
||||
const swaggerMethod = {}
|
||||
const parameters = []
|
||||
|
||||
// Parse out the security prop keys to ignore
|
||||
const securityIgnores = [
|
||||
...(swaggerObject?.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]
|
||||
}
|
||||
}
|
||||
}
|
||||
|
||||
// If there is no schema or schema.params, we need to generate them
|
||||
if ((!schema || !schema.params) && hasParams(url)) {
|
||||
const schemaGenerated = generateParamsSchema(url)
|
||||
resolveCommonParams('path', parameters, schemaGenerated.params, ref, swaggerObject.definitions)
|
||||
swaggerMethod.parameters = parameters
|
||||
}
|
||||
|
||||
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
|
||||
}
|
||||
Reference in New Issue
Block a user