@taicode/common-server 3.7.13 → 4.0.0-beta.1

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 (130) 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 +48 -0
  5. package/output/app/boot.d.ts.map +1 -0
  6. package/output/app/boot.js +123 -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 +68 -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 +32 -0
  35. package/output/app/define-server.negative-cases.d.ts.map +1 -0
  36. package/output/app/define-server.negative-cases.js +209 -0
  37. package/output/app/define-server.test.d.ts +2 -0
  38. package/output/app/define-server.test.d.ts.map +1 -0
  39. package/output/app/define-server.test.js +97 -0
  40. package/output/app/entries.d.ts +29 -0
  41. package/output/app/entries.d.ts.map +1 -0
  42. package/output/app/entries.js +64 -0
  43. package/output/app/env.d.ts +59 -0
  44. package/output/app/env.d.ts.map +1 -0
  45. package/output/app/env.js +66 -0
  46. package/output/app/env.test.d.ts +2 -0
  47. package/output/app/env.test.d.ts.map +1 -0
  48. package/output/app/env.test.js +124 -0
  49. package/output/app/error-contract.test.d.ts +2 -0
  50. package/output/app/error-contract.test.d.ts.map +1 -0
  51. package/output/app/error-contract.test.js +125 -0
  52. package/output/app/errors.d.ts +20 -0
  53. package/output/app/errors.d.ts.map +1 -0
  54. package/output/app/errors.js +51 -0
  55. package/output/app/generate-openapi.test.d.ts +2 -0
  56. package/output/app/generate-openapi.test.d.ts.map +1 -0
  57. package/output/app/generate-openapi.test.js +123 -0
  58. package/output/app/index.d.ts +16 -0
  59. package/output/app/index.d.ts.map +1 -0
  60. package/output/app/index.js +18 -0
  61. package/output/app/lifecycle-logging.test.d.ts +2 -0
  62. package/output/app/lifecycle-logging.test.d.ts.map +1 -0
  63. package/output/app/lifecycle-logging.test.js +155 -0
  64. package/output/app/lifecycle.d.ts +70 -0
  65. package/output/app/lifecycle.d.ts.map +1 -0
  66. package/output/app/lifecycle.js +99 -0
  67. package/output/app/lifecycle.test.d.ts +2 -0
  68. package/output/app/lifecycle.test.d.ts.map +1 -0
  69. package/output/app/lifecycle.test.js +152 -0
  70. package/output/app/middleware/auth.d.ts +71 -0
  71. package/output/app/middleware/auth.d.ts.map +1 -0
  72. package/output/app/middleware/auth.js +190 -0
  73. package/output/app/middleware/context.d.ts +13 -0
  74. package/output/app/middleware/context.d.ts.map +1 -0
  75. package/output/app/middleware/context.js +15 -0
  76. package/output/app/middleware/cors.d.ts +14 -0
  77. package/output/app/middleware/cors.d.ts.map +1 -0
  78. package/output/app/middleware/cors.js +30 -0
  79. package/output/app/middleware/index.d.ts +17 -0
  80. package/output/app/middleware/index.d.ts.map +1 -0
  81. package/output/app/middleware/index.js +16 -0
  82. package/output/app/middleware/rate-limit.d.ts +24 -0
  83. package/output/app/middleware/rate-limit.d.ts.map +1 -0
  84. package/output/app/middleware/rate-limit.js +46 -0
  85. package/output/app/middleware-order.test.d.ts +2 -0
  86. package/output/app/middleware-order.test.d.ts.map +1 -0
  87. package/output/app/middleware-order.test.js +148 -0
  88. package/output/app/permission-operations.d.ts +48 -0
  89. package/output/app/permission-operations.d.ts.map +1 -0
  90. package/output/app/permission-operations.js +77 -0
  91. package/output/app/permission-operations.test.d.ts +2 -0
  92. package/output/app/permission-operations.test.d.ts.map +1 -0
  93. package/output/app/permission-operations.test.js +72 -0
  94. package/output/app/permission-registration.test.d.ts +2 -0
  95. package/output/app/permission-registration.test.d.ts.map +1 -0
  96. package/output/app/permission-registration.test.js +138 -0
  97. package/output/app/redis.d.ts +45 -0
  98. package/output/app/redis.d.ts.map +1 -0
  99. package/output/app/redis.js +61 -0
  100. package/output/app/registered-operations.test.d.ts +2 -0
  101. package/output/app/registered-operations.test.d.ts.map +1 -0
  102. package/output/app/registered-operations.test.js +57 -0
  103. package/output/app/request-id.test.d.ts +2 -0
  104. package/output/app/request-id.test.d.ts.map +1 -0
  105. package/output/app/request-id.test.js +130 -0
  106. package/output/app/resources.test.d.ts +2 -0
  107. package/output/app/resources.test.d.ts.map +1 -0
  108. package/output/app/resources.test.js +197 -0
  109. package/output/app/shutdown.d.ts +22 -0
  110. package/output/app/shutdown.d.ts.map +1 -0
  111. package/output/app/shutdown.js +30 -0
  112. package/output/app/shutdown.test.d.ts +2 -0
  113. package/output/app/shutdown.test.d.ts.map +1 -0
  114. package/output/app/shutdown.test.js +103 -0
  115. package/output/app/signal.d.ts +12 -0
  116. package/output/app/signal.d.ts.map +1 -0
  117. package/output/app/signal.js +45 -0
  118. package/output/app/smoke.test.d.ts +2 -0
  119. package/output/app/smoke.test.d.ts.map +1 -0
  120. package/output/app/smoke.test.js +210 -0
  121. package/output/app/test-support.d.ts +21 -0
  122. package/output/app/test-support.d.ts.map +1 -0
  123. package/output/app/test-support.js +24 -0
  124. package/output/app/type-augment.d.ts +34 -0
  125. package/output/app/type-augment.d.ts.map +1 -0
  126. package/output/app/type-augment.js +7 -0
  127. package/output/app/types.d.ts +211 -0
  128. package/output/app/types.d.ts.map +1 -0
  129. package/output/app/types.js +2 -0
  130. package/package.json +12 -5
@@ -0,0 +1,2 @@
1
+ export {};
2
+ //# sourceMappingURL=error-contract.test.d.ts.map
@@ -0,0 +1 @@
1
+ {"version":3,"file":"error-contract.test.d.ts","sourceRoot":"","sources":["../../source/app/error-contract.test.ts"],"names":[],"mappings":""}
@@ -0,0 +1,125 @@
1
+ import { describe, expect, it } from 'vitest';
2
+ import { z } from 'zod';
3
+ import { defineServer, startServer } from './entries.js';
4
+ import { SystemError, UserError } from './errors.js';
5
+ /**
6
+ * 错误契约——**强制、不可配置**。
7
+ *
8
+ * 响应信封 `{ status, message, data }`(错误时 `data: null`,`status` 放错误码字符串);
9
+ * 映射固定:`UserError`→400(`error.status` 字面量)/ `SystemError`→500 / zod→400 / 未知→500 /
10
+ * 未匹配路由→404。错误只给稳定**码**,文案不归框架。
11
+ */
12
+ /** zod validator 由 core 的 `schema` 统一装配,运行时按 zod 校验(类型上仍是 FastifySchema) */
13
+ const validatedQuerySchema = {
14
+ querystring: z.object({ count: z.coerce.number().int().min(1) }),
15
+ };
16
+ function createErrorContractServer() {
17
+ return defineServer({
18
+ app: { service: 'error-contract', title: 'Error Contract', version: '1.0.0' },
19
+ resources: () => ({}),
20
+ config: () => ({}),
21
+ services: () => ({}),
22
+ middlewares: () => [],
23
+ routers: (ctx) => {
24
+ ctx.app.get('/ok', () => ({ hello: 'world' }));
25
+ ctx.app.get('/envelope', () => ({ status: 'CUSTOM', message: 'm', data: 1 }));
26
+ ctx.app.get('/user-error', () => {
27
+ throw new UserError('INVALID_TOKEN', undefined, 'token 无效');
28
+ });
29
+ ctx.app.get('/system-error', () => {
30
+ throw new SystemError('DATABASE_UNAVAILABLE', undefined, '数据库不可用');
31
+ });
32
+ ctx.app.get('/unknown-error', () => {
33
+ throw new Error('boom');
34
+ });
35
+ ctx.app.get('/validated', { schema: validatedQuerySchema }, (request) => ({
36
+ count: request.query.count,
37
+ }));
38
+ },
39
+ });
40
+ }
41
+ describe('错误契约', () => {
42
+ it('成功信封:{ status: SUCCESS, message: ok, data }', async () => {
43
+ const app = await startServer(createErrorContractServer(), { listen: false });
44
+ const response = await app.inject({ method: 'GET', url: '/ok' });
45
+ expect(response.statusCode).toBe(200);
46
+ expect(response.json()).toEqual({ status: 'SUCCESS', message: 'ok', data: { hello: 'world' } });
47
+ await app.close();
48
+ });
49
+ it('已是信封的载荷不被二次包裹', async () => {
50
+ const app = await startServer(createErrorContractServer(), { listen: false });
51
+ const response = await app.inject({ method: 'GET', url: '/envelope' });
52
+ expect(response.statusCode).toBe(200);
53
+ expect(response.json()).toEqual({ status: 'CUSTOM', message: 'm', data: 1 });
54
+ await app.close();
55
+ });
56
+ it('UserError → 400(取 `error.status` 字面量),status 放错误码、data 为 null', async () => {
57
+ const app = await startServer(createErrorContractServer(), { listen: false });
58
+ const response = await app.inject({ method: 'GET', url: '/user-error' });
59
+ expect(response.statusCode).toBe(400);
60
+ expect(response.json()).toEqual({ status: 'INVALID_TOKEN', message: 'token 无效', data: null });
61
+ await app.close();
62
+ });
63
+ it('SystemError → 500(取 `error.status` 字面量)', async () => {
64
+ const app = await startServer(createErrorContractServer(), { listen: false });
65
+ const response = await app.inject({ method: 'GET', url: '/system-error' });
66
+ expect(response.statusCode).toBe(500);
67
+ expect(response.json()).toEqual({ status: 'DATABASE_UNAVAILABLE', message: '数据库不可用', data: null });
68
+ await app.close();
69
+ });
70
+ it('未知错误 → 500 + UNKNOWN_ERROR', async () => {
71
+ const app = await startServer(createErrorContractServer(), { listen: false });
72
+ const response = await app.inject({ method: 'GET', url: '/unknown-error' });
73
+ expect(response.statusCode).toBe(500);
74
+ expect(response.json()).toEqual({ status: 'UNKNOWN_ERROR', message: 'boom', data: null });
75
+ await app.close();
76
+ });
77
+ it('zod 校验失败 → 400(INVALID_INPUT);通过校验 → 200', async () => {
78
+ const app = await startServer(createErrorContractServer(), { listen: false });
79
+ const invalid = await app.inject({ method: 'GET', url: '/validated?count=0' });
80
+ expect(invalid.statusCode).toBe(400);
81
+ const invalidBody = invalid.json();
82
+ expect(invalidBody.status).toBe('INVALID_INPUT');
83
+ expect(invalidBody.data).toBeNull();
84
+ expect(invalidBody.message).toContain('count');
85
+ const valid = await app.inject({ method: 'GET', url: '/validated?count=3' });
86
+ expect(valid.statusCode).toBe(200);
87
+ expect(valid.json()).toEqual({ status: 'SUCCESS', message: 'ok', data: { count: 3 } });
88
+ await app.close();
89
+ });
90
+ it('未匹配路由 → 404 + NOT_IMPLEMENTED', async () => {
91
+ const app = await startServer(createErrorContractServer(), { listen: false });
92
+ const response = await app.inject({ method: 'GET', url: '/missing' });
93
+ expect(response.statusCode).toBe(404);
94
+ const body = response.json();
95
+ expect(body.status).toBe('NOT_IMPLEMENTED');
96
+ expect(body.data).toBeNull();
97
+ expect(body.message).toContain('GET /missing');
98
+ await app.close();
99
+ });
100
+ it('资源包里的 `classifyError` 被用作数据层错误分类(→500)', async () => {
101
+ const server = defineServer({
102
+ app: { service: 'error-contract-database', title: 'Error Contract Database', version: '1.0.0' },
103
+ resources: () => ({
104
+ database: {
105
+ classifyError: (error) => error instanceof Error && error.message === 'pg down'
106
+ ? new SystemError('DATABASE_ERROR', { operation: 'query' }, 'pg down')
107
+ : null,
108
+ },
109
+ }),
110
+ config: () => ({}),
111
+ services: () => ({}),
112
+ middlewares: () => [],
113
+ routers: (ctx) => {
114
+ ctx.app.get('/database-down', () => {
115
+ throw new Error('pg down');
116
+ });
117
+ },
118
+ });
119
+ const app = await startServer(server, { listen: false });
120
+ const response = await app.inject({ method: 'GET', url: '/database-down' });
121
+ expect(response.statusCode).toBe(500);
122
+ expect(response.json()).toEqual({ status: 'DATABASE_ERROR', message: 'pg down', data: null });
123
+ await app.close();
124
+ });
125
+ });
@@ -0,0 +1,20 @@
1
+ import type { ErrorContext } from '@taicode/common-base';
2
+ export declare class UserError extends Error {
3
+ readonly type: string;
4
+ readonly context?: ErrorContext | undefined;
5
+ /** 类型守卫(`Symbol.for` 品牌,不用 `instanceof`) */
6
+ static is(value: unknown): value is UserError;
7
+ /** 固定 HTTP 状态(字面量类型) */
8
+ readonly status: 400;
9
+ constructor(type: string, context?: ErrorContext | undefined, message?: string);
10
+ }
11
+ export declare class SystemError extends Error {
12
+ readonly type: string;
13
+ readonly context?: ErrorContext | undefined;
14
+ /** 类型守卫(`Symbol.for` 品牌,不用 `instanceof`) */
15
+ static is(value: unknown): value is SystemError;
16
+ /** 固定 HTTP 状态(字面量类型) */
17
+ readonly status: 500;
18
+ constructor(type: string, context?: ErrorContext | undefined, message?: string);
19
+ }
20
+ //# sourceMappingURL=errors.d.ts.map
@@ -0,0 +1 @@
1
+ {"version":3,"file":"errors.d.ts","sourceRoot":"","sources":["../../source/app/errors.ts"],"names":[],"mappings":"AAAA,OAAO,KAAK,EAAE,YAAY,EAAE,MAAM,sBAAsB,CAAA;AAsBxD,qBAAa,SAAU,SAAQ,KAAK;aASC,IAAI,EAAE,MAAM;aAAkB,OAAO,CAAC,EAAE,YAAY;IARvF,4CAA4C;IAC5C,MAAM,CAAC,EAAE,CAAC,KAAK,EAAE,OAAO,GAAG,KAAK,IAAI,SAAS;IAI7C,wBAAwB;IACxB,QAAQ,CAAC,MAAM,EAAG,GAAG,CAAS;gBAEK,IAAI,EAAE,MAAM,EAAkB,OAAO,CAAC,EAAE,YAAY,YAAA,EAAE,OAAO,SAAK;CAKtG;AAED,qBAAa,WAAY,SAAQ,KAAK;aASD,IAAI,EAAE,MAAM;aAAkB,OAAO,CAAC,EAAE,YAAY;IARvF,4CAA4C;IAC5C,MAAM,CAAC,EAAE,CAAC,KAAK,EAAE,OAAO,GAAG,KAAK,IAAI,WAAW;IAI/C,wBAAwB;IACxB,QAAQ,CAAC,MAAM,EAAG,GAAG,CAAS;gBAEK,IAAI,EAAE,MAAM,EAAkB,OAAO,CAAC,EAAE,YAAY,YAAA,EAAE,OAAO,SAAK;CAKtG"}
@@ -0,0 +1,51 @@
1
+ /**
2
+ * 错误契约(N2 §2.12)。
3
+ *
4
+ * 框架直接提供并导出 `UserError` / `SystemError`:
5
+ * - `status` 是**字面量**(`400` / `500`)—— 映射表写成
6
+ * `Record<UserError['status'] | SystemError['status'], number>` 时若改错直接编译失败;
7
+ * - `is()` 是**类型守卫**,走 `Symbol.for` 品牌(**不用 `instanceof`**:跨包 / 编译后 / 原型链丢失
8
+ * 都仍然可靠);品牌与 `@taicode/common-base` 同名,故基座抛出的同类错误同样被识别;
9
+ * - 业务只抛这两个错误,状态码映射由框架固定(`UserError`→400 / `SystemError`→500)。
10
+ *
11
+ * 构造签名保持 `(code, context?, message?)`。
12
+ */
13
+ const USER_ERROR_BRAND = Symbol.for('@taicode/common/UserError');
14
+ const SYSTEM_ERROR_BRAND = Symbol.for('@taicode/common/SystemError');
15
+ function hasBrand(value, brand) {
16
+ return typeof value === 'object' && value !== null && value[brand] === true;
17
+ }
18
+ export class UserError extends Error {
19
+ type;
20
+ context;
21
+ /** 类型守卫(`Symbol.for` 品牌,不用 `instanceof`) */
22
+ static is(value) {
23
+ return hasBrand(value, USER_ERROR_BRAND);
24
+ }
25
+ /** 固定 HTTP 状态(字面量类型) */
26
+ status = 400;
27
+ constructor(type, context, message = '') {
28
+ super(message, context);
29
+ this.type = type;
30
+ this.context = context;
31
+ this.name = 'UserError';
32
+ Object.defineProperty(this, USER_ERROR_BRAND, { value: true });
33
+ }
34
+ }
35
+ export class SystemError extends Error {
36
+ type;
37
+ context;
38
+ /** 类型守卫(`Symbol.for` 品牌,不用 `instanceof`) */
39
+ static is(value) {
40
+ return hasBrand(value, SYSTEM_ERROR_BRAND);
41
+ }
42
+ /** 固定 HTTP 状态(字面量类型) */
43
+ status = 500;
44
+ constructor(type, context, message = '') {
45
+ super(message, context);
46
+ this.type = type;
47
+ this.context = context;
48
+ this.name = 'SystemError';
49
+ Object.defineProperty(this, SYSTEM_ERROR_BRAND, { value: true });
50
+ }
51
+ }
@@ -0,0 +1,2 @@
1
+ export {};
2
+ //# sourceMappingURL=generate-openapi.test.d.ts.map
@@ -0,0 +1 @@
1
+ {"version":3,"file":"generate-openapi.test.d.ts","sourceRoot":"","sources":["../../source/app/generate-openapi.test.ts"],"names":[],"mappings":""}
@@ -0,0 +1,123 @@
1
+ import { describe, expect, it } from 'vitest';
2
+ import { z } from 'zod';
3
+ import { defineServer, generateOpenApi, startServer } from './entries.js';
4
+ import { createRedis } from './redis.js';
5
+ /**
6
+ * `generateOpenApi()` 红线。
7
+ *
8
+ * 全程**不得要求配置有效**:不跑 `config` 槽位、不要求 `DATABASE_URL` / `REDIS_URL` 有效、不建连;
9
+ * `resources`(只构造不建连)/ `services` / 中间件注册 / 路由注册照常执行。
10
+ * 跳过集:`config` / `initLifecycleUnits` / `listen`。
11
+ */
12
+ const routeSchema = {
13
+ querystring: z.object({ name: z.string().optional() }),
14
+ };
15
+ describe('generateOpenApi 红线', () => {
16
+ it('不跑 config、不要求 DATABASE_URL / REDIS_URL,能出 spec', async () => {
17
+ delete process.env.DATABASE_URL;
18
+ delete process.env.REDIS_URL;
19
+ let configCalls = 0;
20
+ const server = defineServer({
21
+ app: { service: 'openapi-test', title: 'OpenAPI Test', version: '1.2.3' },
22
+ resources: () => ({ database: { connected: false } }),
23
+ config: () => {
24
+ configCalls += 1;
25
+ return {};
26
+ },
27
+ services: () => ({}),
28
+ middlewares: () => [],
29
+ routers: (ctx) => {
30
+ ctx.app.get('/widgets', { schema: routeSchema }, () => []);
31
+ },
32
+ });
33
+ const spec = (await generateOpenApi(server));
34
+ expect(configCalls).toBe(0);
35
+ expect(spec.openapi).toBe('3.0.3');
36
+ expect(spec.info).toMatchObject({ title: 'OpenAPI Test', version: '1.2.3' });
37
+ // 身份块只有 service / title / version:`info.description` 不再由框架填
38
+ expect(spec.info?.description).toBeUndefined();
39
+ expect(spec.servers).toEqual([{ url: '/' }]);
40
+ expect(Object.keys(spec.paths ?? {})).toContain('/widgets');
41
+ expect(Object.keys(spec.components?.securitySchemes ?? {})).toContain('bearerAuth');
42
+ });
43
+ it('跳过集:不跑 config / initLifecycleUnits(但跑 resources / services / 中间件注册 / routers)', async () => {
44
+ const calls = [];
45
+ const server = defineServer({
46
+ app: { service: 'openapi-skip-set', title: 'OpenAPI Skip Set', version: '1.0.0' },
47
+ resources: () => {
48
+ calls.push('resources');
49
+ return {};
50
+ },
51
+ config: () => {
52
+ calls.push('config');
53
+ return {};
54
+ },
55
+ services: () => {
56
+ calls.push('services');
57
+ return {
58
+ a: {
59
+ init: () => {
60
+ calls.push('init:a');
61
+ },
62
+ },
63
+ };
64
+ },
65
+ middlewares: () => [
66
+ {
67
+ name: 'probe',
68
+ register: () => {
69
+ calls.push('register:probe');
70
+ },
71
+ },
72
+ ],
73
+ routers: () => {
74
+ calls.push('routers');
75
+ },
76
+ });
77
+ await generateOpenApi(server);
78
+ expect(calls).toEqual(['resources', 'services', 'register:probe', 'routers']);
79
+ });
80
+ it('REDIS_URL 存在时仍不建连(惰性包装)', async () => {
81
+ process.env.REDIS_URL = 'redis://127.0.0.1:1/0';
82
+ try {
83
+ const server = defineServer({
84
+ app: { service: 'openapi-redis-lazy', title: 'Redis Lazy', version: '1.0.0' },
85
+ // resources 里照常构造 Redis 句柄(惰性包装:不建连、不 ping)
86
+ resources: (ctx) => ({ redis: createRedis(ctx.env, ctx.log) }),
87
+ config: () => ({}),
88
+ services: () => ({}),
89
+ middlewares: () => [],
90
+ routers: () => undefined,
91
+ });
92
+ const spec = (await generateOpenApi(server));
93
+ expect(spec.openapi).toBe('3.0.3');
94
+ }
95
+ finally {
96
+ delete process.env.REDIS_URL;
97
+ }
98
+ });
99
+ });
100
+ describe('startServer 与 generateOpenApi 共用同一份定义', () => {
101
+ it('startServer(server, { listen: false }) 返回原生实例、可 inject,且会跑 config', async () => {
102
+ let configCalls = 0;
103
+ const server = defineServer({
104
+ app: { service: 'openapi-shared-definition', title: 'Shared Definition', version: '1.0.0' },
105
+ resources: () => ({}),
106
+ config: () => {
107
+ configCalls += 1;
108
+ return {};
109
+ },
110
+ services: () => ({}),
111
+ middlewares: () => [],
112
+ routers: (ctx) => {
113
+ ctx.app.get('/ping', () => ({ pong: true }));
114
+ },
115
+ });
116
+ const app = await startServer(server, { listen: false });
117
+ expect(typeof app.inject).toBe('function');
118
+ const response = await app.inject({ method: 'GET', url: '/ping' });
119
+ expect(response.statusCode).toBe(200);
120
+ expect(configCalls).toBe(1);
121
+ await app.close();
122
+ });
123
+ });
@@ -0,0 +1,16 @@
1
+ import './type-augment.js';
2
+ export { defineServer, isServer, SERVER_BRAND, startServer, generateOpenApi } from './entries.js';
3
+ export { loadEnvFiles, loadRuntimeEnvFiles, resolveConfigEnv, ENV_FILE_LAYERS, TEST_ENV_FILE_LAYER, DEFAULT_LOG_LEVEL } from './env.js';
4
+ export type { ConfigEnv } from './env.js';
5
+ export { UserError, SystemError } from './errors.js';
6
+ export { createRedis, createCache, DEFAULT_REDIS_CACHE_TTL_SECONDS } from './redis.js';
7
+ export { createCorsMiddleware, createContextMiddleware, createAuthMiddleware, createRateLimitMiddleware, defaultMiddlewares, defaultAuthSkip } from './middleware/index.js';
8
+ export { REQUEST_ID_HEADER } from './core/request-id.js';
9
+ export { DOC_ROUTE_PREFIX, DOC_SPEC_ROUTE } from './core/openapi.js';
10
+ export { extractPermissionOperations } from './permission-operations.js';
11
+ export type { AppIdentity, AppInstance, LoggerLike, MaybePromise, RedisHandle, Live, ResourcesView, AssertNoDependencies, ServicesShape, ResourcesContext, ResourcesFactory, ConfigContext, ConfigFactory, ServiceUnit, ServiceContext, ServicesFactory, MiddlewareUnit, MiddlewareContext, MiddlewaresFactory, RouterContext, RoutersFactory, ServerConfig, Server, StartServerOptions, ShutdownContext, OpenApiDocument, } from './types.js';
12
+ export type { CacheSetupParams } from './redis.js';
13
+ export type { PermissionOperation } from './permission-operations.js';
14
+ export type { CorsMiddlewareOptions, ContextMiddlewareOptions, AuthMiddlewareOptions, AuthSkip, AuthVerifyInput, AuthVerifyMetadata, AuthVerifyResult, AccountIntegrationConfig, RateLimitMiddlewareOptions } from './middleware/index.js';
15
+ export type { AccessControl, RateLimitRule } from './type-augment.js';
16
+ //# sourceMappingURL=index.d.ts.map
@@ -0,0 +1 @@
1
+ {"version":3,"file":"index.d.ts","sourceRoot":"","sources":["../../source/app/index.ts"],"names":[],"mappings":"AAEA,OAAO,mBAAmB,CAAA;AAG1B,OAAO,EAAE,YAAY,EAAE,QAAQ,EAAE,YAAY,EAAE,WAAW,EAAE,eAAe,EAAE,MAAM,cAAc,CAAA;AAGjG,OAAO,EAAE,YAAY,EAAE,mBAAmB,EAAE,gBAAgB,EAAE,eAAe,EAAE,mBAAmB,EAAE,iBAAiB,EAAE,MAAM,UAAU,CAAA;AACvI,YAAY,EAAE,SAAS,EAAE,MAAM,UAAU,CAAA;AAGzC,OAAO,EAAE,SAAS,EAAE,WAAW,EAAE,MAAM,aAAa,CAAA;AAGpD,OAAO,EAAE,WAAW,EAAE,WAAW,EAAE,+BAA+B,EAAE,MAAM,YAAY,CAAA;AAGtF,OAAO,EAAE,oBAAoB,EAAE,uBAAuB,EAAE,oBAAoB,EAAE,yBAAyB,EAAE,kBAAkB,EAAE,eAAe,EAAE,MAAM,uBAAuB,CAAA;AAG3K,OAAO,EAAE,iBAAiB,EAAE,MAAM,sBAAsB,CAAA;AACxD,OAAO,EAAE,gBAAgB,EAAE,cAAc,EAAE,MAAM,mBAAmB,CAAA;AAGpE,OAAO,EAAE,2BAA2B,EAAE,MAAM,4BAA4B,CAAA;AAGxE,YAAY,EAEV,WAAW,EACX,WAAW,EACX,UAAU,EACV,YAAY,EACZ,WAAW,EAEX,IAAI,EACJ,aAAa,EACb,oBAAoB,EACpB,aAAa,EAEb,gBAAgB,EAChB,gBAAgB,EAChB,aAAa,EACb,aAAa,EACb,WAAW,EACX,cAAc,EACd,eAAe,EACf,cAAc,EACd,iBAAiB,EACjB,kBAAkB,EAClB,aAAa,EACb,cAAc,EAEd,YAAY,EACZ,MAAM,EACN,kBAAkB,EAClB,eAAe,EACf,eAAe,GAChB,MAAM,YAAY,CAAA;AACnB,YAAY,EAAE,gBAAgB,EAAE,MAAM,YAAY,CAAA;AAClD,YAAY,EAAE,mBAAmB,EAAE,MAAM,4BAA4B,CAAA;AACrE,YAAY,EAAE,qBAAqB,EAAE,wBAAwB,EAAE,qBAAqB,EAAE,QAAQ,EAAE,eAAe,EAAE,kBAAkB,EAAE,gBAAgB,EAAE,wBAAwB,EAAE,0BAA0B,EAAE,MAAM,uBAAuB,CAAA;AAC1O,YAAY,EAAE,aAAa,EAAE,aAAa,EAAE,MAAM,mBAAmB,CAAA"}
@@ -0,0 +1,18 @@
1
+ // 提供 `request.user` / `request.token` / `request.ctx` 与 `routeOptions.config.accessControls`
2
+ // 的统一类型增强。
3
+ import './type-augment.js';
4
+ // ── 入口 API(N1 §1 / N2 §2.8 / §2.10 / §2.11)───────────────────
5
+ export { defineServer, isServer, SERVER_BRAND, startServer, generateOpenApi } from './entries.js';
6
+ // ── 配置层(env 固定加载)────────────────────────────────────────
7
+ export { loadEnvFiles, loadRuntimeEnvFiles, resolveConfigEnv, ENV_FILE_LAYERS, TEST_ENV_FILE_LAYER, DEFAULT_LOG_LEVEL } from './env.js';
8
+ // ── 错误类型(N2 §2.12)──────────────────────────────────────────
9
+ export { UserError, SystemError } from './errors.js';
10
+ // ── 引导期 helper(`resources(ctx)` 里按需调用)─────────────────
11
+ export { createRedis, createCache, DEFAULT_REDIS_CACHE_TTL_SECONDS } from './redis.js';
12
+ // ── 标准中间件单元 ───────────────────────────────────────────────
13
+ export { createCorsMiddleware, createContextMiddleware, createAuthMiddleware, createRateLimitMiddleware, defaultMiddlewares, defaultAuthSkip } from './middleware/index.js';
14
+ // ── 公共路由 / header 常量 ───────────────────────────────────────
15
+ export { REQUEST_ID_HEADER } from './core/request-id.js';
16
+ export { DOC_ROUTE_PREFIX, DOC_SPEC_ROUTE } from './core/openapi.js';
17
+ // ── 权限操作目录(框架内置;各仓用它替代自写实现)───────────────
18
+ export { extractPermissionOperations } from './permission-operations.js';
@@ -0,0 +1,2 @@
1
+ export {};
2
+ //# sourceMappingURL=lifecycle-logging.test.d.ts.map
@@ -0,0 +1 @@
1
+ {"version":3,"file":"lifecycle-logging.test.d.ts","sourceRoot":"","sources":["../../source/app/lifecycle-logging.test.ts"],"names":[],"mappings":""}
@@ -0,0 +1,155 @@
1
+ import { describe, expect, it, vi } from 'vitest';
2
+ import { defineServer, startServer } from './entries.js';
3
+ import { buildDisposeOrder, initServiceUnits, LIFECYCLE_PHASE_DISPOSE, LIFECYCLE_PHASE_INIT, MIDDLEWARES_LOG_STAGE, middlewareUnitName, SERVICES_STAGE, serviceUnitName, } from './lifecycle.js';
4
+ import { createRecordingLogger } from './test-support.js';
5
+ /**
6
+ * 逐单元日志。
7
+ *
8
+ * - **注册期**:每个中间件注册后打一条 `debug`,带 `{ service, unit, stage: 'middlewares' }`;
9
+ * - **init / dispose**:逐单元 `debug`(开始 / 完成),失败 `error`;带 `{ service, unit, stage, phase }`;
10
+ * - 中间件名字:`unit.name ?? '<service>:middleware[<index>]'`;services 用对象键。
11
+ *
12
+ * 分两层验证:
13
+ * 1. **单元层**:直接调 `initServiceUnits` / `buildDisposeOrder`,注入记录型 logger;
14
+ * 2. **真路径**:`startServer(server, { listen: false })` 走完整装配;把框架 logger 的写入流接到
15
+ * 捕获数组(mock `./core/logging.js` 的 `createLoggerOptions`),再按字段断言。
16
+ */
17
+ // ── 单元层:记录型 logger ────────────────────────────────────────
18
+ /** 按相位 / 阶段 / 消息片段挑出 `unit` 列表(保序) */
19
+ function unitsOf(log, phase, stage, messagePart) {
20
+ return log.entries
21
+ .filter((entry) => entry.level === 'debug' && entry.msg?.includes(messagePart) === true)
22
+ .map((entry) => entry.obj)
23
+ .filter((fields) => fields.phase === phase && fields.stage === stage)
24
+ .map((fields) => fields.unit);
25
+ }
26
+ function expectServiceField(log, service) {
27
+ const fields = log.entries
28
+ .filter((entry) => entry.level === 'debug')
29
+ .map((entry) => entry.obj);
30
+ expect(fields.length).toBeGreaterThan(0);
31
+ for (const field of fields) {
32
+ expect(field.service).toBe(service);
33
+ }
34
+ }
35
+ describe('逐单元日志:init / dispose', () => {
36
+ it('init 逐单元 debug:单元名 = 对象键', async () => {
37
+ const log = createRecordingLogger();
38
+ const services = {
39
+ alpha: { init: () => undefined },
40
+ beta: { init: () => undefined },
41
+ };
42
+ await initServiceUnits({ service: 'svc', log, services });
43
+ expect(unitsOf(log, LIFECYCLE_PHASE_INIT, SERVICES_STAGE, 'start')).toEqual(['alpha', 'beta']);
44
+ expect(unitsOf(log, LIFECYCLE_PHASE_INIT, SERVICES_STAGE, 'done')).toEqual(['alpha', 'beta']);
45
+ expectServiceField(log, 'svc');
46
+ expect(serviceUnitName('alpha')).toBe('alpha');
47
+ });
48
+ it('dispose 逐单元 debug:声明序逆序', async () => {
49
+ const log = createRecordingLogger();
50
+ const services = {
51
+ alpha: { dispose: () => undefined },
52
+ beta: { dispose: () => undefined },
53
+ };
54
+ for (const step of buildDisposeOrder({ service: 'svc', log, services })) {
55
+ await step();
56
+ }
57
+ expect(unitsOf(log, LIFECYCLE_PHASE_DISPOSE, SERVICES_STAGE, 'start')).toEqual(['beta', 'alpha']);
58
+ expectServiceField(log, 'svc');
59
+ });
60
+ it('单元 init 抛错 → 逐单元 error(带 unit / stage / phase)后重新抛出', async () => {
61
+ const log = createRecordingLogger();
62
+ const services = {
63
+ boom: {
64
+ init: () => {
65
+ throw new Error('init boom');
66
+ },
67
+ },
68
+ };
69
+ await expect(initServiceUnits({ service: 'svc', log, services })).rejects.toThrow('init boom');
70
+ const failure = log.entries.find((entry) => entry.level === 'error');
71
+ expect(failure).toBeDefined();
72
+ const fields = failure?.obj;
73
+ expect(fields.unit).toBe('boom');
74
+ expect(fields.stage).toBe(SERVICES_STAGE);
75
+ expect(fields.phase).toBe(LIFECYCLE_PHASE_INIT);
76
+ expect(fields.error.message).toBe('init boom');
77
+ });
78
+ it('不传 logger → 生命周期函数照常工作(纯函数式调用不发日志)', async () => {
79
+ const calls = [];
80
+ const services = {
81
+ alpha: {
82
+ init: () => {
83
+ calls.push('init');
84
+ },
85
+ },
86
+ };
87
+ await initServiceUnits({ services });
88
+ expect(calls).toEqual(['init']);
89
+ });
90
+ it('中间件日志名字退路:`<service>:middleware[index]`', () => {
91
+ expect(middlewareUnitName('svc', {}, 3)).toBe('svc:middleware[3]');
92
+ expect(middlewareUnitName('svc', { name: 'named' }, 1)).toBe('named');
93
+ });
94
+ });
95
+ // ── 真路径:`startServer` 走完整装配,捕获框架 logger 写入 ────────
96
+ const captured = vi.hoisted(() => ({ lines: [] }));
97
+ vi.mock('./core/logging.js', () => ({
98
+ LOG_REDACT_PATHS: [],
99
+ // 把 pino 的目标流换成捕获数组(level=debug,否则逐单元 debug 不会落盘)
100
+ createLoggerOptions: () => ({
101
+ level: 'debug',
102
+ stream: {
103
+ write: (chunk) => {
104
+ captured.lines.push(chunk);
105
+ },
106
+ },
107
+ }),
108
+ }));
109
+ function capturedRecords() {
110
+ return captured.lines
111
+ .join('')
112
+ .split('\n')
113
+ .filter((line) => line.trim().length > 0)
114
+ .map((line) => JSON.parse(line));
115
+ }
116
+ /** 按字段匹配捕获到的记录(pino 还会加 `time` / `pid` 等,故只比关键字段) */
117
+ function matchRecords(records, match) {
118
+ return records.filter((record) => Object.entries(match).every(([key, value]) => record[key] === value));
119
+ }
120
+ describe('startServer 真路径:注册 / init / dispose 逐单元日志', () => {
121
+ it('中间件注册、services 的 init / dispose 日志都能拿到名字;未命名用退路', async () => {
122
+ captured.lines.length = 0;
123
+ const server = defineServer({
124
+ app: { service: 'logging-svc', title: 'Logging Service', version: '1.0.0' },
125
+ resources: () => ({}),
126
+ config: () => ({}),
127
+ services: () => ({
128
+ alpha: {
129
+ init: () => undefined,
130
+ dispose: () => undefined,
131
+ },
132
+ }),
133
+ middlewares: () => [
134
+ { name: 'named', register: () => undefined },
135
+ // 未命名 → 退路 `logging-svc:middleware[1]`
136
+ { register: () => undefined },
137
+ ],
138
+ routers: () => undefined,
139
+ });
140
+ const app = await startServer(server, { listen: false });
141
+ await app.close();
142
+ const records = capturedRecords();
143
+ // 注册期:字段 `{ service, unit, stage }`,未命名走退路
144
+ const registered = matchRecords(records, { msg: '[middleware] registered' });
145
+ expect(matchRecords(registered, { unit: 'named', stage: MIDDLEWARES_LOG_STAGE }).length).toBe(1);
146
+ expect(matchRecords(registered, { unit: 'logging-svc:middleware[1]', stage: MIDDLEWARES_LOG_STAGE }).length).toBe(1);
147
+ expect(matchRecords(registered, { service: 'logging-svc' }).length).toBe(2);
148
+ // init / dispose:services 逐单元 debug(带 `phase`)
149
+ expect(matchRecords(records, { unit: 'alpha', stage: SERVICES_STAGE, phase: LIFECYCLE_PHASE_INIT }).length).toBeGreaterThan(0);
150
+ expect(matchRecords(records, { unit: 'alpha', stage: SERVICES_STAGE, phase: LIFECYCLE_PHASE_DISPOSE }).length).toBeGreaterThan(0);
151
+ // 中间件**不参与生命周期**:没有 middlewares 阶段的 init / dispose 日志
152
+ expect(matchRecords(records, { stage: MIDDLEWARES_LOG_STAGE, phase: LIFECYCLE_PHASE_INIT })).toHaveLength(0);
153
+ expect(matchRecords(records, { stage: MIDDLEWARES_LOG_STAGE, phase: LIFECYCLE_PHASE_DISPOSE })).toHaveLength(0);
154
+ });
155
+ });
@@ -0,0 +1,70 @@
1
+ import type { LoggerLike } from './types.js';
2
+ /**
3
+ * 生命周期序(N1 §4 / N2 §2.5)。
4
+ *
5
+ * **只有 `services` 参与生命周期**:
6
+ * - 单元之间**不得相互依赖**(`services` 的 ctx 没有 `services` 字段,编译期禁止);
7
+ * - 顺序语义只有一条:按**对象声明序** `init`、**逆序** `dispose`;
8
+ * - `middlewares` 按数组序注册,**不参与生命周期**(要常驻资源就放 `services`);
9
+ * - `resources` 只是惰性包装(构造不建连),框架不替它们做生命周期。
10
+ */
11
+ export declare const SERVICES_STAGE = "services";
12
+ /** `middlewares` 的**注册期日志**阶段标签(中间件不参与生命周期,只用于注册日志) */
13
+ export declare const MIDDLEWARES_LOG_STAGE = "middlewares";
14
+ /** 生命周期相位(逐单元日志字段 `phase`) */
15
+ export declare const LIFECYCLE_PHASE_INIT = "init";
16
+ export declare const LIFECYCLE_PHASE_DISPOSE = "dispose";
17
+ export type LifecyclePhase = typeof LIFECYCLE_PHASE_INIT | typeof LIFECYCLE_PHASE_DISPOSE;
18
+ /**
19
+ * 逐单元生命周期日志上下文。
20
+ *
21
+ * `log` / `service` 缺省时**不打日志**(纯单测可直接调用生命周期函数,不注入假 logger)。
22
+ * 字段名固定为全拼:`service` + `unit` + `stage` + `phase`。
23
+ */
24
+ export interface LifecycleLogContext {
25
+ /** 日志标签(`defineServer({ app: { service } })`);缺省时退路名字失去前缀 */
26
+ service?: string;
27
+ /** 记录型 logger(`ctx.log`);缺省 = 不打日志 */
28
+ log?: LoggerLike;
29
+ }
30
+ /** 逐单元日志字段(全拼):`unit` = 有效名字,`stage` = `services` */
31
+ export interface LifecycleUnitLogFields {
32
+ service: string;
33
+ unit: string;
34
+ stage: string;
35
+ phase: LifecyclePhase;
36
+ }
37
+ /**
38
+ * `services` 有效名字:**对象键即单元名**(N1 §4:没有 `name` 字段)。
39
+ */
40
+ export declare function serviceUnitName(key: string): string;
41
+ /**
42
+ * `middlewares` 日志名字:`unit.name ?? '<service>:middleware[<index>]'`(仅用于注册期日志)。
43
+ */
44
+ export declare function middlewareUnitName(service: string, unit: unknown, index: number): string;
45
+ /** `initServiceUnits` 入参(参数签名内联对象类型已按 lint 规则外提) */
46
+ export interface InitServiceUnitsParams extends LifecycleLogContext {
47
+ /** `services(ctx)` 返回的对象(**对象键即单元名**) */
48
+ services?: Record<string, unknown>;
49
+ }
50
+ /**
51
+ * 启动序:`services` 按**对象声明序**逐项 `await init?.()`;逐项打日志。
52
+ */
53
+ export declare function initServiceUnits(params: InitServiceUnitsParams): Promise<void>;
54
+ /** `buildDisposeOrder` / `disposeServiceUnits` 入参(参数签名内联对象类型已按 lint 规则外提) */
55
+ export interface DisposeServiceUnitsParams extends LifecycleLogContext {
56
+ /** `services(ctx)` 返回的对象(**对象键即单元名**) */
57
+ services?: Record<string, unknown>;
58
+ }
59
+ /**
60
+ * 关闭序:`services` 按声明序**逆序**逐项 `dispose?.()`。
61
+ */
62
+ export declare function buildDisposeOrder(params: DisposeServiceUnitsParams): Array<() => void | Promise<void>>;
63
+ /**
64
+ * 关闭步 `disposeServices`:`services` 声明序逆序释放。
65
+ *
66
+ * 单元级失败已在 `runLifecycleUnit` 内 `log.error`(带 service / unit / stage / phase);
67
+ * 此处仅保证单步不阻断后续。
68
+ */
69
+ export declare function disposeServices(params: DisposeServiceUnitsParams): Promise<void>;
70
+ //# sourceMappingURL=lifecycle.d.ts.map
@@ -0,0 +1 @@
1
+ {"version":3,"file":"lifecycle.d.ts","sourceRoot":"","sources":["../../source/app/lifecycle.ts"],"names":[],"mappings":"AAAA,OAAO,KAAK,EAAE,UAAU,EAAE,MAAM,YAAY,CAAA;AAE5C;;;;;;;;GAQG;AAEH,eAAO,MAAM,cAAc,aAAa,CAAA;AACxC,uDAAuD;AACvD,eAAO,MAAM,qBAAqB,gBAAgB,CAAA;AAElD,8BAA8B;AAC9B,eAAO,MAAM,oBAAoB,SAAS,CAAA;AAC1C,eAAO,MAAM,uBAAuB,YAAY,CAAA;AAChD,MAAM,MAAM,cAAc,GAAG,OAAO,oBAAoB,GAAG,OAAO,uBAAuB,CAAA;AAEzF;;;;;GAKG;AACH,MAAM,WAAW,mBAAmB;IAClC,6DAA6D;IAC7D,OAAO,CAAC,EAAE,MAAM,CAAA;IAChB,sCAAsC;IACtC,GAAG,CAAC,EAAE,UAAU,CAAA;CACjB;AAED,qDAAqD;AACrD,MAAM,WAAW,sBAAsB;IACrC,OAAO,EAAE,MAAM,CAAA;IACf,IAAI,EAAE,MAAM,CAAA;IACZ,KAAK,EAAE,MAAM,CAAA;IACb,KAAK,EAAE,cAAc,CAAA;CACtB;AAMD;;GAEG;AACH,wBAAgB,eAAe,CAAC,GAAG,EAAE,MAAM,GAAG,MAAM,CAEnD;AAED;;GAEG;AACH,wBAAgB,kBAAkB,CAAC,OAAO,EAAE,MAAM,EAAE,IAAI,EAAE,OAAO,EAAE,KAAK,EAAE,MAAM,GAAG,MAAM,CAKxF;AAED,oDAAoD;AACpD,MAAM,WAAW,sBAAuB,SAAQ,mBAAmB;IACjE,yCAAyC;IACzC,QAAQ,CAAC,EAAE,MAAM,CAAC,MAAM,EAAE,OAAO,CAAC,CAAA;CACnC;AAmCD;;GAEG;AACH,wBAAsB,gBAAgB,CAAC,MAAM,EAAE,sBAAsB,GAAG,OAAO,CAAC,IAAI,CAAC,CAIpF;AAED,6EAA6E;AAC7E,MAAM,WAAW,yBAA0B,SAAQ,mBAAmB;IACpE,yCAAyC;IACzC,QAAQ,CAAC,EAAE,MAAM,CAAC,MAAM,EAAE,OAAO,CAAC,CAAA;CACnC;AAED;;GAEG;AACH,wBAAgB,iBAAiB,CAAC,MAAM,EAAE,yBAAyB,GAAG,KAAK,CAAC,MAAM,IAAI,GAAG,OAAO,CAAC,IAAI,CAAC,CAAC,CAMtG;AAED;;;;;GAKG;AACH,wBAAsB,eAAe,CAAC,MAAM,EAAE,yBAAyB,GAAG,OAAO,CAAC,IAAI,CAAC,CAQtF"}