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.
Files changed (84) hide show
  1. package/README.md +11 -1
  2. package/dist/demo/index.js +41 -31
  3. package/dist/http/cors.d.ts +33 -0
  4. package/dist/http/cors.js +33 -0
  5. package/dist/http/index.d.ts +1 -0
  6. package/dist/http/index.js +1 -0
  7. package/dist/http/mime-types.js +1 -20
  8. package/dist/http/request.d.ts +18 -2
  9. package/dist/http/request.js +28 -7
  10. package/dist/http/response.d.ts +9 -5
  11. package/dist/http/response.js +16 -8
  12. package/dist/http/server.d.ts +1 -1
  13. package/dist/http/server.js +1 -1
  14. package/dist/index.d.ts +3 -1
  15. package/dist/index.js +3 -2
  16. package/dist/logging.d.ts +4 -3
  17. package/dist/logging.js +7 -19
  18. package/dist/router/helpers.d.ts +6 -0
  19. package/dist/router/helpers.js +24 -0
  20. package/dist/router/index.d.ts +67 -38
  21. package/dist/router/index.js +107 -36
  22. package/dist/router/match-path.d.ts +13 -0
  23. package/dist/router/match-path.js +160 -0
  24. package/dist/swagger/factories.d.ts +143 -0
  25. package/dist/swagger/factories.js +231 -0
  26. package/dist/swagger/index.d.ts +54 -6
  27. package/dist/swagger/index.js +94 -58
  28. package/dist/swagger/specification.d.ts +802 -0
  29. package/dist/swagger/specification.js +130 -0
  30. package/package.json +9 -9
  31. package/src/demo/index.ts +45 -46
  32. package/src/http/cors.ts +59 -0
  33. package/src/http/index.ts +1 -0
  34. package/src/http/mime-types.ts +1 -20
  35. package/src/http/request.ts +38 -10
  36. package/src/http/response.ts +18 -17
  37. package/src/http/server.ts +2 -2
  38. package/src/index.ts +3 -2
  39. package/src/logging.ts +7 -21
  40. package/src/router/helpers.ts +27 -0
  41. package/src/router/index.ts +141 -38
  42. package/src/router/match-path.ts +178 -0
  43. package/src/swagger/factories.ts +345 -0
  44. package/src/swagger/index.ts +113 -80
  45. package/src/swagger/specification.ts +823 -0
  46. package/dist/router/cors.d.ts +0 -24
  47. package/dist/router/cors.js +0 -35
  48. package/dist/router/match.d.ts +0 -23
  49. package/dist/router/match.js +0 -172
  50. package/dist/router/parameters.d.ts +0 -50
  51. package/dist/router/parameters.js +0 -118
  52. package/dist/router/router.d.ts +0 -128
  53. package/dist/router/router.js +0 -97
  54. package/dist/swagger/factory.d.ts +0 -114
  55. package/dist/swagger/factory.js +0 -159
  56. package/dist/swagger/openapi-spec.d.ts +0 -261
  57. package/dist/swagger/openapi-spec.js +0 -5
  58. package/dist/validators/array.d.ts +0 -9
  59. package/dist/validators/array.js +0 -28
  60. package/dist/validators/boolean.d.ts +0 -4
  61. package/dist/validators/boolean.js +0 -28
  62. package/dist/validators/common.d.ts +0 -23
  63. package/dist/validators/common.js +0 -25
  64. package/dist/validators/index.d.ts +0 -20
  65. package/dist/validators/index.js +0 -38
  66. package/dist/validators/number.d.ts +0 -10
  67. package/dist/validators/number.js +0 -30
  68. package/dist/validators/object.d.ts +0 -13
  69. package/dist/validators/object.js +0 -36
  70. package/dist/validators/string.d.ts +0 -11
  71. package/dist/validators/string.js +0 -29
  72. package/src/router/cors.ts +0 -54
  73. package/src/router/match.ts +0 -194
  74. package/src/router/parameters.ts +0 -175
  75. package/src/router/router.ts +0 -234
  76. package/src/swagger/factory.ts +0 -184
  77. package/src/swagger/openapi-spec.ts +0 -312
  78. package/src/validators/array.ts +0 -33
  79. package/src/validators/boolean.ts +0 -23
  80. package/src/validators/common.ts +0 -46
  81. package/src/validators/index.ts +0 -50
  82. package/src/validators/number.ts +0 -36
  83. package/src/validators/object.ts +0 -41
  84. 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.0",
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.0.9",
33
+ "@anjianshi/utils": "^1.3.1",
34
34
  "chalk": "^5.3.0",
35
35
  "debug": "^4.3.4",
36
- "lodash": "^4.17.21",
37
- "swagger-ui-dist": "^5.10.3"
36
+ "swagger-ui-dist": "^5.11.10"
38
37
  },
39
38
  "devDependencies": {
40
- "@anjianshi/presets": "^1.4.0",
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
- type DemoContext = BaseContext & {
22
- now: () => number
23
- }
24
- class DemoRouter extends Router<DemoContext> {
25
- async executeWithContext(
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.registerResponseReference('hello', { object: [{ name: 'hello', type: 'string' }] })
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
- query: [
47
- { name: 'q1', type: 'number', required: true },
48
- { name: 'q2', type: 'string', description: '这是q2' },
49
- ],
50
- body: [
51
- { name: 'abc', type: 'number' },
52
- { name: 'def', type: { array: { type: 'string' } } },
53
- ],
54
- response: {
55
- object: [{ name: 'key', type: 'string' }],
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({ response }) {
58
- response.json({ hello: 'world' })
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
- response: { ref: 'hello' },
64
+ document: {
65
+ category: 'demo',
66
+ response: swagger.ref('response', 'hello'),
67
+ },
67
68
  handler({ response }) {
68
- response.json({ hello: 'world post' })
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'),
@@ -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
@@ -7,3 +7,4 @@ export * from './response.js'
7
7
  export * from './server.js'
8
8
  export * from './http-status.js'
9
9
  export * from './mime-types.js'
10
+ export * from './cors.js'
@@ -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'] },
@@ -1,10 +1,12 @@
1
1
  import type http from 'node:http'
2
- import { type Logger } from '@anjianshi/utils'
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
- readonly query: URLSearchParams
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
- const url = new URL(nodeRequest.url ?? '', `http://${nodeRequest.headers.host ?? ''}`)
33
- this.host = url.host
34
- this.path = url.pathname
35
- this.query = url.searchParams
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('parse url failed', e)
38
- throw new HTTPError(400, 'url invalid')
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
  }
@@ -1,4 +1,4 @@
1
- import { type Logger } from '@anjianshi/utils'
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.nodeResponse.statusCode = 500 // eslint-disable-line require-atomic-updates
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
  }
@@ -1,5 +1,5 @@
1
1
  import http from 'node:http'
2
- import { type Logger, LogLevel } from '@anjianshi/utils'
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 '@/logging.js'
4
- // export * from '@/swagger/index.js'
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
- const level = levelMap[options.level.toLowerCase()]
48
- if (level !== undefined) logger.setLevel(level)
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
+ }