befly 3.9.37 → 3.9.39

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 (155) hide show
  1. package/README.md +38 -39
  2. package/befly.config.ts +62 -40
  3. package/checks/checkApi.ts +16 -16
  4. package/checks/checkApp.ts +19 -25
  5. package/checks/checkTable.ts +42 -42
  6. package/docs/README.md +42 -35
  7. package/docs/{api.md → api/api.md} +225 -235
  8. package/docs/cipher.md +71 -69
  9. package/docs/database.md +155 -153
  10. package/docs/{examples.md → guide/examples.md} +181 -181
  11. package/docs/guide/quickstart.md +331 -0
  12. package/docs/hooks/auth.md +38 -0
  13. package/docs/hooks/cors.md +28 -0
  14. package/docs/{hook.md → hooks/hook.md} +140 -57
  15. package/docs/hooks/parser.md +19 -0
  16. package/docs/hooks/rateLimit.md +47 -0
  17. package/docs/{redis.md → infra/redis.md} +84 -93
  18. package/docs/plugins/cipher.md +61 -0
  19. package/docs/plugins/database.md +128 -0
  20. package/docs/{plugin.md → plugins/plugin.md} +83 -81
  21. package/docs/quickstart.md +26 -26
  22. package/docs/{addon.md → reference/addon.md} +46 -46
  23. package/docs/{config.md → reference/config.md} +32 -80
  24. package/docs/{logger.md → reference/logger.md} +52 -52
  25. package/docs/{sync.md → reference/sync.md} +32 -35
  26. package/docs/{table.md → reference/table.md} +7 -7
  27. package/docs/{validator.md → reference/validator.md} +57 -57
  28. package/hooks/auth.ts +8 -4
  29. package/hooks/cors.ts +13 -13
  30. package/hooks/parser.ts +37 -17
  31. package/hooks/permission.ts +26 -14
  32. package/hooks/rateLimit.ts +276 -0
  33. package/hooks/validator.ts +15 -7
  34. package/lib/asyncContext.ts +43 -0
  35. package/lib/cacheHelper.ts +212 -81
  36. package/lib/cacheKeys.ts +38 -0
  37. package/lib/cipher.ts +30 -30
  38. package/lib/connect.ts +28 -28
  39. package/lib/dbHelper.ts +211 -109
  40. package/lib/jwt.ts +16 -16
  41. package/lib/logger.ts +610 -19
  42. package/lib/redisHelper.ts +185 -44
  43. package/lib/sqlBuilder.ts +90 -91
  44. package/lib/validator.ts +59 -39
  45. package/loader/loadApis.ts +53 -47
  46. package/loader/loadHooks.ts +40 -14
  47. package/loader/loadPlugins.ts +16 -17
  48. package/main.ts +57 -47
  49. package/package.json +47 -45
  50. package/paths.ts +15 -14
  51. package/plugins/cache.ts +5 -4
  52. package/plugins/cipher.ts +3 -3
  53. package/plugins/config.ts +2 -2
  54. package/plugins/db.ts +9 -9
  55. package/plugins/jwt.ts +3 -3
  56. package/plugins/logger.ts +8 -12
  57. package/plugins/redis.ts +8 -8
  58. package/plugins/tool.ts +6 -6
  59. package/router/api.ts +85 -56
  60. package/router/static.ts +12 -12
  61. package/sync/syncAll.ts +12 -12
  62. package/sync/syncApi.ts +55 -54
  63. package/sync/syncDb/apply.ts +20 -19
  64. package/sync/syncDb/constants.ts +25 -23
  65. package/sync/syncDb/ddl.ts +35 -36
  66. package/sync/syncDb/helpers.ts +6 -9
  67. package/sync/syncDb/schema.ts +10 -9
  68. package/sync/syncDb/sqlite.ts +7 -8
  69. package/sync/syncDb/table.ts +37 -35
  70. package/sync/syncDb/tableCreate.ts +21 -20
  71. package/sync/syncDb/types.ts +23 -20
  72. package/sync/syncDb/version.ts +10 -10
  73. package/sync/syncDb.ts +43 -36
  74. package/sync/syncDev.ts +74 -66
  75. package/sync/syncMenu.ts +190 -57
  76. package/tests/api-integration-array-number.test.ts +282 -0
  77. package/tests/befly-config-env.test.ts +78 -0
  78. package/tests/cacheHelper.test.ts +135 -104
  79. package/tests/cacheKeys.test.ts +41 -0
  80. package/tests/cipher.test.ts +90 -89
  81. package/tests/dbHelper-advanced.test.ts +140 -134
  82. package/tests/dbHelper-all-array-types.test.ts +316 -0
  83. package/tests/dbHelper-array-serialization.test.ts +258 -0
  84. package/tests/dbHelper-columns.test.ts +56 -55
  85. package/tests/dbHelper-execute.test.ts +45 -44
  86. package/tests/dbHelper-joins.test.ts +124 -119
  87. package/tests/fields-redis-cache.test.ts +29 -27
  88. package/tests/fields-validate.test.ts +38 -38
  89. package/tests/getClientIp.test.ts +54 -0
  90. package/tests/integration.test.ts +69 -67
  91. package/tests/jwt.test.ts +27 -26
  92. package/tests/logger.test.ts +267 -34
  93. package/tests/rateLimit-hook.test.ts +477 -0
  94. package/tests/redisHelper.test.ts +187 -188
  95. package/tests/redisKeys.test.ts +6 -73
  96. package/tests/scanConfig.test.ts +144 -0
  97. package/tests/sqlBuilder-advanced.test.ts +217 -215
  98. package/tests/sqlBuilder.test.ts +92 -91
  99. package/tests/sync-connection.test.ts +29 -29
  100. package/tests/syncDb-apply.test.ts +97 -96
  101. package/tests/syncDb-array-number.test.ts +160 -0
  102. package/tests/syncDb-constants.test.ts +48 -47
  103. package/tests/syncDb-ddl.test.ts +99 -98
  104. package/tests/syncDb-helpers.test.ts +29 -28
  105. package/tests/syncDb-schema.test.ts +61 -60
  106. package/tests/syncDb-types.test.ts +60 -59
  107. package/tests/syncMenu-paths.test.ts +68 -0
  108. package/tests/util.test.ts +42 -41
  109. package/tests/validator-array-number.test.ts +310 -0
  110. package/tests/validator-default.test.ts +373 -0
  111. package/tests/validator.test.ts +271 -266
  112. package/tsconfig.json +4 -5
  113. package/types/api.d.ts +7 -12
  114. package/types/befly.d.ts +60 -13
  115. package/types/cache.d.ts +8 -4
  116. package/types/common.d.ts +17 -9
  117. package/types/context.d.ts +2 -2
  118. package/types/crypto.d.ts +23 -0
  119. package/types/database.d.ts +19 -19
  120. package/types/hook.d.ts +2 -2
  121. package/types/jwt.d.ts +118 -0
  122. package/types/logger.d.ts +30 -0
  123. package/types/plugin.d.ts +4 -4
  124. package/types/redis.d.ts +7 -3
  125. package/types/roleApisCache.ts +23 -0
  126. package/types/sync.d.ts +10 -10
  127. package/types/table.d.ts +50 -9
  128. package/types/validate.d.ts +69 -0
  129. package/utils/addonHelper.ts +90 -0
  130. package/utils/arrayKeysToCamel.ts +18 -0
  131. package/utils/calcPerfTime.ts +13 -0
  132. package/utils/configTypes.ts +3 -0
  133. package/utils/cors.ts +19 -0
  134. package/utils/fieldClear.ts +75 -0
  135. package/utils/genShortId.ts +12 -0
  136. package/utils/getClientIp.ts +45 -0
  137. package/utils/keysToCamel.ts +22 -0
  138. package/utils/keysToSnake.ts +22 -0
  139. package/utils/modules.ts +98 -0
  140. package/utils/pickFields.ts +19 -0
  141. package/utils/process.ts +56 -0
  142. package/utils/regex.ts +225 -0
  143. package/utils/response.ts +115 -0
  144. package/utils/route.ts +23 -0
  145. package/utils/scanConfig.ts +142 -0
  146. package/utils/scanFiles.ts +48 -0
  147. package/.prettierignore +0 -2
  148. package/.prettierrc +0 -12
  149. package/docs/1-/345/237/272/346/234/254/344/273/213/347/273/215.md +0 -35
  150. package/docs/2-/345/210/235/346/255/245/344/275/223/351/252/214.md +0 -64
  151. package/docs/3-/347/254/254/344/270/200/344/270/252/346/216/245/345/217/243.md +0 -46
  152. package/docs/4-/346/223/215/344/275/234/346/225/260/346/215/256/345/272/223.md +0 -172
  153. package/hooks/requestLogger.ts +0 -84
  154. package/types/index.ts +0 -24
  155. package/util.ts +0 -283
@@ -9,170 +9,171 @@
9
9
  * 注意:这些是模拟测试,实际数据库操作需要集成测试
10
10
  */
11
11
 
12
- import { describe, test, expect, beforeAll, mock } from 'bun:test';
13
- import { setDbType } from '../sync/syncDb/constants.js';
12
+ import { describe, test, expect, beforeAll } from "bun:test";
13
+
14
+ import { setDbType } from "../sync/syncDb/constants.js";
14
15
 
15
16
  // 设置数据库类型为 MySQL
16
- setDbType('mysql');
17
+ setDbType("mysql");
17
18
 
18
19
  let tableExists: any;
19
20
  let getTableColumns: any;
20
21
  let getTableIndexes: any;
21
22
 
22
23
  beforeAll(async () => {
23
- const schema = await import('../sync/syncDb/schema.js');
24
+ const schema = await import("../sync/syncDb/schema.js");
24
25
  tableExists = schema.tableExists;
25
26
  getTableColumns = schema.getTableColumns;
26
27
  getTableIndexes = schema.getTableIndexes;
27
28
  });
28
29
 
29
- describe('tableExists', () => {
30
- test('sql 客户端未初始化时抛出错误', async () => {
30
+ describe("tableExists", () => {
31
+ test("sql 客户端未初始化时抛出错误", async () => {
31
32
  try {
32
- await tableExists(null, 'user');
33
+ await tableExists(null, "user");
33
34
  expect(true).toBe(false); // 不应该到这里
34
35
  } catch (error: any) {
35
- expect(error.message).toBe('SQL 客户端未初始化');
36
+ expect(error.message).toBe("SQL 客户端未初始化");
36
37
  }
37
38
  });
38
39
 
39
- test('传入有效 sql 客户端时正常执行', async () => {
40
+ test("传入有效 sql 客户端时正常执行", async () => {
40
41
  // 创建模拟 SQL 客户端
41
42
  const mockSql = Object.assign(
42
- async function (strings: TemplateStringsArray, ...values: any[]) {
43
+ async function (_strings: TemplateStringsArray, ..._values: any[]) {
43
44
  // 模拟 MySQL 查询返回
44
45
  return [{ count: 1 }];
45
46
  },
46
47
  {
47
- unsafe: async (query: string) => []
48
+ unsafe: async (_query: string) => []
48
49
  }
49
50
  );
50
51
 
51
- const result = await tableExists(mockSql, 'user', 'test_db');
52
+ const result = await tableExists(mockSql, "user", "test_db");
52
53
  expect(result).toBe(true);
53
54
  });
54
55
 
55
- test('表不存在时返回 false', async () => {
56
+ test("表不存在时返回 false", async () => {
56
57
  const mockSql = Object.assign(
57
- async function (strings: TemplateStringsArray, ...values: any[]) {
58
+ async function (_strings: TemplateStringsArray, ..._values: any[]) {
58
59
  return [{ count: 0 }];
59
60
  },
60
61
  {
61
- unsafe: async (query: string) => []
62
+ unsafe: async (_query: string) => []
62
63
  }
63
64
  );
64
65
 
65
- const result = await tableExists(mockSql, 'nonexistent', 'test_db');
66
+ const result = await tableExists(mockSql, "nonexistent", "test_db");
66
67
  expect(result).toBe(false);
67
68
  });
68
69
  });
69
70
 
70
- describe('getTableColumns', () => {
71
- test('返回正确的列信息结构', async () => {
71
+ describe("getTableColumns", () => {
72
+ test("返回正确的列信息结构", async () => {
72
73
  const mockSql = Object.assign(
73
- async function (strings: TemplateStringsArray, ...values: any[]) {
74
+ async function (_strings: TemplateStringsArray, ..._values: any[]) {
74
75
  // 模拟 MySQL information_schema 返回
75
76
  return [
76
77
  {
77
- COLUMN_NAME: 'id',
78
- DATA_TYPE: 'bigint',
78
+ COLUMN_NAME: "id",
79
+ DATA_TYPE: "bigint",
79
80
  CHARACTER_MAXIMUM_LENGTH: null,
80
- IS_NULLABLE: 'NO',
81
+ IS_NULLABLE: "NO",
81
82
  COLUMN_DEFAULT: null,
82
- COLUMN_COMMENT: '主键ID',
83
- COLUMN_TYPE: 'bigint unsigned'
83
+ COLUMN_COMMENT: "主键ID",
84
+ COLUMN_TYPE: "bigint unsigned"
84
85
  },
85
86
  {
86
- COLUMN_NAME: 'user_name',
87
- DATA_TYPE: 'varchar',
87
+ COLUMN_NAME: "user_name",
88
+ DATA_TYPE: "varchar",
88
89
  CHARACTER_MAXIMUM_LENGTH: 50,
89
- IS_NULLABLE: 'NO',
90
- COLUMN_DEFAULT: '',
91
- COLUMN_COMMENT: '用户名',
92
- COLUMN_TYPE: 'varchar(50)'
90
+ IS_NULLABLE: "NO",
91
+ COLUMN_DEFAULT: "",
92
+ COLUMN_COMMENT: "用户名",
93
+ COLUMN_TYPE: "varchar(50)"
93
94
  },
94
95
  {
95
- COLUMN_NAME: 'age',
96
- DATA_TYPE: 'bigint',
96
+ COLUMN_NAME: "age",
97
+ DATA_TYPE: "bigint",
97
98
  CHARACTER_MAXIMUM_LENGTH: null,
98
- IS_NULLABLE: 'YES',
99
- COLUMN_DEFAULT: '0',
100
- COLUMN_COMMENT: '年龄',
101
- COLUMN_TYPE: 'bigint'
99
+ IS_NULLABLE: "YES",
100
+ COLUMN_DEFAULT: "0",
101
+ COLUMN_COMMENT: "年龄",
102
+ COLUMN_TYPE: "bigint"
102
103
  }
103
104
  ];
104
105
  },
105
106
  {
106
- unsafe: async (query: string) => []
107
+ unsafe: async (_query: string) => []
107
108
  }
108
109
  );
109
110
 
110
- const columns = await getTableColumns(mockSql, 'user', 'test_db');
111
+ const columns = await getTableColumns(mockSql, "user", "test_db");
111
112
 
112
113
  expect(columns.id).toBeDefined();
113
- expect(columns.id.type).toBe('bigint');
114
+ expect(columns.id.type).toBe("bigint");
114
115
  expect(columns.id.nullable).toBe(false);
115
- expect(columns.id.comment).toBe('主键ID');
116
+ expect(columns.id.comment).toBe("主键ID");
116
117
 
117
118
  expect(columns.user_name).toBeDefined();
118
- expect(columns.user_name.type).toBe('varchar');
119
+ expect(columns.user_name.type).toBe("varchar");
119
120
  expect(columns.user_name.max).toBe(50);
120
121
  expect(columns.user_name.nullable).toBe(false);
121
- expect(columns.user_name.defaultValue).toBe('');
122
+ expect(columns.user_name.defaultValue).toBe("");
122
123
 
123
124
  expect(columns.age).toBeDefined();
124
125
  expect(columns.age.nullable).toBe(true);
125
- expect(columns.age.defaultValue).toBe('0');
126
+ expect(columns.age.defaultValue).toBe("0");
126
127
  });
127
128
  });
128
129
 
129
- describe('getTableIndexes', () => {
130
- test('返回正确的索引信息结构', async () => {
130
+ describe("getTableIndexes", () => {
131
+ test("返回正确的索引信息结构", async () => {
131
132
  const mockSql = Object.assign(
132
- async function (strings: TemplateStringsArray, ...values: any[]) {
133
+ async function (_strings: TemplateStringsArray, ..._values: any[]) {
133
134
  // 模拟 MySQL information_schema.STATISTICS 返回
134
135
  // 注意:PRIMARY 索引被排除
135
136
  return [
136
- { INDEX_NAME: 'idx_created_at', COLUMN_NAME: 'created_at' },
137
- { INDEX_NAME: 'idx_user_name', COLUMN_NAME: 'user_name' }
137
+ { INDEX_NAME: "idx_created_at", COLUMN_NAME: "created_at" },
138
+ { INDEX_NAME: "idx_user_name", COLUMN_NAME: "user_name" }
138
139
  ];
139
140
  },
140
141
  {
141
- unsafe: async (query: string) => []
142
+ unsafe: async (_query: string) => []
142
143
  }
143
144
  );
144
145
 
145
- const indexes = await getTableIndexes(mockSql, 'user', 'test_db');
146
+ const indexes = await getTableIndexes(mockSql, "user", "test_db");
146
147
 
147
148
  // PRIMARY 索引被排除,不应存在
148
149
  expect(indexes.PRIMARY).toBeUndefined();
149
150
 
150
151
  expect(indexes.idx_created_at).toBeDefined();
151
- expect(indexes.idx_created_at).toContain('created_at');
152
+ expect(indexes.idx_created_at).toContain("created_at");
152
153
 
153
154
  expect(indexes.idx_user_name).toBeDefined();
154
- expect(indexes.idx_user_name).toContain('user_name');
155
+ expect(indexes.idx_user_name).toContain("user_name");
155
156
  });
156
157
 
157
- test('复合索引包含多个列', async () => {
158
+ test("复合索引包含多个列", async () => {
158
159
  const mockSql = Object.assign(
159
- async function (strings: TemplateStringsArray, ...values: any[]) {
160
+ async function (_strings: TemplateStringsArray, ..._values: any[]) {
160
161
  // 模拟复合索引,同一索引名包含多个列
161
162
  return [
162
- { INDEX_NAME: 'idx_composite', COLUMN_NAME: 'user_id' },
163
- { INDEX_NAME: 'idx_composite', COLUMN_NAME: 'created_at' }
163
+ { INDEX_NAME: "idx_composite", COLUMN_NAME: "user_id" },
164
+ { INDEX_NAME: "idx_composite", COLUMN_NAME: "created_at" }
164
165
  ];
165
166
  },
166
167
  {
167
- unsafe: async (query: string) => []
168
+ unsafe: async (_query: string) => []
168
169
  }
169
170
  );
170
171
 
171
- const indexes = await getTableIndexes(mockSql, 'user', 'test_db');
172
+ const indexes = await getTableIndexes(mockSql, "user", "test_db");
172
173
 
173
174
  expect(indexes.idx_composite).toBeDefined();
174
175
  expect(indexes.idx_composite.length).toBe(2);
175
- expect(indexes.idx_composite).toContain('user_id');
176
- expect(indexes.idx_composite).toContain('created_at');
176
+ expect(indexes.idx_composite).toContain("user_id");
177
+ expect(indexes.idx_composite).toContain("created_at");
177
178
  });
178
179
  });
@@ -8,11 +8,12 @@
8
8
  * - generateDefaultSql
9
9
  */
10
10
 
11
- import { describe, test, expect, beforeAll, afterAll } from 'bun:test';
12
- import { setDbType } from '../sync/syncDb/constants.js';
11
+ import { describe, test, expect, beforeAll } from "bun:test";
12
+
13
+ import { setDbType } from "../sync/syncDb/constants.js";
13
14
 
14
15
  // 设置数据库类型为 MySQL
15
- setDbType('mysql');
16
+ setDbType("mysql");
16
17
 
17
18
  // 动态导入以确保环境变量生效
18
19
  let isStringOrArrayType: any;
@@ -21,118 +22,118 @@ let resolveDefaultValue: any;
21
22
  let generateDefaultSql: any;
22
23
 
23
24
  beforeAll(async () => {
24
- const types = await import('../sync/syncDb/types.js');
25
+ const types = await import("../sync/syncDb/types.js");
25
26
  isStringOrArrayType = types.isStringOrArrayType;
26
27
  getSqlType = types.getSqlType;
27
28
  resolveDefaultValue = types.resolveDefaultValue;
28
29
  generateDefaultSql = types.generateDefaultSql;
29
30
  });
30
31
 
31
- describe('isStringOrArrayType', () => {
32
- test('string 类型返回 true', () => {
33
- expect(isStringOrArrayType('string')).toBe(true);
32
+ describe("isStringOrArrayType", () => {
33
+ test("string 类型返回 true", () => {
34
+ expect(isStringOrArrayType("string")).toBe(true);
34
35
  });
35
36
 
36
- test('array_string 类型返回 true', () => {
37
- expect(isStringOrArrayType('array_string')).toBe(true);
37
+ test("array_string 类型返回 true", () => {
38
+ expect(isStringOrArrayType("array_string")).toBe(true);
38
39
  });
39
40
 
40
- test('number 类型返回 false', () => {
41
- expect(isStringOrArrayType('number')).toBe(false);
41
+ test("number 类型返回 false", () => {
42
+ expect(isStringOrArrayType("number")).toBe(false);
42
43
  });
43
44
 
44
- test('text 类型返回 false', () => {
45
- expect(isStringOrArrayType('text')).toBe(false);
45
+ test("text 类型返回 false", () => {
46
+ expect(isStringOrArrayType("text")).toBe(false);
46
47
  });
47
48
 
48
- test('array_text 类型返回 false', () => {
49
- expect(isStringOrArrayType('array_text')).toBe(false);
49
+ test("array_text 类型返回 false", () => {
50
+ expect(isStringOrArrayType("array_text")).toBe(false);
50
51
  });
51
52
  });
52
53
 
53
- describe('resolveDefaultValue', () => {
54
- test('null 值 + string 类型 => 空字符串', () => {
55
- expect(resolveDefaultValue(null, 'string')).toBe('');
54
+ describe("resolveDefaultValue", () => {
55
+ test("null 值 + string 类型 => 空字符串", () => {
56
+ expect(resolveDefaultValue(null, "string")).toBe("");
56
57
  });
57
58
 
58
- test('null 值 + number 类型 => 0', () => {
59
- expect(resolveDefaultValue(null, 'number')).toBe(0);
59
+ test("null 值 + number 类型 => 0", () => {
60
+ expect(resolveDefaultValue(null, "number")).toBe(0);
60
61
  });
61
62
 
62
63
  test('"null" 字符串 + number 类型 => 0', () => {
63
- expect(resolveDefaultValue('null', 'number')).toBe(0);
64
+ expect(resolveDefaultValue("null", "number")).toBe(0);
64
65
  });
65
66
 
66
67
  test('null 值 + array_string 类型 => "[]"', () => {
67
- expect(resolveDefaultValue(null, 'array_string')).toBe('[]');
68
+ expect(resolveDefaultValue(null, "array_string")).toBe("[]");
68
69
  });
69
70
 
70
71
  test('null 值 + text 类型 => "null"', () => {
71
- expect(resolveDefaultValue(null, 'text')).toBe('null');
72
+ expect(resolveDefaultValue(null, "text")).toBe("null");
72
73
  });
73
74
 
74
75
  test('null 值 + array_text 类型 => "null"(TEXT 不支持默认值)', () => {
75
- expect(resolveDefaultValue(null, 'array_text')).toBe('null');
76
+ expect(resolveDefaultValue(null, "array_text")).toBe("null");
76
77
  });
77
78
 
78
- test('有实际值时直接返回', () => {
79
- expect(resolveDefaultValue('admin', 'string')).toBe('admin');
80
- expect(resolveDefaultValue(100, 'number')).toBe(100);
81
- expect(resolveDefaultValue(0, 'number')).toBe(0);
79
+ test("有实际值时直接返回", () => {
80
+ expect(resolveDefaultValue("admin", "string")).toBe("admin");
81
+ expect(resolveDefaultValue(100, "number")).toBe(100);
82
+ expect(resolveDefaultValue(0, "number")).toBe(0);
82
83
  });
83
84
  });
84
85
 
85
- describe('generateDefaultSql', () => {
86
- test('number 类型生成数字默认值', () => {
87
- expect(generateDefaultSql(0, 'number')).toBe(' DEFAULT 0');
88
- expect(generateDefaultSql(100, 'number')).toBe(' DEFAULT 100');
86
+ describe("generateDefaultSql", () => {
87
+ test("number 类型生成数字默认值", () => {
88
+ expect(generateDefaultSql(0, "number")).toBe(" DEFAULT 0");
89
+ expect(generateDefaultSql(100, "number")).toBe(" DEFAULT 100");
89
90
  });
90
91
 
91
- test('string 类型生成带引号默认值', () => {
92
- expect(generateDefaultSql('admin', 'string')).toBe(" DEFAULT 'admin'");
93
- expect(generateDefaultSql('', 'string')).toBe(" DEFAULT ''");
92
+ test("string 类型生成带引号默认值", () => {
93
+ expect(generateDefaultSql("admin", "string")).toBe(" DEFAULT 'admin'");
94
+ expect(generateDefaultSql("", "string")).toBe(" DEFAULT ''");
94
95
  });
95
96
 
96
- test('text 类型不生成默认值', () => {
97
- expect(generateDefaultSql('null', 'text')).toBe('');
97
+ test("text 类型不生成默认值", () => {
98
+ expect(generateDefaultSql("null", "text")).toBe("");
98
99
  });
99
100
 
100
- test('array_string 类型生成 JSON 数组默认值', () => {
101
- expect(generateDefaultSql('[]', 'array_string')).toBe(" DEFAULT '[]'");
101
+ test("array_string 类型生成 JSON 数组默认值", () => {
102
+ expect(generateDefaultSql("[]", "array_string")).toBe(" DEFAULT '[]'");
102
103
  });
103
104
 
104
- test('array_text 类型不生成默认值(MySQL TEXT 不支持)', () => {
105
- expect(generateDefaultSql('[]', 'array_text')).toBe('');
105
+ test("array_text 类型不生成默认值(MySQL TEXT 不支持)", () => {
106
+ expect(generateDefaultSql("[]", "array_text")).toBe("");
106
107
  });
107
108
 
108
- test('单引号被正确转义', () => {
109
- expect(generateDefaultSql("it's", 'string')).toBe(" DEFAULT 'it''s'");
109
+ test("单引号被正确转义", () => {
110
+ expect(generateDefaultSql("it's", "string")).toBe(" DEFAULT 'it''s'");
110
111
  });
111
112
  });
112
113
 
113
- describe('getSqlType', () => {
114
- test('string 类型带长度', () => {
115
- const result = getSqlType('string', 100);
116
- expect(result).toBe('VARCHAR(100)');
114
+ describe("getSqlType", () => {
115
+ test("string 类型带长度", () => {
116
+ const result = getSqlType("string", 100);
117
+ expect(result).toBe("VARCHAR(100)");
117
118
  });
118
119
 
119
- test('array_string 类型带长度', () => {
120
- const result = getSqlType('array_string', 500);
121
- expect(result).toBe('VARCHAR(500)');
120
+ test("array_string 类型带长度", () => {
121
+ const result = getSqlType("array_string", 500);
122
+ expect(result).toBe("VARCHAR(500)");
122
123
  });
123
124
 
124
- test('number 类型无符号', () => {
125
- const result = getSqlType('number', null, true);
126
- expect(result).toBe('BIGINT UNSIGNED');
125
+ test("number 类型无符号", () => {
126
+ const result = getSqlType("number", null, true);
127
+ expect(result).toBe("BIGINT UNSIGNED");
127
128
  });
128
129
 
129
- test('number 类型有符号', () => {
130
- const result = getSqlType('number', null, false);
131
- expect(result).toBe('BIGINT');
130
+ test("number 类型有符号", () => {
131
+ const result = getSqlType("number", null, false);
132
+ expect(result).toBe("BIGINT");
132
133
  });
133
134
 
134
- test('text 类型', () => {
135
- const result = getSqlType('text', null);
136
- expect(result).toBe('MEDIUMTEXT');
135
+ test("text 类型", () => {
136
+ const result = getSqlType("text", null);
137
+ expect(result).toBe("MEDIUMTEXT");
137
138
  });
138
139
  });
@@ -0,0 +1,68 @@
1
+ import { describe, test, expect, beforeAll, afterAll } from "bun:test";
2
+ import { existsSync, mkdirSync, rmSync, writeFileSync } from "node:fs";
3
+ import { join } from "node:path";
4
+
5
+ import { __test__ } from "../sync/syncMenu.js";
6
+
7
+ const testRootDir = join(process.cwd(), "temp", "test-sync-menu-views");
8
+ const viewsDir = join(testRootDir, "views");
9
+
10
+ function writeVueIndex(dir: string, title: string, order?: number): void {
11
+ if (!existsSync(dir)) {
12
+ mkdirSync(dir, { recursive: true });
13
+ }
14
+
15
+ const metaOrder = typeof order === "number" ? `, order: ${order}` : "";
16
+
17
+ const content = `<script setup>\ndefinePage({ meta: { title: "${title}"${metaOrder} } });\n</script>\n\n<template>\n <div />\n</template>\n`;
18
+ writeFileSync(join(dir, "index.vue"), content, { encoding: "utf8" });
19
+ }
20
+
21
+ beforeAll(() => {
22
+ if (existsSync(testRootDir)) {
23
+ rmSync(testRootDir, { recursive: true, force: true });
24
+ }
25
+ mkdirSync(viewsDir, { recursive: true });
26
+
27
+ // 根级 index:应映射为 prefix 本身(不带尾随 /)
28
+ writeVueIndex(join(viewsDir, "index"), "Root", 2);
29
+
30
+ // 带数字后缀目录:应清理后缀
31
+ writeVueIndex(join(viewsDir, "user_1"), "User", 1);
32
+ });
33
+
34
+ afterAll(() => {
35
+ if (existsSync(testRootDir)) {
36
+ rmSync(testRootDir, { recursive: true, force: true });
37
+ }
38
+ });
39
+
40
+ describe("syncMenu - scanViewsDir paths", () => {
41
+ test("根级 index 不应生成尾随斜杠", async () => {
42
+ const prefix = "/addon/addonAdmin";
43
+ const menus = await __test__.scanViewsDir(viewsDir, prefix);
44
+
45
+ const root = menus.find((m) => m.path === prefix);
46
+ expect(root).toBeDefined();
47
+ expect(root?.path).toBe(prefix);
48
+ });
49
+
50
+ test("目录名 _数字 后缀应被清理", async () => {
51
+ const prefix = "/addon/addonAdmin";
52
+ const menus = await __test__.scanViewsDir(viewsDir, prefix);
53
+
54
+ const user = menus.find((m) => m.path === "/addon/addonAdmin/user");
55
+ expect(user).toBeDefined();
56
+ expect(user?.name).toBe("User");
57
+ expect(user?.sort).toBe(1);
58
+ });
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,99 +1,100 @@
1
- import { describe, test, expect } from 'bun:test';
2
- import { keysToCamel } from 'befly-shared/keysToCamel';
3
- import { keysToSnake } from 'befly-shared/keysToSnake';
4
- import { arrayKeysToCamel } from 'befly-shared/arrayKeysToCamel';
5
- import { calcPerfTime } from 'befly-shared/calcPerfTime';
6
- import { fieldClear } from 'befly-shared/fieldClear';
1
+ import { describe, test, expect } from "bun:test";
7
2
 
8
- describe('Util - keysToCamel', () => {
9
- test('转换对象键名为驼峰', () => {
10
- const result = keysToCamel({ user_name: 'John', user_id: 123 });
11
- expect(result.userName).toBe('John');
3
+ import { arrayKeysToCamel } from "../utils/arrayKeysToCamel.js";
4
+ import { calcPerfTime } from "../utils/calcPerfTime.js";
5
+ import { fieldClear } from "../utils/fieldClear.js";
6
+ import { keysToCamel } from "../utils/keysToCamel.js";
7
+ import { keysToSnake } from "../utils/keysToSnake.js";
8
+
9
+ describe("Util - keysToCamel", () => {
10
+ test("转换对象键名为驼峰", () => {
11
+ const result = keysToCamel({ user_name: "John", user_id: 123 });
12
+ expect(result.userName).toBe("John");
12
13
  expect(result.userId).toBe(123);
13
14
  });
14
15
 
15
- test('保持已有驼峰格式', () => {
16
- const result = keysToCamel({ userName: 'John', userId: 123 });
17
- expect(result.userName).toBe('John');
16
+ test("保持已有驼峰格式", () => {
17
+ const result = keysToCamel({ userName: "John", userId: 123 });
18
+ expect(result.userName).toBe("John");
18
19
  expect(result.userId).toBe(123);
19
20
  });
20
21
 
21
- test('处理空对象', () => {
22
+ test("处理空对象", () => {
22
23
  const result = keysToCamel({});
23
24
  expect(Object.keys(result).length).toBe(0);
24
25
  });
25
26
 
26
- test('处理嵌套对象', () => {
27
- const result = keysToCamel({ user_info: { first_name: 'John' } });
27
+ test("处理嵌套对象", () => {
28
+ const result = keysToCamel({ user_info: { first_name: "John" } });
28
29
  expect(result.userInfo).toBeDefined();
29
30
  });
30
31
  });
31
32
 
32
- describe('Util - keysToSnake', () => {
33
- test('转换对象键名为下划线', () => {
34
- const result = keysToSnake({ userName: 'John', userId: 123 });
35
- expect(result.user_name).toBe('John');
33
+ describe("Util - keysToSnake", () => {
34
+ test("转换对象键名为下划线", () => {
35
+ const result = keysToSnake({ userName: "John", userId: 123 });
36
+ expect(result.user_name).toBe("John");
36
37
  expect(result.user_id).toBe(123);
37
38
  });
38
39
 
39
- test('保持已有下划线格式', () => {
40
- const result = keysToSnake({ user_name: 'John', user_id: 123 });
41
- expect(result.user_name).toBe('John');
40
+ test("保持已有下划线格式", () => {
41
+ const result = keysToSnake({ user_name: "John", user_id: 123 });
42
+ expect(result.user_name).toBe("John");
42
43
  expect(result.user_id).toBe(123);
43
44
  });
44
45
 
45
- test('处理空对象', () => {
46
+ test("处理空对象", () => {
46
47
  const result = keysToSnake({});
47
48
  expect(Object.keys(result).length).toBe(0);
48
49
  });
49
50
  });
50
51
 
51
- describe('Util - arrayKeysToCamel', () => {
52
- test('转换数组中对象键名为驼峰', () => {
52
+ describe("Util - arrayKeysToCamel", () => {
53
+ test("转换数组中对象键名为驼峰", () => {
53
54
  const result = arrayKeysToCamel([
54
- { user_name: 'John', user_id: 1 },
55
- { user_name: 'Jane', user_id: 2 }
55
+ { user_name: "John", user_id: 1 },
56
+ { user_name: "Jane", user_id: 2 }
56
57
  ]);
57
- expect(result[0].userName).toBe('John');
58
+ expect(result[0].userName).toBe("John");
58
59
  expect(result[0].userId).toBe(1);
59
- expect(result[1].userName).toBe('Jane');
60
+ expect(result[1].userName).toBe("Jane");
60
61
  expect(result[1].userId).toBe(2);
61
62
  });
62
63
 
63
- test('处理空数组', () => {
64
+ test("处理空数组", () => {
64
65
  const result = arrayKeysToCamel([]);
65
66
  expect(result.length).toBe(0);
66
67
  });
67
68
  });
68
69
 
69
- describe('Util - fieldClear', () => {
70
- test('移除 null 和 undefined', () => {
71
- const result = fieldClear({ a: 1, b: null, c: undefined, d: 'test' }, { excludeValues: [null, undefined] });
70
+ describe("Util - fieldClear", () => {
71
+ test("移除 null 和 undefined", () => {
72
+ const result = fieldClear({ a: 1, b: null, c: undefined, d: "test" }, { excludeValues: [null, undefined] });
72
73
  expect(result.a).toBe(1);
73
74
  expect(result.b).toBeUndefined();
74
75
  expect(result.c).toBeUndefined();
75
- expect(result.d).toBe('test');
76
+ expect(result.d).toBe("test");
76
77
  });
77
78
 
78
- test('保留指定值', () => {
79
+ test("保留指定值", () => {
79
80
  const result = fieldClear({ a: 1, b: null, c: 0 }, { excludeValues: [null, undefined], keepMap: { c: 0 } });
80
81
  expect(result.a).toBe(1);
81
82
  expect(result.b).toBeUndefined();
82
83
  expect(result.c).toBe(0);
83
84
  });
84
85
 
85
- test('处理空对象', () => {
86
+ test("处理空对象", () => {
86
87
  const result = fieldClear({});
87
88
  expect(Object.keys(result).length).toBe(0);
88
89
  });
89
90
  });
90
91
 
91
- describe('Util - calcPerfTime', () => {
92
- test('计算性能时间', () => {
92
+ describe("Util - calcPerfTime", () => {
93
+ test("计算性能时间", () => {
93
94
  const start = Bun.nanoseconds();
94
95
  const result = calcPerfTime(start);
95
- expect(result).toContain('毫秒');
96
- expect(typeof result).toBe('string');
96
+ expect(result).toContain("毫秒");
97
+ expect(typeof result).toBe("string");
97
98
  expect(result).toMatch(/\d+(\.\d+)?\s*毫秒/);
98
99
  });
99
100
  });