diff --git a/packages/web/src/settings/ConnectionDriverFields.svelte b/packages/web/src/settings/ConnectionDriverFields.svelte index 8830861d3..32076ef62 100644 --- a/packages/web/src/settings/ConnectionDriverFields.svelte +++ b/packages/web/src/settings/ConnectionDriverFields.svelte @@ -19,6 +19,7 @@ import { getConnectionLabel } from 'dbgate-tools'; import { _t } from '../translations'; import FormFileInputField from '../forms/FormFileInputField.svelte'; + import FormClusterNodesField from '../forms/FormClusterNodesField.svelte'; export let getDatabaseList; export let currentConnection; @@ -122,6 +123,24 @@ {/key} {/if} +{#if driver?.showConnectionField('clusterNodes', $values, showConnectionFieldArgs)} + +{/if} + +{#if driver?.showConnectionField('autoDetectNatMap', $values, showConnectionFieldArgs)} + +{/if} + {#if driver?.showConnectionField('databaseFile', $values, showConnectionFieldArgs)} {#if electron && !driver?.dialect?.useServerDatabaseFile} { + const single = new Redis({ + host: typeof seed === 'string' ? new URL(seed).hostname : seed.host, + port: typeof seed === 'string' ? Number(new URL(seed).port || 6379) : seed.port, + ...redisOptions, + // Make these connections quick and disposable + lazyConnect: false, + enableReadyCheck: false, + maxRetriesPerRequest: 0, + }); + + try { + const nodes = await single.cluster('nodes'); // text blob + const line = nodes.split(/\r?\n/).find((l) => /\bmyself\b/.test(l)); + if (!line) return; + + // Example addr token: "172.18.0.3:6379@16379" or "redis-node-0:6379@16379" + const addrToken = line.split(' ')[1]; + const hostPort = addrToken.split('@')[0]; // strip bus port + const [advHost, advPortStr] = hostPort.split(':'); + const advKey = `${advHost}:${Number(advPortStr)}`; + + const extHost = typeof seed === 'string' ? new URL(seed).hostname : seed.host; + const extPort = typeof seed === 'string' ? Number(new URL(seed).port || 6379) : seed.port; + + natMap[advKey] = { host: extHost, port: extPort }; + } catch { + // ignore this seed if it fails; others may still succeed + } finally { + single.disconnect(); + } + }) + ); + + return natMap; +} + /** @type {import('dbgate-types').EngineDriver} */ const driver = { ...driverBase, @@ -92,6 +135,9 @@ const driver = { treeKeySeparator, ssl, skipSetName, + authType, + clusterNodes, + autoDetectNatMap, }) { let db = 0; let client; @@ -100,6 +146,26 @@ const driver = { if (!skipSetName) { await client.client('SETNAME', 'dbgate'); } + } else if (authType === 'cluster' && isProApp && isProApp()) { + const redisOptions = { + user, + password, + }; + + let seeds = []; + try { + seeds = JSON.parse(clusterNodes); + } catch {} + if (!Array.isArray(seeds) || seeds.length === 0) { + throw new Error('Cluster nodes must be a non-empty array of host:port or objects with host and port'); + } + + const natMap = autoDetectNatMap ? await buildNatMapFromSeeds(seeds, redisOptions) : undefined; + + client = new Redis.Cluster(seeds, { + redisOptions, + natMap, + }); } else { if (_.isString(database) && database.startsWith('db')) db = parseInt(database.substring(2)); if (_.isNumber(database)) db = database; @@ -487,6 +553,20 @@ const driver = { async close(dbhan) { return dbhan.client.quit(); }, + + getAuthTypes() { + if (isProApp && isProApp()) { + return [ + { name: 'node', title: 'Single Redis node' }, + { name: 'cluster', title: 'Redis Cluster' }, + ]; + } + return null; + }, +}; + +driver.initialize = (dbgateEnv) => { + isProApp = dbgateEnv.isProApp; }; module.exports = driver; diff --git a/plugins/dbgate-plugin-redis/src/backend/index.js b/plugins/dbgate-plugin-redis/src/backend/index.js index cdad41316..100b1962a 100644 --- a/plugins/dbgate-plugin-redis/src/backend/index.js +++ b/plugins/dbgate-plugin-redis/src/backend/index.js @@ -3,4 +3,7 @@ const driver = require('./driver'); module.exports = { packageName: 'dbgate-plugin-redis', drivers: [driver], + initialize(dbgateEnv) { + driver.initialize(dbgateEnv); + }, }; diff --git a/plugins/dbgate-plugin-redis/src/frontend/driver.js b/plugins/dbgate-plugin-redis/src/frontend/driver.js index d440ad2e7..3182cbc25 100644 --- a/plugins/dbgate-plugin-redis/src/frontend/driver.js +++ b/plugins/dbgate-plugin-redis/src/frontend/driver.js @@ -23,10 +23,13 @@ const driver = { title: 'Redis', defaultPort: 6379, editorMode: 'text', + authTypeFirst: true, databaseEngineTypes: ['keyvalue'], supportedCreateDatabase: false, getQuerySplitterOptions: () => redisSplitterOptions, databaseUrlPlaceholder: 'e.g. redis://:authpassword@127.0.0.1:6380/4', + authTypeLabel: 'Connection mode', + defaultAuthTypeName: 'node', supportedKeyTypes: [ { name: 'string', @@ -76,10 +79,14 @@ const driver = { ], showConnectionField: (field, values) => { - if (field == 'useDatabaseUrl') return true; + if (field == 'useDatabaseUrl') return values.authType != 'cluster'; + if (field == 'authType') return !values.useDatabaseUrl; if (values.useDatabaseUrl) { return ['databaseUrl', 'isReadOnly', 'treeKeySeparator'].includes(field); } + if (values.authType == 'cluster') { + return ['user', 'password', 'isReadOnly', 'treeKeySeparator', 'clusterNodes', 'autoDetectNatMap'].includes(field); + } return ['server', 'port', 'user', 'password', 'isReadOnly', 'treeKeySeparator'].includes(field); },