@taicode/common-server 3.8.0 → 4.0.0-beta.10

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 (218) hide show
  1. package/output/app/index.d.ts +18 -15
  2. package/output/app/index.d.ts.map +1 -1
  3. package/output/app/index.js +330 -9
  4. package/output/auth/index.d.ts +73 -0
  5. package/output/auth/index.d.ts.map +1 -0
  6. package/output/auth/index.js +196 -0
  7. package/output/core/assemble.d.ts +64 -0
  8. package/output/core/assemble.d.ts.map +1 -0
  9. package/output/core/assemble.js +88 -0
  10. package/output/core/boot/context.d.ts +109 -0
  11. package/output/core/boot/context.d.ts.map +1 -0
  12. package/output/core/boot/context.js +145 -0
  13. package/output/core/boot/lifecycle.d.ts +206 -0
  14. package/output/core/boot/lifecycle.d.ts.map +1 -0
  15. package/output/core/boot/lifecycle.js +255 -0
  16. package/output/core/boot/shutdown.d.ts +34 -0
  17. package/output/core/boot/shutdown.d.ts.map +1 -0
  18. package/output/core/boot/shutdown.js +50 -0
  19. package/output/core/boot/signal.d.ts +13 -0
  20. package/output/core/boot/signal.d.ts.map +1 -0
  21. package/output/{app → core/boot}/signal.js +5 -4
  22. package/output/core/boot/steps.d.ts +138 -0
  23. package/output/core/boot/steps.d.ts.map +1 -0
  24. package/output/core/boot/steps.js +259 -0
  25. package/output/core/entries.d.ts +151 -0
  26. package/output/core/entries.d.ts.map +1 -0
  27. package/output/core/entries.js +242 -0
  28. package/output/core/entries.test.d.ts +2 -0
  29. package/output/core/entries.test.d.ts.map +1 -0
  30. package/output/core/entries.test.js +458 -0
  31. package/output/core/env.d.ts +83 -0
  32. package/output/core/env.d.ts.map +1 -0
  33. package/output/core/env.js +124 -0
  34. package/output/{app → core}/env.test.d.ts.map +1 -1
  35. package/output/core/env.test.js +105 -0
  36. package/output/core/modules/auth-provider/index.d.ts +10 -0
  37. package/output/core/modules/auth-provider/index.d.ts.map +1 -0
  38. package/output/core/modules/auth-provider/index.js +9 -0
  39. package/output/core/modules/auth-provider/types.d.ts +71 -0
  40. package/output/core/modules/auth-provider/types.d.ts.map +1 -0
  41. package/output/core/modules/context/index.d.ts +18 -0
  42. package/output/core/modules/context/index.d.ts.map +1 -0
  43. package/output/core/modules/context/index.js +23 -0
  44. package/output/core/modules/context/index.test.d.ts.map +1 -0
  45. package/output/core/modules/cookie/index.d.ts +21 -0
  46. package/output/core/modules/cookie/index.d.ts.map +1 -0
  47. package/output/core/modules/cookie/index.js +17 -0
  48. package/output/core/modules/identity/index.d.ts +4 -0
  49. package/output/core/modules/identity/index.d.ts.map +1 -0
  50. package/output/core/modules/identity/index.js +14 -0
  51. package/output/core/modules/logging/index.d.ts +5 -0
  52. package/output/core/modules/logging/index.d.ts.map +1 -0
  53. package/output/core/modules/logging/index.js +29 -0
  54. package/output/core/modules/openapi/index.d.ts +7 -0
  55. package/output/core/modules/openapi/index.d.ts.map +1 -0
  56. package/output/core/modules/openapi/index.js +41 -0
  57. package/output/core/modules/permission-operations/index.d.ts +26 -0
  58. package/output/core/modules/permission-operations/index.d.ts.map +1 -0
  59. package/output/core/modules/permission-operations/index.js +36 -0
  60. package/output/core/modules/permission-operations/index.test.d.ts +2 -0
  61. package/output/core/modules/permission-operations/index.test.d.ts.map +1 -0
  62. package/output/core/modules/permission-operations/index.test.js +45 -0
  63. package/output/core/modules/request-id/index.d.ts +33 -0
  64. package/output/core/modules/request-id/index.d.ts.map +1 -0
  65. package/output/core/modules/request-id/index.js +66 -0
  66. package/output/core/modules/response/index.d.ts +50 -0
  67. package/output/core/modules/response/index.d.ts.map +1 -0
  68. package/output/core/modules/response/index.js +102 -0
  69. package/output/{schema/index.d.ts → core/modules/schema/builders.d.ts} +8 -1
  70. package/output/core/modules/schema/builders.d.ts.map +1 -0
  71. package/output/{schema/index.js → core/modules/schema/builders.js} +7 -4
  72. package/output/core/modules/schema/index.d.ts +34 -0
  73. package/output/core/modules/schema/index.d.ts.map +1 -0
  74. package/output/core/modules/schema/index.js +62 -0
  75. package/output/core/modules/schema/index.test.d.ts +2 -0
  76. package/output/core/modules/schema/index.test.d.ts.map +1 -0
  77. package/output/core/modules/schema/index.test.js +55 -0
  78. package/output/core/modules/system-routes/index.d.ts +15 -0
  79. package/output/core/modules/system-routes/index.d.ts.map +1 -0
  80. package/output/core/modules/system-routes/index.js +205 -0
  81. package/output/core/resource.d.ts +56 -0
  82. package/output/core/resource.d.ts.map +1 -0
  83. package/output/core/resource.js +56 -0
  84. package/output/core/test-support/database.d.ts +89 -0
  85. package/output/core/test-support/database.d.ts.map +1 -0
  86. package/output/core/test-support/database.js +131 -0
  87. package/output/core/test-support/database.test.d.ts.map +1 -0
  88. package/output/core/test-support/database.test.js +111 -0
  89. package/output/core/test-support/environment.d.ts +38 -0
  90. package/output/core/test-support/environment.d.ts.map +1 -0
  91. package/output/core/test-support/environment.js +45 -0
  92. package/output/core/test-support/service-context.d.ts +64 -0
  93. package/output/core/test-support/service-context.d.ts.map +1 -0
  94. package/output/core/test-support/service-context.js +63 -0
  95. package/output/core/test-support/service-context.test.d.ts +2 -0
  96. package/output/core/test-support/service-context.test.d.ts.map +1 -0
  97. package/output/core/test-support/service-context.test.js +55 -0
  98. package/output/core/test-support/test-server.d.ts +154 -0
  99. package/output/core/test-support/test-server.d.ts.map +1 -0
  100. package/output/core/test-support/test-server.js +114 -0
  101. package/output/core/test-support/test-server.test.d.ts +2 -0
  102. package/output/core/test-support/test-server.test.d.ts.map +1 -0
  103. package/output/core/test-support/test-server.test.js +296 -0
  104. package/output/core/test-support/testdata.d.ts +255 -0
  105. package/output/core/test-support/testdata.d.ts.map +1 -0
  106. package/output/core/test-support/testdata.js +271 -0
  107. package/output/core/test-support/testdata.test.d.ts +2 -0
  108. package/output/core/test-support/testdata.test.d.ts.map +1 -0
  109. package/output/core/test-support/testdata.test.js +334 -0
  110. package/output/core/type-augment.d.ts +34 -0
  111. package/output/core/type-augment.d.ts.map +1 -0
  112. package/output/core/type-augment.js +7 -0
  113. package/output/core/types.d.ts +582 -0
  114. package/output/core/types.d.ts.map +1 -0
  115. package/output/core/types.js +2 -0
  116. package/output/cors/index.d.ts +3 -0
  117. package/output/cors/index.d.ts.map +1 -0
  118. package/output/cors/index.js +28 -0
  119. package/output/index.d.ts +10 -4
  120. package/output/index.d.ts.map +1 -1
  121. package/output/index.js +25 -5
  122. package/output/rate-limit/index.d.ts +23 -0
  123. package/output/rate-limit/index.d.ts.map +1 -0
  124. package/output/rate-limit/index.js +43 -0
  125. package/output/{rate-limiter/rate-limiter.d.ts → rate-limit/limiter.d.ts} +1 -1
  126. package/output/rate-limit/limiter.d.ts.map +1 -0
  127. package/output/{rate-limiter → rate-limit}/memory-store.d.ts +6 -0
  128. package/output/rate-limit/memory-store.d.ts.map +1 -0
  129. package/output/{rate-limiter → rate-limit}/memory-store.js +8 -0
  130. package/output/{rate-limiter → rate-limit}/redis-store.d.ts +18 -0
  131. package/output/rate-limit/redis-store.d.ts.map +1 -0
  132. package/output/{rate-limiter → rate-limit}/redis-store.js +35 -0
  133. package/output/rate-limit/redis-store.test.d.ts.map +1 -0
  134. package/output/{rate-limiter → rate-limit}/redis-store.test.js +2 -2
  135. package/output/rate-limit/types.d.ts +45 -0
  136. package/output/rate-limit/types.d.ts.map +1 -0
  137. package/output/{redis-cache/redis-cache.d.ts → redis/cache.d.ts} +1 -1
  138. package/output/redis/cache.d.ts.map +1 -0
  139. package/output/redis/cache.test.d.ts +2 -0
  140. package/output/redis/cache.test.d.ts.map +1 -0
  141. package/output/{redis-cache/redis-cache.test.js → redis/cache.test.js} +1 -1
  142. package/output/redis/index.d.ts +50 -0
  143. package/output/redis/index.d.ts.map +1 -0
  144. package/output/redis/index.js +71 -0
  145. package/output/redis/index.test.d.ts +2 -0
  146. package/output/{context → redis}/index.test.d.ts.map +1 -1
  147. package/output/redis/index.test.js +55 -0
  148. package/output/redis-queue/index.js +1 -1
  149. package/output/redis-queue/test-helpers.d.ts +1 -1
  150. package/output/redis-queue/test-helpers.js +1 -1
  151. package/output/redis-queue/types.d.ts +3 -12
  152. package/output/redis-queue/types.d.ts.map +1 -1
  153. package/package.json +11 -7
  154. package/output/app/assemble.d.ts +0 -16
  155. package/output/app/assemble.d.ts.map +0 -1
  156. package/output/app/assemble.js +0 -124
  157. package/output/app/database.d.ts +0 -36
  158. package/output/app/database.d.ts.map +0 -1
  159. package/output/app/database.js +0 -40
  160. package/output/app/database.test.d.ts.map +0 -1
  161. package/output/app/database.test.js +0 -66
  162. package/output/app/default-modules.d.ts +0 -23
  163. package/output/app/default-modules.d.ts.map +0 -1
  164. package/output/app/default-modules.js +0 -40
  165. package/output/app/define-server.d.ts +0 -12
  166. package/output/app/define-server.d.ts.map +0 -1
  167. package/output/app/define-server.js +0 -289
  168. package/output/app/define-server.test.d.ts +0 -2
  169. package/output/app/define-server.test.d.ts.map +0 -1
  170. package/output/app/define-server.test.js +0 -299
  171. package/output/app/env.d.ts +0 -24
  172. package/output/app/env.d.ts.map +0 -1
  173. package/output/app/env.js +0 -38
  174. package/output/app/env.test.js +0 -78
  175. package/output/app/module.d.ts +0 -88
  176. package/output/app/module.d.ts.map +0 -1
  177. package/output/app/module.js +0 -34
  178. package/output/app/shutdown.d.ts +0 -9
  179. package/output/app/shutdown.d.ts.map +0 -1
  180. package/output/app/shutdown.js +0 -15
  181. package/output/app/shutdown.test.d.ts +0 -2
  182. package/output/app/shutdown.test.d.ts.map +0 -1
  183. package/output/app/shutdown.test.js +0 -149
  184. package/output/app/signal.d.ts +0 -12
  185. package/output/app/signal.d.ts.map +0 -1
  186. package/output/app/steps.d.ts +0 -17
  187. package/output/app/steps.d.ts.map +0 -1
  188. package/output/app/steps.js +0 -41
  189. package/output/app/types.d.ts +0 -149
  190. package/output/app/types.d.ts.map +0 -1
  191. package/output/context/index.d.ts +0 -6
  192. package/output/context/index.d.ts.map +0 -1
  193. package/output/context/index.js +0 -14
  194. package/output/rate-limiter/index.d.ts +0 -5
  195. package/output/rate-limiter/index.d.ts.map +0 -1
  196. package/output/rate-limiter/index.js +0 -4
  197. package/output/rate-limiter/memory-store.d.ts.map +0 -1
  198. package/output/rate-limiter/rate-limiter.d.ts.map +0 -1
  199. package/output/rate-limiter/redis-store.d.ts.map +0 -1
  200. package/output/rate-limiter/redis-store.test.d.ts.map +0 -1
  201. package/output/rate-limiter/types.d.ts +0 -27
  202. package/output/rate-limiter/types.d.ts.map +0 -1
  203. package/output/redis-cache/index.d.ts +0 -4
  204. package/output/redis-cache/index.d.ts.map +0 -1
  205. package/output/redis-cache/index.js +0 -1
  206. package/output/redis-cache/redis-cache.d.ts.map +0 -1
  207. package/output/redis-cache/redis-cache.test.d.ts +0 -2
  208. package/output/redis-cache/redis-cache.test.d.ts.map +0 -1
  209. package/output/schema/index.d.ts.map +0 -1
  210. /package/output/{app → core}/env.test.d.ts +0 -0
  211. /package/output/{app → core/modules/auth-provider}/types.js +0 -0
  212. /package/output/{context → core/modules/context}/index.test.d.ts +0 -0
  213. /package/output/{context → core/modules/context}/index.test.js +0 -0
  214. /package/output/{app → core/test-support}/database.test.d.ts +0 -0
  215. /package/output/{rate-limiter/rate-limiter.js → rate-limit/limiter.js} +0 -0
  216. /package/output/{rate-limiter → rate-limit}/redis-store.test.d.ts +0 -0
  217. /package/output/{rate-limiter → rate-limit}/types.js +0 -0
  218. /package/output/{redis-cache/redis-cache.js → redis/cache.js} +0 -0
@@ -0,0 +1,66 @@
1
+ import { randomUUID } from 'node:crypto';
2
+ /**
3
+ * core `requestId`(设计 §5.1 / §8)——**框架内置、锁定、不可替换/修改**。
4
+ *
5
+ * 入站 header(固定 `x-request-id`)存在且**非空** → 复用,否则用 `randomUUID` 写入 `request.id`;
6
+ * **响应统一回吐 `reply.header('x-request-id', request.id)`**(进日志 + 进响应 header,
7
+ * **不进响应体**);日志字段键名统一为全拼 `requestId`。
8
+ *
9
+ * 归一语义(实现裁定 §10-8):入站值 **trim 后为空**视为缺失 → 重新生成 `randomUUID`;
10
+ * 非空则 **trim 后复用**。Fastify 的 `requestIdHeader` 对**已存在**的 header 直接取用
11
+ * (`headers[name] || genReqId`),故空白串(`' '`)会原样透传、带空格的也会保留——由下面的
12
+ * `onRequest` 钩子在链路最前补齐归一,并同步 `request.id` / 日志字段 / 响应 header。
13
+ */
14
+ export const REQUEST_ID_HEADER = 'x-request-id';
15
+ const REQUEST_ID_LOG_LABEL = 'requestId';
16
+ /** 归一化入站 header 值(设计 §5.1):数组取首个;**trim 后为空视为缺失**(返回 `undefined`) */
17
+ function normalizeInboundRequestId(inbound) {
18
+ const value = Array.isArray(inbound) ? inbound[0] : inbound;
19
+ if (typeof value !== 'string') {
20
+ return undefined;
21
+ }
22
+ const trimmed = value.trim();
23
+ return trimmed === '' ? undefined : trimmed;
24
+ }
25
+ /** 传给 `Fastify()` 的 requestId 相关选项 */
26
+ export function createRequestIdOptions() {
27
+ return {
28
+ requestIdHeader: REQUEST_ID_HEADER,
29
+ requestIdLogLabel: REQUEST_ID_LOG_LABEL,
30
+ // Fastify 仅在「入站 header 缺失 / 为空串」时调用它(空白串为真值,走 `requestIdHeader` 短路)
31
+ genReqId: (request) => normalizeInboundRequestId(request.headers[REQUEST_ID_HEADER]) ?? randomUUID(),
32
+ };
33
+ }
34
+ /**
35
+ * 注册 `requestId` 钩子(设计 §5.1 / §8):
36
+ *
37
+ * ① `onRequest` 归一化入站 `x-request-id`——`trim` 后为空 → 重新 `randomUUID`,非空 → `trim` 后复用。
38
+ * ② `onSend` 响应统一回吐 `x-request-id`(进日志 + 进响应 header,**不进响应体**)。
39
+ *
40
+ * Fastify 在 `onRequest` **之前**已用原始值建好 child logger,故归一后须重绑日志,才能保证
41
+ * 入口 `request.id`、日志字段 `requestId`、响应 header 三处一致。
42
+ */
43
+ export function registerRequestId(app) {
44
+ app.addHook('onRequest', (request, reply, done) => {
45
+ const inbound = request.headers[REQUEST_ID_HEADER];
46
+ if (inbound === undefined) {
47
+ // 缺失:`genReqId` 已生成 `randomUUID`
48
+ done();
49
+ return;
50
+ }
51
+ const normalized = normalizeInboundRequestId(inbound) ?? randomUUID();
52
+ if (normalized !== request.id) {
53
+ request.id = normalized;
54
+ // 从根 logger 重建(而非在原 child 上再 child,避免 JSON 出现重复 `requestId` 键),
55
+ // 效果等同「入站 header 一开始就是干净值」;请求侧与响应侧同步,保证三处一致。
56
+ const log = request.server.log.child({ [REQUEST_ID_LOG_LABEL]: normalized });
57
+ request.log = log;
58
+ reply.log = log;
59
+ }
60
+ done();
61
+ });
62
+ app.addHook('onSend', (request, reply, payload, done) => {
63
+ reply.header(REQUEST_ID_HEADER, request.id);
64
+ done(null, payload);
65
+ });
66
+ }
@@ -0,0 +1,50 @@
1
+ import type { FastifyInstance } from 'fastify';
2
+ import type { ErrorClassifier } from '../../types.js';
3
+ declare const SUCCESS_STATUS = "SUCCESS";
4
+ /** 成功信封的固定 `message`(错误信封的 `message` 是业务文案,不固定) */
5
+ declare const SUCCESS_MESSAGE = "ok";
6
+ /**
7
+ * 响应信封(成功与错误同一形状,只有 `data` 与 `status` 的含义不同)。
8
+ *
9
+ * ## `status` 为什么是字面量 `'SUCCESS'` 而不是 `string`
10
+ *
11
+ * 类型必须**不宽于**运行时 schema,否则它就不是一个可用的类型:
12
+ * 框架的 `responseSchema()` 把成功响应声明为 `z.enum(['SUCCESS'])`(即 `'SUCCESS'`
13
+ * 字面量)。如果这里写 `status: string`,路由 handler 返回 `createSuccessResponse(x)`
14
+ * 就**不满足自己声明的响应 schema**——类型检查要么报错、要么被上游 `as` 掉。
15
+ *
16
+ * 各仓遇到这个矛盾时会自己再写一份「更窄的同名函数」(account 一直有一份),
17
+ * 结果同一个信封在四个仓里有四个实现,而差异只在一个类型宽度上。宽度对齐后
18
+ * 那份副本就没有存在理由了。
19
+ *
20
+ * 错误信封用 {@link createErrorResponse},它的 `status` 是错误码(`string`)。
21
+ * 两种信封共用一个接口,所以 `ResponseEnvelope.status` 的类型是
22
+ * `'SUCCESS' | string`——对它求值为 `string`,但 `createSuccessResponse` 的返回类型
23
+ * 单独收窄为 {@link SuccessResponse},路由 handler 拿到的就是字面量。
24
+ */
25
+ export interface ResponseEnvelope<T> {
26
+ status: string;
27
+ message: string;
28
+ data: T;
29
+ }
30
+ /** 成功信封:`status` / `message` 都是固定字面量(与 `responseSchema()` 一致) */
31
+ export interface SuccessResponse<T> {
32
+ status: typeof SUCCESS_STATUS;
33
+ message: typeof SUCCESS_MESSAGE;
34
+ data: T;
35
+ }
36
+ export declare function createSuccessResponse<T>(data: T): SuccessResponse<T>;
37
+ export declare function createErrorResponse(status: string, message: string): ResponseEnvelope<null>;
38
+ /**
39
+ * 资源错误分类器(单一来源:`types.ts`)。
40
+ *
41
+ * **每个资源注册自己的分类器**(一个资源一个,各管各的错误域);由服务仓在 `resources(ctx)` 里
42
+ * 通过 `ctx.registerClassifyError` 注册;未注册时错误落 `UNKNOWN_ERROR` 兜底。
43
+ */
44
+ export type { ErrorClassifier };
45
+ export interface ResponseCoreOptions {
46
+ /** 资源错误分类(各资源经 `ctx.registerClassifyError` 注册;缺省 = 不做资源错误分类) */
47
+ classifyResourceError?: ErrorClassifier;
48
+ }
49
+ export declare function registerResponse(app: FastifyInstance, options?: ResponseCoreOptions): void;
50
+ //# sourceMappingURL=index.d.ts.map
@@ -0,0 +1 @@
1
+ {"version":3,"file":"index.d.ts","sourceRoot":"","sources":["../../../../source/core/modules/response/index.ts"],"names":[],"mappings":"AAGA,OAAO,KAAK,EAAE,eAAe,EAAE,MAAM,SAAS,CAAA;AAC9C,OAAO,KAAK,EAAE,eAAe,EAAE,MAAM,gBAAgB,CAAA;AA2BrD,QAAA,MAAM,cAAc,YAAY,CAAA;AAShC,mDAAmD;AACnD,QAAA,MAAM,eAAe,OAAO,CAAA;AAE5B;;;;;;;;;;;;;;;;;;GAkBG;AACH,MAAM,WAAW,gBAAgB,CAAC,CAAC;IACjC,MAAM,EAAE,MAAM,CAAA;IACd,OAAO,EAAE,MAAM,CAAA;IACf,IAAI,EAAE,CAAC,CAAA;CACR;AAED,iEAAiE;AACjE,MAAM,WAAW,eAAe,CAAC,CAAC;IAChC,MAAM,EAAE,OAAO,cAAc,CAAA;IAC7B,OAAO,EAAE,OAAO,eAAe,CAAA;IAC/B,IAAI,EAAE,CAAC,CAAA;CACR;AAED,wBAAgB,qBAAqB,CAAC,CAAC,EAAE,IAAI,EAAE,CAAC,GAAG,eAAe,CAAC,CAAC,CAAC,CAEpE;AAED,wBAAgB,mBAAmB,CAAC,MAAM,EAAE,MAAM,EAAE,OAAO,EAAE,MAAM,GAAG,gBAAgB,CAAC,IAAI,CAAC,CAE3F;AAsBD;;;;;GAKG;AACH,YAAY,EAAE,eAAe,EAAE,CAAA;AAE/B,MAAM,WAAW,mBAAmB;IAClC,gEAAgE;IAChE,qBAAqB,CAAC,EAAE,eAAe,CAAA;CACxC;AAED,wBAAgB,gBAAgB,CAAC,GAAG,EAAE,eAAe,EAAE,OAAO,GAAE,mBAAwB,GAAG,IAAI,CAyD9F"}
@@ -0,0 +1,102 @@
1
+ import { SystemError, UserError } from '@taicode/common-base';
2
+ import { hasZodFastifySchemaValidationErrors, isResponseSerializationError } from 'fastify-type-provider-zod';
3
+ /**
4
+ * core `response`(设计 §5.1 / §8,错误契约 D7)——**框架内置、锁定、不可替换/修改**。
5
+ *
6
+ * - 成功信封 `{ status: 'SUCCESS', message: 'ok', data }`(由 `preSerialization` 统一包裹)。
7
+ * - 错误信封 `{ status: <错误码字符串>, message, data: null }`。
8
+ * - `setErrorHandler` 固定分支:`UserError`→400 / `SystemError`→500 /
9
+ * zod→400 / serde→500 / 资源(各资源 `ctx.registerClassifyError` 注册的分类器)→500 / 未知→500;**禁止按 `error.type` 细分**。
10
+ * - `setNotFoundHandler` → 404 + `NOT_IMPLEMENTED`(落 `warn`,文案对齐 expense/notice)。
11
+ * - 错误日志分级:`UserError` / zod → `warn`,其余 → `error`(带 stack)。
12
+ * - **错误只给稳定码**(`status` 放错误码字符串),文案不归框架(由前端 / i18n 决定,§10-6)。
13
+ *
14
+ * 错误类型本身**不在这里定义**:`UserError` / `SystemError` 复用 `@taicode/common-base` 的
15
+ * 单一来源(连 `Symbol.for` 品牌都同名,故 base 抛出的同类错误同样被识别),框架只负责
16
+ * **状态码映射**与响应信封,不重复实现错误类。
17
+ */
18
+ /**
19
+ * 固定状态码映射(D7):`UserError`→400 / `SystemError`→500。
20
+ *
21
+ * 映射写在框架侧而非错误类上(base 的类不带 `status`)。判定一律走 `is()` 品牌守卫,
22
+ * **不用 `instanceof`**——跨包 / 编译后 / 原型链丢失都仍然可靠;**禁止按 `error.type` 细分**。
23
+ */
24
+ const USER_ERROR_STATUS = 400;
25
+ const SYSTEM_ERROR_STATUS = 500;
26
+ const SUCCESS_STATUS = 'SUCCESS';
27
+ const NOT_IMPLEMENTED_STATUS = 'NOT_IMPLEMENTED';
28
+ const INVALID_INPUT_STATUS = 'INVALID_INPUT';
29
+ const SERIALIZATION_ERROR_STATUS = 'RESPONSE_SERIALIZATION_ERROR';
30
+ const UNKNOWN_ERROR_STATUS = 'UNKNOWN_ERROR';
31
+ /** 通用兜底文案(仅在 `error.message` 为空时使用) */
32
+ const DEFAULT_SYSTEM_MESSAGE = '系统错误';
33
+ /** 成功信封的固定 `message`(错误信封的 `message` 是业务文案,不固定) */
34
+ const SUCCESS_MESSAGE = 'ok';
35
+ export function createSuccessResponse(data) {
36
+ return { status: SUCCESS_STATUS, message: SUCCESS_MESSAGE, data };
37
+ }
38
+ export function createErrorResponse(status, message) {
39
+ return { status, message, data: null };
40
+ }
41
+ function isEnvelope(payload) {
42
+ return (typeof payload === 'object' &&
43
+ payload !== null &&
44
+ typeof payload.status === 'string' &&
45
+ typeof payload.message === 'string' &&
46
+ 'data' in payload);
47
+ }
48
+ function resolveMessage(error) {
49
+ const explicit = typeof error.message === 'string' ? error.message : '';
50
+ return explicit || DEFAULT_SYSTEM_MESSAGE;
51
+ }
52
+ export function registerResponse(app, options = {}) {
53
+ // 成功信封:统一包裹非信封载荷(错误 / 404 已自带信封,不会被二次包裹)
54
+ app.addHook('preSerialization', (_request, _reply, payload, done) => {
55
+ if (payload === undefined || payload === null || isEnvelope(payload)) {
56
+ done(null, payload);
57
+ return;
58
+ }
59
+ done(null, createSuccessResponse(payload));
60
+ });
61
+ app.setNotFoundHandler((request, reply) => {
62
+ request.log.warn({ url: request.url }, '[response] route not found');
63
+ return reply
64
+ .code(404)
65
+ .send(createErrorResponse(NOT_IMPLEMENTED_STATUS, `接口不存在:${request.method} ${request.url}`));
66
+ });
67
+ app.setErrorHandler((error, request, reply) => {
68
+ // ① UserError → 400
69
+ if (UserError.is(error)) {
70
+ request.log.warn({ err: error }, '[response] user error');
71
+ return reply.code(USER_ERROR_STATUS).send(createErrorResponse(error.type, resolveMessage(error)));
72
+ }
73
+ // ② SystemError → 500
74
+ if (SystemError.is(error)) {
75
+ request.log.error({ err: error }, '[response] system error');
76
+ return reply.code(SYSTEM_ERROR_STATUS).send(createErrorResponse(error.type, resolveMessage(error)));
77
+ }
78
+ // ③ zod 校验失败 → 400
79
+ if (hasZodFastifySchemaValidationErrors(error)) {
80
+ request.log.warn({ err: error }, '[response] zod validation error');
81
+ return reply.code(400).send(createErrorResponse(INVALID_INPUT_STATUS, error.message));
82
+ }
83
+ // ④ 响应序列化失败 → 500
84
+ if (isResponseSerializationError(error)) {
85
+ request.log.error({ err: error }, '[response] response serialization error');
86
+ return reply.code(500).send(createErrorResponse(SERIALIZATION_ERROR_STATUS, error.message));
87
+ }
88
+ // ⑤ 资源错误(各资源注册的分类器依次认领)→ 500
89
+ const resourceError = options.classifyResourceError?.(error);
90
+ if (resourceError) {
91
+ request.log.error({ err: error }, '[response] resource error');
92
+ return reply
93
+ .code(500)
94
+ .send(createErrorResponse(resourceError.type, resolveMessage(resourceError)));
95
+ }
96
+ // ⑥ 未知 → 500
97
+ request.log.error({ err: error }, '[response] unknown error');
98
+ const cause = error;
99
+ const message = typeof cause.message === 'string' && cause.message ? cause.message : DEFAULT_SYSTEM_MESSAGE;
100
+ return reply.code(500).send(createErrorResponse(UNKNOWN_ERROR_STATUS, message));
101
+ });
102
+ }
@@ -172,6 +172,13 @@ interface RouterSchema extends FastifySchema {
172
172
  operationId: string;
173
173
  description: string;
174
174
  }
175
+ /**
176
+ * 路由 schema 工厂:**四项元信息全部必填**,不留默认值。
177
+ *
178
+ * `operationId` / `summary` / `description` / `tags` 是权限目录与文档可读性的下限,
179
+ * core 的 `installRouteSchemaGuard` 会对运行时缺失逐项报错;这里在类型层面就要求写全,
180
+ * 避免「先给个默认值让它跑起来,回头再补」——补是不会发生的。
181
+ */
175
182
  export declare function routerSchema<T extends RouterSchema>(schema: T & RouterSchema): T & RouterSchema;
176
183
  export {};
177
- //# sourceMappingURL=index.d.ts.map
184
+ //# sourceMappingURL=builders.d.ts.map
@@ -0,0 +1 @@
1
+ {"version":3,"file":"builders.d.ts","sourceRoot":"","sources":["../../../../source/core/modules/schema/builders.ts"],"names":[],"mappings":"AAAA,OAAO,EAAE,CAAC,EAAE,OAAO,EAAE,OAAO,EAAE,MAAM,KAAK,CAAA;AACzC,OAAO,KAAK,EAAE,aAAa,EAAE,MAAM,SAAS,CAAA;AA2B5C;;;;;;GAMG;AACH,wBAAgB,iBAAiB,CAAC,CAAC,SAAS,OAAO,EAAE,CAAC,SAAS,OAAO,EAAE,CAAC,EAAE,CAAC,EAAE,CAAC,CAAC,EAAE,CAAC;;;;;;;;;;;;;kBAMlF;AAED;;;;;GAKG;AACH,wBAAgB,iBAAiB,CAAC,CAAC,SAAS,OAAO,EAAE,CAAC,EAAE,CAAC;;;kBAKxD;AAED;;;;GAIG;AACH,wBAAgB,gBAAgB,kCAE/B;AAED;;;;;GAKG;AACH,wBAAgB,gBAAgB,CAAC,CAAC,SAAS,OAAO,EAAE,CAAC,EAAE,CAAC;;;kBAKvD;AAED;;;;;;GAMG;AACH,wBAAgB,sBAAsB,CAAC,CAAC,SAAS,OAAO,EAAE,CAAC,SAAS,OAAO,EAAE,CAAC,EAAE,CAAC,EAAE,CAAC,CAAC,EAAE,CAAC;;;;;;;;;;kBAMvF;AAED;;;;;GAKG;AACH,wBAAgB,sBAAsB,CAAC,CAAC,SAAS,OAAO,EAAE,CAAC,EAAE,CAAC;;;mBAK7D;AAED;;;;;;GAMG;AACH,wBAAgB,0BAA0B,CAAC,CAAC,SAAS,OAAO,EAAE,CAAC,EAAE,CAAC;;;;;;;;;;;kBAMjE;AAED;;;;;GAKG;AACH,wBAAgB,0BAA0B,CAAC,CAAC,SAAS,OAAO,EAAE,CAAC,EAAE,CAAC;;;mBAKjE;AAED;;;;;;GAMG;AACH,wBAAgB,qBAAqB,CAAC,CAAC,SAAS,OAAO,EAAE,CAAC,SAAS,OAAO,EAAE,CAAC,EAAE,CAAC,EAAE,CAAC,CAAC,EAAE,CAAC;;;;;;;;;;;;;kBAMtF;AAED;;;;;GAKG;AACH,wBAAgB,qBAAqB,CAAC,CAAC,SAAS,OAAO,EAAE,CAAC,EAAE,CAAC;;;kBAK5D;AAGD;;;;;;GAMG;AACH,wBAAgB,kBAAkB,CAAC,CAAC,SAAS,OAAO,EAAE,CAAC,SAAS,OAAO,EAAE,CAAC,EAAE,CAAC,EAAE,CAAC,EAAE,CAAC;;;;kBAMlF;AAED,wBAAgB,cAAc,CAAC,CAAC,SAAS,OAAO,EAAE,CAAC,SAAS,OAAO,EAAE,CAAC,SAAS,OAAO,EAAE,CAAC,EAAE,CAAC,EAAE,CAAC,EAAE,CAAC,EAAE,CAAC,EAAE,CAAC;;;;;;;;;;;;;;;;;;EAMvG;AAED,UAAU,YAAa,SAAQ,aAAa;IAC1C,IAAI,EAAE,MAAM,EAAE,CAAA;IACd,OAAO,EAAE,MAAM,CAAA;IACf,WAAW,EAAE,MAAM,CAAA;IACnB,WAAW,EAAE,MAAM,CAAA;CACpB;AAED;;;;;;GAMG;AACH,wBAAgB,YAAY,CAAC,CAAC,SAAS,YAAY,EAAE,MAAM,EAAE,CAAC,GAAG,YAAY,GAAG,CAAC,GAAG,YAAY,CAO/F"}
@@ -167,14 +167,17 @@ export function responseSchema(t, u, e) {
167
167
  '500': responseDataSchema(z.null(), e),
168
168
  };
169
169
  }
170
+ /**
171
+ * 路由 schema 工厂:**四项元信息全部必填**,不留默认值。
172
+ *
173
+ * `operationId` / `summary` / `description` / `tags` 是权限目录与文档可读性的下限,
174
+ * core 的 `installRouteSchemaGuard` 会对运行时缺失逐项报错;这里在类型层面就要求写全,
175
+ * 避免「先给个默认值让它跑起来,回头再补」——补是不会发生的。
176
+ */
170
177
  export function routerSchema(schema) {
171
178
  // 验证必需字段
172
179
  if (!schema.operationId) {
173
180
  throw new Error('routerSchema requires operationId');
174
181
  }
175
- // 如果没有提供 tags,设置默认值
176
- if (!schema.tags) {
177
- schema.tags = ['默认'];
178
- }
179
182
  return { ...schema };
180
183
  }
@@ -0,0 +1,34 @@
1
+ import type { FastifyInstance } from 'fastify';
2
+ /**
3
+ * core `schema`(设计 §5.1)——**框架内置、锁定、不可替换/修改**。
4
+ *
5
+ * 三部分:
6
+ * 1. `registerSchema`:装配 `fastify-type-provider-zod` 的 validator / serializer compiler;
7
+ * 2. `installRouteSchemaGuard`:**强制每条路由声明 schema**(`operationId` / `summary` /
8
+ * `description` / `tags` 缺一即启动失败);
9
+ * 3. `builders`:业务侧构造路由 schema 的约定工厂(`responseSchema` / `routerSchema` / 分页等)。
10
+ */
11
+ export declare function registerSchema(app: FastifyInstance): void;
12
+ /**
13
+ * 强制所有业务路由提供 schema(**注册处即失败**,不给「先跑起来再补」的口子)。
14
+ *
15
+ * 为什么必须硬性:
16
+ *
17
+ * - OpenAPI 文档是**权限目录的唯一来源**(`auth` 的权限自注册直接读文档)。一条路由没
18
+ * `operationId`,它就在权限系统里**不存在**——表现为「接口永远判不过」,且没有任何报错。
19
+ * 这种失败必须发生在启动期,不能留到线上。
20
+ * - `summary` / `description` / `tags` 是文档可读性的下限;没有它们的文档等于没有文档。
21
+ *
22
+ * ## 为什么没有「声明一个标记就不检查」的入口
23
+ *
24
+ * 守卫要防的就是「路由活着、权限系统里却没有」。任何**按路由声明**的布尔值能放宽它,
25
+ * 都等于把这个洞重新打开,而且是**显式地**打开——比遗漏更危险,因为它看起来像设计。
26
+ *
27
+ * 所以只有两类被排除,且全是方法级的结构事实,路由无法参与决定:
28
+ * `HEAD`(GET 自动派生)与 `OPTIONS`(CORS 预检)。
29
+ *
30
+ * 也没有「不进文档」的隐藏开关:文档就是权限目录,两者不能有分歧。
31
+ */
32
+ export declare function installRouteSchemaGuard(app: FastifyInstance): void;
33
+ export * from './builders.js';
34
+ //# sourceMappingURL=index.d.ts.map
@@ -0,0 +1 @@
1
+ {"version":3,"file":"index.d.ts","sourceRoot":"","sources":["../../../../source/core/modules/schema/index.ts"],"names":[],"mappings":"AAEA,OAAO,KAAK,EAAE,eAAe,EAAiB,MAAM,SAAS,CAAA;AAE7D;;;;;;;;GAQG;AACH,wBAAgB,cAAc,CAAC,GAAG,EAAE,eAAe,GAAG,IAAI,CAMzD;AAeD;;;;;;;;;;;;;;;;;;;GAmBG;AACH,wBAAgB,uBAAuB,CAAC,GAAG,EAAE,eAAe,GAAG,IAAI,CAqBlE;AAED,cAAc,eAAe,CAAA"}
@@ -0,0 +1,62 @@
1
+ import { serializerCompiler, validatorCompiler } from 'fastify-type-provider-zod';
2
+ /**
3
+ * core `schema`(设计 §5.1)——**框架内置、锁定、不可替换/修改**。
4
+ *
5
+ * 三部分:
6
+ * 1. `registerSchema`:装配 `fastify-type-provider-zod` 的 validator / serializer compiler;
7
+ * 2. `installRouteSchemaGuard`:**强制每条路由声明 schema**(`operationId` / `summary` /
8
+ * `description` / `tags` 缺一即启动失败);
9
+ * 3. `builders`:业务侧构造路由 schema 的约定工厂(`responseSchema` / `routerSchema` / 分页等)。
10
+ */
11
+ export function registerSchema(app) {
12
+ app.setValidatorCompiler(validatorCompiler);
13
+ app.setSerializerCompiler(serializerCompiler);
14
+ }
15
+ /** 每条路由必须声明的 schema 字段(与权限目录同源:OpenAPI 文档就是权限目录的唯一来源) */
16
+ const REQUIRED_SCHEMA_FIELDS = ['operationId', 'summary', 'description', 'tags'];
17
+ /**
18
+ * 被排除在外的**方法**(结构事实,不是路由声明)。
19
+ *
20
+ * - `HEAD`:Fastify 为 GET 自动派生,schema 与 GET 完全相同,重复校验无意义;
21
+ * - `OPTIONS`:CORS 预检(`@fastify/cors` 注册的 `OPTIONS *`),永远不是业务操作。
22
+ */
23
+ const NON_OPERATION_METHODS = new Set(['HEAD', 'OPTIONS']);
24
+ /**
25
+ * 强制所有业务路由提供 schema(**注册处即失败**,不给「先跑起来再补」的口子)。
26
+ *
27
+ * 为什么必须硬性:
28
+ *
29
+ * - OpenAPI 文档是**权限目录的唯一来源**(`auth` 的权限自注册直接读文档)。一条路由没
30
+ * `operationId`,它就在权限系统里**不存在**——表现为「接口永远判不过」,且没有任何报错。
31
+ * 这种失败必须发生在启动期,不能留到线上。
32
+ * - `summary` / `description` / `tags` 是文档可读性的下限;没有它们的文档等于没有文档。
33
+ *
34
+ * ## 为什么没有「声明一个标记就不检查」的入口
35
+ *
36
+ * 守卫要防的就是「路由活着、权限系统里却没有」。任何**按路由声明**的布尔值能放宽它,
37
+ * 都等于把这个洞重新打开,而且是**显式地**打开——比遗漏更危险,因为它看起来像设计。
38
+ *
39
+ * 所以只有两类被排除,且全是方法级的结构事实,路由无法参与决定:
40
+ * `HEAD`(GET 自动派生)与 `OPTIONS`(CORS 预检)。
41
+ *
42
+ * 也没有「不进文档」的隐藏开关:文档就是权限目录,两者不能有分歧。
43
+ */
44
+ export function installRouteSchemaGuard(app) {
45
+ app.addHook('onRoute', (routeOptions) => {
46
+ const methods = Array.isArray(routeOptions.method) ? routeOptions.method : [routeOptions.method];
47
+ if (methods.every((method) => NON_OPERATION_METHODS.has(method))) {
48
+ return;
49
+ }
50
+ const schema = routeOptions.schema;
51
+ const missing = REQUIRED_SCHEMA_FIELDS.filter((field) => {
52
+ const value = schema?.[field];
53
+ return value === undefined || value === null || (Array.isArray(value) ? value.length === 0 : value === '');
54
+ });
55
+ if (missing.length > 0) {
56
+ throw new Error(`[schema] 路由 ${methods.join(',')} ${routeOptions.url} 缺少 schema.${missing.join(' / schema.')}:` +
57
+ '所有业务路由都必须用 `routerSchema({ operationId, summary, description, tags, ... })` 声明,' +
58
+ '权限目录直接由 OpenAPI 文档生成,未声明的路由在权限系统里不存在');
59
+ }
60
+ });
61
+ }
62
+ export * from './builders.js';
@@ -0,0 +1,2 @@
1
+ export {};
2
+ //# sourceMappingURL=index.test.d.ts.map
@@ -0,0 +1 @@
1
+ {"version":3,"file":"index.test.d.ts","sourceRoot":"","sources":["../../../../source/core/modules/schema/index.test.ts"],"names":[],"mappings":""}
@@ -0,0 +1,55 @@
1
+ import Fastify from 'fastify';
2
+ import { describe, expect, it } from 'vitest';
3
+ import { installRouteSchemaGuard, registerSchema } from './index.js';
4
+ const FULL_SCHEMA = {
5
+ operationId: 'listUser',
6
+ summary: '列表',
7
+ description: '分页列出用户',
8
+ tags: ['用户'],
9
+ };
10
+ function buildApp() {
11
+ const app = Fastify();
12
+ registerSchema(app);
13
+ installRouteSchemaGuard(app);
14
+ return app;
15
+ }
16
+ describe('installRouteSchemaGuard', () => {
17
+ it('声明完整 schema 的路由通过(含 Fastify 自动派生的 HEAD)', async () => {
18
+ const app = buildApp();
19
+ app.get('/user', { schema: FULL_SCHEMA }, async () => ({ ok: true }));
20
+ await expect(app.ready()).resolves.toBeDefined();
21
+ await app.close();
22
+ });
23
+ it('缺少 schema 时在注册处即抛错(不等启动)', async () => {
24
+ const app = buildApp();
25
+ expect(() => app.get('/user', async () => ({ ok: true }))).toThrow(/GET \/user 缺少 schema\.operationId/);
26
+ await app.close();
27
+ });
28
+ it('缺少 summary / description / tags 时逐个列出', async () => {
29
+ const app = buildApp();
30
+ expect(() => app.post('/user', { schema: { operationId: 'createUser' } }, async () => ({ ok: true }))).toThrow(/schema\.summary \/ schema\.description \/ schema\.tags/);
31
+ await app.close();
32
+ });
33
+ it('OPTIONS 按方法豁免(CORS 预检不是业务操作)', async () => {
34
+ const app = buildApp();
35
+ app.options('/user', async () => ({}));
36
+ await expect(app.ready()).resolves.toBeDefined();
37
+ await app.close();
38
+ });
39
+ it('插件内注册的路由同样受约束(错误冒泡成启动失败)', async () => {
40
+ const app = buildApp();
41
+ let caught;
42
+ try {
43
+ await app.register(async (instance) => {
44
+ instance.get('/nested', async () => ({ ok: true }));
45
+ });
46
+ await app.ready();
47
+ }
48
+ catch (error) {
49
+ caught = error;
50
+ }
51
+ expect(caught).toBeInstanceOf(Error);
52
+ expect(caught.message).toMatch(/GET \/nested 缺少 schema/);
53
+ await app.close();
54
+ });
55
+ });
@@ -0,0 +1,15 @@
1
+ import type { AppIdentity, AppInstance } from '../../types.js';
2
+ export interface SystemRoutesOptions {
3
+ /** 身份块(`service` / `title` / `version`):`GET /` 与 `/system/info` 从这里派生 */
4
+ identity: AppIdentity;
5
+ /** 已构造的 services 实体(`ctx.services`):健康检查遍历其可选 `health()` */
6
+ services: Record<string, unknown>;
7
+ /** 取**已加载**配置中某个键的访问器(请求期惰性现读;出 spec 路径不得调用) */
8
+ readConfig: (key: string) => unknown;
9
+ }
10
+ /**
11
+ * 注册框架内置系统路由(5 个端点)。由 `registerSystemRoutesStep` 在 `server.routers(...)` **之前**调用,
12
+ * 保证框架路由先就位。
13
+ */
14
+ export declare function registerSystemRoutes(app: AppInstance, options: SystemRoutesOptions): void;
15
+ //# sourceMappingURL=index.d.ts.map
@@ -0,0 +1 @@
1
+ {"version":3,"file":"index.d.ts","sourceRoot":"","sources":["../../../../source/core/modules/system-routes/index.ts"],"names":[],"mappings":"AAMA,OAAO,KAAK,EAAE,WAAW,EAAE,WAAW,EAAmB,MAAM,gBAAgB,CAAA;AA4K/E,MAAM,WAAW,mBAAmB;IAClC,0EAA0E;IAC1E,QAAQ,EAAE,WAAW,CAAA;IACrB,4DAA4D;IAC5D,QAAQ,EAAE,MAAM,CAAC,MAAM,EAAE,OAAO,CAAC,CAAA;IACjC,gDAAgD;IAChD,UAAU,EAAE,CAAC,GAAG,EAAE,MAAM,KAAK,OAAO,CAAA;CACrC;AAED;;;GAGG;AACH,wBAAgB,oBAAoB,CAAC,GAAG,EAAE,WAAW,EAAE,OAAO,EAAE,mBAAmB,GAAG,IAAI,CA2DzF"}