Skip to content

Commit 05de831

Browse files
committed
refactor(database): enhance database config loading with ConfigManager
- Updated the `loadDatabaseConfig` function to utilize Forge's ConfigManager for improved configuration handling, including automatic environment variable expansion and file merging. - Simplified the logic for loading database configurations, allowing for better error messages when configurations are missing. - Removed the deprecated `expandEnvVars` function in favor of built-in environment variable support. - Added helpful error messages to guide users in configuring their databases correctly.
1 parent 25c7546 commit 05de831

2 files changed

Lines changed: 73 additions & 264 deletions

File tree

cmd/forge/plugins/database.go

Lines changed: 70 additions & 260 deletions
Original file line numberDiff line numberDiff line change
@@ -5,12 +5,10 @@ import (
55
"fmt"
66
"os"
77
"path/filepath"
8-
"regexp"
98
"strings"
109

1110
"github.com/uptrace/bun"
1211
"github.com/uptrace/bun/migrate"
13-
"gopkg.in/yaml.v3"
1412

1513
"github.com/xraph/forge"
1614
"github.com/xraph/forge/cli"
@@ -837,280 +835,92 @@ func (p *DatabasePlugin) getDatabaseConnection(ctx cli.CommandContext) (*bun.DB,
837835
}
838836
}
839837

840-
// loadDatabaseConfig loads database configuration from the forge config hierarchy.
838+
// loadDatabaseConfig loads database configuration using Forge's ConfigManager.
839+
// This provides automatic environment variable expansion, file merging, and proper
840+
// namespace support for both 'database' and 'extensions.database' keys.
841841
func (p *DatabasePlugin) loadDatabaseConfig(dbName, appName string) (database.DatabaseConfig, error) {
842-
var dbConfig database.DatabaseConfig
843-
844-
// Config file paths in priority order (lowest to highest)
845-
configPaths := []string{
846-
filepath.Join(p.config.RootDir, "config.yaml"), // Global config (root)
847-
filepath.Join(p.config.RootDir, "config", "config.yaml"), // Global config (config/)
848-
filepath.Join(p.config.RootDir, "config.local.yaml"), // Local global override (root)
849-
filepath.Join(p.config.RootDir, "config", "config.local.yaml"), // Local override (config/)
850-
}
851-
852-
// Add app-specific configs if app name provided
853-
if appName != "" {
854-
configPaths = append(configPaths,
855-
filepath.Join(p.config.RootDir, "apps", appName, "config.yaml"), // App config
856-
filepath.Join(p.config.RootDir, "apps", appName, "config.local.yaml"), // App local config
857-
)
858-
}
859-
860-
// Load and merge configs
861-
found := false
842+
// Create a temporary Forge app to access ConfigManager
843+
// This gives us all the benefits: file discovery, merging, env var expansion, etc.
844+
app := forge.NewApp(forge.AppConfig{
845+
Name: "forge-cli",
846+
Version: "1.0.0",
847+
Environment: os.Getenv("FORGE_ENV"),
848+
// Enable config auto-discovery to find config.yaml and config.local.yaml
849+
EnableConfigAutoDiscovery: true,
850+
ConfigSearchPaths: []string{p.config.RootDir, filepath.Join(p.config.RootDir, "config")},
851+
})
862852

863-
for _, path := range configPaths {
864-
if _, err := os.Stat(path); os.IsNotExist(err) {
865-
continue
866-
}
853+
cm := app.Config()
867854

868-
data, err := os.ReadFile(path)
869-
if err != nil {
870-
continue
871-
}
855+
// Try to load from extensions.database (new pattern) or database (legacy pattern)
856+
var dbConfig database.DatabaseConfig
857+
var fullConfig database.Config
872858

873-
// Support both array and map formats
874-
var cfg struct {
875-
Database struct {
876-
Databases []database.DatabaseConfig `yaml:"databases"` // Array format
877-
Map map[string]database.DatabaseConfig `yaml:",inline"` // Map format
878-
} `yaml:"database"`
879-
Apps map[string]struct {
880-
Database struct {
881-
Databases []database.DatabaseConfig `yaml:"databases"`
882-
Map map[string]database.DatabaseConfig `yaml:",inline"`
883-
} `yaml:"database"`
884-
} `yaml:"apps"`
859+
// First, try the namespaced key (preferred)
860+
if cm.IsSet("extensions.database") {
861+
if err := cm.Bind("extensions.database", &fullConfig); err != nil {
862+
return dbConfig, fmt.Errorf("failed to bind extensions.database config: %w", err)
885863
}
886-
887-
if err := yaml.Unmarshal(data, &cfg); err != nil {
888-
continue
864+
} else if cm.IsSet("database") {
865+
// Fallback to legacy key
866+
if err := cm.Bind("database", &fullConfig); err != nil {
867+
return dbConfig, fmt.Errorf("failed to bind database config: %w", err)
889868
}
890-
891-
// Look for database in global config (array format)
892-
for _, db := range cfg.Database.Databases {
893-
if db.Name == dbName {
894-
// Merge configs (later configs override earlier ones)
895-
if !found || db.DSN != "" {
896-
dbConfig.Name = db.Name
897-
if db.DSN != "" {
898-
dbConfig.DSN = db.DSN
899-
}
900-
901-
if db.Type != "" {
902-
dbConfig.Type = db.Type
903-
}
904-
905-
if db.MaxOpenConns > 0 {
906-
dbConfig.MaxOpenConns = db.MaxOpenConns
907-
}
908-
909-
if db.MaxIdleConns > 0 {
910-
dbConfig.MaxIdleConns = db.MaxIdleConns
911-
}
912-
913-
if db.MaxRetries > 0 {
914-
dbConfig.MaxRetries = db.MaxRetries
915-
}
916-
917-
if db.ConnectionTimeout > 0 {
918-
dbConfig.ConnectionTimeout = db.ConnectionTimeout
919-
}
920-
921-
if db.QueryTimeout > 0 {
922-
dbConfig.QueryTimeout = db.QueryTimeout
923-
}
924-
925-
if db.SlowQueryThreshold > 0 {
926-
dbConfig.SlowQueryThreshold = db.SlowQueryThreshold
927-
}
928-
929-
found = true
930-
}
869+
} else {
870+
return dbConfig, fmt.Errorf("database configuration not found in config files. "+
871+
"Add 'extensions.database' or 'database' section to config.yaml or config.local.yaml\n\n"+
872+
"Example:\n"+
873+
"extensions:\n"+
874+
" database:\n"+
875+
" databases:\n"+
876+
" - name: %s\n"+
877+
" type: postgres\n"+
878+
" dsn: postgres://user:pass@localhost:5432/dbname", dbName)
879+
}
880+
881+
// Find the requested database
882+
for _, db := range fullConfig.Databases {
883+
if db.Name == dbName {
884+
// Set defaults if not specified
885+
if db.MaxOpenConns == 0 {
886+
db.MaxOpenConns = 25
931887
}
932-
}
933-
934-
// Look for database in global config (map format)
935-
if db, ok := cfg.Database.Map[dbName]; ok {
936-
// Merge configs (later configs override earlier ones)
937-
if !found || db.DSN != "" {
938-
dbConfig.Name = dbName // Use the key as the name
939-
if db.DSN != "" {
940-
dbConfig.DSN = db.DSN
941-
}
942-
943-
if db.Type != "" {
944-
dbConfig.Type = db.Type
945-
}
946-
947-
if db.MaxOpenConns > 0 {
948-
dbConfig.MaxOpenConns = db.MaxOpenConns
949-
}
950-
951-
if db.MaxIdleConns > 0 {
952-
dbConfig.MaxIdleConns = db.MaxIdleConns
953-
}
954-
955-
if db.MaxRetries > 0 {
956-
dbConfig.MaxRetries = db.MaxRetries
957-
}
958-
959-
if db.ConnectionTimeout > 0 {
960-
dbConfig.ConnectionTimeout = db.ConnectionTimeout
961-
}
962-
963-
if db.QueryTimeout > 0 {
964-
dbConfig.QueryTimeout = db.QueryTimeout
965-
}
966-
967-
if db.SlowQueryThreshold > 0 {
968-
dbConfig.SlowQueryThreshold = db.SlowQueryThreshold
969-
}
970-
971-
found = true
888+
if db.MaxIdleConns == 0 {
889+
db.MaxIdleConns = 25
972890
}
973-
}
974-
975-
// Look for database in app-specific config
976-
if appName != "" {
977-
if appCfg, ok := cfg.Apps[appName]; ok {
978-
// Check array format
979-
for _, db := range appCfg.Database.Databases {
980-
if db.Name == dbName {
981-
// App config overrides global config
982-
if !found || db.DSN != "" {
983-
dbConfig.Name = db.Name
984-
if db.DSN != "" {
985-
dbConfig.DSN = db.DSN
986-
}
987-
988-
if db.Type != "" {
989-
dbConfig.Type = db.Type
990-
}
991-
992-
if db.MaxOpenConns > 0 {
993-
dbConfig.MaxOpenConns = db.MaxOpenConns
994-
}
995-
996-
if db.MaxIdleConns > 0 {
997-
dbConfig.MaxIdleConns = db.MaxIdleConns
998-
}
999-
1000-
if db.MaxRetries > 0 {
1001-
dbConfig.MaxRetries = db.MaxRetries
1002-
}
1003-
1004-
if db.ConnectionTimeout > 0 {
1005-
dbConfig.ConnectionTimeout = db.ConnectionTimeout
1006-
}
1007-
1008-
if db.QueryTimeout > 0 {
1009-
dbConfig.QueryTimeout = db.QueryTimeout
1010-
}
1011-
1012-
if db.SlowQueryThreshold > 0 {
1013-
dbConfig.SlowQueryThreshold = db.SlowQueryThreshold
1014-
}
1015-
1016-
found = true
1017-
}
1018-
}
1019-
}
1020-
1021-
// Check map format
1022-
if db, ok := appCfg.Database.Map[dbName]; ok {
1023-
// App config overrides global config
1024-
if !found || db.DSN != "" {
1025-
dbConfig.Name = dbName
1026-
if db.DSN != "" {
1027-
dbConfig.DSN = db.DSN
1028-
}
1029-
1030-
if db.Type != "" {
1031-
dbConfig.Type = db.Type
1032-
}
1033-
1034-
if db.MaxOpenConns > 0 {
1035-
dbConfig.MaxOpenConns = db.MaxOpenConns
1036-
}
1037-
1038-
if db.MaxIdleConns > 0 {
1039-
dbConfig.MaxIdleConns = db.MaxIdleConns
1040-
}
1041-
1042-
if db.MaxRetries > 0 {
1043-
dbConfig.MaxRetries = db.MaxRetries
1044-
}
1045-
1046-
if db.ConnectionTimeout > 0 {
1047-
dbConfig.ConnectionTimeout = db.ConnectionTimeout
1048-
}
1049-
1050-
if db.QueryTimeout > 0 {
1051-
dbConfig.QueryTimeout = db.QueryTimeout
1052-
}
1053-
1054-
if db.SlowQueryThreshold > 0 {
1055-
dbConfig.SlowQueryThreshold = db.SlowQueryThreshold
1056-
}
1057-
1058-
found = true
1059-
}
1060-
}
891+
if db.MaxRetries == 0 {
892+
db.MaxRetries = 3
1061893
}
1062-
}
1063-
}
1064894

1065-
if !found {
1066-
return dbConfig, fmt.Errorf("database '%s' not found in config files", dbName)
1067-
}
1068-
1069-
// Set defaults
1070-
if dbConfig.MaxOpenConns == 0 {
1071-
dbConfig.MaxOpenConns = 25
1072-
}
1073-
1074-
if dbConfig.MaxIdleConns == 0 {
1075-
dbConfig.MaxIdleConns = 25
895+
return db, nil
896+
}
1076897
}
1077898

1078-
if dbConfig.MaxRetries == 0 {
1079-
dbConfig.MaxRetries = 3
899+
// Database not found - provide helpful error with available databases
900+
availableDbs := getDatabaseNames(fullConfig.Databases)
901+
if len(availableDbs) == 0 {
902+
return dbConfig, fmt.Errorf("no databases configured. Add a database to your config:\n\n"+
903+
"extensions:\n"+
904+
" database:\n"+
905+
" databases:\n"+
906+
" - name: %s\n"+
907+
" type: postgres\n"+
908+
" dsn: ${DATABASE_DSN:-postgres://localhost:5432/dbname}", dbName)
1080909
}
1081910

1082-
// Expand environment variables in DSN
1083-
dbConfig.DSN = expandEnvVars(dbConfig.DSN)
1084-
1085-
return dbConfig, nil
911+
return dbConfig, fmt.Errorf("database '%s' not found in config files.\n"+
912+
"Available databases: %v\n\n"+
913+
"Tip: Check config.yaml or config.local.yaml for 'extensions.database.databases'",
914+
dbName, availableDbs)
1086915
}
1087916

1088-
// expandEnvVars expands environment variables in format ${VAR} or ${VAR:default}.
1089-
func expandEnvVars(s string) string {
1090-
// Pattern matches ${VAR} or ${VAR:default}
1091-
pattern := regexp.MustCompile(`\$\{([^}:]+)(?::([^}]*))?\}`)
1092-
1093-
return pattern.ReplaceAllStringFunc(s, func(match string) string {
1094-
// Extract variable name and default value
1095-
parts := pattern.FindStringSubmatch(match)
1096-
if len(parts) < 2 {
1097-
return match
1098-
}
1099-
1100-
varName := parts[1]
1101-
1102-
defaultValue := ""
1103-
if len(parts) > 2 {
1104-
defaultValue = parts[2]
1105-
}
1106-
1107-
// Get environment variable
1108-
if value := os.Getenv(varName); value != "" {
1109-
return value
1110-
}
1111-
1112-
return defaultValue
1113-
})
917+
// getDatabaseNames extracts database names from a list of database configs.
918+
func getDatabaseNames(databases []database.DatabaseConfig) []string {
919+
names := make([]string, len(databases))
920+
for i, db := range databases {
921+
names[i] = db.Name
922+
}
923+
return names
1114924
}
1115925

1116926
// cliLoggerAdapter adapts CLI context to database.Logger interface.

cmd/forge/plugins/database_go_migrations.go

Lines changed: 3 additions & 4 deletions
Original file line numberDiff line numberDiff line change
@@ -68,19 +68,18 @@ func (p *DatabasePlugin) runWithGoMigrations(ctx cli.CommandContext, command str
6868
}
6969

7070
// Get database config
71+
// Note: ConfigManager already expands environment variables
7172
dbConfig, err := p.loadDatabaseConfig(dbName, ctx.String("app"))
7273
if err != nil {
7374
return fmt.Errorf("failed to load database config: %w", err)
7475
}
7576

7677
// Override with flags if provided
7778
if customDSN := ctx.String("dsn"); customDSN != "" {
78-
dbConfig.DSN = customDSN
79+
// Custom DSN from flag - expand env vars using os.ExpandEnv
80+
dbConfig.DSN = os.ExpandEnv(customDSN)
7981
}
8082

81-
// Expand environment variables in DSN
82-
dbConfig.DSN = expandEnvVars(dbConfig.DSN)
83-
8483
// Create temporary directory for migration runner
8584
tmpDir, err := os.MkdirTemp("", "forge-migrate-*")
8685
if err != nil {

0 commit comments

Comments
 (0)