feat(connection): 支持多数据源额外连接参数配置

- 前端连接表单新增额外连接参数入口,支持 URI query 格式录入与解析回填
- MySQL 兼容驱动支持 JDBC 常见参数映射,修复 UTF-8 字符集与 serverTimezone 兼容问题
- 扩展 Oracle、PostgreSQL 兼容、SQL Server、ClickHouse、MongoDB、达梦、TDengine 参数合并
- 按不同驱动通道处理 DSN、URI、Options 与 Settings,避免统一透传导致连接异常
- 修复编辑已保存连接时解析无认证 URI 会清空已有账号密码的问题
- 补充连接参数透传、缓存隔离、DSN 合并与 URI 回填回归测试
This commit is contained in:
Syngnat
2026-04-30 10:57:52 +08:00
parent c65e429072
commit c92959f3e8
29 changed files with 1143 additions and 99 deletions

View File

@@ -52,6 +52,19 @@ describe('buildRpcConnectionConfig', () => {
expect(result.clickHouseProtocol).toBe('http');
});
it('preserves extra connection params for RPC calls', () => {
const result = buildRpcConnectionConfig({
id: 'conn-mysql',
type: 'mysql',
host: 'db.local',
port: 3306,
user: 'root',
connectionParams: 'characterEncoding=utf8&useSSL=false',
} as any);
expect(result.connectionParams).toBe('characterEncoding=utf8&useSSL=false');
});
it('fills default nested config blocks needed by RPC calls', () => {
const result = buildRpcConnectionConfig({
id: 'conn-redis',

View File

@@ -0,0 +1,77 @@
import { describe, expect, it } from "vitest";
import { mergeParsedUriValuesForForm } from "./connectionUriMerge";
describe("mergeParsedUriValuesForForm", () => {
it("keeps saved credentials when parsed URI has no auth section", () => {
const result = mergeParsedUriValuesForForm(
{
user: "root",
password: "saved-password",
host: "192.168.1.10",
port: 3306,
database: "old_db",
connectionParams: "application_name=GoNavi",
timeout: 30,
},
{
host: "192.168.1.240",
port: 3306,
user: "",
password: "",
database: "mkefu_location_dev_local",
connectionParams: "",
timeout: undefined,
useSSL: false,
},
"jdbc:mysql://192.168.1.240:3306/mkefu_location_dev_local?characterEncoding=UTF-8",
);
expect(result).toMatchObject({
uri: "jdbc:mysql://192.168.1.240:3306/mkefu_location_dev_local?characterEncoding=UTF-8",
host: "192.168.1.240",
port: 3306,
database: "mkefu_location_dev_local",
useSSL: false,
});
expect(result).not.toHaveProperty("user");
expect(result).not.toHaveProperty("password");
expect(result).not.toHaveProperty("connectionParams");
expect(result).not.toHaveProperty("timeout");
});
it("allows URI credentials to replace existing credentials when provided", () => {
const result = mergeParsedUriValuesForForm(
{
user: "root",
password: "old-password",
},
{
user: "uri_user",
password: "uri-password",
},
"mysql://uri_user:uri-password@127.0.0.1:3306/app",
);
expect(result).toMatchObject({
user: "uri_user",
password: "uri-password",
});
});
it("keeps existing database when URI omits a database path", () => {
const result = mergeParsedUriValuesForForm(
{
database: "saved_db",
},
{
host: "127.0.0.1",
database: "",
},
"mysql://127.0.0.1:3306",
);
expect(result.database).toBeUndefined();
expect(result.host).toBe("127.0.0.1");
});
});

View File

@@ -0,0 +1,36 @@
const EMPTY_PRESERVED_URI_FIELDS = new Set([
"user",
"password",
"database",
"connectionParams",
]);
const isEmptyParsedValue = (value: unknown): boolean =>
value === undefined ||
value === null ||
value === "" ||
(Array.isArray(value) && value.length === 0);
export const mergeParsedUriValuesForForm = (
currentValues: Record<string, unknown>,
parsedValues: Record<string, unknown>,
uriText: string,
): Record<string, unknown> => {
const nextValues: Record<string, unknown> = { uri: uriText };
Object.entries(parsedValues).forEach(([key, value]) => {
if (value === undefined) {
return;
}
if (
EMPTY_PRESERVED_URI_FIELDS.has(key) &&
isEmptyParsedValue(value) &&
!isEmptyParsedValue(currentValues[key])
) {
return;
}
nextValues[key] = value;
});
return nextValues;
};