Compare commits

...

5 Commits

Author SHA1 Message Date
47cfc4b3da feat(config): Add PopulateFromDSN method to parse connection DSN 2026-01-02 23:23:20 +02:00
0e8ae75daf feat(config): Add SetConfig method to Manager 2026-01-02 23:21:27 +02:00
ce092d1c62 feat(config): Add default database manager configuration 2026-01-02 23:01:43 +02:00
871dd2e374 feat(config): Add SaveConfig method to persist configuration 2026-01-02 22:51:19 +02:00
Hein
ebd03d10ad feat(dbmanager): 🚑 Singleton for the database manager
Some checks failed
Build , Vet Test, and Lint / Run Vet Tests (1.24.x) (push) Successful in -21m54s
Build , Vet Test, and Lint / Run Vet Tests (1.23.x) (push) Successful in -21m29s
Build , Vet Test, and Lint / Build (push) Successful in -25m3s
Build , Vet Test, and Lint / Lint Code (push) Successful in -24m34s
Tests / Integration Tests (push) Failing after -25m39s
Tests / Unit Tests (push) Successful in -25m26s
2026-01-02 16:47:38 +02:00
3 changed files with 269 additions and 0 deletions

View File

@@ -2,6 +2,9 @@ package config
import (
"fmt"
"net/url"
"strconv"
"strings"
"time"
)
@@ -91,6 +94,160 @@ func (c *DBManagerConfig) ToManagerConfig() interface{} {
return c
}
// PopulateFromDSN parses a DSN and populates the connection fields
func (cc *DBConnectionConfig) PopulateFromDSN() error {
if cc.DSN == "" {
return nil // Nothing to populate
}
switch cc.Type {
case "postgres":
return cc.populatePostgresDSN()
case "mongodb":
return cc.populateMongoDSN()
case "mssql":
return cc.populateMSSQLDSN()
case "sqlite":
return cc.populateSQLiteDSN()
default:
return fmt.Errorf("cannot parse DSN for unsupported database type: %s", cc.Type)
}
}
// populatePostgresDSN parses PostgreSQL DSN format
// Example: host=localhost port=5432 user=postgres password=secret dbname=mydb sslmode=disable
func (cc *DBConnectionConfig) populatePostgresDSN() error {
parts := strings.Fields(cc.DSN)
for _, part := range parts {
kv := strings.SplitN(part, "=", 2)
if len(kv) != 2 {
continue
}
key, value := kv[0], kv[1]
switch key {
case "host":
cc.Host = value
case "port":
port, err := strconv.Atoi(value)
if err != nil {
return fmt.Errorf("invalid port in DSN: %w", err)
}
cc.Port = port
case "user":
cc.User = value
case "password":
cc.Password = value
case "dbname":
cc.Database = value
case "sslmode":
cc.SSLMode = value
case "search_path":
cc.Schema = value
}
}
return nil
}
// populateMongoDSN parses MongoDB DSN format
// Example: mongodb://user:password@host:port/database?authSource=admin&replicaSet=rs0
func (cc *DBConnectionConfig) populateMongoDSN() error {
u, err := url.Parse(cc.DSN)
if err != nil {
return fmt.Errorf("invalid MongoDB DSN: %w", err)
}
// Extract user and password
if u.User != nil {
cc.User = u.User.Username()
if password, ok := u.User.Password(); ok {
cc.Password = password
}
}
// Extract host and port
if u.Host != "" {
host := u.Host
if strings.Contains(host, ":") {
hostPort := strings.SplitN(host, ":", 2)
cc.Host = hostPort[0]
if port, err := strconv.Atoi(hostPort[1]); err == nil {
cc.Port = port
}
} else {
cc.Host = host
}
}
// Extract database
if u.Path != "" {
cc.Database = strings.TrimPrefix(u.Path, "/")
}
// Extract query parameters
params := u.Query()
if authSource := params.Get("authSource"); authSource != "" {
cc.AuthSource = authSource
}
if replicaSet := params.Get("replicaSet"); replicaSet != "" {
cc.ReplicaSet = replicaSet
}
if readPref := params.Get("readPreference"); readPref != "" {
cc.ReadPreference = readPref
}
return nil
}
// populateMSSQLDSN parses MSSQL DSN format
// Example: sqlserver://username:password@host:port?database=dbname&schema=dbo
func (cc *DBConnectionConfig) populateMSSQLDSN() error {
u, err := url.Parse(cc.DSN)
if err != nil {
return fmt.Errorf("invalid MSSQL DSN: %w", err)
}
// Extract user and password
if u.User != nil {
cc.User = u.User.Username()
if password, ok := u.User.Password(); ok {
cc.Password = password
}
}
// Extract host and port
if u.Host != "" {
host := u.Host
if strings.Contains(host, ":") {
hostPort := strings.SplitN(host, ":", 2)
cc.Host = hostPort[0]
if port, err := strconv.Atoi(hostPort[1]); err == nil {
cc.Port = port
}
} else {
cc.Host = host
}
}
// Extract query parameters
params := u.Query()
if database := params.Get("database"); database != "" {
cc.Database = database
}
if schema := params.Get("schema"); schema != "" {
cc.Schema = schema
}
return nil
}
// populateSQLiteDSN parses SQLite DSN format
// Example: /path/to/database.db or :memory:
func (cc *DBConnectionConfig) populateSQLiteDSN() error {
cc.FilePath = cc.DSN
return nil
}
// Validate validates the DBManager configuration
func (c *DBManagerConfig) Validate() error {
if len(c.Connections) == 0 {

View File

@@ -97,6 +97,29 @@ func (m *Manager) GetConfig() (*Config, error) {
return &cfg, nil
}
// SetConfig sets the complete configuration
func (m *Manager) SetConfig(cfg *Config) error {
configMap := make(map[string]interface{})
// Marshal the config to a map structure that viper can use
if err := m.v.Unmarshal(&configMap); err != nil {
return fmt.Errorf("failed to prepare config map: %w", err)
}
// Use viper's merge to apply the config
m.v.Set("server", cfg.Server)
m.v.Set("tracing", cfg.Tracing)
m.v.Set("cache", cfg.Cache)
m.v.Set("logger", cfg.Logger)
m.v.Set("error_tracking", cfg.ErrorTracking)
m.v.Set("middleware", cfg.Middleware)
m.v.Set("cors", cfg.CORS)
m.v.Set("event_broker", cfg.EventBroker)
m.v.Set("dbmanager", cfg.DBManager)
return nil
}
// Get returns a configuration value by key
func (m *Manager) Get(key string) interface{} {
return m.v.Get(key)
@@ -122,6 +145,14 @@ func (m *Manager) Set(key string, value interface{}) {
m.v.Set(key, value)
}
// SaveConfig writes the current configuration to the specified path
func (m *Manager) SaveConfig(path string) error {
if err := m.v.WriteConfigAs(path); err != nil {
return fmt.Errorf("failed to save config to %s: %w", path, err)
}
return nil
}
// setDefaults sets default configuration values
func setDefaults(v *viper.Viper) {
// Server defaults
@@ -166,6 +197,34 @@ func setDefaults(v *viper.Viper) {
// Database defaults
v.SetDefault("database.url", "")
// Database Manager defaults
v.SetDefault("dbmanager.default_connection", "default")
v.SetDefault("dbmanager.max_open_conns", 25)
v.SetDefault("dbmanager.max_idle_conns", 5)
v.SetDefault("dbmanager.conn_max_lifetime", "30m")
v.SetDefault("dbmanager.conn_max_idle_time", "5m")
v.SetDefault("dbmanager.retry_attempts", 3)
v.SetDefault("dbmanager.retry_delay", "1s")
v.SetDefault("dbmanager.retry_max_delay", "10s")
v.SetDefault("dbmanager.health_check_interval", "30s")
v.SetDefault("dbmanager.enable_auto_reconnect", true)
// Default PostgreSQL connection
v.SetDefault("dbmanager.connections.default.name", "default")
v.SetDefault("dbmanager.connections.default.type", "postgres")
v.SetDefault("dbmanager.connections.default.host", "localhost")
v.SetDefault("dbmanager.connections.default.port", 5432)
v.SetDefault("dbmanager.connections.default.user", "postgres")
v.SetDefault("dbmanager.connections.default.password", "")
v.SetDefault("dbmanager.connections.default.database", "resolvespec")
v.SetDefault("dbmanager.connections.default.sslmode", "disable")
v.SetDefault("dbmanager.connections.default.connect_timeout", "10s")
v.SetDefault("dbmanager.connections.default.query_timeout", "30s")
v.SetDefault("dbmanager.connections.default.enable_tracing", false)
v.SetDefault("dbmanager.connections.default.enable_metrics", false)
v.SetDefault("dbmanager.connections.default.enable_logging", false)
v.SetDefault("dbmanager.connections.default.default_orm", "bun")
// Event Broker defaults
v.SetDefault("event_broker.enabled", false)
v.SetDefault("event_broker.provider", "memory")

View File

@@ -50,6 +50,59 @@ type connectionManager struct {
wg sync.WaitGroup
}
var (
// singleton instance of the manager
instance Manager
// instanceMu protects the singleton instance
instanceMu sync.RWMutex
)
// SetupManager initializes the singleton database manager with the provided configuration.
// This function must be called before GetInstance().
// Returns an error if the manager is already initialized or if configuration is invalid.
func SetupManager(cfg ManagerConfig) error {
instanceMu.Lock()
defer instanceMu.Unlock()
if instance != nil {
return fmt.Errorf("manager already initialized")
}
mgr, err := NewManager(cfg)
if err != nil {
return fmt.Errorf("failed to create manager: %w", err)
}
instance = mgr
return nil
}
// GetInstance returns the singleton instance of the database manager.
// Returns an error if SetupManager has not been called yet.
func GetInstance() (Manager, error) {
instanceMu.RLock()
defer instanceMu.RUnlock()
if instance == nil {
return nil, fmt.Errorf("manager not initialized: call SetupManager first")
}
return instance, nil
}
// ResetInstance resets the singleton instance (primarily for testing purposes).
// WARNING: This should only be used in tests. Calling this in production code
// while the manager is in use can lead to undefined behavior.
func ResetInstance() {
instanceMu.Lock()
defer instanceMu.Unlock()
if instance != nil {
_ = instance.Close()
}
instance = nil
}
// NewManager creates a new database connection manager
func NewManager(cfg ManagerConfig) (Manager, error) {
// Apply defaults and validate configuration