package models import ( "context" "errors" "fmt" "log" "reflect" "strings" "time" "github.com/timetracker/backend/internal/config" "github.com/timetracker/backend/internal/db" "github.com/timetracker/backend/internal/permissions" "gorm.io/driver/postgres" "gorm.io/gorm" "gorm.io/gorm/logger" ) // MigrateDB performs database migrations for all models func MigrateDB() error { gormDB := db.GetEngine(context.Background()) if gormDB == nil { return errors.New("database not initialized") } log.Println("Starting database migration...") // Add all models that should be migrated here err := gormDB.AutoMigrate( &Company{}, &User{}, &Customer{}, &Project{}, &Activity{}, &TimeEntry{}, &permissions.Role{}, &permissions.Policy{}, ) if err != nil { return fmt.Errorf("error migrating database: %w", err) } log.Println("Database migration completed successfully") return nil } // GetGormDB is used for special cases like database creation func GetGormDB(dbConfig config.DatabaseConfig, dbName string) (*gorm.DB, error) { dsn := fmt.Sprintf("host=%s port=%d user=%s password=%s dbname=%s sslmode=%s", dbConfig.Host, dbConfig.Port, dbConfig.User, dbConfig.Password, dbName, dbConfig.SSLMode) // Configure GORM logger gormLogger := logger.New( log.New(log.Writer(), "\r\n", log.LstdFlags), // io writer logger.Config{ SlowThreshold: 200 * time.Millisecond, // Slow SQL threshold LogLevel: dbConfig.LogLevel, // Log level IgnoreRecordNotFoundError: true, // Ignore ErrRecordNotFound error for logger Colorful: true, // Enable color }, ) db, err := gorm.Open(postgres.Open(dsn), &gorm.Config{ Logger: gormLogger, }) if err != nil { return nil, fmt.Errorf("error connecting to the database: %w", err) } return db, nil } // UpdateModel updates a model based on the set pointer fields func UpdateModel(ctx context.Context, model any, updates any) error { updateValue := reflect.ValueOf(updates) // If updates is a pointer, use the value behind it if updateValue.Kind() == reflect.Ptr { updateValue = updateValue.Elem() } // Make sure updates is a struct if updateValue.Kind() != reflect.Struct { return errors.New("updates must be a struct") } updateType := updateValue.Type() updateMap := make(map[string]any) // Iterate through all fields for i := range updateValue.NumField() { field := updateValue.Field(i) fieldType := updateType.Field(i) // Skip unexported fields if !fieldType.IsExported() { continue } // Special case: Skip ID field (use only for updates) if fieldType.Name == "ID" { continue } // For pointer types, check if they are not nil if field.Kind() == reflect.Ptr && !field.IsNil() { // Extract field name from GORM tag or use default field name fieldName := fieldType.Name if tag, ok := fieldType.Tag.Lookup("gorm"); ok { // Separate tag options options := strings.Split(tag, ";") for _, option := range options { if strings.HasPrefix(option, "column:") { fieldName = strings.TrimPrefix(option, "column:") break } } } // Use the value behind the pointer updateMap[fieldName] = field.Elem().Interface() } } if len(updateMap) == 0 { return nil // Nothing to update } return db.GetEngine(ctx).Model(model).Updates(updateMap).Error } // InitDB and CloseDB are forwarded to the db package for backward compatibility func InitDB(config config.DatabaseConfig) error { return db.InitDB(config) } func CloseDB() error { return db.CloseDB() }