befly 3.5.7 → 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/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/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/commands/sync.ts
DELETED
|
@@ -1,88 +0,0 @@
|
|
|
1
|
-
/**
|
|
2
|
-
* Sync 命令 - 一次性执行所有同步操作
|
|
3
|
-
* 按顺序执行:syncDb → syncApi → syncMenu → syncDev
|
|
4
|
-
*/
|
|
5
|
-
|
|
6
|
-
import { Logger } from '../lib/logger.js';
|
|
7
|
-
import { Env } from '../env.js';
|
|
8
|
-
import { syncDbCommand, type SyncDbStats } from './syncDb.js';
|
|
9
|
-
import { syncApiCommand, type SyncApiStats } from './syncApi.js';
|
|
10
|
-
import { syncMenuCommand, type SyncMenuStats } from './syncMenu.js';
|
|
11
|
-
import { syncDevCommand, type SyncDevStats } from './syncDev.js';
|
|
12
|
-
import { existsSync, mkdirSync } from 'node:fs';
|
|
13
|
-
|
|
14
|
-
interface SyncOptions {}
|
|
15
|
-
|
|
16
|
-
export async function syncCommand(options: SyncOptions = {}) {
|
|
17
|
-
try {
|
|
18
|
-
const startTime = Date.now();
|
|
19
|
-
|
|
20
|
-
// 确保 logs 目录存在
|
|
21
|
-
if (!existsSync('./logs')) {
|
|
22
|
-
mkdirSync('./logs', { recursive: true });
|
|
23
|
-
}
|
|
24
|
-
|
|
25
|
-
// 1. 同步数据库表结构
|
|
26
|
-
const dbStats = await syncDbCommand({ dryRun: false });
|
|
27
|
-
|
|
28
|
-
// 2. 同步接口(并缓存)
|
|
29
|
-
const apiStats = await syncApiCommand();
|
|
30
|
-
|
|
31
|
-
// 3. 同步菜单(并缓存)
|
|
32
|
-
const menuStats = await syncMenuCommand();
|
|
33
|
-
|
|
34
|
-
// 4. 同步开发管理员(并缓存角色权限)
|
|
35
|
-
const devStats = await syncDevCommand();
|
|
36
|
-
|
|
37
|
-
// 输出总结
|
|
38
|
-
const totalTime = ((Date.now() - startTime) / 1000).toFixed(2);
|
|
39
|
-
Logger.info(`总耗时: ${totalTime} 秒`);
|
|
40
|
-
|
|
41
|
-
console.log(
|
|
42
|
-
Bun.inspect.table([
|
|
43
|
-
{ 项目: '处理表数', 数量: dbStats.processedTables },
|
|
44
|
-
{ 项目: '创建表', 数量: dbStats.createdTables },
|
|
45
|
-
{ 项目: '修改表', 数量: dbStats.modifiedTables },
|
|
46
|
-
{ 项目: '新增字段', 数量: dbStats.addFields },
|
|
47
|
-
{ 项目: '字段名称变更', 数量: dbStats.nameChanges },
|
|
48
|
-
{ 项目: '字段类型变更', 数量: dbStats.typeChanges },
|
|
49
|
-
{ 项目: '索引新增', 数量: dbStats.indexCreate },
|
|
50
|
-
{ 项目: '索引删除', 数量: dbStats.indexDrop }
|
|
51
|
-
])
|
|
52
|
-
);
|
|
53
|
-
|
|
54
|
-
Logger.info('\n📊 接口同步统计');
|
|
55
|
-
console.log(
|
|
56
|
-
Bun.inspect.table([
|
|
57
|
-
{ 项目: '总接口数', 数量: apiStats.totalApis },
|
|
58
|
-
{ 项目: '新增接口', 数量: apiStats.created },
|
|
59
|
-
{ 项目: '更新接口', 数量: apiStats.updated },
|
|
60
|
-
{ 项目: '删除接口', 数量: apiStats.deleted }
|
|
61
|
-
])
|
|
62
|
-
);
|
|
63
|
-
|
|
64
|
-
Logger.info('\n📊 菜单同步统计');
|
|
65
|
-
console.log(
|
|
66
|
-
Bun.inspect.table([
|
|
67
|
-
{ 项目: '总菜单数', 数量: menuStats.totalMenus },
|
|
68
|
-
{ 项目: '父级菜单', 数量: menuStats.parentMenus },
|
|
69
|
-
{ 项目: '子级菜单', 数量: menuStats.childMenus },
|
|
70
|
-
{ 项目: '新增菜单', 数量: menuStats.created },
|
|
71
|
-
{ 项目: '更新菜单', 数量: menuStats.updated },
|
|
72
|
-
{ 项目: '删除菜单', 数量: menuStats.deleted }
|
|
73
|
-
])
|
|
74
|
-
);
|
|
75
|
-
|
|
76
|
-
Logger.info('\n📊 开发账号同步统计');
|
|
77
|
-
console.log(
|
|
78
|
-
Bun.inspect.table([
|
|
79
|
-
{ 项目: '管理员数量', 数量: devStats.adminCount },
|
|
80
|
-
{ 项目: '角色数量', 数量: devStats.roleCount },
|
|
81
|
-
{ 项目: '缓存角色数', 数量: devStats.cachedRoles }
|
|
82
|
-
])
|
|
83
|
-
);
|
|
84
|
-
} catch (error: any) {
|
|
85
|
-
Logger.error('同步过程中发生错误:', error);
|
|
86
|
-
process.exit(1);
|
|
87
|
-
}
|
|
88
|
-
}
|
package/commands/syncApi.ts
DELETED
|
@@ -1,316 +0,0 @@
|
|
|
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 { RedisHelper } from '../lib/redisHelper.js';
|
|
18
|
-
import { scanAddons, getAddonDir, addonDirExists } from '../util.js';
|
|
19
|
-
import { coreApiDir, projectApiDir } from '../paths.js';
|
|
20
|
-
import { readdirSync, statSync } from 'node:fs';
|
|
21
|
-
import { join, dirname, relative, basename } from 'pathe';
|
|
22
|
-
|
|
23
|
-
interface SyncApiOptions {
|
|
24
|
-
plan?: boolean;
|
|
25
|
-
}
|
|
26
|
-
|
|
27
|
-
interface ApiInfo {
|
|
28
|
-
name: string;
|
|
29
|
-
path: string;
|
|
30
|
-
method: string;
|
|
31
|
-
description: string;
|
|
32
|
-
addonName: string;
|
|
33
|
-
addonTitle: string;
|
|
34
|
-
}
|
|
35
|
-
|
|
36
|
-
export interface SyncApiStats {
|
|
37
|
-
totalApis: number;
|
|
38
|
-
created: number;
|
|
39
|
-
updated: number;
|
|
40
|
-
deleted: number;
|
|
41
|
-
}
|
|
42
|
-
|
|
43
|
-
/**
|
|
44
|
-
* 递归扫描目录下的所有 .ts 文件
|
|
45
|
-
*/
|
|
46
|
-
function scanTsFiles(dir: string, fileList: string[] = []): string[] {
|
|
47
|
-
try {
|
|
48
|
-
const files = readdirSync(dir);
|
|
49
|
-
|
|
50
|
-
for (const file of files) {
|
|
51
|
-
const filePath = join(dir, file);
|
|
52
|
-
const stat = statSync(filePath);
|
|
53
|
-
|
|
54
|
-
if (stat.isDirectory()) {
|
|
55
|
-
scanTsFiles(filePath, fileList);
|
|
56
|
-
} else if (file.endsWith('.ts') && !file.endsWith('.d.ts')) {
|
|
57
|
-
fileList.push(filePath);
|
|
58
|
-
}
|
|
59
|
-
}
|
|
60
|
-
} catch (error: any) {
|
|
61
|
-
Logger.warn(`扫描目录失败: ${dir}`, error.message);
|
|
62
|
-
}
|
|
63
|
-
|
|
64
|
-
return fileList;
|
|
65
|
-
}
|
|
66
|
-
|
|
67
|
-
/**
|
|
68
|
-
* 从 API 文件中提取接口信息
|
|
69
|
-
*/
|
|
70
|
-
async function extractApiInfo(filePath: string, apiRoot: string, type: 'core' | 'app' | 'addon', addonName: string = '', addonTitle: string = ''): Promise<ApiInfo | null> {
|
|
71
|
-
try {
|
|
72
|
-
const apiModule = await import(filePath);
|
|
73
|
-
const apiConfig = apiModule.default;
|
|
74
|
-
|
|
75
|
-
if (!apiConfig || !apiConfig.name) {
|
|
76
|
-
return null;
|
|
77
|
-
}
|
|
78
|
-
|
|
79
|
-
let apiPath = '';
|
|
80
|
-
|
|
81
|
-
if (type === 'core') {
|
|
82
|
-
// Core 接口:保留完整目录层级
|
|
83
|
-
// 例: apis/menu/all.ts → /api/core/menu/all
|
|
84
|
-
const relativePath = relative(apiRoot, filePath);
|
|
85
|
-
const pathWithoutExt = relativePath.replace(/\.ts$/, '');
|
|
86
|
-
apiPath = `/api/core/${pathWithoutExt}`;
|
|
87
|
-
} else if (type === 'addon') {
|
|
88
|
-
// Addon 接口:保留完整目录层级
|
|
89
|
-
// 例: apis/menu/list.ts → /api/addon/admin/menu/list
|
|
90
|
-
const relativePath = relative(apiRoot, filePath);
|
|
91
|
-
const pathWithoutExt = relativePath.replace(/\.ts$/, '');
|
|
92
|
-
apiPath = `/api/addon/${addonName}/${pathWithoutExt}`;
|
|
93
|
-
} else {
|
|
94
|
-
// 项目接口:保留完整目录层级
|
|
95
|
-
// 例: apis/user/list.ts → /api/user/list
|
|
96
|
-
const relativePath = relative(apiRoot, filePath);
|
|
97
|
-
const pathWithoutExt = relativePath.replace(/\.ts$/, '');
|
|
98
|
-
apiPath = `/api/${pathWithoutExt}`;
|
|
99
|
-
}
|
|
100
|
-
|
|
101
|
-
return {
|
|
102
|
-
name: apiConfig.name || '',
|
|
103
|
-
path: apiPath,
|
|
104
|
-
method: apiConfig.method || 'POST',
|
|
105
|
-
description: apiConfig.description || '',
|
|
106
|
-
addonName: type === 'core' ? 'core' : addonName,
|
|
107
|
-
addonTitle: type === 'core' ? '核心接口' : addonTitle || addonName
|
|
108
|
-
};
|
|
109
|
-
} catch (error: any) {
|
|
110
|
-
Logger.error(`解析 API 文件失败: ${filePath}`, error);
|
|
111
|
-
return null;
|
|
112
|
-
}
|
|
113
|
-
}
|
|
114
|
-
|
|
115
|
-
/**
|
|
116
|
-
* 扫描所有 API 文件
|
|
117
|
-
*/
|
|
118
|
-
async function scanAllApis(projectRoot: string): Promise<ApiInfo[]> {
|
|
119
|
-
const apis: ApiInfo[] = [];
|
|
120
|
-
|
|
121
|
-
// 1. 扫描 Core 框架 API
|
|
122
|
-
try {
|
|
123
|
-
const coreApiFiles = scanTsFiles(coreApiDir);
|
|
124
|
-
|
|
125
|
-
for (const filePath of coreApiFiles) {
|
|
126
|
-
const apiInfo = await extractApiInfo(filePath, coreApiDir, 'core', '', '核心接口');
|
|
127
|
-
if (apiInfo) {
|
|
128
|
-
apis.push(apiInfo);
|
|
129
|
-
}
|
|
130
|
-
}
|
|
131
|
-
|
|
132
|
-
// 2. 扫描项目 API
|
|
133
|
-
const projectApiFiles = scanTsFiles(projectApiDir);
|
|
134
|
-
|
|
135
|
-
for (const filePath of projectApiFiles) {
|
|
136
|
-
const apiInfo = await extractApiInfo(filePath, projectApiDir, 'app', '', '项目接口');
|
|
137
|
-
if (apiInfo) {
|
|
138
|
-
apis.push(apiInfo);
|
|
139
|
-
}
|
|
140
|
-
}
|
|
141
|
-
|
|
142
|
-
// 3. 扫描组件 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
|
-
continue;
|
|
151
|
-
}
|
|
152
|
-
|
|
153
|
-
const addonApisDir = getAddonDir(addonName, 'apis');
|
|
154
|
-
|
|
155
|
-
// 读取 addon 配置
|
|
156
|
-
const addonConfigPath = getAddonDir(addonName, 'addon.config.json');
|
|
157
|
-
let addonTitle = addonName;
|
|
158
|
-
try {
|
|
159
|
-
const configFile = Bun.file(addonConfigPath);
|
|
160
|
-
const config = await configFile.json();
|
|
161
|
-
addonTitle = config.title || addonName;
|
|
162
|
-
} catch (error) {
|
|
163
|
-
// 忽略配置读取错误
|
|
164
|
-
}
|
|
165
|
-
|
|
166
|
-
const addonApiFiles = scanTsFiles(addonApisDir);
|
|
167
|
-
|
|
168
|
-
for (const filePath of addonApiFiles) {
|
|
169
|
-
const apiInfo = await extractApiInfo(filePath, addonApisDir, 'addon', addonName, addonTitle);
|
|
170
|
-
if (apiInfo) {
|
|
171
|
-
apis.push(apiInfo);
|
|
172
|
-
}
|
|
173
|
-
}
|
|
174
|
-
}
|
|
175
|
-
|
|
176
|
-
return apis;
|
|
177
|
-
} catch (error: any) {
|
|
178
|
-
Logger.error(`接口扫描失败:`, error);
|
|
179
|
-
return apis;
|
|
180
|
-
}
|
|
181
|
-
}
|
|
182
|
-
|
|
183
|
-
/**
|
|
184
|
-
* 同步 API 数据到数据库
|
|
185
|
-
*/
|
|
186
|
-
async function syncApis(helper: any, apis: ApiInfo[]): Promise<{ created: number; updated: number }> {
|
|
187
|
-
const stats = { created: 0, updated: 0 };
|
|
188
|
-
|
|
189
|
-
for (const api of apis) {
|
|
190
|
-
try {
|
|
191
|
-
// 根据 path 查询是否存在
|
|
192
|
-
const existing = await helper.getOne({
|
|
193
|
-
table: 'core_api',
|
|
194
|
-
where: { path: api.path }
|
|
195
|
-
});
|
|
196
|
-
|
|
197
|
-
if (existing) {
|
|
198
|
-
await helper.updData({
|
|
199
|
-
table: 'core_api',
|
|
200
|
-
where: { id: existing.id },
|
|
201
|
-
data: {
|
|
202
|
-
name: api.name,
|
|
203
|
-
method: api.method,
|
|
204
|
-
description: api.description,
|
|
205
|
-
addonName: api.addonName,
|
|
206
|
-
addonTitle: api.addonTitle
|
|
207
|
-
}
|
|
208
|
-
});
|
|
209
|
-
stats.updated++;
|
|
210
|
-
} else {
|
|
211
|
-
const id = await helper.insData({
|
|
212
|
-
table: 'core_api',
|
|
213
|
-
data: {
|
|
214
|
-
name: api.name,
|
|
215
|
-
path: api.path,
|
|
216
|
-
method: api.method,
|
|
217
|
-
description: api.description,
|
|
218
|
-
addonName: api.addonName,
|
|
219
|
-
addonTitle: api.addonTitle
|
|
220
|
-
}
|
|
221
|
-
});
|
|
222
|
-
stats.created++;
|
|
223
|
-
}
|
|
224
|
-
} catch (error: any) {
|
|
225
|
-
Logger.error(`同步接口 "${api.name}" 失败:`, error);
|
|
226
|
-
}
|
|
227
|
-
}
|
|
228
|
-
|
|
229
|
-
return stats;
|
|
230
|
-
}
|
|
231
|
-
|
|
232
|
-
/**
|
|
233
|
-
* 删除配置中不存在的记录
|
|
234
|
-
*/
|
|
235
|
-
async function deleteObsoleteRecords(helper: any, apiPaths: Set<string>): Promise<number> {
|
|
236
|
-
const allRecords = await helper.getAll({
|
|
237
|
-
table: 'core_api',
|
|
238
|
-
fields: ['id', 'path', 'name'],
|
|
239
|
-
where: { state$gte: 0 } // 查询所有状态(包括软删除的 state=0)
|
|
240
|
-
});
|
|
241
|
-
|
|
242
|
-
let deletedCount = 0;
|
|
243
|
-
for (const record of allRecords) {
|
|
244
|
-
if (record.path && !apiPaths.has(record.path)) {
|
|
245
|
-
await helper.delForce({
|
|
246
|
-
table: 'core_api',
|
|
247
|
-
where: { id: record.id }
|
|
248
|
-
});
|
|
249
|
-
deletedCount++;
|
|
250
|
-
}
|
|
251
|
-
}
|
|
252
|
-
|
|
253
|
-
return deletedCount;
|
|
254
|
-
}
|
|
255
|
-
|
|
256
|
-
/**
|
|
257
|
-
* SyncApi 命令主函数
|
|
258
|
-
*/
|
|
259
|
-
export async function syncApiCommand(options: SyncApiOptions = {}): Promise<SyncApiStats> {
|
|
260
|
-
try {
|
|
261
|
-
if (options.plan) {
|
|
262
|
-
Logger.info('[计划] 同步 API 接口到数据库(plan 模式不执行)');
|
|
263
|
-
return { totalApis: 0, created: 0, updated: 0, deleted: 0 };
|
|
264
|
-
}
|
|
265
|
-
|
|
266
|
-
const projectRoot = process.cwd();
|
|
267
|
-
|
|
268
|
-
// 连接数据库(SQL + Redis)
|
|
269
|
-
await Database.connect();
|
|
270
|
-
|
|
271
|
-
const helper = Database.getDbHelper();
|
|
272
|
-
|
|
273
|
-
// 1. 检查表是否存在
|
|
274
|
-
const exists = await helper.tableExists('core_api');
|
|
275
|
-
|
|
276
|
-
if (!exists) {
|
|
277
|
-
Logger.error(`❌ 表 core_api 不存在,请先运行 befly syncDb 同步数据库`);
|
|
278
|
-
process.exit(1);
|
|
279
|
-
}
|
|
280
|
-
|
|
281
|
-
// 2. 扫描所有 API 文件
|
|
282
|
-
const apis = await scanAllApis(projectRoot);
|
|
283
|
-
const apiPaths = new Set(apis.map((api) => api.path));
|
|
284
|
-
|
|
285
|
-
// 3. 同步 API 数据
|
|
286
|
-
const stats = await syncApis(helper, apis);
|
|
287
|
-
|
|
288
|
-
// 4. 删除文件中不存在的接口
|
|
289
|
-
const deletedCount = await deleteObsoleteRecords(helper, apiPaths);
|
|
290
|
-
|
|
291
|
-
// 5. 缓存接口数据到 Redis
|
|
292
|
-
try {
|
|
293
|
-
const apiList = await helper.getAll({
|
|
294
|
-
table: 'core_api',
|
|
295
|
-
fields: ['id', 'name', 'path', 'method', 'description', 'addonName', 'addonTitle'],
|
|
296
|
-
orderBy: ['addonName#ASC', 'path#ASC']
|
|
297
|
-
});
|
|
298
|
-
|
|
299
|
-
await RedisHelper.setObject('apis:all', apiList);
|
|
300
|
-
} catch (error: any) {
|
|
301
|
-
// 忽略缓存错误
|
|
302
|
-
}
|
|
303
|
-
|
|
304
|
-
return {
|
|
305
|
-
totalApis: apis.length,
|
|
306
|
-
created: stats.created,
|
|
307
|
-
updated: stats.updated,
|
|
308
|
-
deleted: deletedCount
|
|
309
|
-
};
|
|
310
|
-
} catch (error: any) {
|
|
311
|
-
Logger.error('API 同步失败:', error);
|
|
312
|
-
process.exit(1);
|
|
313
|
-
} finally {
|
|
314
|
-
await Database?.disconnect();
|
|
315
|
-
}
|
|
316
|
-
}
|
package/commands/syncDb/apply.ts
DELETED
|
@@ -1,171 +0,0 @@
|
|
|
1
|
-
/**
|
|
2
|
-
* syncDb 变更应用模块
|
|
3
|
-
*
|
|
4
|
-
* 包含:
|
|
5
|
-
* - 比较字段定义变化
|
|
6
|
-
* - 应用表结构变更计划
|
|
7
|
-
*/
|
|
8
|
-
|
|
9
|
-
import { Logger } from '../../lib/logger.js';
|
|
10
|
-
import { parseRule } from '../../util.js';
|
|
11
|
-
import { IS_MYSQL, IS_PG, IS_SQLITE, CHANGE_TYPE_LABELS, typeMapping } from './constants.js';
|
|
12
|
-
import { logFieldChange, resolveDefaultValue, isStringOrArrayType } from './helpers.js';
|
|
13
|
-
import { executeDDLSafely, buildIndexSQL } from './ddl.js';
|
|
14
|
-
import { rebuildSqliteTable } from './sqlite.js';
|
|
15
|
-
import type { FieldChange, IndexAction, TablePlan, ColumnInfo } from './types.js';
|
|
16
|
-
import type { SQL } from 'bun';
|
|
17
|
-
|
|
18
|
-
// 是否为计划模式(从环境变量读取)
|
|
19
|
-
const IS_PLAN = process.argv.includes('--plan');
|
|
20
|
-
|
|
21
|
-
/**
|
|
22
|
-
* 比较字段定义变化
|
|
23
|
-
*
|
|
24
|
-
* 对比现有列信息和新的字段规则,识别变化类型:
|
|
25
|
-
* - 长度变化(string/array 类型)
|
|
26
|
-
* - 注释变化(MySQL/PG)
|
|
27
|
-
* - 数据类型变化
|
|
28
|
-
* - 默认值变化
|
|
29
|
-
*
|
|
30
|
-
* @param existingColumn - 现有列信息
|
|
31
|
-
* @param newRule - 新的字段规则字符串
|
|
32
|
-
* @param colName - 列名(未使用,保留参数兼容性)
|
|
33
|
-
* @returns 变化数组
|
|
34
|
-
*/
|
|
35
|
-
export function compareFieldDefinition(existingColumn: ColumnInfo, newRule: string, colName: string): FieldChange[] {
|
|
36
|
-
const parsed = parseRule(newRule);
|
|
37
|
-
const { name: fieldName, type: fieldType, max: fieldMax, default: fieldDefault } = parsed;
|
|
38
|
-
const changes: FieldChange[] = [];
|
|
39
|
-
|
|
40
|
-
// 检查长度变化(string和array类型) - SQLite 不比较长度
|
|
41
|
-
if (!IS_SQLITE && isStringOrArrayType(fieldType)) {
|
|
42
|
-
if (existingColumn.length !== fieldMax) {
|
|
43
|
-
changes.push({
|
|
44
|
-
type: 'length',
|
|
45
|
-
current: existingColumn.length,
|
|
46
|
-
expected: fieldMax
|
|
47
|
-
});
|
|
48
|
-
}
|
|
49
|
-
}
|
|
50
|
-
|
|
51
|
-
// 检查注释变化(MySQL/PG 支持列注释)
|
|
52
|
-
if (!IS_SQLITE) {
|
|
53
|
-
const currentComment = existingColumn.comment || '';
|
|
54
|
-
if (currentComment !== fieldName) {
|
|
55
|
-
changes.push({
|
|
56
|
-
type: 'comment',
|
|
57
|
-
current: currentComment,
|
|
58
|
-
expected: fieldName
|
|
59
|
-
});
|
|
60
|
-
}
|
|
61
|
-
}
|
|
62
|
-
|
|
63
|
-
// 检查数据类型变化(按方言)
|
|
64
|
-
if (existingColumn.type.toLowerCase() !== typeMapping[fieldType].toLowerCase()) {
|
|
65
|
-
changes.push({
|
|
66
|
-
type: 'datatype',
|
|
67
|
-
current: existingColumn.type,
|
|
68
|
-
expected: typeMapping[fieldType].toLowerCase()
|
|
69
|
-
});
|
|
70
|
-
}
|
|
71
|
-
|
|
72
|
-
// 使用公共函数处理默认值
|
|
73
|
-
const expectedDefault = resolveDefaultValue(fieldDefault, fieldType);
|
|
74
|
-
|
|
75
|
-
// 检查默认值变化
|
|
76
|
-
if (String(existingColumn.defaultValue) !== String(expectedDefault)) {
|
|
77
|
-
changes.push({
|
|
78
|
-
type: 'default',
|
|
79
|
-
current: existingColumn.defaultValue,
|
|
80
|
-
expected: expectedDefault
|
|
81
|
-
});
|
|
82
|
-
}
|
|
83
|
-
|
|
84
|
-
return changes;
|
|
85
|
-
}
|
|
86
|
-
|
|
87
|
-
/**
|
|
88
|
-
* 将表结构计划应用到数据库(执行 DDL/索引/注释等)
|
|
89
|
-
*
|
|
90
|
-
* 根据数据库方言和计划内容,执行相应的 DDL 操作:
|
|
91
|
-
* - SQLite: 新增字段直接 ALTER,其他操作需要重建表
|
|
92
|
-
* - MySQL: 尝试在线 DDL(INSTANT/INPLACE)
|
|
93
|
-
* - PostgreSQL: 直接 ALTER
|
|
94
|
-
*
|
|
95
|
-
* @param sql - SQL 客户端实例
|
|
96
|
-
* @param tableName - 表名
|
|
97
|
-
* @param fields - 字段定义对象
|
|
98
|
-
* @param plan - 表结构变更计划
|
|
99
|
-
* @param globalCount - 全局统计对象(用于计数)
|
|
100
|
-
*/
|
|
101
|
-
export async function applyTablePlan(sql: SQL, tableName: string, fields: Record<string, string>, plan: TablePlan, globalCount: Record<string, number>): Promise<void> {
|
|
102
|
-
if (!plan || !plan.changed) return;
|
|
103
|
-
|
|
104
|
-
// SQLite: 仅支持部分 ALTER;需要时走重建
|
|
105
|
-
if (IS_SQLITE) {
|
|
106
|
-
if (plan.modifyClauses.length > 0 || plan.defaultClauses.length > 0) {
|
|
107
|
-
if (IS_PLAN) Logger.info(`[计划] 重建表 ${tableName} 以应用列修改/默认值变化`);
|
|
108
|
-
else await rebuildSqliteTable(sql, tableName, fields);
|
|
109
|
-
} else {
|
|
110
|
-
for (const c of plan.addClauses) {
|
|
111
|
-
const stmt = `ALTER TABLE "${tableName}" ${c}`;
|
|
112
|
-
if (IS_PLAN) Logger.info(`[计划] ${stmt}`);
|
|
113
|
-
else await sql.unsafe(stmt);
|
|
114
|
-
}
|
|
115
|
-
}
|
|
116
|
-
} else {
|
|
117
|
-
const clauses = [...plan.addClauses, ...plan.modifyClauses];
|
|
118
|
-
if (clauses.length > 0) {
|
|
119
|
-
const suffix = IS_MYSQL ? ', ALGORITHM=INSTANT, LOCK=NONE' : '';
|
|
120
|
-
const stmt = IS_MYSQL ? `ALTER TABLE \`${tableName}\` ${clauses.join(', ')}${suffix}` : `ALTER TABLE "${tableName}" ${clauses.join(', ')}`;
|
|
121
|
-
if (IS_PLAN) Logger.info(`[计划] ${stmt}`);
|
|
122
|
-
else if (IS_MYSQL) await executeDDLSafely(sql, stmt);
|
|
123
|
-
else await sql.unsafe(stmt);
|
|
124
|
-
}
|
|
125
|
-
}
|
|
126
|
-
|
|
127
|
-
// 默认值专用 ALTER(SQLite 不支持)
|
|
128
|
-
if (plan.defaultClauses.length > 0) {
|
|
129
|
-
if (IS_SQLITE) {
|
|
130
|
-
Logger.warn(`SQLite 不支持修改默认值,表 ${tableName} 的默认值变更已跳过`);
|
|
131
|
-
} else {
|
|
132
|
-
const suffix = IS_MYSQL ? ', ALGORITHM=INSTANT, LOCK=NONE' : '';
|
|
133
|
-
const stmt = IS_MYSQL ? `ALTER TABLE \`${tableName}\` ${plan.defaultClauses.join(', ')}${suffix}` : `ALTER TABLE "${tableName}" ${plan.defaultClauses.join(', ')}`;
|
|
134
|
-
if (IS_PLAN) Logger.info(`[计划] ${stmt}`);
|
|
135
|
-
else if (IS_MYSQL) await executeDDLSafely(sql, stmt);
|
|
136
|
-
else await sql.unsafe(stmt);
|
|
137
|
-
}
|
|
138
|
-
}
|
|
139
|
-
|
|
140
|
-
// 索引操作
|
|
141
|
-
for (const act of plan.indexActions) {
|
|
142
|
-
const stmt = buildIndexSQL(tableName, act.indexName, act.fieldName, act.action);
|
|
143
|
-
if (IS_PLAN) {
|
|
144
|
-
Logger.info(`[计划] ${stmt}`);
|
|
145
|
-
} else {
|
|
146
|
-
try {
|
|
147
|
-
await sql.unsafe(stmt);
|
|
148
|
-
if (act.action === 'create') {
|
|
149
|
-
Logger.info(`[索引变化] 新建索引 ${tableName}.${act.indexName} (${act.fieldName})`);
|
|
150
|
-
} else {
|
|
151
|
-
Logger.info(`[索引变化] 删除索引 ${tableName}.${act.indexName} (${act.fieldName})`);
|
|
152
|
-
}
|
|
153
|
-
} catch (error: any) {
|
|
154
|
-
Logger.error(`${act.action === 'create' ? '创建' : '删除'}索引失败:`, error);
|
|
155
|
-
Logger.warn(`表名: ${tableName}, 索引名: ${act.indexName}, 字段: ${act.fieldName}`);
|
|
156
|
-
throw error;
|
|
157
|
-
}
|
|
158
|
-
}
|
|
159
|
-
}
|
|
160
|
-
|
|
161
|
-
// PG 列注释
|
|
162
|
-
if (IS_PG && plan.commentActions && plan.commentActions.length > 0) {
|
|
163
|
-
for (const stmt of plan.commentActions) {
|
|
164
|
-
if (IS_PLAN) Logger.info(`[计划] ${stmt}`);
|
|
165
|
-
else await sql.unsafe(stmt);
|
|
166
|
-
}
|
|
167
|
-
}
|
|
168
|
-
|
|
169
|
-
// 计数
|
|
170
|
-
globalCount.modifiedTables++;
|
|
171
|
-
}
|
|
@@ -1,77 +0,0 @@
|
|
|
1
|
-
/**
|
|
2
|
-
* syncDb 常量定义模块
|
|
3
|
-
*
|
|
4
|
-
* 包含:
|
|
5
|
-
* - 数据库类型判断
|
|
6
|
-
* - 版本要求
|
|
7
|
-
* - 数据类型映射
|
|
8
|
-
* - 系统字段定义
|
|
9
|
-
*/
|
|
10
|
-
|
|
11
|
-
import { Env } from '../../env.js';
|
|
12
|
-
import type { Dialect } from './types.js';
|
|
13
|
-
|
|
14
|
-
/**
|
|
15
|
-
* 数据库版本要求
|
|
16
|
-
*/
|
|
17
|
-
export const DB_VERSION_REQUIREMENTS = {
|
|
18
|
-
MYSQL_MIN_MAJOR: 8,
|
|
19
|
-
POSTGRES_MIN_MAJOR: 17,
|
|
20
|
-
SQLITE_MIN_VERSION: '3.50.0',
|
|
21
|
-
SQLITE_MIN_VERSION_NUM: 35000 // 3 * 10000 + 50 * 100
|
|
22
|
-
} as const;
|
|
23
|
-
|
|
24
|
-
/**
|
|
25
|
-
* 系统字段定义(所有表都包含的固定字段)
|
|
26
|
-
*/
|
|
27
|
-
export const SYSTEM_FIELDS = {
|
|
28
|
-
ID: { name: 'id', comment: '主键ID' },
|
|
29
|
-
CREATED_AT: { name: 'created_at', comment: '创建时间' },
|
|
30
|
-
UPDATED_AT: { name: 'updated_at', comment: '更新时间' },
|
|
31
|
-
DELETED_AT: { name: 'deleted_at', comment: '删除时间' },
|
|
32
|
-
STATE: { name: 'state', comment: '状态字段' }
|
|
33
|
-
} as const;
|
|
34
|
-
|
|
35
|
-
/**
|
|
36
|
-
* 需要创建索引的系统字段
|
|
37
|
-
*/
|
|
38
|
-
export const SYSTEM_INDEX_FIELDS = ['created_at', 'updated_at', 'state'] as const;
|
|
39
|
-
|
|
40
|
-
/**
|
|
41
|
-
* 字段变更类型的中文标签映射
|
|
42
|
-
*/
|
|
43
|
-
export const CHANGE_TYPE_LABELS = {
|
|
44
|
-
length: '长度',
|
|
45
|
-
datatype: '类型',
|
|
46
|
-
comment: '注释',
|
|
47
|
-
default: '默认值'
|
|
48
|
-
} as const;
|
|
49
|
-
|
|
50
|
-
/**
|
|
51
|
-
* MySQL 表配置
|
|
52
|
-
*
|
|
53
|
-
* 固定配置说明:
|
|
54
|
-
* - ENGINE: InnoDB(支持事务、外键)
|
|
55
|
-
* - CHARSET: utf8mb4(完整 Unicode 支持,包括 Emoji)
|
|
56
|
-
* - COLLATE: utf8mb4_0900_ai_ci(MySQL 8.0 推荐,不区分重音和大小写)
|
|
57
|
-
*/
|
|
58
|
-
export const MYSQL_TABLE_CONFIG = {
|
|
59
|
-
ENGINE: 'InnoDB',
|
|
60
|
-
CHARSET: 'utf8mb4',
|
|
61
|
-
COLLATE: 'utf8mb4_0900_ai_ci'
|
|
62
|
-
} as const;
|
|
63
|
-
|
|
64
|
-
// 数据库类型判断
|
|
65
|
-
export const DB = (Env.DB_TYPE || 'mysql').toLowerCase();
|
|
66
|
-
export const IS_MYSQL = DB === 'mysql';
|
|
67
|
-
export const IS_PG = DB === 'postgresql' || DB === 'postgres';
|
|
68
|
-
export const IS_SQLITE = DB === 'sqlite';
|
|
69
|
-
|
|
70
|
-
// 字段类型映射(按方言)
|
|
71
|
-
export const typeMapping = {
|
|
72
|
-
number: IS_SQLITE ? 'INTEGER' : 'BIGINT',
|
|
73
|
-
string: IS_SQLITE ? 'TEXT' : IS_PG ? 'character varying' : 'VARCHAR',
|
|
74
|
-
text: IS_MYSQL ? 'MEDIUMTEXT' : 'TEXT',
|
|
75
|
-
array_string: IS_SQLITE ? 'TEXT' : IS_PG ? 'character varying' : 'VARCHAR',
|
|
76
|
-
array_text: IS_MYSQL ? 'MEDIUMTEXT' : 'TEXT'
|
|
77
|
-
};
|