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.
@@ -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
+ }
@@ -1,16 +1,16 @@
1
1
  {
2
- "email": "邮箱⚡string⚡5⚡100⚡null⚡1⚡^[a-zA-Z0-9._%+-]+@[a-zA-Z0-9.-]+\\.[a-zA-Z]{2,}$",
3
- "phone": "手机号⚡string⚡11⚡11⚡null⚡1⚡^1[3-9]\\d{9}$",
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⚡null⚡0⚡null",
7
- "description": "描述⚡string⚡0⚡500⚡null⚡0⚡null",
8
- "keyword": "关键词⚡string⚡1⚡50⚡null⚡1⚡null",
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⚡null⚡0⚡^\\d{4}-\\d{2}-\\d{2}$",
12
- "datetime": "日期时间⚡string⚡19⚡25⚡null⚡0⚡^\\d{4}-\\d{2}-\\d{2}T\\d{2}:\\d{2}:\\d{2}",
13
- "filename": "文件名⚡string⚡1⚡255⚡null⚡0⚡null",
14
- "url": "网址⚡string⚡5⚡500⚡null⚡0⚡^https?://",
15
- "tag": "标签⚡array⚡0⚡10⚡[]⚡0⚡null"
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⚡null⚡1⚡null",
3
- "content": "内容⚡string⚡0⚡100⚡null⚡0⚡null",
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⚡null⚡0⚡null"
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
- export const ruleSplit = (rule) => {
49
- const allParts = rule.split(',');
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
- // 如果部分数量小于等于5,直接返回
52
- if (allParts.length <= 5) {
53
- return allParts;
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
- // 只取前4个部分,剩余的都合并为第5个部分
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 /** @type {any} */ (data.filter((item) => !shouldDropValue(item)).map((item) => (isType(item, 'object') ? cleanObject(item) : item)));
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 /** @type {any} */ (cleanObject(data));
171
+ return cleanObject(data);
184
172
  }
185
173
 
186
174
  // 非对象/数组则原样返回(不处理)
187
- return /** @type {any} */ (data);
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
- export const validateFieldName = (name) => {
260
- const nameRegex = /^[\u4e00-\u9fa5a-zA-Z0-9 _-]+$/;
261
- return nameRegex.test(name);
262
- };
263
-
264
- // 验证字段类型是否为指定的四种类型之一
265
- export const validateFieldType = (type) => {
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
- // 验证最小值/最大值是否为null或数字
271
- export const validateMinMax = (value) => {
272
- return value === 'null' || (!isNaN(parseFloat(value)) && isFinite(parseFloat(value)));
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
- // 第5个值:默认值必须为null、字符串或数字
333
- if (!validateDefaultValue(defaultValue)) {
334
- throw new Error(`默认值 "${defaultValue}" 格式错误,必须为null、字符串或数字`);
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
- // 第6个值:是否创建索引必须为0或1
338
- if (!validateIndex(isIndex)) {
339
- throw new Error(`索引标识 "${isIndex}" 格式错误,必须为0或1`);
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
- // 第7个值:必须为null或正则表达式
343
- if (!validateRegex(regexConstraint)) {
344
- throw new Error(`正则约束 "${regexConstraint}" 格式错误,必须为null或有效的正则表达式`);
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
- return allParts;
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 url = `mysql://${encodeURIComponent(Env.MYSQL_USER)}:${encodeURIComponent(Env.MYSQL_PASSWORD)}@${Env.MYSQL_HOST}:${Env.MYSQL_PORT}/${Env.MYSQL_DB}`;
358
-
359
- const sql = new SQL({
360
- url: url,
361
- max: options.max ?? 1,
362
- bigint: options.bigint ?? true,
363
- ...options
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
- const ver = await sql`SELECT VERSION() AS version`;
367
- const version = ver?.[0]?.version;
368
- Logger.info(`数据库连接成功,MySQL 版本: ${version}`);
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);
@@ -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.MYSQL_DEBUG === 1) {
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, parseFieldRule } from './index.js';
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 = parseFieldRule(rules[fieldName] || '');
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, minStr, maxStr, defaultValue, isIndexStr, regexConstraint] = parseFieldRule(rule);
105
- const min = minStr === 'null' ? null : parseInt(minStr) || 0;
106
- const max = maxStr === 'null' ? null : parseInt(maxStr) || 0;
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':