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.
- package/dist/index.d.mts +77 -52
- package/dist/index.d.ts +77 -52
- package/dist/index.js +2078 -1945
- package/dist/index.mjs +2076 -1944
- package/package.json +9 -2
- package/.vscode/settings.json +0 -8
- package/src/core/checker.ts +0 -33
- package/src/core/decorators.test.ts +0 -96
- package/src/core/decorators.ts +0 -68
- package/src/core/engine.test.ts +0 -218
- package/src/core/engine.ts +0 -635
- package/src/core/errors.ts +0 -28
- package/src/core/factory.test.ts +0 -73
- package/src/core/factory.ts +0 -92
- package/src/core/logger.ts +0 -65
- package/src/core/testing.ts +0 -73
- package/src/core/types.ts +0 -191
- package/src/index.ts +0 -49
- package/src/metadata/README.md +0 -422
- package/src/metadata/metadata.test.ts +0 -369
- package/src/metadata/metadata.ts +0 -512
- package/src/plugins/action/action-plugin.test.ts +0 -660
- package/src/plugins/action/decorator.ts +0 -14
- package/src/plugins/action/index.ts +0 -4
- package/src/plugins/action/plugin.ts +0 -349
- package/src/plugins/action/types.ts +0 -49
- package/src/plugins/action/utils.test.ts +0 -196
- package/src/plugins/action/utils.ts +0 -111
- package/src/plugins/cache/adapter.test.ts +0 -689
- package/src/plugins/cache/adapter.ts +0 -324
- package/src/plugins/cache/cache-plugin.test.ts +0 -269
- package/src/plugins/cache/decorator.ts +0 -26
- package/src/plugins/cache/index.ts +0 -20
- package/src/plugins/cache/plugin.ts +0 -299
- package/src/plugins/cache/types.ts +0 -69
- package/src/plugins/client-code/client-code-plugin.test.ts +0 -511
- package/src/plugins/client-code/format.ts +0 -9
- package/src/plugins/client-code/generator.test.ts +0 -52
- package/src/plugins/client-code/generator.ts +0 -263
- package/src/plugins/client-code/index.ts +0 -15
- package/src/plugins/client-code/plugin.ts +0 -158
- package/src/plugins/client-code/types.ts +0 -52
- package/src/plugins/client-code/utils.ts +0 -164
- package/src/plugins/graceful-shutdown/graceful-shutdown-plugin.test.ts +0 -401
- package/src/plugins/graceful-shutdown/index.ts +0 -3
- package/src/plugins/graceful-shutdown/plugin.ts +0 -279
- package/src/plugins/graceful-shutdown/types.ts +0 -17
- package/src/plugins/rate-limit/rate-limit-plugin.example.ts +0 -171
- package/src/plugins/route/components/Layout.tsx +0 -42
- package/src/plugins/route/components/ServiceStatusPage.tsx +0 -141
- package/src/plugins/route/decorator.ts +0 -50
- package/src/plugins/route/index.ts +0 -16
- package/src/plugins/route/plugin.ts +0 -218
- package/src/plugins/route/route-plugin.test.ts +0 -759
- package/src/plugins/route/types.ts +0 -72
- package/src/plugins/schedule/README.md +0 -309
- package/src/plugins/schedule/decorator.ts +0 -25
- package/src/plugins/schedule/index.ts +0 -12
- package/src/plugins/schedule/mock-etcd.ts +0 -145
- package/src/plugins/schedule/plugin.ts +0 -164
- package/src/plugins/schedule/schedule-plugin.test.ts +0 -312
- package/src/plugins/schedule/scheduler.ts +0 -164
- package/src/plugins/schedule/types.ts +0 -94
- package/src/plugins/schedule/utils.test.ts +0 -163
- package/src/plugins/schedule/utils.ts +0 -41
- package/tests/integration/client.test.ts +0 -203
- package/tests/integration/dev-service.ts +0 -301
- package/tests/integration/generated/client.ts +0 -123
- package/tests/integration/start-service.ts +0 -21
- package/tsconfig.json +0 -27
- package/tsup.config.ts +0 -16
- package/vitest.config.ts +0 -19
|
@@ -1,660 +0,0 @@
|
|
|
1
|
-
import * as ejson from "ejson";
|
|
2
|
-
import { afterEach, beforeEach, describe, expect, it } from "vitest";
|
|
3
|
-
import { z } from "zod";
|
|
4
|
-
import { Testing } from "../../core/testing";
|
|
5
|
-
import { Action, ActionPlugin } from "./index";
|
|
6
|
-
|
|
7
|
-
// 用户 Schema
|
|
8
|
-
const UserSchema = z.object({
|
|
9
|
-
id: z.string(),
|
|
10
|
-
name: z.string(),
|
|
11
|
-
age: z.number().min(0).max(150),
|
|
12
|
-
});
|
|
13
|
-
|
|
14
|
-
type User = z.infer<typeof UserSchema>;
|
|
15
|
-
|
|
16
|
-
/**
|
|
17
|
-
* 解析 ejson 格式的响应
|
|
18
|
-
*/
|
|
19
|
-
async function parseEjsonResponse(response: Response): Promise<any> {
|
|
20
|
-
const text = await response.text();
|
|
21
|
-
return ejson.parse(text);
|
|
22
|
-
}
|
|
23
|
-
|
|
24
|
-
describe("ActionPlugin", () => {
|
|
25
|
-
let engine: ReturnType<typeof Testing.createTestEngine>["engine"];
|
|
26
|
-
let Module: ReturnType<typeof Testing.createTestEngine>["Module"];
|
|
27
|
-
|
|
28
|
-
beforeEach(() => {
|
|
29
|
-
const testEngine = Testing.createTestEngine({
|
|
30
|
-
plugins: [new ActionPlugin()],
|
|
31
|
-
});
|
|
32
|
-
engine = testEngine.engine;
|
|
33
|
-
Module = testEngine.Module;
|
|
34
|
-
});
|
|
35
|
-
|
|
36
|
-
afterEach(async () => {
|
|
37
|
-
console.log("afterEach in action-plugin.test.ts");
|
|
38
|
-
if (engine) {
|
|
39
|
-
await engine.stop();
|
|
40
|
-
}
|
|
41
|
-
});
|
|
42
|
-
|
|
43
|
-
it("应该能够使用Action装饰器装饰方法", async () => {
|
|
44
|
-
const users = new Map<string, User>();
|
|
45
|
-
|
|
46
|
-
@Module("user-service")
|
|
47
|
-
class UserService {
|
|
48
|
-
@Action({
|
|
49
|
-
description: "创建新用户",
|
|
50
|
-
params: [z.string(), z.number().min(0).max(150)],
|
|
51
|
-
returns: UserSchema,
|
|
52
|
-
})
|
|
53
|
-
createUser(name: string, age: number): User {
|
|
54
|
-
const id = (users.size + 1).toString();
|
|
55
|
-
const newUser = { id, name, age };
|
|
56
|
-
users.set(id, newUser);
|
|
57
|
-
return newUser;
|
|
58
|
-
}
|
|
59
|
-
}
|
|
60
|
-
|
|
61
|
-
await engine.start();
|
|
62
|
-
|
|
63
|
-
// 测试请求(新路由模式:引擎prefix + 模块名 + Handler名)
|
|
64
|
-
const response = await fetch(
|
|
65
|
-
`http://localhost:${engine.getPort()}/user-service/createUser`,
|
|
66
|
-
{
|
|
67
|
-
method: "POST",
|
|
68
|
-
headers: { "Content-Type": "application/ejson" },
|
|
69
|
-
body: ejson.stringify({ "0": "Alice", "1": 25 }),
|
|
70
|
-
}
|
|
71
|
-
);
|
|
72
|
-
|
|
73
|
-
expect(response.status).toBe(200);
|
|
74
|
-
const result = await parseEjsonResponse(response);
|
|
75
|
-
expect(result).toMatchObject({
|
|
76
|
-
success: true,
|
|
77
|
-
data: {
|
|
78
|
-
id: expect.any(String),
|
|
79
|
-
name: "Alice",
|
|
80
|
-
age: 25,
|
|
81
|
-
},
|
|
82
|
-
});
|
|
83
|
-
});
|
|
84
|
-
|
|
85
|
-
it("应该校验参数类型", async () => {
|
|
86
|
-
@Module("test-service")
|
|
87
|
-
class TestService {
|
|
88
|
-
@Action({
|
|
89
|
-
params: [z.string(), z.number()],
|
|
90
|
-
})
|
|
91
|
-
testMethod(name: string, age: number): { name: string; age: number } {
|
|
92
|
-
return { name, age };
|
|
93
|
-
}
|
|
94
|
-
}
|
|
95
|
-
|
|
96
|
-
await engine.start();
|
|
97
|
-
|
|
98
|
-
// 测试无效参数(新路由模式:引擎prefix + 模块名 + Handler名)
|
|
99
|
-
const response = await fetch(
|
|
100
|
-
`http://localhost:${engine.getPort()}/test-service/testMethod`,
|
|
101
|
-
{
|
|
102
|
-
method: "POST",
|
|
103
|
-
headers: { "Content-Type": "application/ejson" },
|
|
104
|
-
body: ejson.stringify({ "0": "Alice", "1": "invalid" }),
|
|
105
|
-
}
|
|
106
|
-
);
|
|
107
|
-
|
|
108
|
-
expect(response.status).toBe(400);
|
|
109
|
-
const error = (await parseEjsonResponse(response)) as {
|
|
110
|
-
success: boolean;
|
|
111
|
-
error: string;
|
|
112
|
-
};
|
|
113
|
-
expect(error.success).toBe(false);
|
|
114
|
-
expect(error.error).toContain("Validation failed");
|
|
115
|
-
});
|
|
116
|
-
|
|
117
|
-
it("应该支持可空参数", async () => {
|
|
118
|
-
@Module("test-service")
|
|
119
|
-
class TestService {
|
|
120
|
-
@Action({
|
|
121
|
-
params: [z.string(), z.number().optional(), z.string().optional()],
|
|
122
|
-
})
|
|
123
|
-
testMethod(
|
|
124
|
-
name: string,
|
|
125
|
-
age?: number,
|
|
126
|
-
email?: string
|
|
127
|
-
): { name: string; age?: number; email?: string } {
|
|
128
|
-
return { name, age, email };
|
|
129
|
-
}
|
|
130
|
-
}
|
|
131
|
-
|
|
132
|
-
await engine.start();
|
|
133
|
-
|
|
134
|
-
// 测试只传递第一个参数(可空参数被忽略)(新路由模式:引擎prefix + 模块名 + Handler名)
|
|
135
|
-
const response = await fetch(
|
|
136
|
-
`http://localhost:${engine.getPort()}/test-service/testMethod`,
|
|
137
|
-
{
|
|
138
|
-
method: "POST",
|
|
139
|
-
headers: { "Content-Type": "application/ejson" },
|
|
140
|
-
body: ejson.stringify({ "0": "Alice" }),
|
|
141
|
-
}
|
|
142
|
-
);
|
|
143
|
-
|
|
144
|
-
expect(response.status).toBe(200);
|
|
145
|
-
const result = (await response.json()) as {
|
|
146
|
-
success: boolean;
|
|
147
|
-
data: {
|
|
148
|
-
name: string;
|
|
149
|
-
age?: number;
|
|
150
|
-
email?: string;
|
|
151
|
-
};
|
|
152
|
-
};
|
|
153
|
-
expect(result.success).toBe(true);
|
|
154
|
-
expect(result.data.name).toBe("Alice");
|
|
155
|
-
// 可空参数可能不会出现在结果中(取决于实现)
|
|
156
|
-
expect(result.data.age).toBeUndefined();
|
|
157
|
-
expect(result.data.email).toBeUndefined();
|
|
158
|
-
});
|
|
159
|
-
|
|
160
|
-
it("应该支持参数对齐(跳过中间参数)", async () => {
|
|
161
|
-
@Module("test-service")
|
|
162
|
-
class TestService {
|
|
163
|
-
@Action({
|
|
164
|
-
params: [
|
|
165
|
-
z.string(),
|
|
166
|
-
z.number().optional(),
|
|
167
|
-
z.string().optional(),
|
|
168
|
-
z.boolean(),
|
|
169
|
-
],
|
|
170
|
-
})
|
|
171
|
-
testMethod(
|
|
172
|
-
name: string,
|
|
173
|
-
age?: number,
|
|
174
|
-
email?: string,
|
|
175
|
-
active: boolean = false
|
|
176
|
-
): { name: string; age?: number; email?: string; active: boolean } {
|
|
177
|
-
return { name, age, email, active };
|
|
178
|
-
}
|
|
179
|
-
}
|
|
180
|
-
|
|
181
|
-
await engine.start();
|
|
182
|
-
|
|
183
|
-
// 测试跳过中间参数,只传递第一个和最后一个(新路由模式:引擎prefix + 模块名 + Handler名)
|
|
184
|
-
const response = await fetch(
|
|
185
|
-
`http://localhost:${engine.getPort()}/test-service/testMethod`,
|
|
186
|
-
{
|
|
187
|
-
method: "POST",
|
|
188
|
-
headers: { "Content-Type": "application/ejson" },
|
|
189
|
-
body: ejson.stringify({ "0": "Alice", "3": true }),
|
|
190
|
-
}
|
|
191
|
-
);
|
|
192
|
-
|
|
193
|
-
expect(response.status).toBe(200);
|
|
194
|
-
const result = await parseEjsonResponse(response);
|
|
195
|
-
expect(result).toMatchObject({
|
|
196
|
-
success: true,
|
|
197
|
-
data: {
|
|
198
|
-
name: "Alice",
|
|
199
|
-
active: true,
|
|
200
|
-
},
|
|
201
|
-
});
|
|
202
|
-
});
|
|
203
|
-
|
|
204
|
-
it("应该校验返回值", async () => {
|
|
205
|
-
@Module("test-service")
|
|
206
|
-
class TestService {
|
|
207
|
-
@Action({
|
|
208
|
-
params: [z.string(), z.number()],
|
|
209
|
-
returns: UserSchema,
|
|
210
|
-
})
|
|
211
|
-
createUser(name: string, age: number): any {
|
|
212
|
-
// 返回不符合 schema 的数据
|
|
213
|
-
return { invalid: "data" };
|
|
214
|
-
}
|
|
215
|
-
}
|
|
216
|
-
|
|
217
|
-
await engine.start();
|
|
218
|
-
|
|
219
|
-
const response = await fetch(
|
|
220
|
-
`http://localhost:${engine.getPort()}/test-service/createUser`,
|
|
221
|
-
{
|
|
222
|
-
method: "POST",
|
|
223
|
-
headers: { "Content-Type": "application/ejson" },
|
|
224
|
-
body: ejson.stringify({ "0": "Alice", "1": 25 }),
|
|
225
|
-
}
|
|
226
|
-
);
|
|
227
|
-
|
|
228
|
-
expect(response.status).toBe(400);
|
|
229
|
-
const error = (await parseEjsonResponse(response)) as {
|
|
230
|
-
success: boolean;
|
|
231
|
-
error: string;
|
|
232
|
-
};
|
|
233
|
-
expect(error.success).toBe(false);
|
|
234
|
-
expect(error.error).toContain("Return value validation failed");
|
|
235
|
-
});
|
|
236
|
-
|
|
237
|
-
it("应该同时支持GET和POST方法", async () => {
|
|
238
|
-
@Module("test-service")
|
|
239
|
-
class TestService {
|
|
240
|
-
@Action({
|
|
241
|
-
params: [z.string()],
|
|
242
|
-
})
|
|
243
|
-
testMethod(name: string): { name: string } {
|
|
244
|
-
return { name };
|
|
245
|
-
}
|
|
246
|
-
}
|
|
247
|
-
|
|
248
|
-
await engine.start();
|
|
249
|
-
|
|
250
|
-
// 测试 GET 请求(从 query 参数解析)
|
|
251
|
-
const getResponse = await fetch(
|
|
252
|
-
`http://localhost:${engine.getPort()}/test-service/testMethod?0=Alice`
|
|
253
|
-
);
|
|
254
|
-
|
|
255
|
-
expect(getResponse.status).toBe(200);
|
|
256
|
-
const getResult = await parseEjsonResponse(getResponse);
|
|
257
|
-
expect(getResult).toMatchObject({
|
|
258
|
-
success: true,
|
|
259
|
-
data: { name: "Alice" },
|
|
260
|
-
});
|
|
261
|
-
|
|
262
|
-
// 测试 POST 请求(从 body 解析)
|
|
263
|
-
const postResponse = await fetch(
|
|
264
|
-
`http://localhost:${engine.getPort()}/test-service/testMethod`,
|
|
265
|
-
{
|
|
266
|
-
method: "POST",
|
|
267
|
-
headers: { "Content-Type": "application/ejson" },
|
|
268
|
-
body: ejson.stringify({ "0": "Bob" }),
|
|
269
|
-
}
|
|
270
|
-
);
|
|
271
|
-
|
|
272
|
-
expect(postResponse.status).toBe(200);
|
|
273
|
-
const postResult = await parseEjsonResponse(postResponse);
|
|
274
|
-
expect(postResult).toMatchObject({
|
|
275
|
-
success: true,
|
|
276
|
-
data: { name: "Bob" },
|
|
277
|
-
});
|
|
278
|
-
});
|
|
279
|
-
|
|
280
|
-
it("应该支持引擎prefix", async () => {
|
|
281
|
-
// 创建带prefix的引擎
|
|
282
|
-
const { engine: engineWithPrefix, Module: ModuleWithPrefix } =
|
|
283
|
-
Testing.createTestEngine({
|
|
284
|
-
plugins: [new ActionPlugin()],
|
|
285
|
-
options: {
|
|
286
|
-
name: "test-service",
|
|
287
|
-
version: "1.0.0",
|
|
288
|
-
prefix: "/api",
|
|
289
|
-
},
|
|
290
|
-
});
|
|
291
|
-
|
|
292
|
-
@ModuleWithPrefix("test-service")
|
|
293
|
-
class TestService {
|
|
294
|
-
@Action({
|
|
295
|
-
params: [z.string()],
|
|
296
|
-
})
|
|
297
|
-
testMethod(name: string): { name: string } {
|
|
298
|
-
return { name };
|
|
299
|
-
}
|
|
300
|
-
}
|
|
301
|
-
|
|
302
|
-
const port = await engineWithPrefix.start();
|
|
303
|
-
|
|
304
|
-
// GET 请求从 query 参数解析(新路由模式:引擎prefix + 模块名 + Handler名)
|
|
305
|
-
const response = await fetch(
|
|
306
|
-
`http://localhost:${port}/api/test-service/testMethod?0=Alice`
|
|
307
|
-
);
|
|
308
|
-
|
|
309
|
-
expect(response.status).toBe(200);
|
|
310
|
-
const result = await parseEjsonResponse(response);
|
|
311
|
-
expect(result).toMatchObject({
|
|
312
|
-
success: true,
|
|
313
|
-
data: { name: "Alice" },
|
|
314
|
-
});
|
|
315
|
-
});
|
|
316
|
-
|
|
317
|
-
describe("错误处理和嵌套类型", () => {
|
|
318
|
-
it("应该正确处理嵌套对象的参数验证错误", async () => {
|
|
319
|
-
const AddressSchema = z.object({
|
|
320
|
-
street: z.string(),
|
|
321
|
-
city: z.string(),
|
|
322
|
-
zipCode: z.string().regex(/^\d{5}$/),
|
|
323
|
-
});
|
|
324
|
-
|
|
325
|
-
const UserWithAddressSchema = z.object({
|
|
326
|
-
name: z.string(),
|
|
327
|
-
age: z.number(),
|
|
328
|
-
address: AddressSchema,
|
|
329
|
-
});
|
|
330
|
-
|
|
331
|
-
@Module("test-service")
|
|
332
|
-
class TestService {
|
|
333
|
-
@Action({
|
|
334
|
-
params: [UserWithAddressSchema],
|
|
335
|
-
})
|
|
336
|
-
createUserWithAddress(user: z.infer<typeof UserWithAddressSchema>) {
|
|
337
|
-
return user;
|
|
338
|
-
}
|
|
339
|
-
}
|
|
340
|
-
|
|
341
|
-
await engine.start();
|
|
342
|
-
|
|
343
|
-
// 测试嵌套对象中缺少必需字段
|
|
344
|
-
const response1 = await fetch(
|
|
345
|
-
`http://localhost:${engine.getPort()}/test-service/createUserWithAddress`,
|
|
346
|
-
{
|
|
347
|
-
method: "POST",
|
|
348
|
-
headers: { "Content-Type": "application/ejson" },
|
|
349
|
-
body: ejson.stringify({
|
|
350
|
-
"0": {
|
|
351
|
-
name: "Alice",
|
|
352
|
-
age: 25,
|
|
353
|
-
address: {
|
|
354
|
-
street: "123 Main St",
|
|
355
|
-
// 缺少 city 和 zipCode
|
|
356
|
-
},
|
|
357
|
-
},
|
|
358
|
-
}),
|
|
359
|
-
}
|
|
360
|
-
);
|
|
361
|
-
|
|
362
|
-
expect(response1.status).toBe(400);
|
|
363
|
-
const error1 = (await response1.json()) as {
|
|
364
|
-
success: boolean;
|
|
365
|
-
error: string;
|
|
366
|
-
};
|
|
367
|
-
expect(error1.success).toBe(false);
|
|
368
|
-
expect(error1.error).toContain("Validation failed");
|
|
369
|
-
expect(error1.error).toContain("参数[0]");
|
|
370
|
-
expect(error1.error).toContain("address.city");
|
|
371
|
-
expect(error1.error).toContain("address.zipCode");
|
|
372
|
-
|
|
373
|
-
// 测试嵌套对象中类型错误
|
|
374
|
-
const response2 = await fetch(
|
|
375
|
-
`http://localhost:${engine.getPort()}/test-service/createUserWithAddress`,
|
|
376
|
-
{
|
|
377
|
-
method: "POST",
|
|
378
|
-
headers: { "Content-Type": "application/ejson" },
|
|
379
|
-
body: ejson.stringify({
|
|
380
|
-
"0": {
|
|
381
|
-
name: "Alice",
|
|
382
|
-
age: "invalid", // 类型错误
|
|
383
|
-
address: {
|
|
384
|
-
street: "123 Main St",
|
|
385
|
-
city: "New York",
|
|
386
|
-
zipCode: "12345",
|
|
387
|
-
},
|
|
388
|
-
},
|
|
389
|
-
}),
|
|
390
|
-
}
|
|
391
|
-
);
|
|
392
|
-
|
|
393
|
-
expect(response2.status).toBe(400);
|
|
394
|
-
const error2 = (await response2.json()) as {
|
|
395
|
-
success: boolean;
|
|
396
|
-
error: string;
|
|
397
|
-
};
|
|
398
|
-
expect(error2.success).toBe(false);
|
|
399
|
-
expect(error2.error).toContain("参数[0].age");
|
|
400
|
-
});
|
|
401
|
-
|
|
402
|
-
it("应该正确处理嵌套数组的参数验证错误", async () => {
|
|
403
|
-
const ItemSchema = z.object({
|
|
404
|
-
id: z.string(),
|
|
405
|
-
quantity: z.number().min(1),
|
|
406
|
-
price: z.number().positive(),
|
|
407
|
-
});
|
|
408
|
-
|
|
409
|
-
const OrderSchema = z.object({
|
|
410
|
-
orderId: z.string(),
|
|
411
|
-
items: z.array(ItemSchema),
|
|
412
|
-
total: z.number(),
|
|
413
|
-
});
|
|
414
|
-
|
|
415
|
-
@Module("test-service")
|
|
416
|
-
class TestService {
|
|
417
|
-
@Action({
|
|
418
|
-
params: [OrderSchema],
|
|
419
|
-
})
|
|
420
|
-
createOrder(order: z.infer<typeof OrderSchema>) {
|
|
421
|
-
return order;
|
|
422
|
-
}
|
|
423
|
-
}
|
|
424
|
-
|
|
425
|
-
const port = await engine.start();
|
|
426
|
-
|
|
427
|
-
// 测试嵌套数组中对象的验证错误
|
|
428
|
-
const response = await fetch(
|
|
429
|
-
`http://localhost:${port}/test-service/createOrder`,
|
|
430
|
-
{
|
|
431
|
-
method: "POST",
|
|
432
|
-
headers: { "Content-Type": "application/ejson" },
|
|
433
|
-
body: ejson.stringify({
|
|
434
|
-
"0": {
|
|
435
|
-
orderId: "ORD-001",
|
|
436
|
-
items: [
|
|
437
|
-
{
|
|
438
|
-
id: "ITEM-1",
|
|
439
|
-
quantity: -1, // 无效:应该 >= 1
|
|
440
|
-
price: 10.5,
|
|
441
|
-
},
|
|
442
|
-
{
|
|
443
|
-
id: "ITEM-2",
|
|
444
|
-
quantity: 2,
|
|
445
|
-
price: -5, // 无效:应该是正数
|
|
446
|
-
},
|
|
447
|
-
],
|
|
448
|
-
total: 100,
|
|
449
|
-
},
|
|
450
|
-
}),
|
|
451
|
-
}
|
|
452
|
-
);
|
|
453
|
-
|
|
454
|
-
expect(response.status).toBe(400);
|
|
455
|
-
const error = (await parseEjsonResponse(response)) as {
|
|
456
|
-
success: boolean;
|
|
457
|
-
error: string;
|
|
458
|
-
};
|
|
459
|
-
expect(error.success).toBe(false);
|
|
460
|
-
expect(error.error).toContain("Validation failed");
|
|
461
|
-
expect(error.error).toContain("参数[0]");
|
|
462
|
-
// 应该包含嵌套路径(items数组中的错误)
|
|
463
|
-
expect(error.error).toMatch(/items|quantity|price/);
|
|
464
|
-
});
|
|
465
|
-
|
|
466
|
-
it("应该正确处理深层嵌套对象的错误", async () => {
|
|
467
|
-
const CompanySchema = z.object({
|
|
468
|
-
name: z.string(),
|
|
469
|
-
employees: z.array(
|
|
470
|
-
z.object({
|
|
471
|
-
id: z.string(),
|
|
472
|
-
name: z.string(),
|
|
473
|
-
department: z.object({
|
|
474
|
-
name: z.string(),
|
|
475
|
-
location: z.object({
|
|
476
|
-
city: z.string(),
|
|
477
|
-
country: z.string(),
|
|
478
|
-
}),
|
|
479
|
-
}),
|
|
480
|
-
})
|
|
481
|
-
),
|
|
482
|
-
});
|
|
483
|
-
|
|
484
|
-
@Module("test-service")
|
|
485
|
-
class TestService {
|
|
486
|
-
@Action({
|
|
487
|
-
params: [CompanySchema],
|
|
488
|
-
})
|
|
489
|
-
createCompany(company: z.infer<typeof CompanySchema>) {
|
|
490
|
-
return company;
|
|
491
|
-
}
|
|
492
|
-
}
|
|
493
|
-
|
|
494
|
-
const port = await engine.start();
|
|
495
|
-
|
|
496
|
-
// 测试深层嵌套错误 - 确保对象存在,但嵌套属性有类型错误
|
|
497
|
-
const response = await fetch(
|
|
498
|
-
`http://localhost:${port}/test-service/createCompany`,
|
|
499
|
-
{
|
|
500
|
-
method: "POST",
|
|
501
|
-
headers: { "Content-Type": "application/ejson" },
|
|
502
|
-
body: ejson.stringify({
|
|
503
|
-
"0": {
|
|
504
|
-
name: "Acme Corp",
|
|
505
|
-
employees: [
|
|
506
|
-
{
|
|
507
|
-
id: "EMP-1",
|
|
508
|
-
name: "John",
|
|
509
|
-
department: {
|
|
510
|
-
name: "Engineering",
|
|
511
|
-
location: {
|
|
512
|
-
city: "New York",
|
|
513
|
-
country: 123, // 类型错误:应该是字符串
|
|
514
|
-
},
|
|
515
|
-
},
|
|
516
|
-
},
|
|
517
|
-
],
|
|
518
|
-
},
|
|
519
|
-
}),
|
|
520
|
-
}
|
|
521
|
-
);
|
|
522
|
-
|
|
523
|
-
expect(response.status).toBe(400);
|
|
524
|
-
const error = (await parseEjsonResponse(response)) as {
|
|
525
|
-
success: boolean;
|
|
526
|
-
error: string;
|
|
527
|
-
};
|
|
528
|
-
expect(error.success).toBe(false);
|
|
529
|
-
expect(error.error).toContain("Validation failed");
|
|
530
|
-
expect(error.error).toContain("参数[0]");
|
|
531
|
-
// 应该显示深层嵌套路径
|
|
532
|
-
expect(error.error).toMatch(/employees|department|location|country/);
|
|
533
|
-
});
|
|
534
|
-
|
|
535
|
-
it("应该正确处理返回值嵌套对象的验证错误", async () => {
|
|
536
|
-
const ProductSchema = z.object({
|
|
537
|
-
id: z.string(),
|
|
538
|
-
name: z.string(),
|
|
539
|
-
price: z.number().positive(),
|
|
540
|
-
category: z.object({
|
|
541
|
-
id: z.string(),
|
|
542
|
-
name: z.string(),
|
|
543
|
-
}),
|
|
544
|
-
});
|
|
545
|
-
|
|
546
|
-
@Module("test-service")
|
|
547
|
-
class TestService {
|
|
548
|
-
@Action({
|
|
549
|
-
params: [z.string()],
|
|
550
|
-
returns: ProductSchema,
|
|
551
|
-
})
|
|
552
|
-
getProduct(id: string): any {
|
|
553
|
-
// 返回不符合 schema 的嵌套数据
|
|
554
|
-
return {
|
|
555
|
-
id: "PROD-1",
|
|
556
|
-
name: "Test Product",
|
|
557
|
-
price: 100,
|
|
558
|
-
category: {
|
|
559
|
-
id: "CAT-1",
|
|
560
|
-
// 缺少 name
|
|
561
|
-
},
|
|
562
|
-
};
|
|
563
|
-
}
|
|
564
|
-
}
|
|
565
|
-
|
|
566
|
-
const port = await engine.start();
|
|
567
|
-
|
|
568
|
-
const response = await fetch(
|
|
569
|
-
`http://localhost:${port}/test-service/getProduct`,
|
|
570
|
-
{
|
|
571
|
-
method: "POST",
|
|
572
|
-
headers: { "Content-Type": "application/ejson" },
|
|
573
|
-
body: ejson.stringify({ "0": "PROD-1" }),
|
|
574
|
-
}
|
|
575
|
-
);
|
|
576
|
-
|
|
577
|
-
expect(response.status).toBe(400);
|
|
578
|
-
const error = (await parseEjsonResponse(response)) as {
|
|
579
|
-
success: boolean;
|
|
580
|
-
error: string;
|
|
581
|
-
};
|
|
582
|
-
expect(error.success).toBe(false);
|
|
583
|
-
expect(error.error).toContain("Return value validation failed");
|
|
584
|
-
expect(error.error).toContain("category.name");
|
|
585
|
-
});
|
|
586
|
-
|
|
587
|
-
it("应该正确处理多个参数的嵌套错误", async () => {
|
|
588
|
-
const UserSchema = z.object({
|
|
589
|
-
name: z.string(),
|
|
590
|
-
profile: z.object({
|
|
591
|
-
email: z.string().email(),
|
|
592
|
-
age: z.number(),
|
|
593
|
-
}),
|
|
594
|
-
});
|
|
595
|
-
|
|
596
|
-
const OrderSchema = z.object({
|
|
597
|
-
orderId: z.string(),
|
|
598
|
-
items: z.array(
|
|
599
|
-
z.object({
|
|
600
|
-
productId: z.string(),
|
|
601
|
-
quantity: z.number(),
|
|
602
|
-
})
|
|
603
|
-
),
|
|
604
|
-
});
|
|
605
|
-
|
|
606
|
-
@Module("test-service")
|
|
607
|
-
class TestService {
|
|
608
|
-
@Action({
|
|
609
|
-
params: [UserSchema, OrderSchema],
|
|
610
|
-
})
|
|
611
|
-
processOrder(
|
|
612
|
-
user: z.infer<typeof UserSchema>,
|
|
613
|
-
order: z.infer<typeof OrderSchema>
|
|
614
|
-
) {
|
|
615
|
-
return { user, order };
|
|
616
|
-
}
|
|
617
|
-
}
|
|
618
|
-
|
|
619
|
-
const port = await engine.start();
|
|
620
|
-
|
|
621
|
-
// 测试多个参数都有嵌套错误
|
|
622
|
-
const response = await fetch(
|
|
623
|
-
`http://localhost:${port}/test-service/processOrder`,
|
|
624
|
-
{
|
|
625
|
-
method: "POST",
|
|
626
|
-
headers: { "Content-Type": "application/ejson" },
|
|
627
|
-
body: ejson.stringify({
|
|
628
|
-
"0": {
|
|
629
|
-
name: "Alice",
|
|
630
|
-
profile: {
|
|
631
|
-
email: "invalid-email", // 无效邮箱
|
|
632
|
-
age: 25,
|
|
633
|
-
},
|
|
634
|
-
},
|
|
635
|
-
"1": {
|
|
636
|
-
orderId: "ORD-001",
|
|
637
|
-
items: [
|
|
638
|
-
{
|
|
639
|
-
productId: "PROD-1",
|
|
640
|
-
quantity: "invalid", // 类型错误
|
|
641
|
-
},
|
|
642
|
-
],
|
|
643
|
-
},
|
|
644
|
-
}),
|
|
645
|
-
}
|
|
646
|
-
);
|
|
647
|
-
|
|
648
|
-
expect(response.status).toBe(400);
|
|
649
|
-
const error = (await parseEjsonResponse(response)) as {
|
|
650
|
-
success: boolean;
|
|
651
|
-
error: string;
|
|
652
|
-
};
|
|
653
|
-
expect(error.success).toBe(false);
|
|
654
|
-
expect(error.error).toContain("Validation failed");
|
|
655
|
-
// 应该包含两个参数的错误
|
|
656
|
-
expect(error.error).toContain("参数[0]");
|
|
657
|
-
expect(error.error).toContain("参数[1]");
|
|
658
|
-
});
|
|
659
|
-
});
|
|
660
|
-
});
|
|
@@ -1,14 +0,0 @@
|
|
|
1
|
-
import { Handler } from "../../core/decorators";
|
|
2
|
-
import { ActionOptions } from "./types";
|
|
3
|
-
|
|
4
|
-
/**
|
|
5
|
-
* Action装饰器(Handler的语法糖封装)
|
|
6
|
-
* 固定type="action",用于标注动作处理方法
|
|
7
|
-
*/
|
|
8
|
-
export function Action(options: ActionOptions) {
|
|
9
|
-
return Handler({
|
|
10
|
-
type: "action",
|
|
11
|
-
options,
|
|
12
|
-
});
|
|
13
|
-
}
|
|
14
|
-
|