@planet-matrix/mobius-model 0.6.0 → 0.9.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 (233) hide show
  1. package/CHANGELOG.md +37 -0
  2. package/dist/index.js +706 -36
  3. package/dist/index.js.map +855 -59
  4. package/package.json +28 -16
  5. package/src/ai/README.md +1 -0
  6. package/src/ai/ai.ts +107 -0
  7. package/src/ai/chat-completion-ai/aihubmix-chat-completion.ts +78 -0
  8. package/src/ai/chat-completion-ai/chat-completion-ai.ts +270 -0
  9. package/src/ai/chat-completion-ai/chat-completion.ts +189 -0
  10. package/src/ai/chat-completion-ai/index.ts +7 -0
  11. package/src/ai/chat-completion-ai/lingyiwanwu-chat-completion.ts +78 -0
  12. package/src/ai/chat-completion-ai/ohmygpt-chat-completion.ts +78 -0
  13. package/src/ai/chat-completion-ai/openai-next-chat-completion.ts +78 -0
  14. package/src/ai/embedding-ai/embedding-ai.ts +63 -0
  15. package/src/ai/embedding-ai/embedding.ts +50 -0
  16. package/src/ai/embedding-ai/index.ts +4 -0
  17. package/src/ai/embedding-ai/openai-next-embedding.ts +23 -0
  18. package/src/ai/index.ts +4 -0
  19. package/src/aio/README.md +100 -0
  20. package/src/aio/content.ts +141 -0
  21. package/src/aio/index.ts +3 -0
  22. package/src/aio/json.ts +127 -0
  23. package/src/aio/prompt.ts +246 -0
  24. package/src/basic/README.md +20 -15
  25. package/src/basic/error.ts +19 -5
  26. package/src/basic/function.ts +2 -2
  27. package/src/basic/index.ts +1 -0
  28. package/src/basic/schedule.ts +111 -0
  29. package/src/basic/stream.ts +135 -25
  30. package/src/credential/README.md +107 -0
  31. package/src/credential/api-key.ts +158 -0
  32. package/src/credential/bearer.ts +73 -0
  33. package/src/credential/index.ts +4 -0
  34. package/src/credential/json-web-token.ts +96 -0
  35. package/src/credential/password.ts +170 -0
  36. package/src/cron/README.md +86 -0
  37. package/src/cron/cron.ts +87 -0
  38. package/src/cron/index.ts +1 -0
  39. package/src/drizzle/README.md +1 -0
  40. package/src/drizzle/drizzle.ts +1 -0
  41. package/src/drizzle/helper.ts +47 -0
  42. package/src/drizzle/index.ts +5 -0
  43. package/src/drizzle/infer.ts +52 -0
  44. package/src/drizzle/kysely.ts +8 -0
  45. package/src/drizzle/pagination.ts +200 -0
  46. package/src/email/README.md +1 -0
  47. package/src/email/index.ts +1 -0
  48. package/src/email/resend.ts +25 -0
  49. package/src/event/class-event-proxy.ts +6 -5
  50. package/src/event/common.ts +13 -3
  51. package/src/event/event-manager.ts +3 -3
  52. package/src/event/instance-event-proxy.ts +6 -5
  53. package/src/event/internal.ts +4 -4
  54. package/src/form/README.md +25 -0
  55. package/src/form/index.ts +1 -0
  56. package/src/form/inputor-controller/base.ts +874 -0
  57. package/src/form/inputor-controller/boolean.ts +39 -0
  58. package/src/form/inputor-controller/file.ts +39 -0
  59. package/src/form/inputor-controller/form.ts +181 -0
  60. package/src/form/inputor-controller/helper.ts +117 -0
  61. package/src/form/inputor-controller/index.ts +17 -0
  62. package/src/form/inputor-controller/multi-select.ts +99 -0
  63. package/src/form/inputor-controller/number.ts +116 -0
  64. package/src/form/inputor-controller/select.ts +109 -0
  65. package/src/form/inputor-controller/text.ts +82 -0
  66. package/src/http/READMD.md +1 -0
  67. package/src/http/api/api-core.ts +84 -0
  68. package/src/http/api/api-handler.ts +79 -0
  69. package/src/http/api/api-host.ts +47 -0
  70. package/src/http/api/api-result.ts +56 -0
  71. package/src/http/api/api-schema.ts +154 -0
  72. package/src/http/api/api-server.ts +130 -0
  73. package/src/http/api/api-test.ts +142 -0
  74. package/src/http/api/api-type.ts +37 -0
  75. package/src/http/api/api.ts +81 -0
  76. package/src/http/api/index.ts +11 -0
  77. package/src/http/api-adapter/api-core-node-http.ts +260 -0
  78. package/src/http/api-adapter/api-host-node-http.ts +156 -0
  79. package/src/http/api-adapter/api-result-arktype.ts +297 -0
  80. package/src/http/api-adapter/api-result-zod.ts +286 -0
  81. package/src/http/api-adapter/index.ts +5 -0
  82. package/src/http/bin/gen-api-list/gen-api-list.ts +126 -0
  83. package/src/http/bin/gen-api-list/index.ts +1 -0
  84. package/src/http/bin/gen-api-test/gen-api-test.ts +136 -0
  85. package/src/http/bin/gen-api-test/index.ts +1 -0
  86. package/src/http/bin/gen-api-type/calc-code.ts +25 -0
  87. package/src/http/bin/gen-api-type/gen-api-type.ts +127 -0
  88. package/src/http/bin/gen-api-type/index.ts +2 -0
  89. package/src/http/bin/index.ts +2 -0
  90. package/src/http/index.ts +3 -0
  91. package/src/huawei/README.md +1 -0
  92. package/src/huawei/index.ts +2 -0
  93. package/src/huawei/moderation/index.ts +1 -0
  94. package/src/huawei/moderation/moderation.ts +355 -0
  95. package/src/huawei/obs/esdk-obs-nodejs.d.ts +87 -0
  96. package/src/huawei/obs/index.ts +1 -0
  97. package/src/huawei/obs/obs.ts +42 -0
  98. package/src/index.ts +19 -2
  99. package/src/json/README.md +92 -0
  100. package/src/json/index.ts +1 -0
  101. package/src/json/repair.ts +18 -0
  102. package/src/log/logger.ts +15 -4
  103. package/src/openai/README.md +1 -0
  104. package/src/openai/index.ts +1 -0
  105. package/src/openai/openai.ts +510 -0
  106. package/src/orchestration/README.md +9 -7
  107. package/src/orchestration/dispatching/dispatcher.ts +83 -0
  108. package/src/orchestration/dispatching/index.ts +2 -0
  109. package/src/orchestration/dispatching/selector/base-selector.ts +39 -0
  110. package/src/orchestration/dispatching/selector/down-count-selector.ts +119 -0
  111. package/src/orchestration/dispatching/selector/index.ts +2 -0
  112. package/src/orchestration/index.ts +2 -0
  113. package/src/orchestration/scheduling/index.ts +2 -0
  114. package/src/orchestration/scheduling/scheduler.ts +103 -0
  115. package/src/orchestration/scheduling/task.ts +32 -0
  116. package/src/random/README.md +8 -7
  117. package/src/random/base.ts +66 -0
  118. package/src/random/index.ts +5 -1
  119. package/src/random/random-boolean.ts +40 -0
  120. package/src/random/random-integer.ts +60 -0
  121. package/src/random/random-number.ts +72 -0
  122. package/src/random/random-string.ts +66 -0
  123. package/src/request/README.md +108 -0
  124. package/src/request/fetch/base.ts +108 -0
  125. package/src/request/fetch/browser.ts +285 -0
  126. package/src/request/fetch/general.ts +20 -0
  127. package/src/request/fetch/index.ts +4 -0
  128. package/src/request/fetch/nodejs.ts +285 -0
  129. package/src/request/index.ts +2 -0
  130. package/src/request/request/base.ts +250 -0
  131. package/src/request/request/general.ts +64 -0
  132. package/src/request/request/index.ts +3 -0
  133. package/src/request/request/resource.ts +68 -0
  134. package/src/result/README.md +4 -0
  135. package/src/result/controller.ts +54 -0
  136. package/src/result/either.ts +193 -0
  137. package/src/result/index.ts +2 -0
  138. package/src/route/README.md +105 -0
  139. package/src/route/adapter/browser.ts +122 -0
  140. package/src/route/adapter/driver.ts +56 -0
  141. package/src/route/adapter/index.ts +2 -0
  142. package/src/route/index.ts +3 -0
  143. package/src/route/router/index.ts +2 -0
  144. package/src/route/router/route.ts +630 -0
  145. package/src/route/router/router.ts +1642 -0
  146. package/src/route/uri/hash.ts +308 -0
  147. package/src/route/uri/index.ts +7 -0
  148. package/src/route/uri/pathname.ts +376 -0
  149. package/src/route/uri/search.ts +413 -0
  150. package/src/socket/README.md +105 -0
  151. package/src/socket/client/index.ts +2 -0
  152. package/src/socket/client/socket-unit.ts +660 -0
  153. package/src/socket/client/socket.ts +203 -0
  154. package/src/socket/common/index.ts +2 -0
  155. package/src/socket/common/socket-unit-common.ts +23 -0
  156. package/src/socket/common/socket-unit-heartbeat.ts +427 -0
  157. package/src/socket/index.ts +3 -0
  158. package/src/socket/server/index.ts +3 -0
  159. package/src/socket/server/server.ts +183 -0
  160. package/src/socket/server/socket-unit.ts +449 -0
  161. package/src/socket/server/socket.ts +264 -0
  162. package/src/storage/table.ts +3 -3
  163. package/src/timer/expiration/expiration-manager.ts +3 -3
  164. package/src/timer/expiration/remaining-manager.ts +3 -3
  165. package/src/tube/README.md +99 -0
  166. package/src/tube/helper.ts +138 -0
  167. package/src/tube/index.ts +2 -0
  168. package/src/tube/tube.ts +880 -0
  169. package/src/weixin/README.md +1 -0
  170. package/src/weixin/index.ts +2 -0
  171. package/src/weixin/official-account/authorization.ts +159 -0
  172. package/src/weixin/official-account/index.ts +2 -0
  173. package/src/weixin/official-account/js-api.ts +134 -0
  174. package/src/weixin/open/index.ts +1 -0
  175. package/src/weixin/open/oauth2.ts +133 -0
  176. package/tests/unit/ai/ai.spec.ts +85 -0
  177. package/tests/unit/aio/content.spec.ts +105 -0
  178. package/tests/unit/aio/json.spec.ts +147 -0
  179. package/tests/unit/aio/prompt.spec.ts +111 -0
  180. package/tests/unit/basic/error.spec.ts +16 -4
  181. package/tests/unit/basic/schedule.spec.ts +74 -0
  182. package/tests/unit/basic/stream.spec.ts +90 -37
  183. package/tests/unit/credential/api-key.spec.ts +37 -0
  184. package/tests/unit/credential/bearer.spec.ts +23 -0
  185. package/tests/unit/credential/json-web-token.spec.ts +23 -0
  186. package/tests/unit/credential/password.spec.ts +41 -0
  187. package/tests/unit/cron/cron.spec.ts +84 -0
  188. package/tests/unit/event/class-event-proxy.spec.ts +3 -3
  189. package/tests/unit/event/event-manager.spec.ts +3 -3
  190. package/tests/unit/event/instance-event-proxy.spec.ts +3 -3
  191. package/tests/unit/form/inputor-controller/base.spec.ts +458 -0
  192. package/tests/unit/form/inputor-controller/boolean.spec.ts +30 -0
  193. package/tests/unit/form/inputor-controller/file.spec.ts +27 -0
  194. package/tests/unit/form/inputor-controller/form.spec.ts +120 -0
  195. package/tests/unit/form/inputor-controller/helper.spec.ts +67 -0
  196. package/tests/unit/form/inputor-controller/multi-select.spec.ts +34 -0
  197. package/tests/unit/form/inputor-controller/number.spec.ts +36 -0
  198. package/tests/unit/form/inputor-controller/select.spec.ts +49 -0
  199. package/tests/unit/form/inputor-controller/text.spec.ts +34 -0
  200. package/tests/unit/http/api/api-core-host.spec.ts +207 -0
  201. package/tests/unit/http/api/api-schema.spec.ts +120 -0
  202. package/tests/unit/http/api/api-server.spec.ts +363 -0
  203. package/tests/unit/http/api/api-test.spec.ts +117 -0
  204. package/tests/unit/http/api/api.spec.ts +121 -0
  205. package/tests/unit/http/api-adapter/node-http.spec.ts +191 -0
  206. package/tests/unit/json/repair.spec.ts +11 -0
  207. package/tests/unit/log/logger.spec.ts +19 -4
  208. package/tests/unit/openai/openai.spec.ts +64 -0
  209. package/tests/unit/orchestration/dispatching/dispatcher.spec.ts +41 -0
  210. package/tests/unit/orchestration/dispatching/selector/down-count-selector.spec.ts +81 -0
  211. package/tests/unit/orchestration/scheduling/scheduler.spec.ts +103 -0
  212. package/tests/unit/random/base.spec.ts +58 -0
  213. package/tests/unit/random/random-boolean.spec.ts +25 -0
  214. package/tests/unit/random/random-integer.spec.ts +32 -0
  215. package/tests/unit/random/random-number.spec.ts +33 -0
  216. package/tests/unit/random/random-string.spec.ts +22 -0
  217. package/tests/unit/request/fetch/browser.spec.ts +222 -0
  218. package/tests/unit/request/fetch/general.spec.ts +43 -0
  219. package/tests/unit/request/fetch/nodejs.spec.ts +225 -0
  220. package/tests/unit/request/request/base.spec.ts +385 -0
  221. package/tests/unit/request/request/general.spec.ts +161 -0
  222. package/tests/unit/route/router/route.spec.ts +431 -0
  223. package/tests/unit/route/router/router.spec.ts +407 -0
  224. package/tests/unit/route/uri/hash.spec.ts +72 -0
  225. package/tests/unit/route/uri/pathname.spec.ts +147 -0
  226. package/tests/unit/route/uri/search.spec.ts +107 -0
  227. package/tests/unit/socket/client.spec.ts +208 -0
  228. package/tests/unit/socket/server.spec.ts +135 -0
  229. package/tests/unit/socket/socket-unit-heartbeat.spec.ts +214 -0
  230. package/tests/unit/tube/helper.spec.ts +139 -0
  231. package/tests/unit/tube/tube.spec.ts +501 -0
  232. package/src/random/string.ts +0 -35
  233. package/tests/unit/random/string.spec.ts +0 -11
@@ -0,0 +1,130 @@
1
+ import { generateUuidV4 } from "#Source/identifier/uuid.ts"
2
+ import type { LoggerFriendly, LoggerFriendlyOptions } from "#Source/log/logger.ts"
3
+ import { Logger } from "#Source/log/logger.ts"
4
+ import { toQueryObject } from "#Source/route/index.ts"
5
+
6
+ import type { AnyApiHost, ApiHostStartResult } from "./api-host.ts"
7
+ import type { AnyApi } from "./api.ts"
8
+
9
+ export interface ApiServerOptions extends LoggerFriendlyOptions {
10
+ apiList: AnyApi[]
11
+ apiHost: AnyApiHost
12
+ }
13
+ /**
14
+ * {@link ApiServer} 包含若干个 {@link Api}(接口),并且需要指定一个监听端口(Port)。Server 启动
15
+ * 后会监听对应的端口,将端口接收到的请求分发给不同的接口。
16
+ *
17
+ * 请求 -> Server -> Api
18
+ *
19
+ * 具体来说,当服务器收到请求时:
20
+ * - 通过请求的路径(path)和方法(method)与所有的 {@link Api} 进行匹配。
21
+ * - 如果能匹配到,将请求分给第一个匹配到的接口。
22
+ * - 如果匹配不到任何接口,将返回 404。
23
+ * - 按照 {@link Api} 的处理逻辑进行处理,最终将结果返回给请求方。
24
+ */
25
+ export class ApiServer implements LoggerFriendly {
26
+ readonly options: ApiServerOptions
27
+
28
+ readonly logger: Logger
29
+
30
+ constructor(options: ApiServerOptions) {
31
+ this.options = options
32
+
33
+ this.logger = Logger.fromOptions(options).setDefaultName("ApiServer")
34
+
35
+ this.initialize()
36
+ }
37
+
38
+ protected initialize(): void {
39
+ const { apiList, apiHost } = this.options
40
+
41
+ apiHost.attachApiCoreHandler(async (apiCore) => {
42
+ const path = await apiCore.getRequestPath()
43
+ const method = (await apiCore.getRequestMethod()).toUpperCase()
44
+
45
+ // tag every request with a unique identifier for better logging
46
+ const logger = Logger
47
+ .derive(this.logger)
48
+ .setDefaultName(`${method} ${path} ${generateUuidV4()}`)
49
+
50
+ try {
51
+ logger.log(`start handling request`)
52
+
53
+ logger.log(`start finding target api`)
54
+ // 根据请求的路径和方法找到对应的接口
55
+ const targetApi = apiList.find((api) => {
56
+ const apiSchema = api.getApiSchema()
57
+ const apiSchemaPath = apiSchema.getPath()
58
+ const apiSchemaMethod = apiSchema.getMethod().toUpperCase()
59
+ return apiSchemaPath === path && apiSchemaMethod === method
60
+ })
61
+
62
+ // 如果找不到对应的接口,抛出错误
63
+ if (targetApi === undefined) {
64
+ logger.error(`finding target api failed`)
65
+ throw new Error(`finding target api failed`)
66
+ }
67
+ else {
68
+ logger.log(`finding target api success`)
69
+ }
70
+
71
+ // 如果找到对应的接口,获取接口定义,按照接口的定义进行处理
72
+ const apiSchema = targetApi.getApiSchema()
73
+
74
+ // 1. 准备 ApiHandlerContext
75
+ logger.log(`start preparing api handler context`)
76
+ const inputSearch = await apiCore.getRequestSearch()
77
+ const inputQueryObject = toQueryObject(inputSearch)
78
+ const inputQuerySchema = apiSchema.getInputQuerySchema()
79
+ const validateResultQuery = await inputQuerySchema["~standard"].validate(inputQueryObject)
80
+ if (validateResultQuery.issues !== undefined) {
81
+ logger.error(`validating api input query failed, issues: ${JSON.stringify(validateResultQuery.issues)}`)
82
+ throw new Error(`validating api input query failed, issues: ${JSON.stringify(validateResultQuery.issues)}`)
83
+ }
84
+ const inputQuery = validateResultQuery.value
85
+
86
+ const inputBodySchema = apiSchema.getInputBodySchema()
87
+ const inputBodyJson = await apiCore.getRequestBodyJson()
88
+ const validateResult = await inputBodySchema["~standard"].validate(inputBodyJson)
89
+ if (validateResult.issues !== undefined) {
90
+ logger.error(`validating api input body failed, issues: ${JSON.stringify(validateResult.issues)}`)
91
+ throw new Error(`validating api input body failed, issues: ${JSON.stringify(validateResult.issues)}`)
92
+ }
93
+ const inputBody = validateResult.value
94
+ logger.log(`preparing api handler context success`)
95
+
96
+ // 2. 执行 ApiHandler
97
+ logger.log(`start executing api handler`)
98
+ const apiHandler = targetApi.getApiHandler()
99
+ const apiResult = await apiHandler({
100
+ ...apiSchema.getApiSchemaOptions(),
101
+ inputQuery,
102
+ inputBody,
103
+ apiCore,
104
+ logger,
105
+ })
106
+ logger.log(`executing api handler success`)
107
+
108
+ // 3. 处理 ApiHandler 的执行结果,这一步会将数据返回给请求方
109
+ logger.log(`start handling api result`)
110
+ await apiResult.handle(apiCore)
111
+ logger.log(`handling api result success`)
112
+
113
+ logger.log(`handling request success`)
114
+ } catch (exception) {
115
+ logger.error(`handling request failed, exception: ${String(exception)}`)
116
+ await apiCore.error()
117
+ }
118
+ })
119
+ }
120
+
121
+ async start(): Promise<ApiHostStartResult> {
122
+ const { apiHost } = this.options
123
+ const result = await apiHost.start()
124
+ return result
125
+ }
126
+ }
127
+
128
+ export const createApiServer = (options: ApiServerOptions): ApiServer => {
129
+ return new ApiServer(options)
130
+ }
@@ -0,0 +1,142 @@
1
+ import type { LoggerFriendly, LoggerFriendlyOptions } from "#Source/log/index.ts"
2
+ import { Logger } from "#Source/log/index.ts"
3
+
4
+ import type { AnyApiSchema } from "./api-schema.ts"
5
+
6
+ export interface BeforeApiTestHandlerContext<
7
+ ApiSchema extends AnyApiSchema,
8
+ > {
9
+ apiSchema: ApiSchema
10
+ }
11
+ export type BeforeApiTestHandler<
12
+ ApiSchema extends AnyApiSchema,
13
+ BeforeApiTestResult,
14
+ > = (context: BeforeApiTestHandlerContext<ApiSchema>) => Promise<BeforeApiTestResult>
15
+ export interface ApiTestHandlerContext<
16
+ ApiSchema extends AnyApiSchema,
17
+ BeforeApiTestHandlerResult,
18
+ > {
19
+ apiSchema: ApiSchema
20
+ beforeApiTestResult: BeforeApiTestHandlerResult
21
+ }
22
+ export type ApiTestHandler<
23
+ ApiSchema extends AnyApiSchema,
24
+ BeforeApiTestResult,
25
+ ApiTestResult,
26
+ > = (context: ApiTestHandlerContext<ApiSchema, BeforeApiTestResult>) => Promise<ApiTestResult>
27
+ export interface AfterApiTestContext<
28
+ ApiSchema extends AnyApiSchema,
29
+ BeforeApiTestResult,
30
+ ApiTestResult,
31
+ > {
32
+ apiSchema: ApiSchema
33
+ beforeApiTestResult: BeforeApiTestResult
34
+ apiTestResult: ApiTestResult
35
+ }
36
+ export type AfterApiTestHandler<
37
+ ApiSchema extends AnyApiSchema,
38
+ BeforeApiTestResult,
39
+ ApiTestResult,
40
+ AfterApiTestResult,
41
+ > = (context: AfterApiTestContext<ApiSchema, BeforeApiTestResult, ApiTestResult>) => Promise<AfterApiTestResult>
42
+
43
+ export interface ApiTestOptions<
44
+ ApiSchema extends AnyApiSchema,
45
+ BeforeApiTestResult,
46
+ ApiTestResult,
47
+ AfterApiTestResult,
48
+ > extends LoggerFriendlyOptions {
49
+ apiSchema: ApiSchema
50
+ beforeApiTestHandler: BeforeApiTestHandler<ApiSchema, BeforeApiTestResult>
51
+ apiTestHandler: ApiTestHandler<ApiSchema, BeforeApiTestResult, ApiTestResult>
52
+ afterApiTestHandler: AfterApiTestHandler<ApiSchema, BeforeApiTestResult, ApiTestResult, AfterApiTestResult>
53
+ }
54
+ export class ApiTest<
55
+ ApiSchema extends AnyApiSchema,
56
+ BeforeApiTestResult,
57
+ ApiTestResult,
58
+ AfterApiTestResult,
59
+ > implements LoggerFriendly {
60
+ readonly logger: Logger
61
+
62
+ protected __ApiSchemaPhantomType__?: ApiSchema
63
+ protected __BeforeApiTestResultPhantomType__?: BeforeApiTestResult
64
+ protected __ApiTestResultPhantomType__?: ApiTestResult
65
+ protected __AfterApiTestResultPhantomType__?: AfterApiTestResult
66
+
67
+ private apiSchema: ApiSchema
68
+ private beforeApiTestHandler: BeforeApiTestHandler<ApiSchema, BeforeApiTestResult>
69
+ private apiTestHandler: ApiTestHandler<ApiSchema, BeforeApiTestResult, ApiTestResult>
70
+ private afterApiTestHandler: AfterApiTestHandler<ApiSchema, BeforeApiTestResult, ApiTestResult, AfterApiTestResult>
71
+
72
+ constructor(options: ApiTestOptions<ApiSchema, BeforeApiTestResult, ApiTestResult, AfterApiTestResult>) {
73
+ this.logger = Logger.fromOptions(options).setDefaultName("ApiTest")
74
+
75
+ this.apiSchema = options.apiSchema
76
+ this.beforeApiTestHandler = options.beforeApiTestHandler
77
+ this.apiTestHandler = options.apiTestHandler
78
+ this.afterApiTestHandler = options.afterApiTestHandler
79
+ }
80
+
81
+ getApiSchema(): ApiSchema {
82
+ return this.apiSchema
83
+ }
84
+
85
+ getBeforeApiTestHandler(): BeforeApiTestHandler<ApiSchema, BeforeApiTestResult> {
86
+ return this.beforeApiTestHandler
87
+ }
88
+
89
+ getApiTestHandler(): ApiTestHandler<ApiSchema, BeforeApiTestResult, ApiTestResult> {
90
+ return this.apiTestHandler
91
+ }
92
+
93
+ getAfterApiTestHandler(): AfterApiTestHandler<ApiSchema, BeforeApiTestResult, ApiTestResult, AfterApiTestResult> {
94
+ return this.afterApiTestHandler
95
+ }
96
+ }
97
+
98
+ export const createApiTest = <
99
+ ApiSchema extends AnyApiSchema,
100
+ BeforeApiTestResult,
101
+ ApiTestResult,
102
+ AfterApiTestResult,
103
+ >(
104
+ options: ApiTestOptions<ApiSchema, BeforeApiTestResult, ApiTestResult, AfterApiTestResult>,
105
+ ): ApiTest<ApiSchema, BeforeApiTestResult, ApiTestResult, AfterApiTestResult> => {
106
+ return new ApiTest(options)
107
+ }
108
+
109
+ // oxlint-disable-next-line typescript/no-explicit-any
110
+ export type AnyApiTest = ApiTest<any, any, any, any>
111
+
112
+ export interface TestApiOptions<
113
+ ApiSchema extends AnyApiSchema,
114
+ BeforeApiTestResult,
115
+ ApiTestResult,
116
+ AfterApiTestResult,
117
+ > {
118
+ apiTest: ApiTest<ApiSchema, BeforeApiTestResult, ApiTestResult, AfterApiTestResult>
119
+ }
120
+ export const testApi = async <
121
+ ApiSchema extends AnyApiSchema,
122
+ BeforeApiTestResult,
123
+ ApiTestResult,
124
+ AfterApiTestResult,
125
+ >(
126
+ options: TestApiOptions<ApiSchema, BeforeApiTestResult, ApiTestResult, AfterApiTestResult>,
127
+ ): Promise<AfterApiTestResult> => {
128
+ const { apiTest } = options
129
+
130
+ const apiSchema = apiTest.getApiSchema()
131
+
132
+ const beforeApiTestHandler = apiTest.getBeforeApiTestHandler()
133
+ const beforeApiTestResult = await beforeApiTestHandler({ apiSchema })
134
+
135
+ const apiTestHandler = apiTest.getApiTestHandler()
136
+ const apiTestResult = await apiTestHandler({ apiSchema, beforeApiTestResult })
137
+
138
+ const afterApiTestHandler = apiTest.getAfterApiTestHandler()
139
+ const afterApiTestResult = await afterApiTestHandler({ apiSchema, beforeApiTestResult, apiTestResult })
140
+
141
+ return afterApiTestResult
142
+ }
@@ -0,0 +1,37 @@
1
+ import type { StandardSchemaV1 } from "@standard-schema/spec"
2
+
3
+ import type { AnyApiSchema, ApiSchema } from './api-schema.ts';
4
+
5
+ export type ApiType<Target extends AnyApiSchema> = Target extends ApiSchema<
6
+ infer Path,
7
+ infer Method,
8
+ infer InputQuerySchema,
9
+ infer InputBodySchema,
10
+ infer OutputSuccessSchema,
11
+ infer OutputErrorSchema
12
+ > ? {
13
+ path: Path
14
+ method: Method
15
+ inputQuery: StandardSchemaV1.InferOutput<InputQuerySchema>
16
+ inputBody: StandardSchemaV1.InferOutput<InputBodySchema>
17
+ outputSuccess: StandardSchemaV1.InferOutput<OutputSuccessSchema>
18
+ outputError: StandardSchemaV1.InferOutput<OutputErrorSchema>
19
+ } : never
20
+ export const apiType = <const Target extends AnyApiSchema>(apiSchema: Target): ApiType<Target> => {
21
+ // oxlint-disable-next-line typescript/no-unsafe-type-assertion
22
+ return apiSchema as unknown as ApiType<Target>
23
+ }
24
+
25
+ export type ApiTypesInTuple<Target extends readonly AnyApiSchema[]> = {
26
+ [Index in keyof Target]: ApiType<Target[Index]>
27
+ }
28
+ export const apiTypesInTuple = <const Target extends readonly AnyApiSchema[]>(apiSchemas: Target): ApiTypesInTuple<Target> => {
29
+ // oxlint-disable-next-line typescript/no-unsafe-type-assertion
30
+ return apiSchemas.map(apiSchema => apiType(apiSchema)) as unknown as ApiTypesInTuple<Target>
31
+ }
32
+
33
+ export type ApiTypesInUnion<Target extends readonly AnyApiSchema[]> = ApiType<Target[number]>
34
+ export const apiTypesInUnion = <const Target extends readonly AnyApiSchema[]>(apiSchemas: Target): ApiTypesInUnion<Target> => {
35
+ // oxlint-disable-next-line typescript/no-unsafe-type-assertion
36
+ return apiSchemas.map(apiSchema => apiType(apiSchema)) as unknown as ApiTypesInUnion<Target>
37
+ }
@@ -0,0 +1,81 @@
1
+ import type { StandardSchemaV1 } from "@standard-schema/spec"
2
+
3
+ import type { LoggerFriendly, LoggerFriendlyOptions } from "#Source/log/index.ts"
4
+ import { Logger } from "#Source/log/index.ts"
5
+
6
+ import type { ApiSchema } from "./api-schema.ts"
7
+ import type { ApiHandler } from "./api-handler.ts"
8
+
9
+ export interface ApiOptions<
10
+ Path extends string,
11
+ Method extends "get" | "post",
12
+ InputQuerySchema extends StandardSchemaV1,
13
+ InputBodySchema extends StandardSchemaV1,
14
+ OutputSuccessSchema extends StandardSchemaV1,
15
+ OutputErrorSchema extends StandardSchemaV1,
16
+ > extends LoggerFriendlyOptions {
17
+ apiSchema: ApiSchema<Path, Method, InputQuerySchema, InputBodySchema, OutputSuccessSchema, OutputErrorSchema>
18
+ apiHandler: ApiHandler<Path, Method, InputQuerySchema, InputBodySchema, OutputSuccessSchema, OutputErrorSchema>
19
+ }
20
+ /**
21
+ * Api 应该严格遵循 {@link ApiSchema} 的描述进行实现。
22
+ *
23
+ * 当一个请求被 {@link ApiServer} 按照路径(Path)和方法(Method)匹配到一个接口后,
24
+ * 便会开始按照 {@link Api} 的逻辑对请求进行处理。
25
+ *
26
+ * Api 的整个处理流程如下:
27
+ *
28
+ * 请求 -> 构造上下文 -> 处理器(Api Runner) -> 返回结果(Result)
29
+ *
30
+ * 其中:
31
+ * - 构造上下文包括:
32
+ * - 从处理原始请求并从中提取输入数据,主要来自 query 和 body。
33
+ * - 校验输入数据,确保输入数据符合 {@link ApiSchema} 的描述。
34
+ * - 处理器是 Api 的核心逻辑,负责对请求进行处理,可以包含副作用,比如查询数据库、调用其他服务等。
35
+ * - 一般情况下,处理器不应该直接返回数据给前端,而应该通过 {@link SuccessResult} 或 {@link ErrorResult}
36
+ * 定义自己想要返回什么,然后 {@link ApiServer} 会负责执行返回逻辑,将结果返回给前端,这样做既可以使 Api 的
37
+ * 核心逻辑更纯粹,也可以保证所有的接口始终给前端返回一致的数据结构。
38
+ */
39
+ export class Api<
40
+ Path extends string,
41
+ Method extends "get" | "post",
42
+ InputQuerySchema extends StandardSchemaV1,
43
+ InputBodySchema extends StandardSchemaV1,
44
+ OutputSuccessSchema extends StandardSchemaV1,
45
+ OutputErrorSchema extends StandardSchemaV1,
46
+ > implements LoggerFriendly {
47
+ readonly logger: Logger
48
+
49
+ private readonly apiSchema: ApiSchema<Path, Method, InputQuerySchema, InputBodySchema, OutputSuccessSchema, OutputErrorSchema>
50
+ private readonly apiHandler: ApiHandler<Path, Method, InputQuerySchema, InputBodySchema, OutputSuccessSchema, OutputErrorSchema>
51
+
52
+ constructor(options: ApiOptions<Path, Method, InputQuerySchema, InputBodySchema, OutputSuccessSchema, OutputErrorSchema>) {
53
+ this.logger = Logger.fromOptions(options).setDefaultName("Api")
54
+
55
+ this.apiSchema = options.apiSchema
56
+ this.apiHandler = options.apiHandler
57
+ }
58
+
59
+ getApiSchema(): ApiSchema<Path, Method, InputQuerySchema, InputBodySchema, OutputSuccessSchema, OutputErrorSchema> {
60
+ return this.apiSchema
61
+ }
62
+
63
+ getApiHandler(): ApiHandler<Path, Method, InputQuerySchema, InputBodySchema, OutputSuccessSchema, OutputErrorSchema> {
64
+ return this.apiHandler
65
+ }
66
+ }
67
+
68
+ export const createApi = <
69
+ Path extends string,
70
+ Method extends "get" | "post",
71
+ InputQuerySchema extends StandardSchemaV1,
72
+ InputBodySchema extends StandardSchemaV1,
73
+ OutputSuccessSchema extends StandardSchemaV1,
74
+ OutputErrorSchema extends StandardSchemaV1,
75
+ >(
76
+ options: ApiOptions<Path, Method, InputQuerySchema, InputBodySchema, OutputSuccessSchema, OutputErrorSchema>,
77
+ ): Api<Path, Method, InputQuerySchema, InputBodySchema, OutputSuccessSchema, OutputErrorSchema> => {
78
+ return new Api(options)
79
+ }
80
+
81
+ export type AnyApi = Api<string, "get" | "post", StandardSchemaV1, StandardSchemaV1, StandardSchemaV1, StandardSchemaV1>
@@ -0,0 +1,11 @@
1
+ export * from "./api-core.ts"
2
+ export * from "./api-host.ts"
3
+
4
+ export * from "./api-result.ts"
5
+ export * from "./api-schema.ts"
6
+ export * from "./api-type.ts"
7
+ export type * from "./api-handler.ts"
8
+ export * from "./api.ts"
9
+
10
+ export * from "./api-test.ts"
11
+ export * from "./api-server.ts"
@@ -0,0 +1,260 @@
1
+ import type { IncomingMessage, ServerResponse } from "node:http"
2
+
3
+ import type {
4
+ ApiCoreHeaders,
5
+ ApiCoreRequestFormData,
6
+ ApiCoreStream,
7
+ } from "../api/api-core.ts"
8
+ import { ApiCore } from "../api/api-core.ts"
9
+
10
+ export class NodeHttpApiCoreStream implements ApiCoreStream {
11
+ protected readonly response: ServerResponse<IncomingMessage>
12
+
13
+ constructor(response: ServerResponse<IncomingMessage>) {
14
+ this.response = response
15
+ }
16
+
17
+ async write(chunk: Buffer | string, encoding: BufferEncoding): Promise<void> {
18
+ await new Promise<void>((resolve, reject) => {
19
+ this.response.write(chunk, encoding, (error) => {
20
+ if (error !== undefined) {
21
+ reject(error instanceof Error ? error : new Error(String(error)))
22
+ return
23
+ }
24
+
25
+ resolve()
26
+ })
27
+ })
28
+ }
29
+
30
+ async end(): Promise<void> {
31
+ await new Promise<void>((resolve) => {
32
+ this.response.end(() => {
33
+ resolve()
34
+ })
35
+ })
36
+ }
37
+ }
38
+
39
+ export interface ApiCoreNodeHttpOptions {
40
+ request: IncomingMessage
41
+ response: ServerResponse<IncomingMessage>
42
+ }
43
+ export class ApiCoreNodeHttp extends ApiCore {
44
+ protected readonly request: IncomingMessage
45
+ protected readonly response: ServerResponse<IncomingMessage>
46
+
47
+ protected requestUrl: URL | undefined
48
+ protected bodyBufferTask: Promise<Buffer> | undefined
49
+ protected bodyTextTask: Promise<string> | undefined
50
+ protected bodyFormDataTask: Promise<ApiCoreRequestFormData> | undefined
51
+
52
+ constructor(options: ApiCoreNodeHttpOptions) {
53
+ super()
54
+
55
+ this.request = options.request
56
+ this.response = options.response
57
+
58
+ this.bodyBufferTask = undefined
59
+ this.bodyTextTask = undefined
60
+ this.bodyFormDataTask = undefined
61
+ this.requestUrl = undefined
62
+ }
63
+
64
+ async getRequestHeaders(): Promise<ApiCoreHeaders> {
65
+ const headers: ApiCoreHeaders = {}
66
+
67
+ for (const [name, rawValue] of Object.entries(this.request.headers)) {
68
+ if (Array.isArray(rawValue)) {
69
+ const [firstValue, ...restValues] = rawValue
70
+
71
+ if (firstValue === undefined) {
72
+ headers[name] = undefined
73
+ continue
74
+ }
75
+
76
+ headers[name] = [firstValue, ...restValues]
77
+ continue
78
+ }
79
+
80
+ headers[name] = rawValue === undefined ? undefined : [rawValue]
81
+ }
82
+
83
+ return await Promise.resolve(headers)
84
+ }
85
+
86
+ getRequestUrl(): string {
87
+ let requestUrl = this.requestUrl
88
+
89
+ if (requestUrl === undefined) {
90
+ const partialUrl = this.request.url ?? "/"
91
+ const host = this.request.headers.host
92
+
93
+ if (host === undefined || host.length === 0) {
94
+ requestUrl = new URL(partialUrl, "http://127.0.0.1")
95
+ } else {
96
+ requestUrl = new URL(partialUrl, `http://${host}`)
97
+ }
98
+ this.requestUrl = requestUrl
99
+ }
100
+
101
+ return requestUrl.href
102
+ }
103
+
104
+ async getRequestMethod(): Promise<string> {
105
+ const method = this.request.method ?? "GET"
106
+ return await Promise.resolve(method)
107
+ }
108
+
109
+ protected async getRequestBodyBuffer(): Promise<Buffer> {
110
+ if (this.bodyBufferTask !== undefined) {
111
+ return await this.bodyBufferTask
112
+ }
113
+
114
+ this.bodyBufferTask = new Promise<Buffer>((resolve, reject) => {
115
+ const chunks: Buffer[] = []
116
+
117
+ this.request.on("data", (chunk: Buffer | string) => {
118
+ if (typeof chunk === "string") {
119
+ chunks.push(Buffer.from(chunk))
120
+ return
121
+ }
122
+
123
+ chunks.push(chunk)
124
+ })
125
+
126
+ this.request.once("aborted", () => {
127
+ reject(new Error("Request body was aborted."))
128
+ })
129
+
130
+ this.request.once("error", (error) => {
131
+ reject(error)
132
+ })
133
+
134
+ this.request.once("end", () => {
135
+ resolve(Buffer.concat(chunks))
136
+ })
137
+ })
138
+
139
+ return await this.bodyBufferTask
140
+ }
141
+
142
+ async getRequestBodyText(): Promise<string> {
143
+ if (this.bodyTextTask !== undefined) {
144
+ return await this.bodyTextTask
145
+ }
146
+
147
+ this.bodyTextTask = this.getRequestBodyBuffer().then((bodyBuffer) => {
148
+ return bodyBuffer.toString("utf8")
149
+ })
150
+
151
+ return await this.bodyTextTask
152
+ }
153
+
154
+ async getRequestBodyFormData(): Promise<ApiCoreRequestFormData> {
155
+ if (this.bodyFormDataTask !== undefined) {
156
+ return await this.bodyFormDataTask
157
+ }
158
+
159
+ this.bodyFormDataTask = (async () => {
160
+ const contentType = this.request.headers["content-type"]
161
+
162
+ if (
163
+ contentType === undefined
164
+ || /^(application\/x-www-form-urlencoded|multipart\/form-data)\b/i.test(contentType) === false
165
+ ) {
166
+ return []
167
+ }
168
+
169
+ const bodyBuffer = await this.getRequestBodyBuffer()
170
+ const requestHeaders = Object.entries(await this.getRequestHeaders()).flatMap(([name, value]) => {
171
+ if (value === undefined) {
172
+ return []
173
+ }
174
+
175
+ return value.map((item) => {
176
+ return [name, item] as [string, string]
177
+ })
178
+ })
179
+ const request = new Request(this.getRequestUrl(), {
180
+ method: await this.getRequestMethod(),
181
+ headers: requestHeaders,
182
+ body: new Uint8Array(bodyBuffer),
183
+ })
184
+ const formData = await request.formData()
185
+
186
+ return Array.from(formData.entries()).map(([name, value]) => {
187
+ return {
188
+ name,
189
+ value,
190
+ }
191
+ })
192
+ })()
193
+
194
+ return await this.bodyFormDataTask
195
+ }
196
+
197
+ protected prepareResponse(contentType: string, statusCode: number = 200): void {
198
+ if (this.response.writableEnded === true) {
199
+ return
200
+ }
201
+
202
+ if (this.response.headersSent === false) {
203
+ this.response.statusCode = statusCode
204
+ this.response.setHeader("Content-Type", contentType)
205
+ }
206
+ }
207
+
208
+ async text(data: unknown): Promise<void> {
209
+ this.prepareResponse("text/plain; charset=utf-8")
210
+
211
+ await new Promise<void>((resolve) => {
212
+ this.response.end(String(data), () => {
213
+ resolve()
214
+ })
215
+ })
216
+ }
217
+
218
+ async json(data: unknown): Promise<void> {
219
+ this.prepareResponse("application/json; charset=utf-8")
220
+
221
+ await new Promise<void>((resolve) => {
222
+ this.response.end(JSON.stringify(data), () => {
223
+ resolve()
224
+ })
225
+ })
226
+ }
227
+
228
+ async stream(): Promise<ApiCoreStream> {
229
+ this.prepareResponse("text/event-stream; charset=utf-8")
230
+ this.response.setHeader("Cache-Control", "no-cache")
231
+ this.response.setHeader("Connection", "keep-alive")
232
+ this.response.flushHeaders()
233
+
234
+ const stream = new NodeHttpApiCoreStream(this.response)
235
+ return await Promise.resolve(stream)
236
+ }
237
+
238
+ async error(): Promise<void> {
239
+ if (this.response.writableEnded === true) {
240
+ return await Promise.resolve()
241
+ }
242
+
243
+ this.prepareResponse("application/json; charset=utf-8", 500)
244
+
245
+ await new Promise<void>((resolve) => {
246
+ this.response.end(JSON.stringify({
247
+ status: "error",
248
+ data: {
249
+ reason: "internal_server_error",
250
+ },
251
+ }), () => {
252
+ resolve()
253
+ })
254
+ })
255
+ }
256
+ }
257
+
258
+ export const createApiCoreNodeHttp = (options: ApiCoreNodeHttpOptions): ApiCoreNodeHttp => {
259
+ return new ApiCoreNodeHttp(options)
260
+ }