befly 3.2.1 → 3.3.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/bin/index.ts +138 -0
- package/checks/conflict.ts +35 -25
- package/checks/table.ts +6 -6
- package/commands/addon.ts +57 -0
- package/commands/build.ts +74 -0
- package/commands/dev.ts +94 -0
- package/commands/index.ts +252 -0
- package/commands/script.ts +303 -0
- package/commands/start.ts +80 -0
- package/commands/syncApi.ts +327 -0
- package/{scripts → commands}/syncDb/apply.ts +2 -2
- package/{scripts → commands}/syncDb/constants.ts +13 -7
- package/{scripts → commands}/syncDb/ddl.ts +7 -5
- package/{scripts → commands}/syncDb/helpers.ts +18 -18
- package/{scripts → commands}/syncDb/index.ts +37 -23
- package/{scripts → commands}/syncDb/sqlite.ts +1 -1
- package/{scripts → commands}/syncDb/state.ts +10 -4
- package/{scripts → commands}/syncDb/table.ts +7 -7
- package/{scripts → commands}/syncDb/tableCreate.ts +7 -6
- package/{scripts → commands}/syncDb/types.ts +5 -5
- package/{scripts → commands}/syncDb/version.ts +1 -1
- package/commands/syncDb.ts +35 -0
- package/commands/syncDev.ts +174 -0
- package/commands/syncMenu.ts +368 -0
- package/config/env.ts +4 -4
- package/config/menu.json +67 -0
- package/{utils/crypto.ts → lib/cipher.ts} +16 -67
- package/lib/database.ts +296 -0
- package/{utils → lib}/dbHelper.ts +102 -56
- package/{utils → lib}/jwt.ts +124 -151
- package/{utils → lib}/logger.ts +47 -24
- package/lib/middleware.ts +271 -0
- package/{utils → lib}/redisHelper.ts +4 -4
- package/{utils/validate.ts → lib/validator.ts} +101 -78
- package/lifecycle/bootstrap.ts +63 -0
- package/lifecycle/checker.ts +165 -0
- package/lifecycle/cluster.ts +241 -0
- package/lifecycle/lifecycle.ts +139 -0
- package/lifecycle/loader.ts +513 -0
- package/main.ts +14 -12
- package/package.json +21 -9
- package/paths.ts +34 -0
- package/plugins/cache.ts +187 -0
- package/plugins/db.ts +4 -4
- package/plugins/logger.ts +1 -1
- package/plugins/redis.ts +4 -4
- package/router/api.ts +155 -0
- package/router/root.ts +53 -0
- package/router/static.ts +76 -0
- package/types/api.d.ts +0 -36
- package/types/befly.d.ts +8 -6
- package/types/common.d.ts +1 -1
- package/types/context.d.ts +3 -3
- package/types/util.d.ts +45 -0
- package/util.ts +299 -0
- package/config/fields.ts +0 -55
- package/config/regexAliases.ts +0 -51
- package/config/reserved.ts +0 -96
- package/scripts/syncDb/tests/constants.test.ts +0 -105
- package/scripts/syncDb/tests/ddl.test.ts +0 -134
- package/scripts/syncDb/tests/helpers.test.ts +0 -70
- package/scripts/syncDb.ts +0 -10
- package/types/index.d.ts +0 -450
- package/types/index.ts +0 -438
- package/types/validator.ts +0 -43
- package/utils/colors.ts +0 -221
- package/utils/database.ts +0 -348
- package/utils/helper.ts +0 -812
- package/utils/index.ts +0 -33
- package/utils/requestContext.ts +0 -167
- /package/{scripts → commands}/syncDb/schema.ts +0 -0
- /package/{utils → lib}/sqlBuilder.ts +0 -0
- /package/{utils → lib}/xml.ts +0 -0
|
@@ -0,0 +1,327 @@
|
|
|
1
|
+
/**
|
|
2
|
+
* SyncApi 命令 - 同步 API 接口数据到数据库
|
|
3
|
+
* 说明:遍历所有 API 文件,收集接口路由信息并同步到 core_api 表
|
|
4
|
+
*
|
|
5
|
+
* 流程:
|
|
6
|
+
* 1. 扫描 core/apis 目录下所有 Core API 文件
|
|
7
|
+
* 2. 扫描项目 apis 目录下所有项目 API 文件
|
|
8
|
+
* 3. 扫描 node_modules/@befly-addon/* 目录下所有组件 API 文件
|
|
9
|
+
* 4. 提取每个 API 的 name、method、auth 等信息
|
|
10
|
+
* 5. 根据接口路径检查是否存在
|
|
11
|
+
* 6. 存在则更新,不存在则新增
|
|
12
|
+
* 7. 删除配置中不存在的接口记录
|
|
13
|
+
*/
|
|
14
|
+
|
|
15
|
+
import { Logger } from '../lib/logger.js';
|
|
16
|
+
import { Database } from '../lib/database.js';
|
|
17
|
+
import { scanAddons, getAddonDir, addonDirExists } from '../util.js';
|
|
18
|
+
import { readdirSync, statSync } from 'node:fs';
|
|
19
|
+
import { join, dirname, relative, basename } from 'pathe';
|
|
20
|
+
|
|
21
|
+
interface SyncApiOptions {
|
|
22
|
+
plan?: boolean;
|
|
23
|
+
}
|
|
24
|
+
|
|
25
|
+
interface ApiInfo {
|
|
26
|
+
name: string;
|
|
27
|
+
path: string;
|
|
28
|
+
method: string;
|
|
29
|
+
description: string;
|
|
30
|
+
addonName: string;
|
|
31
|
+
addonTitle: string;
|
|
32
|
+
}
|
|
33
|
+
|
|
34
|
+
/**
|
|
35
|
+
* 递归扫描目录下的所有 .ts 文件
|
|
36
|
+
*/
|
|
37
|
+
function scanTsFiles(dir: string, fileList: string[] = []): string[] {
|
|
38
|
+
try {
|
|
39
|
+
const files = readdirSync(dir);
|
|
40
|
+
|
|
41
|
+
for (const file of files) {
|
|
42
|
+
const filePath = join(dir, file);
|
|
43
|
+
const stat = statSync(filePath);
|
|
44
|
+
|
|
45
|
+
if (stat.isDirectory()) {
|
|
46
|
+
scanTsFiles(filePath, fileList);
|
|
47
|
+
} else if (file.endsWith('.ts') && !file.endsWith('.d.ts')) {
|
|
48
|
+
fileList.push(filePath);
|
|
49
|
+
}
|
|
50
|
+
}
|
|
51
|
+
} catch (error: any) {
|
|
52
|
+
Logger.warn(`扫描目录失败: ${dir}`, error.message);
|
|
53
|
+
}
|
|
54
|
+
|
|
55
|
+
return fileList;
|
|
56
|
+
}
|
|
57
|
+
|
|
58
|
+
/**
|
|
59
|
+
* 从 API 文件中提取接口信息
|
|
60
|
+
*/
|
|
61
|
+
async function extractApiInfo(filePath: string, apiRoot: string, type: 'core' | 'app' | 'addon', addonName: string = '', addonTitle: string = ''): Promise<ApiInfo | null> {
|
|
62
|
+
try {
|
|
63
|
+
const apiModule = await import(filePath);
|
|
64
|
+
const apiConfig = apiModule.default;
|
|
65
|
+
|
|
66
|
+
if (!apiConfig || !apiConfig.name) {
|
|
67
|
+
return null;
|
|
68
|
+
}
|
|
69
|
+
|
|
70
|
+
let apiPath = '';
|
|
71
|
+
|
|
72
|
+
if (type === 'core') {
|
|
73
|
+
// Core 接口:保留完整目录层级
|
|
74
|
+
// 例: apis/menu/all.ts → /api/core/menu/all
|
|
75
|
+
const relativePath = relative(apiRoot, filePath);
|
|
76
|
+
const pathWithoutExt = relativePath.replace(/\.ts$/, '');
|
|
77
|
+
apiPath = `/api/core/${pathWithoutExt}`;
|
|
78
|
+
} else if (type === 'addon') {
|
|
79
|
+
// Addon 接口:保留完整目录层级
|
|
80
|
+
// 例: apis/menu/list.ts → /api/addon/admin/menu/list
|
|
81
|
+
const relativePath = relative(apiRoot, filePath);
|
|
82
|
+
const pathWithoutExt = relativePath.replace(/\.ts$/, '');
|
|
83
|
+
apiPath = `/api/addon/${addonName}/${pathWithoutExt}`;
|
|
84
|
+
} else {
|
|
85
|
+
// 项目接口:保留完整目录层级
|
|
86
|
+
// 例: apis/user/list.ts → /api/user/list
|
|
87
|
+
const relativePath = relative(apiRoot, filePath);
|
|
88
|
+
const pathWithoutExt = relativePath.replace(/\.ts$/, '');
|
|
89
|
+
apiPath = `/api/${pathWithoutExt}`;
|
|
90
|
+
}
|
|
91
|
+
|
|
92
|
+
return {
|
|
93
|
+
name: apiConfig.name || '',
|
|
94
|
+
path: apiPath,
|
|
95
|
+
method: apiConfig.method || 'POST',
|
|
96
|
+
description: apiConfig.description || '',
|
|
97
|
+
addonName: type === 'core' ? 'core' : addonName,
|
|
98
|
+
addonTitle: type === 'core' ? '核心接口' : addonTitle || addonName
|
|
99
|
+
};
|
|
100
|
+
} catch (error: any) {
|
|
101
|
+
Logger.error(`解析 API 文件失败: ${filePath}`, error);
|
|
102
|
+
return null;
|
|
103
|
+
}
|
|
104
|
+
}
|
|
105
|
+
|
|
106
|
+
/**
|
|
107
|
+
* 扫描所有 API 文件
|
|
108
|
+
*/
|
|
109
|
+
async function scanAllApis(projectRoot: string): Promise<ApiInfo[]> {
|
|
110
|
+
const apis: ApiInfo[] = [];
|
|
111
|
+
|
|
112
|
+
// 1. 扫描 Core 框架 API
|
|
113
|
+
Logger.info('=== 扫描 Core 框架 API (core/apis) ===');
|
|
114
|
+
const coreApisDir = join(dirname(projectRoot), 'core', 'apis');
|
|
115
|
+
try {
|
|
116
|
+
const coreApiFiles = scanTsFiles(coreApisDir);
|
|
117
|
+
Logger.info(` 找到 ${coreApiFiles.length} 个核心 API 文件`);
|
|
118
|
+
|
|
119
|
+
for (const filePath of coreApiFiles) {
|
|
120
|
+
const apiInfo = await extractApiInfo(filePath, coreApisDir, 'core', '', '核心接口');
|
|
121
|
+
if (apiInfo) {
|
|
122
|
+
apis.push(apiInfo);
|
|
123
|
+
Logger.info(` └ ${apiInfo.path} - ${apiInfo.name}`);
|
|
124
|
+
}
|
|
125
|
+
}
|
|
126
|
+
|
|
127
|
+
// 2. 扫描项目 API
|
|
128
|
+
Logger.info('\n=== 扫描项目 API (apis) ===');
|
|
129
|
+
const projectApisDir = join(projectRoot, 'apis');
|
|
130
|
+
const projectApiFiles = scanTsFiles(projectApisDir);
|
|
131
|
+
Logger.info(` 找到 ${projectApiFiles.length} 个项目 API 文件`);
|
|
132
|
+
|
|
133
|
+
for (const filePath of projectApiFiles) {
|
|
134
|
+
const apiInfo = await extractApiInfo(filePath, projectApisDir, 'app', '', '项目接口');
|
|
135
|
+
if (apiInfo) {
|
|
136
|
+
apis.push(apiInfo);
|
|
137
|
+
Logger.info(` └ ${apiInfo.path} - ${apiInfo.name}`);
|
|
138
|
+
}
|
|
139
|
+
}
|
|
140
|
+
|
|
141
|
+
// 3. 扫描组件 API (node_modules/@befly-addon/*)
|
|
142
|
+
Logger.info('\n=== 扫描组件 API (node_modules/@befly-addon/*) ===');
|
|
143
|
+
const addonNames = scanAddons();
|
|
144
|
+
|
|
145
|
+
for (const addonName of addonNames) {
|
|
146
|
+
// addonName 格式: admin, demo 等
|
|
147
|
+
|
|
148
|
+
// 检查 apis 子目录是否存在
|
|
149
|
+
if (!addonDirExists(addonName, 'apis')) {
|
|
150
|
+
Logger.info(` [${addonName}] 无 apis 目录,跳过`);
|
|
151
|
+
continue;
|
|
152
|
+
}
|
|
153
|
+
|
|
154
|
+
const addonApisDir = getAddonDir(addonName, 'apis');
|
|
155
|
+
|
|
156
|
+
// 读取 addon 配置
|
|
157
|
+
const addonConfigPath = getAddonDir(addonName, 'addon.config.json');
|
|
158
|
+
let addonTitle = addonName;
|
|
159
|
+
try {
|
|
160
|
+
const configFile = Bun.file(addonConfigPath);
|
|
161
|
+
const config = await configFile.json();
|
|
162
|
+
addonTitle = config.title || addonName;
|
|
163
|
+
} catch (error) {
|
|
164
|
+
Logger.debug(` [${addonName}] 无法读取配置文件,使用默认标题`);
|
|
165
|
+
}
|
|
166
|
+
|
|
167
|
+
const addonApiFiles = scanTsFiles(addonApisDir);
|
|
168
|
+
Logger.info(` [${addonName}] 找到 ${addonApiFiles.length} 个 API 文件`);
|
|
169
|
+
|
|
170
|
+
for (const filePath of addonApiFiles) {
|
|
171
|
+
const apiInfo = await extractApiInfo(filePath, addonApisDir, 'addon', addonName, addonTitle);
|
|
172
|
+
if (apiInfo) {
|
|
173
|
+
apis.push(apiInfo);
|
|
174
|
+
Logger.info(` └ ${apiInfo.path} - ${apiInfo.name}`);
|
|
175
|
+
}
|
|
176
|
+
}
|
|
177
|
+
}
|
|
178
|
+
|
|
179
|
+
return apis;
|
|
180
|
+
} catch (error: any) {
|
|
181
|
+
Logger.error(`接口扫描失败:`, error);
|
|
182
|
+
}
|
|
183
|
+
}
|
|
184
|
+
|
|
185
|
+
/**
|
|
186
|
+
* 同步 API 数据到数据库
|
|
187
|
+
*/
|
|
188
|
+
async function syncApis(helper: any, apis: ApiInfo[]): Promise<{ created: number; updated: number }> {
|
|
189
|
+
const stats = { created: 0, updated: 0 };
|
|
190
|
+
|
|
191
|
+
for (const api of apis) {
|
|
192
|
+
try {
|
|
193
|
+
const existing = await helper.getOne({
|
|
194
|
+
table: 'core_api',
|
|
195
|
+
where: { path: api.path }
|
|
196
|
+
});
|
|
197
|
+
|
|
198
|
+
if (existing) {
|
|
199
|
+
await helper.updData({
|
|
200
|
+
table: 'core_api',
|
|
201
|
+
where: { id: existing.id },
|
|
202
|
+
data: {
|
|
203
|
+
name: api.name,
|
|
204
|
+
method: api.method,
|
|
205
|
+
description: api.description,
|
|
206
|
+
addonName: api.addonName,
|
|
207
|
+
addonTitle: api.addonTitle
|
|
208
|
+
}
|
|
209
|
+
});
|
|
210
|
+
stats.updated++;
|
|
211
|
+
Logger.info(` └ 更新接口: ${api.name} (ID: ${existing.id}, Path: ${api.path})`);
|
|
212
|
+
} else {
|
|
213
|
+
const id = await helper.insData({
|
|
214
|
+
table: 'core_api',
|
|
215
|
+
data: {
|
|
216
|
+
name: api.name,
|
|
217
|
+
path: api.path,
|
|
218
|
+
method: api.method,
|
|
219
|
+
description: api.description,
|
|
220
|
+
addonName: api.addonName,
|
|
221
|
+
addonTitle: api.addonTitle
|
|
222
|
+
}
|
|
223
|
+
});
|
|
224
|
+
stats.created++;
|
|
225
|
+
Logger.info(` └ 新增接口: ${api.name} (ID: ${id}, Path: ${api.path})`);
|
|
226
|
+
}
|
|
227
|
+
} catch (error: any) {
|
|
228
|
+
Logger.error(`同步接口 "${api.name}" 失败:`, error);
|
|
229
|
+
}
|
|
230
|
+
}
|
|
231
|
+
|
|
232
|
+
return stats;
|
|
233
|
+
}
|
|
234
|
+
|
|
235
|
+
/**
|
|
236
|
+
* 删除配置中不存在的记录
|
|
237
|
+
*/
|
|
238
|
+
async function deleteObsoleteRecords(helper: any, apiPaths: Set<string>): Promise<number> {
|
|
239
|
+
Logger.info(`\n=== 删除配置中不存在的记录 ===`);
|
|
240
|
+
|
|
241
|
+
const allRecords = await helper.getAll({
|
|
242
|
+
table: 'core_api',
|
|
243
|
+
fields: ['id', 'path', 'name'],
|
|
244
|
+
where: { state$gte: 0 } // 查询所有状态(包括软删除的 state=0)
|
|
245
|
+
});
|
|
246
|
+
|
|
247
|
+
let deletedCount = 0;
|
|
248
|
+
for (const record of allRecords) {
|
|
249
|
+
if (record.path && !apiPaths.has(record.path)) {
|
|
250
|
+
await helper.delForce({
|
|
251
|
+
table: 'core_api',
|
|
252
|
+
where: { id: record.id }
|
|
253
|
+
});
|
|
254
|
+
deletedCount++;
|
|
255
|
+
Logger.info(` └ 删除记录: ${record.name} (ID: ${record.id}, path: ${record.path})`);
|
|
256
|
+
}
|
|
257
|
+
}
|
|
258
|
+
|
|
259
|
+
if (deletedCount === 0) {
|
|
260
|
+
Logger.info(' ✅ 无需删除的记录');
|
|
261
|
+
}
|
|
262
|
+
|
|
263
|
+
return deletedCount;
|
|
264
|
+
}
|
|
265
|
+
|
|
266
|
+
/**
|
|
267
|
+
* SyncApi 命令主函数
|
|
268
|
+
*/
|
|
269
|
+
export async function syncApiCommand(options: SyncApiOptions = {}) {
|
|
270
|
+
try {
|
|
271
|
+
if (options.plan) {
|
|
272
|
+
Logger.info('[计划] 同步 API 接口到数据库(plan 模式不执行)');
|
|
273
|
+
Logger.info('[计划] 1. 扫描 apis 和 addons/*/apis 目录');
|
|
274
|
+
Logger.info('[计划] 2. 提取每个 API 的配置信息');
|
|
275
|
+
Logger.info('[计划] 3. 根据 path 检查接口是否存在');
|
|
276
|
+
Logger.info('[计划] 4. 存在则更新,不存在则新增');
|
|
277
|
+
Logger.info('[计划] 5. 删除文件中不存在的接口记录');
|
|
278
|
+
return;
|
|
279
|
+
}
|
|
280
|
+
|
|
281
|
+
Logger.info('开始同步 API 接口到数据库...\n');
|
|
282
|
+
|
|
283
|
+
const projectRoot = process.cwd();
|
|
284
|
+
|
|
285
|
+
// 连接数据库(SQL + Redis)
|
|
286
|
+
await Database.connect();
|
|
287
|
+
|
|
288
|
+
const helper = Database.getDbHelper();
|
|
289
|
+
|
|
290
|
+
// 1. 检查表是否存在
|
|
291
|
+
Logger.info('=== 检查数据表 ===');
|
|
292
|
+
const exists = await helper.tableExists('core_api');
|
|
293
|
+
|
|
294
|
+
if (!exists) {
|
|
295
|
+
Logger.error(`❌ 表 core_api 不存在,请先运行 befly syncDb 同步数据库`);
|
|
296
|
+
process.exit(1);
|
|
297
|
+
}
|
|
298
|
+
|
|
299
|
+
Logger.info(`✅ 表 core_api 存在\n`);
|
|
300
|
+
|
|
301
|
+
// 2. 扫描所有 API 文件
|
|
302
|
+
Logger.info('=== 步骤 2: 扫描 API 文件 ===');
|
|
303
|
+
const apis = await scanAllApis(projectRoot);
|
|
304
|
+
const apiPaths = new Set(apis.map((api) => api.path));
|
|
305
|
+
Logger.info(`\n✅ 共扫描到 ${apis.length} 个 API 接口\n`);
|
|
306
|
+
|
|
307
|
+
// 3. 同步 API 数据
|
|
308
|
+
Logger.info('=== 步骤 3: 同步 API 数据(新增/更新) ===');
|
|
309
|
+
const stats = await syncApis(helper, apis);
|
|
310
|
+
|
|
311
|
+
// 4. 删除文件中不存在的接口
|
|
312
|
+
const deletedCount = await deleteObsoleteRecords(helper, apiPaths);
|
|
313
|
+
|
|
314
|
+
// 5. 输出统计信息
|
|
315
|
+
Logger.info(`\n=== 接口同步完成 ===`);
|
|
316
|
+
Logger.info(`新增接口: ${stats.created} 个`);
|
|
317
|
+
Logger.info(`更新接口: ${stats.updated} 个`);
|
|
318
|
+
Logger.info(`删除接口: ${deletedCount} 个`);
|
|
319
|
+
Logger.info(`当前总接口数: ${apis.length} 个`);
|
|
320
|
+
Logger.info('提示: 接口缓存将在服务器启动时自动完成');
|
|
321
|
+
} catch (error: any) {
|
|
322
|
+
Logger.error('API 同步失败:', error);
|
|
323
|
+
process.exit(1);
|
|
324
|
+
} finally {
|
|
325
|
+
await Database?.disconnect();
|
|
326
|
+
}
|
|
327
|
+
}
|
|
@@ -6,8 +6,8 @@
|
|
|
6
6
|
* - 应用表结构变更计划
|
|
7
7
|
*/
|
|
8
8
|
|
|
9
|
-
import { Logger } from '../../
|
|
10
|
-
import { parseRule } from '../../
|
|
9
|
+
import { Logger } from '../../lib/logger.js';
|
|
10
|
+
import { parseRule } from '../../util.js';
|
|
11
11
|
import { IS_MYSQL, IS_PG, IS_SQLITE, CHANGE_TYPE_LABELS, typeMapping } from './constants.js';
|
|
12
12
|
import { logFieldChange, resolveDefaultValue, isStringOrArrayType } from './helpers.js';
|
|
13
13
|
import { executeDDLSafely, buildIndexSQL } from './ddl.js';
|
|
@@ -2,13 +2,14 @@
|
|
|
2
2
|
* syncDb 常量定义模块
|
|
3
3
|
*
|
|
4
4
|
* 包含:
|
|
5
|
-
* -
|
|
5
|
+
* - 数据库类型判断
|
|
6
|
+
* - 版本要求
|
|
7
|
+
* - 数据类型映射
|
|
6
8
|
* - 系统字段定义
|
|
7
|
-
* - 索引字段配置
|
|
8
|
-
* - 字段变更类型标签
|
|
9
9
|
*/
|
|
10
10
|
|
|
11
11
|
import { Env } from '../../config/env.js';
|
|
12
|
+
import type { Dialect } from './types.js';
|
|
12
13
|
|
|
13
14
|
/**
|
|
14
15
|
* 数据库版本要求
|
|
@@ -47,12 +48,17 @@ export const CHANGE_TYPE_LABELS = {
|
|
|
47
48
|
} as const;
|
|
48
49
|
|
|
49
50
|
/**
|
|
50
|
-
* MySQL
|
|
51
|
+
* MySQL 表配置
|
|
52
|
+
*
|
|
53
|
+
* 固定配置说明:
|
|
54
|
+
* - ENGINE: InnoDB(支持事务、外键)
|
|
55
|
+
* - CHARSET: utf8mb4(完整 Unicode 支持,包括 Emoji)
|
|
56
|
+
* - COLLATE: utf8mb4_0900_ai_ci(MySQL 8.0 推荐,不区分重音和大小写)
|
|
51
57
|
*/
|
|
52
58
|
export const MYSQL_TABLE_CONFIG = {
|
|
53
|
-
ENGINE:
|
|
54
|
-
CHARSET:
|
|
55
|
-
COLLATE:
|
|
59
|
+
ENGINE: 'InnoDB',
|
|
60
|
+
CHARSET: 'utf8mb4',
|
|
61
|
+
COLLATE: 'utf8mb4_0900_ai_ci'
|
|
56
62
|
} as const;
|
|
57
63
|
|
|
58
64
|
// 数据库类型判断
|
|
@@ -8,12 +8,14 @@
|
|
|
8
8
|
* - 构建系统列和业务列定义
|
|
9
9
|
*/
|
|
10
10
|
|
|
11
|
-
import {
|
|
12
|
-
import { parseRule
|
|
13
|
-
import
|
|
11
|
+
import { snakeCase } from 'es-toolkit/string';
|
|
12
|
+
import { parseRule } from '../../util.js';
|
|
13
|
+
import { Logger } from '../../lib/logger.js';
|
|
14
14
|
import { IS_MYSQL, IS_PG, typeMapping } from './constants.js';
|
|
15
15
|
import { quoteIdentifier, resolveDefaultValue, generateDefaultSql, getSqlType, escapeComment } from './helpers.js';
|
|
16
|
+
|
|
16
17
|
import type { SQL } from 'bun';
|
|
18
|
+
import type { ParsedFieldRule, AnyObject } from 'befly/types/common.js';
|
|
17
19
|
|
|
18
20
|
/**
|
|
19
21
|
* 构建索引操作 SQL(统一使用在线策略)
|
|
@@ -80,7 +82,7 @@ export function buildBusinessColumnDefs(fields: Record<string, string>): string[
|
|
|
80
82
|
|
|
81
83
|
for (const [fieldKey, fieldRule] of Object.entries(fields)) {
|
|
82
84
|
// 转换字段名为下划线格式
|
|
83
|
-
const dbFieldName =
|
|
85
|
+
const dbFieldName = snakeCase(fieldKey);
|
|
84
86
|
|
|
85
87
|
const parsed = parseRule(fieldRule);
|
|
86
88
|
const { name: fieldName, type: fieldType, max: fieldMax, default: fieldDefault } = parsed;
|
|
@@ -110,7 +112,7 @@ export function buildBusinessColumnDefs(fields: Record<string, string>): string[
|
|
|
110
112
|
*/
|
|
111
113
|
export function generateDDLClause(fieldKey: string, fieldRule: string, isAdd: boolean = false): string {
|
|
112
114
|
// 转换字段名为下划线格式
|
|
113
|
-
const dbFieldName =
|
|
115
|
+
const dbFieldName = snakeCase(fieldKey);
|
|
114
116
|
|
|
115
117
|
const parsed = parseRule(fieldRule);
|
|
116
118
|
const { name: fieldName, type: fieldType, max: fieldMax, default: fieldDefault } = parsed;
|
|
@@ -1,16 +1,14 @@
|
|
|
1
1
|
/**
|
|
2
|
-
* syncDb
|
|
2
|
+
* syncDb 辅助工具模块
|
|
3
3
|
*
|
|
4
4
|
* 包含:
|
|
5
|
-
* -
|
|
6
|
-
* - 日志格式化
|
|
7
|
-
* - 类型判断工具
|
|
5
|
+
* - 标识符引用(反引号/双引号转义)
|
|
8
6
|
* - 默认值处理
|
|
7
|
+
* - 日志输出格式化
|
|
9
8
|
*/
|
|
10
9
|
|
|
11
10
|
import { IS_MYSQL, IS_PG, typeMapping } from './constants.js';
|
|
12
|
-
import {
|
|
13
|
-
import { Logger } from '../../utils/logger.js';
|
|
11
|
+
import { Logger } from '../../lib/logger.js';
|
|
14
12
|
|
|
15
13
|
/**
|
|
16
14
|
* 根据数据库类型引用标识符
|
|
@@ -86,7 +84,7 @@ export function generateDefaultSql(actualDefault: any, fieldType: 'number' | 'st
|
|
|
86
84
|
|
|
87
85
|
// 仅 number/string/array 类型设置默认值
|
|
88
86
|
if (fieldType === 'number' || fieldType === 'string' || fieldType === 'array') {
|
|
89
|
-
if (
|
|
87
|
+
if (typeof actualDefault === 'number' && !Number.isNaN(actualDefault)) {
|
|
90
88
|
return ` DEFAULT ${actualDefault}`;
|
|
91
89
|
} else {
|
|
92
90
|
// 字符串需要转义单引号:' -> ''
|
|
@@ -99,39 +97,41 @@ export function generateDefaultSql(actualDefault: any, fieldType: 'number' | 'st
|
|
|
99
97
|
}
|
|
100
98
|
|
|
101
99
|
/**
|
|
102
|
-
*
|
|
100
|
+
* 判断是否为字符串或数组类型(需要长度参数)
|
|
103
101
|
*
|
|
104
102
|
* @param fieldType - 字段类型
|
|
105
|
-
* @returns
|
|
103
|
+
* @returns 是否为字符串或数组类型
|
|
106
104
|
*
|
|
107
105
|
* @example
|
|
108
106
|
* isStringOrArrayType('string') // => true
|
|
109
|
-
* isStringOrArrayType('
|
|
107
|
+
* isStringOrArrayType('array_string') // => true
|
|
108
|
+
* isStringOrArrayType('array_text') // => false
|
|
110
109
|
* isStringOrArrayType('number') // => false
|
|
111
110
|
* isStringOrArrayType('text') // => false
|
|
112
111
|
*/
|
|
113
112
|
export function isStringOrArrayType(fieldType: string): boolean {
|
|
114
|
-
return fieldType === 'string' || fieldType === '
|
|
113
|
+
return fieldType === 'string' || fieldType === 'array_string';
|
|
115
114
|
}
|
|
116
115
|
|
|
117
116
|
/**
|
|
118
117
|
* 获取 SQL 数据类型
|
|
119
118
|
*
|
|
120
|
-
* @param fieldType - 字段类型(number/string/text/
|
|
121
|
-
* @param fieldMax - 最大长度(string/
|
|
119
|
+
* @param fieldType - 字段类型(number/string/text/array_string/array_text)
|
|
120
|
+
* @param fieldMax - 最大长度(string/array_string 类型需要)
|
|
122
121
|
* @returns SQL 类型字符串
|
|
123
122
|
*
|
|
124
123
|
* @example
|
|
125
124
|
* getSqlType('string', 100) // => 'VARCHAR(100)'
|
|
126
125
|
* getSqlType('number', null) // => 'BIGINT'
|
|
127
|
-
* getSqlType('text', null) // => '
|
|
128
|
-
* getSqlType('
|
|
126
|
+
* getSqlType('text', null) // => 'MEDIUMTEXT'
|
|
127
|
+
* getSqlType('array_string', 500) // => 'VARCHAR(500)'
|
|
128
|
+
* getSqlType('array_text', null) // => 'MEDIUMTEXT'
|
|
129
129
|
*/
|
|
130
130
|
export function getSqlType(fieldType: string, fieldMax: number | null): string {
|
|
131
131
|
if (isStringOrArrayType(fieldType)) {
|
|
132
132
|
return `${typeMapping[fieldType]}(${fieldMax})`;
|
|
133
133
|
}
|
|
134
|
-
return typeMapping[fieldType];
|
|
134
|
+
return typeMapping[fieldType] || 'TEXT';
|
|
135
135
|
}
|
|
136
136
|
|
|
137
137
|
/**
|
|
@@ -149,7 +149,7 @@ export function escapeComment(str: string): string {
|
|
|
149
149
|
}
|
|
150
150
|
|
|
151
151
|
/**
|
|
152
|
-
*
|
|
152
|
+
* 记录字段变更信息(紧凑格式)
|
|
153
153
|
*
|
|
154
154
|
* @param tableName - 表名
|
|
155
155
|
* @param fieldName - 字段名
|
|
@@ -159,7 +159,7 @@ export function escapeComment(str: string): string {
|
|
|
159
159
|
* @param changeLabel - 变更类型的中文标签
|
|
160
160
|
*/
|
|
161
161
|
export function logFieldChange(tableName: string, fieldName: string, changeType: string, oldValue: any, newValue: any, changeLabel: string): void {
|
|
162
|
-
Logger.info(`
|
|
162
|
+
Logger.info(` ~ 修改 ${fieldName} ${changeLabel}: ${oldValue} -> ${newValue}`);
|
|
163
163
|
}
|
|
164
164
|
|
|
165
165
|
/**
|
|
@@ -7,14 +7,14 @@
|
|
|
7
7
|
* - 提供统计信息和错误处理
|
|
8
8
|
*/
|
|
9
9
|
|
|
10
|
-
import
|
|
11
|
-
import {
|
|
10
|
+
import { basename } from 'pathe';
|
|
11
|
+
import { snakeCase } from 'es-toolkit/string';
|
|
12
|
+
import { Logger } from '../../lib/logger.js';
|
|
12
13
|
import { Env } from '../../config/env.js';
|
|
13
|
-
import {
|
|
14
|
-
import {
|
|
14
|
+
import { scanAddons, addonDirExists, getAddonDir } from '../../util.js';
|
|
15
|
+
import { Database } from '../../lib/database.js';
|
|
15
16
|
import checkTable from '../../checks/table.js';
|
|
16
17
|
import { paths } from '../../paths.js';
|
|
17
|
-
import { scanAddons, getAddonDir, addonDirExists } from '../../utils/helper.js';
|
|
18
18
|
|
|
19
19
|
// 导入模块化的功能
|
|
20
20
|
import { ensureDbVersion } from './version.js';
|
|
@@ -72,22 +72,27 @@ export const SyncDb = async (): Promise<void> => {
|
|
|
72
72
|
|
|
73
73
|
// 阶段2:建立数据库连接并检查版本
|
|
74
74
|
perfTracker.markPhase('数据库连接');
|
|
75
|
-
sql = await
|
|
75
|
+
sql = await Database.connectSql({ max: 1 });
|
|
76
76
|
await ensureDbVersion(sql);
|
|
77
77
|
Logger.info(`✓ 数据库连接建立,耗时: ${perfTracker.getPhaseTime('数据库连接')}`);
|
|
78
78
|
|
|
79
79
|
// 阶段3:扫描表定义文件
|
|
80
80
|
perfTracker.markPhase('扫描表文件');
|
|
81
81
|
const tablesGlob = new Bun.Glob('*.json');
|
|
82
|
-
const directories: Array<{ path: string;
|
|
83
|
-
|
|
84
|
-
|
|
82
|
+
const directories: Array<{ path: string; type: 'core' | 'app' | 'addon'; addonName?: string }> = [
|
|
83
|
+
// 1. core 框架表(core_ 前缀)
|
|
84
|
+
{ path: paths.coreTableDir, type: 'core' },
|
|
85
|
+
// 2. 项目表(无前缀)
|
|
86
|
+
{ path: paths.projectTableDir, type: 'app' }
|
|
87
|
+
];
|
|
88
|
+
|
|
89
|
+
// 添加所有 addon 的 tables 目录(addon_{name}_ 前缀)
|
|
85
90
|
const addons = scanAddons();
|
|
86
91
|
for (const addon of addons) {
|
|
87
92
|
if (addonDirExists(addon, 'tables')) {
|
|
88
93
|
directories.push({
|
|
89
94
|
path: getAddonDir(addon, 'tables'),
|
|
90
|
-
|
|
95
|
+
type: 'addon',
|
|
91
96
|
addonName: addon
|
|
92
97
|
});
|
|
93
98
|
}
|
|
@@ -101,7 +106,7 @@ export const SyncDb = async (): Promise<void> => {
|
|
|
101
106
|
absolute: true,
|
|
102
107
|
onlyFiles: true
|
|
103
108
|
})) {
|
|
104
|
-
const fileName =
|
|
109
|
+
const fileName = basename(file, '.json');
|
|
105
110
|
if (!fileName.startsWith('_')) {
|
|
106
111
|
totalTables++;
|
|
107
112
|
}
|
|
@@ -115,15 +120,15 @@ export const SyncDb = async (): Promise<void> => {
|
|
|
115
120
|
let processedCount = 0;
|
|
116
121
|
|
|
117
122
|
for (const dirConfig of directories) {
|
|
118
|
-
const { path: dir,
|
|
119
|
-
const dirType =
|
|
123
|
+
const { path: dir, type, addonName } = dirConfig;
|
|
124
|
+
const dirType = type === 'core' ? '核心' : type === 'addon' ? `组件${addonName}` : '项目';
|
|
120
125
|
|
|
121
126
|
for await (const file of tablesGlob.scan({
|
|
122
127
|
cwd: dir,
|
|
123
128
|
absolute: true,
|
|
124
129
|
onlyFiles: true
|
|
125
130
|
})) {
|
|
126
|
-
const fileName =
|
|
131
|
+
const fileName = basename(file, '.json');
|
|
127
132
|
|
|
128
133
|
// 跳过以下划线开头的文件(这些是公共字段规则,不是表定义)
|
|
129
134
|
if (fileName.startsWith('_')) {
|
|
@@ -131,17 +136,26 @@ export const SyncDb = async (): Promise<void> => {
|
|
|
131
136
|
continue;
|
|
132
137
|
}
|
|
133
138
|
|
|
134
|
-
//
|
|
135
|
-
// -
|
|
136
|
-
//
|
|
137
|
-
|
|
138
|
-
|
|
139
|
-
|
|
139
|
+
// 确定表名:
|
|
140
|
+
// - core 表:core_{表名}
|
|
141
|
+
// 例如:user.json → core_user
|
|
142
|
+
// - addon 表:{addonName}_{表名}
|
|
143
|
+
// 例如:admin addon 的 user.json → admin_user
|
|
144
|
+
// - 项目表:{表名}
|
|
145
|
+
// 例如:user.json → user
|
|
146
|
+
let tableName = snakeCase(fileName);
|
|
147
|
+
if (type === 'core') {
|
|
148
|
+
// core 框架表,添加 core_ 前缀
|
|
149
|
+
tableName = `core_${tableName}`;
|
|
150
|
+
} else if (type === 'addon') {
|
|
151
|
+
// addon 表,添加 {addonName}_ 前缀
|
|
152
|
+
// 使用 snakeCase 统一转换(admin → admin)
|
|
153
|
+
const addonNameSnake = snakeCase(addonName!);
|
|
154
|
+
tableName = `${addonNameSnake}_${tableName}`;
|
|
140
155
|
}
|
|
141
156
|
|
|
142
157
|
processedCount++;
|
|
143
|
-
progressLogger.logTableProgress(processedCount, totalTables, tableName);
|
|
144
|
-
Logger.info(` 类型: ${dirType}`);
|
|
158
|
+
progressLogger.logTableProgress(processedCount, totalTables, tableName, dirType);
|
|
145
159
|
|
|
146
160
|
const tableDefinition = await Bun.file(file).json();
|
|
147
161
|
const existsTable = await tableExists(sql!, tableName);
|
|
@@ -186,7 +200,7 @@ export const SyncDb = async (): Promise<void> => {
|
|
|
186
200
|
} finally {
|
|
187
201
|
if (sql) {
|
|
188
202
|
try {
|
|
189
|
-
await
|
|
203
|
+
await Database.disconnectSql();
|
|
190
204
|
} catch (error: any) {
|
|
191
205
|
Logger.warn('关闭数据库连接时出错:', error.message);
|
|
192
206
|
}
|
|
@@ -6,7 +6,8 @@
|
|
|
6
6
|
* - 进度信息记录
|
|
7
7
|
*/
|
|
8
8
|
|
|
9
|
-
import {
|
|
9
|
+
import type { SQL } from 'bun';
|
|
10
|
+
import { Logger } from '../../lib/logger.js';
|
|
10
11
|
|
|
11
12
|
/**
|
|
12
13
|
* 阶段统计信息
|
|
@@ -84,10 +85,15 @@ export class PerformanceTracker {
|
|
|
84
85
|
*/
|
|
85
86
|
export class ProgressLogger {
|
|
86
87
|
/**
|
|
87
|
-
*
|
|
88
|
+
* 记录表处理进度(紧凑格式)
|
|
89
|
+
* @param current 当前进度
|
|
90
|
+
* @param total 总数
|
|
91
|
+
* @param tableName 表名
|
|
92
|
+
* @param dirType 目录类型(核心/项目/组件名)
|
|
88
93
|
*/
|
|
89
|
-
logTableProgress(current: number, total: number, tableName: string): void {
|
|
90
|
-
|
|
94
|
+
logTableProgress(current: number, total: number, tableName: string, dirType?: string): void {
|
|
95
|
+
const typeInfo = dirType ? ` (${dirType})` : '';
|
|
96
|
+
Logger.info(`[${current}/${total}] ${tableName}${typeInfo}`);
|
|
91
97
|
}
|
|
92
98
|
|
|
93
99
|
/**
|