@dtdyq/restbase 1.0.0 → 2.0.0

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.
@@ -0,0 +1,1465 @@
1
+ /**
2
+ * test.ts — RestBase 全面集成测试
3
+ *
4
+ * 运行: bun test test.ts
5
+ *
6
+ * bun test 自动设置 NODE_ENV=test 并加载 .env.test:
7
+ * DB=sqlite://:memory: → 每次全新内存库
8
+ * DB_INIT_SQL=init.sql → 自动执行种子 SQL
9
+ * SVR_PORT=13333 → 避免与开发服务端口冲突
10
+ * LOG_CONSOLE=false → 测试时不刷日志
11
+ *
12
+ * 服务器在 import 时启动,测试结束进程退出,内存库自动释放。
13
+ *
14
+ * 覆盖范围:
15
+ * Part 0: 元数据接口 (/api/meta/tables)
16
+ * Part 1: Client API(Auth / CRUD / QueryBuilder / DeleteBuilder)
17
+ * Part 2: 原始 HTTP(GET / DELETE URL 查询参数全场景)
18
+ */
19
+
20
+ /* 启动服务(顶层 await,initDb + Bun.serve 全部完成后继续) */
21
+ import {server} from "./server.ts";
22
+
23
+ import {afterAll, beforeAll, describe, expect, test} from "bun:test";
24
+ import {cfg} from "./types.ts";
25
+ import RestBase, {
26
+ agg,
27
+ and,
28
+ type ApiResponse,
29
+ between,
30
+ eq,
31
+ ge,
32
+ gt,
33
+ isIn,
34
+ isNotNull,
35
+ isNull,
36
+ le,
37
+ like,
38
+ lt,
39
+ ne,
40
+ nlike,
41
+ notIn,
42
+ or,
43
+ sel,
44
+ } from "../client/restbase-client.ts";
45
+
46
+ const BASE = `http://localhost:${cfg.port}`;
47
+
48
+ /* 测试全部完成后关闭服务器,释放端口并退出进程 */
49
+ afterAll(() => {
50
+ server.stop(true);
51
+ });
52
+
53
+ /* ═══════════════════════════════════════════════════════════════
54
+ 工具函数
55
+ ═══════════════════════════════════════════════════════════════ */
56
+
57
+ /** 原始 HTTP 请求 */
58
+ async function raw(
59
+ method: string,
60
+ path: string,
61
+ opts?: { auth?: string; body?: unknown },
62
+ ): Promise<ApiResponse> {
63
+ const headers: Record<string, string> = {"Content-Type": "application/json"};
64
+ if (opts?.auth) headers["Authorization"] = opts.auth;
65
+ const init: RequestInit = {method, headers};
66
+ if (opts?.body !== undefined) init.body = JSON.stringify(opts.body);
67
+ const res = await fetch(`${BASE}${path}`, init);
68
+ return res.json() as Promise<ApiResponse>;
69
+ }
70
+
71
+ /** Basic Auth header */
72
+ const basic = (u: string, p: string) =>
73
+ `Basic ${Buffer.from(`${u}:${p}`).toString("base64")}`;
74
+
75
+ const AUTH = basic("admin", "admin");
76
+
77
+ /* ═══════════════════════════════════════════════════════════════
78
+ Part 0: 元数据接口
79
+ ═══════════════════════════════════════════════════════════════ */
80
+
81
+ describe("Meta API", () => {
82
+
83
+ test("GET /api/meta/tables — 获取表元数据", async () => {
84
+ const res = await raw("GET", "/api/meta/tables", {auth: AUTH});
85
+ expect(res.code).toBe("OK");
86
+ const tables = res.data as any[];
87
+ expect(Array.isArray(tables)).toBe(true);
88
+ expect(tables.length).toBeGreaterThanOrEqual(2); // products + logs
89
+
90
+ /* 不应包含 users 表 */
91
+ const names = tables.map((t: any) => t.name);
92
+ expect(names).not.toContain("users");
93
+ expect(names).toContain("products");
94
+ expect(names).toContain("logs");
95
+ });
96
+
97
+ test("products 元数据结构", async () => {
98
+ const res = await raw("GET", "/api/meta/tables", {auth: AUTH});
99
+ const tables = res.data as any[];
100
+ const products = tables.find((t: any) => t.name === "products");
101
+ expect(products).toBeDefined();
102
+ expect(products.pk).toBe("id");
103
+ expect(products.hasOwner).toBe(true);
104
+ expect(products.columns.length).toBeGreaterThan(0);
105
+
106
+ const colNames = products.columns.map((c: any) => c.name);
107
+ expect(colNames).toContain("id");
108
+ expect(colNames).toContain("name");
109
+ expect(colNames).toContain("price");
110
+ expect(colNames).toContain("owner");
111
+
112
+ /* 列类型信息 */
113
+ const idCol = products.columns.find((c: any) => c.name === "id");
114
+ expect(idCol.type).toBe("integer");
115
+ expect(idCol.isNumeric).toBe(true);
116
+
117
+ const nameCol = products.columns.find((c: any) => c.name === "name");
118
+ expect(nameCol.type).toBe("text");
119
+ expect(nameCol.isNumeric).toBe(false);
120
+ });
121
+
122
+ test("logs 元数据结构(无 owner)", async () => {
123
+ const res = await raw("GET", "/api/meta/tables", {auth: AUTH});
124
+ const tables = res.data as any[];
125
+ const logs = tables.find((t: any) => t.name === "logs");
126
+ expect(logs).toBeDefined();
127
+ expect(logs.pk).toBe("id");
128
+ expect(logs.hasOwner).toBe(false);
129
+ });
130
+
131
+ test("GET /api/meta/tables/:name — 获取单表元数据", async () => {
132
+ const res = await raw("GET", "/api/meta/tables/products", {auth: AUTH});
133
+ expect(res.code).toBe("OK");
134
+ const tbl = res.data as any;
135
+ expect(tbl).not.toBeNull();
136
+ expect(tbl.name).toBe("products");
137
+ expect(tbl.pk).toBe("id");
138
+ expect(tbl.hasOwner).toBe(true);
139
+ expect(tbl.columns.length).toBeGreaterThan(0);
140
+ });
141
+
142
+ test("GET /api/meta/tables/:name — 表不存在返回 null", async () => {
143
+ const res = await raw("GET", "/api/meta/tables/nonexistent", {auth: AUTH});
144
+ expect(res.code).toBe("OK");
145
+ expect(res.data).toBeNull();
146
+ });
147
+
148
+ test("GET /api/meta/tables/:name — users 表不暴露", async () => {
149
+ const res = await raw("GET", "/api/meta/tables/users", {auth: AUTH});
150
+ expect(res.code).toBe("OK");
151
+ expect(res.data).toBeNull();
152
+ });
153
+
154
+ test("GET /api/meta/sync — 同步表结构", async () => {
155
+ const res = await raw("GET", "/api/meta/sync", {auth: AUTH});
156
+ expect(res.code).toBe("OK");
157
+ const tables = res.data as any[];
158
+ expect(Array.isArray(tables)).toBe(true);
159
+ const names = tables.map((t: any) => t.name);
160
+ expect(names).toContain("products");
161
+ expect(names).toContain("logs");
162
+ expect(names).not.toContain("users");
163
+ });
164
+
165
+ test("GET /api/meta/sync — 能发现运行时新建的表", async () => {
166
+ await raw("GET", "/api/meta/sync", {auth: AUTH}); // 先 sync 确认初始状态
167
+
168
+ /* 验证 sync 调用后返回的表列表与 GET /api/meta/tables 一致 */
169
+ const syncRes = await raw("GET", "/api/meta/sync", {auth: AUTH});
170
+ const getRes = await raw("GET", "/api/meta/tables", {auth: AUTH});
171
+ expect(syncRes.code).toBe("OK");
172
+ expect(getRes.code).toBe("OK");
173
+
174
+ const syncNames = (syncRes.data as any[]).map((t: any) => t.name).sort();
175
+ const getNames = (getRes.data as any[]).map((t: any) => t.name).sort();
176
+ expect(syncNames).toEqual(getNames);
177
+ });
178
+
179
+ test("未鉴权访问 meta 应失败", async () => {
180
+ const res = await raw("GET", "/api/meta/tables");
181
+ expect(res.code).toBe("AUTH_ERROR");
182
+ });
183
+
184
+ test("未鉴权访问 meta/sync 应失败", async () => {
185
+ const res = await raw("GET", "/api/meta/sync");
186
+ expect(res.code).toBe("AUTH_ERROR");
187
+ });
188
+ });
189
+
190
+ /* ═══════════════════════════════════════════════════════════════
191
+ Part 1: Client API 测试
192
+ ═══════════════════════════════════════════════════════════════ */
193
+
194
+ describe("Client API", () => {
195
+ const rb = new RestBase(BASE);
196
+ let token: string;
197
+
198
+ /* ── 1.1 健康检查 ── */
199
+ test("health check", async () => {
200
+ const res = await rb.health();
201
+ expect(res.code).toBe("OK");
202
+ expect((res.data as any).status).toBe("healthy");
203
+ });
204
+
205
+ /* ── 1.2 注册 ── */
206
+ test("auth register", async () => {
207
+ const res = await rb.auth.register("testuser", "testpass");
208
+ expect(res.code).toBe("OK");
209
+ expect(typeof res.data).toBe("string");
210
+ expect(res.data!.length).toBeGreaterThan(0);
211
+ token = res.data as string;
212
+ });
213
+
214
+ /* ── 1.3 重复注册失败 ── */
215
+ test("auth register duplicate fails", async () => {
216
+ const res = await rb.auth.register("testuser", "testpass");
217
+ expect(res.code).toBe("AUTH_ERROR");
218
+ });
219
+
220
+ /* ── 1.4 登录 ── */
221
+ test("auth login", async () => {
222
+ const res = await rb.auth.login("testuser", "testpass");
223
+ expect(res.code).toBe("OK");
224
+ expect(typeof res.data).toBe("string");
225
+ token = res.data as string;
226
+ });
227
+
228
+ /* ── 1.5 登录错误密码 ── */
229
+ test("auth login wrong password", async () => {
230
+ const rb2 = new RestBase(BASE);
231
+ const res = await rb2.auth.login("testuser", "wrongpass");
232
+ expect(res.code).toBe("AUTH_ERROR");
233
+ });
234
+
235
+ /* ── 1.6 获取用户资料 ── */
236
+ test("auth get profile", async () => {
237
+ const res = await rb.auth.getProfile();
238
+ expect(res.code).toBe("OK");
239
+ expect((res.data as any).username).toBe("testuser");
240
+ });
241
+
242
+ /* ── 1.7 更新用户资料 ── */
243
+ test("auth update profile", async () => {
244
+ const res = await rb.auth.updateProfile({password: "newpass"});
245
+ expect(res.code).toBe("OK");
246
+
247
+ const rb2 = new RestBase(BASE);
248
+ const login = await rb2.auth.login("testuser", "newpass");
249
+ expect(login.code).toBe("OK");
250
+ });
251
+
252
+ /* ── 1.8 Basic Auth ── */
253
+ test("auth basic auth", async () => {
254
+ const rb3 = new RestBase(BASE);
255
+ rb3.auth.useBasicAuth("admin", "admin");
256
+ const res = await rb3.auth.getProfile();
257
+ expect(res.code).toBe("OK");
258
+ expect((res.data as any).username).toBe("admin");
259
+ });
260
+
261
+ /* ── 1.9 Token 管理 ── */
262
+ test("auth token management", () => {
263
+ expect(rb.auth.getToken()).toBeTruthy();
264
+ rb.auth.logout();
265
+ expect(rb.auth.getToken()).toBeNull();
266
+ rb.auth.setToken(token);
267
+ expect(rb.auth.getToken()).toBe(token);
268
+ });
269
+
270
+ /* ── 1.10 未鉴权访问 ── */
271
+ test("auth unauthorized access", async () => {
272
+ const rb4 = new RestBase(BASE);
273
+ const res = await rb4.table("products").getByPk(1);
274
+ expect(res.code).toBe("AUTH_ERROR");
275
+ });
276
+
277
+ /* ────────────── CRUD(用 admin 的 Basic Auth) ────────────── */
278
+
279
+ describe("CRUD (products)", () => {
280
+ const rbAdmin = new RestBase(BASE);
281
+ beforeAll(() => {
282
+ rbAdmin.auth.useBasicAuth("admin", "admin");
283
+ });
284
+ const products = () => rbAdmin.table("products");
285
+
286
+ test("create single record", async () => {
287
+ const res = await products().create({name: "TestItem", price: 42.0, stock: 10, category: "Test"});
288
+ expect(res.code).toBe("OK");
289
+ expect((res.data as any).created).toBeInstanceOf(Array);
290
+ expect((res.data as any).created.length).toBe(1);
291
+ });
292
+
293
+ test("create batch records", async () => {
294
+ const res = await products().create([
295
+ {name: "BatchA", price: 10.0, stock: 5, category: "Batch"},
296
+ {name: "BatchB", price: 20.0, stock: 8, category: "Batch"},
297
+ ]);
298
+ expect(res.code).toBe("OK");
299
+ expect((res.data as any).created.length).toBe(2);
300
+ });
301
+
302
+ test("getByPk", async () => {
303
+ const res = await products().getByPk(1);
304
+ expect(res.code).toBe("OK");
305
+ expect(res.data).not.toBeNull();
306
+ expect((res.data as any).id).toBe(1);
307
+ });
308
+
309
+ test("getByPk not found", async () => {
310
+ const res = await products().getByPk(99999);
311
+ expect(res.code).toBe("OK");
312
+ expect(res.data).toBeNull();
313
+ });
314
+
315
+ test("put update existing", async () => {
316
+ const res = await products().put({id: 1, price: 888.88});
317
+ expect(res.code).toBe("OK");
318
+ expect((res.data as any).updated).toContain(1);
319
+ expect((res.data as any).created.length).toBe(0);
320
+ const check = await products().getByPk(1);
321
+ expect((check.data as any).price).toBe(888.88);
322
+ });
323
+
324
+ test("put create new", async () => {
325
+ const res = await products().put({name: "PutNew", price: 77.77, stock: 3, category: "Put"});
326
+ expect(res.code).toBe("OK");
327
+ expect((res.data as any).created.length).toBe(1);
328
+ const newId = (res.data as any).created[0];
329
+ expect(typeof newId).toBe("number");
330
+ const check = await products().getByPk(newId);
331
+ expect((check.data as any).name).toBe("PutNew");
332
+ });
333
+
334
+ test("deleteByPk", async () => {
335
+ const cr = await products().create({name: "ToDelete", price: 1, stock: 0, category: "Del"});
336
+ const id = (cr.data as any).created[0];
337
+ const res = await products().deleteByPk(id);
338
+ expect(res.code).toBe("OK");
339
+ expect((res.data as any).deleted).toContain(id);
340
+ const check = await products().getByPk(id);
341
+ expect(check.data).toBeNull();
342
+ });
343
+ });
344
+
345
+ /* ────────────── CRUD(logs 表 — 无 owner) ────────────── */
346
+
347
+ describe("CRUD (logs, no owner)", () => {
348
+ const rbAdmin = new RestBase(BASE);
349
+ beforeAll(() => {
350
+ rbAdmin.auth.useBasicAuth("admin", "admin");
351
+ });
352
+ const logs = () => rbAdmin.table("logs");
353
+
354
+ test("getByPk on logs", async () => {
355
+ const res = await logs().getByPk(1);
356
+ expect(res.code).toBe("OK");
357
+ expect(res.data).not.toBeNull();
358
+ });
359
+
360
+ test("create log record", async () => {
361
+ const res = await logs().create({level: "TEST", module: "test", message: "hello"});
362
+ expect(res.code).toBe("OK");
363
+ });
364
+ });
365
+
366
+ /* ────────────── QueryBuilder 测试 ────────────── */
367
+
368
+ describe("QueryBuilder (products)", () => {
369
+ const rbAdmin = new RestBase(BASE);
370
+ beforeAll(() => {
371
+ rbAdmin.auth.useBasicAuth("admin", "admin");
372
+ });
373
+ const products = () => rbAdmin.table("products");
374
+
375
+ test("query all", async () => {
376
+ const res = await products().query().exec();
377
+ expect(res.code).toBe("OK");
378
+ expect((res.data as any[]).length).toBeGreaterThan(0);
379
+ });
380
+
381
+ test("where eq", async () => {
382
+ const res = await products().query().where(eq("is_active", 1)).exec();
383
+ expect(res.code).toBe("OK");
384
+ for (const row of res.data as any[]) expect(row.is_active).toBe(1);
385
+ });
386
+
387
+ test("where ne", async () => {
388
+ const res = await products().query().where(ne("category", "Books")).exec();
389
+ expect(res.code).toBe("OK");
390
+ for (const row of res.data as any[]) expect(row.category).not.toBe("Books");
391
+ });
392
+
393
+ test("where gt", async () => {
394
+ const res = await products().query().where(gt("price", 500)).exec();
395
+ expect(res.code).toBe("OK");
396
+ expect((res.data as any[]).length).toBeGreaterThan(0);
397
+ for (const row of res.data as any[]) expect(row.price).toBeGreaterThan(500);
398
+ });
399
+
400
+ test("where ge", async () => {
401
+ const res = await products().query().where(ge("stock", 200)).exec();
402
+ expect(res.code).toBe("OK");
403
+ for (const row of res.data as any[]) expect(row.stock).toBeGreaterThanOrEqual(200);
404
+ });
405
+
406
+ test("where lt", async () => {
407
+ const res = await products().query().where(lt("price", 50)).exec();
408
+ expect(res.code).toBe("OK");
409
+ for (const row of res.data as any[]) expect(row.price).toBeLessThan(50);
410
+ });
411
+
412
+ test("where le", async () => {
413
+ const res = await products().query().where(le("rating", 2)).exec();
414
+ expect(res.code).toBe("OK");
415
+ for (const row of res.data as any[]) expect(row.rating).toBeLessThanOrEqual(2);
416
+ });
417
+
418
+ test("where like", async () => {
419
+ const res = await products().query().where(like("name", "%Pro%")).exec();
420
+ expect(res.code).toBe("OK");
421
+ expect((res.data as any[]).length).toBeGreaterThan(0);
422
+ for (const row of res.data as any[]) expect(row.name.toLowerCase()).toContain("pro");
423
+ });
424
+
425
+ test("where nlike", async () => {
426
+ const res = await products().query().where(nlike("name", "%Pro%")).exec();
427
+ expect(res.code).toBe("OK");
428
+ for (const row of res.data as any[]) expect(row.name.toLowerCase().includes("pro")).toBe(false);
429
+ });
430
+
431
+ test("where isNull", async () => {
432
+ const res = await products().query().where(isNull("tags")).exec();
433
+ expect(res.code).toBe("OK");
434
+ /* init.sql 中有 6 条 tags=NULL 的产品 */
435
+ expect((res.data as any[]).length).toBeGreaterThanOrEqual(5);
436
+ });
437
+
438
+ test("where isNotNull", async () => {
439
+ const res = await products().query().where(isNotNull("name")).exec();
440
+ expect(res.code).toBe("OK");
441
+ expect((res.data as any[]).length).toBeGreaterThan(0);
442
+ });
443
+
444
+ test("where in", async () => {
445
+ const res = await products().query().where(isIn("category", ["Books", "Toys"])).exec();
446
+ expect(res.code).toBe("OK");
447
+ for (const row of res.data as any[]) expect(["Books", "Toys"]).toContain(row.category);
448
+ });
449
+
450
+ test("where notIn", async () => {
451
+ const res = await products().query().where(notIn("category", ["Books", "Toys"])).exec();
452
+ expect(res.code).toBe("OK");
453
+ for (const row of res.data as any[]) expect(["Books", "Toys"]).not.toContain(row.category);
454
+ });
455
+
456
+ test("where between", async () => {
457
+ const res = await products().query().where(between("price", 100, 300)).exec();
458
+ expect(res.code).toBe("OK");
459
+ expect((res.data as any[]).length).toBeGreaterThan(0);
460
+ for (const row of res.data as any[]) {
461
+ expect(row.price).toBeGreaterThanOrEqual(100);
462
+ expect(row.price).toBeLessThanOrEqual(300);
463
+ }
464
+ });
465
+
466
+ test("where and", async () => {
467
+ const res = await products().query()
468
+ .where(and(gt("price", 100), eq("is_active", 1)))
469
+ .exec();
470
+ expect(res.code).toBe("OK");
471
+ for (const row of res.data as any[]) {
472
+ expect(row.price).toBeGreaterThan(100);
473
+ expect(row.is_active).toBe(1);
474
+ }
475
+ });
476
+
477
+ test("where or", async () => {
478
+ const res = await products().query()
479
+ .where(or(eq("category", "Books"), eq("category", "Toys")))
480
+ .exec();
481
+ expect(res.code).toBe("OK");
482
+ for (const row of res.data as any[]) expect(["Books", "Toys"]).toContain(row.category);
483
+ });
484
+
485
+ test("where nested and/or", async () => {
486
+ const res = await products().query()
487
+ .where(and(
488
+ gt("price", 50),
489
+ or(eq("category", "Electronics"), eq("category", "Sports")),
490
+ ))
491
+ .exec();
492
+ expect(res.code).toBe("OK");
493
+ for (const row of res.data as any[]) {
494
+ expect(row.price).toBeGreaterThan(50);
495
+ expect(["Electronics", "Sports"]).toContain(row.category);
496
+ }
497
+ });
498
+
499
+ test("multiple where calls (AND)", async () => {
500
+ const res = await products().query()
501
+ .where(gt("price", 100))
502
+ .where(eq("is_active", 1))
503
+ .exec();
504
+ expect(res.code).toBe("OK");
505
+ for (const row of res.data as any[]) {
506
+ expect(row.price).toBeGreaterThan(100);
507
+ expect(row.is_active).toBe(1);
508
+ }
509
+ });
510
+
511
+ test("select specific fields", async () => {
512
+ const res = await products().query().select("name", "price").page(1, 5).exec();
513
+ expect(res.code).toBe("OK");
514
+ const first = (res.data as any[])[0];
515
+ expect(first).toHaveProperty("name");
516
+ expect(first).toHaveProperty("price");
517
+ });
518
+
519
+ test("select with alias", async () => {
520
+ const res = await products().query().select(sel("price", "unitPrice")).page(1, 5).exec();
521
+ expect(res.code).toBe("OK");
522
+ expect((res.data as any[])[0]).toHaveProperty("unitPrice");
523
+ });
524
+
525
+ test("select with aggregation", async () => {
526
+ const res = await products().query()
527
+ .select("category", agg("count", "id", "total"), agg("avg", "price", "avgPrice"),
528
+ agg("max", "price", "maxPrice"), agg("min", "price", "minPrice"),
529
+ agg("sum", "stock", "totalStock"))
530
+ .groupBy("category")
531
+ .exec();
532
+ expect(res.code).toBe("OK");
533
+ const first = (res.data as any[])[0];
534
+ expect(first).toHaveProperty("category");
535
+ expect(first).toHaveProperty("total");
536
+ expect(first).toHaveProperty("avgPrice");
537
+ });
538
+
539
+ test("orderAsc", async () => {
540
+ const res = await products().query().orderAsc("price").page(1, 10).exec();
541
+ expect(res.code).toBe("OK");
542
+ const prices = (res.data as any[]).map((r: any) => r.price);
543
+ for (let i = 1; i < prices.length; i++) expect(prices[i]).toBeGreaterThanOrEqual(prices[i - 1]);
544
+ });
545
+
546
+ test("orderDesc", async () => {
547
+ const res = await products().query().orderDesc("price").page(1, 10).exec();
548
+ expect(res.code).toBe("OK");
549
+ const prices = (res.data as any[]).map((r: any) => r.price);
550
+ for (let i = 1; i < prices.length; i++) expect(prices[i]).toBeLessThanOrEqual(prices[i - 1]);
551
+ });
552
+
553
+ test("groupBy", async () => {
554
+ const res = await products().query()
555
+ .select("category", agg("count", "id", "cnt"))
556
+ .groupBy("category").exec();
557
+ expect(res.code).toBe("OK");
558
+ const cats = (res.data as any[]).map((r: any) => r.category);
559
+ expect(new Set(cats).size).toBe(cats.length);
560
+ });
561
+
562
+ test("pagination", async () => {
563
+ const p1 = await products().query().orderAsc("id").page(1, 5).exec();
564
+ const p2 = await products().query().orderAsc("id").page(2, 5).exec();
565
+ expect(p1.pageNo).toBe(1);
566
+ expect(p1.pageSize).toBe(5);
567
+ expect(typeof p1.total).toBe("number");
568
+ expect((p1.data as any[]).length).toBe(5);
569
+ const lastP1 = (p1.data as any[])[(p1.data as any[]).length - 1];
570
+ const firstP2 = (p2.data as any[])[0];
571
+ expect(firstP2.id).toBeGreaterThan(lastP1.id);
572
+ });
573
+
574
+ test("data() shortcut", async () => {
575
+ const data = await products().query().page(1, 3).data();
576
+ expect(Array.isArray(data)).toBe(true);
577
+ expect(data.length).toBe(3);
578
+ });
579
+
580
+ test("first() shortcut", async () => {
581
+ const first = await products().query().orderAsc("id").first();
582
+ expect(first).not.toBeNull();
583
+ expect((first as any).id).toBe(1);
584
+ });
585
+
586
+ test("complex chain query", async () => {
587
+ const res = await products().query()
588
+ .where(gt("price", 10), eq("is_active", 1))
589
+ .where(isNotNull("category"))
590
+ .select("name", "price", "category")
591
+ .orderDesc("price")
592
+ .page(1, 10)
593
+ .exec();
594
+ expect(res.code).toBe("OK");
595
+ expect(res.pageNo).toBe(1);
596
+ expect(res.pageSize).toBe(10);
597
+ expect(typeof res.total).toBe("number");
598
+ });
599
+ });
600
+
601
+ /* ────────────── QueryBuilder (logs — 无 owner) ────────────── */
602
+
603
+ describe("QueryBuilder (logs, no owner)", () => {
604
+ const rbAdmin = new RestBase(BASE);
605
+ beforeAll(() => {
606
+ rbAdmin.auth.useBasicAuth("admin", "admin");
607
+ });
608
+ const logs = () => rbAdmin.table("logs");
609
+
610
+ test("query logs with where", async () => {
611
+ const res = await logs().query().where(eq("level", "ERROR")).exec();
612
+ expect(res.code).toBe("OK");
613
+ expect((res.data as any[]).length).toBeGreaterThan(0);
614
+ for (const row of res.data as any[]) expect(row.level).toBe("ERROR");
615
+ });
616
+
617
+ test("query logs group by level", async () => {
618
+ const res = await logs().query()
619
+ .select("level", agg("count", "id", "cnt"))
620
+ .groupBy("level").exec();
621
+ expect(res.code).toBe("OK");
622
+ expect((res.data as any[]).length).toBeGreaterThan(0);
623
+ });
624
+ });
625
+
626
+ /* ────────────── DeleteBuilder 测试 ────────────── */
627
+
628
+ describe("DeleteBuilder (products)", () => {
629
+ const rbAdmin = new RestBase(BASE);
630
+ beforeAll(() => {
631
+ rbAdmin.auth.useBasicAuth("admin", "admin");
632
+ });
633
+ const products = () => rbAdmin.table("products");
634
+
635
+ test("deleteWhere with single condition", async () => {
636
+ await products().create([
637
+ {name: "DelTest1", price: 0.01, stock: 0, category: "DelCat"},
638
+ {name: "DelTest2", price: 0.02, stock: 0, category: "DelCat"},
639
+ ]);
640
+ const res = await products().deleteWhere().where(eq("category", "DelCat")).exec();
641
+ expect(res.code).toBe("OK");
642
+ expect((res.data as any).deleted.length).toBeGreaterThanOrEqual(2);
643
+ expect(Array.isArray((res.data as any).deleted)).toBe(true);
644
+ });
645
+
646
+ test("deleteWhere with and/or", async () => {
647
+ await products().create([
648
+ {name: "DelOr1", price: 0.01, stock: 0, category: "OrCat1"},
649
+ {name: "DelOr2", price: 0.02, stock: 0, category: "OrCat2"},
650
+ ]);
651
+ const res = await products().deleteWhere()
652
+ .where(or(eq("category", "OrCat1"), eq("category", "OrCat2")))
653
+ .exec();
654
+ expect(res.code).toBe("OK");
655
+ expect((res.data as any).deleted.length).toBeGreaterThanOrEqual(2);
656
+ });
657
+ });
658
+
659
+ /* ────────────── 禁止操作 auth 表 ────────────── */
660
+
661
+ describe("auth table protection", () => {
662
+ const rbAdmin = new RestBase(BASE);
663
+ beforeAll(() => {
664
+ rbAdmin.auth.useBasicAuth("admin", "admin");
665
+ });
666
+
667
+ test("cannot CRUD users table directly", async () => {
668
+ const res = await rbAdmin.table("users").getByPk(1);
669
+ expect(res.code).toBe("FORBIDDEN");
670
+ });
671
+ });
672
+
673
+ /* ────────────── requestId ────────────── */
674
+
675
+ describe("requestId", () => {
676
+ test("set custom requestId", async () => {
677
+ const rbAdmin = new RestBase(BASE);
678
+ rbAdmin.auth.useBasicAuth("admin", "admin");
679
+ rbAdmin.setRequestId("test-req-id-12345");
680
+ const res = await rbAdmin.health();
681
+ expect(res.code).toBe("OK");
682
+ });
683
+ });
684
+
685
+ /* ────────────── Meta(通过 client) ────────────── */
686
+
687
+ describe("meta via client", () => {
688
+ const rbAdmin = new RestBase(BASE);
689
+ beforeAll(() => {
690
+ rbAdmin.auth.useBasicAuth("admin", "admin");
691
+ });
692
+
693
+ test("tables() returns all metadata", async () => {
694
+ const res = await rbAdmin.tables();
695
+ expect(res.code).toBe("OK");
696
+ const names = (res.data as any[]).map((t: any) => t.name);
697
+ expect(names).toContain("products");
698
+ expect(names).toContain("logs");
699
+ expect(names).not.toContain("users");
700
+ });
701
+
702
+ test("tableMeta(name) returns single table", async () => {
703
+ const res = await rbAdmin.tableMeta("logs");
704
+ expect(res.code).toBe("OK");
705
+ expect((res.data as any).name).toBe("logs");
706
+ expect((res.data as any).hasOwner).toBe(false);
707
+ });
708
+
709
+ test("tableMeta(name) returns null for unknown", async () => {
710
+ const res = await rbAdmin.tableMeta("nope");
711
+ expect(res.code).toBe("OK");
712
+ expect(res.data).toBeNull();
713
+ });
714
+
715
+ test("syncMeta() refreshes and returns metadata", async () => {
716
+ const res = await rbAdmin.syncMeta();
717
+ expect(res.code).toBe("OK");
718
+ const names = (res.data as any[]).map((t: any) => t.name);
719
+ expect(names).toContain("products");
720
+ });
721
+ });
722
+
723
+ /* ══════════════════════════════════════════════════════════════
724
+ Type-safe SELECT — 运行时验证 + 编译期类型推导
725
+ ══════════════════════════════════════════════════════════════ */
726
+
727
+ describe("Type-safe select (typed table)", () => {
728
+ interface Product {
729
+ id: number;
730
+ name: string;
731
+ category: string;
732
+ price: number;
733
+ stock: number;
734
+ rating: number;
735
+ is_active: number;
736
+ tags: string | null;
737
+ description: string | null;
738
+ created_at: string;
739
+ owner: number;
740
+ }
741
+
742
+ const rb = new RestBase(BASE);
743
+ beforeAll(() => {
744
+ rb.auth.useBasicAuth("admin", "admin");
745
+ });
746
+ const products = () => rb.table<Product>("products");
747
+
748
+ test("select 单字段 → { name: string }", async () => {
749
+ // TypeScript: data is { name: string }[]
750
+ const data = await products().query().select("name").page(1, 3).data();
751
+ expect(data.length).toBe(3);
752
+ expect(data[0]).toHaveProperty("name");
753
+
754
+ if (data[0]) {
755
+ expect(typeof data[0].name).toBe("string");
756
+ }
757
+ // 不应包含未选择的字段
758
+ expect(data[0]).not.toHaveProperty("price");
759
+ });
760
+
761
+ test("select 多字段 → { name: string; price: number }", async () => {
762
+ // TypeScript: data is { name: string; price: number }[]
763
+ const data = await products().query().select("name", "price").page(1, 3).data();
764
+ expect(data.length).toBe(3);
765
+ expect(data[0]).toHaveProperty("name");
766
+ expect(data[0]).toHaveProperty("price");
767
+ if (data[0]) {
768
+ expect(typeof data[0].name).toBe("string");
769
+ expect(typeof data[0].price).toBe("number");
770
+ }
771
+ });
772
+
773
+ test("select + 别名 sel(field, alias) → { unitPrice: number; name: string }", async () => {
774
+ // TypeScript: data is { unitPrice: number; name: string }[]
775
+ const data = await products().query()
776
+ .select(sel("price", "unitPrice"), "name")
777
+ .page(1, 3)
778
+ .data();
779
+ expect(data.length).toBe(3);
780
+ expect(data[0]).toHaveProperty("unitPrice");
781
+ expect(data[0]).toHaveProperty("name");
782
+ if (data[0]) {
783
+ expect(typeof data[0].unitPrice).toBe("number");
784
+ }
785
+ });
786
+
787
+ test("select + 聚合 agg() → { category: string; total: number }", async () => {
788
+ // TypeScript: data is { category: string; total: number }[]
789
+ const data = await products().query()
790
+ .select("category", agg("count", "id", "total"))
791
+ .groupBy("category")
792
+ .data();
793
+ expect(data.length).toBeGreaterThan(0);
794
+ expect(data[0]).toHaveProperty("category");
795
+ expect(data[0]).toHaveProperty("total");
796
+ if (data[0]) {
797
+ expect(typeof data[0].total).toBe("number");
798
+ }
799
+ });
800
+
801
+ test("select + 多聚合 → { category: string; total: number; avgPrice: number }", async () => {
802
+ // TypeScript: data is { category: string; total: number; avgPrice: number }[]
803
+ const data = await products().query()
804
+ .select("category", agg("count", "id", "total"), agg("avg", "price", "avgPrice"))
805
+ .groupBy("category")
806
+ .data();
807
+ expect(data.length).toBeGreaterThan(0);
808
+ expect(data[0]).toHaveProperty("category");
809
+ expect(data[0]).toHaveProperty("total");
810
+ expect(data[0]).toHaveProperty("avgPrice");
811
+ });
812
+
813
+ test("select + where + order + page 全链式", async () => {
814
+ // TypeScript: data is { name: string; price: number }[]
815
+ const data = await products().query()
816
+ .select("name", "price")
817
+ .where(gt("price", 10))
818
+ .orderDesc("price")
819
+ .page(1, 5)
820
+ .data();
821
+ expect(data.length).toBeLessThanOrEqual(5);
822
+ for (const row of data) {
823
+ expect(row).toHaveProperty("name");
824
+ expect(row).toHaveProperty("price");
825
+ expect(row.price).toBeGreaterThan(10);
826
+ }
827
+ });
828
+
829
+ test("select 字符串模板 'field:alias'", async () => {
830
+ const data = await products().query()
831
+ .select("price:unitPrice" as any, "name")
832
+ .page(1, 3)
833
+ .data();
834
+ expect(data.length).toBe(3);
835
+ expect(data[0]).toHaveProperty("unitPrice");
836
+ expect(data[0]).toHaveProperty("name");
837
+ });
838
+
839
+ test("select 字符串模板 'func:field:alias'", async () => {
840
+ const data = await products().query()
841
+ .select("category", "count:id:total" as any)
842
+ .groupBy("category")
843
+ .data();
844
+ expect(data.length).toBeGreaterThan(0);
845
+ expect(data[0]).toHaveProperty("category");
846
+ expect(data[0]).toHaveProperty("total");
847
+ });
848
+
849
+ test("agg 无 alias → 默认 key 为 'fn:field'", async () => {
850
+ // TypeScript: data is { category: string; "count:id": number }[]
851
+ const data = await products().query()
852
+ .select("category", agg("count", "id"))
853
+ .groupBy("category")
854
+ .data();
855
+ expect(data.length).toBeGreaterThan(0);
856
+ expect(data[0]).toHaveProperty("category");
857
+ expect(data[0]).toHaveProperty("count:id");
858
+
859
+ if (data[0]) {
860
+ expect(typeof data[0]["count:id"]).toBe("number");
861
+ }
862
+ });
863
+
864
+ test("agg 无 alias 多聚合 → key 均为 'fn:field'", async () => {
865
+ // TypeScript: data is { category: string; "count:id": number; "avg:price": number }[]
866
+ const data = await products().query()
867
+ .select("category", agg("count", "id"), agg("avg", "price"))
868
+ .groupBy("category")
869
+ .data();
870
+ expect(data.length).toBeGreaterThan(0);
871
+ expect(data[0]).toHaveProperty("count:id");
872
+ expect(data[0]).toHaveProperty("avg:price");
873
+ });
874
+
875
+ test("字符串 'max:price' 与 agg('max','price') 返回相同 key", async () => {
876
+ // 字符串方式
877
+ const data1 = await products().query()
878
+ .select("max:price" as any)
879
+ .data();
880
+ // agg 方式
881
+ const data2 = await products().query()
882
+ .select(agg("max", "price"))
883
+ .data();
884
+ // 两者都应该返回 "max:price" 作为 key
885
+ expect(data1[0]).toHaveProperty("max:price");
886
+ expect(data2[0]).toHaveProperty("max:price");
887
+ });
888
+
889
+ test("first() 返回单条带类型", async () => {
890
+ // TypeScript: row is { name: string; price: number } | null
891
+ const row = await products().query()
892
+ .select("name", "price")
893
+ .orderAsc("price")
894
+ .first();
895
+ expect(row).not.toBeNull();
896
+ expect(row!).toHaveProperty("name");
897
+ expect(row!).toHaveProperty("price");
898
+ });
899
+
900
+ test("exec() 完整响应带类型", async () => {
901
+ // TypeScript: res.data is { name: string }[]
902
+ const res = await products().query()
903
+ .select("name")
904
+ .page(1, 2)
905
+ .exec();
906
+ expect(res.code).toBe("OK");
907
+ expect(res.data.length).toBeLessThanOrEqual(2);
908
+ expect(res.data[0]).toHaveProperty("name");
909
+ });
910
+ });
911
+ });
912
+
913
+ /* ═══════════════════════════════════════════════════════════════
914
+ Part 2: 原始 HTTP — URL 查询参数全场景
915
+ ═══════════════════════════════════════════════════════════════ */
916
+
917
+ describe("Raw HTTP — URL query params", () => {
918
+
919
+ test("GET /api/health", async () => {
920
+ const res = await raw("GET", "/api/health");
921
+ expect(res.code).toBe("OK");
922
+ });
923
+
924
+ /* ══════════════════════════════════════════════════════════
925
+ GET /api/data/:table — 各种条件
926
+ ══════════════════════════════════════════════════════════ */
927
+
928
+ describe("GET /api/data/products — operators", () => {
929
+
930
+ test("no params (all records)", async () => {
931
+ const res = await raw("GET", "/api/data/products", {auth: AUTH});
932
+ expect(res.code).toBe("OK");
933
+ expect((res.data as any[]).length).toBeGreaterThanOrEqual(30);
934
+ });
935
+
936
+ test("eq implicit: ?category=Books", async () => {
937
+ const res = await raw("GET", "/api/data/products?category=Books", {auth: AUTH});
938
+ expect(res.code).toBe("OK");
939
+ for (const r of res.data as any[]) expect(r.category).toBe("Books");
940
+ });
941
+
942
+ test("eq explicit: ?is_active=eq.1", async () => {
943
+ const res = await raw("GET", "/api/data/products?is_active=eq.1", {auth: AUTH});
944
+ expect(res.code).toBe("OK");
945
+ for (const r of res.data as any[]) expect(r.is_active).toBe(1);
946
+ });
947
+
948
+ test("ne: ?category=ne.Books", async () => {
949
+ const res = await raw("GET", "/api/data/products?category=ne.Books", {auth: AUTH});
950
+ expect(res.code).toBe("OK");
951
+ for (const r of res.data as any[]) expect(r.category).not.toBe("Books");
952
+ });
953
+
954
+ test("gt: ?price=gt.500", async () => {
955
+ const res = await raw("GET", "/api/data/products?price=gt.500", {auth: AUTH});
956
+ expect(res.code).toBe("OK");
957
+ expect((res.data as any[]).length).toBeGreaterThan(0);
958
+ for (const r of res.data as any[]) expect(r.price).toBeGreaterThan(500);
959
+ });
960
+
961
+ test("ge: ?stock=ge.200", async () => {
962
+ const res = await raw("GET", "/api/data/products?stock=ge.200", {auth: AUTH});
963
+ expect(res.code).toBe("OK");
964
+ for (const r of res.data as any[]) expect(r.stock).toBeGreaterThanOrEqual(200);
965
+ });
966
+
967
+ test("lt: ?price=lt.50", async () => {
968
+ const res = await raw("GET", "/api/data/products?price=lt.50", {auth: AUTH});
969
+ expect(res.code).toBe("OK");
970
+ for (const r of res.data as any[]) expect(r.price).toBeLessThan(50);
971
+ });
972
+
973
+ test("le: ?rating=le.2", async () => {
974
+ const res = await raw("GET", "/api/data/products?rating=le.2", {auth: AUTH});
975
+ expect(res.code).toBe("OK");
976
+ for (const r of res.data as any[]) expect(r.rating).toBeLessThanOrEqual(2);
977
+ });
978
+
979
+ test("like: ?name=like.Pro*", async () => {
980
+ const res = await raw("GET", "/api/data/products?name=like.Pro*", {auth: AUTH});
981
+ expect(res.code).toBe("OK");
982
+ expect((res.data as any[]).length).toBeGreaterThan(0);
983
+ for (const r of res.data as any[]) expect(r.name.startsWith("Pro")).toBe(true);
984
+ });
985
+
986
+ test("nlike: ?name=nlike.Pro*", async () => {
987
+ const res = await raw("GET", "/api/data/products?name=nlike.Pro*", {auth: AUTH});
988
+ expect(res.code).toBe("OK");
989
+ for (const r of res.data as any[]) expect(r.name.startsWith("Pro")).toBe(false);
990
+ });
991
+
992
+ test("is null: ?tags=is.null", async () => {
993
+ const res = await raw("GET", "/api/data/products?tags=is.null", {auth: AUTH});
994
+ expect(res.code).toBe("OK");
995
+ expect((res.data as any[]).length).toBeGreaterThanOrEqual(5);
996
+ });
997
+
998
+ test("is not null: ?name=nis.null", async () => {
999
+ const res = await raw("GET", "/api/data/products?name=nis.null", {auth: AUTH});
1000
+ expect(res.code).toBe("OK");
1001
+ expect((res.data as any[]).length).toBeGreaterThan(0);
1002
+ });
1003
+
1004
+ test("in: ?category=in.(Books,Toys)", async () => {
1005
+ const res = await raw("GET", "/api/data/products?category=in.(Books,Toys)", {auth: AUTH});
1006
+ expect(res.code).toBe("OK");
1007
+ for (const r of res.data as any[]) expect(["Books", "Toys"]).toContain(r.category);
1008
+ });
1009
+
1010
+ test("nin: ?category=nin.(Books,Toys)", async () => {
1011
+ const res = await raw("GET", "/api/data/products?category=nin.(Books,Toys)", {auth: AUTH});
1012
+ expect(res.code).toBe("OK");
1013
+ for (const r of res.data as any[]) expect(["Books", "Toys"]).not.toContain(r.category);
1014
+ });
1015
+
1016
+ test("in between: ?price=in(100...500)", async () => {
1017
+ const res = await raw("GET", "/api/data/products?price=in(100...500)", {auth: AUTH});
1018
+ expect(res.code).toBe("OK");
1019
+ for (const r of res.data as any[]) {
1020
+ expect(r.price).toBeGreaterThanOrEqual(100);
1021
+ expect(r.price).toBeLessThanOrEqual(500);
1022
+ }
1023
+ });
1024
+
1025
+ test("multi field AND: ?price=gt.100&is_active=eq.1", async () => {
1026
+ const res = await raw("GET", "/api/data/products?price=gt.100&is_active=eq.1", {auth: AUTH});
1027
+ expect(res.code).toBe("OK");
1028
+ for (const r of res.data as any[]) {
1029
+ expect(r.price).toBeGreaterThan(100);
1030
+ expect(r.is_active).toBe(1);
1031
+ }
1032
+ });
1033
+ });
1034
+
1035
+ /* ══════════════════════════════════════════════════════════
1036
+ 逻辑组合(or / and / 嵌套)
1037
+ ══════════════════════════════════════════════════════════ */
1038
+
1039
+ describe("GET /api/data/products — logic groups", () => {
1040
+
1041
+ test("or: ?or=category.eq.Books,category.eq.Toys", async () => {
1042
+ const res = await raw("GET", "/api/data/products?or=category.eq.Books,category.eq.Toys", {auth: AUTH});
1043
+ expect(res.code).toBe("OK");
1044
+ for (const r of res.data as any[]) expect(["Books", "Toys"]).toContain(r.category);
1045
+ });
1046
+
1047
+ test("and: ?and=price.gt.100,is_active.eq.1", async () => {
1048
+ const res = await raw("GET", "/api/data/products?and=price.gt.100,is_active.eq.1", {auth: AUTH});
1049
+ expect(res.code).toBe("OK");
1050
+ for (const r of res.data as any[]) {
1051
+ expect(r.price).toBeGreaterThan(100);
1052
+ expect(r.is_active).toBe(1);
1053
+ }
1054
+ });
1055
+
1056
+ test("nested: ?and=category.eq.Books,or.(price.gt.500,stock.lt.10)", async () => {
1057
+ const res = await raw("GET",
1058
+ "/api/data/products?and=category.eq.Books,or.(price.gt.500,stock.lt.10)", {auth: AUTH});
1059
+ expect(res.code).toBe("OK");
1060
+ for (const r of res.data as any[]) {
1061
+ expect(r.category).toBe("Books");
1062
+ expect(r.price > 500 || r.stock < 10).toBe(true);
1063
+ }
1064
+ });
1065
+
1066
+ test("nested: ?or=price.gt.900,and.(category.eq.Toys,stock.lt.50)", async () => {
1067
+ const res = await raw("GET",
1068
+ "/api/data/products?or=price.gt.900,and.(category.eq.Toys,stock.lt.50)", {auth: AUTH});
1069
+ expect(res.code).toBe("OK");
1070
+ for (const r of res.data as any[]) {
1071
+ expect(r.price > 900 || (r.category === "Toys" && r.stock < 50)).toBe(true);
1072
+ }
1073
+ });
1074
+
1075
+ test("field + or: ?is_active=eq.1&or=category.eq.Books,category.eq.Toys", async () => {
1076
+ const res = await raw("GET",
1077
+ "/api/data/products?is_active=eq.1&or=category.eq.Books,category.eq.Toys", {auth: AUTH});
1078
+ expect(res.code).toBe("OK");
1079
+ for (const r of res.data as any[]) {
1080
+ expect(r.is_active).toBe(1);
1081
+ expect(["Books", "Toys"]).toContain(r.category);
1082
+ }
1083
+ });
1084
+ });
1085
+
1086
+ /* ══════════════════════════════════════════════════════════
1087
+ SELECT / ORDER / GROUP / PAGINATION
1088
+ ══════════════════════════════════════════════════════════ */
1089
+
1090
+ describe("GET select/order/group/page", () => {
1091
+
1092
+ test("select=name,price", async () => {
1093
+ const res = await raw("GET", "/api/data/products?select=name,price&pageNo=1&pageSize=5", {auth: AUTH});
1094
+ expect(res.code).toBe("OK");
1095
+ const first = (res.data as any[])[0];
1096
+ expect(Object.keys(first)).toContain("name");
1097
+ expect(Object.keys(first)).toContain("price");
1098
+ });
1099
+
1100
+ test("select alias: select=price:unitPrice", async () => {
1101
+ const res = await raw("GET", "/api/data/products?select=price:unitPrice&pageNo=1&pageSize=5", {auth: AUTH});
1102
+ expect(res.code).toBe("OK");
1103
+ expect((res.data as any[])[0]).toHaveProperty("unitPrice");
1104
+ });
1105
+
1106
+ test("select agg: select=count:id → key 为 'count:id'", async () => {
1107
+ const res = await raw("GET", "/api/data/products?select=count:id", {auth: AUTH});
1108
+ expect(res.code).toBe("OK");
1109
+ const first = (res.data as any[])[0];
1110
+ expect(first).toHaveProperty("count:id");
1111
+ });
1112
+
1113
+ test("select agg+alias: select=max:price:maxPrice,min:price:minPrice", async () => {
1114
+ const res = await raw("GET", "/api/data/products?select=max:price:maxPrice,min:price:minPrice", {auth: AUTH});
1115
+ expect(res.code).toBe("OK");
1116
+ const first = (res.data as any[])[0];
1117
+ expect(first).toHaveProperty("maxPrice");
1118
+ expect(first).toHaveProperty("minPrice");
1119
+ });
1120
+
1121
+ test("select+group: ?select=category,count:id:total,avg:price:avgPrice&group=category", async () => {
1122
+ const res = await raw("GET",
1123
+ "/api/data/products?select=category,count:id:total,avg:price:avgPrice&group=category", {auth: AUTH});
1124
+ expect(res.code).toBe("OK");
1125
+ const first = (res.data as any[])[0];
1126
+ expect(first).toHaveProperty("category");
1127
+ expect(first).toHaveProperty("total");
1128
+ expect(first).toHaveProperty("avgPrice");
1129
+ });
1130
+
1131
+ test("order=asc.price", async () => {
1132
+ const res = await raw("GET", "/api/data/products?order=asc.price&pageNo=1&pageSize=10", {auth: AUTH});
1133
+ expect(res.code).toBe("OK");
1134
+ const prices = (res.data as any[]).map((r: any) => r.price);
1135
+ for (let i = 1; i < prices.length; i++) expect(prices[i]).toBeGreaterThanOrEqual(prices[i - 1]);
1136
+ });
1137
+
1138
+ test("order=desc.price", async () => {
1139
+ const res = await raw("GET", "/api/data/products?order=desc.price&pageNo=1&pageSize=10", {auth: AUTH});
1140
+ expect(res.code).toBe("OK");
1141
+ const prices = (res.data as any[]).map((r: any) => r.price);
1142
+ for (let i = 1; i < prices.length; i++) expect(prices[i]).toBeLessThanOrEqual(prices[i - 1]);
1143
+ });
1144
+
1145
+ test("order multi: asc.category,desc.price", async () => {
1146
+ const res = await raw("GET", "/api/data/products?order=asc.category,desc.price&pageNo=1&pageSize=20", {auth: AUTH});
1147
+ expect(res.code).toBe("OK");
1148
+ expect((res.data as any[]).length).toBeGreaterThan(0);
1149
+ });
1150
+
1151
+ test("order default (asc): order=name", async () => {
1152
+ const res = await raw("GET", "/api/data/products?order=name&pageNo=1&pageSize=10", {auth: AUTH});
1153
+ expect(res.code).toBe("OK");
1154
+ const names = (res.data as any[]).map((r: any) => r.name);
1155
+ for (let i = 1; i < names.length; i++) expect(names[i] >= names[i - 1]).toBe(true);
1156
+ });
1157
+
1158
+ test("pagination page 1", async () => {
1159
+ const res = await raw("GET", "/api/data/products?pageNo=1&pageSize=5", {auth: AUTH});
1160
+ expect(res.code).toBe("OK");
1161
+ expect(res.pageNo).toBe(1);
1162
+ expect(res.pageSize).toBe(5);
1163
+ expect(typeof res.total).toBe("number");
1164
+ expect((res.data as any[]).length).toBeLessThanOrEqual(5);
1165
+ });
1166
+
1167
+ test("pagination page 2 vs page 1", async () => {
1168
+ const p1 = await raw("GET", "/api/data/products?order=asc.id&pageNo=1&pageSize=5", {auth: AUTH});
1169
+ const p2 = await raw("GET", "/api/data/products?order=asc.id&pageNo=2&pageSize=5", {auth: AUTH});
1170
+ if ((p2.data as any[]).length > 0) {
1171
+ const last1 = (p1.data as any[])[(p1.data as any[]).length - 1];
1172
+ const first2 = (p2.data as any[])[0];
1173
+ expect(first2.id).toBeGreaterThan(last1.id);
1174
+ }
1175
+ });
1176
+
1177
+ test("combined: where + order + page", async () => {
1178
+ const res = await raw("GET",
1179
+ "/api/data/products?is_active=eq.1&order=desc.price&pageNo=1&pageSize=10", {auth: AUTH});
1180
+ expect(res.code).toBe("OK");
1181
+ expect(res.pageNo).toBe(1);
1182
+ for (const r of res.data as any[]) expect(r.is_active).toBe(1);
1183
+ const prices = (res.data as any[]).map((r: any) => r.price);
1184
+ for (let i = 1; i < prices.length; i++) expect(prices[i]).toBeLessThanOrEqual(prices[i - 1]);
1185
+ });
1186
+ });
1187
+
1188
+ /* ══════════════════════════════════════════════════════════
1189
+ GET /api/data/:table/:id
1190
+ ══════════════════════════════════════════════════════════ */
1191
+
1192
+ describe("GET /api/data/:table/:id", () => {
1193
+ test("get by pk (exists)", async () => {
1194
+ const res = await raw("GET", "/api/data/products/1", {auth: AUTH});
1195
+ expect(res.code).toBe("OK");
1196
+ expect(res.data).not.toBeNull();
1197
+ expect((res.data as any).id).toBe(1);
1198
+ });
1199
+
1200
+ test("get by pk (not exists)", async () => {
1201
+ const res = await raw("GET", "/api/data/products/99999", {auth: AUTH});
1202
+ expect(res.code).toBe("OK");
1203
+ expect(res.data).toBeNull();
1204
+ });
1205
+ });
1206
+
1207
+ /* ══════════════════════════════════════════════════════════
1208
+ DELETE /api/data/:table — URL 条件删除
1209
+ ══════════════════════════════════════════════════════════ */
1210
+
1211
+ describe("DELETE /api/data/products (URL params)", () => {
1212
+
1213
+ test("setup: create deletable records", async () => {
1214
+ for (let i = 0; i < 5; i++) {
1215
+ await raw("POST", "/api/data/products", {
1216
+ auth: AUTH,
1217
+ body: {name: `UrlDel_${i}`, price: 0.001 + i * 0.001, stock: i, category: "UrlDelCat"},
1218
+ });
1219
+ }
1220
+ const check = await raw("GET", "/api/data/products?category=UrlDelCat", {auth: AUTH});
1221
+ expect((check.data as any[]).length).toBeGreaterThanOrEqual(5);
1222
+ });
1223
+
1224
+ test("delete eq: ?name=UrlDel_0", async () => {
1225
+ const res = await raw("DELETE", "/api/data/products?name=UrlDel_0", {auth: AUTH});
1226
+ expect(res.code).toBe("OK");
1227
+ expect(Array.isArray((res.data as any).deleted)).toBe(true);
1228
+ expect((res.data as any).deleted.length).toBeGreaterThanOrEqual(1);
1229
+ });
1230
+
1231
+ test("delete gt: ?stock=gt.3&category=UrlDelCat", async () => {
1232
+ const res = await raw("DELETE", "/api/data/products?stock=gt.3&category=UrlDelCat", {auth: AUTH});
1233
+ expect(res.code).toBe("OK");
1234
+ });
1235
+
1236
+ test("delete like: ?name=like.UrlDel*", async () => {
1237
+ const res = await raw("DELETE", "/api/data/products?name=like.UrlDel*", {auth: AUTH});
1238
+ expect(res.code).toBe("OK");
1239
+ const check = await raw("GET", "/api/data/products?category=UrlDelCat", {auth: AUTH});
1240
+ expect((check.data as any[]).length).toBe(0);
1241
+ });
1242
+
1243
+ test("delete or conditions", async () => {
1244
+ await raw("POST", "/api/data/products", {
1245
+ auth: AUTH,
1246
+ body: [
1247
+ {name: "OrDel_A", price: 0.001, stock: 0, category: "OrDelCatA"},
1248
+ {name: "OrDel_B", price: 0.002, stock: 0, category: "OrDelCatB"},
1249
+ ],
1250
+ });
1251
+ const res = await raw("DELETE",
1252
+ "/api/data/products?or=category.eq.OrDelCatA,category.eq.OrDelCatB", {auth: AUTH});
1253
+ expect(res.code).toBe("OK");
1254
+ expect((res.data as any).deleted.length).toBeGreaterThanOrEqual(2);
1255
+ });
1256
+ });
1257
+
1258
+ /* ══════════════════════════════════════════════════════════
1259
+ DELETE /api/data/:table/:id — 按主键删除
1260
+ ══════════════════════════════════════════════════════════ */
1261
+
1262
+ describe("DELETE /api/data/:table/:id", () => {
1263
+ test("delete by pk", async () => {
1264
+ const cr = await raw("POST", "/api/data/products", {
1265
+ auth: AUTH, body: {name: "PkDel", price: 0.001, stock: 0, category: "PkDel"},
1266
+ });
1267
+ const id = (cr.data as any).created[0];
1268
+ const res = await raw("DELETE", `/api/data/products/${id}`, {auth: AUTH});
1269
+ expect(res.code).toBe("OK");
1270
+ expect((res.data as any).deleted).toContain(id);
1271
+ const check = await raw("GET", `/api/data/products/${id}`, {auth: AUTH});
1272
+ expect(check.data).toBeNull();
1273
+ });
1274
+ });
1275
+
1276
+ /* ══════════════════════════════════════════════════════════
1277
+ POST /api/query/:table — Body 查询(原始 HTTP)
1278
+ ══════════════════════════════════════════════════════════ */
1279
+
1280
+ describe("POST /api/query/products (raw)", () => {
1281
+
1282
+ test("body: where 二元组", async () => {
1283
+ const res = await raw("POST", "/api/query/products", {
1284
+ auth: AUTH, body: {where: ["is_active", 1]},
1285
+ });
1286
+ expect(res.code).toBe("OK");
1287
+ for (const r of res.data as any[]) expect(r.is_active).toBe(1);
1288
+ });
1289
+
1290
+ test("body: where 三元组", async () => {
1291
+ const res = await raw("POST", "/api/query/products", {
1292
+ auth: AUTH, body: {where: ["price", "gt", 500]},
1293
+ });
1294
+ expect(res.code).toBe("OK");
1295
+ for (const r of res.data as any[]) expect(r.price).toBeGreaterThan(500);
1296
+ });
1297
+
1298
+ test("body: where 对象格式", async () => {
1299
+ const res = await raw("POST", "/api/query/products", {
1300
+ auth: AUTH, body: {where: [{field: "stock", op: "ge", value: 100}]},
1301
+ });
1302
+ expect(res.code).toBe("OK");
1303
+ for (const r of res.data as any[]) expect(r.stock).toBeGreaterThanOrEqual(100);
1304
+ });
1305
+
1306
+ test("body: where 逻辑组合 or", async () => {
1307
+ const res = await raw("POST", "/api/query/products", {
1308
+ auth: AUTH,
1309
+ body: {where: [{op: "or", cond: [["category", "eq", "Books"], ["category", "eq", "Toys"]]}]},
1310
+ });
1311
+ expect(res.code).toBe("OK");
1312
+ for (const r of res.data as any[]) expect(["Books", "Toys"]).toContain(r.category);
1313
+ });
1314
+
1315
+ test("body: where 嵌套 and + or", async () => {
1316
+ const res = await raw("POST", "/api/query/products", {
1317
+ auth: AUTH,
1318
+ body: {
1319
+ where: [
1320
+ ["price", "gt", 50],
1321
+ {
1322
+ op: "or", cond: [
1323
+ {field: "category", op: "eq", value: "Electronics"},
1324
+ {field: "category", op: "eq", value: "Sports"},
1325
+ ]
1326
+ },
1327
+ ],
1328
+ },
1329
+ });
1330
+ expect(res.code).toBe("OK");
1331
+ for (const r of res.data as any[]) {
1332
+ expect(r.price).toBeGreaterThan(50);
1333
+ expect(["Electronics", "Sports"]).toContain(r.category);
1334
+ }
1335
+ });
1336
+
1337
+ test("body: select 字符串 + 别名 + 聚合", async () => {
1338
+ const res = await raw("POST", "/api/query/products", {
1339
+ auth: AUTH,
1340
+ body: {
1341
+ select: ["category", "count:id:total", "avg:price:avgPrice", {field: "price", func: "max", alias: "maxPrice"}],
1342
+ group: ["category"],
1343
+ },
1344
+ });
1345
+ expect(res.code).toBe("OK");
1346
+ const first = (res.data as any[])[0];
1347
+ expect(first).toHaveProperty("category");
1348
+ expect(first).toHaveProperty("total");
1349
+ expect(first).toHaveProperty("avgPrice");
1350
+ expect(first).toHaveProperty("maxPrice");
1351
+ });
1352
+
1353
+ test("body: order 字符串 + 对象 + 分页", async () => {
1354
+ const res = await raw("POST", "/api/query/products", {
1355
+ auth: AUTH,
1356
+ body: {
1357
+ order: ["desc.price", {field: "name", dir: "asc"}],
1358
+ pageNo: 1, pageSize: 10,
1359
+ },
1360
+ });
1361
+ expect(res.code).toBe("OK");
1362
+ expect(res.pageNo).toBe(1);
1363
+ expect(res.pageSize).toBe(10);
1364
+ const prices = (res.data as any[]).map((r: any) => r.price);
1365
+ for (let i = 1; i < prices.length; i++) expect(prices[i]).toBeLessThanOrEqual(prices[i - 1]);
1366
+ });
1367
+
1368
+ test("body: 分页 + total", async () => {
1369
+ const res = await raw("POST", "/api/query/products", {
1370
+ auth: AUTH, body: {pageNo: 1, pageSize: 5},
1371
+ });
1372
+ expect(res.code).toBe("OK");
1373
+ expect(res.pageNo).toBe(1);
1374
+ expect(res.pageSize).toBe(5);
1375
+ expect(typeof res.total).toBe("number");
1376
+ expect((res.data as any[]).length).toBeLessThanOrEqual(5);
1377
+ });
1378
+ });
1379
+
1380
+ /* ══════════════════════════════════════════════════════════
1381
+ POST /api/delete/:table — Body 条件删除(原始 HTTP)
1382
+ ══════════════════════════════════════════════════════════ */
1383
+
1384
+ describe("POST /api/delete/products (raw)", () => {
1385
+
1386
+ test("body: 三元组 where", async () => {
1387
+ await raw("POST", "/api/data/products", {
1388
+ auth: AUTH,
1389
+ body: [
1390
+ {name: "BodyDel1", price: 0.001, stock: 0, category: "BodyDelCat"},
1391
+ {name: "BodyDel2", price: 0.002, stock: 0, category: "BodyDelCat"},
1392
+ ],
1393
+ });
1394
+ const res = await raw("POST", "/api/delete/products", {
1395
+ auth: AUTH, body: ["category", "eq", "BodyDelCat"],
1396
+ });
1397
+ expect(res.code).toBe("OK");
1398
+ expect(Array.isArray((res.data as any).deleted)).toBe(true);
1399
+ expect((res.data as any).deleted.length).toBeGreaterThanOrEqual(2);
1400
+ });
1401
+
1402
+ test("body: 数组 where", async () => {
1403
+ await raw("POST", "/api/data/products", {
1404
+ auth: AUTH,
1405
+ body: [
1406
+ {name: "BodyDel3", price: 0.001, stock: 0, category: "BodyDelCat2"},
1407
+ {name: "BodyDel4", price: 0.002, stock: 0, category: "BodyDelCat2"},
1408
+ ],
1409
+ });
1410
+ const res = await raw("POST", "/api/delete/products", {
1411
+ auth: AUTH, body: [["category", "eq", "BodyDelCat2"]],
1412
+ });
1413
+ expect(res.code).toBe("OK");
1414
+ expect((res.data as any).deleted.length).toBeGreaterThanOrEqual(2);
1415
+ });
1416
+
1417
+ test("body: or 条件", async () => {
1418
+ await raw("POST", "/api/data/products", {
1419
+ auth: AUTH,
1420
+ body: [
1421
+ {name: "BodyDelOr1", price: 0.001, stock: 0, category: "BDOrCat1"},
1422
+ {name: "BodyDelOr2", price: 0.002, stock: 0, category: "BDOrCat2"},
1423
+ ],
1424
+ });
1425
+ const res = await raw("POST", "/api/delete/products", {
1426
+ auth: AUTH,
1427
+ body: [{op: "or", cond: [["category", "eq", "BDOrCat1"], ["category", "eq", "BDOrCat2"]]}],
1428
+ });
1429
+ expect(res.code).toBe("OK");
1430
+ expect((res.data as any).deleted.length).toBeGreaterThanOrEqual(2);
1431
+ });
1432
+ });
1433
+
1434
+ /* ══════════════════════════════════════════════════════════
1435
+ logs 表(无 owner)原始 HTTP
1436
+ ══════════════════════════════════════════════════════════ */
1437
+
1438
+ describe("GET /api/data/logs (no owner)", () => {
1439
+ test("query all logs", async () => {
1440
+ const res = await raw("GET", "/api/data/logs", {auth: AUTH});
1441
+ expect(res.code).toBe("OK");
1442
+ expect((res.data as any[]).length).toBeGreaterThan(0);
1443
+ });
1444
+
1445
+ test("query logs with filter", async () => {
1446
+ const res = await raw("GET", "/api/data/logs?level=ERROR", {auth: AUTH});
1447
+ expect(res.code).toBe("OK");
1448
+ for (const r of res.data as any[]) expect(r.level).toBe("ERROR");
1449
+ });
1450
+
1451
+ test("query logs pagination", async () => {
1452
+ const res = await raw("GET", "/api/data/logs?pageNo=1&pageSize=10&order=desc.id", {auth: AUTH});
1453
+ expect(res.code).toBe("OK");
1454
+ expect(res.pageNo).toBe(1);
1455
+ expect(res.pageSize).toBe(10);
1456
+ });
1457
+
1458
+ test("logs group by level", async () => {
1459
+ const res = await raw("GET", "/api/data/logs?select=level,count:id:cnt&group=level", {auth: AUTH});
1460
+ expect(res.code).toBe("OK");
1461
+ const levels = (res.data as any[]).map((r: any) => r.level);
1462
+ expect(new Set(levels).size).toBe(levels.length);
1463
+ });
1464
+ });
1465
+ });