routing-controllers-openapi
Version:
Runtime OpenAPI v3 spec generation for routing-controllers
154 lines (136 loc) • 4.68 kB
text/typescript
import _merge from 'lodash.merge'
import { OperationObject, ReferenceObject, ResponsesObject } from 'openapi3-ts'
import 'reflect-metadata'
import { getContentType, getStatusCode, IRoute } from './index'
const OPEN_API_KEY = Symbol('routing-controllers-openapi:OpenAPI')
export type OpenAPIParam =
| Partial<OperationObject>
| ((source: OperationObject, route: IRoute) => OperationObject)
/**
* Supplement action with additional OpenAPI Operation keywords.
*
* @param spec OpenAPI Operation object that is merged into the schema derived
* from routing-controllers decorators. In case of conflicts, keywords defined
* here overwrite the existing ones. Alternatively you can supply a function
* that receives as parameters the existing Operation and target route,
* returning an updated Operation.
*/
export function OpenAPI(spec: OpenAPIParam) {
// tslint:disable-next-line:ban-types
return (...args: [Function] | [object, string, PropertyDescriptor]) => {
if (args.length === 1) {
const [target] = args
const currentMeta = getOpenAPIMetadata(target)
setOpenAPIMetadata([spec, ...currentMeta], target)
} else {
const [target, key] = args
const currentMeta = getOpenAPIMetadata(target, key)
setOpenAPIMetadata([spec, ...currentMeta], target, key)
}
}
}
/**
* Apply the keywords defined in @OpenAPI decorator to its target route.
*/
export function applyOpenAPIDecorator(
originalOperation: OperationObject,
route: IRoute
): OperationObject {
const { action } = route
const openAPIParams = [
...getOpenAPIMetadata(action.target),
...getOpenAPIMetadata(action.target.prototype, action.method),
]
return openAPIParams.reduce((acc: OperationObject, oaParam: OpenAPIParam) => {
return typeof oaParam === 'function'
? oaParam(acc, route)
: _merge({}, acc, oaParam)
}, originalOperation) as OperationObject
}
/**
* Get the OpenAPI Operation object stored in given target property's metadata.
*/
export function getOpenAPIMetadata(
target: object,
key?: string
): OpenAPIParam[] {
return (
(key
? Reflect.getMetadata(OPEN_API_KEY, target.constructor, key)
: Reflect.getMetadata(OPEN_API_KEY, target)) || []
)
}
/**
* Store given OpenAPI Operation object into target property's metadata.
*/
export function setOpenAPIMetadata(
value: OpenAPIParam[],
target: object,
key?: string
) {
return key
? Reflect.defineMetadata(OPEN_API_KEY, value, target.constructor, key)
: Reflect.defineMetadata(OPEN_API_KEY, value, target)
}
/**
* Supplement action with response body type annotation.
*/
export function ResponseSchema(
responseClass: Function | string, // tslint:disable-line
options: {
contentType?: string
description?: string
statusCode?: string | number
isArray?: boolean
} = {}
) {
const setResponseSchema = (source: OperationObject, route: IRoute) => {
const contentType = options.contentType || getContentType(route)
const description = options.description || ''
const isArray = options.isArray || false
const statusCode = (options.statusCode || getStatusCode(route)) + ''
let responseSchemaName = ''
if (typeof responseClass === 'function' && responseClass.name) {
responseSchemaName = responseClass.name
} else if (typeof responseClass === 'string') {
responseSchemaName = responseClass
}
if (responseSchemaName) {
const reference: ReferenceObject = {
$ref: `#/components/schemas/${responseSchemaName}`,
}
const schema = isArray ? { items: reference, type: 'array' } : reference
const responses: ResponsesObject = {
[statusCode]: {
content: {
[contentType]: {
schema,
},
},
description,
},
}
const oldSchema =
source.responses[statusCode]?.content[contentType].schema
if (oldSchema?.$ref || oldSchema?.items || oldSchema?.oneOf) {
// case where we're adding multiple schemas under single statuscode/contentType
const newStatusCodeResponse = _merge(
{},
source.responses[statusCode],
responses[statusCode]
)
const newSchema = oldSchema.oneOf
? {
oneOf: [...oldSchema.oneOf, schema],
}
: { oneOf: [oldSchema, schema] }
newStatusCodeResponse.content[contentType].schema = newSchema
source.responses[statusCode] = newStatusCodeResponse
return source
}
return _merge({}, source, { responses })
}
return source
}
return OpenAPI(setResponseSchema)
}