fdb2 1.0.37 → 1.0.39
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/README.md +47 -0
- package/bin/fdb2.js +78 -3
- package/dist/public/explorer.css +517 -478
- package/dist/public/explorer.js +1114 -872
- package/dist/public/index.css +87 -27
- package/dist/public/index.js +97 -49
- package/dist/public/layout.js +2 -2
- package/dist/public/vue.js +1 -1
- package/dist/server/core/schema/__tests__/canonical.test.ts +195 -0
- package/dist/server/core/schema/__tests__/ddl-emitter.test.ts +456 -0
- package/dist/server/core/schema/__tests__/diff.test.ts +242 -0
- package/dist/server/core/schema/__tests__/roundtrip.property.test.ts +403 -0
- package/dist/server/core/schema/canonical.d.ts +50 -0
- package/dist/server/core/schema/canonical.d.ts.map +1 -0
- package/dist/server/core/schema/canonical.js +212 -0
- package/dist/server/core/schema/canonical.js.map +1 -0
- package/dist/server/core/schema/canonical.ts +223 -0
- package/dist/server/core/schema/ddl-emitter.d.ts +68 -0
- package/dist/server/core/schema/ddl-emitter.d.ts.map +1 -0
- package/dist/server/core/schema/ddl-emitter.js +813 -0
- package/dist/server/core/schema/ddl-emitter.js.map +1 -0
- package/dist/server/core/schema/ddl-emitter.ts +925 -0
- package/dist/server/core/schema/diff.d.ts +16 -0
- package/dist/server/core/schema/diff.d.ts.map +1 -0
- package/dist/server/core/schema/diff.js +115 -0
- package/dist/server/core/schema/diff.js.map +1 -0
- package/dist/server/core/schema/diff.ts +134 -0
- package/dist/server/core/schema/type-defaults.d.ts +27 -0
- package/dist/server/core/schema/type-defaults.d.ts.map +1 -0
- package/dist/server/core/schema/type-defaults.js +61 -0
- package/dist/server/core/schema/type-defaults.js.map +1 -0
- package/dist/server/core/schema/type-defaults.ts +79 -0
- package/dist/server/index.d.ts +1 -1
- package/dist/server/index.d.ts.map +1 -1
- package/dist/server/index.js +347 -124
- package/dist/server/index.js.map +1 -1
- package/dist/server/index.ts +336 -161
- package/dist/server/model/ddl-spec.d.ts +163 -0
- package/dist/server/model/ddl-spec.d.ts.map +1 -0
- package/dist/server/model/ddl-spec.js +32 -0
- package/dist/server/model/ddl-spec.js.map +1 -0
- package/dist/server/model/ddl-spec.ts +175 -0
- package/dist/server/service/connection.service.d.ts +20 -0
- package/dist/server/service/connection.service.d.ts.map +1 -1
- package/dist/server/service/connection.service.js +99 -25
- package/dist/server/service/connection.service.js.map +1 -1
- package/dist/server/service/connection.service.ts +104 -25
- package/dist/server/service/database/base.service.d.ts +0 -4
- package/dist/server/service/database/base.service.d.ts.map +1 -1
- package/dist/server/service/database/base.service.js.map +1 -1
- package/dist/server/service/database/base.service.ts +0 -5
- package/dist/server/service/database/clickhouse.service.d.ts +0 -5
- package/dist/server/service/database/clickhouse.service.d.ts.map +1 -1
- package/dist/server/service/database/clickhouse.service.js +0 -49
- package/dist/server/service/database/clickhouse.service.js.map +1 -1
- package/dist/server/service/database/clickhouse.service.ts +0 -52
- package/dist/server/service/database/cockroachdb.service.d.ts +0 -5
- package/dist/server/service/database/cockroachdb.service.d.ts.map +1 -1
- package/dist/server/service/database/cockroachdb.service.js +11 -142
- package/dist/server/service/database/cockroachdb.service.js.map +1 -1
- package/dist/server/service/database/cockroachdb.service.ts +11 -161
- package/dist/server/service/database/database.service.d.ts +55 -2
- package/dist/server/service/database/database.service.d.ts.map +1 -1
- package/dist/server/service/database/database.service.js +246 -4
- package/dist/server/service/database/database.service.js.map +1 -1
- package/dist/server/service/database/database.service.ts +295 -7
- package/dist/server/service/database/mongodb.service.d.ts +0 -6
- package/dist/server/service/database/mongodb.service.d.ts.map +1 -1
- package/dist/server/service/database/mongodb.service.js +0 -8
- package/dist/server/service/database/mongodb.service.js.map +1 -1
- package/dist/server/service/database/mongodb.service.ts +0 -8
- package/dist/server/service/database/mssql.service.d.ts +8 -4
- package/dist/server/service/database/mssql.service.d.ts.map +1 -1
- package/dist/server/service/database/mssql.service.js +27 -101
- package/dist/server/service/database/mssql.service.js.map +1 -1
- package/dist/server/service/database/mssql.service.ts +28 -122
- package/dist/server/service/database/mysql.service.d.ts +0 -4
- package/dist/server/service/database/mysql.service.d.ts.map +1 -1
- package/dist/server/service/database/mysql.service.js +11 -137
- package/dist/server/service/database/mysql.service.js.map +1 -1
- package/dist/server/service/database/mysql.service.ts +11 -173
- package/dist/server/service/database/oracle.service.d.ts +0 -4
- package/dist/server/service/database/oracle.service.d.ts.map +1 -1
- package/dist/server/service/database/oracle.service.js +11 -104
- package/dist/server/service/database/oracle.service.js.map +1 -1
- package/dist/server/service/database/oracle.service.ts +11 -127
- package/dist/server/service/database/postgres.service.d.ts +0 -4
- package/dist/server/service/database/postgres.service.d.ts.map +1 -1
- package/dist/server/service/database/postgres.service.js +11 -112
- package/dist/server/service/database/postgres.service.js.map +1 -1
- package/dist/server/service/database/postgres.service.ts +11 -133
- package/dist/server/service/database/sap.service.d.ts +0 -4
- package/dist/server/service/database/sap.service.d.ts.map +1 -1
- package/dist/server/service/database/sap.service.js +11 -137
- package/dist/server/service/database/sap.service.js.map +1 -1
- package/dist/server/service/database/sap.service.ts +11 -158
- package/dist/server/service/database/sql-type.helper.d.ts.map +1 -1
- package/dist/server/service/database/sql-type.helper.js +9 -47
- package/dist/server/service/database/sql-type.helper.js.map +1 -1
- package/dist/server/service/database/sql-type.helper.ts +11 -53
- package/dist/server/service/database/sqlite.service.d.ts +0 -5
- package/dist/server/service/database/sqlite.service.d.ts.map +1 -1
- package/dist/server/service/database/sqlite.service.js +11 -147
- package/dist/server/service/database/sqlite.service.js.map +1 -1
- package/dist/server/service/database/sqlite.service.ts +11 -177
- package/package.json +1 -1
- package/server.js +1 -1
package/dist/server/index.ts
CHANGED
|
@@ -1,19 +1,308 @@
|
|
|
1
1
|
import * as http from "node:http";
|
|
2
|
+
import * as https from "node:https";
|
|
2
3
|
import { ConnectionService } from './service/connection.service';
|
|
3
4
|
import { DatabaseService } from './service/database/database.service';
|
|
4
5
|
|
|
6
|
+
// 远程代理相关常量
|
|
7
|
+
const REMOTE_ID_SEP = '::';
|
|
8
|
+
const REMOTE_TOKEN_HEADER = 'x-db-tool-token';
|
|
9
|
+
// 代理请求标记:用于识别"本请求本身来自代理",避免远程地址指向自身(或互指)时无限递归合并
|
|
10
|
+
const REMOTE_PROXY_HEADER = 'x-db-tool-proxy';
|
|
11
|
+
|
|
12
|
+
// 永不转发的路由:只作用于本地连接配置(无远程对应物或需要本地语义)
|
|
13
|
+
const LOCAL_ONLY_ROUTES = new Set<string>([
|
|
14
|
+
'/api/database/getConnection',
|
|
15
|
+
'/api/database/addConnection',
|
|
16
|
+
'/api/database/getSupportedDatabaseTypes'
|
|
17
|
+
]);
|
|
18
|
+
|
|
19
|
+
// 连接配置类路由:复合 id 时转发到远程,但允许直接操作「远程代理配置」本身
|
|
20
|
+
const CONNECTION_CONFIG_ROUTES = new Set<string>([
|
|
21
|
+
'/api/database/getConnections',
|
|
22
|
+
'/api/database/getConnection',
|
|
23
|
+
'/api/database/addConnection',
|
|
24
|
+
'/api/database/updateConnection',
|
|
25
|
+
'/api/database/deleteConnection',
|
|
26
|
+
'/api/database/testConnection',
|
|
27
|
+
'/api/database/getSupportedDatabaseTypes'
|
|
28
|
+
]);
|
|
29
|
+
|
|
30
|
+
function isRemoteConnection(conn: any): boolean {
|
|
31
|
+
return !!conn && conn.type === 'remote';
|
|
32
|
+
}
|
|
33
|
+
|
|
34
|
+
/** 从请求中取出被操作连接的 id(body 或路径段) */
|
|
35
|
+
function extractConnId(pathname: string, body: any): string | undefined {
|
|
36
|
+
if (pathname.startsWith('/api/database/truncateTable/') || pathname.startsWith('/api/database/dropTable/')) {
|
|
37
|
+
const parts = pathname.split('/').filter(Boolean);
|
|
38
|
+
return parts[3];
|
|
39
|
+
}
|
|
40
|
+
if (body && typeof body.id === 'string') return body.id;
|
|
41
|
+
return undefined;
|
|
42
|
+
}
|
|
43
|
+
|
|
44
|
+
/**
|
|
45
|
+
* 拆分复合 id:proxyId::remoteConnId。
|
|
46
|
+
* remoteConnId 自身可能仍是复合 id(多级代理),必须保留剩余部分,不能用 split 截断。
|
|
47
|
+
*/
|
|
48
|
+
function splitProxyId(connId: string): { proxyId: string; remoteConnId: string } | null {
|
|
49
|
+
const idx = connId.indexOf(REMOTE_ID_SEP);
|
|
50
|
+
if (idx <= 0) return null;
|
|
51
|
+
return {
|
|
52
|
+
proxyId: connId.slice(0, idx),
|
|
53
|
+
remoteConnId: connId.slice(idx + REMOTE_ID_SEP.length)
|
|
54
|
+
};
|
|
55
|
+
}
|
|
56
|
+
|
|
57
|
+
/** 判断是否为指向远程的复合 id(proxyId::remoteConnId) */
|
|
58
|
+
async function resolveRemoteProxy(pathname: string, body: any): Promise<{ proxyConn: any; remoteConnId: string } | null> {
|
|
59
|
+
if (LOCAL_ONLY_ROUTES.has(pathname)) return null;
|
|
60
|
+
const connId = extractConnId(pathname, body);
|
|
61
|
+
if (!connId || !connId.includes(REMOTE_ID_SEP)) return null;
|
|
62
|
+
const parts = splitProxyId(connId);
|
|
63
|
+
if (!parts) return null;
|
|
64
|
+
const proxyConn = await connectionService.getConnectionById(parts.proxyId);
|
|
65
|
+
if (!proxyConn || !isRemoteConnection(proxyConn)) return null;
|
|
66
|
+
return { proxyConn, remoteConnId: parts.remoteConnId };
|
|
67
|
+
}
|
|
68
|
+
|
|
69
|
+
/** 解析远程响应体(远程服务统一返回 { ret, msg, data } 信封) */
|
|
70
|
+
function parseRemotePayload(text: string): any {
|
|
71
|
+
if (!text) return {};
|
|
72
|
+
try {
|
|
73
|
+
return JSON.parse(text);
|
|
74
|
+
} catch {
|
|
75
|
+
return {};
|
|
76
|
+
}
|
|
77
|
+
}
|
|
78
|
+
|
|
79
|
+
/** 通过 Node 18+ 全局 fetch 调用远程服务 */
|
|
80
|
+
async function postRemoteByFetch(
|
|
81
|
+
url: string,
|
|
82
|
+
body: any,
|
|
83
|
+
headers: Record<string, string>,
|
|
84
|
+
timeoutMs: number
|
|
85
|
+
): Promise<any> {
|
|
86
|
+
const controller = new AbortController();
|
|
87
|
+
const timer = setTimeout(() => controller.abort(), timeoutMs);
|
|
88
|
+
try {
|
|
89
|
+
const resp = await fetch(url, {
|
|
90
|
+
method: 'POST',
|
|
91
|
+
headers: { 'Content-Type': 'application/json', ...headers },
|
|
92
|
+
body: JSON.stringify(body ?? {}),
|
|
93
|
+
signal: controller.signal
|
|
94
|
+
});
|
|
95
|
+
const text = await resp.text();
|
|
96
|
+
const payload = parseRemotePayload(text);
|
|
97
|
+
if (!resp.ok) {
|
|
98
|
+
throw new Error(payload?.msg || `远程服务返回 HTTP ${resp.status}`);
|
|
99
|
+
}
|
|
100
|
+
return payload;
|
|
101
|
+
} catch (e: any) {
|
|
102
|
+
if (e?.name === 'AbortError') throw new Error('远程服务请求超时');
|
|
103
|
+
throw e;
|
|
104
|
+
} finally {
|
|
105
|
+
clearTimeout(timer);
|
|
106
|
+
}
|
|
107
|
+
}
|
|
108
|
+
|
|
109
|
+
/**
|
|
110
|
+
* 低版本 Node 没有全局 fetch 时的回退实现。
|
|
111
|
+
* VSCode 1.75-1.84 的扩展宿主内置 Node 16(无全局 fetch),扩展声明支持 ^1.75.0,故保留此路径。
|
|
112
|
+
*/
|
|
113
|
+
function postRemoteByNodeHttp(
|
|
114
|
+
url: string,
|
|
115
|
+
body: any,
|
|
116
|
+
headers: Record<string, string>,
|
|
117
|
+
timeoutMs: number
|
|
118
|
+
): Promise<any> {
|
|
119
|
+
return new Promise((resolve, reject) => {
|
|
120
|
+
let target: URL;
|
|
121
|
+
try {
|
|
122
|
+
target = new URL(url);
|
|
123
|
+
} catch {
|
|
124
|
+
reject(new Error(`远程服务地址无效: ${url}`));
|
|
125
|
+
return;
|
|
126
|
+
}
|
|
127
|
+
const payload = Buffer.from(JSON.stringify(body ?? {}));
|
|
128
|
+
const lib = target.protocol === 'https:' ? https : http;
|
|
129
|
+
const req = lib.request(
|
|
130
|
+
{
|
|
131
|
+
hostname: target.hostname,
|
|
132
|
+
port: target.port || (target.protocol === 'https:' ? 443 : 80),
|
|
133
|
+
path: (target.pathname || '/') + (target.search || ''),
|
|
134
|
+
method: 'POST',
|
|
135
|
+
headers: { ...headers, 'Content-Length': payload.length }
|
|
136
|
+
},
|
|
137
|
+
(res) => {
|
|
138
|
+
let raw = '';
|
|
139
|
+
res.setEncoding('utf8');
|
|
140
|
+
res.on('data', (chunk: string) => { raw += chunk; });
|
|
141
|
+
res.on('end', () => {
|
|
142
|
+
const data = parseRemotePayload(raw);
|
|
143
|
+
const status = res.statusCode || 0;
|
|
144
|
+
if (status < 200 || status >= 300) {
|
|
145
|
+
reject(new Error(data?.msg || `远程服务返回 HTTP ${status}`));
|
|
146
|
+
return;
|
|
147
|
+
}
|
|
148
|
+
resolve(data);
|
|
149
|
+
});
|
|
150
|
+
}
|
|
151
|
+
);
|
|
152
|
+
req.setTimeout(timeoutMs, () => { req.destroy(new Error('远程服务请求超时')); });
|
|
153
|
+
req.on('error', reject);
|
|
154
|
+
req.write(payload);
|
|
155
|
+
req.end();
|
|
156
|
+
});
|
|
157
|
+
}
|
|
158
|
+
|
|
159
|
+
/** 统一的远程调用入口:优先全局 fetch,低版本回退 node:http(s);返回 { ret, msg, data } 信封 */
|
|
160
|
+
async function postRemote(
|
|
161
|
+
url: string,
|
|
162
|
+
body: any,
|
|
163
|
+
headers: Record<string, string>,
|
|
164
|
+
timeoutMs: number
|
|
165
|
+
): Promise<any> {
|
|
166
|
+
if (typeof fetch === 'function') {
|
|
167
|
+
return await postRemoteByFetch(url, body, headers, timeoutMs);
|
|
168
|
+
}
|
|
169
|
+
return await postRemoteByNodeHttp(url, body, headers, timeoutMs);
|
|
170
|
+
}
|
|
171
|
+
|
|
172
|
+
/** 构造带令牌与代理标记的请求头 */
|
|
173
|
+
function buildRemoteHeaders(proxyConn: any): Record<string, string> {
|
|
174
|
+
const headers: Record<string, string> = { [REMOTE_PROXY_HEADER]: '1' };
|
|
175
|
+
const token = proxyConn?.options?.remoteToken;
|
|
176
|
+
if (token) headers[REMOTE_TOKEN_HEADER] = String(token);
|
|
177
|
+
return headers;
|
|
178
|
+
}
|
|
179
|
+
|
|
180
|
+
/** 把请求原样转发到远程 db_tool 服务端(自动解包远程响应,避免二次包装) */
|
|
181
|
+
async function proxyToRemote(proxyConn: any, remoteConnId: string, pathname: string, body: any): Promise<any> {
|
|
182
|
+
const base = String(proxyConn.options?.remoteUrl || '').replace(/\/+$/, '');
|
|
183
|
+
if (!base) throw new Error('远程代理连接缺少 remoteUrl');
|
|
184
|
+
let targetPath = pathname;
|
|
185
|
+
if (pathname.startsWith('/api/database/truncateTable/') || pathname.startsWith('/api/database/dropTable/')) {
|
|
186
|
+
const parts = pathname.split('/').filter(Boolean);
|
|
187
|
+
// 不做 URL 编码:连接 id 由本地生成(仅含安全字符),而对端解析路径段时不做 decode,
|
|
188
|
+
// 编码会把 ':' 变成 %3A 导致对端无法解析复合 id。
|
|
189
|
+
parts[3] = remoteConnId;
|
|
190
|
+
targetPath = '/' + parts.join('/');
|
|
191
|
+
}
|
|
192
|
+
const targetBody = { ...body, id: remoteConnId };
|
|
193
|
+
const env = await postRemote(`${base}${targetPath}`, targetBody, buildRemoteHeaders(proxyConn), 60000);
|
|
194
|
+
if (env && typeof env === 'object' && env.ret && env.ret !== 0) {
|
|
195
|
+
throw new Error(env.msg || '远程服务执行失败');
|
|
196
|
+
}
|
|
197
|
+
return env?.data;
|
|
198
|
+
}
|
|
199
|
+
|
|
200
|
+
/** 拉取远程服务上的连接列表(用于网关模式合并,单层、过滤嵌套代理配置) */
|
|
201
|
+
async function fetchRemoteConnections(proxyConn: any): Promise<{ list: any[]; error?: string }> {
|
|
202
|
+
const base = String(proxyConn.options?.remoteUrl || '').replace(/\/+$/, '');
|
|
203
|
+
if (!base) return { list: [], error: '缺少 remoteUrl' };
|
|
204
|
+
try {
|
|
205
|
+
const env = await postRemote(`${base}/api/database/getConnections`, {}, buildRemoteHeaders(proxyConn), 10000);
|
|
206
|
+
if (env && env.ret && env.ret !== 0) {
|
|
207
|
+
console.warn('[remote] 获取远程连接列表失败:', env.msg);
|
|
208
|
+
return { list: [], error: env.msg || '远程服务返回错误' };
|
|
209
|
+
}
|
|
210
|
+
return { list: Array.isArray(env?.data) ? env.data : [] };
|
|
211
|
+
} catch (e: any) {
|
|
212
|
+
console.warn('[remote] 获取远程连接列表失败:', e?.message);
|
|
213
|
+
return { list: [], error: e?.message || '远程服务不可达' };
|
|
214
|
+
}
|
|
215
|
+
}
|
|
216
|
+
|
|
217
|
+
/** 网关模式:本地连接列表 = 本地连接 + 远程代理暴露的连接 */
|
|
218
|
+
async function handleGetConnectionsWithProxy(): Promise<any[]> {
|
|
219
|
+
const locals = await connectionService.getAllConnections();
|
|
220
|
+
// 并行拉取各远程服务,避免多个远程时串行等待
|
|
221
|
+
const remoteResults = await Promise.all(
|
|
222
|
+
locals.map(conn => (isRemoteConnection(conn) ? fetchRemoteConnections(conn) : Promise.resolve(null)))
|
|
223
|
+
);
|
|
224
|
+
const result: any[] = [];
|
|
225
|
+
locals.forEach((conn, index) => {
|
|
226
|
+
if (!isRemoteConnection(conn)) {
|
|
227
|
+
result.push(conn);
|
|
228
|
+
return;
|
|
229
|
+
}
|
|
230
|
+
const remote = remoteResults[index];
|
|
231
|
+
// 代理配置本身保留(用于编辑/删除),标记 _isProxy
|
|
232
|
+
result.push({ ...conn, _isProxy: true, _remoteError: remote?.error });
|
|
233
|
+
for (const rc of remote?.list || []) {
|
|
234
|
+
if (rc.type === 'remote' || rc._isProxy) continue; // 不暴露嵌套代理配置
|
|
235
|
+
const safe = { ...rc };
|
|
236
|
+
delete safe.password; // 不把远程库的密码下发到客户端
|
|
237
|
+
result.push({
|
|
238
|
+
...safe,
|
|
239
|
+
id: `${conn.id}${REMOTE_ID_SEP}${rc.id}`,
|
|
240
|
+
_proxyId: conn.id,
|
|
241
|
+
_remote: true,
|
|
242
|
+
_remoteName: conn.name
|
|
243
|
+
});
|
|
244
|
+
}
|
|
245
|
+
});
|
|
246
|
+
return result;
|
|
247
|
+
}
|
|
248
|
+
|
|
249
|
+
/** 测试远程代理连接(探活远程服务) */
|
|
250
|
+
async function testRemoteConnection(conn: any): Promise<boolean> {
|
|
251
|
+
const base = String(conn.options?.remoteUrl || '').replace(/\/+$/, '');
|
|
252
|
+
if (!base) throw new Error('缺少 remoteUrl');
|
|
253
|
+
const env = await postRemote(`${base}/api/database/getConnections`, {}, buildRemoteHeaders(conn), 10000);
|
|
254
|
+
if (env && env.ret && env.ret !== 0) throw new Error(env.msg || '远程服务返回错误');
|
|
255
|
+
return true;
|
|
256
|
+
}
|
|
257
|
+
|
|
5
258
|
// 初始化服务实例
|
|
6
259
|
const connectionService = new ConnectionService();
|
|
7
260
|
const databaseService = new DatabaseService();
|
|
8
261
|
|
|
9
262
|
|
|
10
263
|
// 数据库相关路由处理
|
|
11
|
-
export async function handleDatabaseRoutes(pathname: string, body: any) {
|
|
264
|
+
export async function handleDatabaseRoutes(pathname: string, body: any, headers?: any) {
|
|
12
265
|
try {
|
|
13
|
-
//
|
|
266
|
+
// 远程访问令牌校验(仅当远程服务端设置了 DB_TOOL_REMOTE_TOKEN 且请求经由 HTTP 入口时生效)
|
|
267
|
+
const envToken = process.env.DB_TOOL_REMOTE_TOKEN;
|
|
268
|
+
if (envToken && headers) {
|
|
269
|
+
const incoming = String(headers[REMOTE_TOKEN_HEADER] || headers['x-db-tool-token'] || '');
|
|
270
|
+
if (incoming !== envToken) {
|
|
271
|
+
throw new Error('远程访问令牌无效');
|
|
272
|
+
}
|
|
273
|
+
}
|
|
274
|
+
|
|
275
|
+
// 网关模式:合并远程服务暴露的连接
|
|
14
276
|
if (pathname === '/api/database/getConnections') {
|
|
15
|
-
|
|
16
|
-
|
|
277
|
+
// 请求本身来自代理时不再二次合并,避免远程地址指向自身(或互指)造成无限递归
|
|
278
|
+
if (headers && headers[REMOTE_PROXY_HEADER]) {
|
|
279
|
+
return await connectionService.getAllConnections();
|
|
280
|
+
}
|
|
281
|
+
return await handleGetConnectionsWithProxy();
|
|
282
|
+
}
|
|
283
|
+
|
|
284
|
+
// 远程代理连接本身的测试:直接探活远程服务
|
|
285
|
+
if (pathname === '/api/database/testConnection' && body && isRemoteConnection(body)) {
|
|
286
|
+
return await testRemoteConnection(body);
|
|
287
|
+
}
|
|
288
|
+
|
|
289
|
+
// 复合 id(proxyId::remoteConnId)→ 转发到远程:
|
|
290
|
+
// 覆盖数据操作(查询/建表/改结构等)与连接配置的增删改
|
|
291
|
+
const proxy = await resolveRemoteProxy(pathname, body);
|
|
292
|
+
if (proxy) {
|
|
293
|
+
return await proxyToRemote(proxy.proxyConn, proxy.remoteConnId, pathname, body);
|
|
294
|
+
}
|
|
295
|
+
|
|
296
|
+
// 对「远程代理配置」本身做数据操作时给出明确提示(配置本身的增删改是允许的)
|
|
297
|
+
if (!CONNECTION_CONFIG_ROUTES.has(pathname)) {
|
|
298
|
+
const connId = extractConnId(pathname, body);
|
|
299
|
+
if (connId && !connId.includes(REMOTE_ID_SEP)) {
|
|
300
|
+
const conn = await connectionService.getConnectionById(connId);
|
|
301
|
+
if (conn && isRemoteConnection(conn)) {
|
|
302
|
+
if (pathname === '/api/database/closeConnection') return true; // 代理配置本身无需关闭
|
|
303
|
+
throw new Error(`请选择「${conn.name}」代理下的具体数据库连接进行操作`);
|
|
304
|
+
}
|
|
305
|
+
}
|
|
17
306
|
}
|
|
18
307
|
|
|
19
308
|
// /api/database/getConnection
|
|
@@ -245,22 +534,48 @@ export async function handleDatabaseRoutes(pathname: string, body: any) {
|
|
|
245
534
|
return exportPath;
|
|
246
535
|
}
|
|
247
536
|
|
|
248
|
-
// /api/database/
|
|
249
|
-
if (pathname === '/api/database/
|
|
250
|
-
const { id, database
|
|
251
|
-
if (!id || !database
|
|
537
|
+
// /api/database/refreshTableCache(手动全量拉取表列表,忽略预览缓存)
|
|
538
|
+
if (pathname === '/api/database/refreshTableCache') {
|
|
539
|
+
const { id, database } = body;
|
|
540
|
+
if (!id || !database) throw Error('Missing parameters');
|
|
252
541
|
|
|
253
|
-
const
|
|
254
|
-
|
|
255
|
-
|
|
542
|
+
const result = await databaseService.refreshTableCache(id, database);
|
|
543
|
+
return result;
|
|
544
|
+
}
|
|
545
|
+
|
|
546
|
+
// /api/database/previewCreateTable(纯计算:目标态 TableSpec -> 建表 DDL 与方言能力)
|
|
547
|
+
if (pathname === '/api/database/previewCreateTable') {
|
|
548
|
+
const { id, database, spec } = body;
|
|
549
|
+
if (!id || !database || !spec) throw Error('Missing parameters');
|
|
550
|
+
|
|
551
|
+
const result = await databaseService.previewCreateTable(id, database, spec);
|
|
552
|
+
return result;
|
|
553
|
+
}
|
|
554
|
+
|
|
555
|
+
// /api/database/executeCreateTable(执行:后端检测重名后生成并执行建表语句)
|
|
556
|
+
if (pathname === '/api/database/executeCreateTable') {
|
|
557
|
+
const { id, database, spec } = body;
|
|
558
|
+
if (!id || !database || !spec) throw Error('Missing parameters');
|
|
559
|
+
|
|
560
|
+
const result = await databaseService.executeCreateTable(id, database, spec);
|
|
561
|
+
return result;
|
|
562
|
+
}
|
|
563
|
+
|
|
564
|
+
// /api/database/previewAlterTable(纯计算:目标态 TableSpec -> DDL 语句与变更集)
|
|
565
|
+
if (pathname === '/api/database/previewAlterTable') {
|
|
566
|
+
const { id, database, tableName, spec } = body;
|
|
567
|
+
if (!id || !database || !tableName || !spec) throw Error('Missing parameters');
|
|
568
|
+
|
|
569
|
+
const result = await databaseService.previewAlterTable(id, database, tableName, spec);
|
|
570
|
+
return result;
|
|
256
571
|
}
|
|
257
572
|
|
|
258
|
-
// /api/database/
|
|
259
|
-
if (pathname === '/api/database/
|
|
260
|
-
const { id, database,
|
|
261
|
-
if (!id || !database || !
|
|
573
|
+
// /api/database/executeAlterTable(执行:后端重读实时结构生成并执行,所见即所得)
|
|
574
|
+
if (pathname === '/api/database/executeAlterTable') {
|
|
575
|
+
const { id, database, tableName, spec } = body;
|
|
576
|
+
if (!id || !database || !tableName || !spec) throw Error('Missing parameters');
|
|
262
577
|
|
|
263
|
-
const result = await databaseService.
|
|
578
|
+
const result = await databaseService.executeAlterTable(id, database, tableName, spec);
|
|
264
579
|
return result;
|
|
265
580
|
}
|
|
266
581
|
|
|
@@ -378,7 +693,11 @@ export async function handleDatabaseRoutes(pathname: string, body: any) {
|
|
|
378
693
|
// 使用数据库服务生成正确的CREATE VIEW语法
|
|
379
694
|
const dataSource = await databaseService['connectionService'].getActiveConnection(id, database);
|
|
380
695
|
const dbService = databaseService['getDatabaseService'](dataSource.options.type as string);
|
|
381
|
-
|
|
696
|
+
// 防御:若传入的已是完整 CREATE VIEW 语句(SQLite/Oracle 的 getViewDefinition 形态),
|
|
697
|
+
// 直接执行,避免拼成 `CREATE VIEW v AS CREATE VIEW v AS ...`
|
|
698
|
+
const def = String(definition).trim();
|
|
699
|
+
const isFullCreate = /^CREATE\s+(?:OR\s+REPLACE\s+)?(?:ALGORITHM\s*=\s*\w+\s+)?VIEW\s/is.test(def);
|
|
700
|
+
const sql = isFullCreate ? def : `CREATE VIEW ${dbService['quoteIdentifier'](viewName)} AS ${def}`;
|
|
382
701
|
const result = await databaseService.executeQuery(id, sql, database);
|
|
383
702
|
return { success: true, sql, result };
|
|
384
703
|
}
|
|
@@ -445,115 +764,6 @@ function convertToCSV(data: any[]): string {
|
|
|
445
764
|
return [csvHeaders, ...csvRows].join('\n');
|
|
446
765
|
}
|
|
447
766
|
|
|
448
|
-
function generateTableSQL(tableName: string, columns: any[], tableInfo: any): string {
|
|
449
|
-
let sql = `CREATE TABLE \`${tableName}\` (\n`;
|
|
450
|
-
|
|
451
|
-
const primaryKeys = [] as Array<string>;
|
|
452
|
-
|
|
453
|
-
columns.forEach((column, index) => {
|
|
454
|
-
const columnDef = [] as Array<string>;
|
|
455
|
-
columnDef.push(` \`${column.name}\``);
|
|
456
|
-
columnDef.push(getColumnTypeDefinition(column));
|
|
457
|
-
|
|
458
|
-
if (!column.nullable) {
|
|
459
|
-
columnDef.push('NOT NULL');
|
|
460
|
-
}
|
|
461
|
-
|
|
462
|
-
if (column.defaultValue !== null && column.defaultValue !== '') {
|
|
463
|
-
columnDef.push(`DEFAULT ${formatDefaultValue(column.defaultValue, column.type)}`);
|
|
464
|
-
}
|
|
465
|
-
|
|
466
|
-
if (column.isAutoIncrement) {
|
|
467
|
-
columnDef.push('AUTO_INCREMENT');
|
|
468
|
-
}
|
|
469
|
-
|
|
470
|
-
if (column.comment) {
|
|
471
|
-
columnDef.push(`COMMENT '${column.comment}'`);
|
|
472
|
-
}
|
|
473
|
-
|
|
474
|
-
sql += columnDef.join(' ');
|
|
475
|
-
|
|
476
|
-
if (index < columns.length - 1) {
|
|
477
|
-
sql += ',\n';
|
|
478
|
-
}
|
|
479
|
-
|
|
480
|
-
if (column.isPrimary) {
|
|
481
|
-
primaryKeys.push(column.name);
|
|
482
|
-
}
|
|
483
|
-
});
|
|
484
|
-
|
|
485
|
-
// 添加主键约束
|
|
486
|
-
if (primaryKeys.length > 0) {
|
|
487
|
-
sql += `,\n PRIMARY KEY (\`${primaryKeys.join('`, `')}\`)`;
|
|
488
|
-
}
|
|
489
|
-
|
|
490
|
-
sql += `\n) ENGINE=${tableInfo.engine} DEFAULT CHARSET=${tableInfo.charset} COLLATE=${tableInfo.collation}`;
|
|
491
|
-
|
|
492
|
-
if (tableInfo.comment) {
|
|
493
|
-
sql += ` COMMENT='${tableInfo.comment}'`;
|
|
494
|
-
}
|
|
495
|
-
|
|
496
|
-
sql += ';';
|
|
497
|
-
|
|
498
|
-
return sql;
|
|
499
|
-
}
|
|
500
|
-
|
|
501
|
-
function generateAlterTableSQL(tableName: string, newColumns: any[], oldColumns: any[] = []): string[] {
|
|
502
|
-
const statements = [] as Array<string>;
|
|
503
|
-
|
|
504
|
-
// 简化实现:删除所有列,重新添加列
|
|
505
|
-
// 注意:实际生产中需要更复杂的对比逻辑
|
|
506
|
-
statements.push(`DROP TABLE IF EXISTS \`${tableName}_temp\`;`);
|
|
507
|
-
|
|
508
|
-
let createTempSQL = `CREATE TABLE \`${tableName}_temp\` (\n`;
|
|
509
|
-
const primaryKeys = [] as Array<string>;
|
|
510
|
-
|
|
511
|
-
newColumns.forEach((column, index) => {
|
|
512
|
-
const columnDef = [] as Array<string>;
|
|
513
|
-
columnDef.push(` \`${column.name}\``);
|
|
514
|
-
columnDef.push(getColumnTypeDefinition(column));
|
|
515
|
-
|
|
516
|
-
if (!column.nullable) {
|
|
517
|
-
columnDef.push('NOT NULL');
|
|
518
|
-
}
|
|
519
|
-
|
|
520
|
-
if (column.defaultValue !== null && column.defaultValue !== '') {
|
|
521
|
-
columnDef.push(`DEFAULT ${formatDefaultValue(column.defaultValue, column.type)}`);
|
|
522
|
-
}
|
|
523
|
-
|
|
524
|
-
if (column.isAutoIncrement) {
|
|
525
|
-
columnDef.push('AUTO_INCREMENT');
|
|
526
|
-
}
|
|
527
|
-
|
|
528
|
-
if (column.comment) {
|
|
529
|
-
columnDef.push(`COMMENT '${column.comment}'`);
|
|
530
|
-
}
|
|
531
|
-
|
|
532
|
-
createTempSQL += columnDef.join(' ');
|
|
533
|
-
|
|
534
|
-
if (index < newColumns.length - 1) {
|
|
535
|
-
createTempSQL += ',\n';
|
|
536
|
-
}
|
|
537
|
-
|
|
538
|
-
if (column.isPrimary) {
|
|
539
|
-
primaryKeys.push(column.name);
|
|
540
|
-
}
|
|
541
|
-
});
|
|
542
|
-
|
|
543
|
-
if (primaryKeys.length > 0) {
|
|
544
|
-
createTempSQL += `,\n PRIMARY KEY (\`${primaryKeys.join('`, `')}\`)`;
|
|
545
|
-
}
|
|
546
|
-
|
|
547
|
-
createTempSQL += '\n);';
|
|
548
|
-
statements.push(createTempSQL);
|
|
549
|
-
|
|
550
|
-
// 复制数据和重命名
|
|
551
|
-
statements.push(`INSERT INTO \`${tableName}_temp\` SELECT * FROM \`${tableName}\`;`);
|
|
552
|
-
statements.push(`DROP TABLE \`${tableName}\`;`);
|
|
553
|
-
statements.push(`RENAME TABLE \`${tableName}_temp\` TO \`${tableName}\`;`);
|
|
554
|
-
|
|
555
|
-
return statements;
|
|
556
|
-
}
|
|
557
767
|
|
|
558
768
|
function generateInsertSQL(dbType: string, tableName: string, data: any, dbService: any, columns?: any[]): string {
|
|
559
769
|
// 如果提供了列信息,过滤掉自增字段
|
|
@@ -608,41 +818,6 @@ function generateDeleteSQL(dbType: string, tableName: string, where: any, dbServ
|
|
|
608
818
|
return `DELETE FROM ${dbService.quoteIdentifier(tableName)} WHERE ${whereClause};`;
|
|
609
819
|
}
|
|
610
820
|
|
|
611
|
-
function getColumnTypeDefinition(column: any): string {
|
|
612
|
-
let type = column.type.toUpperCase();
|
|
613
|
-
|
|
614
|
-
if (needsLength(column.type) && column.length) {
|
|
615
|
-
type += `(${column.length})`;
|
|
616
|
-
}
|
|
617
|
-
|
|
618
|
-
return type;
|
|
619
|
-
}
|
|
620
|
-
|
|
621
|
-
function needsLength(type: string): boolean {
|
|
622
|
-
const typesNeedingLength = ['varchar', 'char', 'decimal', 'float', 'double'];
|
|
623
|
-
return typesNeedingLength.some(t => type.startsWith(t));
|
|
624
|
-
}
|
|
625
|
-
|
|
626
|
-
function formatDefaultValue(value: string, type: string): string {
|
|
627
|
-
if (value === '' || value === null || value === undefined) {
|
|
628
|
-
return 'NULL';
|
|
629
|
-
}
|
|
630
|
-
|
|
631
|
-
if (isNumberType(type) || ['float', 'double', 'decimal'].includes(type)) {
|
|
632
|
-
return value;
|
|
633
|
-
}
|
|
634
|
-
|
|
635
|
-
// 特殊关键字处理
|
|
636
|
-
if (value.toString().toUpperCase() === 'CURRENT_TIMESTAMP' ||
|
|
637
|
-
value.toString().toUpperCase() === 'NOW()' ||
|
|
638
|
-
value.toString().toUpperCase() === 'CURRENT_DATE') {
|
|
639
|
-
return value.toString().toUpperCase();
|
|
640
|
-
}
|
|
641
|
-
|
|
642
|
-
// 字符串类型需要加引号
|
|
643
|
-
return `'${value.replace(/'/g, "''")}'`;
|
|
644
|
-
}
|
|
645
|
-
|
|
646
821
|
function formatValue(dbType: string, value: any): string {
|
|
647
822
|
if (value === null || value === undefined) {
|
|
648
823
|
return 'NULL';
|