feat: Refactor database configuration loading and seeding logic for improved clarity and maintainability
This commit is contained in:
parent
a0b0b98624
commit
ec250570a6
13
.clinerules
13
.clinerules
@ -33,14 +33,17 @@
|
|||||||
- Client components must use TanStack Query
|
- Client components must use TanStack Query
|
||||||
- UI state management via Zustand
|
- UI state management via Zustand
|
||||||
|
|
||||||
7. DEVOPS
|
|
||||||
- Docker builds must pass Hadolint checks
|
|
||||||
- Kubernetes manifests in gitops/ directory
|
|
||||||
- Monitoring via OpenTelemetry instrumentation
|
|
||||||
|
|
||||||
8. DEVELOPMENT WORKFLOW
|
8. DEVELOPMENT WORKFLOW
|
||||||
- Use Makefile commands for common tasks:
|
- Makefile commands are only available in the backend folder
|
||||||
|
- Common make commands:
|
||||||
- make generate: Run code generation (tygo, swagger, etc.)
|
- make generate: Run code generation (tygo, swagger, etc.)
|
||||||
- make test: Run all tests
|
- make test: Run all tests
|
||||||
- make build: Build the application
|
- make build: Build the application
|
||||||
|
|
||||||
|
9. CUSTOM RULES
|
||||||
|
- Add custom rules to .clinerules if:
|
||||||
|
- Unexpected behavior is encountered
|
||||||
|
- Specific conditions require warnings
|
||||||
|
- New patterns emerge that need documentation
|
||||||
- make run: Start the development server
|
- make run: Start the development server
|
@ -1,3 +1,4 @@
|
|||||||
|
|
||||||
# Time Tracker Backend Makefile
|
# Time Tracker Backend Makefile
|
||||||
|
|
||||||
.PHONY: db-start db-stop db-test model-test run build clean migrate seed help
|
.PHONY: db-start db-stop db-test model-test run build clean migrate seed help
|
||||||
|
@ -86,12 +86,6 @@ func main() {
|
|||||||
log.Fatalf("Error migrating database: %v", err)
|
log.Fatalf("Error migrating database: %v", err)
|
||||||
}
|
}
|
||||||
|
|
||||||
// Seed database with initial data if needed
|
|
||||||
ctx := context.Background()
|
|
||||||
if err := models.SeedDB(ctx); err != nil {
|
|
||||||
log.Fatalf("Error seeding database: %v", err)
|
|
||||||
}
|
|
||||||
|
|
||||||
// Create Gin router
|
// Create Gin router
|
||||||
r := gin.Default()
|
r := gin.Default()
|
||||||
|
|
||||||
|
@ -2,56 +2,28 @@ package main
|
|||||||
|
|
||||||
import (
|
import (
|
||||||
"context"
|
"context"
|
||||||
|
"flag"
|
||||||
"fmt"
|
"fmt"
|
||||||
"log"
|
"log"
|
||||||
"os"
|
|
||||||
"time"
|
|
||||||
|
|
||||||
|
"github.com/timetracker/backend/internal/config"
|
||||||
"github.com/timetracker/backend/internal/models"
|
"github.com/timetracker/backend/internal/models"
|
||||||
"gorm.io/gorm/logger"
|
"gorm.io/gorm"
|
||||||
)
|
)
|
||||||
|
|
||||||
func main() {
|
func main() {
|
||||||
// Parse command line flags
|
// Parse CLI flags
|
||||||
force := false
|
_ = flag.String("config", "", "Path to .env config file")
|
||||||
for _, arg := range os.Args[1:] {
|
flag.Parse()
|
||||||
if arg == "--force" || arg == "-f" {
|
|
||||||
force = true
|
|
||||||
}
|
|
||||||
}
|
|
||||||
|
|
||||||
// Get database configuration with sensible defaults
|
// Load configuration
|
||||||
dbConfig := models.DefaultDatabaseConfig()
|
cfg, err := config.LoadConfig()
|
||||||
|
if err != nil {
|
||||||
// Override with environment variables if provided
|
log.Fatalf("Failed to load config: %v", err)
|
||||||
if host := os.Getenv("DB_HOST"); host != "" {
|
|
||||||
dbConfig.Host = host
|
|
||||||
}
|
}
|
||||||
if port := os.Getenv("DB_PORT"); port != "" {
|
|
||||||
var portInt int
|
|
||||||
if _, err := fmt.Sscanf(port, "%d", &portInt); err == nil && portInt > 0 {
|
|
||||||
dbConfig.Port = portInt
|
|
||||||
}
|
|
||||||
}
|
|
||||||
if user := os.Getenv("DB_USER"); user != "" {
|
|
||||||
dbConfig.User = user
|
|
||||||
}
|
|
||||||
if password := os.Getenv("DB_PASSWORD"); password != "" {
|
|
||||||
dbConfig.Password = password
|
|
||||||
}
|
|
||||||
if dbName := os.Getenv("DB_NAME"); dbName != "" {
|
|
||||||
dbConfig.DBName = dbName
|
|
||||||
}
|
|
||||||
if sslMode := os.Getenv("DB_SSLMODE"); sslMode != "" {
|
|
||||||
dbConfig.SSLMode = sslMode
|
|
||||||
}
|
|
||||||
|
|
||||||
// Set log level
|
|
||||||
dbConfig.LogLevel = logger.Info
|
|
||||||
|
|
||||||
// Initialize database
|
// Initialize database
|
||||||
fmt.Println("Connecting to database...")
|
if err := models.InitDB(cfg.Database); err != nil {
|
||||||
if err := models.InitDB(dbConfig); err != nil {
|
|
||||||
log.Fatalf("Error initializing database: %v", err)
|
log.Fatalf("Error initializing database: %v", err)
|
||||||
}
|
}
|
||||||
defer func() {
|
defer func() {
|
||||||
@ -59,31 +31,62 @@ func main() {
|
|||||||
log.Printf("Error closing database connection: %v", err)
|
log.Printf("Error closing database connection: %v", err)
|
||||||
}
|
}
|
||||||
}()
|
}()
|
||||||
fmt.Println("✓ Database connection successful")
|
|
||||||
|
|
||||||
// Create context with timeout
|
// Execute seed operation
|
||||||
ctx, cancel := context.WithTimeout(context.Background(), 30*time.Second)
|
if err := seedDatabase(context.Background()); err != nil {
|
||||||
defer cancel()
|
|
||||||
|
|
||||||
// Check if we need to seed (e.g., no companies exist)
|
|
||||||
if !force {
|
|
||||||
var count int64
|
|
||||||
db := models.GetEngine(ctx)
|
|
||||||
if err := db.Model(&models.Company{}).Count(&count).Error; err != nil {
|
|
||||||
log.Fatalf("Error checking if seeding is needed: %v", err)
|
|
||||||
}
|
|
||||||
|
|
||||||
// If data already exists, skip seeding
|
|
||||||
if count > 0 {
|
|
||||||
fmt.Println("Database already contains data. Use --force to override.")
|
|
||||||
return
|
|
||||||
}
|
|
||||||
}
|
|
||||||
|
|
||||||
// Seed the database
|
|
||||||
fmt.Println("Seeding database with initial data...")
|
|
||||||
if err := models.SeedDB(ctx); err != nil {
|
|
||||||
log.Fatalf("Error seeding database: %v", err)
|
log.Fatalf("Error seeding database: %v", err)
|
||||||
}
|
}
|
||||||
fmt.Println("✓ Database seeding completed successfully")
|
|
||||||
|
log.Println("Database seeding completed successfully")
|
||||||
|
}
|
||||||
|
|
||||||
|
// seedDatabase performs the database seeding operation
|
||||||
|
func seedDatabase(ctx context.Context) error {
|
||||||
|
// Check if seeding is needed
|
||||||
|
var count int64
|
||||||
|
if err := models.GetEngine(ctx).Model(&models.Company{}).Count(&count).Error; err != nil {
|
||||||
|
return fmt.Errorf("error checking if seeding is needed: %w", err)
|
||||||
|
}
|
||||||
|
|
||||||
|
// If data exists, skip seeding
|
||||||
|
if count > 0 {
|
||||||
|
log.Println("Database already contains data, skipping seeding")
|
||||||
|
return nil
|
||||||
|
}
|
||||||
|
|
||||||
|
log.Println("Seeding database with initial data...")
|
||||||
|
|
||||||
|
// Start transaction
|
||||||
|
return models.GetEngine(ctx).Transaction(func(tx *gorm.DB) error {
|
||||||
|
// Create default company
|
||||||
|
defaultCompany := models.Company{
|
||||||
|
Name: "Default Company",
|
||||||
|
}
|
||||||
|
if err := tx.Create(&defaultCompany).Error; err != nil {
|
||||||
|
return fmt.Errorf("error creating default company: %w", err)
|
||||||
|
}
|
||||||
|
|
||||||
|
// Create admin user
|
||||||
|
adminUser := models.User{
|
||||||
|
Email: "admin@example.com",
|
||||||
|
Role: models.RoleAdmin,
|
||||||
|
CompanyID: defaultCompany.ID,
|
||||||
|
HourlyRate: 100.0,
|
||||||
|
}
|
||||||
|
|
||||||
|
// Hash password
|
||||||
|
pwData, err := models.HashPassword("Admin@123456")
|
||||||
|
if err != nil {
|
||||||
|
return fmt.Errorf("error hashing password: %w", err)
|
||||||
|
}
|
||||||
|
|
||||||
|
adminUser.Salt = pwData.Salt
|
||||||
|
adminUser.Hash = pwData.Hash
|
||||||
|
|
||||||
|
if err := tx.Create(&adminUser).Error; err != nil {
|
||||||
|
return fmt.Errorf("error creating admin user: %w", err)
|
||||||
|
}
|
||||||
|
|
||||||
|
return nil
|
||||||
|
})
|
||||||
}
|
}
|
||||||
|
@ -4,6 +4,7 @@ go 1.23.6
|
|||||||
|
|
||||||
require (
|
require (
|
||||||
github.com/gin-gonic/gin v1.10.0
|
github.com/gin-gonic/gin v1.10.0
|
||||||
|
github.com/joho/godotenv v1.5.1
|
||||||
github.com/oklog/ulid/v2 v2.1.0
|
github.com/oklog/ulid/v2 v2.1.0
|
||||||
github.com/swaggo/files v1.0.1
|
github.com/swaggo/files v1.0.1
|
||||||
github.com/swaggo/gin-swagger v1.6.0
|
github.com/swaggo/gin-swagger v1.6.0
|
||||||
|
@ -54,6 +54,8 @@ github.com/jinzhu/inflection v1.0.0 h1:K317FqzuhWc8YvSVlFMCCUb36O/S9MCKRDI7QkRKD
|
|||||||
github.com/jinzhu/inflection v1.0.0/go.mod h1:h+uFLlag+Qp1Va5pdKtLDYj+kHp5pxUVkryuEj+Srlc=
|
github.com/jinzhu/inflection v1.0.0/go.mod h1:h+uFLlag+Qp1Va5pdKtLDYj+kHp5pxUVkryuEj+Srlc=
|
||||||
github.com/jinzhu/now v1.1.5 h1:/o9tlHleP7gOFmsnYNz3RGnqzefHA47wQpKrrdTIwXQ=
|
github.com/jinzhu/now v1.1.5 h1:/o9tlHleP7gOFmsnYNz3RGnqzefHA47wQpKrrdTIwXQ=
|
||||||
github.com/jinzhu/now v1.1.5/go.mod h1:d3SSVoowX0Lcu0IBviAWJpolVfI5UJVZZ7cO71lE/z8=
|
github.com/jinzhu/now v1.1.5/go.mod h1:d3SSVoowX0Lcu0IBviAWJpolVfI5UJVZZ7cO71lE/z8=
|
||||||
|
github.com/joho/godotenv v1.5.1 h1:7eLL/+HRGLY0ldzfGMeQkb7vMd0as4CfYvUVzLqw0N0=
|
||||||
|
github.com/joho/godotenv v1.5.1/go.mod h1:f4LDr5Voq0i2e/R5DDNOoa2zzDfwtkZa6DnEwAbqwq4=
|
||||||
github.com/josharian/intern v1.0.0 h1:vlS4z54oSdjm0bgjRigI+G1HpF+tI+9rE5LLzOg8HmY=
|
github.com/josharian/intern v1.0.0 h1:vlS4z54oSdjm0bgjRigI+G1HpF+tI+9rE5LLzOg8HmY=
|
||||||
github.com/josharian/intern v1.0.0/go.mod h1:5DoeVV0s6jJacbCEi61lwdGj/aVlrQvzHFFd8Hwg//Y=
|
github.com/josharian/intern v1.0.0/go.mod h1:5DoeVV0s6jJacbCEi61lwdGj/aVlrQvzHFFd8Hwg//Y=
|
||||||
github.com/json-iterator/go v1.1.12 h1:PV8peI4a0ysnczrg+LtxykD8LfKY9ML6u2jnxaEnrnM=
|
github.com/json-iterator/go v1.1.12 h1:PV8peI4a0ysnczrg+LtxykD8LfKY9ML6u2jnxaEnrnM=
|
||||||
|
86
backend/internal/config/config.go
Normal file
86
backend/internal/config/config.go
Normal file
@ -0,0 +1,86 @@
|
|||||||
|
package config
|
||||||
|
|
||||||
|
import (
|
||||||
|
"errors"
|
||||||
|
"fmt"
|
||||||
|
"log"
|
||||||
|
"os"
|
||||||
|
"strconv"
|
||||||
|
|
||||||
|
"github.com/joho/godotenv"
|
||||||
|
"github.com/timetracker/backend/internal/models"
|
||||||
|
"gorm.io/gorm/logger"
|
||||||
|
)
|
||||||
|
|
||||||
|
// Config represents the application configuration
|
||||||
|
type Config struct {
|
||||||
|
Database models.DatabaseConfig
|
||||||
|
}
|
||||||
|
|
||||||
|
// LoadConfig loads configuration from environment variables and .env file
|
||||||
|
func LoadConfig() (*Config, error) {
|
||||||
|
// Try loading .env file, but don't fail if it doesn't exist
|
||||||
|
_ = godotenv.Load()
|
||||||
|
|
||||||
|
cfg := &Config{
|
||||||
|
Database: models.DefaultDatabaseConfig(),
|
||||||
|
}
|
||||||
|
|
||||||
|
// Load database configuration
|
||||||
|
if err := loadDatabaseConfig(cfg); err != nil {
|
||||||
|
return nil, fmt.Errorf("failed to load database config: %w", err)
|
||||||
|
}
|
||||||
|
|
||||||
|
return cfg, nil
|
||||||
|
}
|
||||||
|
|
||||||
|
// loadDatabaseConfig loads database configuration from environment
|
||||||
|
func loadDatabaseConfig(cfg *Config) error {
|
||||||
|
// Required fields
|
||||||
|
cfg.Database.Host = getEnv("DB_HOST", cfg.Database.Host)
|
||||||
|
cfg.Database.User = getEnv("DB_USER", cfg.Database.User)
|
||||||
|
cfg.Database.Password = getEnv("DB_PASSWORD", cfg.Database.Password)
|
||||||
|
cfg.Database.DBName = getEnv("DB_NAME", cfg.Database.DBName)
|
||||||
|
cfg.Database.SSLMode = getEnv("DB_SSLMODE", cfg.Database.SSLMode)
|
||||||
|
|
||||||
|
// Optional fields with parsing
|
||||||
|
if port := getEnv("DB_PORT", ""); port != "" {
|
||||||
|
portInt, err := strconv.Atoi(port)
|
||||||
|
if err != nil || portInt <= 0 {
|
||||||
|
return errors.New("invalid DB_PORT value")
|
||||||
|
}
|
||||||
|
cfg.Database.Port = portInt
|
||||||
|
}
|
||||||
|
|
||||||
|
// Log level based on environment
|
||||||
|
if os.Getenv("ENVIRONMENT") == "production" {
|
||||||
|
cfg.Database.LogLevel = logger.Error
|
||||||
|
} else {
|
||||||
|
cfg.Database.LogLevel = logger.Info
|
||||||
|
}
|
||||||
|
|
||||||
|
// Validate required fields
|
||||||
|
if cfg.Database.Host == "" || cfg.Database.User == "" ||
|
||||||
|
cfg.Database.Password == "" || cfg.Database.DBName == "" {
|
||||||
|
return errors.New("missing required database configuration")
|
||||||
|
}
|
||||||
|
|
||||||
|
return nil
|
||||||
|
}
|
||||||
|
|
||||||
|
// getEnv gets an environment variable with fallback
|
||||||
|
func getEnv(key, fallback string) string {
|
||||||
|
if value, exists := os.LookupEnv(key); exists {
|
||||||
|
return value
|
||||||
|
}
|
||||||
|
return fallback
|
||||||
|
}
|
||||||
|
|
||||||
|
// MustLoadConfig loads configuration or panics on failure
|
||||||
|
func MustLoadConfig() *Config {
|
||||||
|
cfg, err := LoadConfig()
|
||||||
|
if err != nil {
|
||||||
|
log.Fatalf("Failed to load config: %v", err)
|
||||||
|
}
|
||||||
|
return cfg
|
||||||
|
}
|
@ -114,64 +114,6 @@ func MigrateDB() error {
|
|||||||
return nil
|
return nil
|
||||||
}
|
}
|
||||||
|
|
||||||
// SeedDB seeds the database with initial data if needed
|
|
||||||
func SeedDB(ctx context.Context) error {
|
|
||||||
if defaultDB == nil {
|
|
||||||
return errors.New("database not initialized")
|
|
||||||
}
|
|
||||||
|
|
||||||
log.Println("Checking if database seeding is needed...")
|
|
||||||
|
|
||||||
// Check if we need to seed (e.g., no companies exist)
|
|
||||||
var count int64
|
|
||||||
if err := defaultDB.Model(&Company{}).Count(&count).Error; err != nil {
|
|
||||||
return fmt.Errorf("error checking if seeding is needed: %w", err)
|
|
||||||
}
|
|
||||||
|
|
||||||
// If data already exists, skip seeding
|
|
||||||
if count > 0 {
|
|
||||||
log.Println("Database already contains data, skipping seeding")
|
|
||||||
return nil
|
|
||||||
}
|
|
||||||
|
|
||||||
log.Println("Seeding database with initial data...")
|
|
||||||
|
|
||||||
// Start a transaction for all seed operations
|
|
||||||
return defaultDB.Transaction(func(tx *gorm.DB) error {
|
|
||||||
// Create a default company
|
|
||||||
defaultCompany := Company{
|
|
||||||
Name: "Default Company",
|
|
||||||
}
|
|
||||||
if err := tx.Create(&defaultCompany).Error; err != nil {
|
|
||||||
return fmt.Errorf("error creating default company: %w", err)
|
|
||||||
}
|
|
||||||
|
|
||||||
// Create an admin user
|
|
||||||
adminUser := User{
|
|
||||||
Email: "admin@example.com",
|
|
||||||
Role: RoleAdmin,
|
|
||||||
CompanyID: defaultCompany.ID,
|
|
||||||
HourlyRate: 100.0,
|
|
||||||
}
|
|
||||||
|
|
||||||
// Hash a default password
|
|
||||||
pwData, err := HashPassword("Admin@123456")
|
|
||||||
if err != nil {
|
|
||||||
return fmt.Errorf("error hashing password: %w", err)
|
|
||||||
}
|
|
||||||
|
|
||||||
adminUser.Salt = pwData.Salt
|
|
||||||
adminUser.Hash = pwData.Hash
|
|
||||||
|
|
||||||
if err := tx.Create(&adminUser).Error; err != nil {
|
|
||||||
return fmt.Errorf("error creating admin user: %w", err)
|
|
||||||
}
|
|
||||||
|
|
||||||
log.Println("Database seeding completed successfully")
|
|
||||||
return nil
|
|
||||||
})
|
|
||||||
}
|
|
||||||
|
|
||||||
// GetEngine returns the DB instance, possibly with context
|
// GetEngine returns the DB instance, possibly with context
|
||||||
func GetEngine(ctx context.Context) *gorm.DB {
|
func GetEngine(ctx context.Context) *gorm.DB {
|
||||||
if defaultDB == nil {
|
if defaultDB == nil {
|
||||||
|
Loading…
x
Reference in New Issue
Block a user