@taicode/common-server 4.0.0-beta.2 → 4.0.0-beta.4
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 +13 -15
- package/output/app/index.d.ts.map +1 -1
- package/output/app/index.js +256 -18
- package/output/auth/index.d.ts +72 -0
- package/output/auth/index.d.ts.map +1 -0
- package/output/auth/index.js +176 -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 +89 -0
- package/output/core/boot/context.d.ts.map +1 -0
- package/output/core/boot/context.js +106 -0
- package/output/core/boot/lifecycle.d.ts +94 -0
- package/output/core/boot/lifecycle.d.ts.map +1 -0
- package/output/{app → core/boot}/lifecycle.js +45 -14
- package/output/core/boot/shutdown.d.ts +30 -0
- package/output/core/boot/shutdown.d.ts.map +1 -0
- package/output/core/boot/shutdown.js +45 -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 +107 -0
- package/output/core/boot/steps.d.ts.map +1 -0
- package/output/core/boot/steps.js +200 -0
- package/output/core/entries.d.ts +123 -0
- package/output/core/entries.d.ts.map +1 -0
- package/output/core/entries.js +193 -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 +217 -0
- package/output/core/env.d.ts +62 -0
- package/output/core/env.d.ts.map +1 -0
- package/output/core/env.js +96 -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 +22 -0
- package/output/core/modules/response/index.d.ts.map +1 -0
- package/output/{app/core/response.js → core/modules/response/index.js} +22 -11
- 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 +171 -0
- package/output/core/test-support/database.d.ts +96 -0
- package/output/core/test-support/database.d.ts.map +1 -0
- package/output/core/test-support/database.js +144 -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 +110 -0
- package/output/core/test-support/environment.d.ts +33 -0
- package/output/core/test-support/environment.d.ts.map +1 -0
- package/output/core/test-support/environment.js +42 -0
- package/output/core/test-support/test-server.d.ts +94 -0
- package/output/core/test-support/test-server.d.ts.map +1 -0
- package/output/core/test-support/test-server.js +60 -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 +162 -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 +338 -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 +27 -0
- package/output/index.d.ts +8 -4
- package/output/index.d.ts.map +1 -1
- package/output/index.js +24 -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 +42 -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-limit/memory-store.d.ts.map +1 -0
- package/output/rate-limit/redis-store.d.ts.map +1 -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-limiter → rate-limit}/types.d.ts +0 -1
- 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 +16 -15
- package/LICENSE +0 -21
- 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 -39
- package/output/app/boot.d.ts.map +0 -1
- package/output/app/boot.js +0 -140
- 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 -70
- 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 -40
- package/output/app/define-server.negative-cases.d.ts.map +0 -1
- package/output/app/define-server.negative-cases.js +0 -268
- 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 -227
- 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.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 -199
- 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 -228
- 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.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}/memory-store.d.ts +0 -0
- /package/output/{rate-limiter → rate-limit}/memory-store.js +0 -0
- /package/output/{rate-limiter → rate-limit}/redis-store.d.ts +0 -0
- /package/output/{rate-limiter → rate-limit}/redis-store.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
|
@@ -0,0 +1,217 @@
|
|
|
1
|
+
import { beforeAll, describe, expect, it, vi } from 'vitest';
|
|
2
|
+
import { createAuthMiddleware } from '../auth/index.js';
|
|
3
|
+
import { defineServer, generateOpenApi, startServer } from './entries.js';
|
|
4
|
+
import { routerSchema } from './modules/schema/builders.js';
|
|
5
|
+
const IDENTITY = { service: 'probe', title: 'Probe', version: '0.0.0' };
|
|
6
|
+
const ROUTE_SCHEMA = routerSchema({
|
|
7
|
+
operationId: 'probeGet',
|
|
8
|
+
summary: '探测',
|
|
9
|
+
description: '探测路由',
|
|
10
|
+
tags: ['探测'],
|
|
11
|
+
});
|
|
12
|
+
/**
|
|
13
|
+
* 出 spec 用的定义:`config` 工厂**故意抛错**、`middlewares` 工厂记调用次数。
|
|
14
|
+
*
|
|
15
|
+
* 两者都能跑通就证明:出 spec 路径**既不需要配置有效**,**也不装配任何中间件**
|
|
16
|
+
* (它的文档只取决于路由声明 + zod 转换)。
|
|
17
|
+
*/
|
|
18
|
+
function buildServer(registerPermissionOperations, onMiddlewares = vi.fn()) {
|
|
19
|
+
return defineServer({
|
|
20
|
+
app: IDENTITY,
|
|
21
|
+
resources: () => ({}),
|
|
22
|
+
config: () => {
|
|
23
|
+
throw new Error('config 不应在出 spec 路径被调用');
|
|
24
|
+
},
|
|
25
|
+
clients: () => ({}),
|
|
26
|
+
services: () => ({}),
|
|
27
|
+
middlewares: (ctx) => {
|
|
28
|
+
onMiddlewares();
|
|
29
|
+
return {
|
|
30
|
+
auth: createAuthMiddleware(ctx, {
|
|
31
|
+
client: { verify: async () => ({ user: {}, token: 't' }), registerPermissionOperations },
|
|
32
|
+
}),
|
|
33
|
+
};
|
|
34
|
+
},
|
|
35
|
+
routers: ({ app }) => {
|
|
36
|
+
app.get('/probe', { schema: ROUTE_SCHEMA }, async () => ({ ok: true }));
|
|
37
|
+
},
|
|
38
|
+
});
|
|
39
|
+
}
|
|
40
|
+
/** 启动用的定义:`config` 正常返回值(`startServer` 会跑它)。 */
|
|
41
|
+
function buildStartableServer(registerPermissionOperations) {
|
|
42
|
+
return defineServer({
|
|
43
|
+
app: IDENTITY,
|
|
44
|
+
resources: () => ({}),
|
|
45
|
+
config: () => ({ server: { cookieNames: ['token'] } }),
|
|
46
|
+
clients: () => ({}),
|
|
47
|
+
services: () => ({}),
|
|
48
|
+
middlewares: (ctx) => ({
|
|
49
|
+
auth: createAuthMiddleware(ctx, {
|
|
50
|
+
client: { verify: async () => ({ user: {}, token: 't' }), registerPermissionOperations },
|
|
51
|
+
}),
|
|
52
|
+
}),
|
|
53
|
+
routers: ({ app }) => {
|
|
54
|
+
app.get('/probe', { schema: ROUTE_SCHEMA }, async () => ({ ok: true }));
|
|
55
|
+
},
|
|
56
|
+
});
|
|
57
|
+
}
|
|
58
|
+
describe('generateOpenApi', () => {
|
|
59
|
+
it('产出完整文档(含框架系统路由与业务路由)', async () => {
|
|
60
|
+
const spec = await generateOpenApi(buildServer(async () => { }));
|
|
61
|
+
expect(Object.keys(spec.paths ?? {})).toEqual(expect.arrayContaining(['/', '/health', '/system/openapi', '/system/info', '/probe']));
|
|
62
|
+
expect(spec.info).toEqual({ title: 'Probe', version: '0.0.0' });
|
|
63
|
+
expect(spec.openapi).toBe('3.0.3');
|
|
64
|
+
});
|
|
65
|
+
it('不向鉴权提供方提交权限目录(构建期不产生写操作)', async () => {
|
|
66
|
+
const registerPermissionOperations = vi.fn(async () => { });
|
|
67
|
+
await generateOpenApi(buildServer(registerPermissionOperations));
|
|
68
|
+
expect(registerPermissionOperations).not.toHaveBeenCalled();
|
|
69
|
+
});
|
|
70
|
+
it('不装配中间件(文档只取决于路由声明,副作用随之消失)', async () => {
|
|
71
|
+
const onMiddlewares = vi.fn();
|
|
72
|
+
await generateOpenApi(buildServer(async () => { }, onMiddlewares));
|
|
73
|
+
// `auth` 单元在出文档路径里压根不会被构造——这是「不向鉴权方写数据」的根因,
|
|
74
|
+
// 而不是靠某个入口标记把它关掉。
|
|
75
|
+
expect(onMiddlewares).not.toHaveBeenCalled();
|
|
76
|
+
});
|
|
77
|
+
it('不构造 resources / config / clients / services(文档不来自它们,构造它们只为了跑服务)', async () => {
|
|
78
|
+
const onResources = vi.fn();
|
|
79
|
+
const onServices = vi.fn();
|
|
80
|
+
const onClients = vi.fn();
|
|
81
|
+
const server = defineServer({
|
|
82
|
+
app: IDENTITY,
|
|
83
|
+
resources: () => {
|
|
84
|
+
onResources();
|
|
85
|
+
return {};
|
|
86
|
+
},
|
|
87
|
+
config: () => {
|
|
88
|
+
throw new Error('config 不应在出 spec 路径被调用');
|
|
89
|
+
},
|
|
90
|
+
clients: () => {
|
|
91
|
+
onClients();
|
|
92
|
+
return {};
|
|
93
|
+
},
|
|
94
|
+
services: () => {
|
|
95
|
+
onServices();
|
|
96
|
+
return {};
|
|
97
|
+
},
|
|
98
|
+
middlewares: () => ({}),
|
|
99
|
+
routers: ({ app }) => {
|
|
100
|
+
app.get('/probe', { schema: ROUTE_SCHEMA }, async () => ({ ok: true }));
|
|
101
|
+
},
|
|
102
|
+
});
|
|
103
|
+
const spec = await generateOpenApi(server);
|
|
104
|
+
expect(onResources).not.toHaveBeenCalled();
|
|
105
|
+
expect(onClients).not.toHaveBeenCalled();
|
|
106
|
+
expect(onServices).not.toHaveBeenCalled();
|
|
107
|
+
// 传的是空 services 占位,路由仍能正常注册并进入文档
|
|
108
|
+
expect(Object.keys(spec.paths ?? {})).toContain('/probe');
|
|
109
|
+
});
|
|
110
|
+
it('不跑 config 槽位(出 spec 不要求配置有效)', async () => {
|
|
111
|
+
// `config` 工厂本身就会抛错,能出 spec 即证明它没被调用
|
|
112
|
+
await expect(generateOpenApi(buildServer(async () => { }))).resolves.toBeDefined();
|
|
113
|
+
});
|
|
114
|
+
it('不监听端口(不占用 API_PORT)', async () => {
|
|
115
|
+
const spec = await generateOpenApi(buildServer(async () => { }));
|
|
116
|
+
expect(spec.servers).toEqual([{ url: '/' }]);
|
|
117
|
+
});
|
|
118
|
+
it('业务路由违反 schema 契约时出 spec 直接失败(不给漏声明留活路)', async () => {
|
|
119
|
+
const server = defineServer({
|
|
120
|
+
app: IDENTITY,
|
|
121
|
+
resources: () => ({}),
|
|
122
|
+
config: () => ({}),
|
|
123
|
+
clients: () => ({}),
|
|
124
|
+
services: () => ({}),
|
|
125
|
+
middlewares: () => ({}),
|
|
126
|
+
routers: ({ app }) => {
|
|
127
|
+
// 故意不声明 schema,验证运行期守卫
|
|
128
|
+
app.get('/bad', async () => ({ ok: true }));
|
|
129
|
+
},
|
|
130
|
+
});
|
|
131
|
+
await expect(generateOpenApi(server)).rejects.toThrow(/GET \/bad 缺少 schema/);
|
|
132
|
+
});
|
|
133
|
+
});
|
|
134
|
+
describe('startServer', () => {
|
|
135
|
+
// 入口一定 listen,所以这里要一个合法端口:`0` 让内核分配空闲端口,测试不抢固定端口
|
|
136
|
+
beforeAll(() => {
|
|
137
|
+
process.env.API_PORT = '0';
|
|
138
|
+
});
|
|
139
|
+
it('运行期照常提交权限目录(与出 spec 入口相反)', async () => {
|
|
140
|
+
const registerPermissionOperations = vi.fn(async () => { });
|
|
141
|
+
const app = await startServer(buildStartableServer(registerPermissionOperations));
|
|
142
|
+
// `startServer` 的契约就是「一定 listen」:端口交给内核分配(`API_PORT=0`),不靠开关
|
|
143
|
+
expect(app.server.listening).toBe(true);
|
|
144
|
+
// startServer 内部已 `ready()`,钩子应在返回前执行完
|
|
145
|
+
expect(registerPermissionOperations).toHaveBeenCalledTimes(1);
|
|
146
|
+
const [payload] = registerPermissionOperations.mock.calls[0];
|
|
147
|
+
expect(payload.service).toBe('probe');
|
|
148
|
+
expect(payload.version).toBe('0.0.0');
|
|
149
|
+
expect(payload.operations.map((item) => item.operation)).toEqual(expect.arrayContaining(['probeGet', 'getOpenApiDocument']));
|
|
150
|
+
await app.close();
|
|
151
|
+
});
|
|
152
|
+
it('clients 在 config 之后、services 之前构造,并被 services / middlewares / routers 共享同一实例', async () => {
|
|
153
|
+
const order = [];
|
|
154
|
+
const seen = {};
|
|
155
|
+
const server = defineServer({
|
|
156
|
+
app: IDENTITY,
|
|
157
|
+
resources: () => ({}),
|
|
158
|
+
config: () => {
|
|
159
|
+
order.push('config');
|
|
160
|
+
return { account: { serviceUrl: 'https://account.invalid' } };
|
|
161
|
+
},
|
|
162
|
+
clients: (ctx) => {
|
|
163
|
+
order.push('clients');
|
|
164
|
+
// 客户端地址来自业务配置(不是 env)——这正是它不能放进 resources 的原因
|
|
165
|
+
return { account: { baseUrl: ctx.config.account.serviceUrl } };
|
|
166
|
+
},
|
|
167
|
+
services: (ctx) => {
|
|
168
|
+
order.push('services');
|
|
169
|
+
// 两个 service 拿到的是**同一份**客户端实例:services 之间禁止互依,只能靠这一层共享
|
|
170
|
+
seen.serviceFirst = ctx.clients.account;
|
|
171
|
+
seen.serviceSecond = ctx.clients.account;
|
|
172
|
+
return {};
|
|
173
|
+
},
|
|
174
|
+
middlewares: (ctx) => {
|
|
175
|
+
seen.middleware = ctx.clients.account;
|
|
176
|
+
return {};
|
|
177
|
+
},
|
|
178
|
+
routers: (ctx) => {
|
|
179
|
+
order.push('routers');
|
|
180
|
+
seen.router = ctx.clients.account;
|
|
181
|
+
ctx.app.get('/probe', { schema: ROUTE_SCHEMA }, async () => ({ ok: true }));
|
|
182
|
+
},
|
|
183
|
+
});
|
|
184
|
+
const app = await startServer(server);
|
|
185
|
+
expect(order).toEqual(['config', 'clients', 'services', 'routers']);
|
|
186
|
+
expect(seen.serviceFirst).toEqual({ baseUrl: 'https://account.invalid' });
|
|
187
|
+
expect(seen.serviceSecond).toBe(seen.serviceFirst);
|
|
188
|
+
expect(seen.middleware).toBe(seen.serviceFirst);
|
|
189
|
+
expect(seen.router).toBe(seen.serviceFirst);
|
|
190
|
+
await app.close();
|
|
191
|
+
});
|
|
192
|
+
it('clients 阶段在 services 之后关闭(服务 dispose 时外部客户端仍可用)', async () => {
|
|
193
|
+
const order = [];
|
|
194
|
+
const server = defineServer({
|
|
195
|
+
app: IDENTITY,
|
|
196
|
+
resources: () => ({}),
|
|
197
|
+
config: () => ({}),
|
|
198
|
+
clients: (ctx) => {
|
|
199
|
+
ctx.disposer.add(() => {
|
|
200
|
+
order.push('clients');
|
|
201
|
+
});
|
|
202
|
+
return {};
|
|
203
|
+
},
|
|
204
|
+
services: (ctx) => {
|
|
205
|
+
ctx.disposer.add(() => {
|
|
206
|
+
order.push('services');
|
|
207
|
+
});
|
|
208
|
+
return {};
|
|
209
|
+
},
|
|
210
|
+
middlewares: () => ({}),
|
|
211
|
+
routers: () => { },
|
|
212
|
+
});
|
|
213
|
+
const app = await startServer(server);
|
|
214
|
+
await app.close();
|
|
215
|
+
expect(order).toEqual(['services', 'clients']);
|
|
216
|
+
});
|
|
217
|
+
});
|
|
@@ -0,0 +1,62 @@
|
|
|
1
|
+
/** `LOG_LEVEL` 未设置时的默认级别(设计 §3.1 ① / §5.1 `logging`) */
|
|
2
|
+
export declare const DEFAULT_LOG_LEVEL = "info";
|
|
3
|
+
/**
|
|
4
|
+
* 框架在配置前固定加载的 env(N2 §2.2)。
|
|
5
|
+
*
|
|
6
|
+
* 只读固定约定值:`ENV` / `API_PORT` / `LOG_LEVEL` / `DATABASE_URL` / `REDIS_URL`;
|
|
7
|
+
* `debug` 由 `ENV === 'development'` 派生;**绝不回退 `PORT`**(`PORT` 属 nginx)。
|
|
8
|
+
*
|
|
9
|
+
* **不给索引签名**:访问 `env.XXX` 拼错即编译错误。各仓若有额外 env,不扩展这个类型 ——
|
|
10
|
+
* 走 `process.env.XXX`(在 `resources(ctx)` 里读),框架不背书。
|
|
11
|
+
*/
|
|
12
|
+
export interface ConfigEnv {
|
|
13
|
+
/** 运行环境标记;`debug` 由 `ENV === 'development'` 派生 */
|
|
14
|
+
ENV: string;
|
|
15
|
+
/** 监听端口;**只读 `API_PORT`,绝不回退 `PORT`** */
|
|
16
|
+
API_PORT: string;
|
|
17
|
+
/** 日志级别;默认 `info` */
|
|
18
|
+
LOG_LEVEL: string;
|
|
19
|
+
/** 数据层连接串(`resources` 只读它构造数据层客户端) */
|
|
20
|
+
DATABASE_URL: string;
|
|
21
|
+
/** Redis 连接串;缺省 → redis 资源为 `undefined` */
|
|
22
|
+
REDIS_URL?: string;
|
|
23
|
+
}
|
|
24
|
+
/** 读取固定 env(不加载文件);入口会先 `loadEnvironment()` 再调用本函数 */
|
|
25
|
+
export declare function resolveConfigEnv(): ConfigEnv;
|
|
26
|
+
/**
|
|
27
|
+
* env 加载模式:
|
|
28
|
+
*
|
|
29
|
+
* - `runtime`(默认)`startServer` 与工具进程用:三层开发分层,`ENV=test` 时再叠测试层。
|
|
30
|
+
* - `test` 测试入口用:**无条件**叠测试层,且三/五层都没给出 `ENV` 时兜底成 `test`。
|
|
31
|
+
*/
|
|
32
|
+
export type EnvironmentMode = 'runtime' | 'test';
|
|
33
|
+
/** `loadEnvironment()` 的选项 */
|
|
34
|
+
export interface LoadEnvironmentOptions {
|
|
35
|
+
mode?: EnvironmentMode;
|
|
36
|
+
}
|
|
37
|
+
/**
|
|
38
|
+
* 加载 env 文件并在加载后返回固定约定值。
|
|
39
|
+
*
|
|
40
|
+
* **这是 env 的唯一出口**:层名、顺序、覆盖规则全在框架内部,业务与工具进程只表达
|
|
41
|
+
* 「我要运行期 env」还是「我要测试 env」(`mode: 'test'`),不接触 `.env.dev` /
|
|
42
|
+
* `.env.test.local` 这些文件名。
|
|
43
|
+
*
|
|
44
|
+
* 三种调用方:
|
|
45
|
+
*
|
|
46
|
+
* - `startServer`:`loadEnvironment()`(最早阶段固定调用);
|
|
47
|
+
* - `createTestServer`:经 `prepareTestDatabase()` 间接调用(再跑两道闸门);
|
|
48
|
+
* - 工具进程(`vitest.config.ts` / `prisma.config.ts` / `scripts/*.ts`):调
|
|
49
|
+
* `loadRuntimeEnvironment()`(它只是本函数的具名包装)—— 它们不被任何入口覆盖。
|
|
50
|
+
*
|
|
51
|
+
* `mode: 'test'` 的语义顺序:
|
|
52
|
+
*
|
|
53
|
+
* 1. 先加载三层开发分层,再叠测试层(两套 .env.test 逐层覆盖);
|
|
54
|
+
* 2. `loadLayers` 末尾把**外部注入**(进程 / 容器 / CI)原样恢复 —— 所以线上注入的
|
|
55
|
+
* `ENV=production` 一定压过仓库里的 `.env.test`,随后的环境闸门照旧拒绕启动;
|
|
56
|
+
* 3. 前面都没给出 `ENV` 时才兜底成 `test`:测试入口的默认身份就是测试,不该要求调用方
|
|
57
|
+
* 额外声明一次(否则漏写就变成「环境必须是 development/test」的莫名报错)。
|
|
58
|
+
*
|
|
59
|
+
* `generateOpenApi` **不**调用本函数(它不要求配置有效,设计 §3.2 规则 A/B)。
|
|
60
|
+
*/
|
|
61
|
+
export declare function loadEnvironment(options?: LoadEnvironmentOptions): ConfigEnv;
|
|
62
|
+
//# sourceMappingURL=env.d.ts.map
|
|
@@ -0,0 +1 @@
|
|
|
1
|
+
{"version":3,"file":"env.d.ts","sourceRoot":"","sources":["../../source/core/env.ts"],"names":[],"mappings":"AAgCA,wDAAwD;AACxD,eAAO,MAAM,iBAAiB,SAAS,CAAA;AA2BvC;;;;;;;;GAQG;AACH,MAAM,WAAW,SAAS;IACxB,kDAAkD;IAClD,GAAG,EAAE,MAAM,CAAA;IACX,yCAAyC;IACzC,QAAQ,EAAE,MAAM,CAAA;IAChB,qBAAqB;IACrB,SAAS,EAAE,MAAM,CAAA;IACjB,sCAAsC;IACtC,YAAY,EAAE,MAAM,CAAA;IACpB,2CAA2C;IAC3C,SAAS,CAAC,EAAE,MAAM,CAAA;CACnB;AAED,sDAAsD;AACtD,wBAAgB,gBAAgB,IAAI,SAAS,CAQ5C;AAED;;;;;GAKG;AACH,MAAM,MAAM,eAAe,GAAG,SAAS,GAAG,MAAM,CAAA;AAEhD,8BAA8B;AAC9B,MAAM,WAAW,sBAAsB;IACrC,IAAI,CAAC,EAAE,eAAe,CAAA;CACvB;AAED;;;;;;;;;;;;;;;;;;;;;;;GAuBG;AACH,wBAAgB,eAAe,CAAC,OAAO,GAAE,sBAA2B,GAAG,SAAS,CAY/E"}
|
|
@@ -0,0 +1,96 @@
|
|
|
1
|
+
import { config } from 'dotenv';
|
|
2
|
+
/**
|
|
3
|
+
* 默认 env 文件分层(低 → 高优先级,后者覆盖前者):
|
|
4
|
+
*
|
|
5
|
+
* .env.dev 已提交进仓库的 dev 默认值(克隆后 `pnpm run dev` 开箱即用)
|
|
6
|
+
* .env 本机私有覆盖(已被 gitignore)
|
|
7
|
+
* .env.local 本机更高优先级覆盖(已被 gitignore)
|
|
8
|
+
*
|
|
9
|
+
* 真实环境变量(进程/容器注入)优先级最高,任何 .env 文件都不会覆盖它们 —— 这条保证让 CI、
|
|
10
|
+
* helm、`docker run -e` 等显式注入始终生效,也避免测试探针
|
|
11
|
+
* (`check-test-database-isolation`)注入的连接串被仓库里的默认值顶掉。
|
|
12
|
+
*
|
|
13
|
+
* 解析器与四家现状保持一致,统一使用 `dotenv`,以保证多行值、引号/转义、`export` 前缀等
|
|
14
|
+
* 细节语义完全等价(设计 §3.1 ①:与现状逐字一致)。
|
|
15
|
+
*
|
|
16
|
+
* **分层是框架的内部实现**:不导出、不参数化。调用方只表达「我要运行期 env」还是「我要测试
|
|
17
|
+
* env」(`mode`),层名与顺序不进入业务代码。
|
|
18
|
+
*/
|
|
19
|
+
const ENV_FILE_LAYERS = ['.env.dev', '.env', '.env.local'];
|
|
20
|
+
/**
|
|
21
|
+
* 测试层(叠在 `ENV_FILE_LAYERS` 之上,低 → 高):
|
|
22
|
+
*
|
|
23
|
+
* .env.test 测试环境默认值(可提交:`ENV=test`、测试库连接串)
|
|
24
|
+
* .env.test.local 本机覆盖(已被 gitignore)
|
|
25
|
+
*
|
|
26
|
+
* 测试入口**无条件**加载这两层(不等 `ENV === 'test'`):测试入口的存在本身就意味着「这是
|
|
27
|
+
* 测试」,让调用方再声明一次是多余且容易漏的。
|
|
28
|
+
*/
|
|
29
|
+
const TEST_ENV_FILE_LAYERS = ['.env.test', '.env.test.local'];
|
|
30
|
+
/** `LOG_LEVEL` 未设置时的默认级别(设计 §3.1 ① / §5.1 `logging`) */
|
|
31
|
+
export const DEFAULT_LOG_LEVEL = 'info';
|
|
32
|
+
/**
|
|
33
|
+
* 首次 import 时的真实环境变量快照(此时还没有任何 .env 文件被加载)。
|
|
34
|
+
* 加载结束后原样恢复,实现「外部注入 > .env.local > .env > .env.dev」。
|
|
35
|
+
*/
|
|
36
|
+
const EXTERNAL_ENV = new Map(Object.entries(process.env).filter((entry) => entry[1] !== undefined));
|
|
37
|
+
/**
|
|
38
|
+
* 按分层顺序加载 env 文件(内部实现,见上方「分层不导出」的约定)。
|
|
39
|
+
*
|
|
40
|
+
* @param extraLayers 追加在最高优先级的额外层
|
|
41
|
+
*/
|
|
42
|
+
function loadLayers(extraLayers = []) {
|
|
43
|
+
const layers = [...ENV_FILE_LAYERS, ...extraLayers];
|
|
44
|
+
layers.forEach((path, index) => {
|
|
45
|
+
// 第一层不覆盖已有值,后续层逐层覆盖前一层
|
|
46
|
+
config({ path, override: index > 0 });
|
|
47
|
+
});
|
|
48
|
+
for (const [key, value] of EXTERNAL_ENV) {
|
|
49
|
+
process.env[key] = value;
|
|
50
|
+
}
|
|
51
|
+
}
|
|
52
|
+
/** 读取固定 env(不加载文件);入口会先 `loadEnvironment()` 再调用本函数 */
|
|
53
|
+
export function resolveConfigEnv() {
|
|
54
|
+
return {
|
|
55
|
+
ENV: process.env.ENV ?? '',
|
|
56
|
+
API_PORT: process.env.API_PORT ?? '',
|
|
57
|
+
LOG_LEVEL: process.env.LOG_LEVEL ?? DEFAULT_LOG_LEVEL,
|
|
58
|
+
DATABASE_URL: process.env.DATABASE_URL ?? '',
|
|
59
|
+
REDIS_URL: process.env.REDIS_URL,
|
|
60
|
+
};
|
|
61
|
+
}
|
|
62
|
+
/**
|
|
63
|
+
* 加载 env 文件并在加载后返回固定约定值。
|
|
64
|
+
*
|
|
65
|
+
* **这是 env 的唯一出口**:层名、顺序、覆盖规则全在框架内部,业务与工具进程只表达
|
|
66
|
+
* 「我要运行期 env」还是「我要测试 env」(`mode: 'test'`),不接触 `.env.dev` /
|
|
67
|
+
* `.env.test.local` 这些文件名。
|
|
68
|
+
*
|
|
69
|
+
* 三种调用方:
|
|
70
|
+
*
|
|
71
|
+
* - `startServer`:`loadEnvironment()`(最早阶段固定调用);
|
|
72
|
+
* - `createTestServer`:经 `prepareTestDatabase()` 间接调用(再跑两道闸门);
|
|
73
|
+
* - 工具进程(`vitest.config.ts` / `prisma.config.ts` / `scripts/*.ts`):调
|
|
74
|
+
* `loadRuntimeEnvironment()`(它只是本函数的具名包装)—— 它们不被任何入口覆盖。
|
|
75
|
+
*
|
|
76
|
+
* `mode: 'test'` 的语义顺序:
|
|
77
|
+
*
|
|
78
|
+
* 1. 先加载三层开发分层,再叠测试层(两套 .env.test 逐层覆盖);
|
|
79
|
+
* 2. `loadLayers` 末尾把**外部注入**(进程 / 容器 / CI)原样恢复 —— 所以线上注入的
|
|
80
|
+
* `ENV=production` 一定压过仓库里的 `.env.test`,随后的环境闸门照旧拒绕启动;
|
|
81
|
+
* 3. 前面都没给出 `ENV` 时才兜底成 `test`:测试入口的默认身份就是测试,不该要求调用方
|
|
82
|
+
* 额外声明一次(否则漏写就变成「环境必须是 development/test」的莫名报错)。
|
|
83
|
+
*
|
|
84
|
+
* `generateOpenApi` **不**调用本函数(它不要求配置有效,设计 §3.2 规则 A/B)。
|
|
85
|
+
*/
|
|
86
|
+
export function loadEnvironment(options = {}) {
|
|
87
|
+
const test = options.mode === 'test';
|
|
88
|
+
loadLayers(test ? TEST_ENV_FILE_LAYERS : undefined);
|
|
89
|
+
if (!test && (process.env.ENV ?? '') === 'test') {
|
|
90
|
+
loadLayers(TEST_ENV_FILE_LAYERS);
|
|
91
|
+
}
|
|
92
|
+
if (test && (process.env.ENV ?? '') === '') {
|
|
93
|
+
process.env.ENV = 'test';
|
|
94
|
+
}
|
|
95
|
+
return resolveConfigEnv();
|
|
96
|
+
}
|
|
@@ -1 +1 @@
|
|
|
1
|
-
{"version":3,"file":"env.test.d.ts","sourceRoot":"","sources":["../../source/
|
|
1
|
+
{"version":3,"file":"env.test.d.ts","sourceRoot":"","sources":["../../source/core/env.test.ts"],"names":[],"mappings":""}
|
|
@@ -0,0 +1,105 @@
|
|
|
1
|
+
import { mkdtempSync, rmSync, writeFileSync } from 'node:fs';
|
|
2
|
+
import { tmpdir } from 'node:os';
|
|
3
|
+
import { join } from 'node:path';
|
|
4
|
+
import { afterEach, describe, expect, it, vi } from 'vitest';
|
|
5
|
+
import { loadEnvironment } from './env.js';
|
|
6
|
+
const ORIGINAL_ENV = { ...process.env };
|
|
7
|
+
const ORIGINAL_CWD = process.cwd();
|
|
8
|
+
const TEMP_ROOTS = [];
|
|
9
|
+
afterEach(() => {
|
|
10
|
+
process.env = { ...ORIGINAL_ENV };
|
|
11
|
+
process.chdir(ORIGINAL_CWD);
|
|
12
|
+
for (const root of TEMP_ROOTS.splice(0)) {
|
|
13
|
+
rmSync(root, { recursive: true, force: true });
|
|
14
|
+
}
|
|
15
|
+
vi.resetModules();
|
|
16
|
+
});
|
|
17
|
+
/** 在临时目录里起一个项目根并写文件,然后切过去(`dotenv` 按 `process.cwd()` 解析相对路径) */
|
|
18
|
+
function useTempProject(files) {
|
|
19
|
+
const root = mkdtempSync(join(tmpdir(), 'common-env-'));
|
|
20
|
+
TEMP_ROOTS.push(root);
|
|
21
|
+
for (const [name, content] of Object.entries(files)) {
|
|
22
|
+
writeFileSync(join(root, name), content);
|
|
23
|
+
}
|
|
24
|
+
process.chdir(root);
|
|
25
|
+
}
|
|
26
|
+
describe('loadEnvironment · runtime(工具进程 / startServer)', () => {
|
|
27
|
+
it('加载 .env.dev / .env / .env.local 三层,且逐层覆盖', () => {
|
|
28
|
+
useTempProject({
|
|
29
|
+
'.env.dev': 'PROBE_LAYER=dev\nPROBE_DEV_ONLY=yes\n',
|
|
30
|
+
'.env': 'PROBE_LAYER=env\n',
|
|
31
|
+
'.env.local': 'PROBE_LAYER=local\n',
|
|
32
|
+
});
|
|
33
|
+
delete process.env.PROBE_LAYER;
|
|
34
|
+
delete process.env.PROBE_DEV_ONLY;
|
|
35
|
+
loadEnvironment();
|
|
36
|
+
expect(process.env.PROBE_LAYER).toBe('local');
|
|
37
|
+
expect(process.env.PROBE_DEV_ONLY).toBe('yes');
|
|
38
|
+
});
|
|
39
|
+
it('不加载测试层(ENV 不是 test 时)', () => {
|
|
40
|
+
useTempProject({ '.env.test': 'PROBE_TEST_ONLY=yes\n' });
|
|
41
|
+
process.env.ENV = 'development';
|
|
42
|
+
delete process.env.PROBE_TEST_ONLY;
|
|
43
|
+
loadEnvironment();
|
|
44
|
+
expect(process.env.PROBE_TEST_ONLY).toBeUndefined();
|
|
45
|
+
});
|
|
46
|
+
it('ENV=test 时追加测试层', () => {
|
|
47
|
+
useTempProject({ '.env.test': 'PROBE_TEST_ONLY=yes\n' });
|
|
48
|
+
process.env.ENV = 'test';
|
|
49
|
+
delete process.env.PROBE_TEST_ONLY;
|
|
50
|
+
loadEnvironment();
|
|
51
|
+
expect(process.env.PROBE_TEST_ONLY).toBe('yes');
|
|
52
|
+
});
|
|
53
|
+
it('不给 ENV 时不兜底(运行期必须有明确身份)', () => {
|
|
54
|
+
useTempProject({});
|
|
55
|
+
delete process.env.ENV;
|
|
56
|
+
expect(loadEnvironment().ENV).toBe('');
|
|
57
|
+
});
|
|
58
|
+
});
|
|
59
|
+
describe('loadEnvironment · test(测试入口)', () => {
|
|
60
|
+
it('无条件加载测试层,不需要调用方先声明 ENV=test', () => {
|
|
61
|
+
useTempProject({ '.env.test': 'PROBE_TEST_ONLY=yes\n' });
|
|
62
|
+
process.env.ENV = 'development';
|
|
63
|
+
delete process.env.PROBE_TEST_ONLY;
|
|
64
|
+
loadEnvironment({ mode: 'test' });
|
|
65
|
+
expect(process.env.PROBE_TEST_ONLY).toBe('yes');
|
|
66
|
+
});
|
|
67
|
+
it('.env.test.local 覆盖 .env.test', () => {
|
|
68
|
+
useTempProject({
|
|
69
|
+
'.env.test': 'PROBE_LAYER=from-test\n',
|
|
70
|
+
'.env.test.local': 'PROBE_LAYER=from-local\n',
|
|
71
|
+
});
|
|
72
|
+
delete process.env.PROBE_LAYER;
|
|
73
|
+
loadEnvironment({ mode: 'test' });
|
|
74
|
+
expect(process.env.PROBE_LAYER).toBe('from-local');
|
|
75
|
+
});
|
|
76
|
+
it('没有任何 ENV 时兜底成 test', () => {
|
|
77
|
+
useTempProject({});
|
|
78
|
+
delete process.env.ENV;
|
|
79
|
+
expect(loadEnvironment({ mode: 'test' }).ENV).toBe('test');
|
|
80
|
+
});
|
|
81
|
+
it('已声明 ENV 时不被兜底改写', () => {
|
|
82
|
+
useTempProject({});
|
|
83
|
+
process.env.ENV = 'development';
|
|
84
|
+
expect(loadEnvironment({ mode: 'test' }).ENV).toBe('development');
|
|
85
|
+
});
|
|
86
|
+
});
|
|
87
|
+
describe('外部注入优先级(两种模式都必须成立)', () => {
|
|
88
|
+
it('runtime:.env 不覆盖进程已注入的值', async () => {
|
|
89
|
+
useTempProject({ '.env': 'PROBE_LAYER=from-file\n' });
|
|
90
|
+
// 同上:外部注入的快照在模块 import 时建立,重新 import 才能模拟真实注入时机
|
|
91
|
+
vi.resetModules();
|
|
92
|
+
process.env.PROBE_LAYER = 'from-process';
|
|
93
|
+
const fresh = await import('./env.js');
|
|
94
|
+
fresh.loadEnvironment();
|
|
95
|
+
expect(process.env.PROBE_LAYER).toBe('from-process');
|
|
96
|
+
});
|
|
97
|
+
it('test:线上注入的 ENV=production 不被 .env.test 顶掉(环境闸门因此仍会拒绝)', async () => {
|
|
98
|
+
useTempProject({ '.env.test': 'ENV=test\n' });
|
|
99
|
+
// EXTERNAL_ENV 是模块首次 import 时的快照,重新 import 才能模拟「进程启动前注入」
|
|
100
|
+
vi.resetModules();
|
|
101
|
+
process.env.ENV = 'production';
|
|
102
|
+
const fresh = await import('./env.js');
|
|
103
|
+
expect(fresh.loadEnvironment({ mode: 'test' }).ENV).toBe('production');
|
|
104
|
+
});
|
|
105
|
+
});
|
|
@@ -0,0 +1,10 @@
|
|
|
1
|
+
/**
|
|
2
|
+
* `core/modules/auth-provider` 的出口:**只有类型,没有实现、没有地址**。
|
|
3
|
+
*
|
|
4
|
+
* 实现由业务服务提供(`createAuthMiddleware({ client })`):框架不建 HTTP 客户端、
|
|
5
|
+
* 不认任何地址、不做签名、不管超时——见 `types.ts` 文件头与设计 §5.2.2。
|
|
6
|
+
*
|
|
7
|
+
* 提供方是**可替换**的:account 只是当前的主要实现,不是框架的预设。
|
|
8
|
+
*/
|
|
9
|
+
export type { AuthClient, AuthVerifyMetadata, AuthVerifyPayload, AuthVerifyResult, RegisterPermissionOperationsPayload, } from './types.js';
|
|
10
|
+
//# sourceMappingURL=index.d.ts.map
|
|
@@ -0,0 +1 @@
|
|
|
1
|
+
{"version":3,"file":"index.d.ts","sourceRoot":"","sources":["../../../../source/core/modules/auth-provider/index.ts"],"names":[],"mappings":"AAAA;;;;;;;GAOG;AAEH,YAAY,EACV,UAAU,EACV,kBAAkB,EAClB,iBAAiB,EACjB,gBAAgB,EAChB,mCAAmC,GACpC,MAAM,YAAY,CAAA"}
|
|
@@ -0,0 +1,71 @@
|
|
|
1
|
+
import type { PermissionOperation } from '../permission-operations/index.js';
|
|
2
|
+
import type { AccessControl } from '../../type-augment.js';
|
|
3
|
+
/**
|
|
4
|
+
* core `auth-provider` 集成层:**只有最小接口约束,没有实现、没有地址**。
|
|
5
|
+
*
|
|
6
|
+
* 鉴权提供方(Auth Provider)是**可替换**的——account 只是当前的主要实现之一,
|
|
7
|
+
* 任何能「校验凭证 + 接收权限目录」的系统都可以填进来(自建 IAM、OIDC 网关、
|
|
8
|
+
* 单点登录服务、甚至进程内的静态校验)。
|
|
9
|
+
*
|
|
10
|
+
* 为什么只留契约(设计 §5.2.2):
|
|
11
|
+
*
|
|
12
|
+
* - 客户端的**构造**依赖业务侧才知道的东西——`serviceUrl`(来自配置 / 服务发现 /
|
|
13
|
+
* 私有网络)、端点路径(属于提供方的业务契约,不在 framework)、服务凭证、
|
|
14
|
+
* 以及项目自己的 `fetch` / 拦截器 / 超时 / 重试策略。框架猜不出来,也不该猜。
|
|
15
|
+
* - 所以框架只声明「**调用鉴权提供方需要什么形状**」,由每个服务在自己 `config` /
|
|
16
|
+
* `resources` 槽位里实现 `AuthClient`,注入给 `auth` 的 `client` 选项。
|
|
17
|
+
*
|
|
18
|
+
* 契约本身是冻结的(框架只用到两个能力:校验凭证、注册权限操作):改形状等于改契约。
|
|
19
|
+
* **地址与路径不进 framework**——它们随部署环境变化,属于业务配置。
|
|
20
|
+
*/
|
|
21
|
+
/** 校验请求附带的请求元信息(仅供下游审计 / 日志;不参与鉴权判定) */
|
|
22
|
+
export interface AuthVerifyMetadata {
|
|
23
|
+
ip?: string;
|
|
24
|
+
ips?: string[];
|
|
25
|
+
url?: string;
|
|
26
|
+
method?: string;
|
|
27
|
+
referrer?: string;
|
|
28
|
+
requestId?: string;
|
|
29
|
+
userAgent?: string;
|
|
30
|
+
}
|
|
31
|
+
/** `verify` 的请求体:调鉴权提供方的校验能力时提交什么 */
|
|
32
|
+
export interface AuthVerifyPayload {
|
|
33
|
+
token?: string;
|
|
34
|
+
apiKey?: string;
|
|
35
|
+
/** 调用方服务名(`AppIdentity.service`) */
|
|
36
|
+
service: string;
|
|
37
|
+
/** 操作标识(路由 `schema.operationId`,缺省回落 url) */
|
|
38
|
+
operation?: string;
|
|
39
|
+
/** 路由参数(`request.params`),如 `:id` 这类路径参数 */
|
|
40
|
+
resource?: Record<string, unknown>;
|
|
41
|
+
accessControls?: AccessControl[];
|
|
42
|
+
metadata?: AuthVerifyMetadata;
|
|
43
|
+
}
|
|
44
|
+
/** `verify` 的响应数据 */
|
|
45
|
+
export interface AuthVerifyResult {
|
|
46
|
+
user?: unknown;
|
|
47
|
+
token?: unknown;
|
|
48
|
+
}
|
|
49
|
+
/** 权限操作自注册的请求体:调鉴权提供方的权限目录端点时提交什么 */
|
|
50
|
+
export interface RegisterPermissionOperationsPayload {
|
|
51
|
+
service: string;
|
|
52
|
+
version: string;
|
|
53
|
+
operations: PermissionOperation[];
|
|
54
|
+
}
|
|
55
|
+
/**
|
|
56
|
+
* 鉴权客户端契约:框架只需要**两个**方法。
|
|
57
|
+
*
|
|
58
|
+
* 实现归业务服务(`createAuthMiddleware` 的 `client` 选项):**URL / 路径 / 签名 /
|
|
59
|
+
* 协议(HTTP、gRPC、进程内调用)/ 超时 / 重试全在业务侧**,框架不认识任何地址。
|
|
60
|
+
* account 只是其中一种实现,不是唯一一种。
|
|
61
|
+
*
|
|
62
|
+
* 失败语义:
|
|
63
|
+
* - `verify` 抛错 = 校验不通过或提供方不可用;提供方应**自己抛** `UserError` /
|
|
64
|
+
* `SystemError`(框架原样透出),其余错误统一归类为 `EXTERNAL_SERVICE_ERROR`;
|
|
65
|
+
* - `registerPermissionOperations` 抛错**不致命**(`auth` 只记日志,不中断启动)。
|
|
66
|
+
*/
|
|
67
|
+
export interface AuthClient {
|
|
68
|
+
verify(payload: AuthVerifyPayload): Promise<AuthVerifyResult>;
|
|
69
|
+
registerPermissionOperations(payload: RegisterPermissionOperationsPayload): Promise<void>;
|
|
70
|
+
}
|
|
71
|
+
//# sourceMappingURL=types.d.ts.map
|
|
@@ -0,0 +1 @@
|
|
|
1
|
+
{"version":3,"file":"types.d.ts","sourceRoot":"","sources":["../../../../source/core/modules/auth-provider/types.ts"],"names":[],"mappings":"AAAA,OAAO,KAAK,EAAE,mBAAmB,EAAE,MAAM,mCAAmC,CAAA;AAC5E,OAAO,KAAK,EAAE,aAAa,EAAE,MAAM,uBAAuB,CAAA;AAE1D;;;;;;;;;;;;;;;;;GAiBG;AAEH,wCAAwC;AACxC,MAAM,WAAW,kBAAkB;IACjC,EAAE,CAAC,EAAE,MAAM,CAAA;IACX,GAAG,CAAC,EAAE,MAAM,EAAE,CAAA;IACd,GAAG,CAAC,EAAE,MAAM,CAAA;IACZ,MAAM,CAAC,EAAE,MAAM,CAAA;IACf,QAAQ,CAAC,EAAE,MAAM,CAAA;IACjB,SAAS,CAAC,EAAE,MAAM,CAAA;IAClB,SAAS,CAAC,EAAE,MAAM,CAAA;CACnB;AAED,qCAAqC;AACrC,MAAM,WAAW,iBAAiB;IAChC,KAAK,CAAC,EAAE,MAAM,CAAA;IACd,MAAM,CAAC,EAAE,MAAM,CAAA;IACf,oCAAoC;IACpC,OAAO,EAAE,MAAM,CAAA;IACf,6CAA6C;IAC7C,SAAS,CAAC,EAAE,MAAM,CAAA;IAClB,4CAA4C;IAC5C,QAAQ,CAAC,EAAE,MAAM,CAAC,MAAM,EAAE,OAAO,CAAC,CAAA;IAClC,cAAc,CAAC,EAAE,aAAa,EAAE,CAAA;IAChC,QAAQ,CAAC,EAAE,kBAAkB,CAAA;CAC9B;AAED,qBAAqB;AACrB,MAAM,WAAW,gBAAgB;IAC/B,IAAI,CAAC,EAAE,OAAO,CAAA;IACd,KAAK,CAAC,EAAE,OAAO,CAAA;CAChB;AAED,qCAAqC;AACrC,MAAM,WAAW,mCAAmC;IAClD,OAAO,EAAE,MAAM,CAAA;IACf,OAAO,EAAE,MAAM,CAAA;IACf,UAAU,EAAE,mBAAmB,EAAE,CAAA;CAClC;AAED;;;;;;;;;;;GAWG;AACH,MAAM,WAAW,UAAU;IACzB,MAAM,CAAC,OAAO,EAAE,iBAAiB,GAAG,OAAO,CAAC,gBAAgB,CAAC,CAAA;IAC7D,4BAA4B,CAAC,OAAO,EAAE,mCAAmC,GAAG,OAAO,CAAC,IAAI,CAAC,CAAA;CAC1F"}
|
|
@@ -0,0 +1,18 @@
|
|
|
1
|
+
import type { FastifyBaseLogger, FastifyInstance, FastifyRequest } from 'fastify';
|
|
2
|
+
/**
|
|
3
|
+
* core `context`(设计 §5.1)——**框架内置、锁定、不可替换/修改**。
|
|
4
|
+
*
|
|
5
|
+
* 两部分:
|
|
6
|
+
* 1. `RequestContext` / `createContext`:请求级上下文的形状与构造;
|
|
7
|
+
* 2. `registerRequestContext`:把上下文装配到 `request.ctx`(`decorateRequest` + `onRequest`)。
|
|
8
|
+
*
|
|
9
|
+
* `request.ctx` 是 `type-augment` 对外承诺的类型契约,装配必须由框架保证——不能交由
|
|
10
|
+
* 各仓决定「装不装 / 装成什么名字」,否则类型存在而运行时缺失。装饰名固定 `ctx`,无选项。
|
|
11
|
+
*/
|
|
12
|
+
export interface RequestContext {
|
|
13
|
+
log: FastifyBaseLogger;
|
|
14
|
+
}
|
|
15
|
+
export declare function createContext(request: FastifyRequest): RequestContext;
|
|
16
|
+
/** 装配 `request.ctx`(框架固定,先于任何路由 / 业务中间件注册) */
|
|
17
|
+
export declare function registerRequestContext(app: FastifyInstance): void;
|
|
18
|
+
//# sourceMappingURL=index.d.ts.map
|
|
@@ -0,0 +1 @@
|
|
|
1
|
+
{"version":3,"file":"index.d.ts","sourceRoot":"","sources":["../../../../source/core/modules/context/index.ts"],"names":[],"mappings":"AAAA,OAAO,KAAK,EAAE,iBAAiB,EAAE,eAAe,EAAE,cAAc,EAAE,MAAM,SAAS,CAAA;AAEjF;;;;;;;;;GASG;AAEH,MAAM,WAAW,cAAc;IAC7B,GAAG,EAAE,iBAAiB,CAAA;CACvB;AAcD,wBAAgB,aAAa,CAAC,OAAO,EAAE,cAAc,GAAG,cAAc,CAErE;AAED,8CAA8C;AAC9C,wBAAgB,sBAAsB,CAAC,GAAG,EAAE,eAAe,GAAG,IAAI,CAOjE"}
|
|
@@ -0,0 +1,23 @@
|
|
|
1
|
+
const fallbackLogger = {
|
|
2
|
+
trace: () => { },
|
|
3
|
+
debug: () => { },
|
|
4
|
+
info: () => { },
|
|
5
|
+
warn: () => { },
|
|
6
|
+
error: () => { },
|
|
7
|
+
fatal: () => { },
|
|
8
|
+
child: () => fallbackLogger,
|
|
9
|
+
level: 'info',
|
|
10
|
+
silent: () => { },
|
|
11
|
+
};
|
|
12
|
+
export function createContext(request) {
|
|
13
|
+
return { log: request.log || fallbackLogger };
|
|
14
|
+
}
|
|
15
|
+
/** 装配 `request.ctx`(框架固定,先于任何路由 / 业务中间件注册) */
|
|
16
|
+
export function registerRequestContext(app) {
|
|
17
|
+
// 先声明属性(保持 hidden class 稳定),再在 `onRequest` 逐个请求写入
|
|
18
|
+
app.decorateRequest('ctx', null);
|
|
19
|
+
app.addHook('onRequest', (request, _reply, done) => {
|
|
20
|
+
request.ctx = createContext(request);
|
|
21
|
+
done();
|
|
22
|
+
});
|
|
23
|
+
}
|
|
@@ -0,0 +1 @@
|
|
|
1
|
+
{"version":3,"file":"index.test.d.ts","sourceRoot":"","sources":["../../../../source/core/modules/context/index.test.ts"],"names":[],"mappings":""}
|