feat(redis): 支持 Redis Sentinel 连接模式

This commit is contained in:
Syngnat
2026-06-12 01:04:43 +08:00
parent 480edbe501
commit 156fce531c
22 changed files with 697 additions and 200 deletions

View File

@@ -36,3 +36,16 @@ describe('ConnectionModal data source registry', () => {
expect(source).toContain('label="显示数据库 (留空显示全部)"');
});
});
describe('ConnectionModal Redis Sentinel configuration', () => {
it('exposes Sentinel topology fields and safe defaults', () => {
expect(source).toContain('label: "哨兵模式"');
expect(source).toContain('name="redisSentinelMaster"');
expect(source).toContain('Sentinel master 名称');
expect(source).toContain('name="redisSentinelPassword"');
expect(source).toContain('hasRedisSentinelPassword');
expect(source).toContain('clearKey: "redisSentinelPassword"');
expect(source).toContain('form.setFieldValue("port", 26379)');
expect(source).toContain('form.setFieldValue("port", 6379)');
});
});

View File

@@ -176,6 +176,7 @@ type ConnectionSecretKey =
| "httpTunnelPassword"
| "mysqlReplicaPassword"
| "mongoReplicaPassword"
| "redisSentinelPassword"
| "opaqueURI"
| "opaqueDSN";
@@ -189,6 +190,7 @@ const createEmptyConnectionSecretClearState =
httpTunnelPassword: false,
mysqlReplicaPassword: false,
mongoReplicaPassword: false,
redisSentinelPassword: false,
opaqueURI: false,
opaqueDSN: false,
});
@@ -215,6 +217,8 @@ const resolveInitialSecretFieldValue = (
return String(config.mysqlReplicaPassword || "");
case "mongoReplicaPassword":
return String(config.mongoReplicaPassword || "");
case "redisSentinelPassword":
return String(config.redisSentinelPassword || "");
case "uri":
return String(config.uri || "");
case "dsn":
@@ -915,6 +919,19 @@ const ConnectionModal: React.FC<{
setMongoMembers([]);
}
if (fieldName === "redisTopology") {
const nextRedisTopology = String(value || "single").toLowerCase();
const currentRedisPort = Number(form.getFieldValue("port") || 0);
if (
nextRedisTopology === "sentinel" &&
(!currentRedisPort || currentRedisPort === 6379)
) {
form.setFieldValue("port", 26379);
} else if (
nextRedisTopology !== "sentinel" &&
currentRedisPort === 26379
) {
form.setFieldValue("port", 6379);
}
const supportedDbs = Array.from({ length: 16 }, (_, i) => i);
setRedisDbList(supportedDbs);
const selectedDbsRaw = form.getFieldValue("includeRedisDatabases");
@@ -1688,14 +1705,19 @@ const ConnectionModal: React.FC<{
if (parsed.hosts.some((entry) => !isValidUriHostEntry(entry))) {
return null;
}
const hostList = normalizeAddressList(parsed.hosts, 6379);
if (!hostList.length) {
return null;
}
const primary = parseHostPort(hostList[0] || "localhost:6379", 6379);
const topologyParam = String(
parsed.params.get("topology") || "",
).toLowerCase();
const isSentinelTopology = topologyParam === "sentinel";
const redisNodeDefaultPort = isSentinelTopology ? 26379 : 6379;
const hostList = normalizeAddressList(parsed.hosts, redisNodeDefaultPort);
if (!hostList.length) {
return null;
}
const primary = parseHostPort(
hostList[0] || `localhost:${redisNodeDefaultPort}`,
redisNodeDefaultPort,
);
const dbText = String(parsed.database || "")
.trim()
.replace(/^\//, "");
@@ -1711,7 +1733,7 @@ const ConnectionModal: React.FC<{
skipVerifyText === "on";
return {
host: primary?.host || "localhost",
port: primary?.port || 6379,
port: primary?.port || redisNodeDefaultPort,
user: parsed.username || "",
password: parsed.password || "",
useSSL: isRediss,
@@ -1721,11 +1743,30 @@ const ConnectionModal: React.FC<{
: "required"
: "disable",
...extractSSLPathValuesFromParams(parsed.params, type),
redisTopology:
hostList.length > 1 || topologyParam === "cluster"
redisTopology: isSentinelTopology
? "sentinel"
: hostList.length > 1 || topologyParam === "cluster"
? "cluster"
: "single",
redisHosts: hostList.slice(1),
redisSentinelMaster: isSentinelTopology
? String(
parsed.params.get("master") ||
parsed.params.get("master_name") ||
parsed.params.get("sentinel_master") ||
"",
).trim()
: "",
redisSentinelUser: isSentinelTopology
? String(
parsed.params.get("sentinel_user") ||
parsed.params.get("sentinel_username") ||
"",
).trim()
: "",
redisSentinelPassword: isSentinelTopology
? String(parsed.params.get("sentinel_password") || "")
: "",
redisDB:
Number.isFinite(dbIndex) && dbIndex >= 0 && dbIndex <= 15
? Math.trunc(dbIndex)
@@ -2037,7 +2078,7 @@ const ConnectionModal: React.FC<{
return "clickhouse://default:pass@127.0.0.1:9000/default";
}
if (dbType === "redis") {
return "redis://:pass@127.0.0.1:6379,127.0.0.2:6379/0?topology=cluster";
return "redis://:pass@127.0.0.1:6379,127.0.0.2:6379/0?topology=cluster 或 redis://:pass@10.0.0.1:26379,10.0.0.2:26379/0?topology=sentinel&master=mymaster";
}
if (dbType === "oracle") {
return "oracle://user:pass@127.0.0.1:1521/ORCLPDB1";
@@ -2144,14 +2185,33 @@ const ConnectionModal: React.FC<{
}
if (type === "redis") {
const primary = toAddress(host, port, 6379);
const clusterHosts =
values.redisTopology === "cluster"
? normalizeAddressList(values.redisHosts, 6379)
const redisTopology = String(values.redisTopology || "single");
const redisNodeDefaultPort = redisTopology === "sentinel" ? 26379 : 6379;
const primary = toAddress(host, port, redisNodeDefaultPort);
const extraRedisHosts =
redisTopology === "cluster" || redisTopology === "sentinel"
? normalizeAddressList(values.redisHosts, redisNodeDefaultPort)
: [];
const hosts = normalizeAddressList([primary, ...clusterHosts], 6379);
const hosts = normalizeAddressList(
[primary, ...extraRedisHosts],
redisNodeDefaultPort,
);
const params = new URLSearchParams();
if (hosts.length > 1 || values.redisTopology === "cluster") {
if (redisTopology === "sentinel") {
params.set("topology", "sentinel");
const sentinelMaster = String(values.redisSentinelMaster || "").trim();
if (sentinelMaster) {
params.set("master", sentinelMaster);
}
const sentinelUser = String(values.redisSentinelUser || "").trim();
if (sentinelUser) {
params.set("sentinel_user", sentinelUser);
}
const sentinelPassword = String(values.redisSentinelPassword || "");
if (sentinelPassword) {
params.set("sentinel_password", sentinelPassword);
}
} else if (hosts.length > 1 || redisTopology === "cluster") {
params.set("topology", "cluster");
}
const redisUser = String(values.user || "").trim();
@@ -2540,9 +2600,11 @@ const ConnectionModal: React.FC<{
String(config.topology || "").toLowerCase() === "replica" ||
mongoHosts.length > 0 ||
!!config.replicaSet;
const redisTopologyValue = String(config.topology || "").toLowerCase();
const redisIsSentinel = redisTopologyValue === "sentinel";
const redisIsCluster =
String(config.topology || "").toLowerCase() === "cluster" ||
redisHosts.length > 0;
!redisIsSentinel &&
(redisTopologyValue === "cluster" || redisHosts.length > 0);
const {
allowedModes: resolvedJvmAllowedModes,
preferredMode: resolvedJvmPreferredMode,
@@ -2610,8 +2672,15 @@ const ConnectionModal: React.FC<{
mysqlReplicaPassword: config.mysqlReplicaPassword || "",
mongoTopology: mongoIsReplica ? "replica" : "single",
mongoHosts: mongoHosts,
redisTopology: redisIsCluster ? "cluster" : "single",
redisTopology: redisIsSentinel
? "sentinel"
: redisIsCluster
? "cluster"
: "single",
redisHosts: redisHosts,
redisSentinelMaster: config.redisSentinelMaster || "",
redisSentinelUser: config.redisSentinelUser || "",
redisSentinelPassword: config.redisSentinelPassword || "",
mongoSrv: !!config.mongoSrv,
mongoReplicaSet: config.replicaSet || "",
mongoAuthSource: config.authSource || "",
@@ -2808,6 +2877,16 @@ const ConnectionModal: React.FC<{
clearSecret: clearSecrets.mongoReplicaPassword,
forceClear: !mongoReplicaEnabled,
});
const redisSentinelEnabled =
config.type === "redis" &&
config.topology === "sentinel" &&
values.savePassword !== false;
const redisSentinelDraft = resolveConnectionSecretDraft({
hasSecret: initialValues?.hasRedisSentinelPassword,
valueInput: config.redisSentinelPassword,
clearSecret: clearSecrets.redisSentinelPassword,
forceClear: !redisSentinelEnabled,
});
const opaqueUriDraft = resolveConnectionSecretDraft({
hasSecret: initialValues?.hasOpaqueURI,
valueInput: config.uri,
@@ -2876,6 +2955,7 @@ const ConnectionModal: React.FC<{
dsn: opaqueDsnDraft.value,
mysqlReplicaPassword: mysqlReplicaDraft.value,
mongoReplicaPassword: mongoReplicaDraft.value,
redisSentinelPassword: redisSentinelDraft.value,
},
includeDatabases: values.includeDatabases,
includeRedisDatabases: isRedisType
@@ -2889,6 +2969,7 @@ const ConnectionModal: React.FC<{
clearHttpTunnelPassword: httpTunnelDraft.clearStoredSecret,
clearMySQLReplicaPassword: mysqlReplicaDraft.clearStoredSecret,
clearMongoReplicaPassword: mongoReplicaDraft.clearStoredSecret,
clearRedisSentinelPassword: redisSentinelDraft.clearStoredSecret,
clearOpaqueURI: opaqueUriDraft.clearStoredSecret,
clearOpaqueDSN: opaqueDsnDraft.clearStoredSecret,
};
@@ -3035,6 +3116,14 @@ const ConnectionModal: React.FC<{
) {
return "测试连接前请填写新的副本集密码,或取消清除已保存副本集密码";
}
if (
clearSecrets.redisSentinelPassword &&
values.type === "redis" &&
values.redisTopology === "sentinel" &&
String(values.redisSentinelPassword ?? "") === ""
) {
return "测试连接前请填写新的 Sentinel 密码,或取消清除已保存 Sentinel 密码";
}
if (
values.type === "mongodb" &&
values.savePassword === false &&
@@ -3472,7 +3561,7 @@ const ConnectionModal: React.FC<{
}
let hosts: string[] = [];
let topology: "single" | "replica" | "cluster" | undefined;
let topology: "single" | "replica" | "cluster" | "sentinel" | undefined;
let replicaSet = "";
let authSource = "";
let readPreference = "";
@@ -3482,6 +3571,9 @@ const ConnectionModal: React.FC<{
let mongoAuthMechanism = "";
let mongoReplicaUser = "";
let mongoReplicaPassword = "";
let redisSentinelMaster = "";
let redisSentinelUser = "";
let redisSentinelPassword = "";
const savePassword =
type === "mongodb" ? mergedValues.savePassword !== false : true;
@@ -3543,15 +3635,29 @@ const ConnectionModal: React.FC<{
}
if (type === "redis") {
const clusterNodes =
mergedValues.redisTopology === "cluster"
? normalizeAddressList(mergedValues.redisHosts, defaultPort)
const redisTopology = String(mergedValues.redisTopology || "single");
const redisNodeDefaultPort = redisTopology === "sentinel" ? 26379 : defaultPort;
if (
redisTopology === "sentinel" &&
(!Number(mergedValues.port) || Number(mergedValues.port) === defaultPort)
) {
primaryPort = redisNodeDefaultPort;
}
const extraRedisNodes =
redisTopology === "cluster" || redisTopology === "sentinel"
? normalizeAddressList(mergedValues.redisHosts, redisNodeDefaultPort)
: [];
const allHosts = normalizeAddressList(
[`${primaryHost}:${primaryPort}`, ...clusterNodes],
defaultPort,
[`${primaryHost}:${primaryPort}`, ...extraRedisNodes],
redisNodeDefaultPort,
);
if (mergedValues.redisTopology === "cluster" || allHosts.length > 1) {
if (redisTopology === "sentinel") {
hosts = allHosts;
topology = "sentinel";
redisSentinelMaster = String(mergedValues.redisSentinelMaster || "").trim();
redisSentinelUser = String(mergedValues.redisSentinelUser || "").trim();
redisSentinelPassword = String(mergedValues.redisSentinelPassword || "");
} else if (redisTopology === "cluster" || allHosts.length > 1) {
hosts = allHosts;
topology = "cluster";
} else {
@@ -3661,6 +3767,9 @@ const ConnectionModal: React.FC<{
redisDB: Number.isFinite(Number(mergedValues.redisDB))
? Math.max(0, Math.min(15, Math.trunc(Number(mergedValues.redisDB))))
: 0,
redisSentinelMaster: redisSentinelMaster,
redisSentinelUser: redisSentinelUser,
redisSentinelPassword: keepPassword ? redisSentinelPassword : "",
uri: String(mergedValues.uri || "").trim(),
clickHouseProtocol:
type === "clickhouse"
@@ -3751,6 +3860,9 @@ const ConnectionModal: React.FC<{
savePassword: true,
mysqlReplicaHosts: [],
redisHosts: [],
redisSentinelMaster: "",
redisSentinelUser: "",
redisSentinelPassword: "",
mongoHosts: [],
mysqlReplicaUser: "",
mysqlReplicaPassword: "",
@@ -3810,6 +3922,9 @@ const ConnectionModal: React.FC<{
savePassword: true,
mysqlReplicaHosts: [],
redisHosts: [],
redisSentinelMaster: "",
redisSentinelUser: "",
redisSentinelPassword: "",
mongoHosts: [],
mysqlReplicaUser: "",
mysqlReplicaPassword: "",
@@ -3849,6 +3964,9 @@ const ConnectionModal: React.FC<{
savePassword: true,
mysqlReplicaHosts: [],
redisHosts: [],
redisSentinelMaster: "",
redisSentinelUser: "",
redisSentinelPassword: "",
mongoHosts: [],
mysqlReplicaUser: "",
mysqlReplicaPassword: "",
@@ -5539,21 +5657,59 @@ const ConnectionModal: React.FC<{
label: "集群模式",
description: "Redis Cluster配置多个种子节点。",
},
{
value: "sentinel",
label: "哨兵模式",
description: "通过 Sentinel 发现主节点,适合主从高可用。",
},
],
})}
{redisTopology === "cluster" && (
<Form.Item
name="redisHosts"
label="集群附加节点地址"
help="主节点使用上方主机地址这里填写其他种子节点格式host:port"
style={{ marginTop: 16, marginBottom: 0 }}
>
<Select
mode="tags"
placeholder="例如10.10.0.12:6379、10.10.0.13:6379"
tokenSeparators={[",", ";", " "]}
/>
</Form.Item>
{(redisTopology === "cluster" ||
redisTopology === "sentinel") && (
<>
<Form.Item
name="redisHosts"
label={
redisTopology === "sentinel"
? "Sentinel 附加节点地址"
: "集群附加节点地址"
}
help={
redisTopology === "sentinel"
? "上方主机地址作为第一个 Sentinel这里填写其他 Sentinel 节点格式host:port"
: "主节点使用上方主机地址这里填写其他种子节点格式host:port"
}
style={{ marginTop: 16, marginBottom: 0 }}
>
<Select
mode="tags"
placeholder={
redisTopology === "sentinel"
? "例如10.10.0.12:26379、10.10.0.13:26379"
: "例如10.10.0.12:6379、10.10.0.13:6379"
}
tokenSeparators={[",", ";", " "]}
/>
</Form.Item>
{redisTopology === "sentinel" && (
<Form.Item
name="redisSentinelMaster"
label="Sentinel master 名称"
help="填写 Sentinel 配置中的 monitor 名称,例如 mymaster。"
rules={[
createUriAwareRequiredRule(
"请输入 Sentinel master 名称",
),
]}
style={{ marginTop: 16, marginBottom: 0 }}
>
<Input
{...noAutoCapInputProps}
placeholder="例如mymaster"
/>
</Form.Item>
)}
</>
)}
</>
),
@@ -5580,6 +5736,54 @@ const ConnectionModal: React.FC<{
})}
/>
</Form.Item>
{redisTopology === "sentinel" && (
<>
<div
style={{
display: "grid",
gridTemplateColumns:
"repeat(2, minmax(0, 1fr))",
gap: 16,
}}
>
<Form.Item
name="redisSentinelUser"
label="Sentinel 用户名(可选)"
style={{ marginBottom: 0 }}
>
<Input
{...noAutoCapInputProps}
placeholder="留空表示 Sentinel 不使用 ACL 用户名"
/>
</Form.Item>
<Form.Item
name="redisSentinelPassword"
label="Sentinel 密码(可选)"
style={{ marginBottom: 0 }}
>
<Input.Password
{...noAutoCapInputProps}
placeholder={getStoredSecretPlaceholder({
hasStoredSecret:
initialValues?.hasRedisSentinelPassword,
emptyPlaceholder:
"Sentinel 自身认证密码,留空则不发送",
retainedLabel: "已保存 Sentinel 密码",
})}
/>
</Form.Item>
</div>
{renderStoredSecretControls({
fieldName: "redisSentinelPassword",
clearKey: "redisSentinelPassword",
hasStoredSecret:
initialValues?.hasRedisSentinelPassword,
clearLabel: "清除已保存 Sentinel 密码",
description:
"当前已保存 Sentinel 密码。留空表示继续沿用,输入新值表示替换。",
})}
</>
)}
</>
),
})}
@@ -6566,6 +6770,9 @@ const ConnectionModal: React.FC<{
savePassword: true,
mysqlReplicaHosts: [],
redisHosts: [],
redisSentinelMaster: "",
redisSentinelUser: "",
redisSentinelPassword: "",
mongoHosts: [],
mysqlReplicaUser: "",
mysqlReplicaPassword: "",
@@ -6685,6 +6892,21 @@ const ConnectionModal: React.FC<{
);
}
if (changed.redisTopology !== undefined) {
const nextRedisTopology = String(
changed.redisTopology || "single",
).toLowerCase();
const currentRedisPort = Number(form.getFieldValue("port") || 0);
if (
nextRedisTopology === "sentinel" &&
(!currentRedisPort || currentRedisPort === 6379)
) {
form.setFieldValue("port", 26379);
} else if (
nextRedisTopology !== "sentinel" &&
currentRedisPort === 26379
) {
form.setFieldValue("port", 6379);
}
const supportedDbs = Array.from({ length: 16 }, (_, i) => i);
setRedisDbList(supportedDbs);
const selectedDbsRaw = form.getFieldValue("includeRedisDatabases");

View File

@@ -106,6 +106,7 @@ if (typeof window !== 'undefined' && (!(window as any).go?.app?.App || !(window
httpTunnelPassword: String(httpTunnel.password ?? existingSecrets.httpTunnelPassword ?? ''),
mysqlReplicaPassword: String(config.mysqlReplicaPassword ?? existingSecrets.mysqlReplicaPassword ?? ''),
mongoReplicaPassword: String(config.mongoReplicaPassword ?? existingSecrets.mongoReplicaPassword ?? ''),
redisSentinelPassword: String(config.redisSentinelPassword ?? existingSecrets.redisSentinelPassword ?? ''),
uri: String(config.uri ?? existingSecrets.uri ?? ''),
dsn: String(config.dsn ?? existingSecrets.dsn ?? ''),
};
@@ -115,6 +116,7 @@ if (typeof window !== 'undefined' && (!(window as any).go?.app?.App || !(window
if (input?.clearHttpTunnelPassword) nextSecrets.httpTunnelPassword = '';
if (input?.clearMySQLReplicaPassword) nextSecrets.mysqlReplicaPassword = '';
if (input?.clearMongoReplicaPassword) nextSecrets.mongoReplicaPassword = '';
if (input?.clearRedisSentinelPassword) nextSecrets.redisSentinelPassword = '';
if (input?.clearOpaqueURI) nextSecrets.uri = '';
if (input?.clearOpaqueDSN) nextSecrets.dsn = '';
mockConnectionSecrets.set(nextId, nextSecrets);
@@ -132,6 +134,7 @@ if (typeof window !== 'undefined' && (!(window as any).go?.app?.App || !(window
dsn: '',
mysqlReplicaPassword: '',
mongoReplicaPassword: '',
redisSentinelPassword: '',
},
includeDatabases: Array.isArray(input?.includeDatabases) ? [...input.includeDatabases] : existing?.includeDatabases,
includeRedisDatabases: Array.isArray(input?.includeRedisDatabases) ? [...input.includeRedisDatabases] : existing?.includeRedisDatabases,
@@ -143,6 +146,7 @@ if (typeof window !== 'undefined' && (!(window as any).go?.app?.App || !(window
hasHttpTunnelPassword: resolveBrowserMockSecretFlag(httpTunnel.password, !!input?.clearHttpTunnelPassword, existing?.hasHttpTunnelPassword),
hasMySQLReplicaPassword: resolveBrowserMockSecretFlag(config.mysqlReplicaPassword, !!input?.clearMySQLReplicaPassword, existing?.hasMySQLReplicaPassword),
hasMongoReplicaPassword: resolveBrowserMockSecretFlag(config.mongoReplicaPassword, !!input?.clearMongoReplicaPassword, existing?.hasMongoReplicaPassword),
hasRedisSentinelPassword: resolveBrowserMockSecretFlag(config.redisSentinelPassword, !!input?.clearRedisSentinelPassword, existing?.hasRedisSentinelPassword),
hasOpaqueURI: resolveBrowserMockSecretFlag(config.uri, !!input?.clearOpaqueURI, existing?.hasOpaqueURI),
hasOpaqueDSN: resolveBrowserMockSecretFlag(config.dsn, !!input?.clearOpaqueDSN, existing?.hasOpaqueDSN),
};
@@ -213,6 +217,7 @@ if (typeof window !== 'undefined' && (!(window as any).go?.app?.App || !(window
httpTunnel: { ...(existing.config?.httpTunnel || {}), password: secrets.httpTunnelPassword || '' },
mysqlReplicaPassword: secrets.mysqlReplicaPassword || '',
mongoReplicaPassword: secrets.mongoReplicaPassword || '',
redisSentinelPassword: secrets.redisSentinelPassword || '',
uri: secrets.uri || '',
dsn: secrets.dsn || '',
},

View File

@@ -756,6 +756,8 @@ const sanitizeConnectionConfig = (value: unknown): ConnectionConfig => {
? "replica"
: raw.topology === "cluster"
? "cluster"
: raw.topology === "sentinel"
? "sentinel"
: "single",
mysqlReplicaUser: toTrimmedString(raw.mysqlReplicaUser),
mysqlReplicaPassword: savePassword
@@ -780,6 +782,11 @@ const sanitizeConnectionConfig = (value: unknown): ConnectionConfig => {
if (type === "redis") {
safeConfig.redisDB = normalizeIntegerInRange(raw.redisDB, 0, 0, 15);
safeConfig.redisSentinelMaster = toTrimmedString(raw.redisSentinelMaster);
safeConfig.redisSentinelUser = toTrimmedString(raw.redisSentinelUser);
safeConfig.redisSentinelPassword = savePassword
? toTrimmedString(raw.redisSentinelPassword)
: "";
}
if (type === "clickhouse") {
@@ -864,6 +871,7 @@ const sanitizeSavedConnection = (
hasHttpTunnelPassword: raw.hasHttpTunnelPassword === true,
hasMySQLReplicaPassword: raw.hasMySQLReplicaPassword === true,
hasMongoReplicaPassword: raw.hasMongoReplicaPassword === true,
hasRedisSentinelPassword: raw.hasRedisSentinelPassword === true,
hasOpaqueURI: raw.hasOpaqueURI === true,
hasOpaqueDSN: raw.hasOpaqueDSN === true,
includeDatabases:
@@ -1596,6 +1604,7 @@ const hasLegacyConnectionSecrets = (
toTrimmedString(httpTunnel.password) !== "" ||
toTrimmedString(config.mysqlReplicaPassword) !== "" ||
toTrimmedString(config.mongoReplicaPassword) !== "" ||
toTrimmedString(config.redisSentinelPassword) !== "" ||
toTrimmedString(config.uri) !== "" ||
toTrimmedString(config.dsn) !== ""
);

View File

@@ -302,7 +302,10 @@ export interface ConnectionConfig {
clickHouseProtocol?: "auto" | "http" | "native"; // ClickHouse connection protocol override
oceanBaseProtocol?: "mysql" | "oracle"; // OceanBase tenant compatibility protocol
hosts?: string[]; // Multi-host addresses: host:port
topology?: "single" | "replica" | "cluster";
topology?: "single" | "replica" | "cluster" | "sentinel";
redisSentinelMaster?: string;
redisSentinelUser?: string;
redisSentinelPassword?: string;
mysqlReplicaUser?: string;
mysqlReplicaPassword?: string;
replicaSet?: string;
@@ -335,6 +338,7 @@ export interface SavedConnection {
hasHttpTunnelPassword?: boolean;
hasMySQLReplicaPassword?: boolean;
hasMongoReplicaPassword?: boolean;
hasRedisSentinelPassword?: boolean;
hasOpaqueURI?: boolean;
hasOpaqueDSN?: boolean;
includeDatabases?: string[];