befly 3.8.19 → 3.8.20

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.
Files changed (49) hide show
  1. package/README.md +7 -6
  2. package/bunfig.toml +1 -1
  3. package/lib/database.ts +28 -25
  4. package/lib/dbHelper.ts +3 -3
  5. package/lib/jwt.ts +90 -99
  6. package/lib/logger.ts +44 -23
  7. package/lib/redisHelper.ts +19 -22
  8. package/loader/loadApis.ts +69 -114
  9. package/loader/loadHooks.ts +65 -0
  10. package/loader/loadPlugins.ts +50 -219
  11. package/main.ts +106 -133
  12. package/package.json +15 -7
  13. package/paths.ts +20 -0
  14. package/plugins/cache.ts +1 -3
  15. package/plugins/db.ts +8 -11
  16. package/plugins/logger.ts +5 -3
  17. package/plugins/redis.ts +10 -14
  18. package/router/api.ts +60 -106
  19. package/router/root.ts +15 -12
  20. package/router/static.ts +54 -58
  21. package/sync/syncAll.ts +58 -0
  22. package/sync/syncApi.ts +264 -0
  23. package/sync/syncDb/apply.ts +194 -0
  24. package/sync/syncDb/constants.ts +76 -0
  25. package/sync/syncDb/ddl.ts +194 -0
  26. package/sync/syncDb/helpers.ts +200 -0
  27. package/sync/syncDb/index.ts +164 -0
  28. package/sync/syncDb/schema.ts +201 -0
  29. package/sync/syncDb/sqlite.ts +50 -0
  30. package/sync/syncDb/table.ts +321 -0
  31. package/sync/syncDb/tableCreate.ts +146 -0
  32. package/sync/syncDb/version.ts +72 -0
  33. package/sync/syncDb.ts +19 -0
  34. package/sync/syncDev.ts +206 -0
  35. package/sync/syncMenu.ts +331 -0
  36. package/tsconfig.json +2 -4
  37. package/types/api.d.ts +6 -0
  38. package/types/befly.d.ts +152 -28
  39. package/types/context.d.ts +29 -3
  40. package/types/hook.d.ts +35 -0
  41. package/types/index.ts +14 -1
  42. package/types/plugin.d.ts +6 -7
  43. package/types/sync.d.ts +403 -0
  44. package/check.ts +0 -378
  45. package/env.ts +0 -106
  46. package/lib/middleware.ts +0 -275
  47. package/types/env.ts +0 -65
  48. package/types/util.d.ts +0 -45
  49. package/util.ts +0 -257
package/check.ts DELETED
@@ -1,378 +0,0 @@
1
- /**
2
- * 表规则检查器 - TypeScript 版本
3
- * 验证表定义文件的格式和规则
4
- */
5
-
6
- import { basename, relative } from 'pathe';
7
- import { join } from 'node:path';
8
- import { existsSync, mkdirSync } from 'node:fs';
9
- import { isPlainObject } from 'es-toolkit/compat';
10
- import { Logger } from './lib/logger.js';
11
- import { projectTableDir, projectApiDir, projectDir } from './paths.js';
12
- import { scanAddons, getAddonDir, addonDirExists } from './util.js';
13
- import type { FieldDefinition } from './types/common.d.ts';
14
-
15
- /**
16
- * 表文件信息接口
17
- */
18
- interface TableFileInfo {
19
- /** 表文件路径 */
20
- file: string;
21
- /** 文件类型:project(项目)或 addon(组件) */
22
- type: 'project' | 'addon';
23
- /** 如果是 addon 类型,记录 addon 名称 */
24
- addonName?: string;
25
- }
26
-
27
- /**
28
- * 保留字段列表
29
- */
30
- const RESERVED_FIELDS = ['id', 'created_at', 'updated_at', 'deleted_at', 'state'] as const;
31
-
32
- /**
33
- * 允许的字段类型
34
- */
35
- const FIELD_TYPES = ['string', 'number', 'text', 'array_string', 'array_text'] as const;
36
-
37
- /**
38
- * 小驼峰命名正则
39
- * 可选:以下划线开头(用于特殊文件,如通用字段定义)
40
- * 必须以小写字母开头,后续可包含小写/数字,或多个 [大写+小写/数字] 片段
41
- * 示例:userTable、testCustomers、common
42
- */
43
- const LOWER_CAMEL_CASE_REGEX = /^_?[a-z][a-z0-9]*(?:[A-Z][a-z0-9]*)*$/;
44
-
45
- /**
46
- * 字段名称正则
47
- * 必须为中文、数字、字母、下划线、短横线、空格
48
- */
49
- const FIELD_NAME_REGEX = /^[\u4e00-\u9fa5a-zA-Z0-9 _-]+$/;
50
-
51
- /**
52
- * VARCHAR 最大长度限制
53
- */
54
- const MAX_VARCHAR_LENGTH = 65535;
55
-
56
- /**
57
- * 检查表定义文件
58
- * @throws 当检查失败时抛出异常
59
- */
60
- export const checkTable = async function (): Promise<boolean> {
61
- try {
62
- const tablesGlob = new Bun.Glob('*.json');
63
-
64
- // 收集所有表文件
65
- const allTableFiles: TableFileInfo[] = [];
66
- let hasError = false;
67
-
68
- // 收集项目表字段定义文件(如果目录存在)
69
- if (existsSync(projectTableDir)) {
70
- for await (const file of tablesGlob.scan({
71
- cwd: projectTableDir,
72
- absolute: true,
73
- onlyFiles: true
74
- })) {
75
- allTableFiles.push({
76
- file: file,
77
- typeCode: 'project',
78
- typeName: '项目'
79
- });
80
- }
81
- }
82
-
83
- // 收集 addon 表字段定义文件
84
- const addons = scanAddons();
85
- for (const addonName of addons) {
86
- const addonTablesDir = getAddonDir(addonName, 'tables');
87
-
88
- // 检查 addon tables 目录是否存在
89
- if (!existsSync(addonTablesDir)) {
90
- continue;
91
- }
92
-
93
- for await (const file of tablesGlob.scan({
94
- cwd: addonTablesDir,
95
- absolute: true,
96
- onlyFiles: true
97
- })) {
98
- allTableFiles.push({
99
- file: file,
100
- typeCode: 'addon',
101
- typeName: `组件${addonName}`,
102
- addonName: addonName
103
- });
104
- }
105
- }
106
-
107
- // 合并进行验证逻辑
108
- for (const item of allTableFiles) {
109
- const fileName = basename(item.file);
110
- const fileBaseName = basename(item.file, '.json');
111
-
112
- try {
113
- // 1) 文件名小驼峰校验
114
- if (!LOWER_CAMEL_CASE_REGEX.test(fileBaseName)) {
115
- Logger.warn(`${item.typeName}表 ${fileName} 文件名必须使用小驼峰命名(例如 testCustomers.json)`);
116
- hasError = true;
117
- continue;
118
- }
119
-
120
- // 动态导入 JSON 文件
121
- const tableModule = await import(item.file, { with: { type: 'json' } });
122
- const table = tableModule.default;
123
-
124
- // 检查 table 中的每个验证规则
125
- for (const [colKey, fieldDef] of Object.entries(table)) {
126
- if (typeof fieldDef !== 'object' || fieldDef === null || Array.isArray(fieldDef)) {
127
- Logger.warn(`${item.typeName}表 ${fileName} 文件 ${colKey} 规则必须为对象`);
128
- hasError = true;
129
- continue;
130
- }
131
-
132
- // 检查是否使用了保留字段
133
- if (RESERVED_FIELDS.includes(colKey as any)) {
134
- Logger.warn(`${item.typeName}表 ${fileName} 文件包含保留字段 ${colKey},` + `不能在表定义中使用以下字段: ${RESERVED_FIELDS.join(', ')}`);
135
- hasError = true;
136
- }
137
-
138
- // 直接使用字段对象
139
- const field = fieldDef as FieldDefinition;
140
-
141
- // 检查必填字段:name, type
142
- if (!field.name || typeof field.name !== 'string') {
143
- Logger.warn(`${item.typeName}表 ${fileName} 文件 ${colKey} 缺少必填字段 name 或类型错误`);
144
- hasError = true;
145
- continue;
146
- }
147
- if (!field.type || typeof field.type !== 'string') {
148
- Logger.warn(`${item.typeName}表 ${fileName} 文件 ${colKey} 缺少必填字段 type 或类型错误`);
149
- hasError = true;
150
- continue;
151
- }
152
-
153
- // 检查可选字段的类型
154
- if (field.min !== undefined && !(field.min === null || typeof field.min === 'number')) {
155
- Logger.warn(`${item.typeName}表 ${fileName} 文件 ${colKey} 字段 min 类型错误,必须为 null 或数字`);
156
- hasError = true;
157
- }
158
- if (field.max !== undefined && !(field.max === null || typeof field.max === 'number')) {
159
- Logger.warn(`${item.typeName}表 ${fileName} 文件 ${colKey} 字段 max 类型错误,必须为 null 或数字`);
160
- hasError = true;
161
- }
162
- if (field.detail !== undefined && typeof field.detail !== 'string') {
163
- Logger.warn(`${item.typeName}表 ${fileName} 文件 ${colKey} 字段 detail 类型错误,必须为字符串`);
164
- hasError = true;
165
- }
166
- if (field.index !== undefined && typeof field.index !== 'boolean') {
167
- Logger.warn(`${item.typeName}表 ${fileName} 文件 ${colKey} 字段 index 类型错误,必须为布尔值`);
168
- hasError = true;
169
- }
170
- if (field.unique !== undefined && typeof field.unique !== 'boolean') {
171
- Logger.warn(`${item.typeName}表 ${fileName} 文件 ${colKey} 字段 unique 类型错误,必须为布尔值`);
172
- hasError = true;
173
- }
174
- if (field.nullable !== undefined && typeof field.nullable !== 'boolean') {
175
- Logger.warn(`${item.typeName}表 ${fileName} 文件 ${colKey} 字段 nullable 类型错误,必须为布尔值`);
176
- hasError = true;
177
- }
178
- if (field.unsigned !== undefined && typeof field.unsigned !== 'boolean') {
179
- Logger.warn(`${item.typeName}表 ${fileName} 文件 ${colKey} 字段 unsigned 类型错误,必须为布尔值`);
180
- hasError = true;
181
- }
182
- if (field.regexp !== undefined && field.regexp !== null && typeof field.regexp !== 'string') {
183
- Logger.warn(`${item.typeName}表 ${fileName} 文件 ${colKey} 字段 regexp 类型错误,必须为 null 或字符串`);
184
- hasError = true;
185
- }
186
-
187
- const { name: fieldName, type: fieldType, min: fieldMin, max: fieldMax, default: fieldDefault } = field;
188
-
189
- // 字段名称必须为中文、数字、字母、下划线、短横线、空格
190
- if (!FIELD_NAME_REGEX.test(fieldName)) {
191
- Logger.warn(`${item.typeName}表 ${fileName} 文件 ${colKey} 字段名称 "${fieldName}" 格式错误,` + `必须为中文、数字、字母、下划线、短横线、空格`);
192
- hasError = true;
193
- }
194
-
195
- // 字段类型必须为string,number,text,array_string,array_text之一
196
- if (!FIELD_TYPES.includes(fieldType as any)) {
197
- Logger.warn(`${item.typeName}表 ${fileName} 文件 ${colKey} 字段类型 "${fieldType}" 格式错误,` + `必须为${FIELD_TYPES.join('、')}之一`);
198
- hasError = true;
199
- }
200
-
201
- // 约束:当最小值与最大值均为数字时,要求最小值 <= 最大值
202
- if (fieldMin !== undefined && fieldMax !== undefined && fieldMin !== null && fieldMax !== null) {
203
- if (fieldMin > fieldMax) {
204
- Logger.warn(`${item.typeName}表 ${fileName} 文件 ${colKey} 最小值 "${fieldMin}" 不能大于最大值 "${fieldMax}"`);
205
- hasError = true;
206
- }
207
- }
208
-
209
- // 类型联动校验 + 默认值规则
210
- if (fieldType === 'text') {
211
- // text:min/max 应该为 null,默认值必须为 null
212
- if (fieldMin !== undefined && fieldMin !== null) {
213
- Logger.warn(`${item.typeName}表 ${fileName} 文件 ${colKey} 的 text 类型最小值应为 null,当前为 "${fieldMin}"`);
214
- hasError = true;
215
- }
216
- if (fieldMax !== undefined && fieldMax !== null) {
217
- Logger.warn(`${item.typeName}表 ${fileName} 文件 ${colKey} 的 text 类型最大长度应为 null,当前为 "${fieldMax}"`);
218
- hasError = true;
219
- }
220
- if (fieldDefault !== undefined && fieldDefault !== null) {
221
- Logger.warn(`${item.typeName}表 ${fileName} 文件 ${colKey} 为 text 类型,默认值必须为 null,当前为 "${fieldDefault}"`);
222
- hasError = true;
223
- }
224
- } else if (fieldType === 'string' || fieldType === 'array_string') {
225
- if (fieldMax !== undefined && (fieldMax === null || typeof fieldMax !== 'number')) {
226
- Logger.warn(`${item.typeName}表 ${fileName} 文件 ${colKey} 为 ${fieldType} 类型,` + `最大长度必须为数字,当前为 "${fieldMax}"`);
227
- hasError = true;
228
- } else if (fieldMax !== undefined && fieldMax > MAX_VARCHAR_LENGTH) {
229
- Logger.warn(`${item.typeName}表 ${fileName} 文件 ${colKey} 最大长度 ${fieldMax} 越界,` + `${fieldType} 类型长度必须在 1..${MAX_VARCHAR_LENGTH} 范围内`);
230
- hasError = true;
231
- }
232
- } else if (fieldType === 'number') {
233
- // number 类型:default 如果存在,必须为 null 或 number
234
- if (fieldDefault !== undefined && fieldDefault !== null && typeof fieldDefault !== 'number') {
235
- Logger.warn(`${item.typeName}表 ${fileName} 文件 ${colKey} 为 number 类型,` + `默认值必须为数字或 null,当前为 "${fieldDefault}"`);
236
- hasError = true;
237
- }
238
- }
239
- }
240
- } catch (error: any) {
241
- Logger.error(`${item.typeName}表 ${fileName} 解析失败`, error);
242
- hasError = true;
243
- }
244
- }
245
-
246
- return !hasError;
247
- } catch (error: any) {
248
- Logger.error('数据表定义检查过程中出错', error);
249
- return false;
250
- }
251
- };
252
-
253
- /**
254
- * 检查所有 API 定义
255
- */
256
- export const checkApi = async function (): Promise<boolean> {
257
- try {
258
- const apiGlob = new Bun.Glob('**/*.{ts,js}');
259
-
260
- // 收集所有 API 文件
261
- const allApiFiles: Array<{ file: string; displayName: string }> = [];
262
-
263
- // 收集项目 API 文件
264
- if (existsSync(projectApiDir)) {
265
- for await (const file of apiGlob.scan({
266
- cwd: projectApiDir,
267
- onlyFiles: true,
268
- absolute: true
269
- })) {
270
- if (file.endsWith('.d.ts')) {
271
- continue;
272
- }
273
- allApiFiles.push({
274
- file: file,
275
- displayName: '用户'
276
- });
277
- }
278
- }
279
-
280
- // 收集组件 API 文件
281
- const addons = scanAddons();
282
- for (const addon of addons) {
283
- if (!addonDirExists(addon, 'apis')) continue;
284
- const addonApiDir = getAddonDir(addon, 'apis');
285
-
286
- for await (const file of apiGlob.scan({
287
- cwd: addonApiDir,
288
- onlyFiles: true,
289
- absolute: true
290
- })) {
291
- if (file.endsWith('.d.ts')) {
292
- continue;
293
- }
294
- allApiFiles.push({
295
- file: file,
296
- displayName: `组件${addon}`
297
- });
298
- }
299
- }
300
-
301
- // 合并进行验证逻辑
302
- for (const item of allApiFiles) {
303
- const fileName = basename(item.file).replace(/\.(ts|js)$/, '');
304
- const apiPath = relative(item.displayName === '用户' ? projectApiDir : getAddonDir(item.displayName.replace('组件', ''), 'apis'), item.file).replace(/\.(ts|js)$/, '');
305
-
306
- // 跳过以下划线开头的文件
307
- if (apiPath.indexOf('_') !== -1) continue;
308
-
309
- try {
310
- // Windows 下路径需要转换为正斜杠格式
311
- const filePath = item.file.replace(/\\/g, '/');
312
- const apiImport = await import(filePath);
313
- const api = apiImport.default;
314
-
315
- // 验证必填属性:name 和 handler
316
- if (typeof api.name !== 'string' || api.name.trim() === '') {
317
- Logger.warn(`[${item.displayName}] 接口 ${apiPath} 的 name 属性必须是非空字符串`);
318
- continue;
319
- }
320
- if (typeof api.handler !== 'function') {
321
- Logger.warn(`[${item.displayName}] 接口 ${apiPath} 的 handler 属性必须是函数`);
322
- continue;
323
- }
324
-
325
- // 验证可选属性的类型(如果提供了)
326
- if (api.method && !['GET', 'POST', 'PUT', 'DELETE', 'PATCH', 'HEAD', 'OPTIONS'].includes(api.method.toUpperCase())) {
327
- Logger.warn(`[${item.displayName}] 接口 ${apiPath} 的 method 属性必须是有效的 HTTP 方法`);
328
- }
329
- if (api.auth !== undefined && typeof api.auth !== 'boolean') {
330
- Logger.warn(`[${item.displayName}] 接口 ${apiPath} 的 auth 属性必须是布尔值 (true=需登录, false=公开)`);
331
- }
332
- if (api.fields && !isPlainObject(api.fields)) {
333
- Logger.warn(`[${item.displayName}] 接口 ${apiPath} 的 fields 属性必须是对象`);
334
- }
335
- if (api.required && !Array.isArray(api.required)) {
336
- Logger.warn(`[${item.displayName}] 接口 ${apiPath} 的 required 属性必须是数组`);
337
- }
338
- if (api.required && api.required.some((item: any) => typeof item !== 'string')) {
339
- Logger.warn(`[${item.displayName}] 接口 ${apiPath} 的 required 属性必须是字符串数组`);
340
- }
341
- } catch (error: any) {
342
- Logger.error(`[${item.displayName}] 接口 ${apiPath} 解析失败`, error);
343
- }
344
- }
345
-
346
- return true;
347
- } catch (error: any) {
348
- Logger.error('API 定义检查过程中出错', error);
349
- return false;
350
- }
351
- };
352
-
353
- /**
354
- * 检查项目结构
355
- */
356
- export const checkApp = async function (): Promise<boolean> {
357
- try {
358
- // 检查项目 apis 目录下是否存在名为 addon 的目录
359
- if (existsSync(projectApiDir)) {
360
- const addonDir = join(projectApiDir, 'addon');
361
- if (existsSync(addonDir)) {
362
- Logger.error('项目 apis 目录下不能存在名为 addon 的目录,addon 是保留名称,用于组件接口路由');
363
- return false;
364
- }
365
- }
366
-
367
- // 检查并创建 logs 目录
368
- const logsDir = join(projectDir, 'logs');
369
- if (!existsSync(logsDir)) {
370
- mkdirSync(logsDir, { recursive: true });
371
- }
372
-
373
- return true;
374
- } catch (error: any) {
375
- Logger.error('项目结构检查过程中出错', error);
376
- return false;
377
- }
378
- };
package/env.ts DELETED
@@ -1,106 +0,0 @@
1
- /**
2
- * 环境变量配置
3
- * 根据 NODE_ENV 自动切换开发/生产环境配置
4
- * 项目可通过创建 env.ts 或 env.js 文件覆盖这些配置
5
- */
6
-
7
- import { existsSync } from 'node:fs';
8
- import type { EnvConfig } from './types/env.js';
9
-
10
- const isProd = process.env.NODE_ENV === 'production';
11
-
12
- /**
13
- * 核心默认配置
14
- * 使用三元运算符根据环境切换配置
15
- */
16
- const coreEnv: EnvConfig = {
17
- // ========== 项目配置 ==========
18
- NODE_ENV: process.env.NODE_ENV || 'development',
19
- APP_NAME: isProd ? '野蜂飞舞正式环境' : '野蜂飞舞开发环境',
20
- APP_PORT: 3000,
21
- APP_HOST: '127.0.0.1',
22
- DEV_EMAIL: '',
23
- DEV_PASSWORD: '123456',
24
- BODY_LIMIT: 10 * 1024 * 1024, // 10MB
25
- DATABASE_ENABLE: 0,
26
- // ========== 时区配置 ==========
27
- TZ: 'Asia/Shanghai',
28
-
29
- // ========== 日志配置 ==========
30
- LOG_DEBUG: 1,
31
- LOG_EXCLUDE_FIELDS: 'password,token,secret',
32
- LOG_DIR: './logs',
33
- LOG_TO_CONSOLE: 1,
34
- LOG_MAX_SIZE: 10 * 1024 * 1024, // 10MB
35
-
36
- // ========== 数据库配置 ==========
37
- DB_TYPE: 'mysql',
38
- DB_HOST: '127.0.0.1',
39
- DB_PORT: 3306,
40
- DB_USER: 'root',
41
- DB_PASS: 'root',
42
- DB_NAME: 'befly_demo',
43
- DB_POOL_MAX: 10,
44
-
45
- // ========== Redis 配置 ==========
46
- REDIS_HOST: '127.0.0.1',
47
- REDIS_PORT: 6379,
48
- REDIS_USERNAME: '',
49
- REDIS_PASSWORD: '',
50
- REDIS_DB: 0,
51
- REDIS_KEY_PREFIX: 'befly_demo',
52
-
53
- // ========== JWT 配置 ==========
54
- JWT_SECRET: 'befly-secret',
55
- JWT_EXPIRES_IN: '7d',
56
- JWT_ALGORITHM: 'HS256',
57
-
58
- // ========== CORS 配置 ==========
59
- CORS_ALLOWED_ORIGIN: '*',
60
- CORS_ALLOWED_METHODS: 'GET, POST, PUT, DELETE, OPTIONS',
61
- CORS_ALLOWED_HEADERS: 'Content-Type, Authorization, authorization, token',
62
- CORS_EXPOSE_HEADERS: 'Content-Range, X-Content-Range, Authorization, authorization, token',
63
- CORS_MAX_AGE: 86400,
64
- CORS_ALLOW_CREDENTIALS: 'true'
65
- };
66
-
67
- /**
68
- * 尝试加载项目级别的 env.ts 配置
69
- */
70
- async function loadProjectEnv(): Promise<Partial<EnvConfig>> {
71
- try {
72
- // 尝试从项目根目录加载 env.ts 或 env.js
73
- const projectEnvPathTs = process.cwd() + '/env.ts';
74
- const projectEnvPathJs = process.cwd() + '/env.js';
75
-
76
- let projectEnvPath = '';
77
- if (existsSync(projectEnvPathTs)) {
78
- projectEnvPath = projectEnvPathTs;
79
- } else if (existsSync(projectEnvPathJs)) {
80
- projectEnvPath = projectEnvPathJs;
81
- }
82
-
83
- // 检查文件是否存在
84
- if (!projectEnvPath) {
85
- return {};
86
- }
87
-
88
- // 动态导入
89
- const module = await import(projectEnvPath);
90
- return module.Env || module.default || {};
91
- } catch (error) {
92
- // 项目没有自定义配置,使用核心默认配置
93
- return {};
94
- }
95
- }
96
-
97
- // 使用 top-level await 加载项目配置
98
- const projectEnv = await loadProjectEnv();
99
-
100
- /**
101
- * 合并配置:项目配置覆盖核心配置
102
- */
103
- export const Env: EnvConfig = {
104
- ...coreEnv,
105
- ...projectEnv
106
- };