imean-service-engine 2.0.0 → 2.0.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 (72) hide show
  1. package/dist/index.d.mts +77 -52
  2. package/dist/index.d.ts +77 -52
  3. package/dist/index.js +2078 -1945
  4. package/dist/index.mjs +2076 -1944
  5. package/package.json +9 -2
  6. package/.vscode/settings.json +0 -8
  7. package/src/core/checker.ts +0 -33
  8. package/src/core/decorators.test.ts +0 -96
  9. package/src/core/decorators.ts +0 -68
  10. package/src/core/engine.test.ts +0 -218
  11. package/src/core/engine.ts +0 -635
  12. package/src/core/errors.ts +0 -28
  13. package/src/core/factory.test.ts +0 -73
  14. package/src/core/factory.ts +0 -92
  15. package/src/core/logger.ts +0 -65
  16. package/src/core/testing.ts +0 -73
  17. package/src/core/types.ts +0 -191
  18. package/src/index.ts +0 -49
  19. package/src/metadata/README.md +0 -422
  20. package/src/metadata/metadata.test.ts +0 -369
  21. package/src/metadata/metadata.ts +0 -512
  22. package/src/plugins/action/action-plugin.test.ts +0 -660
  23. package/src/plugins/action/decorator.ts +0 -14
  24. package/src/plugins/action/index.ts +0 -4
  25. package/src/plugins/action/plugin.ts +0 -349
  26. package/src/plugins/action/types.ts +0 -49
  27. package/src/plugins/action/utils.test.ts +0 -196
  28. package/src/plugins/action/utils.ts +0 -111
  29. package/src/plugins/cache/adapter.test.ts +0 -689
  30. package/src/plugins/cache/adapter.ts +0 -324
  31. package/src/plugins/cache/cache-plugin.test.ts +0 -269
  32. package/src/plugins/cache/decorator.ts +0 -26
  33. package/src/plugins/cache/index.ts +0 -20
  34. package/src/plugins/cache/plugin.ts +0 -299
  35. package/src/plugins/cache/types.ts +0 -69
  36. package/src/plugins/client-code/client-code-plugin.test.ts +0 -511
  37. package/src/plugins/client-code/format.ts +0 -9
  38. package/src/plugins/client-code/generator.test.ts +0 -52
  39. package/src/plugins/client-code/generator.ts +0 -263
  40. package/src/plugins/client-code/index.ts +0 -15
  41. package/src/plugins/client-code/plugin.ts +0 -158
  42. package/src/plugins/client-code/types.ts +0 -52
  43. package/src/plugins/client-code/utils.ts +0 -164
  44. package/src/plugins/graceful-shutdown/graceful-shutdown-plugin.test.ts +0 -401
  45. package/src/plugins/graceful-shutdown/index.ts +0 -3
  46. package/src/plugins/graceful-shutdown/plugin.ts +0 -279
  47. package/src/plugins/graceful-shutdown/types.ts +0 -17
  48. package/src/plugins/rate-limit/rate-limit-plugin.example.ts +0 -171
  49. package/src/plugins/route/components/Layout.tsx +0 -42
  50. package/src/plugins/route/components/ServiceStatusPage.tsx +0 -141
  51. package/src/plugins/route/decorator.ts +0 -50
  52. package/src/plugins/route/index.ts +0 -16
  53. package/src/plugins/route/plugin.ts +0 -218
  54. package/src/plugins/route/route-plugin.test.ts +0 -759
  55. package/src/plugins/route/types.ts +0 -72
  56. package/src/plugins/schedule/README.md +0 -309
  57. package/src/plugins/schedule/decorator.ts +0 -25
  58. package/src/plugins/schedule/index.ts +0 -12
  59. package/src/plugins/schedule/mock-etcd.ts +0 -145
  60. package/src/plugins/schedule/plugin.ts +0 -164
  61. package/src/plugins/schedule/schedule-plugin.test.ts +0 -312
  62. package/src/plugins/schedule/scheduler.ts +0 -164
  63. package/src/plugins/schedule/types.ts +0 -94
  64. package/src/plugins/schedule/utils.test.ts +0 -163
  65. package/src/plugins/schedule/utils.ts +0 -41
  66. package/tests/integration/client.test.ts +0 -203
  67. package/tests/integration/dev-service.ts +0 -301
  68. package/tests/integration/generated/client.ts +0 -123
  69. package/tests/integration/start-service.ts +0 -21
  70. package/tsconfig.json +0 -27
  71. package/tsup.config.ts +0 -16
  72. package/vitest.config.ts +0 -19
@@ -1,349 +0,0 @@
1
- import * as ejson from "ejson";
2
- import { Context } from "hono";
3
- import logger from "../../core/logger";
4
- import {
5
- HandlerMetadata,
6
- Microservice,
7
- Plugin,
8
- PluginModuleOptionsSchema,
9
- PluginPriority,
10
- } from "../../core/types";
11
- import { ActionModuleOptions, ActionOptions } from "./types";
12
- import { buildActionPath, parseAndValidateParams } from "./utils";
13
-
14
- /**
15
- * 检查对象是否是 AsyncIterable
16
- */
17
- function isAsyncIterable(obj: any): obj is AsyncIterable<any> {
18
- return obj != null && typeof obj[Symbol.asyncIterator] === "function";
19
- }
20
-
21
- /**
22
- * ActionPlugin - 动作处理插件
23
- * 提供基于下标的参数传递、zod 校验和自动参数注入
24
- */
25
- export class ActionPlugin implements Plugin<ActionModuleOptions> {
26
- public readonly name = "action-plugin";
27
- public readonly priority = PluginPriority.ROUTE; // 路由插件优先级最低,必须最后执行
28
- private engine!: Microservice;
29
-
30
- /**
31
- * 声明Module配置Schema(用于类型推导+运行时校验)
32
- */
33
- getModuleOptionsSchema(): PluginModuleOptionsSchema<ActionModuleOptions> {
34
- return {
35
- _type: {} as ActionModuleOptions,
36
- validate: (options) => {
37
- if (
38
- options.actionMiddlewares !== undefined &&
39
- !Array.isArray(options.actionMiddlewares)
40
- ) {
41
- return "actionMiddlewares must be an array";
42
- }
43
- return true;
44
- },
45
- };
46
- }
47
-
48
- /**
49
- * 引擎初始化钩子:获取Hono实例
50
- */
51
- onInit(engine: Microservice): void {
52
- this.engine = engine;
53
- logger.info("ActionPlugin initialized");
54
- }
55
-
56
- /**
57
- * Handler加载钩子:解析type="action"的Handler元数据,注册HTTP路由
58
- */
59
- onHandlerLoad(handlers: HandlerMetadata[]): void {
60
- // 筛选出所有type="action"的Handler元数据
61
- const actionHandlers = handlers.filter(
62
- (handler) => handler.type === "action"
63
- );
64
-
65
- logger.info(`Found ${actionHandlers.length} action handler(s)`);
66
-
67
- for (const handler of actionHandlers) {
68
- const actionOptions = handler.options as ActionOptions;
69
- const methodName = handler.methodName;
70
- const moduleClass = handler.module;
71
-
72
- // 获取模块实例
73
- const moduleInstance = this.engine.get(moduleClass);
74
- if (!moduleInstance) {
75
- logger.warn(
76
- `Module instance not found for ${moduleClass.name}, skipping action registration`
77
- );
78
- continue;
79
- }
80
-
81
- // 获取模块配置
82
- const moduleMetadata = this.engine
83
- .getModules()
84
- .find((m: any) => m.clazz === moduleClass);
85
- if (!moduleMetadata) {
86
- logger.warn(
87
- `Module metadata not found for ${moduleClass.name}, skipping action registration`
88
- );
89
- continue;
90
- }
91
- const moduleOptions = (moduleMetadata.options ||
92
- {}) as ActionModuleOptions;
93
-
94
- // 构建路由路径(引擎prefix + 模块名 + Handler名)
95
- const enginePrefix = this.engine.options.prefix || "";
96
- const actionPath = buildActionPath(
97
- enginePrefix,
98
- moduleMetadata.name,
99
- methodName
100
- );
101
-
102
- // 获取参数校验 schemas
103
- const paramSchemas = actionOptions.params || [];
104
- const returnSchema = actionOptions.returns;
105
-
106
- // 构建路由处理器
107
- const actionHandler = async (ctx: Context) => {
108
- // 引擎保证此时原型上的方法已经被所有包装插件包装
109
- // 直接调用方法名即可获取包装后的方法
110
- const method = (moduleInstance as any)[methodName];
111
- if (typeof method !== "function") {
112
- const errorResponse = ejson.stringify({
113
- success: false,
114
- error: "Action method not found",
115
- });
116
- return ctx.text(errorResponse, 500, {
117
- "Content-Type": "application/json",
118
- });
119
- }
120
-
121
- try {
122
- // 解析请求体中的下标参数
123
- // 支持 GET 请求(从 query 参数解析)和 POST 等(从 body 解析)
124
- let body: any = {};
125
- if (ctx.req.method === "GET") {
126
- // GET 请求从 query 参数解析
127
- const url = new URL(ctx.req.url);
128
- url.searchParams.forEach((value, key) => {
129
- body[key] = value;
130
- });
131
- } else {
132
- // POST 等请求从 body 解析,使用 ejson 反序列化以支持更多数据类型
133
- try {
134
- const rawBody = await ctx.req.text();
135
- if (rawBody) {
136
- body = ejson.parse(rawBody);
137
- }
138
- } catch (parseError) {
139
- // 如果 ejson 解析失败,尝试 JSON 解析(向后兼容)
140
- try {
141
- body = await ctx.req.json().catch(() => ({}));
142
- } catch {
143
- body = {};
144
- }
145
- }
146
- }
147
-
148
- // 解析并验证参数(一次性验证所有参数)
149
- const validation = parseAndValidateParams(body, paramSchemas);
150
- if (!validation.success) {
151
- const errors = validation.error.issues || [];
152
- const errorMessage = `Validation failed: ${errors
153
- .map((e) => {
154
- // 处理路径:如果是参数索引(数字),显示为参数位置;否则显示为路径
155
- const path = e.path || [];
156
- const pathStr =
157
- path.length > 0
158
- ? path
159
- .map((p, i) => {
160
- // 第一个路径是参数索引("0", "1"等),转换为参数位置
161
- if (
162
- i === 0 &&
163
- typeof p === "string" &&
164
- /^\d+$/.test(p)
165
- ) {
166
- return `参数[${p}]`;
167
- }
168
- // 后续路径是嵌套属性
169
- return String(p);
170
- })
171
- .join(".")
172
- : "unknown";
173
- return `${pathStr}: ${e.message}`;
174
- })
175
- .join(", ")}`;
176
- const errorResponse = ejson.stringify({
177
- success: false,
178
- error: errorMessage,
179
- });
180
- return ctx.text(errorResponse, 400, {
181
- "Content-Type": "application/json",
182
- });
183
- }
184
-
185
- // 调用方法,自动注入参数
186
- // 检查方法是否需要 Context 参数:如果方法的参数数量比定义的参数多 1,则第一个参数是 Context
187
- const methodLength = method.length;
188
- const paramsLength = paramSchemas.length;
189
- const args = [...validation.data];
190
-
191
- // 如果方法参数数量比定义的参数多 1,且第一个参数可能是 Context
192
- if (methodLength > paramsLength) {
193
- // 将 Context 作为第一个参数注入
194
- args.unshift(ctx);
195
- }
196
-
197
- const result = await method.apply(moduleInstance, args);
198
-
199
- // 如果是流式返回(async generator),转换为 HTTP 流式响应(SSE 格式)
200
- if (actionOptions.stream) {
201
- if (!isAsyncIterable(result)) {
202
- const errorResponse = ejson.stringify({
203
- success: false,
204
- error: "Stream action must return AsyncIterable",
205
- });
206
- return ctx.text(errorResponse, 500, {
207
- "Content-Type": "application/json",
208
- });
209
- }
210
-
211
- // 使用流式响应格式
212
- // 客户端使用 tryParseCompleteJson 函数来处理流式响应,支持从缓冲区解析完整的 JSON 对象
213
- // 格式:{ value?: T, done: boolean, error?: string }
214
- // 客户端会累积不完整的 chunk 到缓冲区,然后使用 tryParseCompleteJson 解析完整的 JSON 对象
215
- // 所以我们可以直接发送 JSON 对象,客户端会自动处理合并的 chunk
216
- const encoder = new TextEncoder();
217
- const iterator = result[Symbol.asyncIterator]();
218
- const stream = new ReadableStream<Uint8Array>({
219
- async start(controller) {
220
- try {
221
- while (true) {
222
- const { value, done } = await iterator.next();
223
- if (done) {
224
- // 发送结束标记
225
- controller.enqueue(
226
- encoder.encode(ejson.stringify({ done: true }))
227
- );
228
- controller.close();
229
- break;
230
- }
231
- // 发送数据块,每个 JSON 对象是完整的
232
- // 客户端会使用 tryParseCompleteJson 来处理可能被合并的 chunk
233
- controller.enqueue(
234
- encoder.encode(ejson.stringify({ value, done: false }))
235
- );
236
- }
237
- } catch (error) {
238
- // 发送错误
239
- controller.enqueue(
240
- encoder.encode(
241
- ejson.stringify({
242
- error:
243
- error instanceof Error
244
- ? error.message
245
- : String(error),
246
- done: true,
247
- })
248
- )
249
- );
250
- controller.close();
251
- }
252
- },
253
- });
254
-
255
- return new Response(stream, {
256
- headers: {
257
- "Content-Type": "text/event-stream",
258
- "Cache-Control": "no-cache",
259
- Connection: "keep-alive",
260
- },
261
- });
262
- }
263
-
264
- // 校验返回值(如果提供了 return schema)
265
- if (returnSchema) {
266
- const returnValidation = returnSchema.safeParse(result);
267
- if (!returnValidation.success) {
268
- logger.error(
269
- `Return value validation failed for ${moduleClass.name}.${methodName}`,
270
- returnValidation.error
271
- );
272
- const returnErrors = returnValidation.error.issues || [];
273
- const errorMessage = `Return value validation failed: ${returnErrors
274
- .map((e) => {
275
- const path = e.path || [];
276
- const pathStr =
277
- path.length > 0
278
- ? path.map((p) => String(p)).join(".")
279
- : "root";
280
- return `${pathStr}: ${e.message}`;
281
- })
282
- .join(", ")}`;
283
- const errorResponse = ejson.stringify({
284
- success: false,
285
- error: errorMessage,
286
- });
287
- return ctx.text(errorResponse, 400, {
288
- "Content-Type": "application/json",
289
- });
290
- }
291
- // 使用 ejson 序列化成功响应
292
- const successResponse = ejson.stringify({
293
- success: true,
294
- data: returnValidation.data,
295
- });
296
- return ctx.text(successResponse, 200, {
297
- "Content-Type": "application/json",
298
- });
299
- }
300
-
301
- // 使用 ejson 序列化成功响应
302
- const successResponse = ejson.stringify({
303
- success: true,
304
- data: result,
305
- });
306
- return ctx.text(successResponse, 200, {
307
- "Content-Type": "application/json",
308
- });
309
- } catch (error) {
310
- logger.error(
311
- `Error in action handler ${moduleClass.name}.${methodName}`,
312
- error
313
- );
314
- const errorResponse = ejson.stringify({
315
- success: false,
316
- error: error instanceof Error ? error.message : String(error),
317
- });
318
- return ctx.text(errorResponse, 500, {
319
- "Content-Type": "application/json",
320
- });
321
- }
322
- };
323
-
324
- // 获取 Hono 实例
325
- const hono = this.engine.getHono();
326
-
327
- // 应用模块级中间件
328
- let route = hono;
329
- if (
330
- moduleOptions.actionMiddlewares &&
331
- moduleOptions.actionMiddlewares.length > 0
332
- ) {
333
- route = route.use(actionPath, ...moduleOptions.actionMiddlewares);
334
- }
335
-
336
- // 应用动作级中间件
337
- if (actionOptions.middlewares && actionOptions.middlewares.length > 0) {
338
- route = route.use(actionPath, ...actionOptions.middlewares);
339
- }
340
-
341
- // 注册路由(固定支持 GET 和 POST)
342
- route.get(actionPath, actionHandler);
343
- route.post(actionPath, actionHandler);
344
- logger.info(
345
- `[注册动作] ${moduleClass.name}.${methodName} ${actionOptions.description}`
346
- );
347
- }
348
- }
349
- }
@@ -1,49 +0,0 @@
1
- import { z } from "zod";
2
- import { MiddlewareHandler } from "hono";
3
-
4
- /**
5
- * Action装饰器配置选项
6
- */
7
- export interface ActionOptions {
8
- /**
9
- * 动作描述
10
- */
11
- description?: string;
12
-
13
- /**
14
- * 参数校验 Schema(使用 zod)
15
- * 数组顺序对应方法参数的顺序
16
- */
17
- params: z.ZodTypeAny[];
18
-
19
- /**
20
- * 返回值校验 Schema(使用 zod)
21
- */
22
- returns?: z.ZodTypeAny;
23
-
24
- /**
25
- * 是否流式返回(默认 false)
26
- */
27
- stream?: boolean;
28
-
29
- /**
30
- * 是否幂等(默认 false)
31
- */
32
- idempotence?: boolean;
33
-
34
- /**
35
- * 路由专属中间件
36
- */
37
- middlewares?: MiddlewareHandler[];
38
- }
39
-
40
- /**
41
- * ActionPlugin的Module配置
42
- */
43
- export interface ActionModuleOptions {
44
- /**
45
- * 模块级路由中间件
46
- */
47
- actionMiddlewares?: MiddlewareHandler[];
48
- }
49
-
@@ -1,196 +0,0 @@
1
- import { describe, expect, it } from "vitest";
2
- import { z } from "zod";
3
- import {
4
- buildActionPath,
5
- buildParamsSchema,
6
- parseAndValidateParams,
7
- } from "./utils";
8
-
9
- describe("ActionPlugin Utils", () => {
10
- describe("buildParamsSchema", () => {
11
- it("应该能够构建参数验证 Schema", () => {
12
- const schemas = [z.string(), z.number()];
13
- const paramsSchema = buildParamsSchema(schemas);
14
-
15
- // 验证 Schema 结构
16
- const result = paramsSchema.safeParse({ "0": "Alice", "1": 25 });
17
- expect(result.success).toBe(true);
18
- if (result.success) {
19
- expect(result.data["0"]).toBe("Alice");
20
- expect(result.data["1"]).toBe(25);
21
- }
22
- });
23
-
24
- it("应该支持空数组", () => {
25
- const schemas: z.ZodTypeAny[] = [];
26
- const paramsSchema = buildParamsSchema(schemas);
27
-
28
- const result = paramsSchema.safeParse({});
29
- expect(result.success).toBe(true);
30
- });
31
-
32
- it("应该支持可选参数", () => {
33
- const schemas = [z.string(), z.number().optional(), z.string()];
34
- const paramsSchema = buildParamsSchema(schemas);
35
-
36
- // 缺少可选参数应该通过验证
37
- const result = paramsSchema.safeParse({ "0": "Alice", "2": "Bob" });
38
- expect(result.success).toBe(true);
39
- if (result.success) {
40
- expect(result.data["0"]).toBe("Alice");
41
- expect(result.data["1"]).toBeUndefined();
42
- expect(result.data["2"]).toBe("Bob");
43
- }
44
- });
45
-
46
- it("应该支持复杂类型", () => {
47
- const UserSchema = z.object({
48
- id: z.string(),
49
- name: z.string(),
50
- });
51
- const schemas = [z.string(), UserSchema];
52
- const paramsSchema = buildParamsSchema(schemas);
53
-
54
- const result = paramsSchema.safeParse({
55
- "0": "test",
56
- "1": { id: "1", name: "Alice" },
57
- });
58
- expect(result.success).toBe(true);
59
- });
60
- });
61
-
62
- describe("parseAndValidateParams", () => {
63
- it("应该能够解析并验证参数", () => {
64
- const body = { "0": "Alice", "1": 25 };
65
- const schemas = [z.string(), z.number()];
66
- const result = parseAndValidateParams(body, schemas);
67
-
68
- expect(result.success).toBe(true);
69
- if (result.success) {
70
- expect(result.data).toEqual(["Alice", 25]);
71
- }
72
- });
73
-
74
- it("应该返回空数组当没有定义参数时", () => {
75
- const body = { "0": "Alice" };
76
- const schemas: z.ZodTypeAny[] = [];
77
- const result = parseAndValidateParams(body, schemas);
78
-
79
- expect(result.success).toBe(true);
80
- if (result.success) {
81
- expect(result.data).toEqual([]);
82
- }
83
- });
84
-
85
- it("应该处理空 body", () => {
86
- const body = null;
87
- const schemas = [z.string().optional()];
88
- const result = parseAndValidateParams(body, schemas);
89
-
90
- expect(result.success).toBe(true);
91
- if (result.success) {
92
- expect(result.data[0]).toBeUndefined();
93
- }
94
- });
95
-
96
- it("应该验证参数类型", () => {
97
- const body = { "0": "Alice", "1": "invalid" };
98
- const schemas = [z.string(), z.number()];
99
- const result = parseAndValidateParams(body, schemas);
100
-
101
- expect(result.success).toBe(false);
102
- if (!result.success) {
103
- // Zod 4.x: 使用 issues 替代 errors
104
- const errors = result.error.errors || result.error.issues || [];
105
- expect(errors.length).toBeGreaterThan(0);
106
- expect(errors[0].path).toContain("1");
107
- }
108
- });
109
-
110
- it("应该支持可选参数", () => {
111
- const body = { "0": "Alice" };
112
- const schemas = [
113
- z.string(),
114
- z.number().optional(),
115
- z.string().optional(),
116
- ];
117
- const result = parseAndValidateParams(body, schemas);
118
-
119
- expect(result.success).toBe(true);
120
- if (result.success) {
121
- expect(result.data[0]).toBe("Alice");
122
- expect(result.data[1]).toBeUndefined();
123
- expect(result.data[2]).toBeUndefined();
124
- }
125
- });
126
-
127
- it("应该支持参数对齐(跳过中间参数)", () => {
128
- const body = { "0": "Alice", "3": true };
129
- const schemas = [
130
- z.string(),
131
- z.number().optional(),
132
- z.string().optional(),
133
- z.boolean(),
134
- ];
135
- const result = parseAndValidateParams(body, schemas);
136
-
137
- expect(result.success).toBe(true);
138
- if (result.success) {
139
- expect(result.data[0]).toBe("Alice");
140
- expect(result.data[1]).toBeUndefined();
141
- expect(result.data[2]).toBeUndefined();
142
- expect(result.data[3]).toBe(true);
143
- }
144
- });
145
-
146
- it("应该正确处理缺失的必需参数", () => {
147
- const body = { "0": "Alice" };
148
- const schemas = [z.string(), z.number()];
149
- const result = parseAndValidateParams(body, schemas);
150
-
151
- expect(result.success).toBe(false);
152
- if (!result.success) {
153
- // Zod 4.x: 使用 issues 替代 errors
154
- const errors = result.error.errors || result.error.issues || [];
155
- expect(errors.length).toBeGreaterThan(0);
156
- // 应该报告缺少 "1" 参数
157
- const hasMissingError = errors.some((err) =>
158
- err.path?.includes("1")
159
- );
160
- expect(hasMissingError).toBe(true);
161
- }
162
- });
163
- });
164
-
165
- describe("buildActionPath", () => {
166
- it("应该能够构建带 prefix 的路由路径", () => {
167
- const path = buildActionPath("/api", "user-service", "createUser");
168
- expect(path).toBe("/api/user-service/createUser");
169
- });
170
-
171
- it("应该能够构建不带 prefix 的路由路径", () => {
172
- const path = buildActionPath("", "user-service", "createUser");
173
- expect(path).toBe("/user-service/createUser");
174
- });
175
-
176
- it("应该处理 prefix 末尾的斜杠", () => {
177
- const path = buildActionPath("/api/", "user-service", "createUser");
178
- expect(path).toBe("/api/user-service/createUser");
179
- });
180
-
181
- it("应该处理多个连续的斜杠", () => {
182
- const path = buildActionPath("//api//", "user-service", "createUser");
183
- expect(path).toBe("/api/user-service/createUser");
184
- });
185
-
186
- it("应该处理 prefix 为空字符串的情况", () => {
187
- const path = buildActionPath("", "test-service", "testMethod");
188
- expect(path).toBe("/test-service/testMethod");
189
- });
190
-
191
- it("应该处理只有斜杠的 prefix", () => {
192
- const path = buildActionPath("/", "test-service", "testMethod");
193
- expect(path).toBe("/test-service/testMethod");
194
- });
195
- });
196
- });
@@ -1,111 +0,0 @@
1
- import { z } from "zod";
2
-
3
- /**
4
- * 构建参数验证 Schema
5
- * 将参数数组转换为对象格式:{"0": schema0, "1": schema1, ...}
6
- *
7
- * @param schemas 参数校验 Schema 数组
8
- * @returns ZodObject Schema
9
- *
10
- * @example
11
- * ```ts
12
- * const schemas = [z.string(), z.number()];
13
- * const paramsSchema = buildParamsSchema(schemas);
14
- * // 等价于 z.object({ "0": z.string(), "1": z.number() })
15
- * ```
16
- */
17
- export function buildParamsSchema(
18
- schemas: z.ZodTypeAny[]
19
- ): z.ZodObject<Record<string, z.ZodTypeAny>> {
20
- const shape: Record<string, z.ZodTypeAny> = {};
21
- for (let i = 0; i < schemas.length; i++) {
22
- shape[String(i)] = schemas[i];
23
- }
24
- return z.object(shape);
25
- }
26
-
27
- /**
28
- * 解析并验证请求体中的下标参数
29
- * 请求格式:{"0":"value1","1":"value2",...}
30
- * 根据 metadata 中的 params 数组定义来确定参数数量和类型
31
- *
32
- * @param body 请求体对象
33
- * @param schemas 参数校验 Schema 数组
34
- * @returns 验证结果,成功时返回参数数组,失败时返回 ZodError
35
- *
36
- * @example
37
- * ```ts
38
- * const body = { "0": "Alice", "1": 25 };
39
- * const schemas = [z.string(), z.number()];
40
- * const result = parseAndValidateParams(body, schemas);
41
- * // result.success === true
42
- * // result.data === ["Alice", 25]
43
- * ```
44
- */
45
- export function parseAndValidateParams(
46
- body: any,
47
- schemas: z.ZodTypeAny[]
48
- ): { success: true; data: any[] } | { success: false; error: z.ZodError } {
49
- if (!body || typeof body !== "object") {
50
- body = {};
51
- }
52
-
53
- // 如果没有定义参数,直接返回空数组
54
- if (schemas.length === 0) {
55
- return { success: true, data: [] };
56
- }
57
-
58
- // 构建对象格式的验证 Schema:{"0": schema0, "1": schema1, ...}
59
- const paramsSchema = buildParamsSchema(schemas);
60
-
61
- // 一次性验证所有参数
62
- const validation = paramsSchema.safeParse(body);
63
- if (!validation.success) {
64
- return {
65
- success: false,
66
- error: validation.error,
67
- };
68
- }
69
-
70
- // 将验证后的对象转换为数组,按索引顺序提取值
71
- const validatedData: any[] = [];
72
- for (let i = 0; i < schemas.length; i++) {
73
- validatedData[i] = validation.data[String(i)];
74
- }
75
-
76
- return {
77
- success: true,
78
- data: validatedData,
79
- };
80
- }
81
-
82
- /**
83
- * 构建 Action 路由路径
84
- * 格式:引擎prefix + 模块名 + Handler名
85
- *
86
- * @param enginePrefix 引擎路由前缀(可选)
87
- * @param moduleName 模块名
88
- * @param handlerName Handler 方法名
89
- * @returns 规范化后的路由路径
90
- *
91
- * @example
92
- * ```ts
93
- * buildActionPath("/api", "user-service", "createUser")
94
- * // => "/api/user-service/createUser"
95
- *
96
- * buildActionPath("", "user-service", "createUser")
97
- * // => "/user-service/createUser"
98
- * ```
99
- */
100
- export function buildActionPath(
101
- enginePrefix: string,
102
- moduleName: string,
103
- handlerName: string
104
- ): string {
105
- // 移除prefix末尾的斜杠,然后拼接路径,最后清理多余的斜杠
106
- const normalizedPrefix = enginePrefix.replace(/\/+$/, "");
107
- return `/${normalizedPrefix}/${moduleName}/${handlerName}`
108
- .replace(/\/+/g, "/")
109
- .replace(/^\/\//, "/"); // 处理prefix为空时产生的双斜杠
110
- }
111
-