befly 3.9.40 → 3.10.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.
Files changed (141) hide show
  1. package/README.md +39 -8
  2. package/befly.config.ts +19 -2
  3. package/checks/checkApi.ts +79 -77
  4. package/checks/checkHook.ts +48 -0
  5. package/checks/checkMenu.ts +168 -0
  6. package/checks/checkPlugin.ts +48 -0
  7. package/checks/checkTable.ts +137 -183
  8. package/docs/README.md +1 -1
  9. package/docs/api/api.md +1 -1
  10. package/docs/guide/quickstart.md +16 -9
  11. package/docs/hooks/hook.md +2 -2
  12. package/docs/hooks/rateLimit.md +1 -1
  13. package/docs/infra/redis.md +7 -7
  14. package/docs/plugins/plugin.md +23 -21
  15. package/docs/quickstart.md +16 -9
  16. package/docs/reference/addon.md +12 -1
  17. package/docs/reference/config.md +13 -30
  18. package/docs/reference/sync.md +62 -193
  19. package/docs/reference/table.md +27 -29
  20. package/hooks/auth.ts +3 -4
  21. package/hooks/cors.ts +4 -6
  22. package/hooks/parser.ts +3 -4
  23. package/hooks/permission.ts +3 -4
  24. package/hooks/validator.ts +3 -4
  25. package/lib/cacheHelper.ts +89 -153
  26. package/lib/cacheKeys.ts +1 -1
  27. package/lib/connect.ts +9 -13
  28. package/lib/dbDialect.ts +285 -0
  29. package/lib/dbHelper.ts +179 -507
  30. package/lib/dbUtils.ts +450 -0
  31. package/lib/logger.ts +41 -5
  32. package/lib/redisHelper.ts +1 -0
  33. package/lib/sqlBuilder.ts +358 -58
  34. package/lib/sqlCheck.ts +136 -0
  35. package/lib/validator.ts +1 -1
  36. package/loader/loadApis.ts +23 -126
  37. package/loader/loadHooks.ts +31 -46
  38. package/loader/loadPlugins.ts +37 -52
  39. package/main.ts +58 -19
  40. package/package.json +24 -25
  41. package/paths.ts +14 -14
  42. package/plugins/cache.ts +12 -6
  43. package/plugins/cipher.ts +2 -2
  44. package/plugins/config.ts +6 -8
  45. package/plugins/db.ts +14 -19
  46. package/plugins/jwt.ts +6 -7
  47. package/plugins/logger.ts +7 -9
  48. package/plugins/redis.ts +8 -10
  49. package/plugins/tool.ts +3 -4
  50. package/router/api.ts +3 -2
  51. package/router/static.ts +7 -5
  52. package/sync/syncApi.ts +80 -235
  53. package/sync/syncCache.ts +16 -0
  54. package/sync/syncDev.ts +167 -202
  55. package/sync/syncMenu.ts +230 -444
  56. package/sync/syncTable.ts +1247 -0
  57. package/tests/_mocks/mockSqliteDb.ts +204 -0
  58. package/tests/addonHelper-cache.test.ts +32 -0
  59. package/tests/apiHandler-routePath-only.test.ts +32 -0
  60. package/tests/cacheHelper.test.ts +16 -51
  61. package/tests/checkApi-routePath-strict.test.ts +166 -0
  62. package/tests/checkMenu.test.ts +346 -0
  63. package/tests/checkTable-smoke.test.ts +157 -0
  64. package/tests/dbDialect-cache.test.ts +23 -0
  65. package/tests/dbDialect.test.ts +46 -0
  66. package/tests/dbHelper-advanced.test.ts +1 -1
  67. package/tests/dbHelper-all-array-types.test.ts +15 -15
  68. package/tests/dbHelper-batch-write.test.ts +90 -0
  69. package/tests/dbHelper-columns.test.ts +36 -54
  70. package/tests/dbHelper-execute.test.ts +26 -26
  71. package/tests/dbHelper-joins.test.ts +85 -176
  72. package/tests/fixtures/scanFilesAddon/node_modules/@befly-addon/demo/apis/sub/b.ts +3 -0
  73. package/tests/fixtures/scanFilesApis/a.ts +3 -0
  74. package/tests/fixtures/scanFilesApis/sub/b.ts +3 -0
  75. package/tests/loadPlugins-order-smoke.test.ts +75 -0
  76. package/tests/logger.test.ts +6 -6
  77. package/tests/redisHelper.test.ts +6 -1
  78. package/tests/scanFiles-routePath.test.ts +46 -0
  79. package/tests/smoke-sql.test.ts +24 -0
  80. package/tests/sqlBuilder-advanced.test.ts +18 -5
  81. package/tests/sqlBuilder.test.ts +24 -0
  82. package/tests/sync-init-guard.test.ts +105 -0
  83. package/tests/syncApi-insBatch-fields-consistent.test.ts +61 -0
  84. package/tests/syncApi-obsolete-records.test.ts +69 -0
  85. package/tests/syncApi-type-compat.test.ts +72 -0
  86. package/tests/syncDev-permissions.test.ts +81 -0
  87. package/tests/syncMenu-disableMenus-hard-delete.test.ts +88 -0
  88. package/tests/syncMenu-duplicate-path.test.ts +122 -0
  89. package/tests/syncMenu-obsolete-records.test.ts +161 -0
  90. package/tests/syncMenu-parentPath-from-tree.test.ts +75 -0
  91. package/tests/syncMenu-paths.test.ts +0 -9
  92. package/tests/{syncDb-apply.test.ts → syncTable-apply.test.ts} +14 -24
  93. package/tests/{syncDb-array-number.test.ts → syncTable-array-number.test.ts} +31 -31
  94. package/tests/syncTable-constants.test.ts +101 -0
  95. package/tests/syncTable-db-integration.test.ts +237 -0
  96. package/tests/{syncDb-ddl.test.ts → syncTable-ddl.test.ts} +67 -53
  97. package/tests/{syncDb-helpers.test.ts → syncTable-helpers.test.ts} +12 -26
  98. package/tests/syncTable-schema.test.ts +99 -0
  99. package/tests/syncTable-testkit.test.ts +25 -0
  100. package/tests/syncTable-types.test.ts +122 -0
  101. package/tests/tableRef-and-deserialize.test.ts +67 -0
  102. package/tsconfig.json +1 -1
  103. package/types/api.d.ts +1 -1
  104. package/types/befly.d.ts +13 -12
  105. package/types/cache.d.ts +2 -2
  106. package/types/context.d.ts +1 -1
  107. package/types/database.d.ts +0 -5
  108. package/types/hook.d.ts +1 -10
  109. package/types/plugin.d.ts +2 -96
  110. package/types/sync.d.ts +19 -25
  111. package/utils/convertBigIntFields.ts +38 -0
  112. package/utils/disableMenusGlob.ts +85 -0
  113. package/utils/importDefault.ts +21 -0
  114. package/utils/isDirentDirectory.ts +23 -0
  115. package/utils/loadMenuConfigs.ts +145 -0
  116. package/utils/processFields.ts +25 -0
  117. package/utils/scanAddons.ts +72 -0
  118. package/utils/scanFiles.ts +129 -21
  119. package/utils/scanSources.ts +64 -0
  120. package/utils/sortModules.ts +137 -0
  121. package/checks/checkApp.ts +0 -55
  122. package/hooks/rateLimit.ts +0 -276
  123. package/sync/syncAll.ts +0 -35
  124. package/sync/syncDb/apply.ts +0 -192
  125. package/sync/syncDb/constants.ts +0 -119
  126. package/sync/syncDb/ddl.ts +0 -251
  127. package/sync/syncDb/helpers.ts +0 -84
  128. package/sync/syncDb/schema.ts +0 -202
  129. package/sync/syncDb/sqlite.ts +0 -48
  130. package/sync/syncDb/table.ts +0 -207
  131. package/sync/syncDb/tableCreate.ts +0 -163
  132. package/sync/syncDb/types.ts +0 -132
  133. package/sync/syncDb/version.ts +0 -69
  134. package/sync/syncDb.ts +0 -168
  135. package/tests/rateLimit-hook.test.ts +0 -477
  136. package/tests/syncDb-constants.test.ts +0 -130
  137. package/tests/syncDb-schema.test.ts +0 -179
  138. package/tests/syncDb-types.test.ts +0 -139
  139. package/utils/addonHelper.ts +0 -90
  140. package/utils/modules.ts +0 -98
  141. package/utils/route.ts +0 -23
@@ -0,0 +1,122 @@
1
+ import { describe, expect, test, beforeEach, afterEach, mock } from "bun:test";
2
+ import { mkdirSync, rmSync, writeFileSync } from "node:fs";
3
+ import { join } from "node:path";
4
+
5
+ import { checkMenu } from "../checks/checkMenu.js";
6
+ import { setMockLogger } from "../lib/logger.js";
7
+ import { syncMenu } from "../sync/syncMenu.js";
8
+
9
+ // Mock pino logger
10
+ const mockPino = {
11
+ info: mock(() => {}),
12
+ warn: mock(() => {}),
13
+ error: mock(() => {}),
14
+ debug: mock(() => {}),
15
+ fatal: mock(() => {}),
16
+ trace: mock(() => {}),
17
+ silent: mock(() => {}),
18
+ child: mock(() => mockPino),
19
+ level: "info"
20
+ };
21
+
22
+ describe("syncMenu - duplicate path records", () => {
23
+ beforeEach(() => {
24
+ setMockLogger(mockPino as any);
25
+ mockPino.warn.mockClear();
26
+ });
27
+
28
+ afterEach(() => {
29
+ setMockLogger(null);
30
+ });
31
+
32
+ test("应检测重复 path 并删除多余记录(保留 id 最大的一条)", async () => {
33
+ const originalCwd = process.cwd();
34
+ const projectDir = join(originalCwd, "temp", `syncMenu-duplicate-path-${Date.now()}-${Math.random().toString(16).slice(2)}`);
35
+ const menusJsonPath = join(projectDir, "menus.json");
36
+
37
+ const existingMenus = [
38
+ { id: 10, path: "/keep", parentPath: "", name: "Keep", sort: 999, state: 0 },
39
+ { id: 20, path: "/keep", parentPath: "", name: "Keep", sort: 999, state: 0 }
40
+ ];
41
+
42
+ const calls = {
43
+ delForceBatch: [] as any[],
44
+ updBatchCount: 0,
45
+ insBatchCount: 0
46
+ };
47
+
48
+ const dbHelper = {
49
+ tableExists: async () => true,
50
+ trans: async (callback: any) => {
51
+ return await callback(dbHelper);
52
+ },
53
+ getAll: async (options: any) => {
54
+ const stateGte = options?.where?.state$gte;
55
+ if (typeof stateGte === "number") {
56
+ return { lists: existingMenus.filter((m) => typeof m.state === "number" && m.state >= stateGte) };
57
+ }
58
+ return { lists: existingMenus };
59
+ },
60
+ updBatch: async () => {
61
+ calls.updBatchCount += 1;
62
+ return 0;
63
+ },
64
+ insBatch: async () => {
65
+ calls.insBatchCount += 1;
66
+ return [];
67
+ },
68
+ delForceBatch: async (table: string, ids: number[]) => {
69
+ calls.delForceBatch.push({ table: table, ids: ids });
70
+ return ids.length;
71
+ }
72
+ } as any;
73
+
74
+ const ctx = {
75
+ db: dbHelper,
76
+ addons: [],
77
+ config: {
78
+ disableMenus: []
79
+ },
80
+ cache: {
81
+ cacheMenus: async () => {}
82
+ }
83
+ } as any;
84
+
85
+ try {
86
+ mkdirSync(projectDir, { recursive: true });
87
+ process.chdir(projectDir);
88
+
89
+ writeFileSync(
90
+ menusJsonPath,
91
+ JSON.stringify(
92
+ [
93
+ {
94
+ name: "Keep",
95
+ path: "/keep",
96
+ sort: 999
97
+ }
98
+ ],
99
+ null,
100
+ 4
101
+ ),
102
+ { encoding: "utf8" }
103
+ );
104
+
105
+ const menus = await checkMenu(ctx.addons);
106
+ await syncMenu(ctx, menus);
107
+ } finally {
108
+ process.chdir(originalCwd);
109
+ rmSync(projectDir, { recursive: true, force: true });
110
+ }
111
+
112
+ expect(mockPino.warn).toHaveBeenCalledTimes(1);
113
+ expect(calls.updBatchCount).toBe(0);
114
+ expect(calls.insBatchCount).toBe(0);
115
+
116
+ expect(calls.delForceBatch).toHaveLength(1);
117
+ expect(calls.delForceBatch[0].table).toBe("addon_admin_menu");
118
+
119
+ // 只应删除较小 id 的重复记录,保留 id=20
120
+ expect(calls.delForceBatch[0].ids).toEqual([10]);
121
+ });
122
+ });
@@ -0,0 +1,161 @@
1
+ import { describe, expect, test } from "bun:test";
2
+ import { mkdirSync, rmSync, writeFileSync } from "node:fs";
3
+ import { join } from "node:path";
4
+
5
+ import { checkMenu } from "../checks/checkMenu.js";
6
+ import { syncMenu } from "../sync/syncMenu.js";
7
+
8
+ describe("syncMenu - delete obsolete records", () => {
9
+ test("应删除不在配置中的菜单记录(仅 state>=0)", async () => {
10
+ const originalCwd = process.cwd();
11
+ const projectDir = join(originalCwd, "temp", `syncMenu-obsolete-records-${Date.now()}-${Math.random().toString(16).slice(2)}`);
12
+ const menusJsonPath = join(projectDir, "menus.json");
13
+
14
+ const existingMenus = [
15
+ { id: 1, path: "/a", parentPath: "", state: 0 },
16
+ { id: 2, path: "/b", parentPath: "", state: -1 },
17
+ { id: 3, path: "", parentPath: "", state: 0 }
18
+ ];
19
+
20
+ const calls = {
21
+ getAllCount: 0,
22
+ delForceBatch: [] as any[]
23
+ };
24
+
25
+ const dbHelper = {
26
+ tableExists: async () => true,
27
+ trans: async (callback: any) => {
28
+ return await callback(dbHelper);
29
+ },
30
+ getAll: async (options: any) => {
31
+ calls.getAllCount += 1;
32
+ const stateGte = options?.where?.state$gte;
33
+ if (typeof stateGte === "number") {
34
+ return { lists: existingMenus.filter((m) => typeof m.state === "number" && m.state >= stateGte) };
35
+ }
36
+ return { lists: existingMenus };
37
+ },
38
+ updBatch: async () => {
39
+ return 0;
40
+ },
41
+ insBatch: async () => {
42
+ return [];
43
+ },
44
+ delForceBatch: async (table: string, ids: number[]) => {
45
+ calls.delForceBatch.push({ table: table, ids: ids });
46
+ return ids.length;
47
+ }
48
+ } as any;
49
+
50
+ const ctx = {
51
+ db: dbHelper,
52
+ addons: [],
53
+ config: {
54
+ disableMenus: []
55
+ },
56
+ cache: {
57
+ cacheMenus: async () => {}
58
+ }
59
+ } as any;
60
+
61
+ try {
62
+ mkdirSync(projectDir, { recursive: true });
63
+ process.chdir(projectDir);
64
+
65
+ writeFileSync(menusJsonPath, "[]", { encoding: "utf8" });
66
+ const menus = await checkMenu(ctx.addons);
67
+ await syncMenu(ctx, menus);
68
+ } finally {
69
+ process.chdir(originalCwd);
70
+ rmSync(projectDir, { recursive: true, force: true });
71
+ }
72
+
73
+ expect(calls.getAllCount).toBe(1);
74
+ expect(calls.delForceBatch).toHaveLength(1);
75
+ expect(calls.delForceBatch[0].table).toBe("addon_admin_menu");
76
+ expect(calls.delForceBatch[0].ids).toEqual([1]);
77
+ });
78
+
79
+ test("不应删除仍在配置中的菜单记录", async () => {
80
+ const originalCwd = process.cwd();
81
+ const projectDir = join(originalCwd, "temp", `syncMenu-obsolete-records-${Date.now()}-${Math.random().toString(16).slice(2)}`);
82
+ const menusJsonPath = join(projectDir, "menus.json");
83
+
84
+ const existingMenus = [
85
+ { id: 1, path: "/keep", parentPath: "", name: "Keep", sort: 999, state: 0 },
86
+ { id: 2, path: "/remove", parentPath: "", name: "Remove", sort: 999, state: 0 }
87
+ ];
88
+
89
+ const calls = {
90
+ delForceBatch: [] as any[],
91
+ updBatchCount: 0
92
+ };
93
+
94
+ const dbHelper = {
95
+ tableExists: async () => true,
96
+ trans: async (callback: any) => {
97
+ return await callback(dbHelper);
98
+ },
99
+ getAll: async (options: any) => {
100
+ const stateGte = options?.where?.state$gte;
101
+ if (typeof stateGte === "number") {
102
+ return { lists: existingMenus.filter((m) => typeof m.state === "number" && m.state >= stateGte) };
103
+ }
104
+ return { lists: existingMenus };
105
+ },
106
+ updBatch: async () => {
107
+ calls.updBatchCount += 1;
108
+ return 0;
109
+ },
110
+ insBatch: async () => {
111
+ return [];
112
+ },
113
+ delForceBatch: async (table: string, ids: number[]) => {
114
+ calls.delForceBatch.push({ table: table, ids: ids });
115
+ return ids.length;
116
+ }
117
+ } as any;
118
+
119
+ const ctx = {
120
+ db: dbHelper,
121
+ addons: [],
122
+ config: {
123
+ disableMenus: []
124
+ },
125
+ cache: {
126
+ cacheMenus: async () => {}
127
+ }
128
+ } as any;
129
+
130
+ try {
131
+ mkdirSync(projectDir, { recursive: true });
132
+ process.chdir(projectDir);
133
+
134
+ writeFileSync(
135
+ menusJsonPath,
136
+ JSON.stringify(
137
+ [
138
+ {
139
+ name: "Keep",
140
+ path: "/keep",
141
+ sort: 999
142
+ }
143
+ ],
144
+ null,
145
+ 4
146
+ ),
147
+ { encoding: "utf8" }
148
+ );
149
+ const menus = await checkMenu(ctx.addons);
150
+ await syncMenu(ctx, menus);
151
+ } finally {
152
+ process.chdir(originalCwd);
153
+ rmSync(projectDir, { recursive: true, force: true });
154
+ }
155
+
156
+ expect(calls.delForceBatch).toHaveLength(1);
157
+ expect(calls.delForceBatch[0].table).toBe("addon_admin_menu");
158
+ expect(calls.delForceBatch[0].ids).toEqual([2]);
159
+ expect(calls.updBatchCount).toBe(0);
160
+ });
161
+ });
@@ -0,0 +1,75 @@
1
+ import { describe, expect, test } from "bun:test";
2
+
3
+ import { __test__ } from "../sync/syncMenu.js";
4
+
5
+ describe("syncMenu - parentPath derived from tree", () => {
6
+ test("根级菜单不应强制按 URL path 推导 parentPath(避免把同级菜单挂到首页下面)", () => {
7
+ const mergedMenus: any[] = [
8
+ {
9
+ name: "首页",
10
+ path: "/addon/admin",
11
+ sort: 1
12
+ },
13
+ {
14
+ name: "日志管理",
15
+ path: "/addon/admin/log",
16
+ sort: 40
17
+ },
18
+ {
19
+ name: "配置管理",
20
+ path: "/addon/admin/config",
21
+ sort: 30
22
+ }
23
+ ];
24
+
25
+ const map = __test__.flattenMenusToDefMap(mergedMenus as any);
26
+
27
+ expect(map.get("/addon/admin")?.parentPath).toBe("");
28
+ expect(map.get("/addon/admin/log")?.parentPath).toBe("");
29
+ expect(map.get("/addon/admin/config")?.parentPath).toBe("");
30
+ });
31
+
32
+ test("子菜单应跟随 children 嵌套关系设置 parentPath", () => {
33
+ const mergedMenus: any[] = [
34
+ {
35
+ name: "日志管理",
36
+ path: "/addon/admin/log",
37
+ sort: 40,
38
+ children: [
39
+ {
40
+ name: "登录日志",
41
+ path: "/addon/admin/log/login",
42
+ sort: 1
43
+ }
44
+ ]
45
+ }
46
+ ];
47
+
48
+ const map = __test__.flattenMenusToDefMap(mergedMenus as any);
49
+
50
+ expect(map.get("/addon/admin/log")?.parentPath).toBe("");
51
+ expect(map.get("/addon/admin/log/login")?.parentPath).toBe("/addon/admin/log");
52
+ });
53
+
54
+ test("显式 parentPath(包括空字符串)优先生效", () => {
55
+ const mergedMenus: any[] = [
56
+ {
57
+ name: "自定义根",
58
+ path: "/x",
59
+ parentPath: "/custom",
60
+ sort: 1
61
+ },
62
+ {
63
+ name: "显式根",
64
+ path: "/y",
65
+ parentPath: "",
66
+ sort: 2
67
+ }
68
+ ];
69
+
70
+ const map = __test__.flattenMenusToDefMap(mergedMenus as any);
71
+
72
+ expect(map.get("/x")?.parentPath).toBe("/custom");
73
+ expect(map.get("/y")?.parentPath).toBe("");
74
+ });
75
+ });
@@ -57,12 +57,3 @@ describe("syncMenu - scanViewsDir paths", () => {
57
57
  expect(user?.sort).toBe(1);
58
58
  });
59
59
  });
60
-
61
- describe("syncMenu - normalizeMenuPath", () => {
62
- test("去掉尾随 / 且折叠多 /", () => {
63
- expect(__test__.normalizeMenuPath("/addon/a/")).toBe("/addon/a");
64
- expect(__test__.normalizeMenuPath("//addon//a//b/")).toBe("/addon/a/b");
65
- expect(__test__.normalizeMenuPath("addon/a")).toBe("/addon/a");
66
- expect(__test__.normalizeMenuPath("/")).toBe("/");
67
- });
68
- });
@@ -1,23 +1,13 @@
1
1
  /**
2
- * syncDb 变更应用模块测试
2
+ * syncTable 变更应用模块测试
3
3
  *
4
4
  * 测试 apply.ts 中的函数:
5
5
  * - compareFieldDefinition
6
6
  */
7
7
 
8
- import { describe, test, expect, beforeAll } from "bun:test";
8
+ import { describe, test, expect } from "bun:test";
9
9
 
10
- import { setDbType } from "../sync/syncDb/constants.js";
11
-
12
- // 设置数据库类型为 MySQL
13
- setDbType("mysql");
14
-
15
- let compareFieldDefinition: any;
16
-
17
- beforeAll(async () => {
18
- const apply = await import("../sync/syncDb/apply.js");
19
- compareFieldDefinition = apply.compareFieldDefinition;
20
- });
10
+ import { syncTable } from "../sync/syncTable.js";
21
11
 
22
12
  describe("compareFieldDefinition", () => {
23
13
  describe("长度变化检测", () => {
@@ -37,7 +27,7 @@ describe("compareFieldDefinition", () => {
37
27
  default: null
38
28
  };
39
29
 
40
- const changes = compareFieldDefinition(existingColumn, fieldDef);
30
+ const changes = syncTable.TestKit.compareFieldDefinition("mysql", existingColumn as any, fieldDef as any);
41
31
  const lengthChange = changes.find((c: any) => c.type === "length");
42
32
 
43
33
  expect(lengthChange).toBeDefined();
@@ -61,7 +51,7 @@ describe("compareFieldDefinition", () => {
61
51
  default: null
62
52
  };
63
53
 
64
- const changes = compareFieldDefinition(existingColumn, fieldDef);
54
+ const changes = syncTable.TestKit.compareFieldDefinition("mysql", existingColumn as any, fieldDef as any);
65
55
  const lengthChange = changes.find((c: any) => c.type === "length");
66
56
 
67
57
  expect(lengthChange).toBeUndefined();
@@ -85,7 +75,7 @@ describe("compareFieldDefinition", () => {
85
75
  default: null
86
76
  };
87
77
 
88
- const changes = compareFieldDefinition(existingColumn, fieldDef);
78
+ const changes = syncTable.TestKit.compareFieldDefinition("mysql", existingColumn as any, fieldDef as any);
89
79
  const commentChange = changes.find((c: any) => c.type === "comment");
90
80
 
91
81
  expect(commentChange).toBeDefined();
@@ -109,7 +99,7 @@ describe("compareFieldDefinition", () => {
109
99
  default: null
110
100
  };
111
101
 
112
- const changes = compareFieldDefinition(existingColumn, fieldDef);
102
+ const changes = syncTable.TestKit.compareFieldDefinition("mysql", existingColumn as any, fieldDef as any);
113
103
  const commentChange = changes.find((c: any) => c.type === "comment");
114
104
 
115
105
  expect(commentChange).toBeUndefined();
@@ -133,7 +123,7 @@ describe("compareFieldDefinition", () => {
133
123
  default: null
134
124
  };
135
125
 
136
- const changes = compareFieldDefinition(existingColumn, fieldDef);
126
+ const changes = syncTable.TestKit.compareFieldDefinition("mysql", existingColumn as any, fieldDef as any);
137
127
  const typeChange = changes.find((c: any) => c.type === "datatype");
138
128
 
139
129
  expect(typeChange).toBeDefined();
@@ -157,7 +147,7 @@ describe("compareFieldDefinition", () => {
157
147
  default: 0
158
148
  };
159
149
 
160
- const changes = compareFieldDefinition(existingColumn, fieldDef);
150
+ const changes = syncTable.TestKit.compareFieldDefinition("mysql", existingColumn as any, fieldDef as any);
161
151
  const typeChange = changes.find((c: any) => c.type === "datatype");
162
152
 
163
153
  expect(typeChange).toBeUndefined();
@@ -181,7 +171,7 @@ describe("compareFieldDefinition", () => {
181
171
  default: null
182
172
  };
183
173
 
184
- const changes = compareFieldDefinition(existingColumn, fieldDef);
174
+ const changes = syncTable.TestKit.compareFieldDefinition("mysql", existingColumn as any, fieldDef as any);
185
175
  const nullableChange = changes.find((c: any) => c.type === "nullable");
186
176
 
187
177
  expect(nullableChange).toBeDefined();
@@ -207,7 +197,7 @@ describe("compareFieldDefinition", () => {
207
197
  default: "new"
208
198
  };
209
199
 
210
- const changes = compareFieldDefinition(existingColumn, fieldDef);
200
+ const changes = syncTable.TestKit.compareFieldDefinition("mysql", existingColumn as any, fieldDef as any);
211
201
  const defaultChange = changes.find((c: any) => c.type === "default");
212
202
 
213
203
  expect(defaultChange).toBeDefined();
@@ -231,7 +221,7 @@ describe("compareFieldDefinition", () => {
231
221
  default: null // null 会被解析为空字符串
232
222
  };
233
223
 
234
- const changes = compareFieldDefinition(existingColumn, fieldDef);
224
+ const changes = syncTable.TestKit.compareFieldDefinition("mysql", existingColumn as any, fieldDef as any);
235
225
  const defaultChange = changes.find((c: any) => c.type === "default");
236
226
 
237
227
  // null -> '' (空字符串),与现有值相同,无变化
@@ -256,7 +246,7 @@ describe("compareFieldDefinition", () => {
256
246
  default: "new"
257
247
  };
258
248
 
259
- const changes = compareFieldDefinition(existingColumn, fieldDef);
249
+ const changes = syncTable.TestKit.compareFieldDefinition("mysql", existingColumn as any, fieldDef as any);
260
250
 
261
251
  expect(changes.length).toBe(4); // length, comment, nullable, default
262
252
  expect(changes.some((c: any) => c.type === "length")).toBe(true);
@@ -281,7 +271,7 @@ describe("compareFieldDefinition", () => {
281
271
  default: null
282
272
  };
283
273
 
284
- const changes = compareFieldDefinition(existingColumn, fieldDef);
274
+ const changes = syncTable.TestKit.compareFieldDefinition("mysql", existingColumn as any, fieldDef as any);
285
275
 
286
276
  expect(changes.length).toBe(0);
287
277
  });
@@ -1,37 +1,37 @@
1
1
  /**
2
- * 测试 syncDb 对 array_number_string 和 array_number_text 类型的支持
2
+ * 测试 syncTable 对 array_number_string 和 array_number_text 类型的支持
3
3
  */
4
4
 
5
5
  import { describe, expect, test } from "bun:test";
6
6
 
7
- import { getSqlType, resolveDefaultValue, generateDefaultSql, isStringOrArrayType } from "../sync/syncDb/types.js";
7
+ import { syncTable } from "../sync/syncTable.js";
8
8
 
9
- describe("syncDb - array_number 类型支持", () => {
9
+ describe("syncTable - array_number 类型支持", () => {
10
10
  // ==================== 类型判断测试 ====================
11
11
 
12
12
  test("isStringOrArrayType: array_number_string 需要长度", () => {
13
- expect(isStringOrArrayType("array_number_string")).toBe(true);
13
+ expect(syncTable.TestKit.isStringOrArrayType("array_number_string")).toBe(true);
14
14
  });
15
15
 
16
16
  test("isStringOrArrayType: array_number_text 不需要长度", () => {
17
- expect(isStringOrArrayType("array_number_text")).toBe(false);
17
+ expect(syncTable.TestKit.isStringOrArrayType("array_number_text")).toBe(false);
18
18
  });
19
19
 
20
20
  // ==================== SQL 类型映射测试 ====================
21
21
 
22
22
  test("getSqlType: array_number_string 生成 VARCHAR(max)", () => {
23
- const sqlType = getSqlType("array_number_string", 500);
23
+ const sqlType = syncTable.TestKit.getSqlType("mysql", "array_number_string", 500);
24
24
  expect(sqlType).toMatch(/VARCHAR\(500\)/i);
25
25
  });
26
26
 
27
27
  test("getSqlType: array_number_text 生成 TEXT/MEDIUMTEXT", () => {
28
- const sqlType = getSqlType("array_number_text", null);
28
+ const sqlType = syncTable.TestKit.getSqlType("mysql", "array_number_text", null);
29
29
  expect(sqlType).toMatch(/TEXT/i);
30
30
  });
31
31
 
32
32
  test("getSqlType: array_number_string 使用 max 参数", () => {
33
- const sqlType1 = getSqlType("array_number_string", 200);
34
- const sqlType2 = getSqlType("array_number_string", 1000);
33
+ const sqlType1 = syncTable.TestKit.getSqlType("mysql", "array_number_string", 200);
34
+ const sqlType2 = syncTable.TestKit.getSqlType("mysql", "array_number_string", 1000);
35
35
 
36
36
  expect(sqlType1).toMatch(/VARCHAR\(200\)/i);
37
37
  expect(sqlType2).toMatch(/VARCHAR\(1000\)/i);
@@ -40,28 +40,28 @@ describe("syncDb - array_number 类型支持", () => {
40
40
  // ==================== 默认值处理测试 ====================
41
41
 
42
42
  test('resolveDefaultValue: array_number_string null 时返回 "[]"', () => {
43
- const result = resolveDefaultValue(null, "array_number_string");
43
+ const result = syncTable.TestKit.resolveDefaultValue(null, "array_number_string");
44
44
  expect(result).toBe("[]");
45
45
  });
46
46
 
47
47
  test("resolveDefaultValue: array_number_string 有默认值时保留", () => {
48
- const result = resolveDefaultValue("[1,2,3]", "array_number_string");
48
+ const result = syncTable.TestKit.resolveDefaultValue("[1,2,3]", "array_number_string");
49
49
  expect(result).toBe("[1,2,3]");
50
50
  });
51
51
 
52
52
  test('resolveDefaultValue: array_number_text null 时返回 "null"', () => {
53
- const result = resolveDefaultValue(null, "array_number_text");
53
+ const result = syncTable.TestKit.resolveDefaultValue(null, "array_number_text");
54
54
  expect(result).toBe("null");
55
55
  });
56
56
 
57
57
  test("resolveDefaultValue: array_number_text 有默认值时保留", () => {
58
- const result = resolveDefaultValue("[100,200]", "array_number_text");
58
+ const result = syncTable.TestKit.resolveDefaultValue("[100,200]", "array_number_text");
59
59
  expect(result).toBe("[100,200]");
60
60
  });
61
61
 
62
62
  test('resolveDefaultValue: 字符串 "null" 也视为 null', () => {
63
- const result1 = resolveDefaultValue("null", "array_number_string");
64
- const result2 = resolveDefaultValue("null", "array_number_text");
63
+ const result1 = syncTable.TestKit.resolveDefaultValue("null", "array_number_string");
64
+ const result2 = syncTable.TestKit.resolveDefaultValue("null", "array_number_text");
65
65
 
66
66
  expect(result1).toBe("[]");
67
67
  expect(result2).toBe("null");
@@ -70,29 +70,29 @@ describe("syncDb - array_number 类型支持", () => {
70
70
  // ==================== SQL DEFAULT 子句测试 ====================
71
71
 
72
72
  test("generateDefaultSql: array_number_string 生成 DEFAULT 子句", () => {
73
- const sql = generateDefaultSql("[]", "array_number_string");
73
+ const sql = syncTable.TestKit.generateDefaultSql("[]", "array_number_string");
74
74
  expect(sql).toBe(" DEFAULT '[]'");
75
75
  });
76
76
 
77
77
  test("generateDefaultSql: array_number_string 自定义默认值", () => {
78
- const sql = generateDefaultSql("[10,20,30]", "array_number_string");
78
+ const sql = syncTable.TestKit.generateDefaultSql("[10,20,30]", "array_number_string");
79
79
  expect(sql).toBe(" DEFAULT '[10,20,30]'");
80
80
  });
81
81
 
82
82
  test("generateDefaultSql: array_number_text 不生成 DEFAULT", () => {
83
- const sql = generateDefaultSql("[]", "array_number_text");
83
+ const sql = syncTable.TestKit.generateDefaultSql("[]", "array_number_text");
84
84
  expect(sql).toBe("");
85
85
  });
86
86
 
87
87
  test("generateDefaultSql: array_number_text null 时不生成 DEFAULT", () => {
88
- const sql = generateDefaultSql("null", "array_number_text");
88
+ const sql = syncTable.TestKit.generateDefaultSql("null", "array_number_text");
89
89
  expect(sql).toBe("");
90
90
  });
91
91
 
92
92
  // ==================== 单引号转义测试 ====================
93
93
 
94
94
  test("generateDefaultSql: 默认值包含单引号时正确转义", () => {
95
- const sql = generateDefaultSql("[1,'test',2]", "array_number_string");
95
+ const sql = syncTable.TestKit.generateDefaultSql("[1,'test',2]", "array_number_string");
96
96
  expect(sql).toBe(" DEFAULT '[1,''test'',2]'");
97
97
  });
98
98
 
@@ -105,18 +105,18 @@ describe("syncDb - array_number 类型支持", () => {
105
105
  const fieldDefault = null;
106
106
 
107
107
  // 1. 判断是否需要长度
108
- expect(isStringOrArrayType(fieldType)).toBe(true);
108
+ expect(syncTable.TestKit.isStringOrArrayType(fieldType)).toBe(true);
109
109
 
110
110
  // 2. 获取 SQL 类型
111
- const sqlType = getSqlType(fieldType, fieldMax);
111
+ const sqlType = syncTable.TestKit.getSqlType("mysql", fieldType, fieldMax);
112
112
  expect(sqlType).toMatch(/VARCHAR\(500\)/i);
113
113
 
114
114
  // 3. 处理默认值
115
- const actualDefault = resolveDefaultValue(fieldDefault, fieldType);
115
+ const actualDefault = syncTable.TestKit.resolveDefaultValue(fieldDefault, fieldType);
116
116
  expect(actualDefault).toBe("[]");
117
117
 
118
118
  // 4. 生成 DEFAULT 子句
119
- const defaultSql = generateDefaultSql(actualDefault, fieldType);
119
+ const defaultSql = syncTable.TestKit.generateDefaultSql(actualDefault, fieldType);
120
120
  expect(defaultSql).toBe(" DEFAULT '[]'");
121
121
  });
122
122
 
@@ -127,18 +127,18 @@ describe("syncDb - array_number 类型支持", () => {
127
127
  const fieldDefault = null;
128
128
 
129
129
  // 1. 判断是否需要长度
130
- expect(isStringOrArrayType(fieldType)).toBe(false);
130
+ expect(syncTable.TestKit.isStringOrArrayType(fieldType)).toBe(false);
131
131
 
132
132
  // 2. 获取 SQL 类型
133
- const sqlType = getSqlType(fieldType, fieldMax);
133
+ const sqlType = syncTable.TestKit.getSqlType("mysql", fieldType, fieldMax);
134
134
  expect(sqlType).toMatch(/TEXT/i);
135
135
 
136
136
  // 3. 处理默认值
137
- const actualDefault = resolveDefaultValue(fieldDefault, fieldType);
137
+ const actualDefault = syncTable.TestKit.resolveDefaultValue(fieldDefault, fieldType);
138
138
  expect(actualDefault).toBe("null");
139
139
 
140
140
  // 4. 生成 DEFAULT 子句(TEXT 类型不支持)
141
- const defaultSql = generateDefaultSql(actualDefault, fieldType);
141
+ const defaultSql = syncTable.TestKit.generateDefaultSql(actualDefault, fieldType);
142
142
  expect(defaultSql).toBe("");
143
143
  });
144
144
 
@@ -148,13 +148,13 @@ describe("syncDb - array_number 类型支持", () => {
148
148
  const fieldMax = 10;
149
149
  const fieldDefault = "[60,70,80]";
150
150
 
151
- const sqlType = getSqlType(fieldType, fieldMax);
151
+ const sqlType = syncTable.TestKit.getSqlType("mysql", fieldType, fieldMax);
152
152
  expect(sqlType).toMatch(/VARCHAR\(10\)/i);
153
153
 
154
- const actualDefault = resolveDefaultValue(fieldDefault, fieldType);
154
+ const actualDefault = syncTable.TestKit.resolveDefaultValue(fieldDefault, fieldType);
155
155
  expect(actualDefault).toBe("[60,70,80]");
156
156
 
157
- const defaultSql = generateDefaultSql(actualDefault, fieldType);
157
+ const defaultSql = syncTable.TestKit.generateDefaultSql(actualDefault, fieldType);
158
158
  expect(defaultSql).toBe(" DEFAULT '[60,70,80]'");
159
159
  });
160
160
  });