diff --git a/backend/internal/features/databases/databases/mongodb/model.go b/backend/internal/features/databases/databases/mongodb/model.go index ca0b98b..ff2c340 100644 --- a/backend/internal/features/databases/databases/mongodb/model.go +++ b/backend/internal/features/databases/databases/mongodb/model.go @@ -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, ) } diff --git a/backend/internal/features/databases/databases/mongodb/model_test.go b/backend/internal/features/databases/databases/mongodb/model_test.go index cf48ba8..c8164a6 100644 --- a/backend/internal/features/databases/databases/mongodb/model_test.go +++ b/backend/internal/features/databases/databases/mongodb/model_test.go @@ -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", diff --git a/backend/migrations/20260218120000_add_mongodb_direct_connection.sql b/backend/migrations/20260218120000_add_mongodb_direct_connection.sql new file mode 100644 index 0000000..d4072f8 --- /dev/null +++ b/backend/migrations/20260218120000_add_mongodb_direct_connection.sql @@ -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 diff --git a/frontend/src/entity/databases/model/mongodb/MongodbConnectionStringParser.test.ts b/frontend/src/entity/databases/model/mongodb/MongodbConnectionStringParser.test.ts index cd00168..e900387 100644 --- a/frontend/src/entity/databases/model/mongodb/MongodbConnectionStringParser.test.ts +++ b/frontend/src/entity/databases/model/mongodb/MongodbConnectionStringParser.test.ts @@ -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 placeholder as empty password in URI format', () => { const result = expectSuccess( diff --git a/frontend/src/entity/databases/model/mongodb/MongodbConnectionStringParser.ts b/frontend/src/entity/databases/model/mongodb/MongodbConnectionStringParser.ts index 91eacf3..effa296 100644 --- a/frontend/src/entity/databases/model/mongodb/MongodbConnectionStringParser.ts +++ b/frontend/src/entity/databases/model/mongodb/MongodbConnectionStringParser.ts @@ -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; diff --git a/frontend/src/entity/databases/model/mongodb/MongodbDatabase.ts b/frontend/src/entity/databases/model/mongodb/MongodbDatabase.ts index 36fdbe9..acec5d2 100644 --- a/frontend/src/entity/databases/model/mongodb/MongodbDatabase.ts +++ b/frontend/src/entity/databases/model/mongodb/MongodbDatabase.ts @@ -11,5 +11,6 @@ export interface MongodbDatabase { authDatabase: string; isHttps: boolean; isSrv: boolean; + directConnection: boolean; cpuCount: number; } diff --git a/frontend/src/features/databases/ui/edit/EditMongoDbSpecificDataComponent.tsx b/frontend/src/features/databases/ui/edit/EditMongoDbSpecificDataComponent.tsx index be9f94f..0234d87 100644 --- a/frontend/src/features/databases/ui/edit/EditMongoDbSpecificDataComponent.tsx +++ b/frontend/src/features/databases/ui/edit/EditMongoDbSpecificDataComponent.tsx @@ -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 = ({ +
+
Direct connection
+
+ { + if (!editingDatabase.mongodb) return; + + setEditingDatabase({ + ...editingDatabase, + mongodb: { ...editingDatabase.mongodb, directConnection: checked }, + }); + setIsConnectionTested(false); + }} + size="small" + /> + + + +
+
+
Auth database
{
{database.mongodb?.cpuCount}
+ {database.mongodb?.directConnection && ( +
+
Direct connection
+
Yes
+
+ )} + {database.mongodb?.authDatabase && (
Auth database