befly 3.5.6 → 3.6.0
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/lib/addon.ts +77 -0
- package/lib/database.ts +2 -2
- package/lib/logger.ts +6 -15
- package/lifecycle/checker.ts +20 -49
- package/lifecycle/lifecycle.ts +7 -5
- package/lifecycle/loader.ts +5 -5
- package/main.ts +10 -1
- package/package.json +2 -9
- package/paths.ts +5 -54
- package/plugins/db.ts +1 -1
- package/types/database.d.ts +1 -1
- package/util.ts +1 -83
- package/apis/admin/del.ts +0 -35
- package/apis/admin/info.ts +0 -50
- package/apis/admin/ins.ts +0 -61
- package/apis/admin/list.ts +0 -20
- package/apis/admin/roleDetail.ts +0 -35
- package/apis/admin/roleSave.ts +0 -40
- package/apis/admin/upd.ts +0 -51
- package/apis/api/all.ts +0 -37
- package/apis/auth/login.ts +0 -78
- package/apis/auth/logout.ts +0 -23
- package/apis/auth/register.ts +0 -50
- package/apis/auth/sendSmsCode.ts +0 -36
- package/apis/cache/refresh.ts +0 -34
- package/apis/dashboard/addonList.ts +0 -47
- package/apis/dashboard/changelog.ts +0 -37
- package/apis/dashboard/configStatus.ts +0 -54
- package/apis/dashboard/environmentInfo.ts +0 -46
- package/apis/dashboard/performanceMetrics.ts +0 -23
- package/apis/dashboard/permissionStats.ts +0 -31
- package/apis/dashboard/serviceStatus.ts +0 -82
- package/apis/dashboard/systemInfo.ts +0 -26
- package/apis/dashboard/systemOverview.ts +0 -32
- package/apis/dashboard/systemResources.ts +0 -119
- package/apis/dict/all.ts +0 -25
- package/apis/dict/del.ts +0 -19
- package/apis/dict/detail.ts +0 -21
- package/apis/dict/ins.ts +0 -27
- package/apis/dict/list.ts +0 -18
- package/apis/dict/upd.ts +0 -31
- package/apis/menu/all.ts +0 -68
- package/apis/menu/del.ts +0 -37
- package/apis/menu/ins.ts +0 -20
- package/apis/menu/list.ts +0 -21
- package/apis/menu/upd.ts +0 -29
- package/apis/role/apiDetail.ts +0 -30
- package/apis/role/apiSave.ts +0 -41
- package/apis/role/del.ts +0 -44
- package/apis/role/detail.ts +0 -24
- package/apis/role/ins.ts +0 -39
- package/apis/role/list.ts +0 -14
- package/apis/role/menuDetail.ts +0 -30
- package/apis/role/menuSave.ts +0 -38
- package/apis/role/save.ts +0 -44
- package/apis/role/upd.ts +0 -40
- package/bin/index.ts +0 -34
- package/checks/conflict.ts +0 -351
- package/checks/table.ts +0 -250
- package/commands/index.ts +0 -73
- package/commands/sync.ts +0 -88
- package/commands/syncApi.ts +0 -316
- package/commands/syncDb/apply.ts +0 -171
- package/commands/syncDb/constants.ts +0 -77
- package/commands/syncDb/ddl.ts +0 -191
- package/commands/syncDb/helpers.ts +0 -173
- package/commands/syncDb/index.ts +0 -217
- package/commands/syncDb/schema.ts +0 -199
- package/commands/syncDb/sqlite.ts +0 -50
- package/commands/syncDb/state.ts +0 -112
- package/commands/syncDb/table.ts +0 -214
- package/commands/syncDb/tableCreate.ts +0 -149
- package/commands/syncDb/types.ts +0 -92
- package/commands/syncDb/version.ts +0 -73
- package/commands/syncDb.ts +0 -34
- package/commands/syncDev.ts +0 -237
- package/commands/syncMenu.ts +0 -349
- package/commands/util.ts +0 -58
- package/tables/admin.json +0 -14
- package/tables/api.json +0 -8
- package/tables/dict.json +0 -8
- package/tables/menu.json +0 -8
- package/tables/role.json +0 -8
package/checks/conflict.ts
DELETED
|
@@ -1,351 +0,0 @@
|
|
|
1
|
-
/**
|
|
2
|
-
* 资源冲突检测
|
|
3
|
-
* 在系统启动前检测表名、API 路由、插件名等资源是否存在冲突
|
|
4
|
-
*/
|
|
5
|
-
|
|
6
|
-
import { relative, basename } from 'pathe';
|
|
7
|
-
import { existsSync } from 'node:fs';
|
|
8
|
-
import { Logger } from '../lib/logger.js';
|
|
9
|
-
import { projectPluginDir, coreTableDir, projectTableDir, projectApiDir } from '../paths.js';
|
|
10
|
-
import { scanAddons, getAddonDir, addonDirExists } from '../util.js';
|
|
11
|
-
|
|
12
|
-
/**
|
|
13
|
-
* 保留名称配置
|
|
14
|
-
*/
|
|
15
|
-
const RESERVED_NAMES = {
|
|
16
|
-
tablePrefix: ['sys_'],
|
|
17
|
-
plugins: ['db', 'logger', 'redis', 'tool'],
|
|
18
|
-
addonNames: ['app', 'api']
|
|
19
|
-
} as const;
|
|
20
|
-
|
|
21
|
-
/**
|
|
22
|
-
* 资源注册表
|
|
23
|
-
*/
|
|
24
|
-
interface ResourceRegistry {
|
|
25
|
-
tables: Map<string, string>; // 表名 -> 来源
|
|
26
|
-
routes: Map<string, string>; // 路由 -> 来源
|
|
27
|
-
plugins: Map<string, string>; // 插件名 -> 来源
|
|
28
|
-
}
|
|
29
|
-
|
|
30
|
-
/**
|
|
31
|
-
* 冲突检测结果
|
|
32
|
-
*/
|
|
33
|
-
interface ConflictResult {
|
|
34
|
-
hasConflicts: boolean;
|
|
35
|
-
conflicts: string[];
|
|
36
|
-
}
|
|
37
|
-
|
|
38
|
-
/**
|
|
39
|
-
* 收集核心插件
|
|
40
|
-
*/
|
|
41
|
-
async function collectCorePlugins(registry: ResourceRegistry): Promise<void> {
|
|
42
|
-
// 检查插件目录是否存在
|
|
43
|
-
if (!existsSync(projectPluginDir)) {
|
|
44
|
-
return;
|
|
45
|
-
}
|
|
46
|
-
|
|
47
|
-
try {
|
|
48
|
-
const glob = new Bun.Glob('*.ts');
|
|
49
|
-
for await (const file of glob.scan({
|
|
50
|
-
cwd: projectPluginDir,
|
|
51
|
-
onlyFiles: true,
|
|
52
|
-
absolute: true
|
|
53
|
-
})) {
|
|
54
|
-
const pluginName = basename(file).replace(/\.ts$/, '');
|
|
55
|
-
if (pluginName.startsWith('_')) continue;
|
|
56
|
-
|
|
57
|
-
if (registry.plugins.has(pluginName)) {
|
|
58
|
-
Logger.warn(`核心插件 "${pluginName}" 重复定义`);
|
|
59
|
-
} else {
|
|
60
|
-
registry.plugins.set(pluginName, 'core');
|
|
61
|
-
}
|
|
62
|
-
}
|
|
63
|
-
} catch (error: any) {
|
|
64
|
-
Logger.error('收集插件时出错:', error);
|
|
65
|
-
}
|
|
66
|
-
}
|
|
67
|
-
|
|
68
|
-
/**
|
|
69
|
-
* 收集 addon 资源
|
|
70
|
-
*/
|
|
71
|
-
async function collectAddonResources(addonName: string, registry: ResourceRegistry): Promise<string[]> {
|
|
72
|
-
const conflicts: string[] = [];
|
|
73
|
-
|
|
74
|
-
// 检查 addon 名称是否使用保留名称
|
|
75
|
-
if (RESERVED_NAMES.addonNames.includes(addonName.toLowerCase())) {
|
|
76
|
-
conflicts.push(`组件名称 "${addonName}" 使用了保留名称,保留名称包括: ${RESERVED_NAMES.addonNames.join(', ')}`);
|
|
77
|
-
return conflicts;
|
|
78
|
-
}
|
|
79
|
-
|
|
80
|
-
// 收集 addon 表定义
|
|
81
|
-
if (addonDirExists(addonName, 'tables')) {
|
|
82
|
-
const addonTablesDir = getAddonDir(addonName, 'tables');
|
|
83
|
-
const glob = new Bun.Glob('*.json');
|
|
84
|
-
|
|
85
|
-
for await (const file of glob.scan({
|
|
86
|
-
cwd: addonTablesDir,
|
|
87
|
-
onlyFiles: true,
|
|
88
|
-
absolute: true
|
|
89
|
-
})) {
|
|
90
|
-
const fileName = basename(file, '.json');
|
|
91
|
-
if (fileName.startsWith('_')) continue;
|
|
92
|
-
|
|
93
|
-
try {
|
|
94
|
-
const tableDefine = await Bun.file(file).json();
|
|
95
|
-
const tableName = tableDefine.tableName || `${addonName}_${fileName}`;
|
|
96
|
-
|
|
97
|
-
// 检查是否使用保留前缀
|
|
98
|
-
if (RESERVED_NAMES.tablePrefix.some((prefix) => tableName.startsWith(prefix))) {
|
|
99
|
-
conflicts.push(`组件 ${addonName} 表 "${tableName}" 使用了保留前缀,保留前缀包括: ${RESERVED_NAMES.tablePrefix.join(', ')}`);
|
|
100
|
-
continue;
|
|
101
|
-
}
|
|
102
|
-
|
|
103
|
-
// 检查是否与已有表冲突
|
|
104
|
-
if (registry.tables.has(tableName)) {
|
|
105
|
-
conflicts.push(`组件 ${addonName} 表 "${tableName}" 与 ${registry.tables.get(tableName)} 冲突`);
|
|
106
|
-
} else {
|
|
107
|
-
registry.tables.set(tableName, `组件${addonName}`);
|
|
108
|
-
}
|
|
109
|
-
} catch (error: any) {
|
|
110
|
-
// 表定义解析错误会在 table.ts 中处理,这里跳过
|
|
111
|
-
}
|
|
112
|
-
}
|
|
113
|
-
}
|
|
114
|
-
|
|
115
|
-
// 收集 addon API 路由
|
|
116
|
-
if (addonDirExists(addonName, 'apis')) {
|
|
117
|
-
const addonApisDir = getAddonDir(addonName, 'apis');
|
|
118
|
-
const glob = new Bun.Glob('**/*.ts');
|
|
119
|
-
|
|
120
|
-
for await (const file of glob.scan({
|
|
121
|
-
cwd: addonApisDir,
|
|
122
|
-
onlyFiles: true,
|
|
123
|
-
absolute: true
|
|
124
|
-
})) {
|
|
125
|
-
const apiPath = relative(addonApisDir, file).replace(/\.ts$/, '');
|
|
126
|
-
if (apiPath.indexOf('_') !== -1) continue;
|
|
127
|
-
|
|
128
|
-
try {
|
|
129
|
-
const api = (await import(file)).default;
|
|
130
|
-
const route = `${(api.method || 'POST').toUpperCase()}/api/${addonName}/${apiPath}`;
|
|
131
|
-
|
|
132
|
-
// 检查是否使用保留路由前缀 /api
|
|
133
|
-
if (route.includes('/api/api/') || route.includes('/api/api')) {
|
|
134
|
-
conflicts.push(`组件 [${addonName}] 路由 "${route}" 使用了保留路径前缀 "/api"`);
|
|
135
|
-
continue;
|
|
136
|
-
}
|
|
137
|
-
|
|
138
|
-
// 检查是否与已有路由冲突
|
|
139
|
-
if (registry.routes.has(route)) {
|
|
140
|
-
conflicts.push(`组件 [${addonName}] 路由 "${route}" 与 ${registry.routes.get(route)} 冲突`);
|
|
141
|
-
} else {
|
|
142
|
-
registry.routes.set(route, `组件${addonName}`);
|
|
143
|
-
}
|
|
144
|
-
} catch (error: any) {
|
|
145
|
-
// API 加载错误会在 loader.ts 中处理,这里跳过
|
|
146
|
-
}
|
|
147
|
-
}
|
|
148
|
-
}
|
|
149
|
-
|
|
150
|
-
// 收集 addon 插件
|
|
151
|
-
if (addonDirExists(addonName, 'plugins')) {
|
|
152
|
-
const addonPluginsDir = getAddonDir(addonName, 'plugins');
|
|
153
|
-
const glob = new Bun.Glob('*.ts');
|
|
154
|
-
|
|
155
|
-
for await (const file of glob.scan({
|
|
156
|
-
cwd: addonPluginsDir,
|
|
157
|
-
onlyFiles: true,
|
|
158
|
-
absolute: true
|
|
159
|
-
})) {
|
|
160
|
-
const fileName = basename(file).replace(/\.ts$/, '');
|
|
161
|
-
if (fileName.startsWith('_')) continue;
|
|
162
|
-
|
|
163
|
-
// Addon 插件使用点号命名空间
|
|
164
|
-
const pluginName = `${addonName}.${fileName}`;
|
|
165
|
-
|
|
166
|
-
// 检查是否使用保留名称(检测核心插件名或点号前缀是保留名称)
|
|
167
|
-
const isReserved = RESERVED_NAMES.plugins.includes(pluginName) || (pluginName.includes('.') && RESERVED_NAMES.plugins.includes(pluginName.split('.')[0]));
|
|
168
|
-
if (isReserved) {
|
|
169
|
-
conflicts.push(`组件 ${addonName} 插件 "${pluginName}" 使用了保留名称,保留名称包括: ${RESERVED_NAMES.plugins.join(', ')}`);
|
|
170
|
-
continue;
|
|
171
|
-
}
|
|
172
|
-
|
|
173
|
-
// 检查是否与已有插件冲突
|
|
174
|
-
if (registry.plugins.has(pluginName)) {
|
|
175
|
-
conflicts.push(`组件 [${addonName}] 插件 "${pluginName}" 与 ${registry.plugins.get(pluginName)} 冲突`);
|
|
176
|
-
} else {
|
|
177
|
-
registry.plugins.set(pluginName, `组件${addonName}`);
|
|
178
|
-
}
|
|
179
|
-
}
|
|
180
|
-
}
|
|
181
|
-
|
|
182
|
-
return conflicts;
|
|
183
|
-
}
|
|
184
|
-
|
|
185
|
-
/**
|
|
186
|
-
* 收集用户资源
|
|
187
|
-
*/
|
|
188
|
-
async function collectUserResources(registry: ResourceRegistry): Promise<string[]> {
|
|
189
|
-
const conflicts: string[] = [];
|
|
190
|
-
|
|
191
|
-
// 收集用户表定义
|
|
192
|
-
const userTablesDir = projectTableDir;
|
|
193
|
-
if (existsSync(userTablesDir)) {
|
|
194
|
-
try {
|
|
195
|
-
const glob = new Bun.Glob('*.json');
|
|
196
|
-
for await (const file of glob.scan({
|
|
197
|
-
cwd: userTablesDir,
|
|
198
|
-
onlyFiles: true,
|
|
199
|
-
absolute: true
|
|
200
|
-
})) {
|
|
201
|
-
const fileName = basename(file, '.json');
|
|
202
|
-
if (fileName.startsWith('_')) continue;
|
|
203
|
-
|
|
204
|
-
try {
|
|
205
|
-
const tableDefine = await Bun.file(file).json();
|
|
206
|
-
const tableName = tableDefine.tableName || fileName;
|
|
207
|
-
|
|
208
|
-
// 检查是否使用保留前缀
|
|
209
|
-
if (RESERVED_NAMES.tablePrefix.some((prefix) => tableName.startsWith(prefix))) {
|
|
210
|
-
conflicts.push(`用户表 "${tableName}" 使用了保留前缀,保留前缀包括: ${RESERVED_NAMES.tablePrefix.join(', ')}`);
|
|
211
|
-
continue;
|
|
212
|
-
}
|
|
213
|
-
|
|
214
|
-
// 检查是否与已有表冲突
|
|
215
|
-
if (registry.tables.has(tableName)) {
|
|
216
|
-
conflicts.push(`用户表 "${tableName}" 与 ${registry.tables.get(tableName)} 冲突`);
|
|
217
|
-
} else {
|
|
218
|
-
registry.tables.set(tableName, 'user');
|
|
219
|
-
}
|
|
220
|
-
} catch (error: any) {
|
|
221
|
-
// 表定义解析错误会在 table.ts 中处理,这里跳过
|
|
222
|
-
}
|
|
223
|
-
}
|
|
224
|
-
} catch (error: any) {
|
|
225
|
-
Logger.error('收集用户表定义时出错:', error);
|
|
226
|
-
}
|
|
227
|
-
}
|
|
228
|
-
|
|
229
|
-
// 收集用户 API 路由
|
|
230
|
-
const userApisDir = projectApiDir;
|
|
231
|
-
if (existsSync(userApisDir)) {
|
|
232
|
-
try {
|
|
233
|
-
const glob = new Bun.Glob('**/*.ts');
|
|
234
|
-
for await (const file of glob.scan({
|
|
235
|
-
cwd: userApisDir,
|
|
236
|
-
onlyFiles: true,
|
|
237
|
-
absolute: true
|
|
238
|
-
})) {
|
|
239
|
-
const apiPath = relative(userApisDir, file).replace(/\.ts$/, '');
|
|
240
|
-
if (apiPath.indexOf('_') !== -1) continue;
|
|
241
|
-
|
|
242
|
-
try {
|
|
243
|
-
const api = (await import(file)).default;
|
|
244
|
-
const route = `${(api.method || 'POST').toUpperCase()}/api/${apiPath}`;
|
|
245
|
-
|
|
246
|
-
// 检查是否使用保留路由前缀 /api
|
|
247
|
-
if (apiPath.startsWith('api/') || apiPath === 'api') {
|
|
248
|
-
conflicts.push(`用户路由 "${route}" 使用了保留路径前缀 "/api"`);
|
|
249
|
-
continue;
|
|
250
|
-
}
|
|
251
|
-
|
|
252
|
-
// 检查是否与已有路由冲突
|
|
253
|
-
if (registry.routes.has(route)) {
|
|
254
|
-
conflicts.push(`用户路由 "${route}" 与 ${registry.routes.get(route)} 冲突`);
|
|
255
|
-
} else {
|
|
256
|
-
registry.routes.set(route, 'user');
|
|
257
|
-
}
|
|
258
|
-
} catch (error: any) {
|
|
259
|
-
// API 加载错误会在 loader.ts 中处理,这里跳过
|
|
260
|
-
}
|
|
261
|
-
}
|
|
262
|
-
} catch (error: any) {
|
|
263
|
-
Logger.error('收集用户 API 路由时出错:', error);
|
|
264
|
-
}
|
|
265
|
-
}
|
|
266
|
-
|
|
267
|
-
// 收集用户插件
|
|
268
|
-
const userPluginsDir = projectPluginDir;
|
|
269
|
-
if (existsSync(userPluginsDir)) {
|
|
270
|
-
try {
|
|
271
|
-
const glob = new Bun.Glob('*.ts');
|
|
272
|
-
for await (const file of glob.scan({
|
|
273
|
-
cwd: userPluginsDir,
|
|
274
|
-
onlyFiles: true,
|
|
275
|
-
absolute: true
|
|
276
|
-
})) {
|
|
277
|
-
const pluginName = basename(file).replace(/\.ts$/, '');
|
|
278
|
-
if (pluginName.startsWith('_')) continue;
|
|
279
|
-
|
|
280
|
-
// 检查是否使用保留名称(检测核心插件名或点号前缀是保留名称)
|
|
281
|
-
const isReserved = RESERVED_NAMES.plugins.includes(pluginName) || (pluginName.includes('.') && RESERVED_NAMES.plugins.includes(pluginName.split('.')[0]));
|
|
282
|
-
if (isReserved) {
|
|
283
|
-
conflicts.push(`用户插件 "${pluginName}" 使用了保留名称,保留名称包括: ${RESERVED_NAMES.plugins.join(', ')}`);
|
|
284
|
-
continue;
|
|
285
|
-
}
|
|
286
|
-
|
|
287
|
-
// 检查是否与已有插件冲突
|
|
288
|
-
if (registry.plugins.has(pluginName)) {
|
|
289
|
-
conflicts.push(`用户插件 "${pluginName}" 与 ${registry.plugins.get(pluginName)} 冲突`);
|
|
290
|
-
} else {
|
|
291
|
-
registry.plugins.set(pluginName, 'user');
|
|
292
|
-
}
|
|
293
|
-
}
|
|
294
|
-
} catch (error: any) {
|
|
295
|
-
Logger.error('收集用户插件时出错:', error);
|
|
296
|
-
}
|
|
297
|
-
}
|
|
298
|
-
|
|
299
|
-
return conflicts;
|
|
300
|
-
}
|
|
301
|
-
|
|
302
|
-
/**
|
|
303
|
-
* 执行资源冲突检测
|
|
304
|
-
*/
|
|
305
|
-
export default async function checkConflict(): Promise<boolean> {
|
|
306
|
-
try {
|
|
307
|
-
// 初始化资源注册表
|
|
308
|
-
const registry: ResourceRegistry = {
|
|
309
|
-
tables: new Map(),
|
|
310
|
-
routes: new Map(),
|
|
311
|
-
plugins: new Map()
|
|
312
|
-
};
|
|
313
|
-
|
|
314
|
-
const allConflicts: string[] = [];
|
|
315
|
-
|
|
316
|
-
// 1. 收集核心插件
|
|
317
|
-
await collectCorePlugins(registry);
|
|
318
|
-
|
|
319
|
-
// 2. 收集 addon 资源
|
|
320
|
-
const addons = scanAddons();
|
|
321
|
-
for (const addon of addons) {
|
|
322
|
-
const addonConflicts = await collectAddonResources(addon, registry);
|
|
323
|
-
allConflicts.push(...addonConflicts);
|
|
324
|
-
}
|
|
325
|
-
|
|
326
|
-
// 3. 收集用户资源
|
|
327
|
-
const userConflicts = await collectUserResources(registry);
|
|
328
|
-
allConflicts.push(...userConflicts);
|
|
329
|
-
|
|
330
|
-
// 4. 报告冲突
|
|
331
|
-
if (allConflicts.length > 0) {
|
|
332
|
-
Logger.warn('检测到资源冲突:');
|
|
333
|
-
allConflicts.forEach((conflict, index) => {
|
|
334
|
-
Logger.warn(` ${index + 1}. ${conflict}`);
|
|
335
|
-
});
|
|
336
|
-
Logger.warn('请解决以上冲突后再启动服务器');
|
|
337
|
-
return false;
|
|
338
|
-
}
|
|
339
|
-
|
|
340
|
-
// 5. 统计信息
|
|
341
|
-
Logger.info(`资源冲突检测通过 ✓`);
|
|
342
|
-
Logger.info(` - 表: ${registry.tables.size} 个`);
|
|
343
|
-
Logger.info(` - 路由: ${registry.routes.size} 个`);
|
|
344
|
-
Logger.info(` - 插件: ${registry.plugins.size} 个`);
|
|
345
|
-
|
|
346
|
-
return true;
|
|
347
|
-
} catch (error: any) {
|
|
348
|
-
Logger.error('资源冲突检测时发生错误:', error);
|
|
349
|
-
return false;
|
|
350
|
-
}
|
|
351
|
-
}
|
package/checks/table.ts
DELETED
|
@@ -1,250 +0,0 @@
|
|
|
1
|
-
/**
|
|
2
|
-
* 表规则检查器 - TypeScript 版本
|
|
3
|
-
* 验证表定义文件的格式和规则
|
|
4
|
-
*/
|
|
5
|
-
|
|
6
|
-
import { basename } from 'pathe';
|
|
7
|
-
import { Logger } from '../lib/logger.js';
|
|
8
|
-
import { parseRule } from '../util.js';
|
|
9
|
-
import { projectTableDir } from '../paths.js';
|
|
10
|
-
import { scanAddons, getAddonDir } from '../util.js';
|
|
11
|
-
|
|
12
|
-
/**
|
|
13
|
-
* 表文件信息接口
|
|
14
|
-
*/
|
|
15
|
-
interface TableFileInfo {
|
|
16
|
-
/** 表文件路径 */
|
|
17
|
-
file: string;
|
|
18
|
-
/** 文件类型:project(项目)或 addon(组件) */
|
|
19
|
-
type: 'project' | 'addon';
|
|
20
|
-
/** 如果是 addon 类型,记录 addon 名称 */
|
|
21
|
-
addonName?: string;
|
|
22
|
-
}
|
|
23
|
-
|
|
24
|
-
/**
|
|
25
|
-
* 保留字段列表
|
|
26
|
-
*/
|
|
27
|
-
const RESERVED_FIELDS = ['id', 'created_at', 'updated_at', 'deleted_at', 'state'] as const;
|
|
28
|
-
|
|
29
|
-
/**
|
|
30
|
-
* 允许的字段类型
|
|
31
|
-
*/
|
|
32
|
-
const FIELD_TYPES = ['string', 'number', 'text', 'array_string', 'array_text'] as const;
|
|
33
|
-
|
|
34
|
-
/**
|
|
35
|
-
* 小驼峰命名正则
|
|
36
|
-
* 可选:以下划线开头(用于特殊文件,如通用字段定义)
|
|
37
|
-
* 必须以小写字母开头,后续可包含小写/数字,或多个 [大写+小写/数字] 片段
|
|
38
|
-
* 示例:userTable、testCustomers、common
|
|
39
|
-
*/
|
|
40
|
-
const LOWER_CAMEL_CASE_REGEX = /^_?[a-z][a-z0-9]*(?:[A-Z][a-z0-9]*)*$/;
|
|
41
|
-
|
|
42
|
-
/**
|
|
43
|
-
* 字段名称正则
|
|
44
|
-
* 必须为中文、数字、字母、下划线、短横线、空格
|
|
45
|
-
*/
|
|
46
|
-
const FIELD_NAME_REGEX = /^[\u4e00-\u9fa5a-zA-Z0-9 _-]+$/;
|
|
47
|
-
|
|
48
|
-
/**
|
|
49
|
-
* VARCHAR 最大长度限制
|
|
50
|
-
*/
|
|
51
|
-
const MAX_VARCHAR_LENGTH = 65535;
|
|
52
|
-
|
|
53
|
-
/**
|
|
54
|
-
* 检查表定义文件
|
|
55
|
-
* @returns 检查是否通过
|
|
56
|
-
*/
|
|
57
|
-
export default async function (): Promise<boolean> {
|
|
58
|
-
try {
|
|
59
|
-
const tablesGlob = new Bun.Glob('*.json');
|
|
60
|
-
|
|
61
|
-
// 统计信息
|
|
62
|
-
let totalFiles = 0;
|
|
63
|
-
let totalRules = 0;
|
|
64
|
-
let validFiles = 0;
|
|
65
|
-
let invalidFiles = 0;
|
|
66
|
-
|
|
67
|
-
// 收集所有表文件
|
|
68
|
-
const allTableFiles: TableFileInfo[] = [];
|
|
69
|
-
|
|
70
|
-
// 收集项目表字段定义文件
|
|
71
|
-
for await (const file of tablesGlob.scan({
|
|
72
|
-
cwd: projectTableDir,
|
|
73
|
-
absolute: true,
|
|
74
|
-
onlyFiles: true
|
|
75
|
-
})) {
|
|
76
|
-
allTableFiles.push({ file, type: 'project' });
|
|
77
|
-
}
|
|
78
|
-
|
|
79
|
-
// 收集 addon 表字段定义文件
|
|
80
|
-
const addons = scanAddons();
|
|
81
|
-
for (const addonName of addons) {
|
|
82
|
-
const addonTablesDir = getAddonDir(addonName, 'tables');
|
|
83
|
-
|
|
84
|
-
try {
|
|
85
|
-
for await (const file of tablesGlob.scan({
|
|
86
|
-
cwd: addonTablesDir,
|
|
87
|
-
absolute: true,
|
|
88
|
-
onlyFiles: true
|
|
89
|
-
})) {
|
|
90
|
-
allTableFiles.push({ file, type: 'addon', addonName });
|
|
91
|
-
}
|
|
92
|
-
} catch (error) {
|
|
93
|
-
// addon 的 tables 目录可能不存在,跳过
|
|
94
|
-
}
|
|
95
|
-
}
|
|
96
|
-
|
|
97
|
-
// 合并进行验证逻辑
|
|
98
|
-
for (const { file, type, addonName } of allTableFiles) {
|
|
99
|
-
totalFiles++;
|
|
100
|
-
const fileName = basename(file);
|
|
101
|
-
const fileBaseName = basename(file, '.json');
|
|
102
|
-
const fileType = type === 'project' ? '项目' : `组件${addonName}`;
|
|
103
|
-
|
|
104
|
-
try {
|
|
105
|
-
// 1) 文件名小驼峰校验
|
|
106
|
-
if (!LOWER_CAMEL_CASE_REGEX.test(fileBaseName)) {
|
|
107
|
-
Logger.warn(`${fileType}表 ${fileName} 文件名必须使用小驼峰命名(例如 testCustomers.json)`);
|
|
108
|
-
// 命名不合规,记录错误并计为无效文件,继续下一个文件
|
|
109
|
-
invalidFiles++;
|
|
110
|
-
continue;
|
|
111
|
-
}
|
|
112
|
-
|
|
113
|
-
// 读取并解析 JSON 文件
|
|
114
|
-
const table = await Bun.file(file).json();
|
|
115
|
-
let fileValid = true;
|
|
116
|
-
let fileRules = 0;
|
|
117
|
-
|
|
118
|
-
// 检查 table 中的每个验证规则
|
|
119
|
-
for (const [colKey, rule] of Object.entries(table)) {
|
|
120
|
-
if (typeof rule !== 'string') {
|
|
121
|
-
Logger.warn(`${fileType}表 ${fileName} 文件 ${colKey} 规则必须为字符串`);
|
|
122
|
-
fileValid = false;
|
|
123
|
-
continue;
|
|
124
|
-
}
|
|
125
|
-
|
|
126
|
-
// 验证规则格式
|
|
127
|
-
try {
|
|
128
|
-
fileRules++;
|
|
129
|
-
totalRules++;
|
|
130
|
-
|
|
131
|
-
// 检查是否使用了保留字段
|
|
132
|
-
if (RESERVED_FIELDS.includes(colKey as any)) {
|
|
133
|
-
Logger.warn(`${fileType}表 ${fileName} 文件包含保留字段 ${colKey},` + `不能在表定义中使用以下字段: ${RESERVED_FIELDS.join(', ')}`);
|
|
134
|
-
fileValid = false;
|
|
135
|
-
}
|
|
136
|
-
|
|
137
|
-
// 使用 parseRule 解析字段规则
|
|
138
|
-
let parsed;
|
|
139
|
-
try {
|
|
140
|
-
parsed = parseRule(rule);
|
|
141
|
-
} catch (error: any) {
|
|
142
|
-
Logger.warn(`${fileType}表 ${fileName} 文件 ${colKey} 字段规则解析失败:${error.message}`);
|
|
143
|
-
fileValid = false;
|
|
144
|
-
continue;
|
|
145
|
-
}
|
|
146
|
-
|
|
147
|
-
const { name: fieldName, type: fieldType, min: fieldMin, max: fieldMax, default: fieldDefault, index: fieldIndex, regex: fieldRegx } = parsed;
|
|
148
|
-
|
|
149
|
-
// 第1个值:名称必须为中文、数字、字母、下划线、短横线、空格
|
|
150
|
-
if (!FIELD_NAME_REGEX.test(fieldName)) {
|
|
151
|
-
Logger.warn(`${fileType}表 ${fileName} 文件 ${colKey} 字段名称 "${fieldName}" 格式错误,` + `必须为中文、数字、字母、下划线、短横线、空格`);
|
|
152
|
-
fileValid = false;
|
|
153
|
-
}
|
|
154
|
-
|
|
155
|
-
// 第2个值:字段类型必须为string,number,text,array_string,array_text之一
|
|
156
|
-
if (!FIELD_TYPES.includes(fieldType as any)) {
|
|
157
|
-
Logger.warn(`${fileType}表 ${fileName} 文件 ${colKey} 字段类型 "${fieldType}" 格式错误,` + `必须为${FIELD_TYPES.join('、')}之一`);
|
|
158
|
-
fileValid = false;
|
|
159
|
-
}
|
|
160
|
-
|
|
161
|
-
// 第3/4个值:需要是 null 或 数字
|
|
162
|
-
if (!(fieldMin === null || typeof fieldMin === 'number')) {
|
|
163
|
-
Logger.warn(`${fileType}表 ${fileName} 文件 ${colKey} 最小值 "${fieldMin}" 格式错误,必须为null或数字`);
|
|
164
|
-
fileValid = false;
|
|
165
|
-
}
|
|
166
|
-
if (!(fieldMax === null || typeof fieldMax === 'number')) {
|
|
167
|
-
Logger.warn(`${fileType}表 ${fileName} 文件 ${colKey} 最大值 "${fieldMax}" 格式错误,必须为null或数字`);
|
|
168
|
-
fileValid = false;
|
|
169
|
-
}
|
|
170
|
-
|
|
171
|
-
// 约束:当最小值与最大值均为数字时,要求最小值 <= 最大值
|
|
172
|
-
if (fieldMin !== null && fieldMax !== null) {
|
|
173
|
-
if (fieldMin > fieldMax) {
|
|
174
|
-
Logger.warn(`${fileType}表 ${fileName} 文件 ${colKey} 最小值 "${fieldMin}" 不能大于最大值 "${fieldMax}"`);
|
|
175
|
-
fileValid = false;
|
|
176
|
-
}
|
|
177
|
-
}
|
|
178
|
-
|
|
179
|
-
// 第6个值:是否创建索引必须为0或1
|
|
180
|
-
if (fieldIndex !== 0 && fieldIndex !== 1) {
|
|
181
|
-
Logger.warn(`${fileType}表 ${fileName} 文件 ${colKey} 索引标识 "${fieldIndex}" 格式错误,必须为0或1`);
|
|
182
|
-
fileValid = false;
|
|
183
|
-
}
|
|
184
|
-
|
|
185
|
-
// 第7个值:必须为null或正则表达式(parseRule已经验证过了)
|
|
186
|
-
// parseRule 已经将正则字符串转换为 RegExp 或 null,这里不需要再验证
|
|
187
|
-
|
|
188
|
-
// 第4个值与类型联动校验 + 默认值规则
|
|
189
|
-
if (fieldType === 'text') {
|
|
190
|
-
// text:min/max 必须为 null,默认值必须为 'null'
|
|
191
|
-
if (fieldMin !== null) {
|
|
192
|
-
Logger.warn(`${fileType}表 ${fileName} 文件 ${colKey} 的 text 类型最小值必须为 null,当前为 "${fieldMin}"`);
|
|
193
|
-
fileValid = false;
|
|
194
|
-
}
|
|
195
|
-
if (fieldMax !== null) {
|
|
196
|
-
Logger.warn(`${fileType}表 ${fileName} 文件 ${colKey} 的 text 类型最大长度必须为 null,当前为 "${fieldMax}"`);
|
|
197
|
-
fileValid = false;
|
|
198
|
-
}
|
|
199
|
-
if (fieldDefault !== 'null') {
|
|
200
|
-
Logger.warn(`${fileType}表 ${fileName} 文件 ${colKey} 为 text 类型,默认值必须为 null,当前为 "${fieldDefault}"`);
|
|
201
|
-
fileValid = false;
|
|
202
|
-
}
|
|
203
|
-
} else if (fieldType === 'string' || fieldType === 'array') {
|
|
204
|
-
if (fieldMax === null || typeof fieldMax !== 'number') {
|
|
205
|
-
Logger.warn(`${fileType}表 ${fileName} 文件 ${colKey} 为 ${fieldType} 类型,` + `最大长度必须为数字,当前为 "${fieldMax}"`);
|
|
206
|
-
fileValid = false;
|
|
207
|
-
} else if (fieldMax > MAX_VARCHAR_LENGTH) {
|
|
208
|
-
Logger.warn(`${fileType}表 ${fileName} 文件 ${colKey} 最大长度 ${fieldMax} 越界,` + `${fieldType} 类型长度必须在 1..${MAX_VARCHAR_LENGTH} 范围内`);
|
|
209
|
-
fileValid = false;
|
|
210
|
-
}
|
|
211
|
-
} else if (fieldType === 'number') {
|
|
212
|
-
if (fieldDefault !== 'null' && typeof fieldDefault !== 'number') {
|
|
213
|
-
Logger.warn(`${fileType}表 ${fileName} 文件 ${colKey} 为 number 类型,` + `默认值必须为数字或null,当前为 "${fieldDefault}"`);
|
|
214
|
-
fileValid = false;
|
|
215
|
-
}
|
|
216
|
-
}
|
|
217
|
-
} catch (error: any) {
|
|
218
|
-
// 单个字段规则解析失败已在上面处理
|
|
219
|
-
}
|
|
220
|
-
}
|
|
221
|
-
|
|
222
|
-
if (fileValid) {
|
|
223
|
-
validFiles++;
|
|
224
|
-
// Logger.info(`${fileType}表 ${fileName} 验证通过(${fileRules} 个字段)`);
|
|
225
|
-
} else {
|
|
226
|
-
invalidFiles++;
|
|
227
|
-
}
|
|
228
|
-
} catch (error: any) {
|
|
229
|
-
Logger.error(`${fileType}表 ${fileName} 解析失败: ${error.message}`);
|
|
230
|
-
invalidFiles++;
|
|
231
|
-
}
|
|
232
|
-
}
|
|
233
|
-
|
|
234
|
-
// 输出统计信息
|
|
235
|
-
// Logger.info(` 总文件数: ${totalFiles}`);
|
|
236
|
-
// Logger.info(` 总规则数: ${totalRules}`);
|
|
237
|
-
// Logger.info(` 通过文件: ${validFiles}`);
|
|
238
|
-
// Logger.info(` 失败文件: ${invalidFiles}`);
|
|
239
|
-
|
|
240
|
-
if (invalidFiles > 0) {
|
|
241
|
-
Logger.warn(`表定义检查失败,请修复上述错误后重试`);
|
|
242
|
-
return false;
|
|
243
|
-
} else {
|
|
244
|
-
return true;
|
|
245
|
-
}
|
|
246
|
-
} catch (error: any) {
|
|
247
|
-
Logger.error('数据表定义检查过程中出错:', error);
|
|
248
|
-
return false;
|
|
249
|
-
}
|
|
250
|
-
}
|
package/commands/index.ts
DELETED
|
@@ -1,73 +0,0 @@
|
|
|
1
|
-
/**
|
|
2
|
-
* Sync 命令实现
|
|
3
|
-
*/
|
|
4
|
-
|
|
5
|
-
import { join } from 'pathe';
|
|
6
|
-
import { existsSync } from 'node:fs';
|
|
7
|
-
import { Logger } from '../lib/logger.js';
|
|
8
|
-
import { getProjectRoot } from './util.js';
|
|
9
|
-
|
|
10
|
-
// ========== Sync 命令 ==========
|
|
11
|
-
interface SyncOptions {
|
|
12
|
-
table?: string;
|
|
13
|
-
force: boolean;
|
|
14
|
-
dryRun: boolean;
|
|
15
|
-
drop: boolean;
|
|
16
|
-
}
|
|
17
|
-
|
|
18
|
-
export async function syncCommand(options: SyncOptions) {
|
|
19
|
-
try {
|
|
20
|
-
const projectRoot = getProjectRoot();
|
|
21
|
-
const syncScript = join(projectRoot, 'node_modules', 'befly', 'scripts', 'syncTable.ts');
|
|
22
|
-
|
|
23
|
-
if (!existsSync(syncScript)) {
|
|
24
|
-
Logger.error('未找到同步脚本,请确保已安装 befly');
|
|
25
|
-
process.exit(1);
|
|
26
|
-
}
|
|
27
|
-
|
|
28
|
-
Logger.info('正在同步数据库表...');
|
|
29
|
-
|
|
30
|
-
const args = ['run', syncScript];
|
|
31
|
-
|
|
32
|
-
if (options.table) {
|
|
33
|
-
args.push('--table', options.table);
|
|
34
|
-
}
|
|
35
|
-
|
|
36
|
-
if (options.force) {
|
|
37
|
-
args.push('--force');
|
|
38
|
-
}
|
|
39
|
-
|
|
40
|
-
if (options.dryRun) {
|
|
41
|
-
args.push('--dry-run');
|
|
42
|
-
}
|
|
43
|
-
|
|
44
|
-
if (options.drop) {
|
|
45
|
-
args.push('--drop');
|
|
46
|
-
}
|
|
47
|
-
|
|
48
|
-
const proc = Bun.spawn(['bun', ...args], {
|
|
49
|
-
cwd: projectRoot,
|
|
50
|
-
stdout: 'inherit',
|
|
51
|
-
stderr: 'inherit',
|
|
52
|
-
stdin: 'inherit'
|
|
53
|
-
});
|
|
54
|
-
|
|
55
|
-
await proc.exited;
|
|
56
|
-
|
|
57
|
-
if (proc.exitCode === 0) {
|
|
58
|
-
Logger.success('数据库同步完成');
|
|
59
|
-
} else {
|
|
60
|
-
Logger.error('数据库同步失败');
|
|
61
|
-
process.exit(1);
|
|
62
|
-
}
|
|
63
|
-
} catch (error) {
|
|
64
|
-
Logger.error('同步失败:');
|
|
65
|
-
console.error(error);
|
|
66
|
-
process.exit(1);
|
|
67
|
-
}
|
|
68
|
-
}
|
|
69
|
-
|
|
70
|
-
// ========== 导出同步命令 ==========
|
|
71
|
-
export { syncApiCommand } from './syncApi.js';
|
|
72
|
-
export { syncMenuCommand } from './syncMenu.js';
|
|
73
|
-
export { syncDevCommand } from './syncDev.js';
|