befly 2.3.2 → 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 ADDED
@@ -0,0 +1,109 @@
1
+ #!/usr/bin/env -S bun run
2
+ // Befly CLI (Bun): 列出并执行 core/scripts 与 tpl/scripts 下的脚本
3
+
4
+ import path from 'node:path';
5
+ import { Glob } from 'bun';
6
+ import { __dirscript as coreScriptsDir, getProjectDir } from '../system.js';
7
+
8
+ // 解析目录(来自 system.js)
9
+ // 核心脚本目录:core/scripts
10
+ // 用户项目(如 tpl)的脚本目录:始终基于当前工作目录
11
+ const tplScriptsDir = getProjectDir('scripts');
12
+
13
+ function safeList(dir) {
14
+ try {
15
+ // 使用 Bun.Glob 查找当前目录下的所有 .js 文件(不递归)
16
+ const glob = new Glob('*.js');
17
+ const files = Array.from(glob.scanSync({ cwd: dir, absolute: false, onlyFiles: true, dot: false }));
18
+ return files.map((f) => path.basename(f, '.js')).sort();
19
+ } catch {
20
+ return [];
21
+ }
22
+ }
23
+
24
+ function buildScriptItems() {
25
+ const coreList = safeList(coreScriptsDir);
26
+ const tplList = safeList(tplScriptsDir);
27
+ const coreSet = new Set(coreList);
28
+
29
+ const items = [];
30
+ for (const name of coreList) {
31
+ items.push({
32
+ name: name,
33
+ source: 'core',
34
+ duplicate: tplList.includes(name),
35
+ path: path.resolve(coreScriptsDir, `${name}.js`)
36
+ });
37
+ }
38
+ for (const name of tplList) {
39
+ items.push({
40
+ name: name,
41
+ source: 'tpl',
42
+ duplicate: coreSet.has(name),
43
+ path: path.resolve(tplScriptsDir, `${name}.js`)
44
+ });
45
+ }
46
+ // 排序:名称字典序,core 在前
47
+ items.sort((a, b) => (a.name === b.name ? (a.source === b.source ? 0 : a.source === 'core' ? -1 : 1) : a.name.localeCompare(b.name)));
48
+ return items;
49
+ }
50
+
51
+ function printAllScripts() {
52
+ const items = buildScriptItems();
53
+ if (items.length === 0) {
54
+ console.log(' • <无>');
55
+ return;
56
+ }
57
+ for (const it of items) {
58
+ if (it.source === 'tpl' && it.duplicate) console.log(` • ${it.name}(重复)`);
59
+ else console.log(` • ${it.name}`);
60
+ }
61
+ }
62
+
63
+ async function resolveScriptPath(name) {
64
+ const base = name.endsWith('.js') ? name.slice(0, -3) : name;
65
+ const filename = `${base}.js`;
66
+ const corePath = path.resolve(coreScriptsDir, filename);
67
+ const tplPath = path.resolve(tplScriptsDir, filename);
68
+ if (await Bun.file(corePath).exists()) return corePath;
69
+ if (await Bun.file(tplPath).exists()) return tplPath;
70
+ // 回退到列表匹配(防止极端路径或大小写差异)
71
+ const items = buildScriptItems();
72
+ const hit = items.find((it) => it.name.toLowerCase() === base.toLowerCase() && it.source === 'core') || items.find((it) => it.name.toLowerCase() === base.toLowerCase());
73
+ return hit ? hit.path : null;
74
+ }
75
+
76
+ async function runScriptAtPath(targetPath, label, args = []) {
77
+ const bunExe = process.execPath || 'bun';
78
+ const child = Bun.spawn({
79
+ cmd: [bunExe, targetPath, ...args],
80
+ stdio: ['inherit', 'inherit', 'inherit'],
81
+ cwd: process.cwd(),
82
+ env: { ...process.env, LOG_TO_CONSOLE: '1' }
83
+ });
84
+ const code = await child.exited;
85
+ return code ?? 0;
86
+ }
87
+
88
+ async function main() {
89
+ const [, , cmd, ...args] = process.argv;
90
+ // 无参数:打印所有脚本
91
+ if (!cmd) {
92
+ printAllScripts();
93
+ process.exit(0);
94
+ }
95
+ // 按名称执行(将剩余参数透传给脚本)
96
+ const target = await resolveScriptPath(cmd);
97
+ if (!target) {
98
+ console.error(`未找到脚本: ${cmd}`);
99
+ printAllScripts();
100
+ process.exit(1);
101
+ }
102
+ const code = await runScriptAtPath(target, cmd, args);
103
+ process.exit(code ?? 0);
104
+ }
105
+
106
+ main().catch((e) => {
107
+ console.error('Befly CLI 执行失败:', e);
108
+ process.exit(1);
109
+ });
package/checks/table.js CHANGED
@@ -1,6 +1,6 @@
1
1
  import path from 'node:path';
2
2
  import { Logger } from '../utils/logger.js';
3
- import { parseFieldRule, validateFieldName, validateFieldType, validateMinMax, validateDefaultValue, validateIndex, validateRegex } from '../utils/index.js';
3
+ import { parseRule } from '../utils/index.js';
4
4
  import { __dirtables, getProjectDir } from '../system.js';
5
5
 
6
6
  // 所有校验函数均复用 utils/index.js 导出的实现
@@ -65,8 +65,9 @@ export const checkTable = async () => {
65
65
  const lowerCamelCaseRegex = /^[a-z][a-z0-9]*(?:[A-Z][a-z0-9]*)*$/;
66
66
  if (!lowerCamelCaseRegex.test(fileBaseName)) {
67
67
  Logger.error(`${fileType}表 ${fileName} 文件名必须使用小驼峰命名(例如 testCustomers.json)`);
68
- // 命名不合规,直接标记为无效文件
69
- throw new Error('文件命名不符合小驼峰规范');
68
+ // 命名不合规,按保留字段处理模式:记录错误并计为无效文件,继续下一个文件
69
+ invalidFiles++;
70
+ continue;
70
71
  }
71
72
 
72
73
  // 读取并解析 JSON 文件
@@ -75,126 +76,110 @@ export const checkTable = async () => {
75
76
  let fileRules = 0;
76
77
 
77
78
  // 检查 table 中的每个验证规则
78
- for (const [fieldName, rule] of Object.entries(table)) {
79
- fileRules++;
80
- totalRules++;
81
-
82
- // 检查是否使用了保留字段
83
- if (reservedFields.includes(fieldName)) {
84
- Logger.error(`${fileType}表 ${fileName} 文件包含保留字段 ${fieldName},不能在表定义中使用以下字段: ${reservedFields.join(', ')}`);
85
- fileValid = false;
86
- continue;
87
- }
88
-
79
+ for (const [colKey, rule] of Object.entries(table)) {
89
80
  // 验证规则格式
90
81
  try {
91
- const ruleParts = parseFieldRule(rule);
82
+ fileRules++;
83
+ totalRules++;
92
84
 
93
- if (ruleParts.length !== 7) {
94
- Logger.warn(`${fileType}表 ${fileName} 文件 ${fieldName} 验证规则错误,应包含 7 个部分,但包含 ${ruleParts.length} 个部分`);
85
+ // 检查是否使用了保留字段
86
+ if (reservedFields.includes(colKey)) {
87
+ Logger.error(`${fileType}表 ${fileName} 文件包含保留字段 ${colKey},不能在表定义中使用以下字段: ${reservedFields.join(', ')}`);
95
88
  fileValid = false;
96
- continue;
97
89
  }
98
90
 
99
- const [name, type, minStr, maxStr, defaultValue, isIndexStr, regexConstraint] = ruleParts;
91
+ const allParts = rule.split('⚡');
92
+
93
+ // 必须包含7个部分:显示名⚡类型⚡最小值⚡最大值⚡默认值⚡是否索引⚡正则约束
94
+ if (allParts.length !== 7) {
95
+ Logger.error(`${fileType}表 ${fileName} 文件 ${colKey} 字段规则格式错误,必须包含7个部分,当前包含${allParts.length}个部分`);
96
+ fileValid = false;
97
+ }
98
+
99
+ const [fieldName, fieldType, fieldMin, fieldMax, fieldDefault, fieldIndex, fieldRegx] = allParts;
100
100
 
101
- // 使用新的验证函数进行严格验证
102
101
  // 第1个值:名称必须为中文、数字、字母
103
- if (!validateFieldName(name)) {
104
- Logger.error(`${fileType}表 ${fileName} 文件 ${fieldName} 名称 "${name}" 格式错误,必须为中文、数字、字母`);
102
+ if (!/^[\u4e00-\u9fa5a-zA-Z0-9 _-]+$/.test(fieldName)) {
103
+ Logger.error(`${fileType}表 ${fileName} 文件 ${colKey} 字段名称 "${fieldName}" 格式错误,必须为中文、数字、字母、下划线、短横线`);
105
104
  fileValid = false;
106
- continue;
107
105
  }
108
106
 
109
107
  // 第2个值:字段类型必须为string,number,text,array之一
110
- if (!validateFieldType(type)) {
111
- Logger.error(`${fileType}表 ${fileName} 文件 ${fieldName} 类型 "${type}" 格式错误,必须为string、number、text、array之一`);
108
+ if (!['string', 'number', 'text', 'array'].includes(fieldType)) {
109
+ Logger.error(`${fileType}表 ${fileName} 文件 ${colKey} 字段类型 "${fieldType}" 格式错误,必须为string、number、text、array之一`);
112
110
  fileValid = false;
113
- continue;
114
111
  }
115
112
 
116
- // 第3个值:最小值必须为null或数字
117
- if (!validateMinMax(minStr)) {
118
- Logger.error(`${fileType}表 ${fileName} 文件 ${fieldName} 最小值 "${minStr}" 格式错误,必须为null或数字`);
113
+ // 第3/4个值:需要是 null 或 数字(并包含最小值<=最大值的约束)
114
+ if (!(fieldMin === 'null' || !Number.isNaN(Number(fieldMin)))) {
115
+ Logger.error(`${fileType}表 ${fileName} 文件 ${colKey} 最小值 "${fieldMin}" 格式错误,必须为null或数字`);
116
+ fileValid = false;
117
+ }
118
+ if (!(fieldMax === 'null' || !Number.isNaN(Number(fieldMax)))) {
119
+ Logger.error(`${fileType}表 ${fileName} 文件 ${colKey} 最大值 "${fieldMax}" 格式错误,必须为null或数字`);
119
120
  fileValid = false;
120
- continue;
121
121
  }
122
122
 
123
- // 第4个值与类型联动校验
124
- if (type === 'text') {
125
- // text 类型:不允许设置最小/最大长度与默认值(均需为 null)
126
- if (minStr !== 'null') {
127
- Logger.error(`${fileType}表 ${fileName} 文件 ${fieldName} 的 text 类型最小值必须为 null,当前为 "${minStr}"`);
123
+ // 约束:当最小值与最大值均为数字时,要求最小值 <= 最大值
124
+ if (fieldMin !== 'null' && fieldMax !== 'null') {
125
+ if (Number(fieldMin) > Number(fieldMax)) {
126
+ Logger.error(`${fileType}表 ${fileName} 文件 ${colKey} 最小值 "${fieldMin}" 不能大于最大值 "${fieldMax}"`);
128
127
  fileValid = false;
129
- continue;
130
128
  }
131
- if (maxStr !== 'null') {
132
- Logger.error(`${fileType}表 ${fileName} 文件 ${fieldName} 的 text 类型最大长度必须为 null,当前为 "${maxStr}"`);
129
+ }
130
+
131
+ // 第6个值:是否创建索引必须为0或1
132
+ if (fieldIndex !== '0' && fieldIndex !== '1') {
133
+ Logger.error(`${fileType}表 ${fileName} 文件 ${colKey} 索引标识 "${fieldIndex}" 格式错误,必须为0或1`);
134
+ fileValid = false;
135
+ }
136
+
137
+ // 第7个值:必须为null或正则表达式
138
+ if (fieldRegx !== 'null') {
139
+ try {
140
+ // 仅尝试构造以校验有效性
141
+ // eslint-disable-next-line no-new
142
+ new RegExp(fieldRegx);
143
+ } catch (_) {
144
+ Logger.error(`${fileType}表 ${fileName} 文件 ${colKey} 正则约束 "${fieldRegx}" 格式错误,必须为null或有效的正则表达式`);
133
145
  fileValid = false;
134
- continue;
135
146
  }
136
- } else if (type === 'string') {
137
- // string:最大长度必为具体数字,且 1..65535
138
- if (maxStr === 'null' || !validateMinMax(maxStr)) {
139
- Logger.error(`${fileType}表 ${fileName} 文件 ${fieldName} 最大长度 "${maxStr}" 格式错误,string 类型必须为具体数字`);
147
+ }
148
+
149
+ // 第4个值与类型联动校验 + 默认值规则(精简实现)
150
+ if (fieldType === 'text') {
151
+ // text:min/max 必须为 null,默认值必须为 'null'
152
+ if (fieldMin !== 'null') {
153
+ Logger.error(`${fileType}表 ${fileName} 文件 ${colKey} 的 text 类型最小值必须为 null,当前为 "${fieldMin}"`);
140
154
  fileValid = false;
141
- continue;
142
155
  }
143
- const maxVal = parseInt(maxStr, 10);
144
- if (!(maxVal > 0 && maxVal <= 65535)) {
145
- Logger.error(`${fileType}表 ${fileName} 文件 ${fieldName} 最大长度 ${maxStr} 越界,string 类型长度必须在 1..65535 范围内`);
156
+ if (fieldMax !== 'null') {
157
+ Logger.error(`${fileType}表 ${fileName} 文件 ${colKey} text 类型最大长度必须为 null,当前为 "${fieldMax}"`);
146
158
  fileValid = false;
147
- continue;
148
159
  }
149
- } else if (type === 'array') {
150
- // array:最大长度必为数字(用于JSON字符串长度限制)
151
- if (maxStr === 'null' || !validateMinMax(maxStr)) {
152
- Logger.error(`${fileType}表 ${fileName} 文件 ${fieldName} 最大长度 "${maxStr}" 格式错误,array 类型必须为具体数字`);
160
+ if (fieldDefault !== 'null') {
161
+ Logger.error(`${fileType}表 ${fileName} 文件 ${colKey} 为 text 类型,默认值必须为 null,当前为 "${fieldDefault}"`);
153
162
  fileValid = false;
154
- continue;
155
163
  }
156
- } else {
157
- // number 等其他:允许 null 或数字
158
- if (!validateMinMax(maxStr)) {
159
- Logger.error(`${fileType}表 ${fileName} 文件 ${fieldName} 最大值 "${maxStr}" 格式错误,必须为null或数字`);
164
+ } else if (fieldType === 'string' || fieldType === 'array') {
165
+ if (Number.isNaN(Number(fieldMax))) {
166
+ Logger.error(`${fileType}表 ${fileName} 文件 ${colKey} 为 string,array 类型,最大长度必须为数字,当前为 "${fieldMax}"`);
160
167
  fileValid = false;
161
- continue;
162
168
  }
163
- }
164
-
165
- // 第5个值:默认值校验
166
- if (type === 'text') {
167
- // text 不允许默认值
168
- if (defaultValue !== 'null') {
169
- Logger.error(`${fileType}表 ${fileName} 文件 ${fieldName} 为 text 类型,默认值必须为 null,当前为 "${defaultValue}"`);
169
+ const maxVal = parseInt(fieldMax, 10);
170
+ if (maxVal > 65535) {
171
+ Logger.error(`${fileType}表 ${fileName} 文件 ${colKey} 最大长度 ${fieldMax} 越界,string,array 类型长度必须在 1..65535 范围内`);
170
172
  fileValid = false;
171
- continue;
172
173
  }
173
- } else {
174
- if (!validateDefaultValue(defaultValue)) {
175
- Logger.error(`${fileType}表 ${fileName} 文件 ${fieldName} 默认值 "${defaultValue}" 格式错误,必须为null、字符串或数字`);
174
+ } else if (fieldType === 'number') {
175
+ if (Number.isNaN(Number(fieldDefault))) {
176
+ Logger.error(`${fileType}表 ${fileName} 文件 ${colKey} number 类型,默认值必须为数字,当前为 "${fieldDefault}"`);
176
177
  fileValid = false;
177
- continue;
178
178
  }
179
179
  }
180
-
181
- // 第6个值:是否创建索引必须为0或1
182
- if (!validateIndex(isIndexStr)) {
183
- Logger.error(`${fileType}表 ${fileName} 文件 ${fieldName} 索引标识 "${isIndexStr}" 格式错误,必须为0或1`);
184
- fileValid = false;
185
- continue;
186
- }
187
-
188
- // 第7个值:必须为null或正则表达式
189
- if (!validateRegex(regexConstraint)) {
190
- Logger.error(`${fileType}表 ${fileName} 文件 ${fieldName} 正则约束 "${regexConstraint}" 格式错误,必须为null或有效的正则表达式`);
191
- fileValid = false;
192
- continue;
193
- }
194
180
  } catch (error) {
195
- Logger.error(`${fileType}表 ${fileName} 文件 ${fieldName} 验证规则解析失败: ${error.message}`);
181
+ Logger.error(`${fileType}表 ${fileName} 文件 ${colKey} 验证规则解析失败: ${error.message}`);
196
182
  fileValid = false;
197
- continue;
198
183
  }
199
184
  }
200
185
 
package/config/env.js CHANGED
@@ -24,14 +24,16 @@ export const Env = {
24
24
  // 时区
25
25
  TZ: process.env.TZ,
26
26
  // 数据库配置
27
- MYSQL_ENABLE: Number(process.env.MYSQL_ENABLE),
28
- MYSQL_HOST: process.env.MYSQL_HOST,
29
- MYSQL_PORT: Number(process.env.MYSQL_PORT),
30
- MYSQL_DB: process.env.MYSQL_DB,
31
- MYSQL_USER: process.env.MYSQL_USER,
32
- MYSQL_PASSWORD: process.env.MYSQL_PASSWORD,
33
- MYSQL_DEBUG: Number(process.env.MYSQL_DEBUG),
34
- MYSQL_POOL_MAX: Number(process.env.MYSQL_POOL_MAX),
27
+ DB_ENABLE: Number(process.env.DB_ENABLE),
28
+ // 通用数据库连接参数
29
+ DB_TYPE: process.env.DB_TYPE, // sqlite | mysql | postgresql
30
+ DB_HOST: process.env.DB_HOST,
31
+ DB_PORT: Number(process.env.DB_PORT),
32
+ DB_USER: process.env.DB_USER,
33
+ DB_PASS: process.env.DB_PASS,
34
+ DB_NAME: process.env.DB_NAME,
35
+ DB_DEBUG: Number(process.env.DB_DEBUG),
36
+ DB_POOL_MAX: Number(process.env.DB_POOL_MAX),
35
37
  // Redis配置
36
38
  REDIS_URL: process.env.REDIS_URL,
37
39
  REDIS_ENABLE: Number(process.env.REDIS_ENABLE),
package/package.json CHANGED
@@ -1,7 +1,7 @@
1
1
  {
2
2
  "name": "befly",
3
- "version": "2.3.2",
4
- "description": "Buma - 为 Bun 专属打造的 API 接口框架核心引擎",
3
+ "version": "2.3.3",
4
+ "description": "Befly - 为 Bun 专属打造的 API 接口框架核心引擎",
5
5
  "type": "module",
6
6
  "private": false,
7
7
  "publishConfig": {
@@ -12,11 +12,10 @@
12
12
  "exports": {
13
13
  ".": "./main.js"
14
14
  },
15
- "scripts": {
16
- "ra": "bun run ../release.js --major",
17
- "rb": "bun run ../release.js --minor",
18
- "rc": "bun run ../release.js --patch"
15
+ "bin": {
16
+ "befly": "./bin/befly.js"
19
17
  },
18
+ "scripts": {},
20
19
  "keywords": [
21
20
  "bun",
22
21
  "api",
@@ -31,6 +30,7 @@
31
30
  "homepage": "https://chensuiyi.me",
32
31
  "license": "Apache-2.0",
33
32
  "files": [
33
+ "bin/",
34
34
  "apis/",
35
35
  "checks/",
36
36
  "config/",
@@ -51,12 +51,5 @@
51
51
  "system.js",
52
52
  "package.json",
53
53
  "README.md"
54
- ],
55
- "simple-git-hooks": {
56
- "pre-commit": "bunx --bun lint-staged"
57
- },
58
- "lint-staged": {
59
- "*.{js,css,scss,less,ts,jsx,vue,html,json,md,yaml}": "bunx --bun prettier --write --cache --ignore-unknown"
60
- },
61
- "gitHead": "1dc5f118a723969456559e758e2ba889f4601224"
54
+ ]
62
55
  }
package/plugins/db.js CHANGED
@@ -11,7 +11,7 @@ export default {
11
11
  let sql = null;
12
12
 
13
13
  try {
14
- if (Env.MYSQL_ENABLE === 1) {
14
+ if (Env.DB_ENABLE === 1) {
15
15
  // 创建 Bun SQL 客户端(内置连接池),并确保连接验证成功后再继续
16
16
  sql = await createSqlClient();
17
17
 
@@ -20,7 +20,7 @@ export default {
20
20
 
21
21
  return dbManager;
22
22
  } else {
23
- Logger.warn(`MySQL 未启用,跳过初始化`);
23
+ Logger.warn(`数据库未启用(DB_ENABLE≠1),跳过初始化`);
24
24
  return {};
25
25
  }
26
26
  } catch (error) {