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 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 导出的实现
@@ -56,135 +56,130 @@ export const checkTable = async () => {
56
56
  for (const { file, type } of allTableFiles) {
57
57
  totalFiles++;
58
58
  const fileName = path.basename(file);
59
+ const fileBaseName = path.basename(file, '.json');
59
60
  const fileType = type === 'core' ? '内核' : '项目';
60
61
 
61
62
  try {
63
+ // 1) 文件名小驼峰校验:必须以小写字母开头,后续可包含小写/数字,或多个 [大写+小写/数字] 片段
64
+ // 示例:userTable、testCustomers、common
65
+ const lowerCamelCaseRegex = /^[a-z][a-z0-9]*(?:[A-Z][a-z0-9]*)*$/;
66
+ if (!lowerCamelCaseRegex.test(fileBaseName)) {
67
+ Logger.error(`${fileType}表 ${fileName} 文件名必须使用小驼峰命名(例如 testCustomers.json)`);
68
+ // 命名不合规,按保留字段处理模式:记录错误并计为无效文件,继续下一个文件
69
+ invalidFiles++;
70
+ continue;
71
+ }
72
+
62
73
  // 读取并解析 JSON 文件
63
74
  const table = await Bun.file(file).json();
64
75
  let fileValid = true;
65
76
  let fileRules = 0;
66
77
 
67
78
  // 检查 table 中的每个验证规则
68
- for (const [fieldName, rule] of Object.entries(table)) {
69
- fileRules++;
70
- totalRules++;
71
-
72
- // 检查是否使用了保留字段
73
- if (reservedFields.includes(fieldName)) {
74
- Logger.error(`${fileType}表 ${fileName} 文件包含保留字段 ${fieldName},不能在表定义中使用以下字段: ${reservedFields.join(', ')}`);
75
- fileValid = false;
76
- continue;
77
- }
78
-
79
+ for (const [colKey, rule] of Object.entries(table)) {
79
80
  // 验证规则格式
80
81
  try {
81
- const ruleParts = parseFieldRule(rule);
82
+ fileRules++;
83
+ totalRules++;
84
+
85
+ // 检查是否使用了保留字段
86
+ if (reservedFields.includes(colKey)) {
87
+ Logger.error(`${fileType}表 ${fileName} 文件包含保留字段 ${colKey},不能在表定义中使用以下字段: ${reservedFields.join(', ')}`);
88
+ fileValid = false;
89
+ }
82
90
 
83
- if (ruleParts.length !== 7) {
84
- Logger.warn(`${fileType}表 ${fileName} 文件 ${fieldName} 验证规则错误,应包含 7 个部分,但包含 ${ruleParts.length} 个部分`);
91
+ const allParts = rule.split('⚡');
92
+
93
+ // 必须包含7个部分:显示名⚡类型⚡最小值⚡最大值⚡默认值⚡是否索引⚡正则约束
94
+ if (allParts.length !== 7) {
95
+ Logger.error(`${fileType}表 ${fileName} 文件 ${colKey} 字段规则格式错误,必须包含7个部分,当前包含${allParts.length}个部分`);
85
96
  fileValid = false;
86
- continue;
87
97
  }
88
98
 
89
- const [name, type, minStr, maxStr, defaultValue, isIndexStr, regexConstraint] = ruleParts;
99
+ const [fieldName, fieldType, fieldMin, fieldMax, fieldDefault, fieldIndex, fieldRegx] = allParts;
90
100
 
91
- // 使用新的验证函数进行严格验证
92
101
  // 第1个值:名称必须为中文、数字、字母
93
- if (!validateFieldName(name)) {
94
- Logger.error(`${fileType}表 ${fileName} 文件 ${fieldName} 名称 "${name}" 格式错误,必须为中文、数字、字母`);
102
+ if (!/^[\u4e00-\u9fa5a-zA-Z0-9 _-]+$/.test(fieldName)) {
103
+ Logger.error(`${fileType}表 ${fileName} 文件 ${colKey} 字段名称 "${fieldName}" 格式错误,必须为中文、数字、字母、下划线、短横线`);
95
104
  fileValid = false;
96
- continue;
97
105
  }
98
106
 
99
107
  // 第2个值:字段类型必须为string,number,text,array之一
100
- if (!validateFieldType(type)) {
101
- 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之一`);
102
110
  fileValid = false;
103
- continue;
104
111
  }
105
112
 
106
- // 第3个值:最小值必须为null或数字
107
- if (!validateMinMax(minStr)) {
108
- 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或数字`);
109
120
  fileValid = false;
110
- continue;
111
121
  }
112
122
 
113
- // 第4个值与类型联动校验
114
- if (type === 'text') {
115
- // text 类型:不允许设置最小/最大长度与默认值(均需为 null)
116
- if (minStr !== 'null') {
117
- 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}"`);
118
127
  fileValid = false;
119
- continue;
120
128
  }
121
- if (maxStr !== 'null') {
122
- 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或有效的正则表达式`);
123
145
  fileValid = false;
124
- continue;
125
146
  }
126
- } else if (type === 'string') {
127
- // string:最大长度必为具体数字,且 1..65535
128
- if (maxStr === 'null' || !validateMinMax(maxStr)) {
129
- 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}"`);
130
154
  fileValid = false;
131
- continue;
132
155
  }
133
- const maxVal = parseInt(maxStr, 10);
134
- if (!(maxVal > 0 && maxVal <= 65535)) {
135
- Logger.error(`${fileType}表 ${fileName} 文件 ${fieldName} 最大长度 ${maxStr} 越界,string 类型长度必须在 1..65535 范围内`);
156
+ if (fieldMax !== 'null') {
157
+ Logger.error(`${fileType}表 ${fileName} 文件 ${colKey} text 类型最大长度必须为 null,当前为 "${fieldMax}"`);
136
158
  fileValid = false;
137
- continue;
138
159
  }
139
- } else if (type === 'array') {
140
- // array:最大长度必为数字(用于JSON字符串长度限制)
141
- if (maxStr === 'null' || !validateMinMax(maxStr)) {
142
- Logger.error(`${fileType}表 ${fileName} 文件 ${fieldName} 最大长度 "${maxStr}" 格式错误,array 类型必须为具体数字`);
160
+ if (fieldDefault !== 'null') {
161
+ Logger.error(`${fileType}表 ${fileName} 文件 ${colKey} 为 text 类型,默认值必须为 null,当前为 "${fieldDefault}"`);
143
162
  fileValid = false;
144
- continue;
145
163
  }
146
- } else {
147
- // number 等其他:允许 null 或数字
148
- if (!validateMinMax(maxStr)) {
149
- 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}"`);
150
167
  fileValid = false;
151
- continue;
152
168
  }
153
- }
154
-
155
- // 第5个值:默认值校验
156
- if (type === 'text') {
157
- // text 不允许默认值
158
- if (defaultValue !== 'null') {
159
- 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 范围内`);
160
172
  fileValid = false;
161
- continue;
162
173
  }
163
- } else {
164
- if (!validateDefaultValue(defaultValue)) {
165
- 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}"`);
166
177
  fileValid = false;
167
- continue;
168
178
  }
169
179
  }
170
-
171
- // 第6个值:是否创建索引必须为0或1
172
- if (!validateIndex(isIndexStr)) {
173
- Logger.error(`${fileType}表 ${fileName} 文件 ${fieldName} 索引标识 "${isIndexStr}" 格式错误,必须为0或1`);
174
- fileValid = false;
175
- continue;
176
- }
177
-
178
- // 第7个值:必须为null或正则表达式
179
- if (!validateRegex(regexConstraint)) {
180
- Logger.error(`${fileType}表 ${fileName} 文件 ${fieldName} 正则约束 "${regexConstraint}" 格式错误,必须为null或有效的正则表达式`);
181
- fileValid = false;
182
- continue;
183
- }
184
180
  } catch (error) {
185
- Logger.error(`${fileType}表 ${fileName} 文件 ${fieldName} 验证规则解析失败: ${error.message}`);
181
+ Logger.error(`${fileType}表 ${fileName} 文件 ${colKey} 验证规则解析失败: ${error.message}`);
186
182
  fileValid = false;
187
- continue;
188
183
  }
189
184
  }
190
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.1",
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) {