@taicode/common-server 4.0.0-beta.1 → 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.
- package/output/app/index.d.ts +18 -15
- package/output/app/index.d.ts.map +1 -1
- package/output/app/index.js +330 -18
- package/output/auth/index.d.ts +73 -0
- package/output/auth/index.d.ts.map +1 -0
- package/output/auth/index.js +196 -0
- package/output/core/assemble.d.ts +64 -0
- package/output/core/assemble.d.ts.map +1 -0
- package/output/core/assemble.js +88 -0
- package/output/core/boot/context.d.ts +109 -0
- package/output/core/boot/context.d.ts.map +1 -0
- package/output/core/boot/context.js +145 -0
- package/output/core/boot/lifecycle.d.ts +206 -0
- package/output/core/boot/lifecycle.d.ts.map +1 -0
- package/output/core/boot/lifecycle.js +255 -0
- package/output/core/boot/shutdown.d.ts +34 -0
- package/output/core/boot/shutdown.d.ts.map +1 -0
- package/output/core/boot/shutdown.js +50 -0
- package/output/core/boot/signal.d.ts +13 -0
- package/output/core/boot/signal.d.ts.map +1 -0
- package/output/{app → core/boot}/signal.js +3 -2
- package/output/core/boot/steps.d.ts +138 -0
- package/output/core/boot/steps.d.ts.map +1 -0
- package/output/core/boot/steps.js +259 -0
- package/output/core/entries.d.ts +151 -0
- package/output/core/entries.d.ts.map +1 -0
- package/output/core/entries.js +242 -0
- package/output/core/entries.test.d.ts +2 -0
- package/output/core/entries.test.d.ts.map +1 -0
- package/output/core/entries.test.js +458 -0
- package/output/core/env.d.ts +83 -0
- package/output/core/env.d.ts.map +1 -0
- package/output/core/env.js +124 -0
- package/output/{app → core}/env.test.d.ts.map +1 -1
- package/output/core/env.test.js +105 -0
- package/output/core/modules/auth-provider/index.d.ts +10 -0
- package/output/core/modules/auth-provider/index.d.ts.map +1 -0
- package/output/core/modules/auth-provider/index.js +9 -0
- package/output/core/modules/auth-provider/types.d.ts +71 -0
- package/output/core/modules/auth-provider/types.d.ts.map +1 -0
- package/output/core/modules/context/index.d.ts +18 -0
- package/output/core/modules/context/index.d.ts.map +1 -0
- package/output/core/modules/context/index.js +23 -0
- package/output/core/modules/context/index.test.d.ts.map +1 -0
- package/output/core/modules/cookie/index.d.ts +21 -0
- package/output/core/modules/cookie/index.d.ts.map +1 -0
- package/output/{app/core/cookie.js → core/modules/cookie/index.js} +1 -6
- package/output/core/modules/identity/index.d.ts +4 -0
- package/output/core/modules/identity/index.d.ts.map +1 -0
- package/output/{app/core/identity.js → core/modules/identity/index.js} +1 -1
- package/output/core/modules/logging/index.d.ts +5 -0
- package/output/core/modules/logging/index.d.ts.map +1 -0
- package/output/{app/core/logging.js → core/modules/logging/index.js} +2 -2
- package/output/core/modules/openapi/index.d.ts +7 -0
- package/output/core/modules/openapi/index.d.ts.map +1 -0
- package/output/{app/core/openapi.js → core/modules/openapi/index.js} +11 -12
- package/output/core/modules/permission-operations/index.d.ts +26 -0
- package/output/core/modules/permission-operations/index.d.ts.map +1 -0
- package/output/core/modules/permission-operations/index.js +36 -0
- package/output/core/modules/permission-operations/index.test.d.ts +2 -0
- package/output/core/modules/permission-operations/index.test.d.ts.map +1 -0
- package/output/core/modules/permission-operations/index.test.js +45 -0
- package/output/{app/core/request-id.d.ts → core/modules/request-id/index.d.ts} +1 -4
- package/output/core/modules/request-id/index.d.ts.map +1 -0
- package/output/{app/core/request-id.js → core/modules/request-id/index.js} +2 -2
- package/output/core/modules/response/index.d.ts +50 -0
- package/output/core/modules/response/index.d.ts.map +1 -0
- package/output/{app/core/response.js → core/modules/response/index.js} +31 -18
- package/output/{schema/index.d.ts → core/modules/schema/builders.d.ts} +8 -1
- package/output/core/modules/schema/builders.d.ts.map +1 -0
- package/output/{schema/index.js → core/modules/schema/builders.js} +7 -4
- package/output/core/modules/schema/index.d.ts +34 -0
- package/output/core/modules/schema/index.d.ts.map +1 -0
- package/output/core/modules/schema/index.js +62 -0
- package/output/core/modules/schema/index.test.d.ts +2 -0
- package/output/core/modules/schema/index.test.d.ts.map +1 -0
- package/output/core/modules/schema/index.test.js +55 -0
- package/output/core/modules/system-routes/index.d.ts +15 -0
- package/output/core/modules/system-routes/index.d.ts.map +1 -0
- package/output/core/modules/system-routes/index.js +205 -0
- package/output/core/resource.d.ts +56 -0
- package/output/core/resource.d.ts.map +1 -0
- package/output/core/resource.js +56 -0
- package/output/core/test-support/database.d.ts +89 -0
- package/output/core/test-support/database.d.ts.map +1 -0
- package/output/core/test-support/database.js +131 -0
- package/output/core/test-support/database.test.d.ts +2 -0
- package/output/core/test-support/database.test.d.ts.map +1 -0
- package/output/core/test-support/database.test.js +111 -0
- package/output/core/test-support/environment.d.ts +38 -0
- package/output/core/test-support/environment.d.ts.map +1 -0
- package/output/core/test-support/environment.js +45 -0
- package/output/core/test-support/service-context.d.ts +64 -0
- package/output/core/test-support/service-context.d.ts.map +1 -0
- package/output/core/test-support/service-context.js +63 -0
- package/output/core/test-support/service-context.test.d.ts +2 -0
- package/output/core/test-support/service-context.test.d.ts.map +1 -0
- package/output/core/test-support/service-context.test.js +55 -0
- package/output/core/test-support/test-server.d.ts +154 -0
- package/output/core/test-support/test-server.d.ts.map +1 -0
- package/output/core/test-support/test-server.js +114 -0
- package/output/core/test-support/test-server.test.d.ts +2 -0
- package/output/core/test-support/test-server.test.d.ts.map +1 -0
- package/output/core/test-support/test-server.test.js +296 -0
- package/output/core/test-support/testdata.d.ts +255 -0
- package/output/core/test-support/testdata.d.ts.map +1 -0
- package/output/core/test-support/testdata.js +271 -0
- package/output/core/test-support/testdata.test.d.ts +2 -0
- package/output/core/test-support/testdata.test.d.ts.map +1 -0
- package/output/core/test-support/testdata.test.js +334 -0
- package/output/{app → core}/type-augment.d.ts +2 -2
- package/output/core/type-augment.d.ts.map +1 -0
- package/output/core/types.d.ts +582 -0
- package/output/core/types.d.ts.map +1 -0
- package/output/cors/index.d.ts +3 -0
- package/output/cors/index.d.ts.map +1 -0
- package/output/cors/index.js +28 -0
- package/output/index.d.ts +10 -4
- package/output/index.d.ts.map +1 -1
- package/output/index.js +25 -5
- package/output/rate-limit/index.d.ts +23 -0
- package/output/rate-limit/index.d.ts.map +1 -0
- package/output/rate-limit/index.js +43 -0
- package/output/{rate-limiter/rate-limiter.d.ts → rate-limit/limiter.d.ts} +1 -1
- package/output/rate-limit/limiter.d.ts.map +1 -0
- package/output/{rate-limiter → rate-limit}/memory-store.d.ts +6 -0
- package/output/rate-limit/memory-store.d.ts.map +1 -0
- package/output/{rate-limiter → rate-limit}/memory-store.js +8 -0
- package/output/{rate-limiter → rate-limit}/redis-store.d.ts +18 -0
- package/output/rate-limit/redis-store.d.ts.map +1 -0
- package/output/{rate-limiter → rate-limit}/redis-store.js +35 -0
- package/output/rate-limit/redis-store.test.d.ts.map +1 -0
- package/output/{rate-limiter → rate-limit}/redis-store.test.js +2 -2
- package/output/rate-limit/types.d.ts +45 -0
- package/output/rate-limit/types.d.ts.map +1 -0
- package/output/rate-limit/types.js +1 -0
- package/output/{redis-cache/redis-cache.d.ts → redis/cache.d.ts} +1 -1
- package/output/redis/cache.d.ts.map +1 -0
- package/output/redis/cache.test.d.ts +2 -0
- package/output/redis/cache.test.d.ts.map +1 -0
- package/output/{redis-cache/redis-cache.test.js → redis/cache.test.js} +1 -1
- package/output/{app/redis.d.ts → redis/index.d.ts} +12 -7
- package/output/redis/index.d.ts.map +1 -0
- package/output/{app/redis.js → redis/index.js} +15 -5
- package/output/redis/index.test.d.ts +2 -0
- package/output/{context → redis}/index.test.d.ts.map +1 -1
- package/output/redis/index.test.js +55 -0
- package/output/redis-queue/index.js +1 -1
- package/output/redis-queue/test-helpers.d.ts +1 -1
- package/output/redis-queue/test-helpers.js +1 -1
- package/output/redis-queue/types.d.ts +3 -12
- package/output/redis-queue/types.d.ts.map +1 -1
- package/package.json +5 -5
- package/output/app/auth.test.d.ts +0 -2
- package/output/app/auth.test.d.ts.map +0 -1
- package/output/app/auth.test.js +0 -148
- package/output/app/boot.d.ts +0 -48
- package/output/app/boot.d.ts.map +0 -1
- package/output/app/boot.js +0 -123
- package/output/app/config.test.d.ts +0 -2
- package/output/app/config.test.d.ts.map +0 -1
- package/output/app/config.test.js +0 -132
- package/output/app/context.d.ts +0 -68
- package/output/app/context.d.ts.map +0 -1
- package/output/app/context.js +0 -73
- package/output/app/core/cookie.d.ts +0 -25
- package/output/app/core/cookie.d.ts.map +0 -1
- package/output/app/core/identity.d.ts +0 -12
- package/output/app/core/identity.d.ts.map +0 -1
- package/output/app/core/index.d.ts +0 -44
- package/output/app/core/index.d.ts.map +0 -1
- package/output/app/core/index.js +0 -57
- package/output/app/core/logging.d.ts +0 -13
- package/output/app/core/logging.d.ts.map +0 -1
- package/output/app/core/openapi.d.ts +0 -17
- package/output/app/core/openapi.d.ts.map +0 -1
- package/output/app/core/request-id.d.ts.map +0 -1
- package/output/app/core/response.d.ts +0 -41
- package/output/app/core/response.d.ts.map +0 -1
- package/output/app/define-server.negative-cases.d.ts +0 -32
- package/output/app/define-server.negative-cases.d.ts.map +0 -1
- package/output/app/define-server.negative-cases.js +0 -209
- package/output/app/define-server.test.d.ts +0 -2
- package/output/app/define-server.test.d.ts.map +0 -1
- package/output/app/define-server.test.js +0 -97
- package/output/app/entries.d.ts +0 -29
- package/output/app/entries.d.ts.map +0 -1
- package/output/app/entries.js +0 -64
- package/output/app/env.d.ts +0 -59
- package/output/app/env.d.ts.map +0 -1
- package/output/app/env.js +0 -66
- package/output/app/env.test.js +0 -124
- package/output/app/error-contract.test.d.ts +0 -2
- package/output/app/error-contract.test.d.ts.map +0 -1
- package/output/app/error-contract.test.js +0 -125
- package/output/app/errors.d.ts +0 -20
- package/output/app/errors.d.ts.map +0 -1
- package/output/app/errors.js +0 -51
- package/output/app/generate-openapi.test.d.ts +0 -2
- package/output/app/generate-openapi.test.d.ts.map +0 -1
- package/output/app/generate-openapi.test.js +0 -123
- package/output/app/lifecycle-logging.test.d.ts +0 -2
- package/output/app/lifecycle-logging.test.d.ts.map +0 -1
- package/output/app/lifecycle-logging.test.js +0 -155
- package/output/app/lifecycle.d.ts +0 -70
- package/output/app/lifecycle.d.ts.map +0 -1
- package/output/app/lifecycle.js +0 -99
- package/output/app/lifecycle.test.d.ts +0 -2
- package/output/app/lifecycle.test.d.ts.map +0 -1
- package/output/app/lifecycle.test.js +0 -152
- package/output/app/middleware/auth.d.ts +0 -71
- package/output/app/middleware/auth.d.ts.map +0 -1
- package/output/app/middleware/auth.js +0 -190
- package/output/app/middleware/context.d.ts +0 -13
- package/output/app/middleware/context.d.ts.map +0 -1
- package/output/app/middleware/context.js +0 -15
- package/output/app/middleware/cors.d.ts +0 -14
- package/output/app/middleware/cors.d.ts.map +0 -1
- package/output/app/middleware/cors.js +0 -30
- package/output/app/middleware/index.d.ts +0 -17
- package/output/app/middleware/index.d.ts.map +0 -1
- package/output/app/middleware/index.js +0 -16
- package/output/app/middleware/rate-limit.d.ts +0 -24
- package/output/app/middleware/rate-limit.d.ts.map +0 -1
- package/output/app/middleware/rate-limit.js +0 -46
- package/output/app/middleware-order.test.d.ts +0 -2
- package/output/app/middleware-order.test.d.ts.map +0 -1
- package/output/app/middleware-order.test.js +0 -148
- package/output/app/permission-operations.d.ts +0 -48
- package/output/app/permission-operations.d.ts.map +0 -1
- package/output/app/permission-operations.js +0 -77
- package/output/app/permission-operations.test.d.ts +0 -2
- package/output/app/permission-operations.test.d.ts.map +0 -1
- package/output/app/permission-operations.test.js +0 -72
- package/output/app/permission-registration.test.d.ts +0 -2
- package/output/app/permission-registration.test.d.ts.map +0 -1
- package/output/app/permission-registration.test.js +0 -138
- package/output/app/redis.d.ts.map +0 -1
- package/output/app/registered-operations.test.d.ts +0 -2
- package/output/app/registered-operations.test.d.ts.map +0 -1
- package/output/app/registered-operations.test.js +0 -57
- package/output/app/request-id.test.d.ts +0 -2
- package/output/app/request-id.test.d.ts.map +0 -1
- package/output/app/request-id.test.js +0 -130
- package/output/app/resources.test.d.ts +0 -2
- package/output/app/resources.test.d.ts.map +0 -1
- package/output/app/resources.test.js +0 -197
- package/output/app/shutdown.d.ts +0 -22
- package/output/app/shutdown.d.ts.map +0 -1
- package/output/app/shutdown.js +0 -30
- package/output/app/shutdown.test.d.ts +0 -2
- package/output/app/shutdown.test.d.ts.map +0 -1
- package/output/app/shutdown.test.js +0 -103
- package/output/app/signal.d.ts +0 -12
- package/output/app/signal.d.ts.map +0 -1
- package/output/app/smoke.test.d.ts +0 -2
- package/output/app/smoke.test.d.ts.map +0 -1
- package/output/app/smoke.test.js +0 -210
- package/output/app/test-support.d.ts +0 -21
- package/output/app/test-support.d.ts.map +0 -1
- package/output/app/test-support.js +0 -24
- package/output/app/type-augment.d.ts.map +0 -1
- package/output/app/types.d.ts +0 -211
- package/output/app/types.d.ts.map +0 -1
- package/output/context/index.d.ts +0 -6
- package/output/context/index.d.ts.map +0 -1
- package/output/context/index.js +0 -14
- package/output/rate-limiter/index.d.ts +0 -5
- package/output/rate-limiter/index.d.ts.map +0 -1
- package/output/rate-limiter/index.js +0 -4
- package/output/rate-limiter/memory-store.d.ts.map +0 -1
- package/output/rate-limiter/rate-limiter.d.ts.map +0 -1
- package/output/rate-limiter/redis-store.d.ts.map +0 -1
- package/output/rate-limiter/redis-store.test.d.ts.map +0 -1
- package/output/rate-limiter/types.d.ts +0 -27
- package/output/rate-limiter/types.d.ts.map +0 -1
- package/output/redis-cache/index.d.ts +0 -4
- package/output/redis-cache/index.d.ts.map +0 -1
- package/output/redis-cache/index.js +0 -1
- package/output/redis-cache/redis-cache.d.ts.map +0 -1
- package/output/redis-cache/redis-cache.test.d.ts +0 -2
- package/output/redis-cache/redis-cache.test.d.ts.map +0 -1
- package/output/schema/index.d.ts.map +0 -1
- /package/output/{app → core}/env.test.d.ts +0 -0
- /package/output/{rate-limiter → core/modules/auth-provider}/types.js +0 -0
- /package/output/{context → core/modules/context}/index.test.d.ts +0 -0
- /package/output/{context → core/modules/context}/index.test.js +0 -0
- /package/output/{app → core}/type-augment.js +0 -0
- /package/output/{app → core}/types.js +0 -0
- /package/output/{rate-limiter/rate-limiter.js → rate-limit/limiter.js} +0 -0
- /package/output/{rate-limiter → rate-limit}/redis-store.test.d.ts +0 -0
- /package/output/{redis-cache/redis-cache.js → redis/cache.js} +0 -0
|
@@ -1,26 +1,39 @@
|
|
|
1
|
+
import { SystemError, UserError } from '@taicode/common-base';
|
|
1
2
|
import { hasZodFastifySchemaValidationErrors, isResponseSerializationError } from 'fastify-type-provider-zod';
|
|
2
|
-
import { SystemError, UserError } from '../errors.js';
|
|
3
3
|
/**
|
|
4
4
|
* core `response`(设计 §5.1 / §8,错误契约 D7)——**框架内置、锁定、不可替换/修改**。
|
|
5
5
|
*
|
|
6
6
|
* - 成功信封 `{ status: 'SUCCESS', message: 'ok', data }`(由 `preSerialization` 统一包裹)。
|
|
7
7
|
* - 错误信封 `{ status: <错误码字符串>, message, data: null }`。
|
|
8
|
-
* - `setErrorHandler` 固定分支:`UserError`→400
|
|
9
|
-
* zod→400 / serde→500 /
|
|
8
|
+
* - `setErrorHandler` 固定分支:`UserError`→400 / `SystemError`→500 /
|
|
9
|
+
* zod→400 / serde→500 / 资源(各资源 `ctx.registerClassifyError` 注册的分类器)→500 / 未知→500;**禁止按 `error.type` 细分**。
|
|
10
10
|
* - `setNotFoundHandler` → 404 + `NOT_IMPLEMENTED`(落 `warn`,文案对齐 expense/notice)。
|
|
11
11
|
* - 错误日志分级:`UserError` / zod → `warn`,其余 → `error`(带 stack)。
|
|
12
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` 细分**。
|
|
13
23
|
*/
|
|
14
|
-
|
|
15
|
-
|
|
16
|
-
|
|
17
|
-
|
|
18
|
-
|
|
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';
|
|
19
31
|
/** 通用兜底文案(仅在 `error.message` 为空时使用) */
|
|
20
|
-
|
|
21
|
-
|
|
32
|
+
const DEFAULT_SYSTEM_MESSAGE = '系统错误';
|
|
33
|
+
/** 成功信封的固定 `message`(错误信封的 `message` 是业务文案,不固定) */
|
|
34
|
+
const SUCCESS_MESSAGE = 'ok';
|
|
22
35
|
export function createSuccessResponse(data) {
|
|
23
|
-
return { status: SUCCESS_STATUS, message:
|
|
36
|
+
return { status: SUCCESS_STATUS, message: SUCCESS_MESSAGE, data };
|
|
24
37
|
}
|
|
25
38
|
export function createErrorResponse(status, message) {
|
|
26
39
|
return { status, message, data: null };
|
|
@@ -55,12 +68,12 @@ export function registerResponse(app, options = {}) {
|
|
|
55
68
|
// ① UserError → 400
|
|
56
69
|
if (UserError.is(error)) {
|
|
57
70
|
request.log.warn({ err: error }, '[response] user error');
|
|
58
|
-
return reply.code(
|
|
71
|
+
return reply.code(USER_ERROR_STATUS).send(createErrorResponse(error.type, resolveMessage(error)));
|
|
59
72
|
}
|
|
60
73
|
// ② SystemError → 500
|
|
61
74
|
if (SystemError.is(error)) {
|
|
62
75
|
request.log.error({ err: error }, '[response] system error');
|
|
63
|
-
return reply.code(
|
|
76
|
+
return reply.code(SYSTEM_ERROR_STATUS).send(createErrorResponse(error.type, resolveMessage(error)));
|
|
64
77
|
}
|
|
65
78
|
// ③ zod 校验失败 → 400
|
|
66
79
|
if (hasZodFastifySchemaValidationErrors(error)) {
|
|
@@ -72,13 +85,13 @@ export function registerResponse(app, options = {}) {
|
|
|
72
85
|
request.log.error({ err: error }, '[response] response serialization error');
|
|
73
86
|
return reply.code(500).send(createErrorResponse(SERIALIZATION_ERROR_STATUS, error.message));
|
|
74
87
|
}
|
|
75
|
-
// ⑤
|
|
76
|
-
const
|
|
77
|
-
if (
|
|
78
|
-
request.log.error({ err: error }, '[response]
|
|
88
|
+
// ⑤ 资源错误(各资源注册的分类器依次认领)→ 500
|
|
89
|
+
const resourceError = options.classifyResourceError?.(error);
|
|
90
|
+
if (resourceError) {
|
|
91
|
+
request.log.error({ err: error }, '[response] resource error');
|
|
79
92
|
return reply
|
|
80
93
|
.code(500)
|
|
81
|
-
.send(createErrorResponse(
|
|
94
|
+
.send(createErrorResponse(resourceError.type, resolveMessage(resourceError)));
|
|
82
95
|
}
|
|
83
96
|
// ⑥ 未知 → 500
|
|
84
97
|
request.log.error({ err: error }, '[response] unknown error');
|
|
@@ -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=
|
|
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 @@
|
|
|
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"}
|
|
@@ -0,0 +1,205 @@
|
|
|
1
|
+
import { catchIt, SystemError } from '@taicode/common-base';
|
|
2
|
+
import { z } from 'zod';
|
|
3
|
+
import { createSuccessResponse } from '../response/index.js';
|
|
4
|
+
import { routerSchema } from '../schema/builders.js';
|
|
5
|
+
/**
|
|
6
|
+
* 框架内置系统路由(`app/core/system-routes.ts`)——**框架自动注册、应用侧零注册**。
|
|
7
|
+
*
|
|
8
|
+
* 背景:各应用此前各自维护一份 `modules/system/router.ts`(account / release / notice /
|
|
9
|
+
* expense / open-trade / observai 各写一份),契约容易漂移。现收进框架 core,由 `registerSystemRoutesStep`
|
|
10
|
+
* 在 `routers` 阶段**之前**统一注册,从 `AppIdentity` + `ctx.services` + `ctx.readConfig` 派生。
|
|
11
|
+
*
|
|
12
|
+
* 与既有应用 router 对齐的契约(路径 / 方法 / 响应信封 / NoLogin 语义):
|
|
13
|
+
*
|
|
14
|
+
* | 方法 | 路径 | 说明 | 信封 `data` |
|
|
15
|
+
* |------|------|------|-------------|
|
|
16
|
+
* | GET | `/` | 服务可用性 | `string`(`identity.service`) |
|
|
17
|
+
* | GET | `/health` | k8s probe(NoLoginRequired) | `{ healthy: true }` |
|
|
18
|
+
* | GET | `/system/openapi`| OpenAPI 文档(NoLoginRequired,**原样返回、不套信封**) | — |
|
|
19
|
+
* | POST | `/system/health` | API 版健康检查 | `{ healthy: true }` |
|
|
20
|
+
* | POST | `/system/info` | 版本 + 运行时信息(NoLoginRequired,**不得含管理员等内部配置**) | `{ version, runtime }` |
|
|
21
|
+
*
|
|
22
|
+
* 健康检查:遍历 `services` 的**可选** `health()`,任一失败 / 非 `true` → 整体不健康,
|
|
23
|
+
* 抛 `SystemError('UNKNOWN_ERROR', { operation: 'healthCheck' })`(错误码与信封稳定,不得改)。
|
|
24
|
+
*
|
|
25
|
+
* `readConfig` 为**请求期惰性读取**(出 spec 路径不跑 `config` 槽位,故任何读取都必须发生在
|
|
26
|
+
* 请求处理器内,而非注册期)。
|
|
27
|
+
*/
|
|
28
|
+
/**
|
|
29
|
+
* 响应信封助手:**直接用 `core/response` 的实现**。
|
|
30
|
+
*
|
|
31
|
+
* 这里曾经保留一份更窄的本地副本,理由写的是「框架的 `createSuccessResponse`
|
|
32
|
+
* 返回 `status: string` 过宽」——那其实是框架自己的类型缺陷(见 `core/modules/response`
|
|
33
|
+
* 里 `SuccessResponse` 的说明),修在源头之后副本就没有存在理由了。
|
|
34
|
+
*
|
|
35
|
+
* 同一个信封在一个仓里有多份实现,迟早会出现「某一份改了、其他没改」的局部不一致,
|
|
36
|
+
* 而它们的目标形状本应完全一样。
|
|
37
|
+
*/
|
|
38
|
+
/** 响应信封(成功 `status: 'SUCCESS'`;错误 `data: null`,`status` 放稳定错误码字符串) */
|
|
39
|
+
function successEnvelope(data) {
|
|
40
|
+
return z.object({
|
|
41
|
+
status: z.enum(['SUCCESS']),
|
|
42
|
+
message: z.string(),
|
|
43
|
+
data,
|
|
44
|
+
});
|
|
45
|
+
}
|
|
46
|
+
/** 错误分支信封:`status` 放开为字符串(错误码集合不由框架枚举),`data` 恒为 `null` */
|
|
47
|
+
function errorEnvelope() {
|
|
48
|
+
return z.object({
|
|
49
|
+
status: z.string(),
|
|
50
|
+
message: z.string(),
|
|
51
|
+
data: z.null(),
|
|
52
|
+
});
|
|
53
|
+
}
|
|
54
|
+
/** 200 / 400 / 500 三分支响应(与各应用 `responseSchema` 同形) */
|
|
55
|
+
function responseEnvelopeSchema(data) {
|
|
56
|
+
return {
|
|
57
|
+
200: successEnvelope(data),
|
|
58
|
+
400: errorEnvelope(),
|
|
59
|
+
500: errorEnvelope(),
|
|
60
|
+
};
|
|
61
|
+
}
|
|
62
|
+
const RootSchema = routerSchema({
|
|
63
|
+
operationId: 'getRoot',
|
|
64
|
+
summary: '根路由',
|
|
65
|
+
tags: ['系统'],
|
|
66
|
+
description: '服务可用性检查(无需认证)',
|
|
67
|
+
response: responseEnvelopeSchema(z.string()),
|
|
68
|
+
});
|
|
69
|
+
const HealthCheckSchema = routerSchema({
|
|
70
|
+
operationId: 'healthCheck',
|
|
71
|
+
summary: '健康检查',
|
|
72
|
+
tags: ['系统'],
|
|
73
|
+
description: '检查服务及其依赖是否正常运行(无需认证,用于 k8s readiness/liveness probe)',
|
|
74
|
+
response: responseEnvelopeSchema(z.object({ healthy: z.boolean().describe('服务是否健康') })),
|
|
75
|
+
});
|
|
76
|
+
const SystemHealthCheckSchema = routerSchema({
|
|
77
|
+
operationId: 'systemHealthCheck',
|
|
78
|
+
summary: '健康检查(API)',
|
|
79
|
+
tags: ['系统'],
|
|
80
|
+
description: '检查服务及其依赖是否正常运行(API 调用版)',
|
|
81
|
+
response: responseEnvelopeSchema(z.object({ healthy: z.boolean().describe('服务是否健康') })),
|
|
82
|
+
});
|
|
83
|
+
const SystemInfoSchema = routerSchema({
|
|
84
|
+
operationId: 'getSystemInfo',
|
|
85
|
+
summary: '获取系统信息',
|
|
86
|
+
tags: ['系统'],
|
|
87
|
+
description: '获取系统版本、运行时配置等信息(无需认证)',
|
|
88
|
+
response: responseEnvelopeSchema(z.object({
|
|
89
|
+
version: z
|
|
90
|
+
.object({
|
|
91
|
+
app: z.string().describe('应用版本'),
|
|
92
|
+
name: z.string().describe('应用名称(`identity.service`)'),
|
|
93
|
+
title: z.string().describe('应用标题(`identity.title`)'),
|
|
94
|
+
node: z.string().describe('Node.js 版本'),
|
|
95
|
+
platform: z.string().describe('运行平台'),
|
|
96
|
+
buildTime: z.string().optional().describe('构建时间'),
|
|
97
|
+
gitCommit: z.string().optional().describe('Git 提交哈希'),
|
|
98
|
+
})
|
|
99
|
+
.describe('版本信息'),
|
|
100
|
+
runtime: z
|
|
101
|
+
.object({
|
|
102
|
+
serverUrl: z.string().nullable().describe('服务公网地址'),
|
|
103
|
+
cookieNames: z
|
|
104
|
+
.array(z.string())
|
|
105
|
+
.describe('Cookie 名称列表(取自已加载配置的 `server.cookieNames`)'),
|
|
106
|
+
})
|
|
107
|
+
.describe('运行时配置'),
|
|
108
|
+
})),
|
|
109
|
+
});
|
|
110
|
+
function readStringArray(value) {
|
|
111
|
+
return Array.isArray(value) ? value.filter((item) => typeof item === 'string') : undefined;
|
|
112
|
+
}
|
|
113
|
+
/** 遍历 services 的可选 `health()`:任一失败 / 非 `true` → false */
|
|
114
|
+
async function checkHealth(services) {
|
|
115
|
+
for (const service of Object.values(services)) {
|
|
116
|
+
const health = service?.health;
|
|
117
|
+
if (typeof health !== 'function') {
|
|
118
|
+
continue;
|
|
119
|
+
}
|
|
120
|
+
const result = await catchIt(health());
|
|
121
|
+
if (result.isError()) {
|
|
122
|
+
return false;
|
|
123
|
+
}
|
|
124
|
+
if (result.value !== true) {
|
|
125
|
+
return false;
|
|
126
|
+
}
|
|
127
|
+
}
|
|
128
|
+
return true;
|
|
129
|
+
}
|
|
130
|
+
/** `/health` 与 `/system/health` 的共用处理器:不健康则抛 `UNKNOWN_ERROR`(k8s probe 依赖 5xx) */
|
|
131
|
+
async function assertHealthy(services) {
|
|
132
|
+
const result = await catchIt(checkHealth(services));
|
|
133
|
+
const ok = result.isError() ? false : result.value;
|
|
134
|
+
if (!ok) {
|
|
135
|
+
throw new SystemError('UNKNOWN_ERROR', {
|
|
136
|
+
operation: 'healthCheck',
|
|
137
|
+
cause: result.isError() ? result.error : 'Service health check failed',
|
|
138
|
+
});
|
|
139
|
+
}
|
|
140
|
+
}
|
|
141
|
+
/** `GET /system/openapi` 的路由元信息 */
|
|
142
|
+
const OpenApiSpecSchema = routerSchema({
|
|
143
|
+
operationId: 'getOpenApiDocument',
|
|
144
|
+
summary: '获取 OpenAPI 文档',
|
|
145
|
+
tags: ['系统'],
|
|
146
|
+
description: '返回本服务的 OpenAPI 文档(无需认证,原样返回、不套响应信封)',
|
|
147
|
+
});
|
|
148
|
+
/**
|
|
149
|
+
* 注册框架内置系统路由(5 个端点)。由 `registerSystemRoutesStep` 在 `server.routers(...)` **之前**调用,
|
|
150
|
+
* 保证框架路由先就位。
|
|
151
|
+
*/
|
|
152
|
+
export function registerSystemRoutes(app, options) {
|
|
153
|
+
const { identity, services, readConfig } = options;
|
|
154
|
+
// GET / —— 服务可用性(服务名取自身份块)
|
|
155
|
+
app.get('/', { config: { accessControls: ['NoLoginRequired'] }, schema: RootSchema }, async () => {
|
|
156
|
+
return createSuccessResponse(identity.service);
|
|
157
|
+
});
|
|
158
|
+
// GET /health —— k8s probe(保持 GET,NoLoginRequired)
|
|
159
|
+
app.get('/health', { config: { accessControls: ['NoLoginRequired'] }, schema: HealthCheckSchema }, async () => {
|
|
160
|
+
await assertHealthy(services);
|
|
161
|
+
return createSuccessResponse({ healthy: true });
|
|
162
|
+
});
|
|
163
|
+
// POST /system/health —— API 版健康检查
|
|
164
|
+
app.post('/system/health', { schema: SystemHealthCheckSchema }, async () => {
|
|
165
|
+
await assertHealthy(services);
|
|
166
|
+
return createSuccessResponse({ healthy: true });
|
|
167
|
+
});
|
|
168
|
+
// GET /system/openapi —— OpenAPI 文档出口(NoLoginRequired)
|
|
169
|
+
//
|
|
170
|
+
// **原样返回、不套信封**:文档是机器可读契约(各仓 `api-gen` / `openapi-ts` 直接消费),
|
|
171
|
+
// 包一层 `{ status, message, data }` 就不再是合法 OpenAPI 文档。
|
|
172
|
+
// 实现上返回**已序列化的 JSON 字符串**:Fastify 对 `string` payload 不触发
|
|
173
|
+
// `preSerialization`,`core/response` 的统一信封因而不会介入;`content-type` 显式设为
|
|
174
|
+
// `application/json` 保证客户端按 JSON 解析。
|
|
175
|
+
app.get('/system/openapi', { config: { accessControls: ['NoLoginRequired'] }, schema: OpenApiSpecSchema }, async (_request, reply) => {
|
|
176
|
+
const document = app.swagger();
|
|
177
|
+
return reply.type('application/json').send(JSON.stringify(document));
|
|
178
|
+
});
|
|
179
|
+
// POST /system/info —— 版本 + 运行时(请求期现读配置)
|
|
180
|
+
//
|
|
181
|
+
// ⚠️ **这是公开接口**(不含 `admin` 等内部配置):任何从这里出去的字段都等于公开。
|
|
182
|
+
// 管理员用户名 / 内部账号列表这类信息**永远不返回**——它是撞库与提权的直接输入,
|
|
183
|
+
// 不是「运行时信息」。需要排查管理员配置走运维通道(日志 / 数据库),不走 HTTP。
|
|
184
|
+
//
|
|
185
|
+
// `NoLoginRequired` 与「它是公开接口」是同一件事的两半,必须成对声明:少了这一行,
|
|
186
|
+
// auth 中间件会把探活 / 版本查询挡成 400,客户端与探针都拿不到运行时信息。
|
|
187
|
+
app.post('/system/info', { config: { accessControls: ['NoLoginRequired'] }, schema: SystemInfoSchema }, async () => {
|
|
188
|
+
const server = readConfig('server');
|
|
189
|
+
return createSuccessResponse({
|
|
190
|
+
version: {
|
|
191
|
+
app: identity.version,
|
|
192
|
+
name: identity.service,
|
|
193
|
+
title: identity.title,
|
|
194
|
+
node: process.version,
|
|
195
|
+
platform: process.platform,
|
|
196
|
+
buildTime: process.env.BUILD_TIME,
|
|
197
|
+
gitCommit: process.env.GIT_COMMIT,
|
|
198
|
+
},
|
|
199
|
+
runtime: {
|
|
200
|
+
serverUrl: server?.url ?? server?.serverUrl ?? null,
|
|
201
|
+
cookieNames: readStringArray(server?.cookieNames) ?? [],
|
|
202
|
+
},
|
|
203
|
+
});
|
|
204
|
+
});
|
|
205
|
+
}
|
|
@@ -0,0 +1,56 @@
|
|
|
1
|
+
import type { ResourceLifecycle, ResourceUnit } from './types.js';
|
|
2
|
+
/**
|
|
3
|
+
* 构造资源单元(唯一的正当方式)。
|
|
4
|
+
*
|
|
5
|
+
* ## 为什么需要外壳
|
|
6
|
+
*
|
|
7
|
+
* 资源多是**第三方对象**:`PrismaClient`、Redis client、HTTP 连接池⋯⋯它们的形状改不了。
|
|
8
|
+
* service 可以直接在对象上挂 `init()`(因为 service 是本仓自己写的),资源不行——
|
|
9
|
+
* `prisma.setup = …` 是在改别人的类型,运行期也未必允许。
|
|
10
|
+
*
|
|
11
|
+
* 所以 `setup()` 挂在**框架私有外壳**上,`value` 原样保留:
|
|
12
|
+
*
|
|
13
|
+
* ```ts
|
|
14
|
+
* resources: (ctx) => ({
|
|
15
|
+
* database: defineResource(createPrisma(ctx.env.DATABASE_URL), {
|
|
16
|
+
* async setup() { await pushSchema(ctx.env.DATABASE_URL) },
|
|
17
|
+
* }),
|
|
18
|
+
* })
|
|
19
|
+
* ```
|
|
20
|
+
*
|
|
21
|
+
* 消费方读 `ctx.resources.database` 拿到的是 `PrismaClient` 本身
|
|
22
|
+
* (`ResourcesView` 会解包),不是 `ResourceUnit<PrismaClient>`。
|
|
23
|
+
*
|
|
24
|
+
* ## 单元是可选的
|
|
25
|
+
*
|
|
26
|
+
* 裸对象照旧可用(无 `setup`、无解包开销):`resources: () => ({ database: prisma })`。
|
|
27
|
+
* 只有需要「装配期准备」的资源才包一层。
|
|
28
|
+
*
|
|
29
|
+
* ## `setup` 何时跑、谁跑
|
|
30
|
+
*
|
|
31
|
+
* - **何时**:`resources` 槽位构造完成后、`config` 槽位之前;按**对象声明序**逐项 `await`。
|
|
32
|
+
* 排在 `config` 前是硬要求——`config` 常从数据库读(如 `SystemConfig` 表),库没准备好
|
|
33
|
+
* 它就连不上。
|
|
34
|
+
* - **谁**:框架。`startServer` / `createTestServer` 都调;`generateOpenApi` 不调
|
|
35
|
+
* (出 spec 不构造资源)。应用**永不手调**。
|
|
36
|
+
* - **失败**:立即中断装配并把错误抛给入口(已构造的资源照常释放)。资源准备失败后
|
|
37
|
+
* `config` / `services` 都没有意义,所以不吞。
|
|
38
|
+
*
|
|
39
|
+
* ## 不要在里面写 `if (ENV === 'test')`
|
|
40
|
+
*
|
|
41
|
+
* 准备动作本身应当是**状态驱动**的:`prisma db push` / `migrate deploy` 都会自己比对现状、
|
|
42
|
+
* 只补差量,生产与测试跑同一份代码。需要区分环境的细节应当从 `ctx.env` 取,而不是让
|
|
43
|
+
* 「跑不跑这个函数」变成环境分支。
|
|
44
|
+
*
|
|
45
|
+
* ## 用例间重置不在本文件
|
|
46
|
+
*
|
|
47
|
+
* 「清表 / 清缓存 / 重新插入样本数据」不适合挂在资源上:资源只知道有哪些表、哪些 key,
|
|
48
|
+
* 不知道**谁往里写了什么**。写在资源上就变成了「新增一个 model 就得回去改一次清库清单」,
|
|
49
|
+
* 漏改只会安静地变脏。
|
|
50
|
+
*
|
|
51
|
+
* 所以它单独成机制:**`packages/server/testdata/*.ts|js`,每个脚本自带 `load()` / `cleanup()`**,
|
|
52
|
+
* 由框架在 `development` / `test` 下自动装载并执行(见 `core/test-support/testdata.ts`)。
|
|
53
|
+
* 数据脚本自己知道要插入什么,也就自己知道怎么删干净。
|
|
54
|
+
*/
|
|
55
|
+
export declare function defineResource<Value>(value: Value, lifecycle?: ResourceLifecycle): ResourceUnit<Value>;
|
|
56
|
+
//# sourceMappingURL=resource.d.ts.map
|
|
@@ -0,0 +1 @@
|
|
|
1
|
+
{"version":3,"file":"resource.d.ts","sourceRoot":"","sources":["../../source/core/resource.ts"],"names":[],"mappings":"AAAA,OAAO,KAAK,EAAE,iBAAiB,EAAE,YAAY,EAAE,MAAM,YAAY,CAAA;AAEjE;;;;;;;;;;;;;;;;;;;;;;;;;;;;;;;;;;;;;;;;;;;;;;;;;;;;GAoDG;AACH,wBAAgB,cAAc,CAAC,KAAK,EAAE,KAAK,EAAE,KAAK,EAAE,SAAS,CAAC,EAAE,iBAAiB,GAAG,YAAY,CAAC,KAAK,CAAC,CAEtG"}
|
|
@@ -0,0 +1,56 @@
|
|
|
1
|
+
/**
|
|
2
|
+
* 构造资源单元(唯一的正当方式)。
|
|
3
|
+
*
|
|
4
|
+
* ## 为什么需要外壳
|
|
5
|
+
*
|
|
6
|
+
* 资源多是**第三方对象**:`PrismaClient`、Redis client、HTTP 连接池⋯⋯它们的形状改不了。
|
|
7
|
+
* service 可以直接在对象上挂 `init()`(因为 service 是本仓自己写的),资源不行——
|
|
8
|
+
* `prisma.setup = …` 是在改别人的类型,运行期也未必允许。
|
|
9
|
+
*
|
|
10
|
+
* 所以 `setup()` 挂在**框架私有外壳**上,`value` 原样保留:
|
|
11
|
+
*
|
|
12
|
+
* ```ts
|
|
13
|
+
* resources: (ctx) => ({
|
|
14
|
+
* database: defineResource(createPrisma(ctx.env.DATABASE_URL), {
|
|
15
|
+
* async setup() { await pushSchema(ctx.env.DATABASE_URL) },
|
|
16
|
+
* }),
|
|
17
|
+
* })
|
|
18
|
+
* ```
|
|
19
|
+
*
|
|
20
|
+
* 消费方读 `ctx.resources.database` 拿到的是 `PrismaClient` 本身
|
|
21
|
+
* (`ResourcesView` 会解包),不是 `ResourceUnit<PrismaClient>`。
|
|
22
|
+
*
|
|
23
|
+
* ## 单元是可选的
|
|
24
|
+
*
|
|
25
|
+
* 裸对象照旧可用(无 `setup`、无解包开销):`resources: () => ({ database: prisma })`。
|
|
26
|
+
* 只有需要「装配期准备」的资源才包一层。
|
|
27
|
+
*
|
|
28
|
+
* ## `setup` 何时跑、谁跑
|
|
29
|
+
*
|
|
30
|
+
* - **何时**:`resources` 槽位构造完成后、`config` 槽位之前;按**对象声明序**逐项 `await`。
|
|
31
|
+
* 排在 `config` 前是硬要求——`config` 常从数据库读(如 `SystemConfig` 表),库没准备好
|
|
32
|
+
* 它就连不上。
|
|
33
|
+
* - **谁**:框架。`startServer` / `createTestServer` 都调;`generateOpenApi` 不调
|
|
34
|
+
* (出 spec 不构造资源)。应用**永不手调**。
|
|
35
|
+
* - **失败**:立即中断装配并把错误抛给入口(已构造的资源照常释放)。资源准备失败后
|
|
36
|
+
* `config` / `services` 都没有意义,所以不吞。
|
|
37
|
+
*
|
|
38
|
+
* ## 不要在里面写 `if (ENV === 'test')`
|
|
39
|
+
*
|
|
40
|
+
* 准备动作本身应当是**状态驱动**的:`prisma db push` / `migrate deploy` 都会自己比对现状、
|
|
41
|
+
* 只补差量,生产与测试跑同一份代码。需要区分环境的细节应当从 `ctx.env` 取,而不是让
|
|
42
|
+
* 「跑不跑这个函数」变成环境分支。
|
|
43
|
+
*
|
|
44
|
+
* ## 用例间重置不在本文件
|
|
45
|
+
*
|
|
46
|
+
* 「清表 / 清缓存 / 重新插入样本数据」不适合挂在资源上:资源只知道有哪些表、哪些 key,
|
|
47
|
+
* 不知道**谁往里写了什么**。写在资源上就变成了「新增一个 model 就得回去改一次清库清单」,
|
|
48
|
+
* 漏改只会安静地变脏。
|
|
49
|
+
*
|
|
50
|
+
* 所以它单独成机制:**`packages/server/testdata/*.ts|js`,每个脚本自带 `load()` / `cleanup()`**,
|
|
51
|
+
* 由框架在 `development` / `test` 下自动装载并执行(见 `core/test-support/testdata.ts`)。
|
|
52
|
+
* 数据脚本自己知道要插入什么,也就自己知道怎么删干净。
|
|
53
|
+
*/
|
|
54
|
+
export function defineResource(value, lifecycle) {
|
|
55
|
+
return lifecycle ? { value, ...lifecycle } : { value };
|
|
56
|
+
}
|