befly 3.10.0 → 3.10.2

This diff represents the content of publicly available package versions that have been released to one of the supported registries. The information contained in this diff is provided for informational purposes only and reflects changes between package versions as they appear in their respective public registries.
Files changed (79) hide show
  1. package/.gitignore +0 -0
  2. package/README.md +10 -13
  3. package/configs/presetFields.ts +10 -0
  4. package/configs/presetRegexp.ts +225 -0
  5. package/docs/README.md +17 -11
  6. package/docs/api/api.md +15 -1
  7. package/docs/guide/quickstart.md +19 -5
  8. package/docs/infra/redis.md +23 -11
  9. package/docs/quickstart.md +5 -335
  10. package/docs/reference/addon.md +0 -15
  11. package/docs/reference/config.md +1 -1
  12. package/docs/reference/logger.md +3 -3
  13. package/docs/reference/sync.md +99 -73
  14. package/docs/reference/table.md +1 -1
  15. package/package.json +15 -16
  16. package/docs/cipher.md +0 -582
  17. package/docs/database.md +0 -1176
  18. package/tests/_mocks/mockSqliteDb.ts +0 -204
  19. package/tests/addonHelper-cache.test.ts +0 -32
  20. package/tests/api-integration-array-number.test.ts +0 -282
  21. package/tests/apiHandler-routePath-only.test.ts +0 -32
  22. package/tests/befly-config-env.test.ts +0 -78
  23. package/tests/cacheHelper.test.ts +0 -323
  24. package/tests/cacheKeys.test.ts +0 -41
  25. package/tests/checkApi-routePath-strict.test.ts +0 -166
  26. package/tests/checkMenu.test.ts +0 -346
  27. package/tests/checkTable-smoke.test.ts +0 -157
  28. package/tests/cipher.test.ts +0 -249
  29. package/tests/dbDialect-cache.test.ts +0 -23
  30. package/tests/dbDialect.test.ts +0 -46
  31. package/tests/dbHelper-advanced.test.ts +0 -723
  32. package/tests/dbHelper-all-array-types.test.ts +0 -316
  33. package/tests/dbHelper-array-serialization.test.ts +0 -258
  34. package/tests/dbHelper-batch-write.test.ts +0 -90
  35. package/tests/dbHelper-columns.test.ts +0 -234
  36. package/tests/dbHelper-execute.test.ts +0 -187
  37. package/tests/dbHelper-joins.test.ts +0 -221
  38. package/tests/fields-redis-cache.test.ts +0 -127
  39. package/tests/fields-validate.test.ts +0 -99
  40. package/tests/fixtures/scanFilesAddon/node_modules/@befly-addon/demo/apis/sub/b.ts +0 -3
  41. package/tests/fixtures/scanFilesApis/a.ts +0 -3
  42. package/tests/fixtures/scanFilesApis/sub/b.ts +0 -3
  43. package/tests/getClientIp.test.ts +0 -54
  44. package/tests/integration.test.ts +0 -189
  45. package/tests/jwt.test.ts +0 -65
  46. package/tests/loadPlugins-order-smoke.test.ts +0 -75
  47. package/tests/logger.test.ts +0 -325
  48. package/tests/redisHelper.test.ts +0 -495
  49. package/tests/redisKeys.test.ts +0 -9
  50. package/tests/scanConfig.test.ts +0 -144
  51. package/tests/scanFiles-routePath.test.ts +0 -46
  52. package/tests/smoke-sql.test.ts +0 -24
  53. package/tests/sqlBuilder-advanced.test.ts +0 -608
  54. package/tests/sqlBuilder.test.ts +0 -209
  55. package/tests/sync-connection.test.ts +0 -183
  56. package/tests/sync-init-guard.test.ts +0 -105
  57. package/tests/syncApi-insBatch-fields-consistent.test.ts +0 -61
  58. package/tests/syncApi-obsolete-records.test.ts +0 -69
  59. package/tests/syncApi-type-compat.test.ts +0 -72
  60. package/tests/syncDev-permissions.test.ts +0 -81
  61. package/tests/syncMenu-disableMenus-hard-delete.test.ts +0 -88
  62. package/tests/syncMenu-duplicate-path.test.ts +0 -122
  63. package/tests/syncMenu-obsolete-records.test.ts +0 -161
  64. package/tests/syncMenu-parentPath-from-tree.test.ts +0 -75
  65. package/tests/syncMenu-paths.test.ts +0 -59
  66. package/tests/syncTable-apply.test.ts +0 -279
  67. package/tests/syncTable-array-number.test.ts +0 -160
  68. package/tests/syncTable-constants.test.ts +0 -101
  69. package/tests/syncTable-db-integration.test.ts +0 -237
  70. package/tests/syncTable-ddl.test.ts +0 -245
  71. package/tests/syncTable-helpers.test.ts +0 -99
  72. package/tests/syncTable-schema.test.ts +0 -99
  73. package/tests/syncTable-testkit.test.ts +0 -25
  74. package/tests/syncTable-types.test.ts +0 -122
  75. package/tests/tableRef-and-deserialize.test.ts +0 -67
  76. package/tests/util.test.ts +0 -100
  77. package/tests/validator-array-number.test.ts +0 -310
  78. package/tests/validator-default.test.ts +0 -373
  79. package/tests/validator.test.ts +0 -679
@@ -1,237 +0,0 @@
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,245 +0,0 @@
1
- /**
2
- * syncTable DDL 构建模块测试
3
- *
4
- * 测试 ddl.ts 中的函数:
5
- * - buildIndexSQL
6
- * - buildSystemColumnDefs
7
- * - buildBusinessColumnDefs
8
- * - generateDDLClause
9
- * - isCompatibleTypeChange
10
- */
11
-
12
- import { describe, test, expect } from "bun:test";
13
-
14
- import { syncTable } from "../sync/syncTable.js";
15
-
16
- describe("buildIndexSQL (MySQL)", () => {
17
- test("创建索引 SQL", () => {
18
- const sql = syncTable.TestKit.buildIndexSQL("mysql", "user", "idx_created_at", "created_at", "create");
19
- expect(sql).toContain("ALTER TABLE `user`");
20
- expect(sql).toContain("ADD INDEX `idx_created_at`");
21
- expect(sql).toContain("(`created_at`)");
22
- expect(sql).toContain("ALGORITHM=INPLACE");
23
- expect(sql).toContain("LOCK=NONE");
24
- });
25
-
26
- test("删除索引 SQL", () => {
27
- const sql = syncTable.TestKit.buildIndexSQL("mysql", "user", "idx_created_at", "created_at", "drop");
28
- expect(sql).toContain("ALTER TABLE `user`");
29
- expect(sql).toContain("DROP INDEX `idx_created_at`");
30
- });
31
- });
32
-
33
- describe("buildSystemColumnDefs (MySQL)", () => {
34
- test("返回 5 个系统字段定义", () => {
35
- const defs = syncTable.TestKit.buildSystemColumnDefs("mysql");
36
- expect(defs.length).toBe(5);
37
- });
38
-
39
- test("包含 id 主键", () => {
40
- const defs = syncTable.TestKit.buildSystemColumnDefs("mysql");
41
- const idDef = defs.find((d: string) => d.includes("`id`"));
42
- expect(idDef).toContain("PRIMARY KEY");
43
- expect(idDef).toContain("AUTO_INCREMENT");
44
- expect(idDef).toContain("BIGINT UNSIGNED");
45
- });
46
-
47
- test("包含 created_at 字段", () => {
48
- const defs = syncTable.TestKit.buildSystemColumnDefs("mysql");
49
- const def = defs.find((d: string) => d.includes("`created_at`"));
50
- expect(def).toContain("BIGINT UNSIGNED");
51
- expect(def).toContain("NOT NULL");
52
- expect(def).toContain("DEFAULT 0");
53
- });
54
-
55
- test("包含 state 字段", () => {
56
- const defs = syncTable.TestKit.buildSystemColumnDefs("mysql");
57
- const def = defs.find((d: string) => d.includes("`state`"));
58
- expect(def).toContain("BIGINT UNSIGNED");
59
- expect(def).toContain("NOT NULL");
60
- expect(def).toContain("DEFAULT 1");
61
- });
62
- });
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
-
96
- describe("buildBusinessColumnDefs (MySQL)", () => {
97
- test("生成 string 类型字段", () => {
98
- const fields = {
99
- userName: {
100
- name: "用户名",
101
- type: "string",
102
- max: 50,
103
- default: null,
104
- unique: false,
105
- nullable: false,
106
- unsigned: true
107
- }
108
- };
109
- const defs = syncTable.TestKit.buildBusinessColumnDefs("mysql", fields as any);
110
- expect(defs.length).toBe(1);
111
- expect(defs[0]).toContain("`user_name`");
112
- expect(defs[0]).toContain("VARCHAR(50)");
113
- expect(defs[0]).toContain("NOT NULL");
114
- expect(defs[0]).toContain("DEFAULT ''");
115
- expect(defs[0]).toContain('COMMENT "用户名"');
116
- });
117
-
118
- test("生成 number 类型字段", () => {
119
- const fields = {
120
- age: {
121
- name: "年龄",
122
- type: "number",
123
- max: null,
124
- default: 0,
125
- unique: false,
126
- nullable: false,
127
- unsigned: true
128
- }
129
- };
130
- const defs = syncTable.TestKit.buildBusinessColumnDefs("mysql", fields as any);
131
- expect(defs[0]).toContain("`age`");
132
- expect(defs[0]).toContain("BIGINT UNSIGNED");
133
- expect(defs[0]).toContain("DEFAULT 0");
134
- });
135
-
136
- test("生成 unique 字段", () => {
137
- const fields = {
138
- email: {
139
- name: "邮箱",
140
- type: "string",
141
- max: 100,
142
- default: null,
143
- unique: true,
144
- nullable: false,
145
- unsigned: true
146
- }
147
- };
148
- const defs = syncTable.TestKit.buildBusinessColumnDefs("mysql", fields as any);
149
- expect(defs[0]).toContain("UNIQUE");
150
- });
151
-
152
- test("生成 nullable 字段", () => {
153
- const fields = {
154
- remark: {
155
- name: "备注",
156
- type: "string",
157
- max: 200,
158
- default: null,
159
- unique: false,
160
- nullable: true,
161
- unsigned: true
162
- }
163
- };
164
- const defs = syncTable.TestKit.buildBusinessColumnDefs("mysql", fields as any);
165
- expect(defs[0]).toContain("NULL");
166
- expect(defs[0]).not.toContain("NOT NULL");
167
- });
168
- });
169
-
170
- describe("generateDDLClause (MySQL)", () => {
171
- test("生成 ADD COLUMN 子句", () => {
172
- const fieldDef = {
173
- name: "用户名",
174
- type: "string",
175
- max: 50,
176
- default: null,
177
- unique: false,
178
- nullable: false,
179
- unsigned: true
180
- };
181
- const clause = syncTable.TestKit.generateDDLClause("mysql", "userName", fieldDef as any, true);
182
- expect(clause).toContain("ADD COLUMN");
183
- expect(clause).toContain("`user_name`");
184
- expect(clause).toContain("VARCHAR(50)");
185
- });
186
-
187
- test("生成 MODIFY COLUMN 子句", () => {
188
- const fieldDef = {
189
- name: "用户名",
190
- type: "string",
191
- max: 100,
192
- default: null,
193
- unique: false,
194
- nullable: false,
195
- unsigned: true
196
- };
197
- const clause = syncTable.TestKit.generateDDLClause("mysql", "userName", fieldDef as any, false);
198
- expect(clause).toContain("MODIFY COLUMN");
199
- expect(clause).toContain("`user_name`");
200
- expect(clause).toContain("VARCHAR(100)");
201
- });
202
- });
203
-
204
- describe("isCompatibleTypeChange", () => {
205
- test("varchar -> text 是兼容变更", () => {
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);
209
- });
210
-
211
- test("text -> varchar 不是兼容变更", () => {
212
- expect(syncTable.TestKit.isCompatibleTypeChange("text", "character varying")).toBe(false);
213
- expect(syncTable.TestKit.isCompatibleTypeChange("text", "varchar(100)")).toBe(false);
214
- });
215
-
216
- test("int -> bigint 是兼容变更", () => {
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);
223
- });
224
-
225
- test("bigint -> int 不是兼容变更(收缩)", () => {
226
- expect(syncTable.TestKit.isCompatibleTypeChange("bigint", "int")).toBe(false);
227
- expect(syncTable.TestKit.isCompatibleTypeChange("int", "tinyint")).toBe(false);
228
- });
229
-
230
- test("PG integer -> bigint 是兼容变更", () => {
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);
234
- });
235
-
236
- test("相同类型不是变更", () => {
237
- expect(syncTable.TestKit.isCompatibleTypeChange("text", "text")).toBe(false);
238
- expect(syncTable.TestKit.isCompatibleTypeChange("bigint", "bigint")).toBe(false);
239
- });
240
-
241
- test("空值处理", () => {
242
- expect(syncTable.TestKit.isCompatibleTypeChange(null as any, "text")).toBe(false);
243
- expect(syncTable.TestKit.isCompatibleTypeChange("text", null as any)).toBe(false);
244
- });
245
- });
@@ -1,99 +0,0 @@
1
- /**
2
- * syncTable 辅助工具模块测试
3
- *
4
- * 测试 helpers.ts 中的函数:
5
- * - quoteIdentifier
6
- * - escapeComment
7
- * - applyFieldDefaults
8
- */
9
-
10
- import { describe, test, expect } from "bun:test";
11
-
12
- import { syncTable } from "../sync/syncTable.js";
13
-
14
- describe("quoteIdentifier (MySQL)", () => {
15
- test("使用反引号包裹标识符", () => {
16
- expect(syncTable.TestKit.quoteIdentifier("mysql", "user_table")).toBe("`user_table`");
17
- });
18
-
19
- test("处理普通表名", () => {
20
- expect(syncTable.TestKit.quoteIdentifier("mysql", "admin")).toBe("`admin`");
21
- });
22
-
23
- test("处理带下划线的表名", () => {
24
- expect(syncTable.TestKit.quoteIdentifier("mysql", "addon_admin_menu")).toBe("`addon_admin_menu`");
25
- });
26
- });
27
-
28
- describe("escapeComment", () => {
29
- test("普通注释不变", () => {
30
- expect(syncTable.TestKit.escapeComment("用户名称")).toBe("用户名称");
31
- });
32
-
33
- test("双引号被转义", () => {
34
- expect(syncTable.TestKit.escapeComment('用户"昵称"')).toBe('用户\\"昵称\\"');
35
- });
36
-
37
- test("空字符串", () => {
38
- expect(syncTable.TestKit.escapeComment("")).toBe("");
39
- });
40
- });
41
-
42
- describe("applyFieldDefaults", () => {
43
- test("为空字段定义应用默认值", () => {
44
- const fieldDef: any = {
45
- name: "用户名",
46
- type: "string"
47
- };
48
-
49
- syncTable.TestKit.applyFieldDefaults(fieldDef);
50
-
51
- expect(fieldDef.detail).toBe("");
52
- expect(fieldDef.min).toBe(0);
53
- expect(fieldDef.max).toBe(100);
54
- expect(fieldDef.default).toBe(null);
55
- expect(fieldDef.index).toBe(false);
56
- expect(fieldDef.unique).toBe(false);
57
- expect(fieldDef.nullable).toBe(false);
58
- expect(fieldDef.unsigned).toBe(true);
59
- expect(fieldDef.regexp).toBe(null);
60
- });
61
-
62
- test("保留已有值", () => {
63
- const fieldDef: any = {
64
- name: "用户名",
65
- type: "string",
66
- max: 200,
67
- index: true,
68
- unique: true,
69
- nullable: true
70
- };
71
-
72
- syncTable.TestKit.applyFieldDefaults(fieldDef);
73
-
74
- expect(fieldDef.max).toBe(200);
75
- expect(fieldDef.index).toBe(true);
76
- expect(fieldDef.unique).toBe(true);
77
- expect(fieldDef.nullable).toBe(true);
78
- });
79
-
80
- test("处理 0 和 false 值", () => {
81
- const fieldDef: any = {
82
- name: "排序",
83
- type: "number",
84
- min: 0,
85
- max: 0,
86
- default: 0,
87
- index: false,
88
- unsigned: false
89
- };
90
-
91
- syncTable.TestKit.applyFieldDefaults(fieldDef);
92
-
93
- expect(fieldDef.min).toBe(0);
94
- expect(fieldDef.max).toBe(0);
95
- expect(fieldDef.default).toBe(0);
96
- expect(fieldDef.index).toBe(false);
97
- expect(fieldDef.unsigned).toBe(false);
98
- });
99
- });
@@ -1,99 +0,0 @@
1
- /**
2
- * syncTable 表结构查询模块测试(纯 mock,不连接真实数据库)
3
- */
4
-
5
- import { describe, expect, test } from "bun:test";
6
-
7
- import { syncTable } from "../sync/syncTable.js";
8
- import { createMockSqliteDb } from "./_mocks/mockSqliteDb.js";
9
-
10
- describe("tableExistsRuntime", () => {
11
- test("sql 执行器未初始化时抛出错误", async () => {
12
- try {
13
- await syncTable.TestKit.tableExistsRuntime(syncTable.TestKit.createRuntime("sqlite", null as any, ""), "user");
14
- expect(true).toBe(false);
15
- } catch (error: any) {
16
- expect(error.message).toBe("SQL 执行器未初始化");
17
- }
18
- });
19
-
20
- test("mock sqlite:表存在返回 true;表不存在返回 false", async () => {
21
- const db = createMockSqliteDb({
22
- executedSql: [],
23
- tables: {
24
- test_sync_table_exists: {
25
- columns: {},
26
- indexes: {}
27
- }
28
- }
29
- });
30
-
31
- const runtime = syncTable.TestKit.createRuntime("sqlite", db as any, "");
32
- const exist = await syncTable.TestKit.tableExistsRuntime(runtime, "test_sync_table_exists");
33
- expect(exist).toBe(true);
34
-
35
- const notExist = await syncTable.TestKit.tableExistsRuntime(runtime, "test_sync_table_not_exist_12345");
36
- expect(notExist).toBe(false);
37
- });
38
- });
39
-
40
- describe("getTableColumnsRuntime", () => {
41
- test("mock sqlite:返回列信息结构(至少包含我们定义的列)", async () => {
42
- const db = createMockSqliteDb({
43
- executedSql: [],
44
- tables: {
45
- test_sync_table_columns: {
46
- columns: {
47
- id: { name: "id", type: "INTEGER", notnull: 1, dflt_value: null },
48
- user_name: { name: "user_name", type: "TEXT", notnull: 1, dflt_value: "''" },
49
- user_id: { name: "user_id", type: "INTEGER", notnull: 1, dflt_value: "0" },
50
- age: { name: "age", type: "INTEGER", notnull: 0, dflt_value: "0" },
51
- created_at: { name: "created_at", type: "INTEGER", notnull: 1, dflt_value: "0" }
52
- },
53
- indexes: {}
54
- }
55
- }
56
- });
57
-
58
- const runtime = syncTable.TestKit.createRuntime("sqlite", db as any, "");
59
- const columns = await syncTable.TestKit.getTableColumnsRuntime(runtime, "test_sync_table_columns");
60
-
61
- expect(columns.id).toBeDefined();
62
- expect(columns.user_name).toBeDefined();
63
- expect(columns.user_id).toBeDefined();
64
- expect(columns.age).toBeDefined();
65
- expect(columns.created_at).toBeDefined();
66
-
67
- expect(columns.user_name.nullable).toBe(false);
68
- });
69
- });
70
-
71
- describe("getTableIndexesRuntime", () => {
72
- test("mock sqlite:返回索引信息结构(仅单列索引;复合索引会被忽略)", async () => {
73
- const db = createMockSqliteDb({
74
- executedSql: [],
75
- tables: {
76
- test_sync_table_indexes: {
77
- columns: {},
78
- indexes: {
79
- idx_created_at: ["created_at"],
80
- idx_user_name: ["user_name"],
81
- idx_composite: ["user_id", "created_at"]
82
- }
83
- }
84
- }
85
- });
86
-
87
- const runtime = syncTable.TestKit.createRuntime("sqlite", db as any, "");
88
- const indexes = await syncTable.TestKit.getTableIndexesRuntime(runtime, "test_sync_table_indexes");
89
-
90
- expect(indexes.idx_created_at).toBeDefined();
91
- expect(indexes.idx_created_at).toContain("created_at");
92
-
93
- expect(indexes.idx_user_name).toBeDefined();
94
- expect(indexes.idx_user_name).toContain("user_name");
95
-
96
- // sqlite 路径下为了避免多列索引误判,仅收集单列索引
97
- expect(indexes.idx_composite).toBeUndefined();
98
- });
99
- });