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.
- package/LICENSE +21 -0
- package/README.md +1 -0
- package/dist/demo/index.d.ts +1 -0
- package/dist/demo/index.js +27 -0
- package/dist/http/body/form-data.d.ts +35 -0
- package/dist/http/body/form-data.js +141 -0
- package/dist/http/body/index.d.ts +23 -0
- package/dist/http/body/index.js +47 -0
- package/dist/http/body/receive.d.ts +7 -0
- package/dist/http/body/receive.js +39 -0
- package/dist/http/http-status.d.ts +9 -0
- package/dist/http/http-status.js +64 -0
- package/dist/http/index.d.ts +9 -0
- package/dist/http/index.js +9 -0
- package/dist/http/mime-types.d.ts +14 -0
- package/dist/http/mime-types.js +764 -0
- package/dist/http/request.d.ts +25 -0
- package/dist/http/request.js +40 -0
- package/dist/http/response.d.ts +32 -0
- package/dist/http/response.js +66 -0
- package/dist/http/server.d.ts +31 -0
- package/dist/http/server.js +52 -0
- package/dist/http/types.d.ts +26 -0
- package/dist/http/types.js +12 -0
- package/dist/index.d.ts +3 -0
- package/dist/index.js +4 -0
- package/dist/logging.d.ts +24 -0
- package/dist/logging.js +30 -0
- package/dist/router/cors.d.ts +24 -0
- package/dist/router/cors.js +35 -0
- package/dist/router/index.d.ts +38 -0
- package/dist/router/index.js +36 -0
- package/dist/router/match.d.ts +23 -0
- package/dist/router/match.js +172 -0
- package/dist/router/parameters.d.ts +51 -0
- package/dist/router/parameters.js +118 -0
- package/dist/router/router.d.ts +127 -0
- package/dist/router/router.js +97 -0
- package/dist/swagger/index.d.ts +1 -0
- package/dist/swagger/index.js +168 -0
- package/dist/swagger/openapi-spec.d.ts +261 -0
- package/dist/swagger/openapi-spec.js +5 -0
- package/dist/validators/array.d.ts +9 -0
- package/dist/validators/array.js +28 -0
- package/dist/validators/boolean.d.ts +4 -0
- package/dist/validators/boolean.js +28 -0
- package/dist/validators/common.d.ts +23 -0
- package/dist/validators/common.js +25 -0
- package/dist/validators/index.d.ts +20 -0
- package/dist/validators/index.js +38 -0
- package/dist/validators/number.d.ts +10 -0
- package/dist/validators/number.js +30 -0
- package/dist/validators/object.d.ts +13 -0
- package/dist/validators/object.js +36 -0
- package/dist/validators/string.d.ts +11 -0
- package/dist/validators/string.js +29 -0
- package/package.json +54 -0
- package/src/demo/index.ts +33 -0
- package/src/http/body/form-data.ts +164 -0
- package/src/http/body/index.ts +59 -0
- package/src/http/body/receive.ts +49 -0
- package/src/http/http-status.ts +65 -0
- package/src/http/index.ts +9 -0
- package/src/http/mime-types.ts +765 -0
- package/src/http/request.ts +44 -0
- package/src/http/response.ts +73 -0
- package/src/http/server.ts +67 -0
- package/src/http/types.ts +31 -0
- package/src/index.ts +4 -0
- package/src/logging.ts +57 -0
- package/src/router/cors.ts +54 -0
- package/src/router/index.ts +38 -0
- package/src/router/match.ts +194 -0
- package/src/router/parameters.ts +172 -0
- package/src/router/router.ts +233 -0
- package/src/swagger/index.ts +184 -0
- package/src/swagger/openapi-spec.ts +312 -0
- package/src/validators/array.ts +33 -0
- package/src/validators/boolean.ts +23 -0
- package/src/validators/common.ts +46 -0
- package/src/validators/index.ts +50 -0
- package/src/validators/number.ts +36 -0
- package/src/validators/object.ts +41 -0
- 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
|
+
// }
|