feature/mongodb-directConnection (#377)

FEATURE (mongodb): Add direct connection
This commit is contained in:
ujstor
2026-02-21 12:10:28 +01:00
committed by GitHub
parent d761c4156c
commit 22cab53dab
8 changed files with 211 additions and 19 deletions

View File

@@ -25,15 +25,16 @@ type MongodbDatabase struct {
Version tools.MongodbVersion `json:"version" gorm:"type:text;not null"`
Host string `json:"host" gorm:"type:text;not null"`
Port *int `json:"port" gorm:"type:int"`
Username string `json:"username" gorm:"type:text;not null"`
Password string `json:"password" gorm:"type:text;not null"`
Database string `json:"database" gorm:"type:text;not null"`
AuthDatabase string `json:"authDatabase" gorm:"type:text;not null;default:'admin'"`
IsHttps bool `json:"isHttps" gorm:"type:boolean;default:false"`
IsSrv bool `json:"isSrv" gorm:"column:is_srv;type:boolean;not null;default:false"`
CpuCount int `json:"cpuCount" gorm:"column:cpu_count;type:int;not null;default:1"`
Host string `json:"host" gorm:"type:text;not null"`
Port *int `json:"port" gorm:"type:int"`
Username string `json:"username" gorm:"type:text;not null"`
Password string `json:"password" gorm:"type:text;not null"`
Database string `json:"database" gorm:"type:text;not null"`
AuthDatabase string `json:"authDatabase" gorm:"type:text;not null;default:'admin'"`
IsHttps bool `json:"isHttps" gorm:"type:boolean;default:false"`
IsSrv bool `json:"isSrv" gorm:"column:is_srv;type:boolean;not null;default:false"`
DirectConnection bool `json:"directConnection" gorm:"column:direct_connection;type:boolean;not null;default:false"`
CpuCount int `json:"cpuCount" gorm:"column:cpu_count;type:int;not null;default:1"`
}
func (m *MongodbDatabase) TableName() string {
@@ -132,6 +133,7 @@ func (m *MongodbDatabase) Update(incoming *MongodbDatabase) {
m.AuthDatabase = incoming.AuthDatabase
m.IsHttps = incoming.IsHttps
m.IsSrv = incoming.IsSrv
m.DirectConnection = incoming.DirectConnection
m.CpuCount = incoming.CpuCount
if incoming.Password != "" {
@@ -457,9 +459,12 @@ func (m *MongodbDatabase) buildConnectionURI(password string) string {
authDB = "admin"
}
tlsParams := ""
extraParams := ""
if m.IsHttps {
tlsParams = "&tls=true&tlsInsecure=true"
extraParams += "&tls=true&tlsInsecure=true"
}
if m.DirectConnection {
extraParams += "&directConnection=true"
}
if m.IsSrv {
@@ -470,7 +475,7 @@ func (m *MongodbDatabase) buildConnectionURI(password string) string {
m.Host,
m.Database,
authDB,
tlsParams,
extraParams,
)
}
@@ -487,7 +492,7 @@ func (m *MongodbDatabase) buildConnectionURI(password string) string {
port,
m.Database,
authDB,
tlsParams,
extraParams,
)
}
@@ -498,9 +503,12 @@ func (m *MongodbDatabase) BuildMongodumpURI(password string) string {
authDB = "admin"
}
tlsParams := ""
extraParams := ""
if m.IsHttps {
tlsParams = "&tls=true&tlsInsecure=true"
extraParams += "&tls=true&tlsInsecure=true"
}
if m.DirectConnection {
extraParams += "&directConnection=true"
}
if m.IsSrv {
@@ -510,7 +518,7 @@ func (m *MongodbDatabase) BuildMongodumpURI(password string) string {
url.QueryEscape(password),
m.Host,
authDB,
tlsParams,
extraParams,
)
}
@@ -526,7 +534,7 @@ func (m *MongodbDatabase) BuildMongodumpURI(password string) string {
m.Host,
port,
authDB,
tlsParams,
extraParams,
)
}

View File

@@ -631,6 +631,89 @@ func Test_Validate_SrvConnection_AllowsNullPort(t *testing.T) {
assert.NoError(t, err)
}
func Test_BuildConnectionURI_WithDirectConnection_ReturnsCorrectUri(t *testing.T) {
port := 27017
model := &MongodbDatabase{
Host: "mongo.example.local",
Port: &port,
Username: "testuser",
Password: "testpass123",
Database: "mydb",
AuthDatabase: "admin",
IsHttps: false,
IsSrv: false,
DirectConnection: true,
}
uri := model.buildConnectionURI("testpass123")
assert.Contains(t, uri, "mongodb://")
assert.Contains(t, uri, "directConnection=true")
assert.Contains(t, uri, "mongo.example.local:27017")
assert.Contains(t, uri, "authSource=admin")
}
func Test_BuildConnectionURI_WithoutDirectConnection_OmitsParam(t *testing.T) {
port := 27017
model := &MongodbDatabase{
Host: "localhost",
Port: &port,
Username: "testuser",
Password: "testpass123",
Database: "mydb",
AuthDatabase: "admin",
IsHttps: false,
IsSrv: false,
DirectConnection: false,
}
uri := model.buildConnectionURI("testpass123")
assert.NotContains(t, uri, "directConnection")
}
func Test_BuildMongodumpURI_WithDirectConnection_ReturnsCorrectUri(t *testing.T) {
port := 27017
model := &MongodbDatabase{
Host: "mongo.example.local",
Port: &port,
Username: "testuser",
Password: "testpass123",
Database: "mydb",
AuthDatabase: "admin",
IsHttps: false,
IsSrv: false,
DirectConnection: true,
}
uri := model.BuildMongodumpURI("testpass123")
assert.Contains(t, uri, "mongodb://")
assert.Contains(t, uri, "directConnection=true")
assert.NotContains(t, uri, "/mydb")
}
func Test_BuildConnectionURI_WithDirectConnectionAndTls_ReturnsBothParams(t *testing.T) {
port := 27017
model := &MongodbDatabase{
Host: "mongo.example.local",
Port: &port,
Username: "testuser",
Password: "testpass123",
Database: "mydb",
AuthDatabase: "admin",
IsHttps: true,
IsSrv: false,
DirectConnection: true,
}
uri := model.buildConnectionURI("testpass123")
assert.Contains(t, uri, "directConnection=true")
assert.Contains(t, uri, "tls=true")
assert.Contains(t, uri, "tlsInsecure=true")
}
func Test_Validate_StandardConnection_RequiresPort(t *testing.T) {
model := &MongodbDatabase{
Host: "localhost",

View File

@@ -0,0 +1,9 @@
-- +goose Up
-- +goose StatementBegin
ALTER TABLE mongodb_databases ADD COLUMN direct_connection BOOLEAN NOT NULL DEFAULT FALSE;
-- +goose StatementEnd
-- +goose Down
-- +goose StatementBegin
ALTER TABLE mongodb_databases DROP COLUMN direct_connection;
-- +goose StatementEnd

View File

@@ -456,6 +456,46 @@ describe('MongodbConnectionStringParser', () => {
});
});
describe('Direct Connection Handling', () => {
it('should parse directConnection=true from URI', () => {
const result = expectSuccess(
MongodbConnectionStringParser.parse(
'mongodb://user:pass@host:27017/db?authSource=admin&directConnection=true',
),
);
expect(result.directConnection).toBe(true);
});
it('should default directConnection to false when not specified in URI', () => {
const result = expectSuccess(
MongodbConnectionStringParser.parse('mongodb://user:pass@host:27017/db'),
);
expect(result.directConnection).toBe(false);
});
it('should parse directConnection=true from key-value format', () => {
const result = expectSuccess(
MongodbConnectionStringParser.parse(
'host=localhost port=27017 database=mydb user=admin password=secret directConnection=true',
),
);
expect(result.directConnection).toBe(true);
});
it('should default directConnection to false in key-value format when not specified', () => {
const result = expectSuccess(
MongodbConnectionStringParser.parse(
'host=localhost port=27017 database=mydb user=admin password=secret',
),
);
expect(result.directConnection).toBe(false);
});
});
describe('Password Placeholder Handling', () => {
it('should treat <db_password> placeholder as empty password in URI format', () => {
const result = expectSuccess(

View File

@@ -7,6 +7,7 @@ export type ParseResult = {
authDatabase: string;
useTls: boolean;
isSrv: boolean;
directConnection: boolean;
};
export type ParseError = {
@@ -69,6 +70,7 @@ export class MongodbConnectionStringParser {
const database = decodeURIComponent(url.pathname.slice(1));
const authDatabase = this.getAuthSource(url.search) || 'admin';
const useTls = isSrv ? true : this.checkTlsMode(url.search);
const directConnection = this.checkDirectConnection(url.search);
if (!host) {
return { error: 'Host is missing from connection string' };
@@ -87,6 +89,7 @@ export class MongodbConnectionStringParser {
authDatabase,
useTls,
isSrv,
directConnection,
};
} catch (e) {
return {
@@ -133,6 +136,7 @@ export class MongodbConnectionStringParser {
}
const useTls = this.isTlsEnabled(tls);
const directConnection = params['directConnection'];
return {
host,
@@ -143,6 +147,7 @@ export class MongodbConnectionStringParser {
authDatabase,
useTls,
isSrv: false,
directConnection: directConnection === 'true',
};
} catch (e) {
return {
@@ -162,6 +167,16 @@ export class MongodbConnectionStringParser {
return params.get('authSource') || params.get('authDatabase') || undefined;
}
private static checkDirectConnection(queryString: string | undefined | null): boolean {
if (!queryString) return false;
const params = new URLSearchParams(
queryString.startsWith('?') ? queryString.slice(1) : queryString,
);
return params.get('directConnection') === 'true';
}
private static checkTlsMode(queryString: string | undefined | null): boolean {
if (!queryString) return false;

View File

@@ -11,5 +11,6 @@ export interface MongodbDatabase {
authDatabase: string;
isHttps: boolean;
isSrv: boolean;
directConnection: boolean;
cpuCount: number;
}

View File

@@ -46,7 +46,10 @@ export const EditMongoDbSpecificDataComponent = ({
const [isTestingConnection, setIsTestingConnection] = useState(false);
const [isConnectionFailed, setIsConnectionFailed] = useState(false);
const hasAdvancedValues = !!database.mongodb?.authDatabase || !!database.mongodb?.isSrv;
const hasAdvancedValues =
!!database.mongodb?.authDatabase ||
!!database.mongodb?.isSrv ||
!!database.mongodb?.directConnection;
const [isShowAdvanced, setShowAdvanced] = useState(hasAdvancedValues);
const parseFromClipboard = async () => {
@@ -80,11 +83,12 @@ export const EditMongoDbSpecificDataComponent = ({
authDatabase: result.authDatabase,
isHttps: result.useTls,
isSrv: result.isSrv,
directConnection: result.directConnection,
cpuCount: 1,
},
};
if (result.isSrv) {
if (result.isSrv || result.directConnection) {
setShowAdvanced(true);
}
@@ -407,6 +411,31 @@ export const EditMongoDbSpecificDataComponent = ({
</div>
</div>
<div className="mb-1 flex w-full items-center">
<div className="min-w-[150px]">Direct connection</div>
<div className="flex items-center">
<Switch
checked={editingDatabase.mongodb?.directConnection || false}
onChange={(checked) => {
if (!editingDatabase.mongodb) return;
setEditingDatabase({
...editingDatabase,
mongodb: { ...editingDatabase.mongodb, directConnection: checked },
});
setIsConnectionTested(false);
}}
size="small"
/>
<Tooltip
className="cursor-pointer"
title="Connect directly to a single server, skipping replica set discovery. Useful when the server is behind a load balancer, proxy, or tunnel."
>
<InfoCircleOutlined className="ml-2" style={{ color: 'gray' }} />
</Tooltip>
</div>
</div>
<div className="mb-1 flex w-full items-center">
<div className="min-w-[150px]">Auth database</div>
<Input

View File

@@ -42,6 +42,13 @@ export const ShowMongoDbSpecificDataComponent = ({ database }: Props) => {
<div>{database.mongodb?.cpuCount}</div>
</div>
{database.mongodb?.directConnection && (
<div className="mb-1 flex w-full items-center">
<div className="min-w-[150px]">Direct connection</div>
<div>Yes</div>
</div>
)}
{database.mongodb?.authDatabase && (
<div className="mb-1 flex w-full items-center">
<div className="min-w-[150px]">Auth database</div>