starlight-server 0.0.1

This diff represents the content of publicly available package versions that have been released to one of the supported registries. The information contained in this diff is provided for informational purposes only and reflects changes between package versions as they appear in their respective public registries.
Files changed (84) hide show
  1. package/LICENSE +21 -0
  2. package/README.md +1 -0
  3. package/dist/demo/index.d.ts +1 -0
  4. package/dist/demo/index.js +27 -0
  5. package/dist/http/body/form-data.d.ts +35 -0
  6. package/dist/http/body/form-data.js +141 -0
  7. package/dist/http/body/index.d.ts +23 -0
  8. package/dist/http/body/index.js +47 -0
  9. package/dist/http/body/receive.d.ts +7 -0
  10. package/dist/http/body/receive.js +39 -0
  11. package/dist/http/http-status.d.ts +9 -0
  12. package/dist/http/http-status.js +64 -0
  13. package/dist/http/index.d.ts +9 -0
  14. package/dist/http/index.js +9 -0
  15. package/dist/http/mime-types.d.ts +14 -0
  16. package/dist/http/mime-types.js +764 -0
  17. package/dist/http/request.d.ts +25 -0
  18. package/dist/http/request.js +40 -0
  19. package/dist/http/response.d.ts +32 -0
  20. package/dist/http/response.js +66 -0
  21. package/dist/http/server.d.ts +31 -0
  22. package/dist/http/server.js +52 -0
  23. package/dist/http/types.d.ts +26 -0
  24. package/dist/http/types.js +12 -0
  25. package/dist/index.d.ts +3 -0
  26. package/dist/index.js +4 -0
  27. package/dist/logging.d.ts +24 -0
  28. package/dist/logging.js +30 -0
  29. package/dist/router/cors.d.ts +24 -0
  30. package/dist/router/cors.js +35 -0
  31. package/dist/router/index.d.ts +38 -0
  32. package/dist/router/index.js +36 -0
  33. package/dist/router/match.d.ts +23 -0
  34. package/dist/router/match.js +172 -0
  35. package/dist/router/parameters.d.ts +51 -0
  36. package/dist/router/parameters.js +118 -0
  37. package/dist/router/router.d.ts +127 -0
  38. package/dist/router/router.js +97 -0
  39. package/dist/swagger/index.d.ts +1 -0
  40. package/dist/swagger/index.js +168 -0
  41. package/dist/swagger/openapi-spec.d.ts +261 -0
  42. package/dist/swagger/openapi-spec.js +5 -0
  43. package/dist/validators/array.d.ts +9 -0
  44. package/dist/validators/array.js +28 -0
  45. package/dist/validators/boolean.d.ts +4 -0
  46. package/dist/validators/boolean.js +28 -0
  47. package/dist/validators/common.d.ts +23 -0
  48. package/dist/validators/common.js +25 -0
  49. package/dist/validators/index.d.ts +20 -0
  50. package/dist/validators/index.js +38 -0
  51. package/dist/validators/number.d.ts +10 -0
  52. package/dist/validators/number.js +30 -0
  53. package/dist/validators/object.d.ts +13 -0
  54. package/dist/validators/object.js +36 -0
  55. package/dist/validators/string.d.ts +11 -0
  56. package/dist/validators/string.js +29 -0
  57. package/package.json +54 -0
  58. package/src/demo/index.ts +33 -0
  59. package/src/http/body/form-data.ts +164 -0
  60. package/src/http/body/index.ts +59 -0
  61. package/src/http/body/receive.ts +49 -0
  62. package/src/http/http-status.ts +65 -0
  63. package/src/http/index.ts +9 -0
  64. package/src/http/mime-types.ts +765 -0
  65. package/src/http/request.ts +44 -0
  66. package/src/http/response.ts +73 -0
  67. package/src/http/server.ts +67 -0
  68. package/src/http/types.ts +31 -0
  69. package/src/index.ts +4 -0
  70. package/src/logging.ts +57 -0
  71. package/src/router/cors.ts +54 -0
  72. package/src/router/index.ts +38 -0
  73. package/src/router/match.ts +194 -0
  74. package/src/router/parameters.ts +172 -0
  75. package/src/router/router.ts +233 -0
  76. package/src/swagger/index.ts +184 -0
  77. package/src/swagger/openapi-spec.ts +312 -0
  78. package/src/validators/array.ts +33 -0
  79. package/src/validators/boolean.ts +23 -0
  80. package/src/validators/common.ts +46 -0
  81. package/src/validators/index.ts +50 -0
  82. package/src/validators/number.ts +36 -0
  83. package/src/validators/object.ts +41 -0
  84. package/src/validators/string.ts +38 -0
@@ -0,0 +1,172 @@
1
+ /**
2
+ * 实现通过 query / body parameters 的解析和验证
3
+ */
4
+ import { success } from '@anjianshi/utils'
5
+ import { type Request, HTTPError } from '@/http/index.js'
6
+ import { type Validator, type CommonOptions } from '@/validators/common.js'
7
+ import * as validators from '@/validators/index.js'
8
+ import { type Route } from './router.js'
9
+
10
+ /**
11
+ * ----------------------------------------
12
+ * 类型定义
13
+ * ----------------------------------------
14
+ */
15
+
16
+ /**
17
+ * 请求参数定义
18
+ */
19
+ export interface Parameter {
20
+ /** 字段名 */
21
+ name: string
22
+
23
+ /** 文字描述 */
24
+ describe?: string
25
+
26
+ type: ParameterDataType
27
+
28
+ /** 默认值,应与 type 匹配。若指定,则不再需要 required 规则。 */
29
+ defaults?: unknown
30
+
31
+ /** 是否允许为 undefined */
32
+ required?: boolean
33
+
34
+ /** 是否允许为 null */
35
+ nullable?: boolean
36
+
37
+ /** 验证规则,会传给 type 对应的 validator */
38
+ validate?: Record<string, unknown>
39
+ }
40
+
41
+ export type BasicDataType = 'string' | 'number' | 'boolean'
42
+
43
+ type BasicParameter = Pick<Parameter, 'type' | 'validate' | 'required' | 'nullable' | 'defaults'>
44
+
45
+ /**
46
+ * 请求参数的数据类型定义
47
+ */
48
+ export type ParameterDataType =
49
+ | BasicDataType
50
+ | { array: BasicParameter }
51
+ | { record: BasicParameter } // 对应 object validator 的 value 类型
52
+ | { object: Record<string, BasicParameter> } // 对应 object validator 的 struct 类型
53
+
54
+ /**
55
+ * ----------------------------------------
56
+ * 基础功能实现
57
+ * ----------------------------------------
58
+ */
59
+
60
+ /**
61
+ * 基于 parameter 定义验证、格式化数据
62
+ */
63
+ function validateParameters<T>(parameters: Parameter[], rawData: Record<string, unknown>) {
64
+ const result = {} as Record<string, unknown>
65
+ for (const parameter of parameters) {
66
+ const validator = getValidatorOfParameter(parameter)
67
+ const validateRes = validator.validate(parameter.name, rawData[parameter.name])
68
+ if (validateRes.success) result[parameter.name] = validateRes.data
69
+ else return validateRes
70
+ }
71
+ return success(result as T)
72
+ }
73
+
74
+ /**
75
+ * 获取指定 parameter 的 validator
76
+ */
77
+ function getValidatorOfParameter(parameter: BasicParameter) {
78
+ if (!validatorCache.has(parameter)) {
79
+ const validatorConstructor = getValidatorConstructor(parameter.type) as typeof validators.any
80
+ const options = {
81
+ null: parameter.nullable,
82
+ void: typeof parameter.required === 'boolean' ? parameter.required : undefined,
83
+ defaults: parameter.defaults,
84
+ ...(parameter.validate ?? {}),
85
+ }
86
+ const validator = validatorConstructor(options)
87
+ validatorCache.set(parameter, validator)
88
+ }
89
+ return validatorCache.get(parameter)!
90
+ }
91
+ const validatorCache = new WeakMap<BasicParameter, ReturnType<typeof validators.any>>()
92
+
93
+ /**
94
+ * 取得与参数类型对应的 validator constructor
95
+ */
96
+ function getValidatorConstructor(type: ParameterDataType): (options: CommonOptions) => Validator {
97
+ if (typeof type === 'string') return validators[type]
98
+
99
+ if ('array' in type) {
100
+ const itemValidator = getValidatorOfParameter(type.array)
101
+ return validators.array.bind(null, itemValidator)
102
+ }
103
+
104
+ if ('record' in type) {
105
+ const itemValidator = getValidatorOfParameter(type.record)
106
+ return validators.object.bind(null, itemValidator)
107
+ }
108
+
109
+ const struct: Record<string, Validator> = {}
110
+ for (const [key, innerParameter] of Object.entries(type.object)) {
111
+ struct[key] = getValidatorOfParameter(innerParameter)
112
+ }
113
+ return validators.object.bind(null, struct)
114
+ }
115
+
116
+ /**
117
+ * ----------------------------------------
118
+ * 与请求内容对接
119
+ * ----------------------------------------
120
+ */
121
+
122
+ /**
123
+ * 验证 query 内容
124
+ */
125
+ export function parseQuery<T>(route: Route, request: Request) {
126
+ if (!route.query) throw new HTTPError(500, 'Call parseQuery() need query parameter definition.')
127
+
128
+ const values: Record<string, unknown> = {}
129
+ for (const parameter of route.query) {
130
+ values[parameter.name] = getQueryParameterValue(request, parameter)
131
+ }
132
+
133
+ const res = validateParameters<T>(route.query, values)
134
+ if (res.success) return res.data
135
+ throw new HTTPError(400, res.error)
136
+ }
137
+
138
+ function getQueryParameterValue(request: Request, parameter: Parameter) {
139
+ // array
140
+ if (typeof parameter.type === 'object' && 'array' in parameter.type) {
141
+ return request.query.getAll(parameter.name + '[]')
142
+ }
143
+
144
+ // undefined
145
+ const raw = request.query.get(parameter.name)
146
+ if (raw === null || raw === '') return undefined
147
+
148
+ // basic type
149
+ if (typeof parameter.type === 'string') return raw
150
+
151
+ // object
152
+ try {
153
+ return JSON.parse(raw) as unknown
154
+ } catch (error) {
155
+ throw new HTTPError(400, `Invalid JSON value for query parameter "${parameter.name}".`)
156
+ }
157
+ }
158
+
159
+ /**
160
+ * 验证 body 内容
161
+ */
162
+ export async function parseJSONBody<T>(route: Route, request: Request) {
163
+ if (!route.body) throw new HTTPError(500, 'call parseJSONBody() need body parameter definition.')
164
+
165
+ const body = await request.body.json()
166
+ if (typeof body !== 'object' || body === null || Array.isArray(body))
167
+ throw new HTTPError(400, 'Invalid JSON body, should be an object.')
168
+
169
+ const res = validateParameters<T>(route.body, body as Record<string, unknown>)
170
+ if (res.success) return res.data
171
+ throw new HTTPError(400, res.error)
172
+ }
@@ -0,0 +1,233 @@
1
+ import { type RequiredFields } from '@anjianshi/utils'
2
+ import { type Request, type ResponseUtils, HTTPError } from '@/http/index.js'
3
+ import { getMethodFromCORSPreflight, outputCORSHeaders, type CORSOptions } from './cors.js'
4
+ import { matchRoutes, type PathParameters, type RouteMatch } from './match.js'
5
+ import { parseQuery, parseJSONBody, type BasicDataType, type Parameter } from './parameters.js'
6
+
7
+ /**
8
+ * -----------------------------------------
9
+ * 类型定义
10
+ * -----------------------------------------
11
+ */
12
+
13
+ /**
14
+ * 请求上下文(由 Router 生成并传给请求处理函数)
15
+ */
16
+ export interface BaseContext {
17
+ request: Request
18
+ response: ResponseUtils
19
+ parseQuery: <T>() => T
20
+ parseBody: <T>() => Promise<T>
21
+ }
22
+
23
+ /**
24
+ * 请求处理函数
25
+ */
26
+ export type RouteHandler<
27
+ Ctx extends BaseContext = BaseContext,
28
+ PathP extends PathParameters = PathParameters,
29
+ > = (context: Ctx, pathParameters: PathP) => unknown
30
+
31
+ /**
32
+ * 路由定义
33
+ *
34
+ * 相比 RawRoute:
35
+ * - 有默认值的字段均填充了默认值
36
+ * - method 一定为全大写
37
+ */
38
+ export type Route<
39
+ Ctx extends BaseContext = BaseContext,
40
+ PathP extends PathParameters = PathParameters,
41
+ > = RequiredFields<RawRoute<Ctx, PathP>, 'describe' | 'category' | 'method'>
42
+
43
+ export interface RawRoute<
44
+ Ctx extends BaseContext = BaseContext,
45
+ PathP extends PathParameters = PathParameters,
46
+ > {
47
+ /** 接口路径。支持变量(/abc/:xxx/def),详见 src/router/match.ts */
48
+ path: string
49
+
50
+ /** 接口描述 */
51
+ describe?: string
52
+
53
+ /** 接口类别 */
54
+ category?: string
55
+
56
+ /** 接口 HTTP Method,有 body 定义的默认为 POST,否则默认为 GET */
57
+ method?: string
58
+
59
+ /** Query String 定义 */
60
+ query?: Parameter[]
61
+
62
+ /** JSON Request Body 定义 */
63
+ body?: Parameter[]
64
+
65
+ /**
66
+ * JSON 返回内容定义(返回的不是 JSON 时(如纯文本、二进制文件),不定义此项)
67
+ * - 若指定的是 ResponseDataType[],代表有多种可能返回的类型。例如 ['string' | 'null'] 可能返回字符串也可能返回 null
68
+ */
69
+ response?: ResponseDataType | ResponseDataType[]
70
+
71
+ /**
72
+ * 指定此接口的 CORS 配置
73
+ * 不指定则由 router.getCORSOptions() 提供
74
+ */
75
+ cors?: CORSOptions
76
+
77
+ handler: RouteHandler<Ctx, PathP>
78
+ }
79
+
80
+ /**
81
+ * JSON 响应内容的类型定义
82
+ * { array: ResponseDataType[] } 代表数组元素有多种可能的类型
83
+ */
84
+ export type ResponseDataType =
85
+ | BasicDataType
86
+ | 'null'
87
+ | { array: ResponseDataType | ResponseDataType[] }
88
+ | { object: ResponseObjectItemType[] }
89
+ | { ref: string; summary?: string; description?: string } // 引用预注册了的类型,以重用类型定义
90
+
91
+ /**
92
+ * JSON object 相应内容的字段定义
93
+ */
94
+ export interface ResponseObjectItemType {
95
+ /** 字段名 */
96
+ name: string
97
+
98
+ /** 文字描述 */
99
+ describe?: string
100
+
101
+ /** 值类型。ResponseDataType[] 代表有多种可能的类型 */
102
+ type: ResponseDataType | ResponseDataType[]
103
+ }
104
+
105
+ /**
106
+ * -----------------------------------------
107
+ * Router 类
108
+ * -----------------------------------------
109
+ */
110
+
111
+ export abstract class Router<Ctx extends BaseContext = BaseContext> {
112
+ /**
113
+ * 路由列表
114
+ */
115
+ readonly routes: Route<Ctx>[] = []
116
+
117
+ /**
118
+ * 注册路由
119
+ */
120
+ register<P = Record<string, never>>(
121
+ method: string,
122
+ path: string,
123
+ handler: RouteHandler<Ctx, PathParameters & P>,
124
+ ): void
125
+ register<P extends Record<string, string>>(raw: RawRoute<Ctx, PathParameters & P>): void
126
+ register<P extends PathParameters = PathParameters>(
127
+ raw: RawRoute<Ctx, PathParameters & P> | string,
128
+ path?: string,
129
+ handler?: RouteHandler<Ctx, PathParameters & P>,
130
+ ) {
131
+ const rawRoute: RawRoute<Ctx, PathParameters & P> =
132
+ typeof raw === 'string' ? { method: raw, path: path!, handler: handler! } : raw
133
+ const route: Route<Ctx, PathParameters & P> = {
134
+ describe: '',
135
+ category: '',
136
+ ...rawRoute,
137
+ method: (rawRoute.method ?? (rawRoute.body ? 'POST' : 'GET')).toUpperCase(),
138
+ }
139
+ this.routes.push(route as Route<Ctx>)
140
+ }
141
+
142
+ /**
143
+ * 响应请求
144
+ */
145
+ readonly handle = async (request: Request, response: ResponseUtils) => {
146
+ const matched = this.match(request, response)
147
+ if (matched === null) return
148
+
149
+ const baseContext = getBaseContext(matched.route as Route, request, response)
150
+ await this.executeWithContext(baseContext, matched.route, matched.pathParameters)
151
+ }
152
+
153
+ /**
154
+ * 匹配路由,成功返回路由对象,失败抛出 HTTPError,提前结束处理返回 null
155
+ */
156
+ protected match(request: Request, response: ResponseUtils) {
157
+ const matches = matchRoutes(request.path, this.routes)
158
+ if (!matches.length) throw new HTTPError(404)
159
+
160
+ const methodFromPreflight = getMethodFromCORSPreflight(request)
161
+ const isPreflight = methodFromPreflight !== null
162
+
163
+ const method = methodFromPreflight ?? request.method
164
+ const methodMatched = matches.find(match => match.route.method === method)
165
+ if (!methodMatched) throw new HTTPError(405)
166
+
167
+ const corsOptions = methodMatched.route.cors ?? this.getCORSOptions(request, methodMatched)
168
+ outputCORSHeaders(response, corsOptions, isPreflight)
169
+ if (isPreflight) {
170
+ response.nodeResponse.end('')
171
+ return null
172
+ }
173
+
174
+ return methodMatched
175
+ }
176
+
177
+ /**
178
+ * 为未指定 CORS 配置的 route 提供默认配置
179
+ */
180
+ // eslint-disable-next-line @typescript-eslint/no-unused-vars
181
+ getCORSOptions(request: Request, routeMatch: RouteMatch<Route<Ctx>>) {
182
+ return false
183
+ }
184
+
185
+ /**
186
+ * 完善 context 对象并执行 route
187
+ * 注意:handler 在很多时候都是异步的,要用 await 等待执行完成
188
+ */
189
+ protected abstract executeWithContext(
190
+ baseContext: BaseContext,
191
+ route: Route<Ctx>,
192
+ pathParameters: PathParameters,
193
+ ): void | Promise<void>
194
+
195
+ /**
196
+ * response 定义中可引用的数据类型
197
+ */
198
+ readonly responseReferences: Record<string, ResponseDataType> = {}
199
+
200
+ /**
201
+ * 注册 response 定义中引用的数据类型
202
+ */
203
+ registerResponseReference(id: string, type: ResponseDataType) {
204
+ this.responseReferences[id] = type
205
+ }
206
+ }
207
+
208
+ /**
209
+ * ------------------------
210
+ * 默认实现
211
+ * ------------------------
212
+ */
213
+
214
+ /**
215
+ * 基础 Context 实现
216
+ */
217
+ function getBaseContext(route: Route, request: Request, response: ResponseUtils) {
218
+ return {
219
+ request,
220
+ response,
221
+ parseQuery: <T>() => parseQuery<T>(route, request),
222
+ parseBody: async <T>() => parseJSONBody<T>(route, request),
223
+ }
224
+ }
225
+
226
+ /**
227
+ * 默认的 Router 实现
228
+ */
229
+ export class DefaultRouter extends Router {
230
+ async executeWithContext(baseContext: BaseContext, route: Route, pathParameters: PathParameters) {
231
+ await route.handler(baseContext, pathParameters)
232
+ }
233
+ }
@@ -0,0 +1,184 @@
1
+ // import fsPromise from 'node:fs/promises'
2
+ // import path from 'node:path'
3
+ // import { getAbsoluteFSPath } from 'swagger-ui-dist'
4
+ // import { HTTPError } from '@/http/index.js'
5
+ // import {
6
+ // type Router,
7
+ // type PathParameters,
8
+ // type BaseContext,
9
+ // type ResponseDataType,
10
+ // } from '@/router/index.js'
11
+ // import { normalizePath } from '@/router/match.js'
12
+ // import type { OpenAPI, Schema, PathItem } from './openapi-spec.js'
13
+
14
+ // export function registerSwaggerRoute(
15
+ // router: Router,
16
+ // endpoint: string = '/swagger',
17
+ // customFields: OpenAPICustomFields = { info: { title: 'API Document', version: '0.0.1' } },
18
+ // ) {
19
+ // router.register('GET', normalizePath(endpoint) + '/*', pageRoute)
20
+ // router.register(
21
+ // 'GET',
22
+ // normalizePath(endpoint) + '/api-swagger.json',
23
+ // apiRoute.bind(null, router, customFields),
24
+ // )
25
+ // }
26
+
27
+ // /**
28
+ // * ---------------------------------
29
+ // * swagger 页面路由
30
+ // * ---------------------------------
31
+ // */
32
+ // async function pageRoute({ response }: BaseContext, pathParameters: PathParameters) {
33
+ // const file = (pathParameters['*'] ?? '') || 'index.html'
34
+
35
+ // const swaggerDir = getAbsoluteFSPath()
36
+ // const abspath = path.join(swaggerDir, file)
37
+ // if (!cache.has(abspath)) {
38
+ // try {
39
+ // const stat = await fsPromise.stat(abspath)
40
+ // if (!stat.isFile()) throw new HTTPError(404)
41
+ // } catch (e) {
42
+ // throw new HTTPError(404)
43
+ // }
44
+ // const content = await fsPromise.readFile(abspath)
45
+ // const replacemented = replacement(abspath, content)
46
+ // cache.set(abspath, replacemented)
47
+ // }
48
+ // response.text(cache.get(abspath)!)
49
+ // }
50
+
51
+ // const cache = new Map<string, Buffer | string>()
52
+
53
+ // function replacement(abspath: string, content: Buffer) {
54
+ // if (/swagger-initializer.js/.exec(abspath)) {
55
+ // return content
56
+ // .toString()
57
+ // .replace('https://petstore.swagger.io/v2/swagger.json', './api-swagger.json')
58
+ // // .replace('SwaggerUIBundle({', 'SwaggerUIBundle({\n defaultModelsExpandDepth: 1,\n defaultModelExpandDepth: 1,')
59
+ // }
60
+ // return content
61
+ // }
62
+
63
+ // /**
64
+ // * ---------------------------------
65
+ // * swagger API 路由
66
+ // * ---------------------------------
67
+ // */
68
+ // type OpenAPICustomFields = Omit<OpenAPI, 'openapi' | 'paths' | 'components'>
69
+
70
+ // function apiRoute(router: Router, customFields: OpenAPICustomFields, { response }: BaseContext) {
71
+ // const swaggerJSON: OpenAPI = {
72
+ // ...customFields,
73
+ // openapi: '3.1.0',
74
+ // paths: makePaths(router),
75
+ // components: {
76
+ // schemas: makeRefs(router),
77
+ // },
78
+ // }
79
+ // response.json(swaggerJSON)
80
+ // }
81
+
82
+ // function makeRefs(router: Router) {
83
+ // const schemas: Record<string, Schema> = {}
84
+ // for (const [id, type] of Object.entries(router.responseReferences)) {
85
+ // schemas[id] = responseDataType2Schema(type)
86
+ // }
87
+ // return schemas
88
+ // }
89
+
90
+ // function makePaths(router: Router) {
91
+ // const paths: Record<string, PathItem> = {}
92
+ // return paths
93
+ // }
94
+
95
+ // function responseDataType2Schema(type: ResponseDataType): Schema {
96
+ // if (type === 'null') return { nullable: true }
97
+ // if (typeof type === 'object' && 'array' in type && type.array.includes('null')) {
98
+ // return {
99
+ // nullable: true,
100
+ // ...responseDataType2Schema(type as ResponseDataType.filter(v => v !== 'null') as ResponseDataType),
101
+ // }
102
+ // }
103
+
104
+ // const nullable = type === 'null' || (Array.isArray(type) && type.includes('null'))
105
+ // return {
106
+ // nullable,
107
+ // }
108
+ // }
109
+
110
+ // type Obj = { [k: string]: unknown }
111
+ // function string(desc?: string, options?: Obj) {
112
+ // return { type: 'string', description: desc, ...(options ?? {}) }
113
+ // }
114
+ // function number(desc?: string, options?: Obj) {
115
+ // return { type: 'number', description: desc, ...(options ?? {}) }
116
+ // }
117
+ // function boolean(desc?: string, options?: Obj) {
118
+ // return { type: 'boolean', description: desc, ...(options ?? {}) }
119
+ // }
120
+ // function object(properties: unknown, extra?: Obj) {
121
+ // return { type: 'object', properties, ...(extra ?? {}) }
122
+ // }
123
+ // function array(items: unknown, extra?: Obj) {
124
+ // return { type: 'array', items, ...(extra ?? {}) }
125
+ // }
126
+
127
+ // function ref(name: string) {
128
+ // return { $ref: '#/components/schemas/' + name }
129
+ // }
130
+
131
+ // function jsonSchema(schema: string | Obj) {
132
+ // return {
133
+ // content: {
134
+ // 'application/json': {
135
+ // schema: typeof schema === 'string' ? ref(schema) : schema,
136
+ // },
137
+ // },
138
+ // }
139
+ // }
140
+
141
+ // function route(params: {
142
+ // category: string
143
+ // describe: string
144
+ // path: string
145
+ // body?: string | Obj
146
+ // response?: string | Obj
147
+ // }) {
148
+ // return {
149
+ // [params.path]: {
150
+ // post: {
151
+ // tags: [params.category],
152
+ // summary: params.describe,
153
+ // operationId: params.path,
154
+ // requestBody: params.body !== undefined ? jsonSchema(params.body) : undefined,
155
+ // responses: {
156
+ // default: {
157
+ // description: 'OK',
158
+ // ...jsonSchema(params.response ?? object({})),
159
+ // },
160
+ // },
161
+ // },
162
+ // },
163
+ // }
164
+ // }
165
+
166
+ // function response(schema: unknown) {
167
+ // return {
168
+ // type: 'object',
169
+ // properties: {
170
+ // success: boolean(undefined, { enum: [true] }),
171
+ // data: schema,
172
+ // },
173
+ // }
174
+ // }
175
+
176
+ // function pageResp(itemSchema: unknown) {
177
+ // return response({
178
+ // type: 'object',
179
+ // properties: {
180
+ // total: number(),
181
+ // list: array(itemSchema),
182
+ // },
183
+ // })
184
+ // }