@@ -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.
841841func (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.
0 commit comments