befly 3.0.0 → 3.1.1
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/checks/conflict.ts +35 -114
- package/checks/table.ts +31 -63
- package/config/env.ts +3 -3
- package/config/fields.ts +55 -0
- package/config/regexAliases.ts +51 -0
- package/config/reserved.ts +1 -1
- package/main.ts +17 -71
- package/package.json +7 -28
- package/plugins/db.ts +11 -10
- package/plugins/redis.ts +5 -9
- package/scripts/syncDb/apply.ts +3 -3
- package/scripts/syncDb/constants.ts +2 -1
- package/scripts/syncDb/ddl.ts +15 -8
- package/scripts/syncDb/helpers.ts +3 -2
- package/scripts/syncDb/index.ts +23 -35
- package/scripts/syncDb/state.ts +8 -6
- package/scripts/syncDb/table.ts +32 -22
- package/scripts/syncDb/tableCreate.ts +9 -3
- package/scripts/syncDb/tests/constants.test.ts +2 -1
- package/scripts/syncDb.ts +10 -9
- package/types/addon.d.ts +53 -0
- package/types/api.d.ts +17 -14
- package/types/befly.d.ts +2 -6
- package/types/context.d.ts +7 -0
- package/types/database.d.ts +9 -14
- package/types/index.d.ts +442 -8
- package/types/index.ts +35 -56
- package/types/redis.d.ts +2 -0
- package/types/validator.d.ts +0 -2
- package/types/validator.ts +43 -0
- package/utils/colors.ts +117 -37
- package/utils/database.ts +348 -0
- package/utils/dbHelper.ts +687 -116
- package/utils/helper.ts +812 -0
- package/utils/index.ts +10 -23
- package/utils/logger.ts +78 -171
- package/utils/redisHelper.ts +135 -152
- package/{types/context.ts → utils/requestContext.ts} +3 -3
- package/utils/sqlBuilder.ts +142 -165
- package/utils/validate.ts +51 -9
- package/apis/health/info.ts +0 -64
- package/apis/tool/tokenCheck.ts +0 -51
- package/bin/befly.ts +0 -202
- package/bunfig.toml +0 -3
- package/plugins/tool.ts +0 -34
- package/scripts/syncDev.ts +0 -112
- package/system.ts +0 -149
- package/tables/_common.json +0 -21
- package/tables/admin.json +0 -10
- package/utils/addonHelper.ts +0 -60
- package/utils/api.ts +0 -23
- package/utils/datetime.ts +0 -51
- package/utils/errorHandler.ts +0 -68
- package/utils/objectHelper.ts +0 -68
- package/utils/pluginHelper.ts +0 -62
- package/utils/response.ts +0 -38
- package/utils/sqlHelper.ts +0 -447
- package/utils/tableHelper.ts +0 -167
- package/utils/tool.ts +0 -230
- package/utils/typeHelper.ts +0 -101
package/checks/conflict.ts
CHANGED
|
@@ -5,9 +5,9 @@
|
|
|
5
5
|
|
|
6
6
|
import path from 'node:path';
|
|
7
7
|
import { Logger } from '../utils/logger.js';
|
|
8
|
-
import {
|
|
9
|
-
import {
|
|
10
|
-
import { isReservedTableName,
|
|
8
|
+
import { paths } from '../paths.js';
|
|
9
|
+
import { scanAddons, getAddonDir, addonDirExists } from '../utils/helper.js';
|
|
10
|
+
import { isReservedTableName, isReservedPluginName, isReservedAddonName, getReservedTablePrefixes, getReservedPlugins, getReservedAddonNames } from '../config/reserved.js';
|
|
11
11
|
|
|
12
12
|
/**
|
|
13
13
|
* 资源注册表
|
|
@@ -26,73 +26,6 @@ interface ConflictResult {
|
|
|
26
26
|
conflicts: string[];
|
|
27
27
|
}
|
|
28
28
|
|
|
29
|
-
/**
|
|
30
|
-
* 收集核心表定义
|
|
31
|
-
*/
|
|
32
|
-
async function collectCoreTables(registry: ResourceRegistry): Promise<void> {
|
|
33
|
-
try {
|
|
34
|
-
const glob = new Bun.Glob('*.json');
|
|
35
|
-
for await (const file of glob.scan({
|
|
36
|
-
cwd: __dirtables,
|
|
37
|
-
onlyFiles: true,
|
|
38
|
-
absolute: true
|
|
39
|
-
})) {
|
|
40
|
-
const fileName = path.basename(file, '.json');
|
|
41
|
-
if (fileName.startsWith('_')) continue;
|
|
42
|
-
|
|
43
|
-
try {
|
|
44
|
-
const tableDefine = await Bun.file(file).json();
|
|
45
|
-
const tableName = tableDefine.tableName || `sys_${fileName}`;
|
|
46
|
-
|
|
47
|
-
if (registry.tables.has(tableName)) {
|
|
48
|
-
Logger.error(`核心表 "${tableName}" 重复定义`);
|
|
49
|
-
} else {
|
|
50
|
-
registry.tables.set(tableName, 'core');
|
|
51
|
-
}
|
|
52
|
-
} catch (error: any) {
|
|
53
|
-
// 表定义解析错误会在 table.ts 中处理,这里跳过
|
|
54
|
-
}
|
|
55
|
-
}
|
|
56
|
-
} catch (error: any) {
|
|
57
|
-
Logger.error('收集核心表定义时出错:', error);
|
|
58
|
-
}
|
|
59
|
-
}
|
|
60
|
-
|
|
61
|
-
/**
|
|
62
|
-
* 收集核心 API 路由
|
|
63
|
-
*/
|
|
64
|
-
async function collectCoreApis(registry: ResourceRegistry): Promise<void> {
|
|
65
|
-
try {
|
|
66
|
-
const glob = new Bun.Glob('**/*.ts');
|
|
67
|
-
for await (const file of glob.scan({
|
|
68
|
-
cwd: __dirapis,
|
|
69
|
-
onlyFiles: true,
|
|
70
|
-
absolute: true
|
|
71
|
-
})) {
|
|
72
|
-
const apiPath = path
|
|
73
|
-
.relative(__dirapis, file)
|
|
74
|
-
.replace(/\.(js|ts)$/, '')
|
|
75
|
-
.replace(/\\/g, '/');
|
|
76
|
-
if (apiPath.indexOf('_') !== -1) continue;
|
|
77
|
-
|
|
78
|
-
try {
|
|
79
|
-
const api = (await import(file)).default;
|
|
80
|
-
const route = `${(api.method || 'POST').toUpperCase()}/api/${apiPath}`;
|
|
81
|
-
|
|
82
|
-
if (registry.routes.has(route)) {
|
|
83
|
-
Logger.error(`核心路由 "${route}" 重复定义`);
|
|
84
|
-
} else {
|
|
85
|
-
registry.routes.set(route, 'core');
|
|
86
|
-
}
|
|
87
|
-
} catch (error: any) {
|
|
88
|
-
// API 加载错误会在 loader.ts 中处理,这里跳过
|
|
89
|
-
}
|
|
90
|
-
}
|
|
91
|
-
} catch (error: any) {
|
|
92
|
-
Logger.error('收集核心 API 路由时出错:', error);
|
|
93
|
-
}
|
|
94
|
-
}
|
|
95
|
-
|
|
96
29
|
/**
|
|
97
30
|
* 收集核心插件
|
|
98
31
|
*/
|
|
@@ -100,15 +33,15 @@ async function collectCorePlugins(registry: ResourceRegistry): Promise<void> {
|
|
|
100
33
|
try {
|
|
101
34
|
const glob = new Bun.Glob('*.ts');
|
|
102
35
|
for await (const file of glob.scan({
|
|
103
|
-
cwd:
|
|
36
|
+
cwd: paths.rootPluginDir,
|
|
104
37
|
onlyFiles: true,
|
|
105
38
|
absolute: true
|
|
106
39
|
})) {
|
|
107
|
-
const pluginName = path.basename(file).replace(/\.
|
|
40
|
+
const pluginName = path.basename(file).replace(/\.ts$/, '');
|
|
108
41
|
if (pluginName.startsWith('_')) continue;
|
|
109
42
|
|
|
110
43
|
if (registry.plugins.has(pluginName)) {
|
|
111
|
-
Logger.
|
|
44
|
+
Logger.warn(`核心插件 "${pluginName}" 重复定义`);
|
|
112
45
|
} else {
|
|
113
46
|
registry.plugins.set(pluginName, 'core');
|
|
114
47
|
}
|
|
@@ -126,12 +59,12 @@ async function collectAddonResources(addonName: string, registry: ResourceRegist
|
|
|
126
59
|
|
|
127
60
|
// 检查 addon 名称是否使用保留名称
|
|
128
61
|
if (isReservedAddonName(addonName)) {
|
|
129
|
-
conflicts.push(
|
|
62
|
+
conflicts.push(`组件名称 "${addonName}" 使用了保留名称,保留名称包括: ${getReservedAddonNames().join(', ')}`);
|
|
130
63
|
return conflicts;
|
|
131
64
|
}
|
|
132
65
|
|
|
133
66
|
// 收集 addon 表定义
|
|
134
|
-
if (
|
|
67
|
+
if (addonDirExists(addonName, 'tables')) {
|
|
135
68
|
const addonTablesDir = getAddonDir(addonName, 'tables');
|
|
136
69
|
const glob = new Bun.Glob('*.json');
|
|
137
70
|
|
|
@@ -149,15 +82,15 @@ async function collectAddonResources(addonName: string, registry: ResourceRegist
|
|
|
149
82
|
|
|
150
83
|
// 检查是否使用保留前缀
|
|
151
84
|
if (isReservedTableName(tableName)) {
|
|
152
|
-
conflicts.push(
|
|
85
|
+
conflicts.push(`组件 ${addonName} 表 "${tableName}" 使用了保留前缀,保留前缀包括: ${getReservedTablePrefixes().join(', ')}`);
|
|
153
86
|
continue;
|
|
154
87
|
}
|
|
155
88
|
|
|
156
89
|
// 检查是否与已有表冲突
|
|
157
90
|
if (registry.tables.has(tableName)) {
|
|
158
|
-
conflicts.push(
|
|
91
|
+
conflicts.push(`组件 ${addonName} 表 "${tableName}" 与 ${registry.tables.get(tableName)} 冲突`);
|
|
159
92
|
} else {
|
|
160
|
-
registry.tables.set(tableName,
|
|
93
|
+
registry.tables.set(tableName, `组件${addonName}`);
|
|
161
94
|
}
|
|
162
95
|
} catch (error: any) {
|
|
163
96
|
// 表定义解析错误会在 table.ts 中处理,这里跳过
|
|
@@ -166,7 +99,7 @@ async function collectAddonResources(addonName: string, registry: ResourceRegist
|
|
|
166
99
|
}
|
|
167
100
|
|
|
168
101
|
// 收集 addon API 路由
|
|
169
|
-
if (
|
|
102
|
+
if (addonDirExists(addonName, 'apis')) {
|
|
170
103
|
const addonApisDir = getAddonDir(addonName, 'apis');
|
|
171
104
|
const glob = new Bun.Glob('**/*.ts');
|
|
172
105
|
|
|
@@ -175,27 +108,24 @@ async function collectAddonResources(addonName: string, registry: ResourceRegist
|
|
|
175
108
|
onlyFiles: true,
|
|
176
109
|
absolute: true
|
|
177
110
|
})) {
|
|
178
|
-
const apiPath = path
|
|
179
|
-
.relative(addonApisDir, file)
|
|
180
|
-
.replace(/\.(js|ts)$/, '')
|
|
181
|
-
.replace(/\\/g, '/');
|
|
111
|
+
const apiPath = path.relative(addonApisDir, file).replace(/\.ts$/, '').replace(/\\/g, '/');
|
|
182
112
|
if (apiPath.indexOf('_') !== -1) continue;
|
|
183
113
|
|
|
184
114
|
try {
|
|
185
115
|
const api = (await import(file)).default;
|
|
186
116
|
const route = `${(api.method || 'POST').toUpperCase()}/api/${addonName}/${apiPath}`;
|
|
187
117
|
|
|
188
|
-
//
|
|
189
|
-
if (
|
|
190
|
-
conflicts.push(
|
|
118
|
+
// 检查是否使用保留路由前缀 /api
|
|
119
|
+
if (route.includes('/api/api/') || route.includes('/api/api')) {
|
|
120
|
+
conflicts.push(`组件 [${addonName}] 路由 "${route}" 使用了保留路径前缀 "/api"`);
|
|
191
121
|
continue;
|
|
192
122
|
}
|
|
193
123
|
|
|
194
124
|
// 检查是否与已有路由冲突
|
|
195
125
|
if (registry.routes.has(route)) {
|
|
196
|
-
conflicts.push(
|
|
126
|
+
conflicts.push(`组件 [${addonName}] 路由 "${route}" 与 ${registry.routes.get(route)} 冲突`);
|
|
197
127
|
} else {
|
|
198
|
-
registry.routes.set(route,
|
|
128
|
+
registry.routes.set(route, `组件${addonName}`);
|
|
199
129
|
}
|
|
200
130
|
} catch (error: any) {
|
|
201
131
|
// API 加载错误会在 loader.ts 中处理,这里跳过
|
|
@@ -204,7 +134,7 @@ async function collectAddonResources(addonName: string, registry: ResourceRegist
|
|
|
204
134
|
}
|
|
205
135
|
|
|
206
136
|
// 收集 addon 插件
|
|
207
|
-
if (
|
|
137
|
+
if (addonDirExists(addonName, 'plugins')) {
|
|
208
138
|
const addonPluginsDir = getAddonDir(addonName, 'plugins');
|
|
209
139
|
const glob = new Bun.Glob('*.ts');
|
|
210
140
|
|
|
@@ -213,7 +143,7 @@ async function collectAddonResources(addonName: string, registry: ResourceRegist
|
|
|
213
143
|
onlyFiles: true,
|
|
214
144
|
absolute: true
|
|
215
145
|
})) {
|
|
216
|
-
const fileName = path.basename(file).replace(/\.
|
|
146
|
+
const fileName = path.basename(file).replace(/\.ts$/, '');
|
|
217
147
|
if (fileName.startsWith('_')) continue;
|
|
218
148
|
|
|
219
149
|
// Addon 插件使用点号命名空间
|
|
@@ -221,15 +151,15 @@ async function collectAddonResources(addonName: string, registry: ResourceRegist
|
|
|
221
151
|
|
|
222
152
|
// 检查是否使用保留名称
|
|
223
153
|
if (isReservedPluginName(pluginName)) {
|
|
224
|
-
conflicts.push(
|
|
154
|
+
conflicts.push(`组件 ${addonName} 插件 "${pluginName}" 使用了保留名称,保留名称包括: ${getReservedPlugins().join(', ')}`);
|
|
225
155
|
continue;
|
|
226
156
|
}
|
|
227
157
|
|
|
228
158
|
// 检查是否与已有插件冲突
|
|
229
159
|
if (registry.plugins.has(pluginName)) {
|
|
230
|
-
conflicts.push(
|
|
160
|
+
conflicts.push(`组件 [${addonName}] 插件 "${pluginName}" 与 ${registry.plugins.get(pluginName)} 冲突`);
|
|
231
161
|
} else {
|
|
232
|
-
registry.plugins.set(pluginName,
|
|
162
|
+
registry.plugins.set(pluginName, `组件${addonName}`);
|
|
233
163
|
}
|
|
234
164
|
}
|
|
235
165
|
}
|
|
@@ -244,7 +174,7 @@ async function collectUserResources(registry: ResourceRegistry): Promise<string[
|
|
|
244
174
|
const conflicts: string[] = [];
|
|
245
175
|
|
|
246
176
|
// 收集用户表定义
|
|
247
|
-
const userTablesDir =
|
|
177
|
+
const userTablesDir = paths.projectTableDir;
|
|
248
178
|
try {
|
|
249
179
|
const glob = new Bun.Glob('*.json');
|
|
250
180
|
for await (const file of glob.scan({
|
|
@@ -280,7 +210,7 @@ async function collectUserResources(registry: ResourceRegistry): Promise<string[
|
|
|
280
210
|
}
|
|
281
211
|
|
|
282
212
|
// 收集用户 API 路由
|
|
283
|
-
const userApisDir =
|
|
213
|
+
const userApisDir = paths.projectApiDir;
|
|
284
214
|
try {
|
|
285
215
|
const glob = new Bun.Glob('**/*.ts');
|
|
286
216
|
for await (const file of glob.scan({
|
|
@@ -288,19 +218,16 @@ async function collectUserResources(registry: ResourceRegistry): Promise<string[
|
|
|
288
218
|
onlyFiles: true,
|
|
289
219
|
absolute: true
|
|
290
220
|
})) {
|
|
291
|
-
const apiPath = path
|
|
292
|
-
.relative(userApisDir, file)
|
|
293
|
-
.replace(/\.(js|ts)$/, '')
|
|
294
|
-
.replace(/\\/g, '/');
|
|
221
|
+
const apiPath = path.relative(userApisDir, file).replace(/\.ts$/, '').replace(/\\/g, '/');
|
|
295
222
|
if (apiPath.indexOf('_') !== -1) continue;
|
|
296
223
|
|
|
297
224
|
try {
|
|
298
225
|
const api = (await import(file)).default;
|
|
299
226
|
const route = `${(api.method || 'POST').toUpperCase()}/api/${apiPath}`;
|
|
300
227
|
|
|
301
|
-
//
|
|
302
|
-
if (
|
|
303
|
-
conflicts.push(`用户路由 "${route}"
|
|
228
|
+
// 检查是否使用保留路由前缀 /api
|
|
229
|
+
if (apiPath.startsWith('api/') || apiPath === 'api') {
|
|
230
|
+
conflicts.push(`用户路由 "${route}" 使用了保留路径前缀 "/api"`);
|
|
304
231
|
continue;
|
|
305
232
|
}
|
|
306
233
|
|
|
@@ -319,7 +246,7 @@ async function collectUserResources(registry: ResourceRegistry): Promise<string[
|
|
|
319
246
|
}
|
|
320
247
|
|
|
321
248
|
// 收集用户插件
|
|
322
|
-
const userPluginsDir =
|
|
249
|
+
const userPluginsDir = paths.projectPluginDir;
|
|
323
250
|
try {
|
|
324
251
|
const glob = new Bun.Glob('*.ts');
|
|
325
252
|
for await (const file of glob.scan({
|
|
@@ -327,7 +254,7 @@ async function collectUserResources(registry: ResourceRegistry): Promise<string[
|
|
|
327
254
|
onlyFiles: true,
|
|
328
255
|
absolute: true
|
|
329
256
|
})) {
|
|
330
|
-
const pluginName = path.basename(file).replace(/\.
|
|
257
|
+
const pluginName = path.basename(file).replace(/\.ts$/, '');
|
|
331
258
|
if (pluginName.startsWith('_')) continue;
|
|
332
259
|
|
|
333
260
|
// 检查是否使用保留名称
|
|
@@ -364,9 +291,7 @@ export default async function checkConflict(): Promise<boolean> {
|
|
|
364
291
|
|
|
365
292
|
const allConflicts: string[] = [];
|
|
366
293
|
|
|
367
|
-
// 1.
|
|
368
|
-
await collectCoreTables(registry);
|
|
369
|
-
await collectCoreApis(registry);
|
|
294
|
+
// 1. 收集核心插件
|
|
370
295
|
await collectCorePlugins(registry);
|
|
371
296
|
|
|
372
297
|
// 2. 收集 addon 资源
|
|
@@ -382,15 +307,11 @@ export default async function checkConflict(): Promise<boolean> {
|
|
|
382
307
|
|
|
383
308
|
// 4. 报告冲突
|
|
384
309
|
if (allConflicts.length > 0) {
|
|
385
|
-
Logger.
|
|
386
|
-
Logger.error('❌ 检测到资源冲突:');
|
|
387
|
-
Logger.error('');
|
|
310
|
+
Logger.warn('检测到资源冲突:');
|
|
388
311
|
allConflicts.forEach((conflict, index) => {
|
|
389
|
-
Logger.
|
|
312
|
+
Logger.warn(` ${index + 1}. ${conflict}`);
|
|
390
313
|
});
|
|
391
|
-
Logger.
|
|
392
|
-
Logger.error('请解决以上冲突后再启动服务器');
|
|
393
|
-
Logger.error('');
|
|
314
|
+
Logger.warn('请解决以上冲突后再启动服务器');
|
|
394
315
|
return false;
|
|
395
316
|
}
|
|
396
317
|
|
package/checks/table.ts
CHANGED
|
@@ -6,8 +6,8 @@
|
|
|
6
6
|
import path from 'node:path';
|
|
7
7
|
import { Logger } from '../utils/logger.js';
|
|
8
8
|
import { parseRule } from '../utils/index.js';
|
|
9
|
-
import {
|
|
10
|
-
import { scanAddons, getAddonDir } from '../utils/
|
|
9
|
+
import { paths } from '../paths.js';
|
|
10
|
+
import { scanAddons, getAddonDir } from '../utils/helper.js';
|
|
11
11
|
|
|
12
12
|
/**
|
|
13
13
|
* 表文件信息接口
|
|
@@ -15,8 +15,8 @@ import { scanAddons, getAddonDir } from '../utils/addonHelper.js';
|
|
|
15
15
|
interface TableFileInfo {
|
|
16
16
|
/** 表文件路径 */
|
|
17
17
|
file: string;
|
|
18
|
-
/** 文件类型:
|
|
19
|
-
type: '
|
|
18
|
+
/** 文件类型:project(项目)或 addon(组件) */
|
|
19
|
+
type: 'project' | 'addon';
|
|
20
20
|
/** 如果是 addon 类型,记录 addon 名称 */
|
|
21
21
|
addonName?: string;
|
|
22
22
|
}
|
|
@@ -29,13 +29,13 @@ const RESERVED_FIELDS = ['id', 'created_at', 'updated_at', 'deleted_at', 'state'
|
|
|
29
29
|
/**
|
|
30
30
|
* 允许的字段类型
|
|
31
31
|
*/
|
|
32
|
-
const FIELD_TYPES = ['string', 'number', 'text', '
|
|
32
|
+
const FIELD_TYPES = ['string', 'number', 'text', 'array_string', 'array_text'] as const;
|
|
33
33
|
|
|
34
34
|
/**
|
|
35
35
|
* 小驼峰命名正则
|
|
36
|
-
*
|
|
36
|
+
* 可选:以下划线开头(用于特殊文件,如通用字段定义)
|
|
37
37
|
* 必须以小写字母开头,后续可包含小写/数字,或多个 [大写+小写/数字] 片段
|
|
38
|
-
* 示例:userTable、testCustomers、common
|
|
38
|
+
* 示例:userTable、testCustomers、common
|
|
39
39
|
*/
|
|
40
40
|
const LOWER_CAMEL_CASE_REGEX = /^_?[a-z][a-z0-9]*(?:[A-Z][a-z0-9]*)*$/;
|
|
41
41
|
|
|
@@ -66,39 +66,17 @@ export default async function (): Promise<boolean> {
|
|
|
66
66
|
|
|
67
67
|
// 收集所有表文件
|
|
68
68
|
const allTableFiles: TableFileInfo[] = [];
|
|
69
|
-
const coreTableNames = new Set<string>(); // 存储内核表文件名
|
|
70
69
|
|
|
71
|
-
//
|
|
70
|
+
// 收集项目表字段定义文件
|
|
72
71
|
for await (const file of tablesGlob.scan({
|
|
73
|
-
cwd:
|
|
72
|
+
cwd: paths.projectTableDir,
|
|
74
73
|
absolute: true,
|
|
75
74
|
onlyFiles: true
|
|
76
75
|
})) {
|
|
77
|
-
const fileName = path.basename(file, '.json');
|
|
78
|
-
coreTableNames.add(fileName);
|
|
79
|
-
allTableFiles.push({ file, type: 'core' });
|
|
80
|
-
}
|
|
81
|
-
|
|
82
|
-
// 收集项目表字段定义文件,并检查是否与内核表同名
|
|
83
|
-
for await (const file of tablesGlob.scan({
|
|
84
|
-
cwd: getProjectDir('tables'),
|
|
85
|
-
absolute: true,
|
|
86
|
-
onlyFiles: true
|
|
87
|
-
})) {
|
|
88
|
-
const fileName = path.basename(file, '.json');
|
|
89
|
-
|
|
90
|
-
// 检查项目表是否与内核表同名
|
|
91
|
-
if (coreTableNames.has(fileName)) {
|
|
92
|
-
Logger.error(`项目表 ${fileName}.json 与内核表同名,项目表不能与内核表定义文件同名`);
|
|
93
|
-
invalidFiles++;
|
|
94
|
-
totalFiles++;
|
|
95
|
-
continue;
|
|
96
|
-
}
|
|
97
|
-
|
|
98
76
|
allTableFiles.push({ file, type: 'project' });
|
|
99
77
|
}
|
|
100
78
|
|
|
101
|
-
// 收集 addon
|
|
79
|
+
// 收集 addon 表字段定义文件
|
|
102
80
|
const addons = scanAddons();
|
|
103
81
|
for (const addonName of addons) {
|
|
104
82
|
const addonTablesDir = getAddonDir(addonName, 'tables');
|
|
@@ -109,16 +87,6 @@ export default async function (): Promise<boolean> {
|
|
|
109
87
|
absolute: true,
|
|
110
88
|
onlyFiles: true
|
|
111
89
|
})) {
|
|
112
|
-
const fileName = path.basename(file, '.json');
|
|
113
|
-
|
|
114
|
-
// 检查 addon 表是否与内核表同名
|
|
115
|
-
if (coreTableNames.has(fileName)) {
|
|
116
|
-
Logger.error(`组件${addonName}表 ${fileName}.json 与内核表同名,addon 表不能与内核表定义文件同名`);
|
|
117
|
-
invalidFiles++;
|
|
118
|
-
totalFiles++;
|
|
119
|
-
continue;
|
|
120
|
-
}
|
|
121
|
-
|
|
122
90
|
allTableFiles.push({ file, type: 'addon', addonName });
|
|
123
91
|
}
|
|
124
92
|
} catch (error) {
|
|
@@ -131,12 +99,12 @@ export default async function (): Promise<boolean> {
|
|
|
131
99
|
totalFiles++;
|
|
132
100
|
const fileName = path.basename(file);
|
|
133
101
|
const fileBaseName = path.basename(file, '.json');
|
|
134
|
-
const fileType = type === '
|
|
102
|
+
const fileType = type === 'project' ? '项目' : `组件${addonName}`;
|
|
135
103
|
|
|
136
104
|
try {
|
|
137
105
|
// 1) 文件名小驼峰校验
|
|
138
106
|
if (!LOWER_CAMEL_CASE_REGEX.test(fileBaseName)) {
|
|
139
|
-
Logger.
|
|
107
|
+
Logger.warn(`${fileType}表 ${fileName} 文件名必须使用小驼峰命名(例如 testCustomers.json)`);
|
|
140
108
|
// 命名不合规,记录错误并计为无效文件,继续下一个文件
|
|
141
109
|
invalidFiles++;
|
|
142
110
|
continue;
|
|
@@ -150,7 +118,7 @@ export default async function (): Promise<boolean> {
|
|
|
150
118
|
// 检查 table 中的每个验证规则
|
|
151
119
|
for (const [colKey, rule] of Object.entries(table)) {
|
|
152
120
|
if (typeof rule !== 'string') {
|
|
153
|
-
Logger.
|
|
121
|
+
Logger.warn(`${fileType}表 ${fileName} 文件 ${colKey} 规则必须为字符串`);
|
|
154
122
|
fileValid = false;
|
|
155
123
|
continue;
|
|
156
124
|
}
|
|
@@ -162,7 +130,7 @@ export default async function (): Promise<boolean> {
|
|
|
162
130
|
|
|
163
131
|
// 检查是否使用了保留字段
|
|
164
132
|
if (RESERVED_FIELDS.includes(colKey as any)) {
|
|
165
|
-
Logger.
|
|
133
|
+
Logger.warn(`${fileType}表 ${fileName} 文件包含保留字段 ${colKey},` + `不能在表定义中使用以下字段: ${RESERVED_FIELDS.join(', ')}`);
|
|
166
134
|
fileValid = false;
|
|
167
135
|
}
|
|
168
136
|
|
|
@@ -171,7 +139,7 @@ export default async function (): Promise<boolean> {
|
|
|
171
139
|
try {
|
|
172
140
|
parsed = parseRule(rule);
|
|
173
141
|
} catch (error: any) {
|
|
174
|
-
Logger.
|
|
142
|
+
Logger.warn(`${fileType}表 ${fileName} 文件 ${colKey} 字段规则解析失败:${error.message}`);
|
|
175
143
|
fileValid = false;
|
|
176
144
|
continue;
|
|
177
145
|
}
|
|
@@ -180,37 +148,37 @@ export default async function (): Promise<boolean> {
|
|
|
180
148
|
|
|
181
149
|
// 第1个值:名称必须为中文、数字、字母、下划线、短横线、空格
|
|
182
150
|
if (!FIELD_NAME_REGEX.test(fieldName)) {
|
|
183
|
-
Logger.
|
|
151
|
+
Logger.warn(`${fileType}表 ${fileName} 文件 ${colKey} 字段名称 "${fieldName}" 格式错误,` + `必须为中文、数字、字母、下划线、短横线、空格`);
|
|
184
152
|
fileValid = false;
|
|
185
153
|
}
|
|
186
154
|
|
|
187
|
-
// 第2个值:字段类型必须为string,number,text,
|
|
155
|
+
// 第2个值:字段类型必须为string,number,text,array_string,array_text之一
|
|
188
156
|
if (!FIELD_TYPES.includes(fieldType as any)) {
|
|
189
|
-
Logger.
|
|
157
|
+
Logger.warn(`${fileType}表 ${fileName} 文件 ${colKey} 字段类型 "${fieldType}" 格式错误,` + `必须为${FIELD_TYPES.join('、')}之一`);
|
|
190
158
|
fileValid = false;
|
|
191
159
|
}
|
|
192
160
|
|
|
193
161
|
// 第3/4个值:需要是 null 或 数字
|
|
194
162
|
if (!(fieldMin === null || typeof fieldMin === 'number')) {
|
|
195
|
-
Logger.
|
|
163
|
+
Logger.warn(`${fileType}表 ${fileName} 文件 ${colKey} 最小值 "${fieldMin}" 格式错误,必须为null或数字`);
|
|
196
164
|
fileValid = false;
|
|
197
165
|
}
|
|
198
166
|
if (!(fieldMax === null || typeof fieldMax === 'number')) {
|
|
199
|
-
Logger.
|
|
167
|
+
Logger.warn(`${fileType}表 ${fileName} 文件 ${colKey} 最大值 "${fieldMax}" 格式错误,必须为null或数字`);
|
|
200
168
|
fileValid = false;
|
|
201
169
|
}
|
|
202
170
|
|
|
203
171
|
// 约束:当最小值与最大值均为数字时,要求最小值 <= 最大值
|
|
204
172
|
if (fieldMin !== null && fieldMax !== null) {
|
|
205
173
|
if (fieldMin > fieldMax) {
|
|
206
|
-
Logger.
|
|
174
|
+
Logger.warn(`${fileType}表 ${fileName} 文件 ${colKey} 最小值 "${fieldMin}" 不能大于最大值 "${fieldMax}"`);
|
|
207
175
|
fileValid = false;
|
|
208
176
|
}
|
|
209
177
|
}
|
|
210
178
|
|
|
211
179
|
// 第6个值:是否创建索引必须为0或1
|
|
212
180
|
if (fieldIndex !== 0 && fieldIndex !== 1) {
|
|
213
|
-
Logger.
|
|
181
|
+
Logger.warn(`${fileType}表 ${fileName} 文件 ${colKey} 索引标识 "${fieldIndex}" 格式错误,必须为0或1`);
|
|
214
182
|
fileValid = false;
|
|
215
183
|
}
|
|
216
184
|
|
|
@@ -221,28 +189,28 @@ export default async function (): Promise<boolean> {
|
|
|
221
189
|
if (fieldType === 'text') {
|
|
222
190
|
// text:min/max 必须为 null,默认值必须为 'null'
|
|
223
191
|
if (fieldMin !== null) {
|
|
224
|
-
Logger.
|
|
192
|
+
Logger.warn(`${fileType}表 ${fileName} 文件 ${colKey} 的 text 类型最小值必须为 null,当前为 "${fieldMin}"`);
|
|
225
193
|
fileValid = false;
|
|
226
194
|
}
|
|
227
195
|
if (fieldMax !== null) {
|
|
228
|
-
Logger.
|
|
196
|
+
Logger.warn(`${fileType}表 ${fileName} 文件 ${colKey} 的 text 类型最大长度必须为 null,当前为 "${fieldMax}"`);
|
|
229
197
|
fileValid = false;
|
|
230
198
|
}
|
|
231
199
|
if (fieldDefault !== 'null') {
|
|
232
|
-
Logger.
|
|
200
|
+
Logger.warn(`${fileType}表 ${fileName} 文件 ${colKey} 为 text 类型,默认值必须为 null,当前为 "${fieldDefault}"`);
|
|
233
201
|
fileValid = false;
|
|
234
202
|
}
|
|
235
203
|
} else if (fieldType === 'string' || fieldType === 'array') {
|
|
236
204
|
if (fieldMax === null || typeof fieldMax !== 'number') {
|
|
237
|
-
Logger.
|
|
205
|
+
Logger.warn(`${fileType}表 ${fileName} 文件 ${colKey} 为 ${fieldType} 类型,` + `最大长度必须为数字,当前为 "${fieldMax}"`);
|
|
238
206
|
fileValid = false;
|
|
239
207
|
} else if (fieldMax > MAX_VARCHAR_LENGTH) {
|
|
240
|
-
Logger.
|
|
208
|
+
Logger.warn(`${fileType}表 ${fileName} 文件 ${colKey} 最大长度 ${fieldMax} 越界,` + `${fieldType} 类型长度必须在 1..${MAX_VARCHAR_LENGTH} 范围内`);
|
|
241
209
|
fileValid = false;
|
|
242
210
|
}
|
|
243
211
|
} else if (fieldType === 'number') {
|
|
244
212
|
if (fieldDefault !== 'null' && typeof fieldDefault !== 'number') {
|
|
245
|
-
Logger.
|
|
213
|
+
Logger.warn(`${fileType}表 ${fileName} 文件 ${colKey} 为 number 类型,` + `默认值必须为数字或null,当前为 "${fieldDefault}"`);
|
|
246
214
|
fileValid = false;
|
|
247
215
|
}
|
|
248
216
|
}
|
|
@@ -264,17 +232,17 @@ export default async function (): Promise<boolean> {
|
|
|
264
232
|
}
|
|
265
233
|
|
|
266
234
|
// 输出统计信息
|
|
267
|
-
Logger.info(
|
|
235
|
+
Logger.info(`表定义检查完成:`);
|
|
268
236
|
Logger.info(` 总文件数: ${totalFiles}`);
|
|
269
237
|
Logger.info(` 总规则数: ${totalRules}`);
|
|
270
238
|
Logger.info(` 通过文件: ${validFiles}`);
|
|
271
239
|
Logger.info(` 失败文件: ${invalidFiles}`);
|
|
272
240
|
|
|
273
241
|
if (invalidFiles > 0) {
|
|
274
|
-
Logger.
|
|
242
|
+
Logger.warn(`表定义检查失败,请修复上述错误后重试`);
|
|
275
243
|
return false;
|
|
276
244
|
} else {
|
|
277
|
-
Logger.info(
|
|
245
|
+
Logger.info(`所有表定义检查通过 ✓`);
|
|
278
246
|
return true;
|
|
279
247
|
}
|
|
280
248
|
} catch (error: any) {
|
package/config/env.ts
CHANGED
|
@@ -26,8 +26,8 @@ export interface EnvConfig {
|
|
|
26
26
|
PARAMS_CHECK: string;
|
|
27
27
|
|
|
28
28
|
// ========== 日志配置 ==========
|
|
29
|
-
/**
|
|
30
|
-
|
|
29
|
+
/** debug日志开关:0 | 1 */
|
|
30
|
+
LOG_DEBUG: number;
|
|
31
31
|
/** 日志排除字段(逗号分隔) */
|
|
32
32
|
LOG_EXCLUDE_FIELDS: string;
|
|
33
33
|
/** 日志目录 */
|
|
@@ -158,7 +158,7 @@ export const Env: EnvConfig = {
|
|
|
158
158
|
PARAMS_CHECK: getEnv('PARAMS_CHECK', 'true'),
|
|
159
159
|
|
|
160
160
|
// ========== 日志配置 ==========
|
|
161
|
-
|
|
161
|
+
LOG_DEBUG: getEnvNumber('LOG_DEBUG', 0),
|
|
162
162
|
LOG_EXCLUDE_FIELDS: getEnv('LOG_EXCLUDE_FIELDS', 'password,token,secret'),
|
|
163
163
|
LOG_DIR: getEnv('LOG_DIR', './logs'),
|
|
164
164
|
LOG_TO_CONSOLE: getEnvNumber('LOG_TO_CONSOLE', 1),
|
package/config/fields.ts
ADDED
|
@@ -0,0 +1,55 @@
|
|
|
1
|
+
/**
|
|
2
|
+
* 通用字段定义
|
|
3
|
+
*
|
|
4
|
+
* 用于在 API 和表定义中复用常见字段规则
|
|
5
|
+
*
|
|
6
|
+
* 格式:`字段标签|数据类型|最小值|最大值|默认值|是否必填|正则表达式`
|
|
7
|
+
*
|
|
8
|
+
* 说明:
|
|
9
|
+
* - 字段标签:用于显示的中文名称
|
|
10
|
+
* - 数据类型:string、number、boolean、array_string、array_text 等
|
|
11
|
+
* - 最小值:string 类型表示最小长度,number 类型表示最小数值,array 类型表示最小元素个数
|
|
12
|
+
* - 最大值:string 类型表示最大长度,number 类型表示最大数值,array 类型表示最大元素个数
|
|
13
|
+
* - 默认值:字段的默认值,无默认值时填 null
|
|
14
|
+
* - 是否必填:0 表示非必填,1 表示必填
|
|
15
|
+
* - 正则表达式:用于验证字段值的正则表达式,无验证时填 null
|
|
16
|
+
*
|
|
17
|
+
* 类型说明:
|
|
18
|
+
* - array_string: 短数组,存储为 VARCHAR,建议设置 max 限制(如 0-100)
|
|
19
|
+
* - array_text: 长数组,存储为 MEDIUMTEXT,min/max 可设为 null 表示不限制
|
|
20
|
+
*
|
|
21
|
+
* 正则表达式别名:
|
|
22
|
+
* - 使用 @ 前缀可以引用内置正则表达式别名,例如:
|
|
23
|
+
* - @number: 纯数字
|
|
24
|
+
* - @alphanumeric: 字母+数字
|
|
25
|
+
* - @email: 邮箱格式
|
|
26
|
+
* - @phone: 中国手机号
|
|
27
|
+
* - @chinese: 纯中文
|
|
28
|
+
* - 完整别名列表见 config/regexAliases.ts
|
|
29
|
+
*
|
|
30
|
+
* 示例:
|
|
31
|
+
* - '用户ID|array_text|null|null|null|0|@number' - 数字数组
|
|
32
|
+
* - '标签|array_string|0|50|null|0|@alphanumeric' - 字母数字数组
|
|
33
|
+
*/
|
|
34
|
+
|
|
35
|
+
export const Fields = {
|
|
36
|
+
_id: 'ID|number|1|null|null|1|null',
|
|
37
|
+
email: '邮箱|string|5|100|null|1|^[a-zA-Z0-9._%+-]+@[a-zA-Z0-9.-]+\\.[a-zA-Z]{2,}$',
|
|
38
|
+
phone: '手机号|string|11|11|null|1|^1[3-9]\\d{9}$',
|
|
39
|
+
page: '页码|number|1|9999|1|0|null',
|
|
40
|
+
limit: '每页数量|number|1|100|10|0|null',
|
|
41
|
+
title: '标题|string|1|200|null|0|null',
|
|
42
|
+
description: '描述|string|0|500|null|0|null',
|
|
43
|
+
keyword: '关键词|string|1|50|null|1|null',
|
|
44
|
+
keywords: '关键词列表|array_string|0|50|null|0|null',
|
|
45
|
+
enabled: '启用状态|number|0|1|1|0|^(0|1)$',
|
|
46
|
+
date: '日期|string|10|10|null|0|^\\d{4}-\\d{2}-\\d{2}$',
|
|
47
|
+
datetime: '日期时间|string|19|25|null|0|^\\d{4}-\\d{2}-\\d{2}T\\d{2}:\\d{2}:\\d{2}',
|
|
48
|
+
filename: '文件名|string|1|255|null|0|null',
|
|
49
|
+
url: '网址|string|5|500|null|0|^https?://',
|
|
50
|
+
tag: '标签|array_string|0|10|null|0|null',
|
|
51
|
+
startTime: '开始时间|number|0|9999999999999|null|0|null',
|
|
52
|
+
endTime: '结束时间|number|0|9999999999999|null|0|null'
|
|
53
|
+
} as const;
|
|
54
|
+
|
|
55
|
+
export default Fields;
|