befly 3.9.40 → 3.10.1

This diff represents the content of publicly available package versions that have been released to one of the supported registries. The information contained in this diff is provided for informational purposes only and reflects changes between package versions as they appear in their respective public registries.
Files changed (144) hide show
  1. package/README.md +47 -19
  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 +17 -11
  9. package/docs/api/api.md +16 -2
  10. package/docs/guide/quickstart.md +31 -10
  11. package/docs/hooks/hook.md +2 -2
  12. package/docs/hooks/rateLimit.md +1 -1
  13. package/docs/infra/redis.md +26 -14
  14. package/docs/plugins/plugin.md +23 -21
  15. package/docs/quickstart.md +5 -328
  16. package/docs/reference/addon.md +0 -4
  17. package/docs/reference/config.md +14 -31
  18. package/docs/reference/logger.md +3 -3
  19. package/docs/reference/sync.md +132 -237
  20. package/docs/reference/table.md +28 -30
  21. package/hooks/auth.ts +3 -4
  22. package/hooks/cors.ts +4 -6
  23. package/hooks/parser.ts +3 -4
  24. package/hooks/permission.ts +3 -4
  25. package/hooks/validator.ts +3 -4
  26. package/lib/cacheHelper.ts +89 -153
  27. package/lib/cacheKeys.ts +1 -1
  28. package/lib/connect.ts +9 -13
  29. package/lib/dbDialect.ts +285 -0
  30. package/lib/dbHelper.ts +179 -507
  31. package/lib/dbUtils.ts +450 -0
  32. package/lib/logger.ts +41 -5
  33. package/lib/redisHelper.ts +1 -0
  34. package/lib/sqlBuilder.ts +358 -58
  35. package/lib/sqlCheck.ts +136 -0
  36. package/lib/validator.ts +1 -1
  37. package/loader/loadApis.ts +23 -126
  38. package/loader/loadHooks.ts +31 -46
  39. package/loader/loadPlugins.ts +37 -52
  40. package/main.ts +58 -19
  41. package/package.json +24 -25
  42. package/paths.ts +14 -14
  43. package/plugins/cache.ts +12 -6
  44. package/plugins/cipher.ts +2 -2
  45. package/plugins/config.ts +6 -8
  46. package/plugins/db.ts +14 -19
  47. package/plugins/jwt.ts +6 -7
  48. package/plugins/logger.ts +7 -9
  49. package/plugins/redis.ts +8 -10
  50. package/plugins/tool.ts +3 -4
  51. package/router/api.ts +3 -2
  52. package/router/static.ts +7 -5
  53. package/sync/syncApi.ts +80 -235
  54. package/sync/syncCache.ts +16 -0
  55. package/sync/syncDev.ts +167 -202
  56. package/sync/syncMenu.ts +230 -444
  57. package/sync/syncTable.ts +1247 -0
  58. package/tests/_mocks/mockSqliteDb.ts +204 -0
  59. package/tests/addonHelper-cache.test.ts +32 -0
  60. package/tests/apiHandler-routePath-only.test.ts +32 -0
  61. package/tests/cacheHelper.test.ts +16 -51
  62. package/tests/checkApi-routePath-strict.test.ts +166 -0
  63. package/tests/checkMenu.test.ts +346 -0
  64. package/tests/checkTable-smoke.test.ts +157 -0
  65. package/tests/dbDialect-cache.test.ts +23 -0
  66. package/tests/dbDialect.test.ts +46 -0
  67. package/tests/dbHelper-advanced.test.ts +1 -1
  68. package/tests/dbHelper-all-array-types.test.ts +15 -15
  69. package/tests/dbHelper-batch-write.test.ts +90 -0
  70. package/tests/dbHelper-columns.test.ts +36 -54
  71. package/tests/dbHelper-execute.test.ts +26 -26
  72. package/tests/dbHelper-joins.test.ts +85 -176
  73. package/tests/fixtures/scanFilesAddon/node_modules/@befly-addon/demo/apis/sub/b.ts +3 -0
  74. package/tests/fixtures/scanFilesApis/a.ts +3 -0
  75. package/tests/fixtures/scanFilesApis/sub/b.ts +3 -0
  76. package/tests/loadPlugins-order-smoke.test.ts +75 -0
  77. package/tests/logger.test.ts +6 -6
  78. package/tests/redisHelper.test.ts +6 -1
  79. package/tests/scanFiles-routePath.test.ts +46 -0
  80. package/tests/smoke-sql.test.ts +24 -0
  81. package/tests/sqlBuilder-advanced.test.ts +18 -5
  82. package/tests/sqlBuilder.test.ts +24 -0
  83. package/tests/sync-init-guard.test.ts +105 -0
  84. package/tests/syncApi-insBatch-fields-consistent.test.ts +61 -0
  85. package/tests/syncApi-obsolete-records.test.ts +69 -0
  86. package/tests/syncApi-type-compat.test.ts +72 -0
  87. package/tests/syncDev-permissions.test.ts +81 -0
  88. package/tests/syncMenu-disableMenus-hard-delete.test.ts +88 -0
  89. package/tests/syncMenu-duplicate-path.test.ts +122 -0
  90. package/tests/syncMenu-obsolete-records.test.ts +161 -0
  91. package/tests/syncMenu-parentPath-from-tree.test.ts +75 -0
  92. package/tests/syncMenu-paths.test.ts +0 -9
  93. package/tests/{syncDb-apply.test.ts → syncTable-apply.test.ts} +14 -24
  94. package/tests/{syncDb-array-number.test.ts → syncTable-array-number.test.ts} +31 -31
  95. package/tests/syncTable-constants.test.ts +101 -0
  96. package/tests/syncTable-db-integration.test.ts +237 -0
  97. package/tests/{syncDb-ddl.test.ts → syncTable-ddl.test.ts} +67 -53
  98. package/tests/{syncDb-helpers.test.ts → syncTable-helpers.test.ts} +12 -26
  99. package/tests/syncTable-schema.test.ts +99 -0
  100. package/tests/syncTable-testkit.test.ts +25 -0
  101. package/tests/syncTable-types.test.ts +122 -0
  102. package/tests/tableRef-and-deserialize.test.ts +67 -0
  103. package/tsconfig.json +1 -1
  104. package/types/api.d.ts +1 -1
  105. package/types/befly.d.ts +13 -12
  106. package/types/cache.d.ts +2 -2
  107. package/types/context.d.ts +1 -1
  108. package/types/database.d.ts +0 -5
  109. package/types/hook.d.ts +1 -10
  110. package/types/plugin.d.ts +2 -96
  111. package/types/sync.d.ts +19 -25
  112. package/utils/convertBigIntFields.ts +38 -0
  113. package/utils/disableMenusGlob.ts +85 -0
  114. package/utils/importDefault.ts +21 -0
  115. package/utils/isDirentDirectory.ts +23 -0
  116. package/utils/loadMenuConfigs.ts +145 -0
  117. package/utils/processFields.ts +25 -0
  118. package/utils/scanAddons.ts +72 -0
  119. package/utils/scanFiles.ts +129 -21
  120. package/utils/scanSources.ts +64 -0
  121. package/utils/sortModules.ts +137 -0
  122. package/checks/checkApp.ts +0 -55
  123. package/docs/cipher.md +0 -582
  124. package/docs/database.md +0 -1176
  125. package/hooks/rateLimit.ts +0 -276
  126. package/sync/syncAll.ts +0 -35
  127. package/sync/syncDb/apply.ts +0 -192
  128. package/sync/syncDb/constants.ts +0 -119
  129. package/sync/syncDb/ddl.ts +0 -251
  130. package/sync/syncDb/helpers.ts +0 -84
  131. package/sync/syncDb/schema.ts +0 -202
  132. package/sync/syncDb/sqlite.ts +0 -48
  133. package/sync/syncDb/table.ts +0 -207
  134. package/sync/syncDb/tableCreate.ts +0 -163
  135. package/sync/syncDb/types.ts +0 -132
  136. package/sync/syncDb/version.ts +0 -69
  137. package/sync/syncDb.ts +0 -168
  138. package/tests/rateLimit-hook.test.ts +0 -477
  139. package/tests/syncDb-constants.test.ts +0 -130
  140. package/tests/syncDb-schema.test.ts +0 -179
  141. package/tests/syncDb-types.test.ts +0 -139
  142. package/utils/addonHelper.ts +0 -90
  143. package/utils/modules.ts +0 -98
  144. package/utils/route.ts +0 -23
@@ -0,0 +1,101 @@
1
+ /**
2
+ * syncTable 常量模块测试
3
+ *
4
+ * 测试 constants.ts 中的常量:
5
+ * - DB_VERSION_REQUIREMENTS
6
+ * - SYSTEM_INDEX_FIELDS
7
+ * - SYSTEM_INDEX_FIELDS
8
+ * - CHANGE_TYPE_LABELS
9
+ * - MYSQL_TABLE_CONFIG
10
+ * - typeMapping
11
+ */
12
+
13
+ import { describe, test, expect } from "bun:test";
14
+
15
+ import { syncTable } from "../sync/syncTable.js";
16
+
17
+ describe("DB_VERSION_REQUIREMENTS", () => {
18
+ test("MySQL 最低版本为 8", () => {
19
+ expect(syncTable.TestKit.DB_VERSION_REQUIREMENTS.MYSQL_MIN_MAJOR).toBe(8);
20
+ });
21
+
22
+ test("PostgreSQL 最低版本为 17", () => {
23
+ expect(syncTable.TestKit.DB_VERSION_REQUIREMENTS.POSTGRES_MIN_MAJOR).toBe(17);
24
+ });
25
+
26
+ test("SQLite 最低版本为 3.50.0", () => {
27
+ expect(syncTable.TestKit.DB_VERSION_REQUIREMENTS.SQLITE_MIN_VERSION).toBe("3.50.0");
28
+ });
29
+ });
30
+
31
+ describe("SYSTEM_INDEX_FIELDS", () => {
32
+ test("包含 created_at", () => {
33
+ expect(syncTable.TestKit.SYSTEM_INDEX_FIELDS).toContain("created_at");
34
+ });
35
+
36
+ test("包含 updated_at", () => {
37
+ expect(syncTable.TestKit.SYSTEM_INDEX_FIELDS).toContain("updated_at");
38
+ });
39
+
40
+ test("包含 state", () => {
41
+ expect(syncTable.TestKit.SYSTEM_INDEX_FIELDS).toContain("state");
42
+ });
43
+
44
+ test("共 3 个系统索引字段", () => {
45
+ expect(syncTable.TestKit.SYSTEM_INDEX_FIELDS.length).toBe(3);
46
+ });
47
+ });
48
+
49
+ describe("CHANGE_TYPE_LABELS", () => {
50
+ test('length 对应 "长度"', () => {
51
+ expect((syncTable.TestKit.CHANGE_TYPE_LABELS as any).length).toBe("长度");
52
+ });
53
+
54
+ test('datatype 对应 "类型"', () => {
55
+ expect(syncTable.TestKit.CHANGE_TYPE_LABELS.datatype).toBe("类型");
56
+ });
57
+
58
+ test('comment 对应 "注释"', () => {
59
+ expect(syncTable.TestKit.CHANGE_TYPE_LABELS.comment).toBe("注释");
60
+ });
61
+
62
+ test('default 对应 "默认值"', () => {
63
+ expect(syncTable.TestKit.CHANGE_TYPE_LABELS.default).toBe("默认值");
64
+ });
65
+ });
66
+
67
+ describe("MYSQL_TABLE_CONFIG", () => {
68
+ test("ENGINE 为 InnoDB", () => {
69
+ expect(syncTable.TestKit.MYSQL_TABLE_CONFIG.ENGINE).toBe("InnoDB");
70
+ });
71
+
72
+ test("CHARSET 为 utf8mb4", () => {
73
+ expect(syncTable.TestKit.MYSQL_TABLE_CONFIG.CHARSET).toBe("utf8mb4");
74
+ });
75
+
76
+ test("COLLATE 为 utf8mb4_0900_ai_ci", () => {
77
+ expect(syncTable.TestKit.MYSQL_TABLE_CONFIG.COLLATE).toBe("utf8mb4_0900_ai_ci");
78
+ });
79
+ });
80
+
81
+ describe("getTypeMapping (MySQL)", () => {
82
+ test("number 映射为 BIGINT", () => {
83
+ expect(syncTable.TestKit.getTypeMapping("mysql").number).toBe("BIGINT");
84
+ });
85
+
86
+ test("string 映射为 VARCHAR", () => {
87
+ expect(syncTable.TestKit.getTypeMapping("mysql").string).toBe("VARCHAR");
88
+ });
89
+
90
+ test("text 映射为 MEDIUMTEXT", () => {
91
+ expect(syncTable.TestKit.getTypeMapping("mysql").text).toBe("MEDIUMTEXT");
92
+ });
93
+
94
+ test("array_string 映射为 VARCHAR", () => {
95
+ expect(syncTable.TestKit.getTypeMapping("mysql").array_string).toBe("VARCHAR");
96
+ });
97
+
98
+ test("array_text 映射为 MEDIUMTEXT", () => {
99
+ expect(syncTable.TestKit.getTypeMapping("mysql").array_text).toBe("MEDIUMTEXT");
100
+ });
101
+ });
@@ -0,0 +1,237 @@
1
+ /**
2
+ * syncTable 端到端行为测试(纯 mock,不连接真实数据库)
3
+ */
4
+
5
+ import type { FieldDefinition } from "../types/validate.js";
6
+ import type { ScanFileResult } from "../utils/scanFiles.js";
7
+ import type { MockSqliteState } from "./_mocks/mockSqliteDb.js";
8
+
9
+ import { describe, expect, test } from "bun:test";
10
+
11
+ import { CacheKeys } from "../lib/cacheKeys.js";
12
+ import { syncTable } from "../sync/syncTable.js";
13
+ import { createMockSqliteDb } from "./_mocks/mockSqliteDb.js";
14
+
15
+ function buildTableItem(options: { tableFileName: string; content: any }): ScanFileResult {
16
+ return {
17
+ source: "app",
18
+ type: "table",
19
+ sourceName: "项目",
20
+ filePath: "",
21
+ relativePath: options.tableFileName,
22
+ fileName: options.tableFileName,
23
+ moduleName: `app_${options.tableFileName}`,
24
+ addonName: "",
25
+ fileBaseName: "",
26
+ fileDir: "",
27
+ content: options.content
28
+ } as any;
29
+ }
30
+
31
+ function fdString(options: { name: string; min: number; max: number; defaultValue: any; nullable: boolean }): FieldDefinition {
32
+ return {
33
+ name: options.name,
34
+ type: "string",
35
+ min: options.min,
36
+ max: options.max,
37
+ default: options.defaultValue,
38
+ nullable: options.nullable
39
+ } as any;
40
+ }
41
+
42
+ function fdNumber(options: { name: string; min: number; max: number; defaultValue: any; nullable: boolean }): FieldDefinition {
43
+ return {
44
+ name: options.name,
45
+ type: "number",
46
+ min: options.min,
47
+ max: options.max,
48
+ default: options.defaultValue,
49
+ nullable: options.nullable
50
+ } as any;
51
+ }
52
+
53
+ function fdText(options: { name: string; min: number; max: number; defaultValue: any; nullable: boolean }): FieldDefinition {
54
+ return {
55
+ name: options.name,
56
+ type: "text",
57
+ min: options.min,
58
+ max: options.max,
59
+ default: options.defaultValue,
60
+ nullable: options.nullable
61
+ } as any;
62
+ }
63
+
64
+ describe("syncTable(ctx, items) - mock sqlite", () => {
65
+ test("首次同步:应创建表并包含系统字段 + 业务字段,同时清理 columns 缓存", async () => {
66
+ const state: MockSqliteState = {
67
+ executedSql: [],
68
+ tables: {}
69
+ };
70
+
71
+ const db = createMockSqliteDb(state);
72
+
73
+ const redisCalls: Array<{ keys: string[] }> = [];
74
+ const ctx = {
75
+ db: db,
76
+ redis: {
77
+ delBatch: async (keys: string[]) => {
78
+ redisCalls.push({ keys: keys });
79
+ return keys.length;
80
+ }
81
+ },
82
+ config: {
83
+ db: { type: "sqlite", database: "" }
84
+ }
85
+ } as any;
86
+
87
+ const tableFileName = "test_sync_table_integration_user";
88
+ const item = buildTableItem({
89
+ tableFileName: tableFileName,
90
+ content: {
91
+ email: fdString({ name: "邮箱", min: 0, max: 100, defaultValue: null, nullable: false }),
92
+ nickname: fdString({ name: "昵称", min: 0, max: 50, defaultValue: "用户", nullable: true }),
93
+ age: fdNumber({ name: "年龄", min: 0, max: 999, defaultValue: 0, nullable: true })
94
+ }
95
+ });
96
+
97
+ await syncTable(ctx, [item]);
98
+
99
+ expect(state.executedSql.some((s) => s.includes("CREATE TABLE") && s.includes(tableFileName))).toBe(true);
100
+
101
+ const runtime = syncTable.TestKit.createRuntime("sqlite", db as any, "");
102
+ const exists = await syncTable.TestKit.tableExistsRuntime(runtime, tableFileName);
103
+ expect(exists).toBe(true);
104
+
105
+ const columns = await syncTable.TestKit.getTableColumnsRuntime(runtime, tableFileName);
106
+
107
+ expect(columns.id).toBeDefined();
108
+ expect(columns.created_at).toBeDefined();
109
+ expect(columns.updated_at).toBeDefined();
110
+ expect(columns.state).toBeDefined();
111
+
112
+ expect(columns.email).toBeDefined();
113
+ expect(columns.nickname).toBeDefined();
114
+ expect(columns.age).toBeDefined();
115
+
116
+ expect(redisCalls.length).toBe(1);
117
+ expect(redisCalls[0].keys).toEqual([CacheKeys.tableColumns(tableFileName)]);
118
+ });
119
+
120
+ test("二次同步:新增字段应落库(ADD COLUMN),同时清理 columns 缓存", async () => {
121
+ const state: MockSqliteState = {
122
+ executedSql: [],
123
+ tables: {}
124
+ };
125
+
126
+ const db = createMockSqliteDb(state);
127
+
128
+ const redisCalls: Array<{ keys: string[] }> = [];
129
+ const ctx = {
130
+ db: db,
131
+ redis: {
132
+ delBatch: async (keys: string[]) => {
133
+ redisCalls.push({ keys: keys });
134
+ return keys.length;
135
+ }
136
+ },
137
+ config: {
138
+ db: { type: "sqlite", database: "" }
139
+ }
140
+ } as any;
141
+
142
+ const tableFileName = "test_sync_table_integration_profile";
143
+
144
+ const itemV1 = buildTableItem({
145
+ tableFileName: tableFileName,
146
+ content: {
147
+ nickname: fdString({ name: "昵称", min: 0, max: 50, defaultValue: "用户", nullable: true })
148
+ }
149
+ });
150
+
151
+ await syncTable(ctx, [itemV1]);
152
+
153
+ const itemV2 = buildTableItem({
154
+ tableFileName: tableFileName,
155
+ content: {
156
+ nickname: fdString({ name: "昵称", min: 0, max: 50, defaultValue: "用户", nullable: true }),
157
+ bio: fdText({ name: "简介", min: 0, max: 200, defaultValue: null, nullable: true })
158
+ }
159
+ });
160
+
161
+ await syncTable(ctx, [itemV2]);
162
+
163
+ expect(state.executedSql.some((s) => s.includes("ALTER TABLE") && s.includes(tableFileName) && s.includes("ADD COLUMN") && s.includes("bio"))).toBe(true);
164
+
165
+ const runtime = syncTable.TestKit.createRuntime("sqlite", db as any, "");
166
+ const columns = await syncTable.TestKit.getTableColumnsRuntime(runtime, tableFileName);
167
+ expect(columns.nickname).toBeDefined();
168
+ expect(columns.bio).toBeDefined();
169
+
170
+ // 两次同步,每次都会清一次缓存
171
+ expect(redisCalls.length).toBe(2);
172
+ expect(redisCalls[0].keys).toEqual([CacheKeys.tableColumns(tableFileName)]);
173
+ expect(redisCalls[1].keys).toEqual([CacheKeys.tableColumns(tableFileName)]);
174
+ });
175
+
176
+ test("索引变更:仅删除单列索引;复合索引不会被误删", async () => {
177
+ const tableFileName = "test_sync_table_integration_indexes";
178
+
179
+ const state: MockSqliteState = {
180
+ executedSql: [],
181
+ tables: {
182
+ [tableFileName]: {
183
+ columns: {
184
+ id: { name: "id", type: "INTEGER", notnull: 1, dflt_value: null },
185
+ created_at: { name: "created_at", type: "INTEGER", notnull: 1, dflt_value: "0" },
186
+ updated_at: { name: "updated_at", type: "INTEGER", notnull: 1, dflt_value: "0" },
187
+ deleted_at: { name: "deleted_at", type: "INTEGER", notnull: 1, dflt_value: "0" },
188
+ state: { name: "state", type: "INTEGER", notnull: 1, dflt_value: "1" },
189
+ user_id: { name: "user_id", type: "INTEGER", notnull: 1, dflt_value: "0" },
190
+ user_name: { name: "user_name", type: "TEXT", notnull: 1, dflt_value: "''" }
191
+ },
192
+ indexes: {
193
+ // 单列索引:应该能被识别并在 index=false 时被 drop
194
+ idx_user_name: ["user_name"],
195
+ // 复合索引:即使名字像单列索引,也因为多列而被 runtime 忽略,因此不会被 drop
196
+ idx_user_id: ["user_id", "created_at"]
197
+ }
198
+ }
199
+ }
200
+ };
201
+
202
+ const db = createMockSqliteDb(state);
203
+
204
+ const redisCalls: Array<{ keys: string[] }> = [];
205
+ const ctx = {
206
+ db: db,
207
+ redis: {
208
+ delBatch: async (keys: string[]) => {
209
+ redisCalls.push({ keys: keys });
210
+ return keys.length;
211
+ }
212
+ },
213
+ config: {
214
+ db: { type: "sqlite", database: "" }
215
+ }
216
+ } as any;
217
+
218
+ const item = buildTableItem({
219
+ tableFileName: tableFileName,
220
+ content: {
221
+ userId: fdNumber({ name: "用户ID", min: 0, max: 999999999, defaultValue: 0, nullable: false }),
222
+ userName: fdString({ name: "用户名", min: 0, max: 50, defaultValue: "", nullable: false })
223
+ }
224
+ });
225
+
226
+ await syncTable(ctx, [item]);
227
+
228
+ const dropUserName = state.executedSql.some((s) => s.includes("DROP INDEX") && s.includes("idx_user_name"));
229
+ expect(dropUserName).toBe(true);
230
+
231
+ const dropUserIdComposite = state.executedSql.some((s) => s.includes("DROP INDEX") && s.includes("idx_user_id"));
232
+ expect(dropUserIdComposite).toBe(false);
233
+
234
+ expect(redisCalls.length).toBe(1);
235
+ expect(redisCalls[0].keys).toEqual([CacheKeys.tableColumns(tableFileName)]);
236
+ });
237
+ });
@@ -1,5 +1,5 @@
1
1
  /**
2
- * syncDb DDL 构建模块测试
2
+ * syncTable DDL 构建模块测试
3
3
  *
4
4
  * 测试 ddl.ts 中的函数:
5
5
  * - buildIndexSQL
@@ -9,31 +9,13 @@
9
9
  * - isCompatibleTypeChange
10
10
  */
11
11
 
12
- import { describe, test, expect, beforeAll } from "bun:test";
12
+ import { describe, test, expect } from "bun:test";
13
13
 
14
- import { setDbType } from "../sync/syncDb/constants.js";
15
-
16
- // 设置数据库类型为 MySQL
17
- setDbType("mysql");
18
-
19
- let buildIndexSQL: any;
20
- let buildSystemColumnDefs: any;
21
- let buildBusinessColumnDefs: any;
22
- let generateDDLClause: any;
23
- let isCompatibleTypeChange: any;
24
-
25
- beforeAll(async () => {
26
- const ddl = await import("../sync/syncDb/ddl.js");
27
- buildIndexSQL = ddl.buildIndexSQL;
28
- buildSystemColumnDefs = ddl.buildSystemColumnDefs;
29
- buildBusinessColumnDefs = ddl.buildBusinessColumnDefs;
30
- generateDDLClause = ddl.generateDDLClause;
31
- isCompatibleTypeChange = ddl.isCompatibleTypeChange;
32
- });
14
+ import { syncTable } from "../sync/syncTable.js";
33
15
 
34
16
  describe("buildIndexSQL (MySQL)", () => {
35
17
  test("创建索引 SQL", () => {
36
- const sql = buildIndexSQL("user", "idx_created_at", "created_at", "create");
18
+ const sql = syncTable.TestKit.buildIndexSQL("mysql", "user", "idx_created_at", "created_at", "create");
37
19
  expect(sql).toContain("ALTER TABLE `user`");
38
20
  expect(sql).toContain("ADD INDEX `idx_created_at`");
39
21
  expect(sql).toContain("(`created_at`)");
@@ -42,7 +24,7 @@ describe("buildIndexSQL (MySQL)", () => {
42
24
  });
43
25
 
44
26
  test("删除索引 SQL", () => {
45
- const sql = buildIndexSQL("user", "idx_created_at", "created_at", "drop");
27
+ const sql = syncTable.TestKit.buildIndexSQL("mysql", "user", "idx_created_at", "created_at", "drop");
46
28
  expect(sql).toContain("ALTER TABLE `user`");
47
29
  expect(sql).toContain("DROP INDEX `idx_created_at`");
48
30
  });
@@ -50,12 +32,12 @@ describe("buildIndexSQL (MySQL)", () => {
50
32
 
51
33
  describe("buildSystemColumnDefs (MySQL)", () => {
52
34
  test("返回 5 个系统字段定义", () => {
53
- const defs = buildSystemColumnDefs();
35
+ const defs = syncTable.TestKit.buildSystemColumnDefs("mysql");
54
36
  expect(defs.length).toBe(5);
55
37
  });
56
38
 
57
39
  test("包含 id 主键", () => {
58
- const defs = buildSystemColumnDefs();
40
+ const defs = syncTable.TestKit.buildSystemColumnDefs("mysql");
59
41
  const idDef = defs.find((d: string) => d.includes("`id`"));
60
42
  expect(idDef).toContain("PRIMARY KEY");
61
43
  expect(idDef).toContain("AUTO_INCREMENT");
@@ -63,7 +45,7 @@ describe("buildSystemColumnDefs (MySQL)", () => {
63
45
  });
64
46
 
65
47
  test("包含 created_at 字段", () => {
66
- const defs = buildSystemColumnDefs();
48
+ const defs = syncTable.TestKit.buildSystemColumnDefs("mysql");
67
49
  const def = defs.find((d: string) => d.includes("`created_at`"));
68
50
  expect(def).toContain("BIGINT UNSIGNED");
69
51
  expect(def).toContain("NOT NULL");
@@ -71,7 +53,7 @@ describe("buildSystemColumnDefs (MySQL)", () => {
71
53
  });
72
54
 
73
55
  test("包含 state 字段", () => {
74
- const defs = buildSystemColumnDefs();
56
+ const defs = syncTable.TestKit.buildSystemColumnDefs("mysql");
75
57
  const def = defs.find((d: string) => d.includes("`state`"));
76
58
  expect(def).toContain("BIGINT UNSIGNED");
77
59
  expect(def).toContain("NOT NULL");
@@ -79,6 +61,38 @@ describe("buildSystemColumnDefs (MySQL)", () => {
79
61
  });
80
62
  });
81
63
 
64
+ describe("buildSystemColumnDefs (PostgreSQL)", () => {
65
+ test("返回 5 个系统字段定义", () => {
66
+ const defs = syncTable.TestKit.buildSystemColumnDefs("postgresql");
67
+ expect(defs.length).toBe(5);
68
+ });
69
+
70
+ test("包含 id 主键 identity", () => {
71
+ const defs = syncTable.TestKit.buildSystemColumnDefs("postgresql");
72
+ const idDef = defs.find((d: string) => d.includes('"id"'));
73
+ expect(idDef).toContain("BIGINT");
74
+ expect(idDef).toContain("PRIMARY KEY");
75
+ expect(idDef).toContain("GENERATED");
76
+ expect(idDef).toContain("IDENTITY");
77
+ });
78
+
79
+ test("包含 created_at BIGINT 默认值", () => {
80
+ const defs = syncTable.TestKit.buildSystemColumnDefs("postgresql");
81
+ const def = defs.find((d: string) => d.includes('"created_at"'));
82
+ expect(def).toContain("BIGINT");
83
+ expect(def).toContain("NOT NULL");
84
+ expect(def).toContain("DEFAULT 0");
85
+ });
86
+
87
+ test("包含 state BIGINT 默认值", () => {
88
+ const defs = syncTable.TestKit.buildSystemColumnDefs("postgresql");
89
+ const def = defs.find((d: string) => d.includes('"state"'));
90
+ expect(def).toContain("BIGINT");
91
+ expect(def).toContain("NOT NULL");
92
+ expect(def).toContain("DEFAULT 1");
93
+ });
94
+ });
95
+
82
96
  describe("buildBusinessColumnDefs (MySQL)", () => {
83
97
  test("生成 string 类型字段", () => {
84
98
  const fields = {
@@ -92,7 +106,7 @@ describe("buildBusinessColumnDefs (MySQL)", () => {
92
106
  unsigned: true
93
107
  }
94
108
  };
95
- const defs = buildBusinessColumnDefs(fields);
109
+ const defs = syncTable.TestKit.buildBusinessColumnDefs("mysql", fields as any);
96
110
  expect(defs.length).toBe(1);
97
111
  expect(defs[0]).toContain("`user_name`");
98
112
  expect(defs[0]).toContain("VARCHAR(50)");
@@ -113,7 +127,7 @@ describe("buildBusinessColumnDefs (MySQL)", () => {
113
127
  unsigned: true
114
128
  }
115
129
  };
116
- const defs = buildBusinessColumnDefs(fields);
130
+ const defs = syncTable.TestKit.buildBusinessColumnDefs("mysql", fields as any);
117
131
  expect(defs[0]).toContain("`age`");
118
132
  expect(defs[0]).toContain("BIGINT UNSIGNED");
119
133
  expect(defs[0]).toContain("DEFAULT 0");
@@ -131,7 +145,7 @@ describe("buildBusinessColumnDefs (MySQL)", () => {
131
145
  unsigned: true
132
146
  }
133
147
  };
134
- const defs = buildBusinessColumnDefs(fields);
148
+ const defs = syncTable.TestKit.buildBusinessColumnDefs("mysql", fields as any);
135
149
  expect(defs[0]).toContain("UNIQUE");
136
150
  });
137
151
 
@@ -147,7 +161,7 @@ describe("buildBusinessColumnDefs (MySQL)", () => {
147
161
  unsigned: true
148
162
  }
149
163
  };
150
- const defs = buildBusinessColumnDefs(fields);
164
+ const defs = syncTable.TestKit.buildBusinessColumnDefs("mysql", fields as any);
151
165
  expect(defs[0]).toContain("NULL");
152
166
  expect(defs[0]).not.toContain("NOT NULL");
153
167
  });
@@ -164,7 +178,7 @@ describe("generateDDLClause (MySQL)", () => {
164
178
  nullable: false,
165
179
  unsigned: true
166
180
  };
167
- const clause = generateDDLClause("userName", fieldDef, true);
181
+ const clause = syncTable.TestKit.generateDDLClause("mysql", "userName", fieldDef as any, true);
168
182
  expect(clause).toContain("ADD COLUMN");
169
183
  expect(clause).toContain("`user_name`");
170
184
  expect(clause).toContain("VARCHAR(50)");
@@ -180,7 +194,7 @@ describe("generateDDLClause (MySQL)", () => {
180
194
  nullable: false,
181
195
  unsigned: true
182
196
  };
183
- const clause = generateDDLClause("userName", fieldDef, false);
197
+ const clause = syncTable.TestKit.generateDDLClause("mysql", "userName", fieldDef as any, false);
184
198
  expect(clause).toContain("MODIFY COLUMN");
185
199
  expect(clause).toContain("`user_name`");
186
200
  expect(clause).toContain("VARCHAR(100)");
@@ -189,43 +203,43 @@ describe("generateDDLClause (MySQL)", () => {
189
203
 
190
204
  describe("isCompatibleTypeChange", () => {
191
205
  test("varchar -> text 是兼容变更", () => {
192
- expect(isCompatibleTypeChange("character varying", "text")).toBe(true);
193
- expect(isCompatibleTypeChange("varchar(100)", "text")).toBe(true);
194
- expect(isCompatibleTypeChange("varchar(100)", "mediumtext")).toBe(true);
206
+ expect(syncTable.TestKit.isCompatibleTypeChange("character varying", "text")).toBe(true);
207
+ expect(syncTable.TestKit.isCompatibleTypeChange("varchar(100)", "text")).toBe(true);
208
+ expect(syncTable.TestKit.isCompatibleTypeChange("varchar(100)", "mediumtext")).toBe(true);
195
209
  });
196
210
 
197
211
  test("text -> varchar 不是兼容变更", () => {
198
- expect(isCompatibleTypeChange("text", "character varying")).toBe(false);
199
- expect(isCompatibleTypeChange("text", "varchar(100)")).toBe(false);
212
+ expect(syncTable.TestKit.isCompatibleTypeChange("text", "character varying")).toBe(false);
213
+ expect(syncTable.TestKit.isCompatibleTypeChange("text", "varchar(100)")).toBe(false);
200
214
  });
201
215
 
202
216
  test("int -> bigint 是兼容变更", () => {
203
- expect(isCompatibleTypeChange("int", "bigint")).toBe(true);
204
- expect(isCompatibleTypeChange("int unsigned", "bigint unsigned")).toBe(true);
205
- expect(isCompatibleTypeChange("tinyint", "int")).toBe(true);
206
- expect(isCompatibleTypeChange("tinyint", "bigint")).toBe(true);
207
- expect(isCompatibleTypeChange("smallint", "int")).toBe(true);
208
- expect(isCompatibleTypeChange("mediumint", "bigint")).toBe(true);
217
+ expect(syncTable.TestKit.isCompatibleTypeChange("int", "bigint")).toBe(true);
218
+ expect(syncTable.TestKit.isCompatibleTypeChange("int unsigned", "bigint unsigned")).toBe(true);
219
+ expect(syncTable.TestKit.isCompatibleTypeChange("tinyint", "int")).toBe(true);
220
+ expect(syncTable.TestKit.isCompatibleTypeChange("tinyint", "bigint")).toBe(true);
221
+ expect(syncTable.TestKit.isCompatibleTypeChange("smallint", "int")).toBe(true);
222
+ expect(syncTable.TestKit.isCompatibleTypeChange("mediumint", "bigint")).toBe(true);
209
223
  });
210
224
 
211
225
  test("bigint -> int 不是兼容变更(收缩)", () => {
212
- expect(isCompatibleTypeChange("bigint", "int")).toBe(false);
213
- expect(isCompatibleTypeChange("int", "tinyint")).toBe(false);
226
+ expect(syncTable.TestKit.isCompatibleTypeChange("bigint", "int")).toBe(false);
227
+ expect(syncTable.TestKit.isCompatibleTypeChange("int", "tinyint")).toBe(false);
214
228
  });
215
229
 
216
230
  test("PG integer -> bigint 是兼容变更", () => {
217
- expect(isCompatibleTypeChange("integer", "bigint")).toBe(true);
218
- expect(isCompatibleTypeChange("smallint", "integer")).toBe(true);
219
- expect(isCompatibleTypeChange("smallint", "bigint")).toBe(true);
231
+ expect(syncTable.TestKit.isCompatibleTypeChange("integer", "bigint")).toBe(true);
232
+ expect(syncTable.TestKit.isCompatibleTypeChange("smallint", "integer")).toBe(true);
233
+ expect(syncTable.TestKit.isCompatibleTypeChange("smallint", "bigint")).toBe(true);
220
234
  });
221
235
 
222
236
  test("相同类型不是变更", () => {
223
- expect(isCompatibleTypeChange("text", "text")).toBe(false);
224
- expect(isCompatibleTypeChange("bigint", "bigint")).toBe(false);
237
+ expect(syncTable.TestKit.isCompatibleTypeChange("text", "text")).toBe(false);
238
+ expect(syncTable.TestKit.isCompatibleTypeChange("bigint", "bigint")).toBe(false);
225
239
  });
226
240
 
227
241
  test("空值处理", () => {
228
- expect(isCompatibleTypeChange(null, "text")).toBe(false);
229
- expect(isCompatibleTypeChange("text", null)).toBe(false);
242
+ expect(syncTable.TestKit.isCompatibleTypeChange(null as any, "text")).toBe(false);
243
+ expect(syncTable.TestKit.isCompatibleTypeChange("text", null as any)).toBe(false);
230
244
  });
231
245
  });
@@ -1,5 +1,5 @@
1
1
  /**
2
- * syncDb 辅助工具模块测试
2
+ * syncTable 辅助工具模块测试
3
3
  *
4
4
  * 测试 helpers.ts 中的函数:
5
5
  * - quoteIdentifier
@@ -7,49 +7,35 @@
7
7
  * - applyFieldDefaults
8
8
  */
9
9
 
10
- import { describe, test, expect, beforeAll } from "bun:test";
10
+ import { describe, test, expect } from "bun:test";
11
11
 
12
- import { setDbType } from "../sync/syncDb/constants.js";
13
-
14
- // 设置数据库类型为 MySQL
15
- setDbType("mysql");
16
-
17
- let quoteIdentifier: any;
18
- let escapeComment: any;
19
- let applyFieldDefaults: any;
20
-
21
- beforeAll(async () => {
22
- const helpers = await import("../sync/syncDb/helpers.js");
23
- quoteIdentifier = helpers.quoteIdentifier;
24
- escapeComment = helpers.escapeComment;
25
- applyFieldDefaults = helpers.applyFieldDefaults;
26
- });
12
+ import { syncTable } from "../sync/syncTable.js";
27
13
 
28
14
  describe("quoteIdentifier (MySQL)", () => {
29
15
  test("使用反引号包裹标识符", () => {
30
- expect(quoteIdentifier("user_table")).toBe("`user_table`");
16
+ expect(syncTable.TestKit.quoteIdentifier("mysql", "user_table")).toBe("`user_table`");
31
17
  });
32
18
 
33
19
  test("处理普通表名", () => {
34
- expect(quoteIdentifier("admin")).toBe("`admin`");
20
+ expect(syncTable.TestKit.quoteIdentifier("mysql", "admin")).toBe("`admin`");
35
21
  });
36
22
 
37
23
  test("处理带下划线的表名", () => {
38
- expect(quoteIdentifier("addon_admin_menu")).toBe("`addon_admin_menu`");
24
+ expect(syncTable.TestKit.quoteIdentifier("mysql", "addon_admin_menu")).toBe("`addon_admin_menu`");
39
25
  });
40
26
  });
41
27
 
42
28
  describe("escapeComment", () => {
43
29
  test("普通注释不变", () => {
44
- expect(escapeComment("用户名称")).toBe("用户名称");
30
+ expect(syncTable.TestKit.escapeComment("用户名称")).toBe("用户名称");
45
31
  });
46
32
 
47
33
  test("双引号被转义", () => {
48
- expect(escapeComment('用户"昵称"')).toBe('用户\\"昵称\\"');
34
+ expect(syncTable.TestKit.escapeComment('用户"昵称"')).toBe('用户\\"昵称\\"');
49
35
  });
50
36
 
51
37
  test("空字符串", () => {
52
- expect(escapeComment("")).toBe("");
38
+ expect(syncTable.TestKit.escapeComment("")).toBe("");
53
39
  });
54
40
  });
55
41
 
@@ -60,7 +46,7 @@ describe("applyFieldDefaults", () => {
60
46
  type: "string"
61
47
  };
62
48
 
63
- applyFieldDefaults(fieldDef);
49
+ syncTable.TestKit.applyFieldDefaults(fieldDef);
64
50
 
65
51
  expect(fieldDef.detail).toBe("");
66
52
  expect(fieldDef.min).toBe(0);
@@ -83,7 +69,7 @@ describe("applyFieldDefaults", () => {
83
69
  nullable: true
84
70
  };
85
71
 
86
- applyFieldDefaults(fieldDef);
72
+ syncTable.TestKit.applyFieldDefaults(fieldDef);
87
73
 
88
74
  expect(fieldDef.max).toBe(200);
89
75
  expect(fieldDef.index).toBe(true);
@@ -102,7 +88,7 @@ describe("applyFieldDefaults", () => {
102
88
  unsigned: false
103
89
  };
104
90
 
105
- applyFieldDefaults(fieldDef);
91
+ syncTable.TestKit.applyFieldDefaults(fieldDef);
106
92
 
107
93
  expect(fieldDef.min).toBe(0);
108
94
  expect(fieldDef.max).toBe(0);