befly 2.3.1 → 2.3.3
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/befly.js +109 -0
- package/checks/table.js +79 -84
- package/config/env.js +10 -8
- package/package.json +7 -14
- package/plugins/db.js +2 -2
- package/scripts/syncDb.js +585 -433
- package/scripts/syncDev.js +96 -0
- package/tables/common.json +10 -10
- package/tables/tool.json +3 -3
- package/utils/index.js +88 -131
- package/utils/sqlManager.js +1 -1
- package/utils/validate.js +7 -9
|
@@ -0,0 +1,96 @@
|
|
|
1
|
+
/**
|
|
2
|
+
* 同步开发管理员账号脚本
|
|
3
|
+
* - 账号: dev
|
|
4
|
+
* - 密码: Crypto2.hmacMd5(Crypto2.md5(Env.DEV_PASSWORD), Env.MD5_SALT)
|
|
5
|
+
* - 行为: 若存在则更新密码与 updated_at;不存在则插入新记录
|
|
6
|
+
*/
|
|
7
|
+
|
|
8
|
+
import { Env } from '../config/env.js';
|
|
9
|
+
import { Logger } from '../utils/logger.js';
|
|
10
|
+
import { createSqlClient } from '../utils/index.js';
|
|
11
|
+
import { Crypto2 } from '../utils/crypto.js';
|
|
12
|
+
|
|
13
|
+
// 命令行参数(保持与 syncDb.js 一致的 plan 行为)
|
|
14
|
+
const ARGV = Array.isArray(process.argv) ? process.argv : [];
|
|
15
|
+
const CLI = { DRY_RUN: ARGV.includes('--plan') };
|
|
16
|
+
|
|
17
|
+
// 执行器封装
|
|
18
|
+
const exec = async (client, query, params = []) => {
|
|
19
|
+
if (params && params.length > 0) return await client.unsafe(query, params);
|
|
20
|
+
return await client.unsafe(query);
|
|
21
|
+
};
|
|
22
|
+
|
|
23
|
+
/**
|
|
24
|
+
* 同步开发管理员账号
|
|
25
|
+
* @param {import('bun').SQL | null} client 可选,复用已有 SQL 客户端;不传则内部创建与关闭
|
|
26
|
+
*/
|
|
27
|
+
export async function SyncDev(client = null) {
|
|
28
|
+
let ownClient = false;
|
|
29
|
+
try {
|
|
30
|
+
if (CLI.DRY_RUN) {
|
|
31
|
+
Logger.info('[计划] 同步完成后将初始化/更新 admin.dev 账号(plan 模式不执行)');
|
|
32
|
+
return true;
|
|
33
|
+
}
|
|
34
|
+
|
|
35
|
+
if (!Env.DEV_PASSWORD || !Env.MD5_SALT) {
|
|
36
|
+
Logger.warn('跳过开发管理员初始化:缺少 DEV_PASSWORD 或 MD5_SALT 配置');
|
|
37
|
+
return false;
|
|
38
|
+
}
|
|
39
|
+
|
|
40
|
+
if (!client) {
|
|
41
|
+
client = await createSqlClient({ max: 1 });
|
|
42
|
+
ownClient = true;
|
|
43
|
+
}
|
|
44
|
+
|
|
45
|
+
// 检查 admin 表是否存在
|
|
46
|
+
const [exist] = await exec(client, 'SELECT COUNT(*) AS cnt FROM information_schema.TABLES WHERE TABLE_SCHEMA = ? AND TABLE_NAME = ? LIMIT 1', [Env.DB_NAME || '', 'admin']);
|
|
47
|
+
if (!exist || Number(exist.cnt) === 0) {
|
|
48
|
+
Logger.warn('跳过开发管理员初始化:未检测到 admin 表');
|
|
49
|
+
return false;
|
|
50
|
+
}
|
|
51
|
+
|
|
52
|
+
const nowTs = Date.now();
|
|
53
|
+
const hashed = Crypto2.hmacMd5(Crypto2.md5(Env.DEV_PASSWORD), Env.MD5_SALT);
|
|
54
|
+
|
|
55
|
+
// 更新存在的 dev 账号
|
|
56
|
+
const updateRes = await exec(client, 'UPDATE `admin` SET `password` = ?, `updated_at` = ? WHERE `account` = ? LIMIT 1', [hashed, nowTs, 'dev']);
|
|
57
|
+
const affected = updateRes?.affectedRows ?? updateRes?.rowsAffected ?? 0;
|
|
58
|
+
|
|
59
|
+
if (!affected || affected === 0) {
|
|
60
|
+
// 插入新账号
|
|
61
|
+
const id = nowTs;
|
|
62
|
+
await exec(client, 'INSERT INTO `admin` (`id`, `created_at`, `updated_at`, `deleted_at`, `state`, `account`, `password`) VALUES (?, ?, ?, 0, 0, ?, ?)', [id, nowTs, nowTs, 'dev', hashed]);
|
|
63
|
+
Logger.info('开发管理员已初始化:account=dev');
|
|
64
|
+
} else {
|
|
65
|
+
Logger.info('开发管理员已更新密码并刷新更新时间:account=dev');
|
|
66
|
+
}
|
|
67
|
+
|
|
68
|
+
return true;
|
|
69
|
+
} catch (e) {
|
|
70
|
+
Logger.warn(`开发管理员初始化步骤出错:${e.message}`);
|
|
71
|
+
return false;
|
|
72
|
+
} finally {
|
|
73
|
+
if (ownClient && client) {
|
|
74
|
+
try {
|
|
75
|
+
await client.close();
|
|
76
|
+
} catch (e) {
|
|
77
|
+
Logger.warn('关闭数据库连接时出错:', e.message);
|
|
78
|
+
}
|
|
79
|
+
}
|
|
80
|
+
}
|
|
81
|
+
}
|
|
82
|
+
|
|
83
|
+
// 允许直接运行该脚本
|
|
84
|
+
if (import.meta.url === `file://${process.argv[1]}`) {
|
|
85
|
+
SyncDev()
|
|
86
|
+
.then((ok) => {
|
|
87
|
+
if (CLI.DRY_RUN) {
|
|
88
|
+
process.exit(0);
|
|
89
|
+
}
|
|
90
|
+
process.exit(ok ? 0 : 1);
|
|
91
|
+
})
|
|
92
|
+
.catch((err) => {
|
|
93
|
+
console.error('❌ 开发管理员同步失败:', err);
|
|
94
|
+
process.exit(1);
|
|
95
|
+
});
|
|
96
|
+
}
|
package/tables/common.json
CHANGED
|
@@ -1,16 +1,16 @@
|
|
|
1
1
|
{
|
|
2
|
-
"email": "邮箱⚡string⚡5⚡100⚡
|
|
3
|
-
"phone": "手机号⚡string⚡11⚡11⚡
|
|
2
|
+
"email": "邮箱⚡string⚡5⚡100⚡''⚡1⚡^[a-zA-Z0-9._%+-]+@[a-zA-Z0-9.-]+\\.[a-zA-Z]{2,}$",
|
|
3
|
+
"phone": "手机号⚡string⚡11⚡11⚡''⚡1⚡^1[3-9]\\d{9}$",
|
|
4
4
|
"page": "页码⚡number⚡1⚡9999⚡1⚡0⚡null",
|
|
5
5
|
"limit": "每页数量⚡number⚡1⚡100⚡10⚡0⚡null",
|
|
6
|
-
"title": "标题⚡string⚡1⚡200⚡
|
|
7
|
-
"description": "描述⚡string⚡0⚡500⚡
|
|
8
|
-
"keyword": "关键词⚡string⚡1⚡50⚡
|
|
6
|
+
"title": "标题⚡string⚡1⚡200⚡''⚡0⚡null",
|
|
7
|
+
"description": "描述⚡string⚡0⚡500⚡''⚡0⚡null",
|
|
8
|
+
"keyword": "关键词⚡string⚡1⚡50⚡''⚡1⚡null",
|
|
9
9
|
"status": "状态⚡string⚡1⚡20⚡active⚡1⚡^(active|inactive|pending|suspended)$",
|
|
10
10
|
"enabled": "启用状态⚡number⚡0⚡1⚡1⚡0⚡^(0|1)$",
|
|
11
|
-
"date": "日期⚡string⚡10⚡10⚡
|
|
12
|
-
"datetime": "日期时间⚡string⚡19⚡25⚡
|
|
13
|
-
"filename": "文件名⚡string⚡1⚡255⚡
|
|
14
|
-
"url": "网址⚡string⚡5⚡500⚡
|
|
15
|
-
"tag": "标签⚡array⚡0⚡10⚡
|
|
11
|
+
"date": "日期⚡string⚡10⚡10⚡''⚡0⚡^\\d{4}-\\d{2}-\\d{2}$",
|
|
12
|
+
"datetime": "日期时间⚡string⚡19⚡25⚡''⚡0⚡^\\d{4}-\\d{2}-\\d{2}T\\d{2}:\\d{2}:\\d{2}",
|
|
13
|
+
"filename": "文件名⚡string⚡1⚡255⚡''⚡0⚡null",
|
|
14
|
+
"url": "网址⚡string⚡5⚡500⚡''⚡0⚡^https?://",
|
|
15
|
+
"tag": "标签⚡array⚡0⚡10⚡''⚡0⚡null"
|
|
16
16
|
}
|
package/tables/tool.json
CHANGED
|
@@ -1,6 +1,6 @@
|
|
|
1
1
|
{
|
|
2
|
-
"filename": "文件名⚡string⚡1⚡255⚡
|
|
3
|
-
"content": "内容⚡string⚡0⚡100⚡
|
|
2
|
+
"filename": "文件名⚡string⚡1⚡255⚡''⚡1⚡null",
|
|
3
|
+
"content": "内容⚡string⚡0⚡100⚡''⚡0⚡null",
|
|
4
4
|
"type": "类型⚡string⚡0⚡50⚡file⚡1⚡^(file|folder|link)$",
|
|
5
|
-
"path": "路径⚡string⚡1⚡500⚡
|
|
5
|
+
"path": "路径⚡string⚡1⚡500⚡''⚡0⚡null"
|
|
6
6
|
}
|
package/utils/index.js
CHANGED
|
@@ -44,17 +44,26 @@ export const sortPlugins = (plugins) => {
|
|
|
44
44
|
return isPass ? result : false;
|
|
45
45
|
};
|
|
46
46
|
|
|
47
|
-
|
|
48
|
-
|
|
49
|
-
|
|
47
|
+
/**
|
|
48
|
+
* 解析字段规则字符串(以 ⚡ 分隔),并进行最小类型转换:
|
|
49
|
+
* - 返回顺序:显示名, 类型, 最小值, 最大值, 默认值, 是否索引, 正则
|
|
50
|
+
* - 最小值/最大值/是否索引:当不为字面量字符串 'null' 时转为数字;否则保留原值
|
|
51
|
+
* - 默认值:当类型为 number 且默认值不为 'null' 时转为数字;否则保留原值
|
|
52
|
+
* - 不做正确性校验(由 checks/table.js 负责)
|
|
53
|
+
* - 保留额外段位(>7)以便上层可检测异常长度
|
|
54
|
+
*
|
|
55
|
+
* @param {string} rule
|
|
56
|
+
* @returns {any[]} 一个数组,至少包含前7段(若原始段位不足则按原样长度返回),多余段位将原样附加
|
|
57
|
+
*/
|
|
58
|
+
export const parseRule = (rule) => {
|
|
59
|
+
let [fieldName, fieldType, fieldMin, fieldMax, fieldDefault, fieldIndex, fieldRegx] = rule.split('⚡');
|
|
50
60
|
|
|
51
|
-
|
|
52
|
-
if (
|
|
53
|
-
|
|
54
|
-
|
|
61
|
+
fieldIndex = Number(fieldIndex);
|
|
62
|
+
if (fieldMin !== 'null') fieldMin = Number(fieldMin);
|
|
63
|
+
if (fieldMax !== 'null') fieldMax = Number(fieldMax);
|
|
64
|
+
if (fieldType !== 'number') fieldDefault = Number(fieldDefault);
|
|
55
65
|
|
|
56
|
-
|
|
57
|
-
return [allParts[0], allParts[1], allParts[2], allParts[3], allParts.slice(4).join(',')];
|
|
66
|
+
return [fieldName, fieldType, fieldMin, fieldMax, fieldDefault, fieldIndex, fieldRegx];
|
|
58
67
|
};
|
|
59
68
|
|
|
60
69
|
export const formatDate = (date = new Date(), format = 'YYYY-MM-DD HH:mm:ss') => {
|
|
@@ -140,27 +149,6 @@ export const pickFields = (obj, keys) => {
|
|
|
140
149
|
return result;
|
|
141
150
|
};
|
|
142
151
|
|
|
143
|
-
/**
|
|
144
|
-
* 从对象或数组数据中按“字段名”和“字段值”进行排除过滤。
|
|
145
|
-
* - 支持对象:移除指定字段名,以及值在排除值列表中的字段。
|
|
146
|
-
* - 支持数组:
|
|
147
|
-
* - 如果元素为对象,按同样规则清洗(移除字段名/字段值命中项)。
|
|
148
|
-
* - 如果元素为原始值(数字/字符串等),当元素值命中排除值则从数组中移除该元素。
|
|
149
|
-
*
|
|
150
|
-
* 约定:excludeKeys 与 excludeValues 均为数组类型。
|
|
151
|
-
*
|
|
152
|
-
* 示例:
|
|
153
|
-
* omitFields({ a:1, b:undefined, c:null }, ['a'], [undefined]) -> { c:null }
|
|
154
|
-
* omitFields([{ a:1, b:null }, null, 0], ['a'], [null]) -> [{}, 0]
|
|
155
|
-
*
|
|
156
|
-
* 注意:仅当第一个参数为对象或数组时执行过滤,否则原样返回。
|
|
157
|
-
*
|
|
158
|
-
* @template T
|
|
159
|
-
* @param {Record<string, any> | Array<any>} data - 原始数据(对象或数组)
|
|
160
|
-
* @param {string[]} [excludeKeys=[]] - 要排除的字段名(对象属性名)数组
|
|
161
|
-
* @param {any[]} [excludeValues=[]] - 要排除的字段值数组;当包含 undefined/null 等时,将移除这些值对应的字段或数组元素
|
|
162
|
-
* @returns {T} 过滤后的数据,类型与入参保持一致
|
|
163
|
-
*/
|
|
164
152
|
export const omitFields = (data, excludeKeys = [], excludeValues = []) => {
|
|
165
153
|
const shouldDropValue = (v) => excludeValues.some((x) => x === v);
|
|
166
154
|
|
|
@@ -176,15 +164,15 @@ export const omitFields = (data, excludeKeys = [], excludeValues = []) => {
|
|
|
176
164
|
};
|
|
177
165
|
|
|
178
166
|
if (isType(data, 'array')) {
|
|
179
|
-
return
|
|
167
|
+
return data.filter((item) => !shouldDropValue(item)).map((item) => (isType(item, 'object') ? cleanObject(item) : item));
|
|
180
168
|
}
|
|
181
169
|
|
|
182
170
|
if (isType(data, 'object')) {
|
|
183
|
-
return
|
|
171
|
+
return cleanObject(data);
|
|
184
172
|
}
|
|
185
173
|
|
|
186
174
|
// 非对象/数组则原样返回(不处理)
|
|
187
|
-
return
|
|
175
|
+
return data;
|
|
188
176
|
};
|
|
189
177
|
|
|
190
178
|
export const isEmptyObject = (obj) => {
|
|
@@ -255,117 +243,86 @@ export const filterLogFields = (body, excludeFields = '') => {
|
|
|
255
243
|
return filtered;
|
|
256
244
|
};
|
|
257
245
|
|
|
258
|
-
//
|
|
259
|
-
|
|
260
|
-
|
|
261
|
-
|
|
262
|
-
|
|
263
|
-
|
|
264
|
-
|
|
265
|
-
|
|
266
|
-
const validTypes = ['string', 'number', 'text', 'array'];
|
|
267
|
-
return validTypes.includes(type);
|
|
246
|
+
// 将 lowerCamelCase 或单词形式转换为下划线风格(snake_case)
|
|
247
|
+
// 例如:userTable -> user_table, testNewFormat -> test_new_format, users -> users, orderV2 -> order_v2
|
|
248
|
+
export const toSnakeTableName = (name) => {
|
|
249
|
+
if (!name) return name;
|
|
250
|
+
return String(name)
|
|
251
|
+
.replace(/([a-z0-9])([A-Z])/g, '$1_$2')
|
|
252
|
+
.replace(/([A-Z]+)([A-Z][a-z0-9]+)/g, '$1_$2')
|
|
253
|
+
.toLowerCase();
|
|
268
254
|
};
|
|
269
255
|
|
|
270
|
-
|
|
271
|
-
|
|
272
|
-
|
|
273
|
-
}
|
|
274
|
-
|
|
275
|
-
// 验证默认值是否为null、字符串或数字
|
|
276
|
-
export const validateDefaultValue = (value) => {
|
|
277
|
-
if (value === 'null') return true;
|
|
278
|
-
// 检查是否为数字
|
|
279
|
-
if (!isNaN(parseFloat(value)) && isFinite(parseFloat(value))) return true;
|
|
280
|
-
// 其他情况视为字符串,都是有效的
|
|
281
|
-
return true;
|
|
282
|
-
};
|
|
283
|
-
|
|
284
|
-
// 验证索引标识是否为0或1
|
|
285
|
-
export const validateIndex = (value) => {
|
|
286
|
-
return value === '0' || value === '1';
|
|
287
|
-
};
|
|
288
|
-
|
|
289
|
-
// 验证正则表达式是否有效
|
|
290
|
-
export const validateRegex = (value) => {
|
|
291
|
-
if (value === 'null') return true;
|
|
292
|
-
try {
|
|
293
|
-
new RegExp(value);
|
|
294
|
-
return true;
|
|
295
|
-
} catch (e) {
|
|
296
|
-
return false;
|
|
297
|
-
}
|
|
298
|
-
};
|
|
299
|
-
|
|
300
|
-
// 专门用于处理⚡分隔的字段规则
|
|
301
|
-
export const parseFieldRule = (rule) => {
|
|
302
|
-
const allParts = rule.split('⚡');
|
|
303
|
-
|
|
304
|
-
// 必须包含7个部分:显示名⚡类型⚡最小值⚡最大值⚡默认值⚡是否索引⚡正则约束
|
|
305
|
-
if (allParts.length !== 7) {
|
|
306
|
-
throw new Error(`字段规则格式错误,必须包含7个部分,当前包含${allParts.length}个部分`);
|
|
307
|
-
}
|
|
308
|
-
|
|
309
|
-
// 验证各个部分的格式
|
|
310
|
-
const [name, type, minValue, maxValue, defaultValue, isIndex, regexConstraint] = allParts;
|
|
311
|
-
|
|
312
|
-
// 第1个值:名称必须为中文、数字、字母
|
|
313
|
-
if (!validateFieldName(name)) {
|
|
314
|
-
throw new Error(`字段名称 "${name}" 格式错误,必须为中文、数字、字母`);
|
|
315
|
-
}
|
|
316
|
-
|
|
317
|
-
// 第2个值:字段类型必须为string,number,text,array之一
|
|
318
|
-
if (!validateFieldType(type)) {
|
|
319
|
-
throw new Error(`字段类型 "${type}" 格式错误,必须为string、number、text、array之一`);
|
|
320
|
-
}
|
|
321
|
-
|
|
322
|
-
// 第3个值:最小值必须为null或数字
|
|
323
|
-
if (!validateMinMax(minValue)) {
|
|
324
|
-
throw new Error(`最小值 "${minValue}" 格式错误,必须为null或数字`);
|
|
325
|
-
}
|
|
326
|
-
|
|
327
|
-
// 第4个值:最大值必须为null或数字
|
|
328
|
-
if (!validateMinMax(maxValue)) {
|
|
329
|
-
throw new Error(`最大值 "${maxValue}" 格式错误,必须为null或数字`);
|
|
330
|
-
}
|
|
256
|
+
/**
|
|
257
|
+
* 创建并校验 Bun SQL 客户端
|
|
258
|
+
* - 连接成功后返回 SQL 实例,失败会自动 close 并抛出
|
|
259
|
+
* @param {object} options 传给 new SQL 的参数(如 { max: 1, bigint: true })
|
|
260
|
+
*/
|
|
331
261
|
|
|
332
|
-
|
|
333
|
-
|
|
334
|
-
|
|
262
|
+
// 组合最终数据库连接串:
|
|
263
|
+
// - 基于 DB_* 环境变量构建(DB_TYPE/DB_HOST/DB_PORT/DB_USER/DB_PASS/DB_NAME)
|
|
264
|
+
// - sqlite: sqlite:<DB_NAME>(文件路径或 :memory:)
|
|
265
|
+
// - postgresql: postgres://[user:pass@]host:port/DB_NAME
|
|
266
|
+
// - mysql: mysql://[user:pass@]host:port/DB_NAME
|
|
267
|
+
export const buildDatabaseUrl = () => {
|
|
268
|
+
const type = Env.DB_TYPE || '';
|
|
269
|
+
const host = Env.DB_HOST || '';
|
|
270
|
+
const port = Env.DB_PORT;
|
|
271
|
+
const user = encodeURIComponent(Env.DB_USER || '');
|
|
272
|
+
const pass = encodeURIComponent(Env.DB_PASS || '');
|
|
273
|
+
const name = Env.DB_NAME || '';
|
|
274
|
+
|
|
275
|
+
if (!type) throw new Error('DB_TYPE 未配置');
|
|
276
|
+
if (!name && type !== 'sqlite') throw new Error('DB_NAME 未配置');
|
|
277
|
+
|
|
278
|
+
if (type === 'sqlite') {
|
|
279
|
+
if (!name) throw new Error('DB_NAME 未配置');
|
|
280
|
+
return `sqlite://${name}`;
|
|
335
281
|
}
|
|
336
282
|
|
|
337
|
-
|
|
338
|
-
|
|
339
|
-
|
|
283
|
+
if (type === 'postgresql' || type === 'postgres') {
|
|
284
|
+
if (!host || !port) throw new Error('DB_HOST/DB_PORT 未配置');
|
|
285
|
+
const auth = user || pass ? `${user}:${pass}@` : '';
|
|
286
|
+
return `postgres://${auth}${host}:${port}/${encodeURIComponent(name)}`;
|
|
340
287
|
}
|
|
341
288
|
|
|
342
|
-
|
|
343
|
-
|
|
344
|
-
|
|
289
|
+
if (type === 'mysql') {
|
|
290
|
+
if (!host || !port) throw new Error('DB_HOST/DB_PORT 未配置');
|
|
291
|
+
const auth = user || pass ? `${user}:${pass}@` : '';
|
|
292
|
+
return `mysql://${auth}${host}:${port}/${encodeURIComponent(name)}`;
|
|
345
293
|
}
|
|
346
294
|
|
|
347
|
-
|
|
295
|
+
throw new Error(`不支持的 DB_TYPE: ${type}`);
|
|
348
296
|
};
|
|
349
297
|
|
|
350
|
-
/**
|
|
351
|
-
* 创建并校验 Bun SQL 客户端
|
|
352
|
-
* - 否则按 scripts/syncDb.js 的方式拼接 URL
|
|
353
|
-
* - 连接成功后返回 SQL 实例,失败会自动 close 并抛出
|
|
354
|
-
* @param {object} options 传给 new SQL 的参数(如 { max: 1, bigint: true })
|
|
355
|
-
*/
|
|
356
298
|
export async function createSqlClient(options = {}) {
|
|
357
|
-
const
|
|
358
|
-
|
|
359
|
-
|
|
360
|
-
|
|
361
|
-
|
|
362
|
-
|
|
363
|
-
|
|
364
|
-
|
|
299
|
+
const finalUrl = buildDatabaseUrl();
|
|
300
|
+
let sql = null;
|
|
301
|
+
if (Env.DB_TYPE === 'sqlite') {
|
|
302
|
+
sql = new SQL(finalUrl);
|
|
303
|
+
} else {
|
|
304
|
+
sql = new SQL({
|
|
305
|
+
url: finalUrl,
|
|
306
|
+
max: options.max ?? 1,
|
|
307
|
+
bigint: options.bigint ?? true,
|
|
308
|
+
...options
|
|
309
|
+
});
|
|
310
|
+
}
|
|
311
|
+
|
|
365
312
|
try {
|
|
366
|
-
|
|
367
|
-
|
|
368
|
-
|
|
313
|
+
// 连接健康检查:按协议选择
|
|
314
|
+
let version = '';
|
|
315
|
+
if (Env.DB_TYPE === 'sqlite') {
|
|
316
|
+
const v = await sql`SELECT sqlite_version() AS version`;
|
|
317
|
+
version = v?.[0]?.version;
|
|
318
|
+
} else if (Env.DB_TYPE === 'postgresql' || Env.DB_TYPE === 'postgres') {
|
|
319
|
+
const v = await sql`SELECT version() AS version`;
|
|
320
|
+
version = v?.[0]?.version;
|
|
321
|
+
} else {
|
|
322
|
+
const v = await sql`SELECT VERSION() AS version`;
|
|
323
|
+
version = v?.[0]?.version;
|
|
324
|
+
}
|
|
325
|
+
Logger.info(`数据库连接成功,version: ${version}`);
|
|
369
326
|
return sql;
|
|
370
327
|
} catch (error) {
|
|
371
328
|
Logger.error('数据库连接测试失败:', error);
|
package/utils/sqlManager.js
CHANGED
|
@@ -71,7 +71,7 @@ export class SqlManager {
|
|
|
71
71
|
const isWriteLike = /^\s*(insert|update|delete|replace)\b/i.test(query);
|
|
72
72
|
const client = conn || this.#sql;
|
|
73
73
|
try {
|
|
74
|
-
if (Env.
|
|
74
|
+
if (Env.DB_DEBUG === 1) {
|
|
75
75
|
Logger.debug('执行SQL:', { sql: query, params });
|
|
76
76
|
}
|
|
77
77
|
// 读查询
|
package/utils/validate.js
CHANGED
|
@@ -1,6 +1,4 @@
|
|
|
1
|
-
import { isType,
|
|
2
|
-
|
|
3
|
-
// 移除本文件重复实现,统一复用 index.js 导出的校验函数与 parseFieldRule
|
|
1
|
+
import { isType, parseRule } from './index.js';
|
|
4
2
|
|
|
5
3
|
/**
|
|
6
4
|
* 验证器类
|
|
@@ -65,8 +63,8 @@ export class Validator {
|
|
|
65
63
|
for (const fieldName of required) {
|
|
66
64
|
if (!(fieldName in data) || data[fieldName] === undefined || data[fieldName] === null || data[fieldName] === '') {
|
|
67
65
|
result.code = 1;
|
|
68
|
-
const ruleParts =
|
|
69
|
-
const fieldLabel = ruleParts[0] || fieldName;
|
|
66
|
+
const ruleParts = parseRule(rules[fieldName] || '');
|
|
67
|
+
const fieldLabel = (ruleParts && ruleParts[0]) || fieldName;
|
|
70
68
|
result.fields[fieldName] = `${fieldLabel}(${fieldName})为必填项`;
|
|
71
69
|
}
|
|
72
70
|
}
|
|
@@ -101,10 +99,10 @@ export class Validator {
|
|
|
101
99
|
* 验证单个字段的值
|
|
102
100
|
*/
|
|
103
101
|
validateFieldValue(value, rule, fieldName) {
|
|
104
|
-
const [name, type,
|
|
105
|
-
const min =
|
|
106
|
-
const max =
|
|
107
|
-
const spec = regexConstraint === 'null' ? null : regexConstraint.trim();
|
|
102
|
+
const [name, type, minRaw, maxRaw, defaultValue, isIndexRaw, regexConstraint] = parseRule(rule);
|
|
103
|
+
const min = minRaw === 'null' ? null : /** @type {number} */ (typeof minRaw === 'number' ? minRaw : Number(minRaw));
|
|
104
|
+
const max = maxRaw === 'null' ? null : /** @type {number} */ (typeof maxRaw === 'number' ? maxRaw : Number(maxRaw));
|
|
105
|
+
const spec = regexConstraint === 'null' ? null : String(regexConstraint).trim();
|
|
108
106
|
|
|
109
107
|
switch (type.toLowerCase()) {
|
|
110
108
|
case 'number':
|