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.
- package/bin/index.ts +138 -0
- package/checks/conflict.ts +35 -25
- package/checks/table.ts +6 -6
- package/commands/addon.ts +57 -0
- package/commands/build.ts +74 -0
- package/commands/dev.ts +94 -0
- package/commands/index.ts +252 -0
- package/commands/script.ts +303 -0
- package/commands/start.ts +80 -0
- package/commands/syncApi.ts +327 -0
- package/{scripts → commands}/syncDb/apply.ts +2 -2
- package/{scripts → commands}/syncDb/constants.ts +13 -7
- package/{scripts → commands}/syncDb/ddl.ts +7 -5
- package/{scripts → commands}/syncDb/helpers.ts +18 -18
- package/{scripts → commands}/syncDb/index.ts +37 -23
- package/{scripts → commands}/syncDb/sqlite.ts +1 -1
- package/{scripts → commands}/syncDb/state.ts +10 -4
- package/{scripts → commands}/syncDb/table.ts +7 -7
- package/{scripts → commands}/syncDb/tableCreate.ts +7 -6
- package/{scripts → commands}/syncDb/types.ts +5 -5
- package/{scripts → commands}/syncDb/version.ts +1 -1
- package/commands/syncDb.ts +35 -0
- package/commands/syncDev.ts +174 -0
- package/commands/syncMenu.ts +368 -0
- package/config/env.ts +4 -4
- package/config/menu.json +67 -0
- package/{utils/crypto.ts → lib/cipher.ts} +16 -67
- package/lib/database.ts +296 -0
- package/{utils → lib}/dbHelper.ts +102 -56
- package/{utils → lib}/jwt.ts +124 -151
- package/{utils → lib}/logger.ts +47 -24
- package/lib/middleware.ts +271 -0
- package/{utils → lib}/redisHelper.ts +4 -4
- package/{utils/validate.ts → lib/validator.ts} +101 -78
- package/lifecycle/bootstrap.ts +63 -0
- package/lifecycle/checker.ts +165 -0
- package/lifecycle/cluster.ts +241 -0
- package/lifecycle/lifecycle.ts +139 -0
- package/lifecycle/loader.ts +513 -0
- package/main.ts +14 -12
- package/package.json +21 -9
- package/paths.ts +34 -0
- package/plugins/cache.ts +187 -0
- package/plugins/db.ts +4 -4
- package/plugins/logger.ts +1 -1
- package/plugins/redis.ts +4 -4
- package/router/api.ts +155 -0
- package/router/root.ts +53 -0
- package/router/static.ts +76 -0
- package/types/api.d.ts +0 -36
- package/types/befly.d.ts +8 -6
- package/types/common.d.ts +1 -1
- package/types/context.d.ts +3 -3
- package/types/util.d.ts +45 -0
- package/util.ts +299 -0
- package/config/fields.ts +0 -55
- package/config/regexAliases.ts +0 -51
- package/config/reserved.ts +0 -96
- package/scripts/syncDb/tests/constants.test.ts +0 -105
- package/scripts/syncDb/tests/ddl.test.ts +0 -134
- package/scripts/syncDb/tests/helpers.test.ts +0 -70
- package/scripts/syncDb.ts +0 -10
- package/types/index.d.ts +0 -450
- package/types/index.ts +0 -438
- package/types/validator.ts +0 -43
- package/utils/colors.ts +0 -221
- package/utils/database.ts +0 -348
- package/utils/helper.ts +0 -812
- package/utils/index.ts +0 -33
- package/utils/requestContext.ts +0 -167
- /package/{scripts → commands}/syncDb/schema.ts +0 -0
- /package/{utils → lib}/sqlBuilder.ts +0 -0
- /package/{utils → lib}/xml.ts +0 -0
package/utils/helper.ts
DELETED
|
@@ -1,812 +0,0 @@
|
|
|
1
|
-
/**
|
|
2
|
-
* Befly 辅助工具集
|
|
3
|
-
*
|
|
4
|
-
* 本文件整合了所有辅助工具函数,包括:
|
|
5
|
-
* - API 响应工具
|
|
6
|
-
* - 环境判断工具
|
|
7
|
-
* - 类型判断工具
|
|
8
|
-
* - 对象操作工具
|
|
9
|
-
* - 日期时间工具
|
|
10
|
-
* - 命名转换工具
|
|
11
|
-
* - 数据清洗工具
|
|
12
|
-
* - Addon 管理工具
|
|
13
|
-
* - Plugin 管理工具
|
|
14
|
-
* - 表定义工具
|
|
15
|
-
*/
|
|
16
|
-
|
|
17
|
-
import fs from 'node:fs';
|
|
18
|
-
import { join } from 'node:path';
|
|
19
|
-
import { paths } from '../paths.js';
|
|
20
|
-
import type { Plugin } from '../types/plugin.js';
|
|
21
|
-
import type { ParsedFieldRule, KeyValue } from '../types/common.js';
|
|
22
|
-
|
|
23
|
-
// ========================================
|
|
24
|
-
// API 响应工具
|
|
25
|
-
// ========================================
|
|
26
|
-
|
|
27
|
-
/**
|
|
28
|
-
* 成功响应
|
|
29
|
-
* @param msg - 响应消息
|
|
30
|
-
* @param data - 响应数据
|
|
31
|
-
* @param other - 其他字段
|
|
32
|
-
* @returns 成功响应对象 { code: 0, msg, data, ...other }
|
|
33
|
-
*/
|
|
34
|
-
export const Yes = <T = any>(msg: string = '', data: T | {} = {}, other: KeyValue = {}): { code: 0; msg: string; data: T | {} } & KeyValue => {
|
|
35
|
-
return {
|
|
36
|
-
...other,
|
|
37
|
-
code: 0,
|
|
38
|
-
msg: msg,
|
|
39
|
-
data: data
|
|
40
|
-
};
|
|
41
|
-
};
|
|
42
|
-
|
|
43
|
-
/**
|
|
44
|
-
* 失败响应
|
|
45
|
-
* @param msg - 错误消息
|
|
46
|
-
* @param data - 错误数据
|
|
47
|
-
* @param other - 其他字段
|
|
48
|
-
* @returns 失败响应对象 { code: 1, msg, data, ...other }
|
|
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
|
-
// 环境判断工具
|
|
61
|
-
// ========================================
|
|
62
|
-
|
|
63
|
-
/**
|
|
64
|
-
* 判断是否开启调试模式
|
|
65
|
-
* @returns 是否开启调试模式
|
|
66
|
-
*
|
|
67
|
-
* 判断逻辑:
|
|
68
|
-
* 1. DEBUG=1 或 DEBUG=true 时返回 true
|
|
69
|
-
* 2. development 环境下默认返回 true(除非 DEBUG=0)
|
|
70
|
-
* 3. 其他情况返回 false
|
|
71
|
-
*
|
|
72
|
-
* @example
|
|
73
|
-
* // DEBUG=1
|
|
74
|
-
* isDebug() // true
|
|
75
|
-
*
|
|
76
|
-
* // NODE_ENV=development
|
|
77
|
-
* isDebug() // true
|
|
78
|
-
*
|
|
79
|
-
* // NODE_ENV=development, DEBUG=0
|
|
80
|
-
* isDebug() // false
|
|
81
|
-
*
|
|
82
|
-
* // NODE_ENV=production
|
|
83
|
-
* isDebug() // false
|
|
84
|
-
*/
|
|
85
|
-
export function isDebug(): boolean {
|
|
86
|
-
return process.env.DEBUG === '1' || process.env.DEBUG === 'true' || (process.env.NODE_ENV === 'development' && process.env.DEBUG !== '0');
|
|
87
|
-
}
|
|
88
|
-
|
|
89
|
-
// ========================================
|
|
90
|
-
// 类型判断工具
|
|
91
|
-
// ========================================
|
|
92
|
-
|
|
93
|
-
/**
|
|
94
|
-
* 类型判断
|
|
95
|
-
* @param value - 要判断的值
|
|
96
|
-
* @param type - 期望的类型
|
|
97
|
-
* @returns 是否匹配指定类型
|
|
98
|
-
*
|
|
99
|
-
* @example
|
|
100
|
-
* isType(123, 'number') // true
|
|
101
|
-
* isType('hello', 'string') // true
|
|
102
|
-
* isType([], 'array') // true
|
|
103
|
-
* isType({}, 'object') // true
|
|
104
|
-
* isType(null, 'null') // true
|
|
105
|
-
* isType(undefined, 'undefined') // true
|
|
106
|
-
* isType(NaN, 'nan') // true
|
|
107
|
-
* isType(42, 'integer') // true
|
|
108
|
-
* isType(3.14, 'float') // true
|
|
109
|
-
* isType(10, 'positive') // true
|
|
110
|
-
* isType(-5, 'negative') // true
|
|
111
|
-
* isType(0, 'zero') // true
|
|
112
|
-
* isType('', 'empty') // true
|
|
113
|
-
* isType(null, 'empty') // true
|
|
114
|
-
* isType(true, 'truthy') // true
|
|
115
|
-
* isType(false, 'falsy') // true
|
|
116
|
-
* isType('str', 'primitive') // true
|
|
117
|
-
* isType({}, 'reference') // true
|
|
118
|
-
*/
|
|
119
|
-
export const isType = (value: any, type: string): boolean => {
|
|
120
|
-
const actualType = Object.prototype.toString.call(value).slice(8, -1).toLowerCase();
|
|
121
|
-
const expectedType = String(type).toLowerCase();
|
|
122
|
-
|
|
123
|
-
// 语义类型单独处理
|
|
124
|
-
switch (expectedType) {
|
|
125
|
-
case 'function':
|
|
126
|
-
return typeof value === 'function';
|
|
127
|
-
case 'nan':
|
|
128
|
-
return typeof value === 'number' && Number.isNaN(value);
|
|
129
|
-
case 'empty':
|
|
130
|
-
return value === '' || value === null || value === undefined;
|
|
131
|
-
case 'integer':
|
|
132
|
-
return Number.isInteger(value);
|
|
133
|
-
case 'float':
|
|
134
|
-
return typeof value === 'number' && !Number.isInteger(value) && !Number.isNaN(value);
|
|
135
|
-
case 'positive':
|
|
136
|
-
return typeof value === 'number' && value > 0;
|
|
137
|
-
case 'negative':
|
|
138
|
-
return typeof value === 'number' && value < 0;
|
|
139
|
-
case 'zero':
|
|
140
|
-
return value === 0;
|
|
141
|
-
case 'truthy':
|
|
142
|
-
return !!value;
|
|
143
|
-
case 'falsy':
|
|
144
|
-
return !value;
|
|
145
|
-
case 'primitive':
|
|
146
|
-
return value !== Object(value);
|
|
147
|
-
case 'reference':
|
|
148
|
-
return value === Object(value);
|
|
149
|
-
default:
|
|
150
|
-
return actualType === expectedType;
|
|
151
|
-
}
|
|
152
|
-
};
|
|
153
|
-
|
|
154
|
-
/**
|
|
155
|
-
* 判断是否为空对象
|
|
156
|
-
* @param obj - 要判断的值
|
|
157
|
-
* @returns 是否为空对象
|
|
158
|
-
*
|
|
159
|
-
* @example
|
|
160
|
-
* isEmptyObject({}) // true
|
|
161
|
-
* isEmptyObject({ a: 1 }) // false
|
|
162
|
-
* isEmptyObject([]) // false
|
|
163
|
-
* isEmptyObject(null) // false
|
|
164
|
-
*/
|
|
165
|
-
export const isEmptyObject = (obj: any): boolean => {
|
|
166
|
-
if (!isType(obj, 'object')) {
|
|
167
|
-
return false;
|
|
168
|
-
}
|
|
169
|
-
return Object.keys(obj).length === 0;
|
|
170
|
-
};
|
|
171
|
-
|
|
172
|
-
/**
|
|
173
|
-
* 判断是否为空数组
|
|
174
|
-
* @param arr - 要判断的值
|
|
175
|
-
* @returns 是否为空数组
|
|
176
|
-
*
|
|
177
|
-
* @example
|
|
178
|
-
* isEmptyArray([]) // true
|
|
179
|
-
* isEmptyArray([1, 2]) // false
|
|
180
|
-
* isEmptyArray({}) // false
|
|
181
|
-
* isEmptyArray(null) // false
|
|
182
|
-
*/
|
|
183
|
-
export const isEmptyArray = (arr: any): boolean => {
|
|
184
|
-
if (!isType(arr, 'array')) {
|
|
185
|
-
return false;
|
|
186
|
-
}
|
|
187
|
-
return arr.length === 0;
|
|
188
|
-
};
|
|
189
|
-
|
|
190
|
-
// ========================================
|
|
191
|
-
// 对象操作工具
|
|
192
|
-
// ========================================
|
|
193
|
-
|
|
194
|
-
/**
|
|
195
|
-
* 挑选指定字段
|
|
196
|
-
* @param obj - 源对象
|
|
197
|
-
* @param keys - 要挑选的字段名数组
|
|
198
|
-
* @returns 包含指定字段的新对象
|
|
199
|
-
*
|
|
200
|
-
* @example
|
|
201
|
-
* pickFields({ a: 1, b: 2, c: 3 }, ['a', 'c']) // { a: 1, c: 3 }
|
|
202
|
-
* pickFields({ name: 'John', age: 30 }, ['name']) // { name: 'John' }
|
|
203
|
-
*/
|
|
204
|
-
export const pickFields = <T extends Record<string, any>>(obj: T, keys: string[]): Partial<T> => {
|
|
205
|
-
if (!obj || (!isType(obj, 'object') && !isType(obj, 'array'))) {
|
|
206
|
-
return {};
|
|
207
|
-
}
|
|
208
|
-
|
|
209
|
-
const result: any = {};
|
|
210
|
-
for (const key of keys) {
|
|
211
|
-
if (key in obj) {
|
|
212
|
-
result[key] = obj[key];
|
|
213
|
-
}
|
|
214
|
-
}
|
|
215
|
-
|
|
216
|
-
return result;
|
|
217
|
-
};
|
|
218
|
-
|
|
219
|
-
/**
|
|
220
|
-
* 字段清理 - 排除指定值,保留特定字段的特定值
|
|
221
|
-
* @param data - 源数据对象(只接受对象类型)
|
|
222
|
-
* @param excludeValues - 要排除的值数组
|
|
223
|
-
* @param keepValues - 要保留的字段值对象(字段名: 值)
|
|
224
|
-
* @returns 清理后的新对象
|
|
225
|
-
*
|
|
226
|
-
* @example
|
|
227
|
-
* // 排除指定值
|
|
228
|
-
* fieldClear({ a: 1, b: null, c: 3, d: undefined }, [null, undefined], {})
|
|
229
|
-
* // { a: 1, c: 3 }
|
|
230
|
-
*
|
|
231
|
-
* // 保留指定字段的特定值
|
|
232
|
-
* fieldClear({ category: '', name: 'John', recordId: 0, age: 30 }, [null, undefined, '', 0], { category: '', recordId: 0 })
|
|
233
|
-
* // { category: '', name: 'John', recordId: 0, age: 30 }
|
|
234
|
-
*
|
|
235
|
-
* // 排除空字符串和0,但保留特定字段的这些值
|
|
236
|
-
* fieldClear({ a: '', b: 'text', c: 0, d: 5 }, ['', 0], { a: '', c: 0 })
|
|
237
|
-
* // { a: '', b: 'text', c: 0, d: 5 }
|
|
238
|
-
*
|
|
239
|
-
* // 默认排除 null 和 undefined
|
|
240
|
-
* fieldClear({ a: 1, b: null, c: undefined, d: 2 }, [null, undefined], {})
|
|
241
|
-
* // { a: 1, d: 2 }
|
|
242
|
-
*/
|
|
243
|
-
export const fieldClear = <T extends Record<string, any> = any>(data: T, excludeValues: any[] = [null, undefined], keepValues: Record<string, any> = {}): Partial<T> => {
|
|
244
|
-
// 只接受对象类型
|
|
245
|
-
if (!data || !isType(data, 'object')) {
|
|
246
|
-
return {};
|
|
247
|
-
}
|
|
248
|
-
|
|
249
|
-
const result: any = {};
|
|
250
|
-
|
|
251
|
-
for (const [key, value] of Object.entries(data)) {
|
|
252
|
-
// 检查是否在 keepValues 中(保留特定字段的特定值,优先级最高)
|
|
253
|
-
if (key in keepValues) {
|
|
254
|
-
// 使用 Object.is 严格比较(处理 NaN、0、-0 等特殊值)
|
|
255
|
-
if (Object.is(keepValues[key], value)) {
|
|
256
|
-
result[key] = value;
|
|
257
|
-
continue;
|
|
258
|
-
}
|
|
259
|
-
}
|
|
260
|
-
|
|
261
|
-
// 排除指定值
|
|
262
|
-
const shouldExclude = excludeValues.some((excludeVal) => Object.is(excludeVal, value));
|
|
263
|
-
if (shouldExclude) {
|
|
264
|
-
continue;
|
|
265
|
-
}
|
|
266
|
-
|
|
267
|
-
// 保留其他字段
|
|
268
|
-
result[key] = value;
|
|
269
|
-
}
|
|
270
|
-
|
|
271
|
-
return result;
|
|
272
|
-
};
|
|
273
|
-
|
|
274
|
-
// ========================================
|
|
275
|
-
// 日期时间工具
|
|
276
|
-
// ========================================
|
|
277
|
-
|
|
278
|
-
/**
|
|
279
|
-
* 格式化日期
|
|
280
|
-
* @param date - 日期对象、时间戳或日期字符串
|
|
281
|
-
* @param format - 格式化模板(支持 YYYY, MM, DD, HH, mm, ss)
|
|
282
|
-
* @returns 格式化后的日期字符串
|
|
283
|
-
*
|
|
284
|
-
* @example
|
|
285
|
-
* formatDate(new Date('2025-10-11 15:30:45')) // '2025-10-11 15:30:45'
|
|
286
|
-
* formatDate(new Date('2025-10-11'), 'YYYY-MM-DD') // '2025-10-11'
|
|
287
|
-
* formatDate(1728648645000, 'YYYY/MM/DD HH:mm') // '2025/10/11 15:30'
|
|
288
|
-
* formatDate('2025-10-11', 'MM-DD') // '10-11'
|
|
289
|
-
*/
|
|
290
|
-
export const formatDate = (date: Date | string | number = new Date(), format: string = 'YYYY-MM-DD HH:mm:ss'): string => {
|
|
291
|
-
const d = new Date(date);
|
|
292
|
-
const year = d.getFullYear();
|
|
293
|
-
const month = String(d.getMonth() + 1).padStart(2, '0');
|
|
294
|
-
const day = String(d.getDate()).padStart(2, '0');
|
|
295
|
-
const hour = String(d.getHours()).padStart(2, '0');
|
|
296
|
-
const minute = String(d.getMinutes()).padStart(2, '0');
|
|
297
|
-
const second = String(d.getSeconds()).padStart(2, '0');
|
|
298
|
-
|
|
299
|
-
return format.replace('YYYY', String(year)).replace('MM', month).replace('DD', day).replace('HH', hour).replace('mm', minute).replace('ss', second);
|
|
300
|
-
};
|
|
301
|
-
|
|
302
|
-
/**
|
|
303
|
-
* 计算性能时间差
|
|
304
|
-
* 用于测量代码执行时间(使用 Bun.nanoseconds())
|
|
305
|
-
* @param startTime - 开始时间(Bun.nanoseconds()返回值)
|
|
306
|
-
* @param endTime - 结束时间(可选,默认为当前时间)
|
|
307
|
-
* @returns 时间差(毫秒或秒)
|
|
308
|
-
*
|
|
309
|
-
* @example
|
|
310
|
-
* const start = Bun.nanoseconds();
|
|
311
|
-
* // ... 执行代码 ...
|
|
312
|
-
* const elapsed = calcPerfTime(start); // '15.23 毫秒' 或 '2.45 秒'
|
|
313
|
-
*/
|
|
314
|
-
export const calcPerfTime = (startTime: number, endTime: number = Bun.nanoseconds()): string => {
|
|
315
|
-
const elapsedMs = (endTime - startTime) / 1_000_000;
|
|
316
|
-
|
|
317
|
-
if (elapsedMs < 1000) {
|
|
318
|
-
return `${elapsedMs.toFixed(2)} 毫秒`;
|
|
319
|
-
} else {
|
|
320
|
-
const elapsedSeconds = elapsedMs / 1000;
|
|
321
|
-
return `${elapsedSeconds.toFixed(2)} 秒`;
|
|
322
|
-
}
|
|
323
|
-
};
|
|
324
|
-
|
|
325
|
-
// ========================================
|
|
326
|
-
// 命名转换工具
|
|
327
|
-
// ========================================
|
|
328
|
-
|
|
329
|
-
/**
|
|
330
|
-
* 小驼峰转下划线(蛇形命名)
|
|
331
|
-
* 支持处理连续大写字母(如 APIKey -> api_key)
|
|
332
|
-
* @param str - 小驼峰字符串
|
|
333
|
-
* @returns 下划线格式字符串
|
|
334
|
-
*
|
|
335
|
-
* @example
|
|
336
|
-
* toSnakeCase('userId') // 'user_id'
|
|
337
|
-
* toSnakeCase('createdAt') // 'created_at'
|
|
338
|
-
* toSnakeCase('userName') // 'user_name'
|
|
339
|
-
* toSnakeCase('APIKey') // 'api_key'
|
|
340
|
-
* toSnakeCase('HTTPResponse') // 'http_response'
|
|
341
|
-
* toSnakeCase('XMLParser') // 'xml_parser'
|
|
342
|
-
*/
|
|
343
|
-
export const toSnakeCase = (str: string): string => {
|
|
344
|
-
if (!str || typeof str !== 'string') return str;
|
|
345
|
-
return String(str)
|
|
346
|
-
.replace(/([a-z0-9])([A-Z])/g, '$1_$2')
|
|
347
|
-
.replace(/([A-Z]+)([A-Z][a-z0-9]+)/g, '$1_$2')
|
|
348
|
-
.toLowerCase();
|
|
349
|
-
};
|
|
350
|
-
|
|
351
|
-
/**
|
|
352
|
-
* 下划线转小驼峰
|
|
353
|
-
* @param str - 下划线格式字符串
|
|
354
|
-
* @returns 小驼峰字符串
|
|
355
|
-
*
|
|
356
|
-
* @example
|
|
357
|
-
* toCamelCase('user_id') // 'userId'
|
|
358
|
-
* toCamelCase('created_at') // 'createdAt'
|
|
359
|
-
* toCamelCase('user_name') // 'userName'
|
|
360
|
-
*/
|
|
361
|
-
export const toCamelCase = (str: string): string => {
|
|
362
|
-
if (!str || typeof str !== 'string') return str;
|
|
363
|
-
return str.replace(/_([a-z])/g, (_, letter) => letter.toUpperCase());
|
|
364
|
-
};
|
|
365
|
-
|
|
366
|
-
/**
|
|
367
|
-
* 对象字段名转下划线
|
|
368
|
-
* @param obj - 源对象
|
|
369
|
-
* @returns 字段名转为下划线格式的新对象
|
|
370
|
-
*
|
|
371
|
-
* @example
|
|
372
|
-
* keysToSnake({ userId: 123, userName: 'John' }) // { user_id: 123, user_name: 'John' }
|
|
373
|
-
* keysToSnake({ createdAt: 1697452800000 }) // { created_at: 1697452800000 }
|
|
374
|
-
*/
|
|
375
|
-
export const keysToSnake = <T = any>(obj: Record<string, any>): T => {
|
|
376
|
-
if (!obj || !isType(obj, 'object')) return obj as T;
|
|
377
|
-
|
|
378
|
-
const result: any = {};
|
|
379
|
-
for (const [key, value] of Object.entries(obj)) {
|
|
380
|
-
const snakeKey = toSnakeCase(key);
|
|
381
|
-
result[snakeKey] = value;
|
|
382
|
-
}
|
|
383
|
-
return result;
|
|
384
|
-
};
|
|
385
|
-
|
|
386
|
-
/**
|
|
387
|
-
* 对象字段名转小驼峰
|
|
388
|
-
* @param obj - 源对象
|
|
389
|
-
* @returns 字段名转为小驼峰格式的新对象
|
|
390
|
-
*
|
|
391
|
-
* @example
|
|
392
|
-
* keysToCamel({ user_id: 123, user_name: 'John' }) // { userId: 123, userName: 'John' }
|
|
393
|
-
* keysToCamel({ created_at: 1697452800000 }) // { createdAt: 1697452800000 }
|
|
394
|
-
*/
|
|
395
|
-
export const keysToCamel = <T = any>(obj: Record<string, any>): T => {
|
|
396
|
-
if (!obj || !isType(obj, 'object')) return obj as T;
|
|
397
|
-
|
|
398
|
-
const result: any = {};
|
|
399
|
-
for (const [key, value] of Object.entries(obj)) {
|
|
400
|
-
const camelKey = toCamelCase(key);
|
|
401
|
-
result[camelKey] = value;
|
|
402
|
-
}
|
|
403
|
-
return result;
|
|
404
|
-
};
|
|
405
|
-
|
|
406
|
-
/**
|
|
407
|
-
* 数组对象字段名批量转小驼峰
|
|
408
|
-
* @param arr - 源数组
|
|
409
|
-
* @returns 字段名转为小驼峰格式的新数组
|
|
410
|
-
*
|
|
411
|
-
* @example
|
|
412
|
-
* arrayKeysToCamel([
|
|
413
|
-
* { user_id: 1, user_name: 'John' },
|
|
414
|
-
* { user_id: 2, user_name: 'Jane' }
|
|
415
|
-
* ])
|
|
416
|
-
* // [{ userId: 1, userName: 'John' }, { userId: 2, userName: 'Jane' }]
|
|
417
|
-
*/
|
|
418
|
-
export const arrayKeysToCamel = <T = any>(arr: Record<string, any>[]): T[] => {
|
|
419
|
-
if (!arr || !isType(arr, 'array')) return arr as T[];
|
|
420
|
-
return arr.map((item) => keysToCamel<T>(item));
|
|
421
|
-
};
|
|
422
|
-
|
|
423
|
-
/**
|
|
424
|
-
* 转换数据库 BIGINT 字段为数字类型
|
|
425
|
-
* 当 bigint: false 时,Bun SQL 会将大于 u32 的 BIGINT 返回为字符串,此函数将其转换为 number
|
|
426
|
-
*
|
|
427
|
-
* 转换规则:
|
|
428
|
-
* 1. 白名单中的字段会被转换
|
|
429
|
-
* 2. 所有以 'Id' 或 '_id' 结尾的字段会被自动转换
|
|
430
|
-
* 3. 所有以 'At' 或 '_at' 结尾的字段会被自动转换(时间戳字段)
|
|
431
|
-
* 4. 其他字段保持不变
|
|
432
|
-
*
|
|
433
|
-
* @param arr - 数据数组
|
|
434
|
-
* @param fields - 额外需要转换的字段名数组(默认:['id', 'pid', 'sort'])
|
|
435
|
-
* @returns 转换后的数组
|
|
436
|
-
*
|
|
437
|
-
* @example
|
|
438
|
-
* // 基础字段 + 自动匹配以 Id/At 结尾的字段
|
|
439
|
-
* convertBigIntFields([
|
|
440
|
-
* {
|
|
441
|
-
* id: '1760695696283001', // ✅ 转换(在白名单)
|
|
442
|
-
* pid: '0', // ✅ 转换(在白名单)
|
|
443
|
-
* categoryId: '123', // ✅ 转换(以 Id 结尾)
|
|
444
|
-
* user_id: '456', // ✅ 转换(以 _id 结尾)
|
|
445
|
-
* createdAt: '1697452800000', // ✅ 转换(以 At 结尾)
|
|
446
|
-
* created_at: '1697452800000', // ✅ 转换(以 _at 结尾)
|
|
447
|
-
* phone: '13800138000', // ❌ 不转换(不匹配规则)
|
|
448
|
-
* name: 'test' // ❌ 不转换(不匹配规则)
|
|
449
|
-
* }
|
|
450
|
-
* ])
|
|
451
|
-
* // [{ id: 1760695696283001, pid: 0, categoryId: 123, user_id: 456, createdAt: 1697452800000, created_at: 1697452800000, phone: '13800138000', name: 'test' }]
|
|
452
|
-
*/
|
|
453
|
-
export const convertBigIntFields = <T = any>(arr: Record<string, any>[], fields: string[] = ['id', 'pid', 'sort']): T[] => {
|
|
454
|
-
if (!arr || !isType(arr, 'array')) return arr as T[];
|
|
455
|
-
|
|
456
|
-
return arr.map((item) => {
|
|
457
|
-
const converted = { ...item };
|
|
458
|
-
|
|
459
|
-
// 遍历对象的所有字段
|
|
460
|
-
for (const [key, value] of Object.entries(converted)) {
|
|
461
|
-
// 跳过 undefined 和 null
|
|
462
|
-
if (value === undefined || value === null) {
|
|
463
|
-
continue;
|
|
464
|
-
}
|
|
465
|
-
|
|
466
|
-
// 判断是否需要转换:
|
|
467
|
-
// 1. 在白名单中
|
|
468
|
-
// 2. 以 'Id' 结尾(如 userId, roleId, categoryId)
|
|
469
|
-
// 3. 以 '_id' 结尾(如 user_id, role_id)
|
|
470
|
-
// 4. 以 'At' 结尾(如 createdAt, updatedAt)
|
|
471
|
-
// 5. 以 '_at' 结尾(如 created_at, updated_at)
|
|
472
|
-
const shouldConvert = fields.includes(key) || key.endsWith('Id') || key.endsWith('_id') || key.endsWith('At') || key.endsWith('_at');
|
|
473
|
-
|
|
474
|
-
if (shouldConvert && typeof value === 'string') {
|
|
475
|
-
const num = Number(value);
|
|
476
|
-
if (!isNaN(num)) {
|
|
477
|
-
converted[key] = num;
|
|
478
|
-
}
|
|
479
|
-
}
|
|
480
|
-
// number 类型保持不变(小于 u32 的值)
|
|
481
|
-
}
|
|
482
|
-
|
|
483
|
-
return converted as T;
|
|
484
|
-
}) as T[];
|
|
485
|
-
};
|
|
486
|
-
|
|
487
|
-
// ========================================
|
|
488
|
-
// 数据清洗工具
|
|
489
|
-
// ========================================
|
|
490
|
-
|
|
491
|
-
/**
|
|
492
|
-
* 数据清洗选项
|
|
493
|
-
*/
|
|
494
|
-
export interface DataCleanOptions {
|
|
495
|
-
/** 要排除的字段名数组 */
|
|
496
|
-
excludeKeys?: string[];
|
|
497
|
-
/** 只包含的字段名数组(优先级高于 excludeKeys) */
|
|
498
|
-
includeKeys?: string[];
|
|
499
|
-
/** 要移除的值数组 */
|
|
500
|
-
removeValues?: any[];
|
|
501
|
-
/** 字段值最大长度(超过会截断) */
|
|
502
|
-
maxLen?: number;
|
|
503
|
-
/** 是否深度清洗(处理嵌套对象) */
|
|
504
|
-
deep?: boolean;
|
|
505
|
-
}
|
|
506
|
-
|
|
507
|
-
/**
|
|
508
|
-
* 数据清洗 - 清理和过滤对象数据
|
|
509
|
-
* @param data - 源数据对象
|
|
510
|
-
* @param options - 清洗选项
|
|
511
|
-
* @returns 清洗后的新对象
|
|
512
|
-
*
|
|
513
|
-
* @example
|
|
514
|
-
* // 排除指定字段
|
|
515
|
-
* cleanData({ a: 1, b: 2, c: 3 }, { excludeKeys: ['b'] })
|
|
516
|
-
* // { a: 1, c: 3 }
|
|
517
|
-
*
|
|
518
|
-
* // 只包含指定字段
|
|
519
|
-
* cleanData({ a: 1, b: 2, c: 3 }, { includeKeys: ['a', 'c'] })
|
|
520
|
-
* // { a: 1, c: 3 }
|
|
521
|
-
*
|
|
522
|
-
* // 移除指定值
|
|
523
|
-
* cleanData({ a: 1, b: null, c: undefined, d: '' }, { removeValues: [null, undefined, ''] })
|
|
524
|
-
* // { a: 1 }
|
|
525
|
-
*
|
|
526
|
-
* // 限制字段值长度
|
|
527
|
-
* cleanData({ name: 'A'.repeat(1000) }, { maxLen: 100 })
|
|
528
|
-
* // { name: 'AAA...AAA' (100个字符) }
|
|
529
|
-
*
|
|
530
|
-
* // 组合使用
|
|
531
|
-
* cleanData(
|
|
532
|
-
* { a: 1, b: null, c: 'very long text...', d: 4 },
|
|
533
|
-
* { excludeKeys: ['d'], removeValues: [null], maxLen: 10 }
|
|
534
|
-
* )
|
|
535
|
-
* // { a: 1, c: 'very long ' }
|
|
536
|
-
*
|
|
537
|
-
* // 深度清洗嵌套对象
|
|
538
|
-
* cleanData(
|
|
539
|
-
* { user: { name: 'John', password: '123', nested: { secret: 'xxx' } } },
|
|
540
|
-
* { excludeKeys: ['password', 'secret'], deep: true }
|
|
541
|
-
* )
|
|
542
|
-
* // { user: { name: 'John', nested: {} } }
|
|
543
|
-
*/
|
|
544
|
-
export const cleanData = <T = any>(data?: Record<string, any>, options: DataCleanOptions = {}): Partial<T> => {
|
|
545
|
-
// 参数默认值
|
|
546
|
-
const { excludeKeys = [], includeKeys = [], removeValues = [null, undefined], maxLen = 0, deep = false } = options;
|
|
547
|
-
|
|
548
|
-
// 非对象直接返回(不做任何处理)
|
|
549
|
-
if (!data || !isType(data, 'object')) {
|
|
550
|
-
return data as Partial<T>;
|
|
551
|
-
}
|
|
552
|
-
|
|
553
|
-
const result: any = {};
|
|
554
|
-
|
|
555
|
-
// 判断值是否应该被移除
|
|
556
|
-
const shouldRemoveValue = (value: any): boolean => {
|
|
557
|
-
return removeValues.some((removeVal) => {
|
|
558
|
-
// 使用 Object.is 进行严格比较(处理 NaN、0、-0 等特殊值)
|
|
559
|
-
return Object.is(removeVal, value);
|
|
560
|
-
});
|
|
561
|
-
};
|
|
562
|
-
|
|
563
|
-
// 处理字段值(截断、深度清洗)
|
|
564
|
-
const processValue = (value: any): any => {
|
|
565
|
-
// 深度清洗嵌套对象
|
|
566
|
-
if (deep && isType(value, 'object')) {
|
|
567
|
-
return cleanData(value, options);
|
|
568
|
-
}
|
|
569
|
-
|
|
570
|
-
// 深度清洗数组对象
|
|
571
|
-
if (deep && isType(value, 'array')) {
|
|
572
|
-
return value.map((item: any) => (isType(item, 'object') ? cleanData(item, options) : item));
|
|
573
|
-
}
|
|
574
|
-
|
|
575
|
-
// 字段值长度限制
|
|
576
|
-
if (maxLen > 0) {
|
|
577
|
-
// 字符串直接截断
|
|
578
|
-
if (isType(value, 'string') && value.length > maxLen) {
|
|
579
|
-
return value.substring(0, maxLen);
|
|
580
|
-
}
|
|
581
|
-
|
|
582
|
-
// 非字符串先转字符串再截断
|
|
583
|
-
if (!isType(value, 'string')) {
|
|
584
|
-
try {
|
|
585
|
-
const strValue = JSON.stringify(value);
|
|
586
|
-
if (strValue && strValue.length > maxLen) {
|
|
587
|
-
return strValue.substring(0, maxLen);
|
|
588
|
-
}
|
|
589
|
-
} catch {
|
|
590
|
-
// JSON.stringify 失败则返回原值
|
|
591
|
-
}
|
|
592
|
-
}
|
|
593
|
-
}
|
|
594
|
-
|
|
595
|
-
return value;
|
|
596
|
-
};
|
|
597
|
-
|
|
598
|
-
// 遍历对象字段
|
|
599
|
-
for (const [key, value] of Object.entries(data)) {
|
|
600
|
-
// 排除指定值
|
|
601
|
-
if (shouldRemoveValue(value)) {
|
|
602
|
-
continue;
|
|
603
|
-
}
|
|
604
|
-
|
|
605
|
-
// includeKeys 优先级最高
|
|
606
|
-
if (includeKeys.length > 0) {
|
|
607
|
-
if (includeKeys.includes(key)) {
|
|
608
|
-
result[key] = processValue(value);
|
|
609
|
-
}
|
|
610
|
-
continue;
|
|
611
|
-
}
|
|
612
|
-
|
|
613
|
-
// 排除指定字段
|
|
614
|
-
if (excludeKeys.includes(key)) {
|
|
615
|
-
continue;
|
|
616
|
-
}
|
|
617
|
-
|
|
618
|
-
// 保留字段并处理值
|
|
619
|
-
result[key] = processValue(value);
|
|
620
|
-
}
|
|
621
|
-
|
|
622
|
-
return result;
|
|
623
|
-
};
|
|
624
|
-
|
|
625
|
-
// ========================================
|
|
626
|
-
// Addon 管理工具
|
|
627
|
-
// ========================================
|
|
628
|
-
|
|
629
|
-
/**
|
|
630
|
-
* 扫描所有可用的 addon
|
|
631
|
-
* @returns addon 名称数组(过滤掉 _ 开头的目录)
|
|
632
|
-
*/
|
|
633
|
-
export const scanAddons = (): string[] => {
|
|
634
|
-
if (!fs.existsSync(paths.projectAddonDir)) {
|
|
635
|
-
return [];
|
|
636
|
-
}
|
|
637
|
-
|
|
638
|
-
try {
|
|
639
|
-
return fs
|
|
640
|
-
.readdirSync(paths.projectAddonDir)
|
|
641
|
-
.filter((name) => {
|
|
642
|
-
const fullPath = join(paths.projectAddonDir, name);
|
|
643
|
-
const stat = fs.statSync(fullPath);
|
|
644
|
-
const isDir = stat.isDirectory();
|
|
645
|
-
const notSkip = !name.startsWith('_'); // 跳过 _ 开头的目录
|
|
646
|
-
return isDir && notSkip;
|
|
647
|
-
})
|
|
648
|
-
.sort(); // 按字母顺序排序
|
|
649
|
-
} catch (error) {
|
|
650
|
-
return [];
|
|
651
|
-
}
|
|
652
|
-
};
|
|
653
|
-
|
|
654
|
-
/**
|
|
655
|
-
* 获取 addon 的指定子目录路径
|
|
656
|
-
* @param addonName - addon 名称
|
|
657
|
-
* @param subDir - 子目录名称(apis, checks, plugins, tables, types, config)
|
|
658
|
-
*/
|
|
659
|
-
export const getAddonDir = (addonName: string, subDir: string): string => {
|
|
660
|
-
return join(paths.projectAddonDir, addonName, subDir);
|
|
661
|
-
};
|
|
662
|
-
|
|
663
|
-
/**
|
|
664
|
-
* 检查 addon 子目录是否存在
|
|
665
|
-
* @param addonName - addon 名称
|
|
666
|
-
* @param subDir - 子目录名称
|
|
667
|
-
*/
|
|
668
|
-
export const addonDirExists = (addonName: string, subDir: string): boolean => {
|
|
669
|
-
const dir = getAddonDir(addonName, subDir);
|
|
670
|
-
return fs.existsSync(dir) && fs.statSync(dir).isDirectory();
|
|
671
|
-
};
|
|
672
|
-
|
|
673
|
-
// ========================================
|
|
674
|
-
// Plugin 管理工具
|
|
675
|
-
// ========================================
|
|
676
|
-
|
|
677
|
-
/**
|
|
678
|
-
* 排序插件(根据依赖关系)
|
|
679
|
-
* 使用拓扑排序算法,确保依赖的插件先加载
|
|
680
|
-
*
|
|
681
|
-
* @param plugins - 插件数组
|
|
682
|
-
* @returns 排序后的插件数组,如果存在循环依赖则返回 false
|
|
683
|
-
*
|
|
684
|
-
* @example
|
|
685
|
-
* const plugins = [
|
|
686
|
-
* { name: 'logger', dependencies: [] },
|
|
687
|
-
* { name: 'db', dependencies: ['logger'] },
|
|
688
|
-
* { name: 'api', dependencies: ['db', 'logger'] }
|
|
689
|
-
* ];
|
|
690
|
-
*
|
|
691
|
-
* const sorted = sortPlugins(plugins);
|
|
692
|
-
* // [
|
|
693
|
-
* // { name: 'logger', dependencies: [] },
|
|
694
|
-
* // { name: 'db', dependencies: ['logger'] },
|
|
695
|
-
* // { name: 'api', dependencies: ['db', 'logger'] }
|
|
696
|
-
* // ]
|
|
697
|
-
*
|
|
698
|
-
* // 循环依赖示例
|
|
699
|
-
* const badPlugins = [
|
|
700
|
-
* { name: 'a', dependencies: ['b'] },
|
|
701
|
-
* { name: 'b', dependencies: ['a'] }
|
|
702
|
-
* ];
|
|
703
|
-
* sortPlugins(badPlugins); // false
|
|
704
|
-
*/
|
|
705
|
-
export const sortPlugins = (plugins: Plugin[]): Plugin[] | false => {
|
|
706
|
-
const result: Plugin[] = [];
|
|
707
|
-
const visited = new Set<string>();
|
|
708
|
-
const visiting = new Set<string>();
|
|
709
|
-
const pluginMap: Record<string, Plugin> = Object.fromEntries(plugins.map((p) => [p.name, p]));
|
|
710
|
-
let isPass = true;
|
|
711
|
-
|
|
712
|
-
const visit = (name: string): void => {
|
|
713
|
-
if (visited.has(name)) return;
|
|
714
|
-
if (visiting.has(name)) {
|
|
715
|
-
isPass = false;
|
|
716
|
-
return;
|
|
717
|
-
}
|
|
718
|
-
|
|
719
|
-
const plugin = pluginMap[name];
|
|
720
|
-
if (!plugin) return; // 依赖不存在时跳过
|
|
721
|
-
|
|
722
|
-
visiting.add(name);
|
|
723
|
-
(plugin.dependencies || []).forEach(visit);
|
|
724
|
-
visiting.delete(name);
|
|
725
|
-
visited.add(name);
|
|
726
|
-
result.push(plugin);
|
|
727
|
-
};
|
|
728
|
-
|
|
729
|
-
plugins.forEach((p) => visit(p.name));
|
|
730
|
-
return isPass ? result : false;
|
|
731
|
-
};
|
|
732
|
-
|
|
733
|
-
// ========================================
|
|
734
|
-
// 表定义工具
|
|
735
|
-
// ========================================
|
|
736
|
-
|
|
737
|
-
/**
|
|
738
|
-
* 解析字段规则字符串(以 | 分隔)
|
|
739
|
-
* 用于解析表定义中的字段规则
|
|
740
|
-
*
|
|
741
|
-
* 规则格式:字段名|类型|最小值|最大值|默认值|索引|正则
|
|
742
|
-
* 注意:只分割前6个|,第7个|之后的所有内容(包括|)都属于正则表达式
|
|
743
|
-
*
|
|
744
|
-
* @param rule - 字段规则字符串
|
|
745
|
-
* @returns 解析后的字段规则对象
|
|
746
|
-
*
|
|
747
|
-
* @example
|
|
748
|
-
* parseRule('用户名|string|2|50|null|1|null')
|
|
749
|
-
* // {
|
|
750
|
-
* // name: '用户名',
|
|
751
|
-
* // type: 'string',
|
|
752
|
-
* // min: 2,
|
|
753
|
-
* // max: 50,
|
|
754
|
-
* // default: 'null',
|
|
755
|
-
* // index: 1,
|
|
756
|
-
* // regex: null
|
|
757
|
-
* // }
|
|
758
|
-
*
|
|
759
|
-
* parseRule('年龄|number|0|150|18|0|null')
|
|
760
|
-
* // {
|
|
761
|
-
* // name: '年龄',
|
|
762
|
-
* // type: 'number',
|
|
763
|
-
* // min: 0,
|
|
764
|
-
* // max: 150,
|
|
765
|
-
* // default: 18,
|
|
766
|
-
* // index: 0,
|
|
767
|
-
* // regex: null
|
|
768
|
-
* // }
|
|
769
|
-
*
|
|
770
|
-
* parseRule('状态|string|1|20|active|1|^(active|inactive|pending)$')
|
|
771
|
-
* // 正则表达式中的 | 会被保留
|
|
772
|
-
*/
|
|
773
|
-
export const parseRule = (rule: string): ParsedFieldRule => {
|
|
774
|
-
// 手动分割前6个|,剩余部分作为正则表达式
|
|
775
|
-
// 这样可以确保正则表达式中的|不会被分割
|
|
776
|
-
const parts: string[] = [];
|
|
777
|
-
let currentPart = '';
|
|
778
|
-
let pipeCount = 0;
|
|
779
|
-
|
|
780
|
-
for (let i = 0; i < rule.length; i++) {
|
|
781
|
-
if (rule[i] === '|' && pipeCount < 6) {
|
|
782
|
-
parts.push(currentPart);
|
|
783
|
-
currentPart = '';
|
|
784
|
-
pipeCount++;
|
|
785
|
-
} else {
|
|
786
|
-
currentPart += rule[i];
|
|
787
|
-
}
|
|
788
|
-
}
|
|
789
|
-
// 添加最后一部分(正则表达式)
|
|
790
|
-
parts.push(currentPart);
|
|
791
|
-
|
|
792
|
-
const [fieldName = '', fieldType = 'string', fieldMinStr = 'null', fieldMaxStr = 'null', fieldDefaultStr = 'null', fieldIndexStr = '0', fieldRegx = 'null'] = parts;
|
|
793
|
-
|
|
794
|
-
const fieldIndex = Number(fieldIndexStr) as 0 | 1;
|
|
795
|
-
const fieldMin = fieldMinStr !== 'null' ? Number(fieldMinStr) : null;
|
|
796
|
-
const fieldMax = fieldMaxStr !== 'null' ? Number(fieldMaxStr) : null;
|
|
797
|
-
|
|
798
|
-
let fieldDefault: any = fieldDefaultStr;
|
|
799
|
-
if (fieldType === 'number' && fieldDefaultStr !== 'null') {
|
|
800
|
-
fieldDefault = Number(fieldDefaultStr);
|
|
801
|
-
}
|
|
802
|
-
|
|
803
|
-
return {
|
|
804
|
-
name: fieldName,
|
|
805
|
-
type: fieldType as 'string' | 'number' | 'text' | 'array',
|
|
806
|
-
min: fieldMin,
|
|
807
|
-
max: fieldMax,
|
|
808
|
-
default: fieldDefault,
|
|
809
|
-
index: fieldIndex,
|
|
810
|
-
regex: fieldRegx !== 'null' ? fieldRegx : null
|
|
811
|
-
};
|
|
812
|
-
};
|