diff --git a/frontend/src/components/ConnectionModal.tsx b/frontend/src/components/ConnectionModal.tsx index f64cc49..8700bea 100644 --- a/frontend/src/components/ConnectionModal.tsx +++ b/frontend/src/components/ConnectionModal.tsx @@ -63,6 +63,11 @@ import { resolveConnectionSecretDraft } from "../utils/connectionSecretDraft"; import { getCustomConnectionDsnValidationMessage } from "../utils/customConnectionDsn"; import { mergeParsedUriValuesForForm } from "../utils/connectionUriMerge"; import { buildRpcConnectionConfig } from "../utils/connectionRpcConfig"; +import { + buildRedisUriFromValues, + parseRedisUriToFormValues, + resolveRedisConfigDraft, +} from "../utils/redisConnectionUri"; import { CONNECTION_TYPE_GROUPS, getAllConnectionTypeCatalogItems, @@ -1484,85 +1489,7 @@ const ConnectionModal: React.FC<{ } if (type === "redis") { - const parsed = - parseMultiHostUri(trimmedUri, "redis") || - parseMultiHostUri(trimmedUri, "rediss"); - if (!parsed) { - return null; - } - if (!parsed.hosts.length || parsed.hosts.length > MAX_URI_HOSTS) { - return null; - } - if (parsed.hosts.some((entry) => !isValidUriHostEntry(entry))) { - return null; - } - 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(/^\//, ""); - const dbIndex = Number(dbText); - const isRediss = trimmedUri.toLowerCase().startsWith("rediss://"); - const skipVerifyText = String(parsed.params.get("skip_verify") || "") - .trim() - .toLowerCase(); - const skipVerify = - skipVerifyText === "1" || - skipVerifyText === "true" || - skipVerifyText === "yes" || - skipVerifyText === "on"; - return { - host: primary?.host || "localhost", - port: primary?.port || redisNodeDefaultPort, - user: parsed.username || "", - password: parsed.password || "", - useSSL: isRediss, - sslMode: isRediss - ? skipVerify - ? "skip-verify" - : "required" - : "disable", - ...extractSSLPathValuesFromParams(parsed.params, type), - 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) - : 0, - }; + return parseRedisUriToFormValues(trimmedUri); } if (type === "mongodb") { @@ -1976,62 +1903,7 @@ const ConnectionModal: React.FC<{ } if (type === "redis") { - 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, ...extraRedisHosts], - redisNodeDefaultPort, - ); - const params = new URLSearchParams(); - 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(); - const redisPassword = String(values.password || ""); - let redisAuth = ""; - if (redisUser || redisPassword) { - const encodedPassword = redisPassword - ? encodeURIComponent(redisPassword) - : ""; - redisAuth = redisUser - ? `${encodeURIComponent(redisUser)}${redisPassword ? `:${encodedPassword}` : ""}@` - : `:${encodedPassword}@`; - } - const redisDB = Number.isFinite(Number(values.redisDB)) - ? Math.max(0, Math.min(15, Math.trunc(Number(values.redisDB)))) - : 0; - const dbPath = `/${redisDB}`; - if (values.useSSL) { - const mode = String(values.sslMode || "preferred") - .trim() - .toLowerCase(); - if (mode === "skip-verify" || mode === "preferred") { - params.set("skip_verify", "true"); - } - } - appendSSLPathParamsForUri(params, type, values); - const query = params.toString(); - const scheme = values.useSSL ? "rediss" : "redis"; - return `${scheme}://${redisAuth}${hosts.join(",")}${dbPath}${query ? `?${query}` : ""}`; + return buildRedisUriFromValues(values); } if (isFileDatabaseType(type)) { @@ -3426,37 +3298,19 @@ const ConnectionModal: React.FC<{ } if (type === "redis") { - 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}`, ...extraRedisNodes], - redisNodeDefaultPort, + const redisDraft = resolveRedisConfigDraft( + mergedValues, + primaryHost, + primaryPort, + defaultPort, ); - 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 { - topology = "single"; - } - mergedValues.redisDB = Number.isFinite(Number(mergedValues.redisDB)) - ? Math.max(0, Math.min(15, Math.trunc(Number(mergedValues.redisDB)))) - : 0; + primaryPort = redisDraft.primaryPort; + hosts = redisDraft.hosts; + topology = redisDraft.topology; + redisSentinelMaster = redisDraft.redisSentinelMaster; + redisSentinelUser = redisDraft.redisSentinelUser; + redisSentinelPassword = redisDraft.redisSentinelPassword; + mergedValues.redisDB = redisDraft.redisDB; } const sshConfig = mergedValues.useSSH diff --git a/frontend/src/utils/redisConnectionUri.test.ts b/frontend/src/utils/redisConnectionUri.test.ts new file mode 100644 index 0000000..a5b73eb --- /dev/null +++ b/frontend/src/utils/redisConnectionUri.test.ts @@ -0,0 +1,85 @@ +import { describe, expect, it } from 'vitest'; + +import { + buildRedisUriFromValues, + parseRedisUriToFormValues, + resolveRedisConfigDraft, +} from './redisConnectionUri'; + +describe('redisConnectionUri', () => { + it('parses Redis Sentinel URI into form values without dropping topology fields', () => { + const result = parseRedisUriToFormValues( + 'rediss://default:redis%40secret@sentinel-a.local:26379,sentinel-b.local/3?topology=sentinel&master=mymaster&sentinel_user=ops&sentinel_password=s%40p&skip_verify=true&sslCAPath=C%3A%2Fcerts%2Fca.pem', + ); + + expect(result).toMatchObject({ + host: 'sentinel-a.local', + port: 26379, + user: 'default', + password: 'redis@secret', + useSSL: true, + sslMode: 'skip-verify', + sslCAPath: 'C:/certs/ca.pem', + redisTopology: 'sentinel', + redisHosts: ['sentinel-b.local:26379'], + redisSentinelMaster: 'mymaster', + redisSentinelUser: 'ops', + redisSentinelPassword: 's@p', + redisDB: 3, + }); + }); + + it('builds Redis Sentinel URI with Sentinel credentials separated from Redis auth', () => { + expect(buildRedisUriFromValues({ + host: 'sentinel-a.local', + port: 26379, + redisHosts: ['sentinel-b.local', 'sentinel-b.local:26379'], + redisTopology: 'sentinel', + user: 'default', + password: 'redis secret', + redisSentinelMaster: 'mymaster', + redisSentinelUser: 'sentinel-user', + redisSentinelPassword: 'sentinel secret', + redisDB: 6, + useSSL: true, + sslMode: 'required', + sslCAPath: 'C:/certs/ca.pem', + })).toBe( + 'rediss://default:redis%20secret@sentinel-a.local:26379,sentinel-b.local:26379/6?topology=sentinel&master=mymaster&sentinel_user=sentinel-user&sentinel_password=sentinel+secret&sslCAPath=C%3A%2Fcerts%2Fca.pem', + ); + }); + + it('resolves Redis config draft for cluster and Sentinel save payloads', () => { + expect(resolveRedisConfigDraft({ + redisTopology: 'cluster', + redisHosts: ['redis-b.local', 'redis-c.local:6380'], + redisDB: 2, + }, 'redis-a.local', 6379, 6379)).toEqual({ + primaryPort: 6379, + hosts: ['redis-a.local:6379', 'redis-b.local:6379', 'redis-c.local:6380'], + topology: 'cluster', + redisSentinelMaster: '', + redisSentinelUser: '', + redisSentinelPassword: '', + redisDB: 2, + }); + + expect(resolveRedisConfigDraft({ + redisTopology: 'sentinel', + port: 6379, + redisHosts: ['sentinel-b.local'], + redisSentinelMaster: 'mymaster', + redisSentinelUser: 'ops', + redisSentinelPassword: 'sentinel-pass', + redisDB: 99, + }, 'sentinel-a.local', 6379, 6379)).toEqual({ + primaryPort: 26379, + hosts: ['sentinel-a.local:26379', 'sentinel-b.local:26379'], + topology: 'sentinel', + redisSentinelMaster: 'mymaster', + redisSentinelUser: 'ops', + redisSentinelPassword: 'sentinel-pass', + redisDB: 0, + }); + }); +}); diff --git a/frontend/src/utils/redisConnectionUri.ts b/frontend/src/utils/redisConnectionUri.ts new file mode 100644 index 0000000..a03a78d --- /dev/null +++ b/frontend/src/utils/redisConnectionUri.ts @@ -0,0 +1,452 @@ +import type { ConnectionConfig } from '../types'; + +export type RedisTopology = Extract< + NonNullable, + 'single' | 'cluster' | 'sentinel' +>; + +export interface RedisUriFormValues { + host: string; + port: number; + user: string; + password: string; + useSSL: boolean; + sslMode: 'required' | 'skip-verify' | 'disable'; + sslCAPath?: string; + sslCertPath?: string; + sslKeyPath?: string; + redisTopology: RedisTopology; + redisHosts: string[]; + redisSentinelMaster: string; + redisSentinelUser: string; + redisSentinelPassword: string; + redisDB: number; +} + +export interface RedisConfigDraft { + primaryPort: number; + hosts: string[]; + topology: RedisTopology; + redisSentinelMaster: string; + redisSentinelUser: string; + redisSentinelPassword: string; + redisDB: number; +} + +const REDIS_DEFAULT_PORT = 6379; +const REDIS_SENTINEL_DEFAULT_PORT = 26379; +const MAX_URI_HOSTS = 32; + +const parseHostPort = ( + raw: string, + defaultPort: number, +): { host: string; port: number } | null => { + const text = String(raw || '').trim(); + if (!text) { + return null; + } + if (text.startsWith('[')) { + const closingBracket = text.indexOf(']'); + if (closingBracket > 0) { + const host = text.slice(1, closingBracket).trim(); + const portText = text + .slice(closingBracket + 1) + .trim() + .replace(/^:/, ''); + const parsedPort = Number(portText); + return { + host: host || 'localhost', + port: + Number.isFinite(parsedPort) && parsedPort > 0 && parsedPort <= 65535 + ? parsedPort + : defaultPort, + }; + } + } + + const colonCount = (text.match(/:/g) || []).length; + if (colonCount === 1) { + const splitIndex = text.lastIndexOf(':'); + const host = text.slice(0, splitIndex).trim(); + const portText = text.slice(splitIndex + 1).trim(); + const parsedPort = Number(portText); + return { + host: host || 'localhost', + port: + Number.isFinite(parsedPort) && parsedPort > 0 && parsedPort <= 65535 + ? parsedPort + : defaultPort, + }; + } + + return { host: text, port: defaultPort }; +}; + +const toAddress = (host: string, port: number, defaultPort: number) => { + const safeHost = String(host || '').trim() || 'localhost'; + const safePort = + Number.isFinite(Number(port)) && Number(port) > 0 + ? Number(port) + : defaultPort; + return `${safeHost}:${safePort}`; +}; + +const normalizeAddressList = ( + rawList: unknown, + defaultPort: number, +): string[] => { + const list = Array.isArray(rawList) ? rawList : []; + const seen = new Set(); + const result: string[] = []; + list.forEach((entry) => { + const parsed = parseHostPort(String(entry || ''), defaultPort); + if (!parsed) { + return; + } + const normalized = toAddress(parsed.host, parsed.port, defaultPort); + if (seen.has(normalized)) { + return; + } + seen.add(normalized); + result.push(normalized); + }); + return result; +}; + +const isValidUriHostEntry = (entry: string): boolean => { + const text = String(entry || '').trim(); + if (!text) return false; + if (text.length > 255) return false; + return !/[()\\/\s]/.test(text); +}; + +const safeDecode = (text: string) => { + try { + return decodeURIComponent(text); + } catch { + return text; + } +}; + +const parseMultiHostUri = (uriText: string, expectedScheme: string) => { + const prefix = `${expectedScheme}://`; + if (!uriText.toLowerCase().startsWith(prefix)) { + return null; + } + let rest = uriText.slice(prefix.length); + const hashIndex = rest.indexOf('#'); + if (hashIndex >= 0) { + rest = rest.slice(0, hashIndex); + } + let queryText = ''; + const queryIndex = rest.indexOf('?'); + if (queryIndex >= 0) { + queryText = rest.slice(queryIndex + 1); + rest = rest.slice(0, queryIndex); + } + + let pathText = ''; + const slashIndex = rest.indexOf('/'); + if (slashIndex >= 0) { + pathText = rest.slice(slashIndex + 1); + rest = rest.slice(0, slashIndex); + } + + let hostText = rest; + let username = ''; + let password = ''; + const atIndex = rest.lastIndexOf('@'); + if (atIndex >= 0) { + const userInfo = rest.slice(0, atIndex); + hostText = rest.slice(atIndex + 1); + const colonIndex = userInfo.indexOf(':'); + if (colonIndex >= 0) { + username = safeDecode(userInfo.slice(0, colonIndex)); + password = safeDecode(userInfo.slice(colonIndex + 1)); + } else { + username = safeDecode(userInfo); + } + } + + const hosts = hostText + .split(',') + .map((item) => item.trim()) + .filter(Boolean); + + return { + username, + password, + hosts, + database: safeDecode(pathText), + params: new URLSearchParams(queryText), + }; +}; + +const firstConnectionParamValue = ( + params: URLSearchParams, + names: string[], +): string => { + for (const name of names) { + const value = String(params.get(name) || '').trim(); + if (value) return value; + } + return ''; +}; + +const extractRedisSSLPathValuesFromParams = ( + params: URLSearchParams, +): Pick => { + const caPath = firstConnectionParamValue(params, [ + 'sslCAPath', + 'ssl_ca_path', + 'sslrootcert', + 'sslRootCert', + 'tlsCAFile', + 'caFile', + 'certificate', + 'servercertificate', + 'serverCertificate', + ]); + const certPath = firstConnectionParamValue(params, [ + 'sslCertPath', + 'ssl_cert_path', + 'SSL_CERT_PATH', + 'sslcert', + 'sslCert', + 'tlsCertificateFile', + ]); + const keyPath = firstConnectionParamValue(params, [ + 'sslKeyPath', + 'ssl_key_path', + 'SSL_KEY_PATH', + 'sslkey', + 'sslKey', + 'tlsKeyFile', + ]); + return { + ...(caPath ? { sslCAPath: caPath } : {}), + ...(certPath ? { sslCertPath: certPath } : {}), + ...(keyPath ? { sslKeyPath: keyPath } : {}), + }; +}; + +const appendRedisSSLPathParamsForUri = ( + params: URLSearchParams, + values: Record, +) => { + const caPath = String(values.sslCAPath || '').trim(); + const certPath = String(values.sslCertPath || '').trim(); + const keyPath = String(values.sslKeyPath || '').trim(); + if (caPath) { + params.set('sslCAPath', caPath); + } + if (certPath) { + params.set('sslCertPath', certPath); + } + if (keyPath) { + params.set('sslKeyPath', keyPath); + } +}; + +const normalizeRedisDB = (value: unknown): number => { + const parsed = Number(value); + return Number.isFinite(parsed) && parsed >= 0 && parsed <= 15 + ? Math.trunc(parsed) + : 0; +}; + +const normalizeRedisTopology = (value: unknown): RedisTopology => { + const text = String(value || '').trim().toLowerCase(); + if (text === 'cluster' || text === 'sentinel') { + return text; + } + return 'single'; +}; + +export const parseRedisUriToFormValues = ( + uriText: string, +): RedisUriFormValues | null => { + const trimmedUri = String(uriText || '').trim(); + const parsed = + parseMultiHostUri(trimmedUri, 'redis') || + parseMultiHostUri(trimmedUri, 'rediss'); + if (!parsed) { + return null; + } + if (!parsed.hosts.length || parsed.hosts.length > MAX_URI_HOSTS) { + return null; + } + if (parsed.hosts.some((entry) => !isValidUriHostEntry(entry))) { + return null; + } + const topologyParam = String(parsed.params.get('topology') || '').toLowerCase(); + const isSentinelTopology = topologyParam === 'sentinel'; + const redisNodeDefaultPort = isSentinelTopology + ? REDIS_SENTINEL_DEFAULT_PORT + : REDIS_DEFAULT_PORT; + 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(/^\//, ''); + const isRediss = trimmedUri.toLowerCase().startsWith('rediss://'); + const skipVerifyText = String(parsed.params.get('skip_verify') || '') + .trim() + .toLowerCase(); + const skipVerify = + skipVerifyText === '1' || + skipVerifyText === 'true' || + skipVerifyText === 'yes' || + skipVerifyText === 'on'; + return { + host: primary?.host || 'localhost', + port: primary?.port || redisNodeDefaultPort, + user: parsed.username || '', + password: parsed.password || '', + useSSL: isRediss, + sslMode: isRediss ? (skipVerify ? 'skip-verify' : 'required') : 'disable', + ...extractRedisSSLPathValuesFromParams(parsed.params), + 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: normalizeRedisDB(dbText), + }; +}; + +export const buildRedisUriFromValues = (values: Record): string => { + const redisTopology = normalizeRedisTopology(values.redisTopology); + const redisNodeDefaultPort = + redisTopology === 'sentinel' + ? REDIS_SENTINEL_DEFAULT_PORT + : REDIS_DEFAULT_PORT; + const primary = toAddress( + String(values.host || '').trim() || 'localhost', + Number(values.port || redisNodeDefaultPort), + redisNodeDefaultPort, + ); + const extraRedisHosts = + redisTopology === 'cluster' || redisTopology === 'sentinel' + ? normalizeAddressList(values.redisHosts, redisNodeDefaultPort) + : []; + const hosts = normalizeAddressList( + [primary, ...extraRedisHosts], + redisNodeDefaultPort, + ); + const params = new URLSearchParams(); + 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(); + const redisPassword = String(values.password || ''); + let redisAuth = ''; + if (redisUser || redisPassword) { + const encodedPassword = redisPassword + ? encodeURIComponent(redisPassword) + : ''; + redisAuth = redisUser + ? `${encodeURIComponent(redisUser)}${redisPassword ? `:${encodedPassword}` : ''}@` + : `:${encodedPassword}@`; + } + const redisDB = normalizeRedisDB(values.redisDB); + if (values.useSSL) { + const mode = String(values.sslMode || 'preferred') + .trim() + .toLowerCase(); + if (mode === 'skip-verify' || mode === 'preferred') { + params.set('skip_verify', 'true'); + } + } + appendRedisSSLPathParamsForUri(params, values); + const query = params.toString(); + const scheme = values.useSSL ? 'rediss' : 'redis'; + return `${scheme}://${redisAuth}${hosts.join(',')}/${redisDB}${query ? `?${query}` : ''}`; +}; + +export const resolveRedisConfigDraft = ( + values: Record, + primaryHost: string, + primaryPort: number, + defaultPort: number, +): RedisConfigDraft => { + const redisTopology = normalizeRedisTopology(values.redisTopology); + const redisNodeDefaultPort = + redisTopology === 'sentinel' + ? REDIS_SENTINEL_DEFAULT_PORT + : defaultPort; + const normalizedPrimaryPort = + redisTopology === 'sentinel' && + (!Number(values.port) || Number(values.port) === defaultPort) + ? redisNodeDefaultPort + : primaryPort; + const extraRedisNodes = + redisTopology === 'cluster' || redisTopology === 'sentinel' + ? normalizeAddressList(values.redisHosts, redisNodeDefaultPort) + : []; + const allHosts = normalizeAddressList( + [`${primaryHost}:${normalizedPrimaryPort}`, ...extraRedisNodes], + redisNodeDefaultPort, + ); + + if (redisTopology === 'sentinel') { + return { + primaryPort: normalizedPrimaryPort, + hosts: allHosts, + topology: 'sentinel', + redisSentinelMaster: String(values.redisSentinelMaster || '').trim(), + redisSentinelUser: String(values.redisSentinelUser || '').trim(), + redisSentinelPassword: String(values.redisSentinelPassword || ''), + redisDB: normalizeRedisDB(values.redisDB), + }; + } + + return { + primaryPort: normalizedPrimaryPort, + hosts: redisTopology === 'cluster' || allHosts.length > 1 ? allHosts : [], + topology: redisTopology === 'cluster' || allHosts.length > 1 ? 'cluster' : 'single', + redisSentinelMaster: '', + redisSentinelUser: '', + redisSentinelPassword: '', + redisDB: normalizeRedisDB(values.redisDB), + }; +};