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

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 (145) hide show
  1. package/output/app/auth.test.d.ts +2 -0
  2. package/output/app/auth.test.d.ts.map +1 -0
  3. package/output/app/auth.test.js +148 -0
  4. package/output/app/boot.d.ts +39 -0
  5. package/output/app/boot.d.ts.map +1 -0
  6. package/output/app/boot.js +140 -0
  7. package/output/app/config.test.d.ts +2 -0
  8. package/output/app/config.test.d.ts.map +1 -0
  9. package/output/app/config.test.js +132 -0
  10. package/output/app/context.d.ts +70 -0
  11. package/output/app/context.d.ts.map +1 -0
  12. package/output/app/context.js +73 -0
  13. package/output/app/core/cookie.d.ts +25 -0
  14. package/output/app/core/cookie.d.ts.map +1 -0
  15. package/output/app/core/cookie.js +22 -0
  16. package/output/app/core/identity.d.ts +12 -0
  17. package/output/app/core/identity.d.ts.map +1 -0
  18. package/output/app/core/identity.js +14 -0
  19. package/output/app/core/index.d.ts +44 -0
  20. package/output/app/core/index.d.ts.map +1 -0
  21. package/output/app/core/index.js +57 -0
  22. package/output/app/core/logging.d.ts +13 -0
  23. package/output/app/core/logging.d.ts.map +1 -0
  24. package/output/app/core/logging.js +29 -0
  25. package/output/app/core/openapi.d.ts +17 -0
  26. package/output/app/core/openapi.d.ts.map +1 -0
  27. package/output/app/core/openapi.js +42 -0
  28. package/output/app/core/request-id.d.ts +36 -0
  29. package/output/app/core/request-id.d.ts.map +1 -0
  30. package/output/app/core/request-id.js +66 -0
  31. package/output/app/core/response.d.ts +41 -0
  32. package/output/app/core/response.d.ts.map +1 -0
  33. package/output/app/core/response.js +89 -0
  34. package/output/app/define-server.negative-cases.d.ts +40 -0
  35. package/output/app/define-server.negative-cases.d.ts.map +1 -0
  36. package/output/app/define-server.negative-cases.js +268 -0
  37. package/output/app/define-server.test.js +86 -288
  38. package/output/app/entries.d.ts +29 -0
  39. package/output/app/entries.d.ts.map +1 -0
  40. package/output/app/entries.js +64 -0
  41. package/output/app/env.d.ts +39 -4
  42. package/output/app/env.d.ts.map +1 -1
  43. package/output/app/env.js +32 -4
  44. package/output/app/env.test.js +67 -21
  45. package/output/app/error-contract.test.d.ts +2 -0
  46. package/output/app/error-contract.test.d.ts.map +1 -0
  47. package/output/app/error-contract.test.js +227 -0
  48. package/output/app/errors.d.ts +20 -0
  49. package/output/app/errors.d.ts.map +1 -0
  50. package/output/app/errors.js +51 -0
  51. package/output/app/generate-openapi.test.d.ts +2 -0
  52. package/output/app/generate-openapi.test.d.ts.map +1 -0
  53. package/output/app/generate-openapi.test.js +123 -0
  54. package/output/app/index.d.ts +15 -15
  55. package/output/app/index.d.ts.map +1 -1
  56. package/output/app/index.js +18 -9
  57. package/output/app/lifecycle-logging.test.d.ts +2 -0
  58. package/output/app/lifecycle-logging.test.d.ts.map +1 -0
  59. package/output/app/lifecycle-logging.test.js +155 -0
  60. package/output/app/lifecycle.d.ts +70 -0
  61. package/output/app/lifecycle.d.ts.map +1 -0
  62. package/output/app/lifecycle.js +99 -0
  63. package/output/app/lifecycle.test.d.ts +2 -0
  64. package/output/app/lifecycle.test.d.ts.map +1 -0
  65. package/output/app/lifecycle.test.js +152 -0
  66. package/output/app/middleware/auth.d.ts +71 -0
  67. package/output/app/middleware/auth.d.ts.map +1 -0
  68. package/output/app/middleware/auth.js +190 -0
  69. package/output/app/middleware/context.d.ts +13 -0
  70. package/output/app/middleware/context.d.ts.map +1 -0
  71. package/output/app/middleware/context.js +15 -0
  72. package/output/app/middleware/cors.d.ts +14 -0
  73. package/output/app/middleware/cors.d.ts.map +1 -0
  74. package/output/app/middleware/cors.js +30 -0
  75. package/output/app/middleware/index.d.ts +17 -0
  76. package/output/app/middleware/index.d.ts.map +1 -0
  77. package/output/app/middleware/index.js +16 -0
  78. package/output/app/middleware/rate-limit.d.ts +24 -0
  79. package/output/app/middleware/rate-limit.d.ts.map +1 -0
  80. package/output/app/middleware/rate-limit.js +46 -0
  81. package/output/app/middleware-order.test.d.ts +2 -0
  82. package/output/app/middleware-order.test.d.ts.map +1 -0
  83. package/output/app/middleware-order.test.js +148 -0
  84. package/output/app/permission-operations.d.ts +48 -0
  85. package/output/app/permission-operations.d.ts.map +1 -0
  86. package/output/app/permission-operations.js +77 -0
  87. package/output/app/permission-operations.test.d.ts +2 -0
  88. package/output/app/permission-operations.test.d.ts.map +1 -0
  89. package/output/app/permission-operations.test.js +72 -0
  90. package/output/app/permission-registration.test.d.ts +2 -0
  91. package/output/app/permission-registration.test.d.ts.map +1 -0
  92. package/output/app/permission-registration.test.js +138 -0
  93. package/output/app/redis.d.ts +45 -0
  94. package/output/app/redis.d.ts.map +1 -0
  95. package/output/app/redis.js +61 -0
  96. package/output/app/registered-operations.test.d.ts +2 -0
  97. package/output/app/registered-operations.test.d.ts.map +1 -0
  98. package/output/app/registered-operations.test.js +57 -0
  99. package/output/app/request-id.test.d.ts +2 -0
  100. package/output/app/request-id.test.d.ts.map +1 -0
  101. package/output/app/request-id.test.js +130 -0
  102. package/output/app/resources.test.d.ts +2 -0
  103. package/output/app/resources.test.d.ts.map +1 -0
  104. package/output/app/resources.test.js +199 -0
  105. package/output/app/shutdown.d.ts +17 -4
  106. package/output/app/shutdown.d.ts.map +1 -1
  107. package/output/app/shutdown.js +27 -12
  108. package/output/app/shutdown.test.js +78 -124
  109. package/output/app/signal.d.ts +4 -4
  110. package/output/app/signal.d.ts.map +1 -1
  111. package/output/app/signal.js +3 -3
  112. package/output/app/smoke.test.d.ts +2 -0
  113. package/output/app/smoke.test.d.ts.map +1 -0
  114. package/output/app/smoke.test.js +210 -0
  115. package/output/app/test-support.d.ts +21 -0
  116. package/output/app/test-support.d.ts.map +1 -0
  117. package/output/app/test-support.js +24 -0
  118. package/output/app/type-augment.d.ts +34 -0
  119. package/output/app/type-augment.d.ts.map +1 -0
  120. package/output/app/type-augment.js +7 -0
  121. package/output/app/types.d.ts +205 -126
  122. package/output/app/types.d.ts.map +1 -1
  123. package/output/app/types.js +2 -1
  124. package/package.json +10 -6
  125. package/output/app/assemble.d.ts +0 -16
  126. package/output/app/assemble.d.ts.map +0 -1
  127. package/output/app/assemble.js +0 -124
  128. package/output/app/database.d.ts +0 -36
  129. package/output/app/database.d.ts.map +0 -1
  130. package/output/app/database.js +0 -40
  131. package/output/app/database.test.d.ts +0 -2
  132. package/output/app/database.test.d.ts.map +0 -1
  133. package/output/app/database.test.js +0 -66
  134. package/output/app/default-modules.d.ts +0 -23
  135. package/output/app/default-modules.d.ts.map +0 -1
  136. package/output/app/default-modules.js +0 -40
  137. package/output/app/define-server.d.ts +0 -12
  138. package/output/app/define-server.d.ts.map +0 -1
  139. package/output/app/define-server.js +0 -289
  140. package/output/app/module.d.ts +0 -88
  141. package/output/app/module.d.ts.map +0 -1
  142. package/output/app/module.js +0 -34
  143. package/output/app/steps.d.ts +0 -17
  144. package/output/app/steps.d.ts.map +0 -1
  145. package/output/app/steps.js +0 -41
@@ -1,149 +1,103 @@
1
1
  import { describe, expect, it } from 'vitest';
2
- import { defineServer } from './define-server.js';
3
- import { SHUTDOWN_ORDER } from './shutdown.js';
4
- const P1_OPT_OUT = {
5
- context: false,
6
- response: false,
7
- cors: false,
8
- schema: false,
9
- 'rate-limit': false,
10
- auth: false,
11
- openapi: false,
12
- database: false,
13
- };
14
- const DUMMY_ERRORS = {
15
- UserError: class extends Error {
16
- },
17
- SystemError: class extends Error {
18
- },
19
- };
20
- function createTestLog() {
21
- const errors = [];
22
- const log = {
23
- debug: () => undefined,
24
- info: () => undefined,
25
- warn: () => undefined,
26
- error: (obj) => {
27
- errors.push(obj);
28
- },
29
- };
30
- return { log, errors };
31
- }
32
- function createBaseOptions(overrides = {}) {
33
- return {
34
- serviceName: 'test',
35
- title: 'Test',
36
- description: 'Test service',
37
- errors: DUMMY_ERRORS,
38
- config: { loadConfig: () => ({ debug: false, port: 0 }) },
39
- modules: { ...P1_OPT_OUT },
40
- serverOptions: { port: 0, host: '127.0.0.1' },
41
- signal: false,
42
- createServices: () => ({}),
43
- registerRouters: () => undefined,
44
- ...overrides,
45
- };
2
+ import { defineServer, startServer } from './entries.js';
3
+ import { runShutdown, SHUTDOWN_ORDER } from './shutdown.js';
4
+ function createShutdownLog() {
5
+ const entries = [];
6
+ return { entries, log: { error: (obj, msg) => void entries.push({ obj, msg }) } };
46
7
  }
47
- function createRecordingShutdownModule(record, throwOn) {
48
- const createStep = (name) => () => {
8
+ function createRecordingHandles(record, throwOn) {
9
+ const step = (name) => () => {
49
10
  record.push(name);
50
11
  if (name === throwOn) {
51
12
  throw new Error(`boom:${name}`);
52
13
  }
53
14
  };
54
- const shutdown = {
55
- stopProducers: createStep('stopProducers'),
56
- unsubscribeConfig: createStep('unsubscribeConfig'),
57
- disposeConfig: createStep('disposeConfig'),
58
- disposeServices: createStep('disposeServices'),
59
- disposeClients: createStep('disposeClients'),
60
- disposeRedis: createStep('disposeRedis'),
61
- disposeDb: createStep('disposeDb'),
15
+ return {
16
+ stopProducers: step('stopProducers'),
17
+ disposeServices: step('disposeServices'),
62
18
  };
63
- return { name: 'env', shutdown };
64
19
  }
65
- describe('关闭 7 步', () => {
66
- it('顺序写死、beforeShutdown 在最前', async () => {
67
- const record = [];
68
- const app = await defineServer(createBaseOptions({
69
- modules: { ...P1_OPT_OUT, env: createRecordingShutdownModule(record) },
70
- beforeShutdown: () => {
71
- record.push('beforeShutdown');
72
- },
73
- }));
74
- await app.close();
75
- expect(record).toEqual(['beforeShutdown', ...SHUTDOWN_ORDER]);
20
+ describe('SHUTDOWN_ORDER', () => {
21
+ it('固定两步且顺序不可变', () => {
22
+ expect(SHUTDOWN_ORDER).toEqual(['stopProducers', 'disposeServices']);
76
23
  });
77
- it('单步抛错不阻断后续,且聚合上报(不吞第一个错误)', async () => {
24
+ });
25
+ describe('runShutdown', () => {
26
+ it('按固定序执行全部步骤', async () => {
78
27
  const record = [];
79
- const { log, errors } = createTestLog();
80
- const app = await defineServer(createBaseOptions({
81
- log,
82
- modules: { ...P1_OPT_OUT, env: createRecordingShutdownModule(record, 'disposeConfig') },
83
- beforeShutdown: () => {
84
- record.push('beforeShutdown');
85
- throw new Error('boom:beforeShutdown');
86
- },
87
- }));
88
- await app.close();
89
- // disposeConfig 抛错后,4~7 步仍然执行
90
- expect(record).toEqual(['beforeShutdown', ...SHUTDOWN_ORDER]);
91
- expect(errors).toHaveLength(1);
92
- const aggregated = errors[0];
93
- expect(aggregated.count).toBe(2);
94
- expect(aggregated.errors).toHaveLength(2);
28
+ const { log } = createShutdownLog();
29
+ await runShutdown(createRecordingHandles(record), log);
30
+ expect(record).toEqual([...SHUTDOWN_ORDER]);
95
31
  });
96
- it('shutdown 选项整体覆盖默认关闭流程', async () => {
32
+ it('单步抛错不阻断后续,并聚合上报', async () => {
97
33
  const record = [];
98
- const app = await defineServer(createBaseOptions({
99
- modules: { ...P1_OPT_OUT, env: createRecordingShutdownModule(record) },
100
- beforeShutdown: () => {
101
- record.push('beforeShutdown');
102
- },
103
- shutdown: () => {
104
- record.push('custom');
105
- },
106
- }));
107
- await app.close();
108
- expect(record).toEqual(['custom']);
34
+ const { entries, log } = createShutdownLog();
35
+ await runShutdown(createRecordingHandles(record, 'stopProducers'), log);
36
+ expect(record).toEqual([...SHUTDOWN_ORDER]);
37
+ const aggregate = entries.find((entry) => entry.msg === '[shutdown] completed with errors');
38
+ expect(aggregate).toBeDefined();
39
+ const payload = aggregate?.obj;
40
+ expect(payload.count).toBe(1);
41
+ expect(payload.errors).toHaveLength(1);
109
42
  });
110
- it('模块 hooks.beforeShutdown 生效(调用方选项优先)', async () => {
111
- const record = [];
112
- const module = {
113
- name: 'env',
114
- hooks: {
115
- beforeShutdown: () => {
116
- record.push('module-before');
117
- },
118
- },
119
- shutdown: {
120
- stopProducers: () => {
121
- record.push('stopProducers');
122
- },
43
+ });
44
+ describe('关闭流程集成', () => {
45
+ it('默认两步按序执行:disposer → services 逆序', async () => {
46
+ const calls = [];
47
+ const server = defineServer({
48
+ app: { service: 'shutdown-integration', title: 'Shutdown Integration', version: '1.0.0' },
49
+ resources: () => ({ database: { dispose: () => calls.push('disposeResource') } }),
50
+ config: () => ({}),
51
+ services: (ctx) => {
52
+ ctx.disposer.add(() => {
53
+ calls.push('stopProducers');
54
+ });
55
+ return {
56
+ a: {
57
+ dispose: () => {
58
+ calls.push('disposeServiceA');
59
+ },
60
+ },
61
+ b: {
62
+ dispose: () => {
63
+ calls.push('disposeServiceB');
64
+ },
65
+ },
66
+ };
123
67
  },
124
- };
125
- const app = await defineServer(createBaseOptions({ modules: { ...P1_OPT_OUT, env: module } }));
68
+ middlewares: () => [],
69
+ routers: () => undefined,
70
+ });
71
+ const app = await startServer(server, { listen: false });
126
72
  await app.close();
127
- expect(record).toEqual(['module-before', 'stopProducers']);
73
+ // 资源不在生命周期内(框架只管 services)
74
+ expect(calls).toEqual(['stopProducers', 'disposeServiceB', 'disposeServiceA']);
128
75
  });
129
- it('默认 disposeServices 用 Promise.allSettled 释放 services(单服务抛错不影响其它)', async () => {
130
- let disposed = 0;
131
- const app = await defineServer(createBaseOptions({
132
- createServices: () => ({
76
+ it('StartServerOptions.shutdown 整体覆盖默认关闭流程', async () => {
77
+ const calls = [];
78
+ const server = defineServer({
79
+ app: { service: 'shutdown-override', title: 'Shutdown Override', version: '1.0.0' },
80
+ resources: () => ({}),
81
+ config: () => ({}),
82
+ services: () => ({
133
83
  a: {
134
84
  dispose: () => {
135
- disposed += 1;
136
- },
137
- },
138
- b: {
139
- dispose: () => {
140
- disposed += 1;
141
- throw new Error('service dispose failed');
85
+ calls.push('default-dispose');
142
86
  },
143
87
  },
144
88
  }),
145
- }));
89
+ middlewares: () => [],
90
+ routers: () => undefined,
91
+ });
92
+ const app = await startServer(server, {
93
+ listen: false,
94
+ shutdown: async (ctx) => {
95
+ expect(ctx.app).toBeDefined();
96
+ expect(ctx.services).toBeDefined();
97
+ calls.push('custom');
98
+ },
99
+ });
146
100
  await app.close();
147
- expect(disposed).toBe(2);
101
+ expect(calls).toEqual(['custom']);
148
102
  });
149
103
  });
@@ -1,10 +1,10 @@
1
- import type { FastifyInstance } from 'fastify';
2
1
  import type { LoggerLike } from './types.js';
2
+ import type { FastifyInstance } from 'fastify';
3
3
  /**
4
- * 内置信号处理(design §4.1 第 15 步尾部)。
4
+ * 内置信号处理(设计 §7.1 `activate` 尾部 / §7.2)。
5
5
  *
6
- * `SIGTERM` / `SIGINT` / `uncaughtException` / `unhandledRejection` ⇒ `app.close()`(触发 onClose
7
- * 的关闭 7 步)⇒ `process.exit(0/1)`。可通过 `signal: false` 整体关掉(测试 / 嵌入场景)。
6
+ * `SIGTERM` / `SIGINT` / `uncaughtException` / `unhandledRejection` ⇒ `app.close()`(触发 `onClose`
7
+ * 的关闭 6 步)⇒ `process.exit(0/1)`。**仅 `listen: true` 时装信号处理**。
8
8
  *
9
9
  * @returns detach 函数(移除本次注册的监听器)
10
10
  */
@@ -1 +1 @@
1
- {"version":3,"file":"signal.d.ts","sourceRoot":"","sources":["../../source/app/signal.ts"],"names":[],"mappings":"AAAA,OAAO,KAAK,EAAE,eAAe,EAAE,MAAM,SAAS,CAAA;AAE9C,OAAO,KAAK,EAAE,UAAU,EAAE,MAAM,YAAY,CAAA;AAE5C;;;;;;;GAOG;AACH,wBAAgB,oBAAoB,CAAC,GAAG,EAAE,eAAe,EAAE,GAAG,EAAE,UAAU,GAAG,MAAM,IAAI,CA2CtF"}
1
+ {"version":3,"file":"signal.d.ts","sourceRoot":"","sources":["../../source/app/signal.ts"],"names":[],"mappings":"AAAA,OAAO,KAAK,EAAE,UAAU,EAAE,MAAM,YAAY,CAAA;AAC5C,OAAO,KAAK,EAAE,eAAe,EAAE,MAAM,SAAS,CAAA;AAE9C;;;;;;;GAOG;AACH,wBAAgB,oBAAoB,CAAC,GAAG,EAAE,eAAe,EAAE,GAAG,EAAE,UAAU,GAAG,MAAM,IAAI,CA2CtF"}
@@ -1,8 +1,8 @@
1
1
  /**
2
- * 内置信号处理(design §4.1 第 15 步尾部)。
2
+ * 内置信号处理(设计 §7.1 `activate` 尾部 / §7.2)。
3
3
  *
4
- * `SIGTERM` / `SIGINT` / `uncaughtException` / `unhandledRejection` ⇒ `app.close()`(触发 onClose
5
- * 的关闭 7 步)⇒ `process.exit(0/1)`。可通过 `signal: false` 整体关掉(测试 / 嵌入场景)。
4
+ * `SIGTERM` / `SIGINT` / `uncaughtException` / `unhandledRejection` ⇒ `app.close()`(触发 `onClose`
5
+ * 的关闭 6 步)⇒ `process.exit(0/1)`。**仅 `listen: true` 时装信号处理**。
6
6
  *
7
7
  * @returns detach 函数(移除本次注册的监听器)
8
8
  */
@@ -0,0 +1,2 @@
1
+ export {};
2
+ //# sourceMappingURL=smoke.test.d.ts.map
@@ -0,0 +1 @@
1
+ {"version":3,"file":"smoke.test.d.ts","sourceRoot":"","sources":["../../source/app/smoke.test.ts"],"names":[],"mappings":""}
@@ -0,0 +1,210 @@
1
+ import { describe, expect, it } from 'vitest';
2
+ import { z } from 'zod';
3
+ import { defineServer, generateOpenApi, startServer } from './entries.js';
4
+ import { SystemError, UserError } from './errors.js';
5
+ import { defaultMiddlewares } from './middleware/index.js';
6
+ import { createCache, createRedis } from './redis.js';
7
+ const summarySchema = {
8
+ querystring: z.object({ currency: z.string().optional() }),
9
+ response: {
10
+ 200: z.object({
11
+ status: z.string(),
12
+ message: z.string(),
13
+ data: z.object({ siteName: z.string(), currency: z.string() }),
14
+ }),
15
+ },
16
+ };
17
+ describe('端到端冒烟:defineServer → startServer → inject → generateOpenApi', () => {
18
+ it('真实装配路径跑通最小服务器(无 Postgres / 无 Redis / 无网络)', async () => {
19
+ const previousDatabaseUrl = process.env.DATABASE_URL;
20
+ const previousRedisUrl = process.env.REDIS_URL;
21
+ delete process.env.DATABASE_URL;
22
+ delete process.env.REDIS_URL;
23
+ const lifecycleEvents = [];
24
+ const counters = { readConfiguration: 0, disposeDatabase: 0 };
25
+ const database = {
26
+ readSystemConfiguration: async () => {
27
+ counters.readConfiguration += 1;
28
+ return { siteName: 'taicode-smoke', currency: 'CNY' };
29
+ },
30
+ };
31
+ try {
32
+ const server = defineServer({
33
+ app: { service: 'smoke', title: 'Smoke Service', version: '1.4.0' },
34
+ resources: (ctx) => {
35
+ const redis = createRedis(ctx.env, ctx.log);
36
+ return {
37
+ // 构造但不建连;出 spec 路径也会构造
38
+ database,
39
+ redis,
40
+ cache: createCache({ env: ctx.env, prefix: 'smoke', redis, log: ctx.log }),
41
+ };
42
+ },
43
+ config: async ({ resources }) => {
44
+ const configuration = await resources.database.readSystemConfiguration();
45
+ return { siteName: configuration.siteName, currency: configuration.currency };
46
+ },
47
+ // 声明序即启动序(services 之间不得相互依赖)
48
+ services: (ctx) => {
49
+ return {
50
+ // routers 拿不到 resources / config,只能穿过 service
51
+ // 配置一律在**调用期**现读:构造期不得读配置(出 spec 路径不跑 config 槽位)
52
+ configuration: {
53
+ readSiteName: () => ctx.config.siteName,
54
+ readCurrency: () => ctx.config.currency,
55
+ },
56
+ ledger: {
57
+ init: () => {
58
+ lifecycleEvents.push('init:ledger');
59
+ },
60
+ dispose: () => {
61
+ lifecycleEvents.push('dispose:ledger');
62
+ },
63
+ },
64
+ repository: {
65
+ init: () => {
66
+ lifecycleEvents.push('init:repository');
67
+ },
68
+ dispose: () => {
69
+ lifecycleEvents.push('dispose:repository');
70
+ },
71
+ },
72
+ mailer: {
73
+ init: () => {
74
+ lifecycleEvents.push('init:mailer');
75
+ },
76
+ dispose: () => {
77
+ lifecycleEvents.push('dispose:mailer');
78
+ },
79
+ },
80
+ databaseLifecycle: {
81
+ dispose: () => {
82
+ counters.disposeDatabase += 1;
83
+ },
84
+ },
85
+ };
86
+ },
87
+ // 默认预设 [cors, context, auth]
88
+ middlewares: (ctx) => defaultMiddlewares(ctx),
89
+ routers: (ctx) => {
90
+ ctx.app.get('/ledger/summary', { schema: summarySchema }, () => ({
91
+ siteName: ctx.services.configuration.readSiteName(),
92
+ currency: ctx.services.configuration.readCurrency(),
93
+ }));
94
+ ctx.app.get('/ledger/invalid-token', () => {
95
+ throw new UserError('INVALID_TOKEN', undefined, '令牌无效或已过期');
96
+ });
97
+ ctx.app.get('/ledger/database-down', () => {
98
+ throw new SystemError('DATABASE_UNAVAILABLE', undefined, '数据库暂不可用');
99
+ });
100
+ },
101
+ });
102
+ const app = await startServer(server, { listen: false });
103
+ // ── 装配期:声明序 init ────────────────────────────────────────────
104
+ expect(lifecycleEvents).toEqual(['init:ledger', 'init:repository', 'init:mailer']);
105
+ expect(counters.readConfiguration).toBe(1);
106
+ try {
107
+ // ── 业务路由:成功信封 ────────────────────────────────────────────
108
+ const summaryResponse = await app.inject({
109
+ method: 'GET',
110
+ url: '/ledger/summary?currency=CNY',
111
+ headers: { 'x-request-id': 'smoke-summary-1' },
112
+ });
113
+ expect(summaryResponse.statusCode).toBe(200);
114
+ expect(summaryResponse.json()).toEqual({
115
+ status: 'SUCCESS',
116
+ message: 'ok',
117
+ data: { siteName: 'taicode-smoke', currency: 'CNY' },
118
+ });
119
+ expect(summaryResponse.headers['x-request-id']).toBe('smoke-summary-1');
120
+ // ── UserError → 400 ───────────────────────────────────────────────
121
+ const userErrorResponse = await app.inject({ method: 'GET', url: '/ledger/invalid-token' });
122
+ expect(userErrorResponse.statusCode).toBe(400);
123
+ expect(userErrorResponse.json()).toEqual({
124
+ status: 'INVALID_TOKEN',
125
+ message: '令牌无效或已过期',
126
+ data: null,
127
+ });
128
+ // ── SystemError → 500 ─────────────────────────────────────────────
129
+ const systemErrorResponse = await app.inject({ method: 'GET', url: '/ledger/database-down' });
130
+ expect(systemErrorResponse.statusCode).toBe(500);
131
+ expect(systemErrorResponse.json()).toEqual({
132
+ status: 'DATABASE_UNAVAILABLE',
133
+ message: '数据库暂不可用',
134
+ data: null,
135
+ });
136
+ // ── 未匹配路由 → 404 ──────────────────────────────────────────────
137
+ const notFoundResponse = await app.inject({ method: 'GET', url: '/no-such-route' });
138
+ expect(notFoundResponse.statusCode).toBe(404);
139
+ expect(notFoundResponse.json()).toMatchObject({ status: 'NOT_IMPLEMENTED', data: null });
140
+ expect(notFoundResponse.headers['x-request-id']).toMatch(/^[0-9a-f]{8}-[0-9a-f]{4}-[0-9a-f]{4}-[0-9a-f]{4}-[0-9a-f]{12}$/);
141
+ expect(notFoundResponse.json()).not.toHaveProperty('requestId');
142
+ }
143
+ finally {
144
+ await app.close();
145
+ }
146
+ // ── 关闭:services 逆序 dispose ─────────────────────────────────────
147
+ expect(lifecycleEvents).toEqual([
148
+ 'init:ledger',
149
+ 'init:repository',
150
+ 'init:mailer',
151
+ 'dispose:mailer',
152
+ 'dispose:repository',
153
+ 'dispose:ledger',
154
+ ]);
155
+ expect(counters.disposeDatabase).toBe(1);
156
+ // ── 同一份定义 → generateOpenApi:不跑 config、不重跑 init ──────────
157
+ const spec = (await generateOpenApi(server));
158
+ expect(counters.readConfiguration).toBe(1);
159
+ expect(lifecycleEvents).toHaveLength(6);
160
+ expect(spec.openapi).toBe('3.0.3');
161
+ expect(spec.info).toMatchObject({ title: 'Smoke Service', version: '1.4.0' });
162
+ expect(Object.keys(spec.paths ?? {})).toEqual(expect.arrayContaining(['/ledger/summary', '/ledger/invalid-token', '/ledger/database-down']));
163
+ }
164
+ finally {
165
+ if (previousDatabaseUrl === undefined) {
166
+ delete process.env.DATABASE_URL;
167
+ }
168
+ else {
169
+ process.env.DATABASE_URL = previousDatabaseUrl;
170
+ }
171
+ if (previousRedisUrl === undefined) {
172
+ delete process.env.REDIS_URL;
173
+ }
174
+ else {
175
+ process.env.REDIS_URL = previousRedisUrl;
176
+ }
177
+ }
178
+ });
179
+ it('非预置原生插件可在 routers(ctx) 里经 ctx.app.register() 注册并生效', async () => {
180
+ const smokeDecoratorPlugin = async (app) => {
181
+ app.decorate('smokeDecorator', () => 'smoke-decorated');
182
+ app.get('/plugin/decorated', () => ({
183
+ value: app.smokeDecorator(),
184
+ }));
185
+ };
186
+ const server = defineServer({
187
+ app: { service: 'smoke-plugin', title: 'Smoke Plugin', version: '1.0.0' },
188
+ resources: () => ({}),
189
+ config: () => ({}),
190
+ services: () => ({}),
191
+ middlewares: () => [],
192
+ routers: (ctx) => {
193
+ void ctx.app.register(smokeDecoratorPlugin);
194
+ },
195
+ });
196
+ const app = await startServer(server, { listen: false });
197
+ try {
198
+ const response = await app.inject({ method: 'GET', url: '/plugin/decorated' });
199
+ expect(response.statusCode).toBe(200);
200
+ expect(response.json()).toEqual({
201
+ status: 'SUCCESS',
202
+ message: 'ok',
203
+ data: { value: 'smoke-decorated' },
204
+ });
205
+ }
206
+ finally {
207
+ await app.close();
208
+ }
209
+ });
210
+ });
@@ -0,0 +1,21 @@
1
+ import type { ConfigEnv } from './env.js';
2
+ import type { LoggerLike } from './types.js';
3
+ /**
4
+ * 测试公共助手(仅测试使用)。
5
+ *
6
+ * 只放「多个测试文件都要用」的小工具:记录型 logger、测试用 env。
7
+ * 断言与业务语义一律写在各自 `*.test.ts` 内,避免助手文件成为第二真相源。
8
+ */
9
+ export interface LogEntry {
10
+ level: 'debug' | 'info' | 'warn' | 'error';
11
+ obj: unknown;
12
+ msg?: string;
13
+ }
14
+ /** 记录型 logger:满足 `LoggerLike`,便于断言「被调用了 / 在什么级别」 */
15
+ export interface RecordingLogger extends LoggerLike {
16
+ readonly entries: LogEntry[];
17
+ }
18
+ export declare function createRecordingLogger(): RecordingLogger;
19
+ /** 测试用 `ConfigEnv`(固定读取约定:`ENV` / `API_PORT` / `LOG_LEVEL` / `DATABASE_URL` / `REDIS_URL`) */
20
+ export declare function createTestConfigEnv(overrides?: Partial<ConfigEnv>): ConfigEnv;
21
+ //# sourceMappingURL=test-support.d.ts.map
@@ -0,0 +1 @@
1
+ {"version":3,"file":"test-support.d.ts","sourceRoot":"","sources":["../../source/app/test-support.ts"],"names":[],"mappings":"AAAA,OAAO,KAAK,EAAE,SAAS,EAAE,MAAM,UAAU,CAAA;AACzC,OAAO,KAAK,EAAE,UAAU,EAAE,MAAM,YAAY,CAAA;AAE5C;;;;;GAKG;AAEH,MAAM,WAAW,QAAQ;IACvB,KAAK,EAAE,OAAO,GAAG,MAAM,GAAG,MAAM,GAAG,OAAO,CAAA;IAC1C,GAAG,EAAE,OAAO,CAAA;IACZ,GAAG,CAAC,EAAE,MAAM,CAAA;CACb;AAED,oDAAoD;AACpD,MAAM,WAAW,eAAgB,SAAQ,UAAU;IACjD,QAAQ,CAAC,OAAO,EAAE,QAAQ,EAAE,CAAA;CAC7B;AAED,wBAAgB,qBAAqB,IAAI,eAAe,CAcvD;AAED,8FAA8F;AAC9F,wBAAgB,mBAAmB,CAAC,SAAS,GAAE,OAAO,CAAC,SAAS,CAAM,GAAG,SAAS,CASjF"}
@@ -0,0 +1,24 @@
1
+ export function createRecordingLogger() {
2
+ const entries = [];
3
+ const record = (level) => (obj, msg) => {
4
+ entries.push({ level, obj, msg });
5
+ };
6
+ return {
7
+ entries,
8
+ debug: record('debug'),
9
+ info: record('info'),
10
+ warn: record('warn'),
11
+ error: record('error'),
12
+ };
13
+ }
14
+ /** 测试用 `ConfigEnv`(固定读取约定:`ENV` / `API_PORT` / `LOG_LEVEL` / `DATABASE_URL` / `REDIS_URL`) */
15
+ export function createTestConfigEnv(overrides = {}) {
16
+ return {
17
+ ENV: 'test',
18
+ API_PORT: '',
19
+ LOG_LEVEL: 'info',
20
+ DATABASE_URL: '',
21
+ REDIS_URL: undefined,
22
+ ...overrides,
23
+ };
24
+ }
@@ -0,0 +1,34 @@
1
+ /**
2
+ * 框架统一提供的类型增强(设计 §8「类型增强」红线)。
3
+ *
4
+ * `request.user` / `request.token` / `request.id` 与 `routeOptions.config.accessControls`(以及
5
+ * 限流规则 `rateLimit`)的 `declare module 'fastify'` 由 common 统一提供,各仓删除自建声明。
6
+ */
7
+ import type { RequestContext } from '../context/index.js';
8
+ /** 路由级访问控制声明(各服务在 `routeOptions.config.accessControls` 里写) */
9
+ export type AccessControl = 'NoLoginRequired' | 'IgnorePermission' | 'RequiredSecurityCode';
10
+ /** 路由级限流规则(`createRateLimitMiddleware` 消费,另有路由级 `config.rateLimit`) */
11
+ export interface RateLimitRule {
12
+ limit: number;
13
+ /** 窗口大小(秒) */
14
+ window: number;
15
+ /** 限流键维度,默认 `['ip']` */
16
+ keys?: string[];
17
+ }
18
+ declare module 'fastify' {
19
+ interface FastifyRequest {
20
+ /** 认证后由 `auth` 中间件填充 */
21
+ user?: unknown;
22
+ /** 认证后由 `auth` 中间件填充 */
23
+ token?: unknown;
24
+ /** `createContextMiddleware()` 注入的请求级上下文 */
25
+ ctx?: RequestContext;
26
+ }
27
+ interface FastifyContextConfig {
28
+ /** 路由级访问控制(`NoLoginRequired` / `IgnorePermission` / `RequiredSecurityCode`) */
29
+ accessControls?: AccessControl[];
30
+ /** 路由级限流规则 */
31
+ rateLimit?: RateLimitRule;
32
+ }
33
+ }
34
+ //# sourceMappingURL=type-augment.d.ts.map
@@ -0,0 +1 @@
1
+ {"version":3,"file":"type-augment.d.ts","sourceRoot":"","sources":["../../source/app/type-augment.ts"],"names":[],"mappings":"AAAA;;;;;GAKG;AAEH,OAAO,KAAK,EAAE,cAAc,EAAE,MAAM,qBAAqB,CAAA;AAEzD,8DAA8D;AAC9D,MAAM,MAAM,aAAa,GAAG,iBAAiB,GAAG,kBAAkB,GAAG,sBAAsB,CAAA;AAE3F,uEAAuE;AACvE,MAAM,WAAW,aAAa;IAC5B,KAAK,EAAE,MAAM,CAAA;IACb,cAAc;IACd,MAAM,EAAE,MAAM,CAAA;IACd,wBAAwB;IACxB,IAAI,CAAC,EAAE,MAAM,EAAE,CAAA;CAChB;AAED,OAAO,QAAQ,SAAS,CAAC;IACvB,UAAU,cAAc;QACtB,wBAAwB;QACxB,IAAI,CAAC,EAAE,OAAO,CAAA;QACd,wBAAwB;QACxB,KAAK,CAAC,EAAE,OAAO,CAAA;QACf,4CAA4C;QAC5C,GAAG,CAAC,EAAE,cAAc,CAAA;KACrB;IAED,UAAU,oBAAoB;QAC5B,+EAA+E;QAC/E,cAAc,CAAC,EAAE,aAAa,EAAE,CAAA;QAChC,cAAc;QACd,SAAS,CAAC,EAAE,aAAa,CAAA;KAC1B;CACF"}
@@ -0,0 +1,7 @@
1
+ /**
2
+ * 框架统一提供的类型增强(设计 §8「类型增强」红线)。
3
+ *
4
+ * `request.user` / `request.token` / `request.id` 与 `routeOptions.config.accessControls`(以及
5
+ * 限流规则 `rateLimit`)的 `declare module 'fastify'` 由 common 统一提供,各仓删除自建声明。
6
+ */
7
+ export {};