starlight-server 1.3.0 → 1.5.0
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/README.md +11 -1
- package/dist/demo/index.js +41 -31
- package/dist/http/cors.d.ts +33 -0
- package/dist/http/cors.js +33 -0
- package/dist/http/index.d.ts +1 -0
- package/dist/http/index.js +1 -0
- package/dist/http/mime-types.js +1 -20
- package/dist/http/request.d.ts +18 -2
- package/dist/http/request.js +28 -7
- package/dist/http/response.d.ts +9 -5
- package/dist/http/response.js +16 -8
- package/dist/http/server.d.ts +1 -1
- package/dist/http/server.js +1 -1
- package/dist/index.d.ts +3 -1
- package/dist/index.js +3 -2
- package/dist/logging.d.ts +4 -3
- package/dist/logging.js +7 -19
- package/dist/router/helpers.d.ts +6 -0
- package/dist/router/helpers.js +24 -0
- package/dist/router/index.d.ts +67 -38
- package/dist/router/index.js +107 -36
- package/dist/router/match-path.d.ts +13 -0
- package/dist/router/match-path.js +160 -0
- package/dist/swagger/factories.d.ts +143 -0
- package/dist/swagger/factories.js +231 -0
- package/dist/swagger/index.d.ts +54 -6
- package/dist/swagger/index.js +94 -58
- package/dist/swagger/specification.d.ts +802 -0
- package/dist/swagger/specification.js +130 -0
- package/package.json +9 -9
- package/src/demo/index.ts +45 -46
- package/src/http/cors.ts +59 -0
- package/src/http/index.ts +1 -0
- package/src/http/mime-types.ts +1 -20
- package/src/http/request.ts +38 -10
- package/src/http/response.ts +18 -17
- package/src/http/server.ts +2 -2
- package/src/index.ts +3 -2
- package/src/logging.ts +7 -21
- package/src/router/helpers.ts +27 -0
- package/src/router/index.ts +141 -38
- package/src/router/match-path.ts +178 -0
- package/src/swagger/factories.ts +345 -0
- package/src/swagger/index.ts +113 -80
- package/src/swagger/specification.ts +823 -0
- package/dist/router/cors.d.ts +0 -24
- package/dist/router/cors.js +0 -35
- package/dist/router/match.d.ts +0 -23
- package/dist/router/match.js +0 -172
- package/dist/router/parameters.d.ts +0 -50
- package/dist/router/parameters.js +0 -118
- package/dist/router/router.d.ts +0 -128
- package/dist/router/router.js +0 -97
- package/dist/swagger/factory.d.ts +0 -114
- package/dist/swagger/factory.js +0 -159
- package/dist/swagger/openapi-spec.d.ts +0 -261
- package/dist/swagger/openapi-spec.js +0 -5
- package/dist/validators/array.d.ts +0 -9
- package/dist/validators/array.js +0 -28
- package/dist/validators/boolean.d.ts +0 -4
- package/dist/validators/boolean.js +0 -28
- package/dist/validators/common.d.ts +0 -23
- package/dist/validators/common.js +0 -25
- package/dist/validators/index.d.ts +0 -20
- package/dist/validators/index.js +0 -38
- package/dist/validators/number.d.ts +0 -10
- package/dist/validators/number.js +0 -30
- package/dist/validators/object.d.ts +0 -13
- package/dist/validators/object.js +0 -36
- package/dist/validators/string.d.ts +0 -11
- package/dist/validators/string.js +0 -29
- package/src/router/cors.ts +0 -54
- package/src/router/match.ts +0 -194
- package/src/router/parameters.ts +0 -175
- package/src/router/router.ts +0 -234
- package/src/swagger/factory.ts +0 -184
- package/src/swagger/openapi-spec.ts +0 -312
- package/src/validators/array.ts +0 -33
- package/src/validators/boolean.ts +0 -23
- package/src/validators/common.ts +0 -46
- package/src/validators/index.ts +0 -50
- package/src/validators/number.ts +0 -36
- package/src/validators/object.ts +0 -41
- package/src/validators/string.ts +0 -38
|
@@ -0,0 +1,130 @@
|
|
|
1
|
+
/**
|
|
2
|
+
* OpenAPI Specification
|
|
3
|
+
* https://swagger.io/specification/
|
|
4
|
+
*
|
|
5
|
+
* ---------------------
|
|
6
|
+
*
|
|
7
|
+
* # 概念解释
|
|
8
|
+
*
|
|
9
|
+
* Operation
|
|
10
|
+
* 即通常说的“接口”。
|
|
11
|
+
* OpenAPI 遵循 RESTFul 的思路,一个路径代表一项资源,每个 HTTP Method 代表对此资源的一项操作(Operation)。
|
|
12
|
+
*
|
|
13
|
+
* Tag
|
|
14
|
+
* 接口标签,可以理解为是接口分组。
|
|
15
|
+
* 一个接口可以有多个标签,即接口能归属多个分组。
|
|
16
|
+
*
|
|
17
|
+
* Security Scheme
|
|
18
|
+
* 认证方案。基于 OAuth 等标准制定的具体执行方案。
|
|
19
|
+
*
|
|
20
|
+
* Security Requirement
|
|
21
|
+
* 认证要求。
|
|
22
|
+
* 要求发起请求时必须实现一种或多种“认证方案”。
|
|
23
|
+
* 一个接口可以有多套“认证要求”,请求方选择其中一种来实现。
|
|
24
|
+
* (注意是选择一种“要求”,而不是选择“要求”里的一种“方案”)。
|
|
25
|
+
*
|
|
26
|
+
* Link
|
|
27
|
+
* 接口关联。
|
|
28
|
+
* 接口之间可能有关联关系,例如 `/schools/:id` 和 `/schools/:schoolId/classes/:id` 就是相关的接口,
|
|
29
|
+
* 并且前者的 `id` 参数对应后者的 `schoolId` 参数。
|
|
30
|
+
* “接口关联”就是用来记录下这些关系。
|
|
31
|
+
*
|
|
32
|
+
* Webhook
|
|
33
|
+
* 由“接口提供方”主动请求“接口使用者”的网址,例如在某些事件发生时通知使用者。
|
|
34
|
+
*
|
|
35
|
+
* Callback
|
|
36
|
+
* 接口回调:某个接口被调用后,反过来请求“请求发起者”的网址。
|
|
37
|
+
* 例如像微信授权流程:“使用者”请求授权接口 - “微信”反过来请求“使用者”的网址来传递授权结果
|
|
38
|
+
* 注意:Callback 和 Webhook 很像,不过一个是“调用接口后”反过来被请求;另一个是跟接口调用无关,单纯被“接口提供方”调用。
|
|
39
|
+
*
|
|
40
|
+
* Data Type
|
|
41
|
+
* 数据类型(string、number、boolean、array、object...)。
|
|
42
|
+
*
|
|
43
|
+
* Schema
|
|
44
|
+
* 数据规范:定义一种数据的“数据格式”和“验证规则”。
|
|
45
|
+
* - “数据格式”包含数据类型(Data Type)信息,对于 object,也包含对象接口信息。
|
|
46
|
+
* 它决定了数据以什么形态存在,例如在内存中怎么存储。
|
|
47
|
+
* - 验证规则则是像“字符串长度在 8 ~ 16 之间”等“业务”上的要求,它与数据的存在方式无关,但决定了数据是否符合业务需求。
|
|
48
|
+
*
|
|
49
|
+
* Media Type
|
|
50
|
+
* 以特定 MIME Type 传递的内容的 Schema 和范例。
|
|
51
|
+
* 固定以 { [MIMEType]: MediaType } 的形式出现。
|
|
52
|
+
*
|
|
53
|
+
* ---------------------
|
|
54
|
+
*
|
|
55
|
+
* # 字符串扩展
|
|
56
|
+
*
|
|
57
|
+
* CommonMark
|
|
58
|
+
* 支持 CommonMark 格式的富文本字符串
|
|
59
|
+
*
|
|
60
|
+
* MIME Type
|
|
61
|
+
* 表示 MIME Type 的字符串,如:application/json。
|
|
62
|
+
* 凡是可指定 MIME Type 字符串的地方,都支持通配符 `image/*`,并可用逗号分隔多项 `image/jpg,image/png`。
|
|
63
|
+
*
|
|
64
|
+
* Runtime Expression
|
|
65
|
+
* 可通过此表达式结合请求信息计算出一个 URL。
|
|
66
|
+
* 详见 <https://swagger.io/specification/#runtime-expressions>
|
|
67
|
+
*
|
|
68
|
+
* ---------------------
|
|
69
|
+
*
|
|
70
|
+
* # 请求参数序列化(Parameter Style)
|
|
71
|
+
*
|
|
72
|
+
* 发起请求时要携带参数,如果业务上要求参数值不是纯文本(如数组、对象),那么需要经过格式化才能传递。
|
|
73
|
+
*
|
|
74
|
+
* 例如一个接口批量返回若干本书的详情:`GET /books`,并通过 `id_list` 参数指定一系列书的 ID。
|
|
75
|
+
* 这个 id_list 应该是一个数组 `[1,2,3]`,但 HTTP 请求只能传递纯文本,需要把数组 `序列化` 成纯文本来传递。
|
|
76
|
+
* 最常见的办法,是把 `id_list` 放在 query 里,多个 ID 用逗号分隔:`/books?id_list=1,2,3`,这种序列化方式在 OpenAPI 规范里被称为 `form`。
|
|
77
|
+
* 还有一种办法是直接加在 path 里:`/books/1,2,3`,这种方式被称为 `simple`。
|
|
78
|
+
* 以上都是使用 OpenAPI 规范里预设的序列化方式(Parameter Style)来序列化参数,这被称为是 `简单场景的参数`。
|
|
79
|
+
*
|
|
80
|
+
* 另外,还可以由服务器端和客户端约定,用某种 MIME Type(如 application/json)的格式序列化参数:`/books/?id_list=[1,2,3]`。
|
|
81
|
+
* 这被称为 `复杂场景的参数`(即“预设方案满足不了需求场景”的参数)。
|
|
82
|
+
*
|
|
83
|
+
* ## 预设序列化方式(Parameter Style)列表
|
|
84
|
+
*
|
|
85
|
+
* ----------------------------------------------------------
|
|
86
|
+
* 序列化方式 | 支持数据类型 | 支持的参数位置
|
|
87
|
+
* ----------------------------------------------------------
|
|
88
|
+
* matrix | primitive,array,object | path
|
|
89
|
+
* label | primitive,array,object | path
|
|
90
|
+
* form | primitive,array,object | query,cookie
|
|
91
|
+
* simple | primitive,array | path,header
|
|
92
|
+
* spaceDelimited | array,object | query
|
|
93
|
+
* pipeDelimited | array,object | query
|
|
94
|
+
* deepObject | object | query
|
|
95
|
+
*
|
|
96
|
+
* ## 各参数位置默认的序列化方式
|
|
97
|
+
*
|
|
98
|
+
* - path: simple
|
|
99
|
+
* - query: form
|
|
100
|
+
* - header: simple
|
|
101
|
+
* - cookie: form
|
|
102
|
+
*
|
|
103
|
+
* ## 参数值范例
|
|
104
|
+
*
|
|
105
|
+
* 假设有一个名为 `color` 的参数,有以下几种值之一:
|
|
106
|
+
* ```
|
|
107
|
+
* string -> "blue"
|
|
108
|
+
* array -> ["blue", "black", "brown"]
|
|
109
|
+
* object -> { "R": 100, "G": 200, "B": 150 }
|
|
110
|
+
* ```
|
|
111
|
+
*
|
|
112
|
+
* -----------------------------------------------------------------------------------------------------------------------------------
|
|
113
|
+
* 序列化方式 | 拆分传递 | 空值 | string | array | object
|
|
114
|
+
* -----------------------------------------------------------------------------------------------------------------------------------
|
|
115
|
+
* matrix | 否 | ;color | ;color=blue | ;color=blue;black;brown | ;color=R,100,G,200,B,150
|
|
116
|
+
* matrix | 是 | ;color | ;color=blue | ;color=blue;color=black;color=brown | ;R=100;G=200;B=150
|
|
117
|
+
* label | 否 | . | .blue | .blue.black.brown | .R.100.G.200.B.150
|
|
118
|
+
* label | 是 | . | .blue | .blue.black.brown | .R=100.G=200.B=150
|
|
119
|
+
* form | 否 | color= | color=blue | color=blue,black,brown | color=R,100,G,200,B,150
|
|
120
|
+
* form | 是 | color= | color=blue | color=blue&color=black&color=brown | R=100&G=200&B=150
|
|
121
|
+
* simple | 否 | 不支持 | blue | blue,black,brown | R,100,G,200,B,150
|
|
122
|
+
* simple | 是 | 不支持 | blue | blue,black,brown | R=100,G=200,B=150
|
|
123
|
+
* spaceDelimited | 否 | 不支持 | 不支持 | blue%20black%20brown | R%20100%20G%20200%20B%20150
|
|
124
|
+
* pipeDelimited | 否 | 不支持 | 不支持 | blue|black|brown | R|100|G|200|B|150
|
|
125
|
+
* deepObject | 是 | 不支持 | 不支持 | 不支持 | color[R]=100&color[G]=200&color[B]=150
|
|
126
|
+
*
|
|
127
|
+
* 注:拆分传递就是值有多项(即值是 array 或 object)时,每项分别传递:`a=1&a=2&a=3`;
|
|
128
|
+
* 反之则是一次性传递:`a=1,2,3`
|
|
129
|
+
*/
|
|
130
|
+
export {};
|
package/package.json
CHANGED
|
@@ -1,6 +1,6 @@
|
|
|
1
1
|
{
|
|
2
2
|
"name": "starlight-server",
|
|
3
|
-
"version": "1.
|
|
3
|
+
"version": "1.5.0",
|
|
4
4
|
"description": "Simple But Powerful Node.js HTTP Server",
|
|
5
5
|
"type": "module",
|
|
6
6
|
"scripts": {
|
|
@@ -30,25 +30,25 @@
|
|
|
30
30
|
"registry": "https://registry.npmjs.org/"
|
|
31
31
|
},
|
|
32
32
|
"dependencies": {
|
|
33
|
-
"@anjianshi/utils": "^1.
|
|
33
|
+
"@anjianshi/utils": "^1.3.1",
|
|
34
34
|
"chalk": "^5.3.0",
|
|
35
35
|
"debug": "^4.3.4",
|
|
36
|
-
"
|
|
37
|
-
"swagger-ui-dist": "^5.10.3"
|
|
36
|
+
"swagger-ui-dist": "^5.11.10"
|
|
38
37
|
},
|
|
39
38
|
"devDependencies": {
|
|
40
|
-
"@anjianshi/presets": "^1.
|
|
39
|
+
"@anjianshi/presets-eslint-node": "^1.0.3",
|
|
40
|
+
"@anjianshi/presets-eslint-typescript": "^1.0.3",
|
|
41
|
+
"@anjianshi/presets-prettier": "^1.0.0",
|
|
42
|
+
"@anjianshi/presets-typescript": "^1.0.1",
|
|
41
43
|
"@types/debug": "^4.1.9",
|
|
42
44
|
"@types/lodash": "^4.14.199",
|
|
43
45
|
"@types/node": "^20.8.6",
|
|
44
46
|
"@types/swagger-ui-dist": "^3.30.4",
|
|
45
47
|
"concurrently": "^8.2.1",
|
|
46
|
-
"eslint": "^8.54.0",
|
|
47
48
|
"nodemon": "^3.0.2",
|
|
48
49
|
"rimraf": "^4.3.0",
|
|
49
|
-
"tsc-alias": "^1.8.8"
|
|
50
|
-
"typescript": "^5.3.2"
|
|
50
|
+
"tsc-alias": "^1.8.8"
|
|
51
51
|
},
|
|
52
52
|
"eslintIgnore": [],
|
|
53
|
-
"prettier": "@anjianshi/presets/prettierrc"
|
|
53
|
+
"prettier": "@anjianshi/presets-prettier/prettierrc"
|
|
54
54
|
}
|
package/src/demo/index.ts
CHANGED
|
@@ -1,14 +1,6 @@
|
|
|
1
1
|
import path from 'node:path'
|
|
2
2
|
import { getFileDir } from '@anjianshi/utils/env-node/index.js'
|
|
3
|
-
import {
|
|
4
|
-
getLogger,
|
|
5
|
-
startHTTPServer,
|
|
6
|
-
type BaseContext,
|
|
7
|
-
type Route,
|
|
8
|
-
type PathParameters,
|
|
9
|
-
Router,
|
|
10
|
-
} from '@/index.js'
|
|
11
|
-
import { registerSwaggerRoute } from '@/swagger/index.js'
|
|
3
|
+
import { getLogger, startHTTPServer, Router } from '@/index.js'
|
|
12
4
|
|
|
13
5
|
const logger = getLogger({
|
|
14
6
|
level: 'debug',
|
|
@@ -18,59 +10,66 @@ const logger = getLogger({
|
|
|
18
10
|
},
|
|
19
11
|
})
|
|
20
12
|
|
|
21
|
-
|
|
22
|
-
|
|
23
|
-
|
|
24
|
-
|
|
25
|
-
|
|
26
|
-
baseContext: BaseContext,
|
|
27
|
-
route: Route<DemoContext>,
|
|
28
|
-
pathParameters: PathParameters,
|
|
29
|
-
) {
|
|
30
|
-
const context: DemoContext = {
|
|
31
|
-
...baseContext,
|
|
32
|
-
now: () => Date.now(),
|
|
33
|
-
}
|
|
34
|
-
await route.handler(context, pathParameters)
|
|
35
|
-
}
|
|
36
|
-
}
|
|
37
|
-
const router = new DemoRouter()
|
|
13
|
+
// declare module '@/index.js' {
|
|
14
|
+
// interface Context {
|
|
15
|
+
// now: () => number
|
|
16
|
+
// }
|
|
17
|
+
// }
|
|
38
18
|
|
|
39
|
-
router
|
|
19
|
+
const router = new Router()
|
|
20
|
+
router.setCors(true)
|
|
21
|
+
router.bindSwagger()
|
|
22
|
+
router.setExecutor(async (basicContext, route) => {
|
|
23
|
+
await route.handler({
|
|
24
|
+
...basicContext,
|
|
25
|
+
// now: () => Date.now(),
|
|
26
|
+
})
|
|
27
|
+
})
|
|
28
|
+
|
|
29
|
+
const swagger = router.swagger
|
|
30
|
+
swagger.registerResponse(
|
|
31
|
+
'hello',
|
|
32
|
+
swagger.response({
|
|
33
|
+
hello: swagger.string(),
|
|
34
|
+
})
|
|
35
|
+
)
|
|
40
36
|
|
|
41
37
|
router.register({
|
|
42
|
-
category: 'demo',
|
|
43
|
-
description: 'hello world',
|
|
44
38
|
method: 'GET',
|
|
45
39
|
path: '/hello',
|
|
46
|
-
|
|
47
|
-
|
|
48
|
-
|
|
49
|
-
|
|
50
|
-
|
|
51
|
-
|
|
52
|
-
|
|
53
|
-
|
|
54
|
-
|
|
55
|
-
|
|
40
|
+
document: {
|
|
41
|
+
category: 'demo',
|
|
42
|
+
description: 'hello world',
|
|
43
|
+
query: {
|
|
44
|
+
q1: { schema: swagger.number(), required: true },
|
|
45
|
+
q2: { schema: swagger.number(), description: '这是q2' },
|
|
46
|
+
q3: swagger.boolean(),
|
|
47
|
+
},
|
|
48
|
+
body: {
|
|
49
|
+
abc: swagger.number(),
|
|
50
|
+
def: swagger.array(swagger.string()),
|
|
51
|
+
},
|
|
52
|
+
response: {
|
|
53
|
+
name: swagger.string(),
|
|
54
|
+
},
|
|
56
55
|
},
|
|
57
|
-
handler(
|
|
58
|
-
response.json({
|
|
56
|
+
handler(ctx) {
|
|
57
|
+
ctx.response.json({ name: 'world' })
|
|
59
58
|
},
|
|
60
59
|
})
|
|
61
60
|
|
|
62
61
|
router.register({
|
|
63
|
-
category: 'demo',
|
|
64
62
|
method: 'POST',
|
|
65
63
|
path: '/hello',
|
|
66
|
-
|
|
64
|
+
document: {
|
|
65
|
+
category: 'demo',
|
|
66
|
+
response: swagger.ref('response', 'hello'),
|
|
67
|
+
},
|
|
67
68
|
handler({ response }) {
|
|
68
|
-
response.json({ hello: 'world
|
|
69
|
+
response.json({ hello: 'world' })
|
|
69
70
|
},
|
|
70
71
|
})
|
|
71
72
|
|
|
72
|
-
registerSwaggerRoute(router)
|
|
73
|
-
|
|
74
73
|
startHTTPServer({
|
|
75
74
|
handler: router.handle,
|
|
76
75
|
logger: logger.getChild('http'),
|
package/src/http/cors.ts
ADDED
|
@@ -0,0 +1,59 @@
|
|
|
1
|
+
/**
|
|
2
|
+
* CORS 处理逻辑
|
|
3
|
+
*/
|
|
4
|
+
import { type Request } from './request.js'
|
|
5
|
+
import { ResponseUtils } from './response.js'
|
|
6
|
+
import { type NodeRequest, type NodeResponse } from './types.js'
|
|
7
|
+
|
|
8
|
+
/**
|
|
9
|
+
* 判断是否是 CORS Preflight 请求
|
|
10
|
+
*/
|
|
11
|
+
export function isPreflight(request: Request | NodeRequest) {
|
|
12
|
+
return (
|
|
13
|
+
request.method === 'OPTIONS' &&
|
|
14
|
+
typeof request.headers['access-control-request-method'] === 'string'
|
|
15
|
+
)
|
|
16
|
+
}
|
|
17
|
+
|
|
18
|
+
/**
|
|
19
|
+
* 若请求是 CORS Preflight 请求,返回客户端原本想请求的 Method
|
|
20
|
+
* 否则返回 null
|
|
21
|
+
*/
|
|
22
|
+
export function getPreflightRequestMethod(request: Request | NodeRequest) {
|
|
23
|
+
return isPreflight(request) ? request.headers['access-control-request-method'] ?? '' : null
|
|
24
|
+
}
|
|
25
|
+
|
|
26
|
+
/**
|
|
27
|
+
* CORS 放行规则
|
|
28
|
+
* - false:不放行
|
|
29
|
+
* - true:无条件放行(等同于 { allowOrigin: '*', allowHeaders: '*', exposeHeaders: '*' })
|
|
30
|
+
* - object:手动指定规则
|
|
31
|
+
* - 未指定的 key 不会输出对应 header
|
|
32
|
+
* - allowHeaders 仅对 Preflight 请求有效
|
|
33
|
+
* - exposeHeaders 仅对非 Preflight 请求有效
|
|
34
|
+
*/
|
|
35
|
+
export type CORSRule =
|
|
36
|
+
| boolean
|
|
37
|
+
| { allowOrigin?: string; allowHeaders?: string; exposeHeaders?: string }
|
|
38
|
+
|
|
39
|
+
/**
|
|
40
|
+
* 输出 CORS 相关 headers
|
|
41
|
+
*/
|
|
42
|
+
export function handleCORS(
|
|
43
|
+
request: Request | NodeRequest,
|
|
44
|
+
response: ResponseUtils | NodeResponse,
|
|
45
|
+
rule: CORSRule
|
|
46
|
+
) {
|
|
47
|
+
// 不输出 CORS 相关 headers,浏览器会默认服务端不允许跨域请求
|
|
48
|
+
if (rule === false) return
|
|
49
|
+
if (rule === true) rule = { allowOrigin: '*', allowHeaders: '*', exposeHeaders: '*' }
|
|
50
|
+
|
|
51
|
+
const nodeResponse = response instanceof ResponseUtils ? response.nodeResponse : response
|
|
52
|
+
const requestIsPreflight = isPreflight(request)
|
|
53
|
+
if (rule.allowOrigin !== undefined)
|
|
54
|
+
nodeResponse.setHeader('Access-Control-Allow-Origin', rule.allowOrigin)
|
|
55
|
+
if (requestIsPreflight && rule.allowHeaders !== undefined)
|
|
56
|
+
nodeResponse.setHeader('Access-Control-Allow-Headers', rule.allowHeaders)
|
|
57
|
+
if (!requestIsPreflight && rule.exposeHeaders !== undefined)
|
|
58
|
+
nodeResponse.setHeader('Access-Control-Expose-Headers', rule.exposeHeaders)
|
|
59
|
+
}
|
package/src/http/index.ts
CHANGED
package/src/http/mime-types.ts
CHANGED
|
@@ -50,26 +50,7 @@ export const MIMETypes = [
|
|
|
50
50
|
{ type: 'application/mxf', extensions: ['.mxf'] },
|
|
51
51
|
{
|
|
52
52
|
type: 'application/octet-stream',
|
|
53
|
-
extensions: [
|
|
54
|
-
'.a',
|
|
55
|
-
'.bin',
|
|
56
|
-
'.bpk',
|
|
57
|
-
'.deploy',
|
|
58
|
-
'.dist',
|
|
59
|
-
'.distz',
|
|
60
|
-
'.dmg',
|
|
61
|
-
'.dms',
|
|
62
|
-
'.dump',
|
|
63
|
-
'.elc',
|
|
64
|
-
'.iso',
|
|
65
|
-
'.lha',
|
|
66
|
-
'.lrf',
|
|
67
|
-
'.lzh',
|
|
68
|
-
'.o',
|
|
69
|
-
'.obj',
|
|
70
|
-
'.pkg',
|
|
71
|
-
'.so',
|
|
72
|
-
],
|
|
53
|
+
extensions: ['.a', '.bin', '.bpk', '.deploy', '.dist', '.distz', '.dmg', '.dms', '.dump', '.elc', '.iso', '.lha', '.lrf', '.lzh', '.o', '.obj', '.pkg', '.so'], // prettier-ignore
|
|
73
54
|
},
|
|
74
55
|
{ type: 'application/oda', extensions: ['.oda'] },
|
|
75
56
|
{ type: 'application/oebps-package+xml', extensions: ['.opf'] },
|
package/src/http/request.ts
CHANGED
|
@@ -1,10 +1,12 @@
|
|
|
1
1
|
import type http from 'node:http'
|
|
2
|
-
import {
|
|
2
|
+
import { parseQuery } from '@anjianshi/utils'
|
|
3
|
+
import { type Logger } from '@/logging.js'
|
|
3
4
|
import { RequestBody, type BodyOptions } from './body/index.js'
|
|
4
5
|
import { HTTPError, type NodeRequest } from './types.js'
|
|
5
6
|
|
|
6
7
|
/**
|
|
7
8
|
* 对 Node.js 请求内容的二次封装
|
|
9
|
+
*
|
|
8
10
|
* - 提供经过整理的请求信息
|
|
9
11
|
* - 自带 body 解析(实现了 “body 大小限制” 和 “JSON、form-data 等格式的内容解析”)
|
|
10
12
|
*
|
|
@@ -13,32 +15,58 @@ import { HTTPError, type NodeRequest } from './types.js'
|
|
|
13
15
|
* - Comes with body parsing (implements 'body size limit' and 'content parsing for formats such as JSON and form-data').
|
|
14
16
|
*/
|
|
15
17
|
export class Request {
|
|
18
|
+
/** HTTP Method,始终为大写 */
|
|
16
19
|
readonly method: string
|
|
20
|
+
|
|
21
|
+
/** 完整的请求 URL */
|
|
22
|
+
readonly url: string
|
|
23
|
+
|
|
24
|
+
/** 域名及端口 */
|
|
17
25
|
readonly host: string
|
|
26
|
+
|
|
27
|
+
/** 请求路径 */
|
|
18
28
|
readonly path: string
|
|
19
|
-
|
|
29
|
+
|
|
30
|
+
/** 解析后的 query 对象 */
|
|
31
|
+
readonly query: Record<string, string>
|
|
32
|
+
|
|
33
|
+
/**
|
|
34
|
+
* headers 对象,以 Record<string, xxx> 形式访问,仅支持小写 key。
|
|
35
|
+
* (可使用 request.getHeader() 方法不区分大小写获取 header)
|
|
36
|
+
*/
|
|
20
37
|
readonly headers: http.IncomingHttpHeaders
|
|
38
|
+
|
|
39
|
+
/** 请求体。出于性能考虑,调用具体解析方法时才执行解析。 */
|
|
21
40
|
readonly body: RequestBody
|
|
22
41
|
|
|
23
42
|
constructor(
|
|
24
43
|
readonly nodeRequest: NodeRequest,
|
|
25
44
|
protected readonly logger: Logger,
|
|
26
|
-
bodyOptions: BodyOptions
|
|
45
|
+
bodyOptions: BodyOptions
|
|
27
46
|
) {
|
|
28
47
|
if (nodeRequest.method === undefined) throw new HTTPError(405)
|
|
29
|
-
this.method = nodeRequest.method
|
|
48
|
+
this.method = nodeRequest.method.toUpperCase()
|
|
30
49
|
|
|
31
50
|
try {
|
|
32
|
-
|
|
33
|
-
|
|
34
|
-
this.
|
|
35
|
-
this.
|
|
51
|
+
// 参考:<https://nodejs.org/api/http.html#messageurl>
|
|
52
|
+
const urlObject = new URL(nodeRequest.url ?? '', `http://${nodeRequest.headers.host ?? ''}`)
|
|
53
|
+
this.url = urlObject.href
|
|
54
|
+
this.host = urlObject.host
|
|
55
|
+
this.path = urlObject.pathname
|
|
36
56
|
} catch (e) {
|
|
37
|
-
this.logger.warn(
|
|
38
|
-
throw new HTTPError(400, '
|
|
57
|
+
this.logger.warn(`parse URL failed:${nodeRequest.url}`, e)
|
|
58
|
+
throw new HTTPError(400, 'URL invalid')
|
|
39
59
|
}
|
|
40
60
|
|
|
61
|
+
this.query = parseQuery(this.url)
|
|
41
62
|
this.headers = nodeRequest.headers
|
|
42
63
|
this.body = new RequestBody(nodeRequest, bodyOptions)
|
|
43
64
|
}
|
|
65
|
+
|
|
66
|
+
/**
|
|
67
|
+
* 获取 HTTP Header,支持任意大小写形式:content-type / Content-Type
|
|
68
|
+
*/
|
|
69
|
+
getHeader<const K extends string>(key: K): http.IncomingHttpHeaders[Lowercase<K>] {
|
|
70
|
+
return this.headers[key.toLowerCase()]
|
|
71
|
+
}
|
|
44
72
|
}
|
package/src/http/response.ts
CHANGED
|
@@ -1,4 +1,4 @@
|
|
|
1
|
-
import { type Logger } from '
|
|
1
|
+
import { type Logger } from '@/logging.js'
|
|
2
2
|
import { path2MIMEType } from './mime-types.js'
|
|
3
3
|
import { HTTPError, type NodeResponse } from './types.js'
|
|
4
4
|
|
|
@@ -7,10 +7,7 @@ import { HTTPError, type NodeResponse } from './types.js'
|
|
|
7
7
|
* Encapsulate functions for outputting response content.
|
|
8
8
|
*/
|
|
9
9
|
export class ResponseUtils {
|
|
10
|
-
constructor(
|
|
11
|
-
readonly nodeResponse: NodeResponse,
|
|
12
|
-
readonly logger: Logger,
|
|
13
|
-
) {}
|
|
10
|
+
constructor(readonly nodeResponse: NodeResponse, readonly logger: Logger) {}
|
|
14
11
|
|
|
15
12
|
header(name: string, value: string) {
|
|
16
13
|
this.nodeResponse.setHeader(name, value)
|
|
@@ -20,16 +17,6 @@ export class ResponseUtils {
|
|
|
20
17
|
items.forEach(([name, value]) => this.header(name, value))
|
|
21
18
|
}
|
|
22
19
|
|
|
23
|
-
/**
|
|
24
|
-
* Output CORS Headers
|
|
25
|
-
*/
|
|
26
|
-
cors(allowOrigin = '*', allowHeaders = '*') {
|
|
27
|
-
this.headers(
|
|
28
|
-
['Access-Control-Allow-Origin', allowOrigin],
|
|
29
|
-
['Access-Control-Allow-Headers', allowHeaders],
|
|
30
|
-
)
|
|
31
|
-
}
|
|
32
|
-
|
|
33
20
|
text(content: string | Buffer) {
|
|
34
21
|
this.nodeResponse.end(content)
|
|
35
22
|
}
|
|
@@ -59,15 +46,29 @@ export class ResponseUtils {
|
|
|
59
46
|
|
|
60
47
|
/**
|
|
61
48
|
* Output HTTP Error
|
|
49
|
+
*
|
|
50
|
+
* response.error(new HTTPError(xxx)) // 传入 HTTP Error
|
|
51
|
+
* response.error(404) // 传入 HTTP Status
|
|
52
|
+
* response.error(xxx) // 传入其他内容,会记录下日志并以 500 状态结束请求
|
|
62
53
|
*/
|
|
63
54
|
error(error: unknown) {
|
|
64
55
|
if (error instanceof HTTPError) {
|
|
65
56
|
this.nodeResponse.statusCode = error.status // eslint-disable-line require-atomic-updates
|
|
66
57
|
this.nodeResponse.end(error.message)
|
|
58
|
+
} else if (typeof error === 'number') {
|
|
59
|
+
this.error(new HTTPError(error))
|
|
67
60
|
} else {
|
|
68
61
|
this.logger.error(error)
|
|
69
|
-
this.
|
|
70
|
-
this.nodeResponse.end(new HTTPError(500).message)
|
|
62
|
+
this.error(new HTTPError(500))
|
|
71
63
|
}
|
|
72
64
|
}
|
|
65
|
+
|
|
66
|
+
/**
|
|
67
|
+
* 执行重定向
|
|
68
|
+
*/
|
|
69
|
+
redirect(url: string, permanent = false) {
|
|
70
|
+
this.nodeResponse.statusCode = permanent ? 301 : 302
|
|
71
|
+
this.header('Location', url)
|
|
72
|
+
this.nodeResponse.end()
|
|
73
|
+
}
|
|
73
74
|
}
|
package/src/http/server.ts
CHANGED
|
@@ -1,5 +1,5 @@
|
|
|
1
1
|
import http from 'node:http'
|
|
2
|
-
import { type Logger, LogLevel } from '
|
|
2
|
+
import { type Logger, LogLevel } from '@/logging.js'
|
|
3
3
|
import { type BodyOptions } from './body/index.js'
|
|
4
4
|
import { Request } from './request.js'
|
|
5
5
|
import { ResponseUtils } from './response.js'
|
|
@@ -33,7 +33,7 @@ export function startHTTPServer(
|
|
|
33
33
|
handler?: RequestHandler
|
|
34
34
|
logger: Logger
|
|
35
35
|
port: number
|
|
36
|
-
}
|
|
36
|
+
}
|
|
37
37
|
) {
|
|
38
38
|
const { handler = placeholderHandler, logger, port, ...bodyOptions } = options
|
|
39
39
|
|
package/src/index.ts
CHANGED
|
@@ -1,4 +1,5 @@
|
|
|
1
|
+
export * from '@/logging.js'
|
|
1
2
|
export * from '@/http/index.js'
|
|
2
3
|
export * from '@/router/index.js'
|
|
3
|
-
export * from '@/
|
|
4
|
-
|
|
4
|
+
export * from '@/swagger/index.js'
|
|
5
|
+
export { validators } from '@anjianshi/utils/validators/index.js'
|
package/src/logging.ts
CHANGED
|
@@ -6,17 +6,18 @@ import {
|
|
|
6
6
|
ConsoleHandler,
|
|
7
7
|
FileHandler,
|
|
8
8
|
type FileHandlerOptions,
|
|
9
|
-
} from '@anjianshi/utils/env-node/logging.js'
|
|
9
|
+
} from '@anjianshi/utils/env-node/logging/index.js'
|
|
10
10
|
import debug from 'debug'
|
|
11
11
|
|
|
12
12
|
export type { FileHandlerOptions }
|
|
13
|
+
export { Logger, LogLevel }
|
|
13
14
|
|
|
14
15
|
export interface LoggingOptions {
|
|
15
16
|
/**
|
|
16
17
|
* 指定日志等级(debug info warn err)
|
|
17
18
|
* Specify log level.
|
|
18
19
|
*/
|
|
19
|
-
level?: string
|
|
20
|
+
level?: string | LogLevel
|
|
20
21
|
|
|
21
22
|
/**
|
|
22
23
|
* 文件日志参数
|
|
@@ -31,27 +32,12 @@ export interface LoggingOptions {
|
|
|
31
32
|
debugLib?: boolean | string
|
|
32
33
|
}
|
|
33
34
|
|
|
34
|
-
const levelMap: Record<string, LogLevel> = {
|
|
35
|
-
debug: LogLevel.Debug,
|
|
36
|
-
info: LogLevel.Info,
|
|
37
|
-
warn: LogLevel.Warning,
|
|
38
|
-
warning: LogLevel.Warning,
|
|
39
|
-
err: LogLevel.Error,
|
|
40
|
-
error: LogLevel.Error,
|
|
41
|
-
}
|
|
42
|
-
|
|
43
35
|
export function getLogger(options: LoggingOptions = {}) {
|
|
44
36
|
const logger = new Logger()
|
|
45
37
|
logger.addHandler(new ConsoleHandler())
|
|
46
|
-
if (options.level !== undefined)
|
|
47
|
-
|
|
48
|
-
|
|
49
|
-
|
|
50
|
-
if (options.file) {
|
|
51
|
-
logger.addHandler(new FileHandler(options.file))
|
|
52
|
-
}
|
|
53
|
-
if (truthy(options.debugLib)) {
|
|
54
|
-
void adaptDebugLib(debug, options.debugLib === true ? '*' : options.debugLib, logger)
|
|
55
|
-
}
|
|
38
|
+
if (options.level !== undefined) logger.setLevel(options.level)
|
|
39
|
+
if (options.file) logger.addHandler(new FileHandler(options.file))
|
|
40
|
+
if (truthy(options.debugLib))
|
|
41
|
+
adaptDebugLib(debug, options.debugLib === true ? '*' : options.debugLib, logger)
|
|
56
42
|
return logger
|
|
57
43
|
}
|
|
@@ -0,0 +1,27 @@
|
|
|
1
|
+
import { type Validator, validators } from '@anjianshi/utils/validators/index.js'
|
|
2
|
+
import { HTTPError } from '@/index.js'
|
|
3
|
+
import { type BasicContext } from './index.js'
|
|
4
|
+
|
|
5
|
+
export { validators }
|
|
6
|
+
|
|
7
|
+
export function validatePathParameters<T>(this: BasicContext, struct: Record<string, Validator>) {
|
|
8
|
+
const result = validators.struct(struct).validate('path', this.pathParameters)
|
|
9
|
+
if (result.success) return result.data as T
|
|
10
|
+
throw new HTTPError(400, result.message)
|
|
11
|
+
}
|
|
12
|
+
|
|
13
|
+
export function validateQuery<T>(this: BasicContext, struct: Record<string, Validator>) {
|
|
14
|
+
const result = validators.struct(struct).validate('query', this.request.query)
|
|
15
|
+
if (result.success) return result.data as T
|
|
16
|
+
throw new HTTPError(400, result.message)
|
|
17
|
+
}
|
|
18
|
+
|
|
19
|
+
export async function validateBody<T>(this: BasicContext, struct: Record<string, Validator>) {
|
|
20
|
+
const body = await this.request.body.json()
|
|
21
|
+
if (typeof body !== 'object' || body === null || Array.isArray(body))
|
|
22
|
+
throw new HTTPError(400, 'Invalid JSON body, should be an object.')
|
|
23
|
+
|
|
24
|
+
const result = validators.struct(struct).validate('body', this.request.body)
|
|
25
|
+
if (result.success) return result.data as T
|
|
26
|
+
throw new HTTPError(400, result.message)
|
|
27
|
+
}
|