| 1 | import type { Type } from '@nestjs/common' |
| 2 | import type { ReferenceObject, SchemaObject } from '@nestjs/swagger/dist/interfaces/open-api-spec.interface' |
| 3 | import type { ZodType } from 'zod' |
| 4 | import { applyDecorators } from '@nestjs/common' |
| 5 | import { ApiBody, ApiExtraModels, ApiOperation, ApiQuery, ApiResponse } from '@nestjs/swagger' |
| 6 | import { z } from 'zod' |
| 7 | import { zodToJsonSchemaOptions } from '../utils' |
| 8 | |
| 9 | export interface ApiDocOptions { |
| 10 | /** |
| 11 | * 接口摘要 |
| 12 | */ |
| 13 | summary: string |
| 14 | |
| 15 | /** |
| 16 | * 接口详细描述 |
| 17 | */ |
| 18 | description?: string |
| 19 | |
| 20 | /** |
| 21 | * 请求体 DTO Schema(可选) |
| 22 | */ |
| 23 | body?: ZodType |
| 24 | |
| 25 | /** |
| 26 | * 请求参数 DTO Schema(可选) |
| 27 | */ |
| 28 | query?: ZodType |
| 29 | |
| 30 | /** |
| 31 | * 响应 VO 类型 |
| 32 | */ |
| 33 | response?: Type | [Type] | ZodType |
| 34 | } |
| 35 | |
| 36 | /** |
| 37 | * Swagger 文档装饰器 |
| 38 | * 用于统一生成接口文档,支持分页响应和请求体验证 |
| 39 | * |
| 40 | * @param options 装饰器选项 |
| 41 | */ |
| 42 | export function ApiDoc(options: ApiDocOptions) { |
| 43 | const { |
| 44 | summary, |
| 45 | description, |
| 46 | body, |
| 47 | query, |
| 48 | response, |
| 49 | } = options |
| 50 | |
| 51 | const responseType = Array.isArray(response) ? response[0] : response |
| 52 | |
| 53 | const decorators: MethodDecorator[] = [ |
| 54 | ApiOperation({ |
| 55 | summary, |
| 56 | description, |
| 57 | }), |
| 58 | ] |
| 59 | |
| 60 | if (responseType && typeof responseType === 'function') { |
| 61 | decorators.push(ApiExtraModels(responseType)) |
| 62 | } |
| 63 | |
| 64 | if (body) { |
| 65 | const meta = z.globalRegistry.get(body) |
| 66 | let schemaObject: SchemaObject | ReferenceObject |
| 67 | if (meta && meta.id) { |
| 68 | schemaObject = { |
| 69 | $ref: `#/components/schemas/${meta.id}`, |
| 70 | } |
| 71 | } |
| 72 | else { |
| 73 | schemaObject = z.toJSONSchema(body, { ...zodToJsonSchemaOptions, io: 'input' }) as SchemaObject |
| 74 | } |
| 75 | decorators.push( |
| 76 | ApiBody({ |
| 77 | schema: schemaObject, |
| 78 | }), |
| 79 | ) |
| 80 | } |
| 81 | if (query) { |
| 82 | const meta = z.globalRegistry.get(query) |
| 83 | let schemaObject: SchemaObject | ReferenceObject |
| 84 | if (meta && meta.id) { |
| 85 | schemaObject = { |
| 86 | $ref: `#/components/schemas/${meta.id}`, |
| 87 | } |
| 88 | } |
| 89 | else { |
| 90 | schemaObject = z.toJSONSchema(query, { ...zodToJsonSchemaOptions, io: 'input' }) as SchemaObject |
| 91 | } |
| 92 | decorators.push( |
| 93 | ApiQuery({ |
| 94 | schema: schemaObject, |
| 95 | }), |
| 96 | ) |
| 97 | } |
| 98 | |
| 99 | let dataSchema: SchemaObject | ReferenceObject | undefined |
| 100 | if (responseType) { |
| 101 | if (typeof responseType === 'function') { |
| 102 | dataSchema = Array.isArray(response) |
| 103 | ? { |
| 104 | type: 'array', |
| 105 | items: { |
| 106 | $ref: `#/components/schemas/${responseType.name}`, |
| 107 | }, |
| 108 | } |
| 109 | : { |
| 110 | $ref: `#/components/schemas/${responseType.name}`, |
| 111 | } |
| 112 | } |
| 113 | else { |
| 114 | const meta = z.globalRegistry.get(responseType) |
| 115 | let schemaObject: SchemaObject | ReferenceObject |
| 116 | if (meta && meta.id) { |
| 117 | schemaObject = { |
| 118 | $ref: `#/components/schemas/${meta.id}`, |
| 119 | } |
| 120 | } |
| 121 | else { |
| 122 | schemaObject = z.toJSONSchema(responseType, { ...zodToJsonSchemaOptions, io: 'output' }) as SchemaObject |
| 123 | } |
| 124 | dataSchema = schemaObject |
| 125 | } |
| 126 | } |
| 127 | |
| 128 | decorators.push( |
| 129 | ApiResponse({ |
| 130 | status: 'default', |
| 131 | schema: { |
| 132 | type: 'object', |
| 133 | properties: { |
| 134 | ...(dataSchema ? { data: dataSchema } : {}), |
| 135 | code: { |
| 136 | type: 'number', |
| 137 | description: '错误码', |
| 138 | }, |
| 139 | message: { |
| 140 | type: 'string', |
| 141 | description: '错误消息', |
| 142 | }, |
| 143 | requestId: { |
| 144 | type: 'string', |
| 145 | description: '请求 ID', |
| 146 | }, |
| 147 | }, |
| 148 | required: ['data', 'code', 'message'], |
| 149 | }, |
| 150 | }), |
| 151 | ) |
| 152 | |
| 153 | return applyDecorators(...decorators) |
| 154 | } |
| 155 |