Files
elysia-swagger/src/utils.ts
2024-07-15 17:38:36 +07:00

360 lines
7.6 KiB
TypeScript

/* eslint-disable @typescript-eslint/ban-ts-comment */
/* eslint-disable @typescript-eslint/no-unused-vars */
import type { HTTPMethod, LocalHook } from 'elysia'
import { Kind, type TSchema } from '@sinclair/typebox'
import type { OpenAPIV3 } from 'openapi-types'
import deepClone from 'lodash.clonedeep'
export const toOpenAPIPath = (path: string) =>
path
.split('/')
.map((x) => {
if (x.startsWith(':')) {
x = x.slice(1, x.length)
if (x.endsWith('?')) x = x.slice(0, -1)
x = `{${x}}`
}
return x
})
.join('/')
export const mapProperties = (
name: string,
schema: TSchema | string | undefined,
models: Record<string, TSchema>
) => {
if (schema === undefined) return []
if (typeof schema === 'string')
if (schema in models) schema = models[schema]
else throw new Error(`Can't find model ${schema}`)
return Object.entries(schema?.properties ?? []).map(([key, value]) => {
const {
type: valueType = undefined,
description,
examples,
...schemaKeywords
} = value as any
return {
// @ts-ignore
description,
examples,
schema: { type: valueType, ...schemaKeywords },
in: name,
name: key,
// @ts-ignore
required: schema!.required?.includes(key) ?? false
}
})
}
const mapTypesResponse = (
types: string[],
schema:
| string
| {
type: string
properties: Object
required: string[]
}
) => {
if (
typeof schema === 'object' &&
['void', 'undefined', 'null'].includes(schema.type)
)
return
const responses: Record<string, OpenAPIV3.MediaTypeObject> = {}
for (const type of types)
responses[type] = {
schema:
typeof schema === 'string'
? {
$ref: `#/components/schemas/${schema}`
}
: { ...(schema as any) }
}
return responses
}
export const capitalize = (word: string) =>
word.charAt(0).toUpperCase() + word.slice(1)
export const generateOperationId = (method: string, paths: string) => {
let operationId = method.toLowerCase()
if (paths === '/') return operationId + 'Index'
for (const path of paths.split('/')) {
if (path.charCodeAt(0) === 123) {
operationId += 'By' + capitalize(path.slice(1, -1))
} else {
operationId += capitalize(path)
}
}
return operationId
}
export const registerSchemaPath = ({
schema,
path,
method,
hook,
models
}: {
schema: Partial<OpenAPIV3.PathsObject>
contentType?: string | string[]
path: string
method: HTTPMethod
hook?: LocalHook<any, any, any, any, any, any, any>
models: Record<string, TSchema>
}) => {
if (hook) hook = deepClone(hook)
const contentType = hook?.type ?? [
'application/json',
'multipart/form-data',
'text/plain'
]
path = toOpenAPIPath(path)
const contentTypes =
typeof contentType === 'string'
? [contentType]
: contentType ?? ['application/json']
const bodySchema = hook?.body
const paramsSchema = hook?.params
const headerSchema = hook?.headers
const querySchema = hook?.query
let responseSchema = hook?.response as unknown as OpenAPIV3.ResponsesObject
if (typeof responseSchema === 'object') {
if (Kind in responseSchema) {
const {
type,
properties,
required,
additionalProperties,
patternProperties,
...rest
} = responseSchema as typeof responseSchema & {
type: string
properties: Object
required: string[]
}
responseSchema = {
'200': {
...rest,
description: rest.description as any,
content: mapTypesResponse(
contentTypes,
type === 'object' || type === 'array'
? ({
type,
properties,
patternProperties,
items: responseSchema.items,
required
} as any)
: responseSchema
)
}
}
} else {
Object.entries(responseSchema as Record<string, TSchema>).forEach(
([key, value]) => {
if (typeof value === 'string') {
if (!models[value]) return
// eslint-disable-next-line @typescript-eslint/no-unused-vars
const {
type,
properties,
required,
additionalProperties: _1,
patternProperties: _2,
...rest
} = models[value] as TSchema & {
type: string
properties: Object
required: string[]
}
responseSchema[key] = {
...rest,
description: rest.description as any,
content: mapTypesResponse(contentTypes, value)
}
} else {
const {
type,
properties,
required,
additionalProperties,
patternProperties,
...rest
} = value as typeof value & {
type: string
properties: Object
required: string[]
}
responseSchema[key] = {
...rest,
description: rest.description as any,
content: mapTypesResponse(
contentTypes,
type === 'object' || type === 'array'
? ({
type,
properties,
patternProperties,
items: value.items,
required
} as any)
: value
)
}
}
}
)
}
} else if (typeof responseSchema === 'string') {
if (!(responseSchema in models)) return
// eslint-disable-next-line @typescript-eslint/no-unused-vars
const {
type,
properties,
required,
additionalProperties: _1,
patternProperties: _2,
...rest
} = models[responseSchema] as TSchema & {
type: string
properties: Object
required: string[]
}
responseSchema = {
// @ts-ignore
'200': {
...rest,
content: mapTypesResponse(contentTypes, responseSchema)
}
}
}
const parameters = [
...mapProperties('header', headerSchema, models),
...mapProperties('path', paramsSchema, models),
...mapProperties('query', querySchema, models)
]
schema[path] = {
...(schema[path] ? schema[path] : {}),
[method.toLowerCase()]: {
...((headerSchema || paramsSchema || querySchema || bodySchema
? ({ parameters } as any)
: {}) satisfies OpenAPIV3.ParameterObject),
...(responseSchema
? {
responses: responseSchema
}
: {}),
operationId:
hook?.detail?.operationId ?? generateOperationId(method, path),
...hook?.detail,
...(bodySchema
? {
requestBody: {
required: true,
content: mapTypesResponse(
contentTypes,
typeof bodySchema === 'string'
? {
$ref: `#/components/schemas/${bodySchema}`
}
: (bodySchema as any)
)
}
}
: null)
} satisfies OpenAPIV3.OperationObject
}
}
export const filterPaths = (
paths: Record<string, any>,
{
excludeStaticFile = true,
exclude = []
}: {
excludeStaticFile: boolean
exclude: (string | RegExp)[]
}
) => {
const newPaths: Record<string, any> = {}
for (const [key, value] of Object.entries(paths))
if (
!exclude.some((x) => {
if (typeof x === 'string') return key === x
return x.test(key)
}) &&
!key.includes('/swagger') &&
!key.includes('*') &&
(excludeStaticFile ? !key.includes('.') : true)
) {
Object.keys(value).forEach((method) => {
const schema = value[method]
if (key.includes('{')) {
if (!schema.parameters) schema.parameters = []
schema.parameters = [
...key
.split('/')
.filter(
(x) =>
x.startsWith('{') &&
!schema.parameters.find(
(params: Record<string, any>) =>
params.in === 'path' &&
params.name ===
x.slice(1, x.length - 1)
)
)
.map((x) => ({
schema: { type: 'string' },
in: 'path',
name: x.slice(1, x.length - 1),
required: true
})),
...schema.parameters
]
}
if (!schema.responses)
schema.responses = {
200: {}
}
})
newPaths[key] = value
}
return newPaths
}