befly 3.2.1 → 3.3.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 (73) hide show
  1. package/bin/index.ts +138 -0
  2. package/checks/conflict.ts +35 -25
  3. package/checks/table.ts +6 -6
  4. package/commands/addon.ts +57 -0
  5. package/commands/build.ts +74 -0
  6. package/commands/dev.ts +94 -0
  7. package/commands/index.ts +252 -0
  8. package/commands/script.ts +303 -0
  9. package/commands/start.ts +80 -0
  10. package/commands/syncApi.ts +327 -0
  11. package/{scripts → commands}/syncDb/apply.ts +2 -2
  12. package/{scripts → commands}/syncDb/constants.ts +13 -7
  13. package/{scripts → commands}/syncDb/ddl.ts +7 -5
  14. package/{scripts → commands}/syncDb/helpers.ts +18 -18
  15. package/{scripts → commands}/syncDb/index.ts +37 -23
  16. package/{scripts → commands}/syncDb/sqlite.ts +1 -1
  17. package/{scripts → commands}/syncDb/state.ts +10 -4
  18. package/{scripts → commands}/syncDb/table.ts +7 -7
  19. package/{scripts → commands}/syncDb/tableCreate.ts +7 -6
  20. package/{scripts → commands}/syncDb/types.ts +5 -5
  21. package/{scripts → commands}/syncDb/version.ts +1 -1
  22. package/commands/syncDb.ts +35 -0
  23. package/commands/syncDev.ts +174 -0
  24. package/commands/syncMenu.ts +368 -0
  25. package/config/env.ts +4 -4
  26. package/config/menu.json +67 -0
  27. package/{utils/crypto.ts → lib/cipher.ts} +16 -67
  28. package/lib/database.ts +296 -0
  29. package/{utils → lib}/dbHelper.ts +102 -56
  30. package/{utils → lib}/jwt.ts +124 -151
  31. package/{utils → lib}/logger.ts +47 -24
  32. package/lib/middleware.ts +271 -0
  33. package/{utils → lib}/redisHelper.ts +4 -4
  34. package/{utils/validate.ts → lib/validator.ts} +101 -78
  35. package/lifecycle/bootstrap.ts +63 -0
  36. package/lifecycle/checker.ts +165 -0
  37. package/lifecycle/cluster.ts +241 -0
  38. package/lifecycle/lifecycle.ts +139 -0
  39. package/lifecycle/loader.ts +513 -0
  40. package/main.ts +14 -12
  41. package/package.json +21 -9
  42. package/paths.ts +34 -0
  43. package/plugins/cache.ts +187 -0
  44. package/plugins/db.ts +4 -4
  45. package/plugins/logger.ts +1 -1
  46. package/plugins/redis.ts +4 -4
  47. package/router/api.ts +155 -0
  48. package/router/root.ts +53 -0
  49. package/router/static.ts +76 -0
  50. package/types/api.d.ts +0 -36
  51. package/types/befly.d.ts +8 -6
  52. package/types/common.d.ts +1 -1
  53. package/types/context.d.ts +3 -3
  54. package/types/util.d.ts +45 -0
  55. package/util.ts +299 -0
  56. package/config/fields.ts +0 -55
  57. package/config/regexAliases.ts +0 -51
  58. package/config/reserved.ts +0 -96
  59. package/scripts/syncDb/tests/constants.test.ts +0 -105
  60. package/scripts/syncDb/tests/ddl.test.ts +0 -134
  61. package/scripts/syncDb/tests/helpers.test.ts +0 -70
  62. package/scripts/syncDb.ts +0 -10
  63. package/types/index.d.ts +0 -450
  64. package/types/index.ts +0 -438
  65. package/types/validator.ts +0 -43
  66. package/utils/colors.ts +0 -221
  67. package/utils/database.ts +0 -348
  68. package/utils/helper.ts +0 -812
  69. package/utils/index.ts +0 -33
  70. package/utils/requestContext.ts +0 -167
  71. /package/{scripts → commands}/syncDb/schema.ts +0 -0
  72. /package/{utils → lib}/sqlBuilder.ts +0 -0
  73. /package/{utils → lib}/xml.ts +0 -0
package/util.ts ADDED
@@ -0,0 +1,299 @@
1
+ /**
2
+ * Befly 核心工具函数集合
3
+ *
4
+ * 本文件整合了框架核心工具函数:
5
+ * - API 响应工具(Yes, No)
6
+ * - 对象操作(pickFields, fieldClear)
7
+ * - 日期时间(calcPerfTime)
8
+ * - 表定义工具(parseRule)
9
+ * - Addon 管理(scanAddons, getAddonDir 等)
10
+ *
11
+ * 注意:
12
+ * - JWT 工具位于 lib/jwt.ts
13
+ * - Logger 位于 lib/logger.ts
14
+ * - Validator 位于 lib/validator.ts
15
+ * - Database 管理位于 lib/database.ts
16
+ */
17
+
18
+ import fs from 'node:fs';
19
+ import { join } from 'pathe';
20
+ import { readdirSync, statSync, readFileSync, existsSync } from 'node:fs';
21
+ import { isEmpty, isPlainObject } from 'es-toolkit/compat';
22
+ import { snakeCase, camelCase, kebabCase } from 'es-toolkit/string';
23
+ import { Env } from './config/env.js';
24
+ import { Logger } from './lib/logger.js';
25
+ import { paths } from './paths.js';
26
+ import type { KeyValue } from './types/common.js';
27
+ import type { JwtPayload, JwtSignOptions, JwtVerifyOptions } from './types/jwt';
28
+ import type { Plugin } from './types/plugin.js';
29
+ import type { ParsedFieldRule } from './types/common.js';
30
+
31
+ // ========================================
32
+ // API 响应工具
33
+ // ========================================
34
+
35
+ /**
36
+ * 成功响应
37
+ */
38
+ export const Yes = <T = any>(msg: string = '', data: T | {} = {}, other: KeyValue = {}): { code: 0; msg: string; data: T | {} } & KeyValue => {
39
+ return {
40
+ ...other,
41
+ code: 0,
42
+ msg: msg,
43
+ data: data
44
+ };
45
+ };
46
+
47
+ /**
48
+ * 失败响应
49
+ */
50
+ export const No = <T = any>(msg: string = '', data: T | {} = {}, other: KeyValue = {}): { code: 1; msg: string; data: T | {} } & KeyValue => {
51
+ return {
52
+ ...other,
53
+ code: 1,
54
+ msg: msg,
55
+ data: data
56
+ };
57
+ };
58
+
59
+ // ========================================
60
+ // 字段转换工具(重新导出 lib/convert.ts)
61
+ // ========================================
62
+
63
+ /**
64
+ * 对象字段名转下划线
65
+ * @param obj - 源对象
66
+ * @returns 字段名转为下划线格式的新对象
67
+ *
68
+ * @example
69
+ * keysToSnake({ userId: 123, userName: 'John' }) // { user_id: 123, user_name: 'John' }
70
+ * keysToSnake({ createdAt: 1697452800000 }) // { created_at: 1697452800000 }
71
+ */
72
+ export const keysToSnake = <T = any>(obj: Record<string, any>): T => {
73
+ if (!obj || !isPlainObject(obj)) return obj as T;
74
+
75
+ const result: any = {};
76
+ for (const [key, value] of Object.entries(obj)) {
77
+ const snakeKey = snakeCase(key);
78
+ result[snakeKey] = value;
79
+ }
80
+ return result;
81
+ };
82
+
83
+ /**
84
+ * 对象字段名转小驼峰
85
+ * @param obj - 源对象
86
+ * @returns 字段名转为小驼峰格式的新对象
87
+ *
88
+ * @example
89
+ * keysToCamel({ user_id: 123, user_name: 'John' }) // { userId: 123, userName: 'John' }
90
+ * keysToCamel({ created_at: 1697452800000 }) // { createdAt: 1697452800000 }
91
+ */
92
+ export const keysToCamel = <T = any>(obj: Record<string, any>): T => {
93
+ if (!obj || !isPlainObject(obj)) return obj as T;
94
+
95
+ const result: any = {};
96
+ for (const [key, value] of Object.entries(obj)) {
97
+ const camelKey = camelCase(key);
98
+ result[camelKey] = value;
99
+ }
100
+ return result;
101
+ };
102
+
103
+ /**
104
+ * 数组对象字段名批量转小驼峰
105
+ * @param arr - 源数组
106
+ * @returns 字段名转为小驼峰格式的新数组
107
+ *
108
+ * @example
109
+ * arrayKeysToCamel([
110
+ * { user_id: 1, user_name: 'John' },
111
+ * { user_id: 2, user_name: 'Jane' }
112
+ * ])
113
+ * // [{ userId: 1, userName: 'John' }, { userId: 2, userName: 'Jane' }]
114
+ */
115
+ export const arrayKeysToCamel = <T = any>(arr: Record<string, any>[]): T[] => {
116
+ if (!arr || !Array.isArray(arr)) return arr as T[];
117
+ return arr.map((item) => keysToCamel<T>(item));
118
+ };
119
+
120
+ // ========================================
121
+ // 对象操作工具
122
+ // ========================================
123
+
124
+ /**
125
+ * 挑选指定字段
126
+ */
127
+ export const pickFields = <T extends Record<string, any>>(obj: T, keys: string[]): Partial<T> => {
128
+ if (!obj || (!isPlainObject(obj) && !Array.isArray(obj))) {
129
+ return {};
130
+ }
131
+
132
+ const result: any = {};
133
+ for (const key of keys) {
134
+ if (key in obj) {
135
+ result[key] = obj[key];
136
+ }
137
+ }
138
+
139
+ return result;
140
+ };
141
+
142
+ /**
143
+ * 字段清理
144
+ */
145
+ export const fieldClear = <T extends Record<string, any> = any>(data: T, excludeValues: any[] = [null, undefined], keepValues: Record<string, any> = {}): Partial<T> => {
146
+ if (!data || !isPlainObject(data)) {
147
+ return {};
148
+ }
149
+
150
+ const result: any = {};
151
+
152
+ for (const [key, value] of Object.entries(data)) {
153
+ if (key in keepValues) {
154
+ if (Object.is(keepValues[key], value)) {
155
+ result[key] = value;
156
+ continue;
157
+ }
158
+ }
159
+
160
+ const shouldExclude = excludeValues.some((excludeVal) => Object.is(excludeVal, value));
161
+ if (shouldExclude) {
162
+ continue;
163
+ }
164
+
165
+ result[key] = value;
166
+ }
167
+
168
+ return result;
169
+ };
170
+
171
+ // ========================================
172
+ // 日期时间工具
173
+ // ========================================
174
+
175
+ /**
176
+ * 计算性能时间差
177
+ */
178
+ export const calcPerfTime = (startTime: number, endTime: number = Bun.nanoseconds()): string => {
179
+ const elapsedMs = (endTime - startTime) / 1_000_000;
180
+
181
+ if (elapsedMs < 1000) {
182
+ return `${elapsedMs.toFixed(2)} 毫秒`;
183
+ } else {
184
+ const elapsedSeconds = elapsedMs / 1000;
185
+ return `${elapsedSeconds.toFixed(2)} 秒`;
186
+ }
187
+ };
188
+
189
+ // ========================================
190
+ // 表定义工具
191
+ // ========================================
192
+
193
+ /**
194
+ * 解析字段规则字符串
195
+ * 格式:"字段名|类型|最小值|最大值|默认值|必填|正则"
196
+ * 注意:只分割前6个|,第7个|之后的所有内容(包括|)都属于正则表达式
197
+ */
198
+ export const parseRule = (rule: string): ParsedFieldRule => {
199
+ const parts: string[] = [];
200
+ let currentPart = '';
201
+ let pipeCount = 0;
202
+
203
+ for (let i = 0; i < rule.length; i++) {
204
+ if (rule[i] === '|' && pipeCount < 6) {
205
+ parts.push(currentPart);
206
+ currentPart = '';
207
+ pipeCount++;
208
+ } else {
209
+ currentPart += rule[i];
210
+ }
211
+ }
212
+ parts.push(currentPart);
213
+
214
+ const [fieldName = '', fieldType = 'string', fieldMinStr = 'null', fieldMaxStr = 'null', fieldDefaultStr = 'null', fieldIndexStr = '0', fieldRegx = 'null'] = parts;
215
+
216
+ const fieldIndex = Number(fieldIndexStr) as 0 | 1;
217
+ const fieldMin = fieldMinStr !== 'null' ? Number(fieldMinStr) : null;
218
+ const fieldMax = fieldMaxStr !== 'null' ? Number(fieldMaxStr) : null;
219
+
220
+ let fieldDefault: any = fieldDefaultStr;
221
+ if (fieldType === 'number' && fieldDefaultStr !== 'null') {
222
+ fieldDefault = Number(fieldDefaultStr);
223
+ }
224
+
225
+ return {
226
+ name: fieldName,
227
+ type: fieldType as 'string' | 'number' | 'text' | 'array_string' | 'array_text',
228
+ min: fieldMin,
229
+ max: fieldMax,
230
+ default: fieldDefault,
231
+ index: fieldIndex,
232
+ regex: fieldRegx !== 'null' ? fieldRegx : null
233
+ };
234
+ };
235
+
236
+ // ========================================
237
+ // Addon 管理工具
238
+ // ========================================
239
+
240
+ /**
241
+ * 扫描所有可用的 addon
242
+ */
243
+ export const scanAddons = (): string[] => {
244
+ const beflyDir = join(paths.projectDir, 'node_modules', '@befly-addon');
245
+
246
+ if (!existsSync(beflyDir)) {
247
+ return [];
248
+ }
249
+
250
+ try {
251
+ return fs
252
+ .readdirSync(beflyDir)
253
+ .filter((name) => {
254
+ // addon 名称格式:admin, demo 等(不带 addon- 前缀)
255
+ const fullPath = join(beflyDir, name);
256
+ try {
257
+ const stat = statSync(fullPath);
258
+ return stat.isDirectory();
259
+ } catch {
260
+ return false;
261
+ }
262
+ })
263
+ .sort();
264
+ } catch {
265
+ return [];
266
+ }
267
+ };
268
+
269
+ /**
270
+ * 获取 addon 的指定子目录路径
271
+ */
272
+ export const getAddonDir = (addonName: string, subDir: string): string => {
273
+ return join(paths.projectDir, 'node_modules', '@befly-addon', addonName, subDir);
274
+ };
275
+
276
+ /**
277
+ * 检查 addon 子目录是否存在
278
+ */
279
+ export const addonDirExists = (addonName: string, subDir: string): boolean => {
280
+ const dir = getAddonDir(addonName, subDir);
281
+ return existsSync(dir) && statSync(dir).isDirectory();
282
+ };
283
+
284
+ /**
285
+ * 获取插件目录列表
286
+ * @param addonsDir - addons 根目录路径
287
+ * @returns 插件名称数组
288
+ */
289
+ export function getAddonDirs(addonsDir: string): string[] {
290
+ // try {
291
+ return readdirSync(addonsDir).filter((name) => {
292
+ const addonPath = path.join(addonsDir, name);
293
+ return statSync(addonPath).isDirectory() && !name.startsWith('_');
294
+ });
295
+ // } catch (error: any) {
296
+ // Logger.error(`读取插件目录失败: ${addonsDir}`, error.message);
297
+ // return [];
298
+ // }
299
+ }
package/config/fields.ts DELETED
@@ -1,55 +0,0 @@
1
- /**
2
- * 通用字段定义
3
- *
4
- * 用于在 API 和表定义中复用常见字段规则
5
- *
6
- * 格式:`字段标签|数据类型|最小值|最大值|默认值|是否必填|正则表达式`
7
- *
8
- * 说明:
9
- * - 字段标签:用于显示的中文名称
10
- * - 数据类型:string、number、boolean、array_string、array_text 等
11
- * - 最小值:string 类型表示最小长度,number 类型表示最小数值,array 类型表示最小元素个数
12
- * - 最大值:string 类型表示最大长度,number 类型表示最大数值,array 类型表示最大元素个数
13
- * - 默认值:字段的默认值,无默认值时填 null
14
- * - 是否必填:0 表示非必填,1 表示必填
15
- * - 正则表达式:用于验证字段值的正则表达式,无验证时填 null
16
- *
17
- * 类型说明:
18
- * - array_string: 短数组,存储为 VARCHAR,建议设置 max 限制(如 0-100)
19
- * - array_text: 长数组,存储为 MEDIUMTEXT,min/max 可设为 null 表示不限制
20
- *
21
- * 正则表达式别名:
22
- * - 使用 @ 前缀可以引用内置正则表达式别名,例如:
23
- * - @number: 纯数字
24
- * - @alphanumeric: 字母+数字
25
- * - @email: 邮箱格式
26
- * - @phone: 中国手机号
27
- * - @chinese: 纯中文
28
- * - 完整别名列表见 config/regexAliases.ts
29
- *
30
- * 示例:
31
- * - '用户ID|array_text|null|null|null|0|@number' - 数字数组
32
- * - '标签|array_string|0|50|null|0|@alphanumeric' - 字母数字数组
33
- */
34
-
35
- export const Fields = {
36
- _id: 'ID|number|1|null|null|1|null',
37
- email: '邮箱|string|5|100|null|1|^[a-zA-Z0-9._%+-]+@[a-zA-Z0-9.-]+\\.[a-zA-Z]{2,}$',
38
- phone: '手机号|string|11|11|null|1|^1[3-9]\\d{9}$',
39
- page: '页码|number|1|9999|1|0|null',
40
- limit: '每页数量|number|1|100|10|0|null',
41
- title: '标题|string|1|200|null|0|null',
42
- description: '描述|string|0|500|null|0|null',
43
- keyword: '关键词|string|1|50|null|1|null',
44
- keywords: '关键词列表|array_string|0|50|null|0|null',
45
- enabled: '启用状态|number|0|1|1|0|^(0|1)$',
46
- date: '日期|string|10|10|null|0|^\\d{4}-\\d{2}-\\d{2}$',
47
- datetime: '日期时间|string|19|25|null|0|^\\d{4}-\\d{2}-\\d{2}T\\d{2}:\\d{2}:\\d{2}',
48
- filename: '文件名|string|1|255|null|0|null',
49
- url: '网址|string|5|500|null|0|^https?://',
50
- tag: '标签|array_string|0|10|null|0|null',
51
- startTime: '开始时间|number|0|9999999999999|null|0|null',
52
- endTime: '结束时间|number|0|9999999999999|null|0|null'
53
- } as const;
54
-
55
- export default Fields;
@@ -1,51 +0,0 @@
1
- /**
2
- * 内置正则表达式别名
3
- *
4
- * 使用方式:在字段定义的 regex 位置使用 @别名 格式
5
- * 例如:'字段名|array_text|null|null|null|0|@number'
6
- */
7
-
8
- export const RegexAliases = {
9
- // 数字类型
10
- number: '^\\d+$', // 纯数字
11
- integer: '^-?\\d+$', // 整数(含负数)
12
- float: '^-?\\d+(\\.\\d+)?$', // 浮点数
13
- positive: '^[1-9]\\d*$', // 正整数(不含0)
14
-
15
- // 字符串类型
16
- word: '^[a-zA-Z]+$', // 纯字母
17
- alphanumeric: '^[a-zA-Z0-9]+$', // 字母+数字
18
- alphanumeric_: '^[a-zA-Z0-9_]+$', // 字母+数字+下划线
19
- lowercase: '^[a-z]+$', // 小写字母
20
- uppercase: '^[A-Z]+$', // 大写字母
21
-
22
- // 中文
23
- chinese: '^[\\u4e00-\\u9fa5]+$', // 纯中文
24
- chinese_word: '^[\\u4e00-\\u9fa5a-zA-Z]+$', // 中文+字母
25
-
26
- // 常用格式
27
- email: '^[a-zA-Z0-9._%+-]+@[a-zA-Z0-9.-]+\\.[a-zA-Z]{2,}$', // 邮箱
28
- phone: '^1[3-9]\\d{9}$', // 中国手机号
29
- url: '^https?://', // URL
30
- ip: '^((25[0-5]|2[0-4]\\d|[01]?\\d\\d?)\\.){3}(25[0-5]|2[0-4]\\d|[01]?\\d\\d?)$', // IPv4
31
-
32
- // 特殊格式
33
- uuid: '^[0-9a-f]{8}-[0-9a-f]{4}-[0-9a-f]{4}-[0-9a-f]{4}-[0-9a-f]{12}$', // UUID
34
- hex: '^[0-9a-fA-F]+$', // 十六进制
35
- base64: '^[A-Za-z0-9+/=]+$', // Base64
36
-
37
- // 日期时间
38
- date: '^\\d{4}-\\d{2}-\\d{2}$', // YYYY-MM-DD
39
- time: '^\\d{2}:\\d{2}:\\d{2}$', // HH:MM:SS
40
- datetime: '^\\d{4}-\\d{2}-\\d{2}T\\d{2}:\\d{2}:\\d{2}', // ISO 8601
41
-
42
- // 代码相关
43
- variable: '^[a-zA-Z_][a-zA-Z0-9_]*$', // 变量名
44
- constant: '^[A-Z][A-Z0-9_]*$', // 常量名(大写)
45
-
46
- // 空值
47
- empty: '^$', // 空字符串
48
- notempty: '.+' // 非空
49
- } as const;
50
-
51
- export type RegexAliasName = keyof typeof RegexAliases;
@@ -1,96 +0,0 @@
1
- /**
2
- * 核心保留名称配置
3
- * 定义框架保留的资源名称,防止用户和 addon 使用
4
- */
5
-
6
- /**
7
- * 保留名称配置
8
- */
9
- export const RESERVED_NAMES = {
10
- /**
11
- * 核心表前缀(禁止用户使用)
12
- */
13
- tablePrefix: ['sys_'],
14
-
15
- /**
16
- * 核心 API 路由前缀(禁止用户使用)
17
- */
18
- apiRoutes: ['/api/health', '/api/tool'],
19
-
20
- /**
21
- * 核心插件名(禁止用户使用)
22
- */
23
- plugins: ['db', 'logger', 'redis', 'tool'],
24
-
25
- /**
26
- * 禁止用作 addon 名称
27
- */
28
- addonNames: ['app', 'api']
29
- } as const;
30
-
31
- /**
32
- * 检测表名是否使用了保留前缀
33
- */
34
- export function isReservedTableName(tableName: string): boolean {
35
- return RESERVED_NAMES.tablePrefix.some((prefix) => tableName.startsWith(prefix));
36
- }
37
-
38
- /**
39
- * 检测 API 路由是否使用了保留路径
40
- */
41
- export function isReservedRoute(route: string): boolean {
42
- // 移除方法前缀(如 POST/GET)
43
- const path = route.replace(/^(GET|POST|PUT|DELETE|PATCH|OPTIONS|HEAD)\//i, '/');
44
- return RESERVED_NAMES.apiRoutes.some((reserved) => path.startsWith(reserved));
45
- }
46
-
47
- /**
48
- * 检测插件名是否使用了保留名称
49
- */
50
- export function isReservedPluginName(pluginName: string): boolean {
51
- // 检测核心插件名
52
- if (RESERVED_NAMES.plugins.includes(pluginName)) {
53
- return true;
54
- }
55
- // 检测是否使用点号命名空间但前缀是保留名称
56
- if (pluginName.includes('.')) {
57
- const prefix = pluginName.split('.')[0];
58
- return RESERVED_NAMES.plugins.includes(prefix);
59
- }
60
- return false;
61
- }
62
-
63
- /**
64
- * 检测 addon 名称是否使用了保留名称
65
- */
66
- export function isReservedAddonName(addonName: string): boolean {
67
- return RESERVED_NAMES.addonNames.includes(addonName.toLowerCase());
68
- }
69
-
70
- /**
71
- * 获取保留前缀列表(用于错误提示)
72
- */
73
- export function getReservedTablePrefixes(): string[] {
74
- return [...RESERVED_NAMES.tablePrefix];
75
- }
76
-
77
- /**
78
- * 获取保留路由列表(用于错误提示)
79
- */
80
- export function getReservedRoutes(): string[] {
81
- return [...RESERVED_NAMES.apiRoutes];
82
- }
83
-
84
- /**
85
- * 获取保留插件名列表(用于错误提示)
86
- */
87
- export function getReservedPlugins(): string[] {
88
- return [...RESERVED_NAMES.plugins];
89
- }
90
-
91
- /**
92
- * 获取保留 addon 名称列表(用于错误提示)
93
- */
94
- export function getReservedAddonNames(): string[] {
95
- return [...RESERVED_NAMES.addonNames];
96
- }
@@ -1,105 +0,0 @@
1
- /**
2
- * syncDb 常量模块测试
3
- */
4
-
5
- import { describe, test, expect } from 'bun:test';
6
- import { DB_VERSION_REQUIREMENTS, SYSTEM_FIELDS, SYSTEM_INDEX_FIELDS, CHANGE_TYPE_LABELS, MYSQL_TABLE_CONFIG, IS_MYSQL, IS_PG, IS_SQLITE, typeMapping } from '../constants.js';
7
-
8
- describe('syncDb/constants', () => {
9
- describe('数据库版本要求', () => {
10
- test('应定义 MySQL 最低版本', () => {
11
- expect(DB_VERSION_REQUIREMENTS.MYSQL_MIN_MAJOR).toBe(8);
12
- });
13
-
14
- test('应定义 PostgreSQL 最低版本', () => {
15
- expect(DB_VERSION_REQUIREMENTS.POSTGRES_MIN_MAJOR).toBe(17);
16
- });
17
-
18
- test('应定义 SQLite 最低版本', () => {
19
- expect(DB_VERSION_REQUIREMENTS.SQLITE_MIN_VERSION).toBe('3.50.0');
20
- expect(DB_VERSION_REQUIREMENTS.SQLITE_MIN_VERSION_NUM).toBe(35000);
21
- });
22
- });
23
-
24
- describe('系统字段定义', () => {
25
- test('应包含 5 个系统字段', () => {
26
- const fields = Object.keys(SYSTEM_FIELDS);
27
- expect(fields).toHaveLength(5);
28
- expect(fields).toContain('ID');
29
- expect(fields).toContain('CREATED_AT');
30
- expect(fields).toContain('UPDATED_AT');
31
- expect(fields).toContain('DELETED_AT');
32
- expect(fields).toContain('STATE');
33
- });
34
-
35
- test('每个系统字段应有名称和注释', () => {
36
- expect(SYSTEM_FIELDS.ID.name).toBe('id');
37
- expect(SYSTEM_FIELDS.ID.comment).toBe('主键ID');
38
- });
39
- });
40
-
41
- describe('系统索引字段', () => {
42
- test('应包含 3 个索引字段', () => {
43
- expect(SYSTEM_INDEX_FIELDS).toHaveLength(3);
44
- expect(SYSTEM_INDEX_FIELDS).toContain('created_at');
45
- expect(SYSTEM_INDEX_FIELDS).toContain('updated_at');
46
- expect(SYSTEM_INDEX_FIELDS).toContain('state');
47
- });
48
- });
49
-
50
- describe('变更类型标签', () => {
51
- test('应包含所有变更类型', () => {
52
- expect(CHANGE_TYPE_LABELS.length).toBe('长度');
53
- expect(CHANGE_TYPE_LABELS.datatype).toBe('类型');
54
- expect(CHANGE_TYPE_LABELS.comment).toBe('注释');
55
- expect(CHANGE_TYPE_LABELS.default).toBe('默认值');
56
- });
57
- });
58
-
59
- describe('MySQL 表配置', () => {
60
- test('应有默认配置', () => {
61
- expect(MYSQL_TABLE_CONFIG.ENGINE).toBeDefined();
62
- expect(MYSQL_TABLE_CONFIG.CHARSET).toBeDefined();
63
- expect(MYSQL_TABLE_CONFIG.COLLATE).toBeDefined();
64
- });
65
-
66
- test('默认引擎应为 InnoDB', () => {
67
- expect(MYSQL_TABLE_CONFIG.ENGINE).toMatch(/InnoDB/i);
68
- });
69
-
70
- test('默认字符集应为 utf8mb4', () => {
71
- expect(MYSQL_TABLE_CONFIG.CHARSET).toMatch(/utf8mb4/i);
72
- });
73
- });
74
-
75
- describe('数据库类型检测', () => {
76
- test('IS_MYSQL, IS_PG, IS_SQLITE 三者只有一个为 true', () => {
77
- const trueCount = [IS_MYSQL, IS_PG, IS_SQLITE].filter(Boolean).length;
78
- expect(trueCount).toBe(1);
79
- });
80
-
81
- test('类型映射应包含所有字段类型', () => {
82
- expect(typeMapping.number).toBeDefined();
83
- expect(typeMapping.string).toBeDefined();
84
- expect(typeMapping.text).toBeDefined();
85
- expect(typeMapping.array_string).toBeDefined();
86
- expect(typeMapping.array_text).toBeDefined();
87
- });
88
-
89
- test('不同数据库的类型映射应不同', () => {
90
- if (IS_MYSQL) {
91
- expect(typeMapping.number).toBe('BIGINT');
92
- expect(typeMapping.string).toBe('VARCHAR');
93
- expect(typeMapping.text).toBe('MEDIUMTEXT');
94
- } else if (IS_PG) {
95
- expect(typeMapping.number).toBe('BIGINT');
96
- expect(typeMapping.string).toBe('character varying');
97
- expect(typeMapping.text).toBe('TEXT');
98
- } else if (IS_SQLITE) {
99
- expect(typeMapping.number).toBe('INTEGER');
100
- expect(typeMapping.string).toBe('TEXT');
101
- expect(typeMapping.text).toBe('TEXT');
102
- }
103
- });
104
- });
105
- });