From 755cabb5f4e4f649e25326331e220e457e492846 Mon Sep 17 00:00:00 2001 From: =?UTF-8?q?Ferdinand=20M=C3=BCtsch?= Date: Sun, 1 Nov 2020 16:56:36 +0100 Subject: [PATCH] refactor: introduce repositories as an additional layer of abstraction to allow for better testability --- main.go | 32 +++++++++++---- repositories/alias.go | 24 +++++++++++ repositories/custom_rule.go | 46 +++++++++++++++++++++ repositories/heartbeart.go | 64 +++++++++++++++++++++++++++++ repositories/key_value.go | 58 +++++++++++++++++++++++++++ repositories/summary.go | 53 ++++++++++++++++++++++++ repositories/user.go | 80 +++++++++++++++++++++++++++++++++++++ routes/settings.go | 8 +++- services/aggregation.go | 35 ++++++++-------- services/alias.go | 24 +++++------ services/custom_rule.go | 48 ++++++++++------------ services/heartbeat.go | 54 +++++-------------------- services/key_value.go | 50 +++++------------------ services/summary.go | 71 +++++++++++--------------------- services/user.go | 76 ++++++----------------------------- 15 files changed, 458 insertions(+), 265 deletions(-) create mode 100644 repositories/alias.go create mode 100644 repositories/custom_rule.go create mode 100644 repositories/heartbeart.go create mode 100644 repositories/key_value.go create mode 100644 repositories/summary.go create mode 100644 repositories/user.go diff --git a/main.go b/main.go index 470fb8b..4424357 100644 --- a/main.go +++ b/main.go @@ -4,6 +4,7 @@ import ( "github.com/gorilla/handlers" conf "github.com/muety/wakapi/config" "github.com/muety/wakapi/migrations/common" + "github.com/muety/wakapi/repositories" "log" "net/http" "strconv" @@ -27,6 +28,15 @@ var ( config *conf.Config ) +var ( + aliasRepository *repositories.AliasRepository + heartbeatRepository *repositories.HeartbeatRepository + userRepository *repositories.UserRepository + customRuleRepository *repositories.CustomRuleRepository + summaryRepository *repositories.SummaryRepository + keyValueRepository *repositories.KeyValueRepository +) + var ( aliasService *services.AliasService heartbeatService *services.HeartbeatService @@ -71,14 +81,22 @@ func main() { runDatabaseMigrations() runCustomMigrations() + // Repositories + aliasRepository = repositories.NewAliasRepository(db) + heartbeatRepository = repositories.NewHeartbeatRepository(db) + userRepository = repositories.NewUserRepository(db) + customRuleRepository = repositories.NewCustomRuleRepository(db) + summaryRepository = repositories.NewSummaryRepository(db) + keyValueRepository = repositories.NewKeyValueRepository(db) + // Services - aliasService = services.NewAliasService(db) - heartbeatService = services.NewHeartbeatService(db) - userService = services.NewUserService(db) - customRuleService = services.NewCustomRuleService(db) - summaryService = services.NewSummaryService(db, heartbeatService, aliasService, customRuleService) - aggregationService = services.NewAggregationService(db, userService, summaryService, heartbeatService) - keyValueService = services.NewKeyValueService(db) + aliasService = services.NewAliasService(aliasRepository) + heartbeatService = services.NewHeartbeatService(heartbeatRepository) + userService = services.NewUserService(userRepository) + customRuleService = services.NewCustomRuleService(customRuleRepository) + summaryService = services.NewSummaryService(summaryRepository, heartbeatService, aliasService, customRuleService) + aggregationService = services.NewAggregationService(userService, summaryService, heartbeatService) + keyValueService = services.NewKeyValueService(keyValueRepository) // Aggregate heartbeats to summaries and persist them go aggregationService.Schedule() diff --git a/repositories/alias.go b/repositories/alias.go new file mode 100644 index 0000000..a1783b8 --- /dev/null +++ b/repositories/alias.go @@ -0,0 +1,24 @@ +package repositories + +import ( + "github.com/jinzhu/gorm" + "github.com/muety/wakapi/models" +) + +type AliasRepository struct { + db *gorm.DB +} + +func NewAliasRepository(db *gorm.DB) *AliasRepository { + return &AliasRepository{db: db} +} + +func (r *AliasRepository) GetByUser(userId string) ([]*models.Alias, error) { + var aliases []*models.Alias + if err := r.db. + Where(&models.Alias{UserID: userId}). + Find(&aliases).Error; err != nil { + return nil, err + } + return aliases, nil +} diff --git a/repositories/custom_rule.go b/repositories/custom_rule.go new file mode 100644 index 0000000..acd1d80 --- /dev/null +++ b/repositories/custom_rule.go @@ -0,0 +1,46 @@ +package repositories + +import ( + "github.com/jinzhu/gorm" + "github.com/muety/wakapi/models" +) + +type CustomRuleRepository struct { + db *gorm.DB +} + +func NewCustomRuleRepository(db *gorm.DB) *CustomRuleRepository { + return &CustomRuleRepository{db: db} +} + +func (r *CustomRuleRepository) GetById(id uint) (*models.CustomRule, error) { + rule := &models.CustomRule{} + if err := r.db.Where(&models.CustomRule{ID: id}).First(rule).Error; err != nil { + return rule, err + } + return rule, nil +} + +func (r *CustomRuleRepository) GetByUser(userId string) ([]*models.CustomRule, error) { + var rules []*models.CustomRule + if err := r.db. + Where(&models.CustomRule{UserID: userId}). + Find(&rules).Error; err != nil { + return rules, err + } + return rules, nil +} + +func (r *CustomRuleRepository) Insert(rule *models.CustomRule) (*models.CustomRule, error) { + result := r.db.Create(rule) + if err := result.Error; err != nil { + return nil, err + } + return rule, nil +} + +func (r *CustomRuleRepository) Delete(id uint) error { + return r.db. + Where("id = ?", id). + Delete(models.CustomRule{}).Error +} diff --git a/repositories/heartbeart.go b/repositories/heartbeart.go new file mode 100644 index 0000000..1eb5ccd --- /dev/null +++ b/repositories/heartbeart.go @@ -0,0 +1,64 @@ +package repositories + +import ( + "github.com/jinzhu/gorm" + "github.com/muety/wakapi/models" + gormbulk "github.com/t-tiger/gorm-bulk-insert" + "time" +) + +type HeartbeatRepository struct { + db *gorm.DB +} + +func NewHeartbeatRepository(db *gorm.DB) *HeartbeatRepository { + return &HeartbeatRepository{db: db} +} + +func (r *HeartbeatRepository) InsertBatch(heartbeats []*models.Heartbeat) error { + var batch []interface{} + for _, h := range heartbeats { + batch = append(batch, *h) + } + + if err := gormbulk.BulkInsert(r.db, batch, 3000); err != nil { + return err + } + return nil +} + +func (r *HeartbeatRepository) GetAllWithin(from, to time.Time, user *models.User) ([]*models.Heartbeat, error) { + var heartbeats []*models.Heartbeat + if err := r.db. + Where(&models.Heartbeat{UserID: user.ID}). + Where("time >= ?", from). + Where("time < ?", to). + Order("time asc"). + Find(&heartbeats).Error; err != nil { + return nil, err + } + return heartbeats, nil +} + +// Will return *models.Heartbeat object with only user_id and time fields filled +func (r *HeartbeatRepository) GetFirstByUsers(userIds []string) ([]*models.Heartbeat, error) { + var heartbeats []*models.Heartbeat + if err := r.db. + Table("heartbeats"). + Select("user_id, min(time) as time"). + Where("user_id IN (?)", userIds). + Group("user_id"). + Scan(&heartbeats).Error; err != nil { + return nil, err + } + return heartbeats, nil +} + +func (r *HeartbeatRepository) DeleteBefore(t time.Time) error { + if err := r.db. + Where("time <= ?", t). + Delete(models.Heartbeat{}).Error; err != nil { + return err + } + return nil +} diff --git a/repositories/key_value.go b/repositories/key_value.go new file mode 100644 index 0000000..0973baa --- /dev/null +++ b/repositories/key_value.go @@ -0,0 +1,58 @@ +package repositories + +import ( + "errors" + "github.com/jinzhu/gorm" + "github.com/muety/wakapi/models" +) + +type KeyValueRepository struct { + db *gorm.DB +} + +func NewKeyValueRepository(db *gorm.DB) *KeyValueRepository { + return &KeyValueRepository{db: db} +} + +func (r *KeyValueRepository) GetString(key string) (*models.KeyStringValue, error) { + kv := &models.KeyStringValue{} + if err := r.db. + Where(&models.KeyStringValue{Key: key}). + First(&kv).Error; err != nil { + return nil, err + } + + return kv, nil +} + +func (r *KeyValueRepository) PutString(kv *models.KeyStringValue) error { + result := r.db. + Where(&models.KeyStringValue{Key: kv.Key}). + Assign(kv). + FirstOrCreate(kv) + + if err := result.Error; err != nil { + return err + } + + if result.RowsAffected != 1 { + return errors.New("nothing updated") + } + + return nil +} + +func (r *KeyValueRepository) DeleteString(key string) error { + result := r.db. + Delete(&models.KeyStringValue{}, &models.KeyStringValue{Key: key}) + + if err := result.Error; err != nil { + return err + } + + if result.RowsAffected != 1 { + return errors.New("nothing deleted") + } + + return nil +} diff --git a/repositories/summary.go b/repositories/summary.go new file mode 100644 index 0000000..1221216 --- /dev/null +++ b/repositories/summary.go @@ -0,0 +1,53 @@ +package repositories + +import ( + "github.com/jinzhu/gorm" + "github.com/muety/wakapi/models" + "time" +) + +type SummaryRepository struct { + db *gorm.DB +} + +func NewSummaryRepository(db *gorm.DB) *SummaryRepository { + return &SummaryRepository{db: db} +} + +func (r *SummaryRepository) Insert(summary *models.Summary) error { + if err := r.db.Create(summary).Error; err != nil { + return err + } + return nil +} + +func (r *SummaryRepository) GetByUserWithin(user *models.User, from, to time.Time) ([]*models.Summary, error) { + var summaries []*models.Summary + if err := r.db. + Where(&models.Summary{UserID: user.ID}). + Where("from_time >= ?", from). + Where("to_time <= ?", to). + Order("from_time asc"). + Preload("Projects", "type = ?", models.SummaryProject). + Preload("Languages", "type = ?", models.SummaryLanguage). + Preload("Editors", "type = ?", models.SummaryEditor). + Preload("OperatingSystems", "type = ?", models.SummaryOS). + Preload("Machines", "type = ?", models.SummaryMachine). + Find(&summaries).Error; err != nil { + return nil, err + } + return summaries, nil +} + +// Will return *models.Index objects with only user_id and to_time filled +func (r *SummaryRepository) GetLatestByUser() ([]*models.Summary, error) { + var summaries []*models.Summary + if err := r.db. + Table("summaries"). + Select("user_id, max(to_time) as to_time"). + Group("user_id"). + Scan(&summaries).Error; err != nil { + return nil, err + } + return summaries, nil +} diff --git a/repositories/user.go b/repositories/user.go new file mode 100644 index 0000000..4cc79b7 --- /dev/null +++ b/repositories/user.go @@ -0,0 +1,80 @@ +package repositories + +import ( + "errors" + "github.com/jinzhu/gorm" + "github.com/muety/wakapi/models" +) + +type UserRepository struct { + db *gorm.DB +} + +func NewUserRepository(db *gorm.DB) *UserRepository { + return &UserRepository{db: db} +} + +func (r *UserRepository) GetById(userId string) (*models.User, error) { + u := &models.User{} + if err := r.db.Where(&models.User{ID: userId}).First(u).Error; err != nil { + return u, err + } + return u, nil +} + +func (r *UserRepository) GetByApiKey(key string) (*models.User, error) { + u := &models.User{} + if err := r.db.Where(&models.User{ApiKey: key}).First(u).Error; err != nil { + return u, err + } + return u, nil +} + +func (r *UserRepository) GetAll() ([]*models.User, error) { + var users []*models.User + if err := r.db. + Table("users"). + Find(&users).Error; err != nil { + return nil, err + } + return users, nil +} + +func (r *UserRepository) InsertOrGet(user *models.User) (*models.User, bool, error) { + result := r.db.FirstOrCreate(user, &models.User{ID: user.ID}) + if err := result.Error; err != nil { + return nil, false, err + } + + if result.RowsAffected == 1 { + return user, true, nil + } + + return user, false, nil +} + +func (r *UserRepository) Update(user *models.User) (*models.User, error) { + result := r.db.Model(&models.User{}).Updates(user) + if err := result.Error; err != nil { + return nil, err + } + + if result.RowsAffected != 1 { + return nil, errors.New("nothing updated") + } + + return user, nil +} + +func (r *UserRepository) UpdateField(user *models.User, key string, value interface{}) (*models.User, error) { + result := r.db.Model(user).Update(key, value) + if err := result.Error; err != nil { + return nil, err + } + + if result.RowsAffected != 1 { + return nil, errors.New("nothing updated") + } + + return user, nil +} diff --git a/routes/settings.go b/routes/settings.go index c1d7621..87126ab 100644 --- a/routes/settings.go +++ b/routes/settings.go @@ -124,7 +124,13 @@ func (h *SettingsHandler) DeleteCustomRule(w http.ResponseWriter, r *http.Reques ID: uint(ruleId), UserID: user.ID, } - h.customRuleSrvc.Delete(rule) + + err = h.customRuleSrvc.Delete(rule) + if err != nil { + respondAlert(w, "internal server error", "", conf.SettingsTemplate, http.StatusInternalServerError) + return + } + msg := url.QueryEscape("Custom rule deleted successfully.") http.Redirect(w, r, fmt.Sprintf("%s/settings?success=%s", h.config.Server.BasePath, msg), http.StatusFound) diff --git a/services/aggregation.go b/services/aggregation.go index 74c1512..0f14b6f 100644 --- a/services/aggregation.go +++ b/services/aggregation.go @@ -7,7 +7,6 @@ import ( "time" "github.com/jasonlvhit/gocron" - "github.com/jinzhu/gorm" "github.com/muety/wakapi/models" ) @@ -16,20 +15,18 @@ const ( ) type AggregationService struct { - Config *config.Config - Db *gorm.DB - UserService *UserService - SummaryService *SummaryService - HeartbeatService *HeartbeatService + config *config.Config + userService *UserService + summaryService *SummaryService + heartbeatService *HeartbeatService } -func NewAggregationService(db *gorm.DB, userService *UserService, summaryService *SummaryService, heartbeatService *HeartbeatService) *AggregationService { +func NewAggregationService(userService *UserService, summaryService *SummaryService, heartbeatService *HeartbeatService) *AggregationService { return &AggregationService{ - Config: config.Get(), - Db: db, - UserService: userService, - SummaryService: summaryService, - HeartbeatService: heartbeatService, + config: config.Get(), + userService: userService, + summaryService: summaryService, + heartbeatService: heartbeatService, } } @@ -50,20 +47,20 @@ func (srv *AggregationService) Schedule() { go srv.summaryWorker(jobs, summaries) } - for i := 0; i < int(srv.Config.Db.MaxConn); i++ { + for i := 0; i < int(srv.config.Db.MaxConn); i++ { go srv.persistWorker(summaries) } // Run once initially srv.trigger(jobs) - gocron.Every(1).Day().At(srv.Config.App.AggregationTime).Do(srv.trigger, jobs) + gocron.Every(1).Day().At(srv.config.App.AggregationTime).Do(srv.trigger, jobs) <-gocron.Start() } func (srv *AggregationService) summaryWorker(jobs <-chan *AggregationJob, summaries chan<- *models.Summary) { for job := range jobs { - if summary, err := srv.SummaryService.Construct(job.From, job.To, &models.User{ID: job.UserID}, true); err != nil { + if summary, err := srv.summaryService.Construct(job.From, job.To, &models.User{ID: job.UserID}, true); err != nil { log.Printf("Failed to generate summary (%v, %v, %s) – %v.\n", job.From, job.To, job.UserID, err) } else { log.Printf("Successfully generated summary (%v, %v, %s).\n", job.From, job.To, job.UserID) @@ -74,7 +71,7 @@ func (srv *AggregationService) summaryWorker(jobs <-chan *AggregationJob, summar func (srv *AggregationService) persistWorker(summaries <-chan *models.Summary) { for summary := range summaries { - if err := srv.SummaryService.Insert(summary); err != nil { + if err := srv.summaryService.Insert(summary); err != nil { log.Printf("Failed to save summary (%v, %v, %s) – %v.\n", summary.UserID, summary.FromTime, summary.ToTime, err) } } @@ -83,13 +80,13 @@ func (srv *AggregationService) persistWorker(summaries <-chan *models.Summary) { func (srv *AggregationService) trigger(jobs chan<- *AggregationJob) error { log.Println("Generating summaries.") - users, err := srv.UserService.GetAll() + users, err := srv.userService.GetAll() if err != nil { log.Println(err) return err } - latestSummaries, err := srv.SummaryService.GetLatestByUser() + latestSummaries, err := srv.summaryService.GetLatestByUser() if err != nil { log.Println(err) return err @@ -107,7 +104,7 @@ func (srv *AggregationService) trigger(jobs chan<- *AggregationJob) error { } } - firstHeartbeats, err := srv.HeartbeatService.GetFirstUserHeartbeats(missingUserIDs) + firstHeartbeats, err := srv.heartbeatService.GetFirstUserHeartbeats(missingUserIDs) if err != nil { log.Println(err) return err diff --git a/services/alias.go b/services/alias.go index 4bad2fb..d2317be 100644 --- a/services/alias.go +++ b/services/alias.go @@ -2,36 +2,32 @@ package services import ( "github.com/muety/wakapi/config" + "github.com/muety/wakapi/repositories" "sync" - "github.com/jinzhu/gorm" "github.com/muety/wakapi/models" ) type AliasService struct { - Config *config.Config - Db *gorm.DB + config *config.Config + repository *repositories.AliasRepository } -func NewAliasService(db *gorm.DB) *AliasService { +func NewAliasService(aliasRepo *repositories.AliasRepository) *AliasService { return &AliasService{ - Config: config.Get(), - Db: db, + config: config.Get(), + repository: aliasRepo, } } var userAliases sync.Map func (srv *AliasService) LoadUserAliases(userId string) error { - var aliases []*models.Alias - if err := srv.Db. - Where(&models.Alias{UserID: userId}). - Find(&aliases).Error; err != nil { - return err + aliases, err := srv.repository.GetByUser(userId) + if err == nil { + userAliases.Store(userId, aliases) } - - userAliases.Store(userId, aliases) - return nil + return err } func (srv *AliasService) GetAliasOrDefault(userId string, summaryType uint8, value string) (string, error) { diff --git a/services/custom_rule.go b/services/custom_rule.go index c5e0386..a17d587 100644 --- a/services/custom_rule.go +++ b/services/custom_rule.go @@ -1,64 +1,56 @@ package services import ( - "github.com/jinzhu/gorm" "github.com/muety/wakapi/config" "github.com/muety/wakapi/models" + "github.com/muety/wakapi/repositories" "github.com/patrickmn/go-cache" "time" ) type CustomRuleService struct { - Config *config.Config - Db *gorm.DB - cache *cache.Cache + config *config.Config + repository *repositories.CustomRuleRepository + cache *cache.Cache } -func NewCustomRuleService(db *gorm.DB) *CustomRuleService { +func NewCustomRuleService(customRuleRepo *repositories.CustomRuleRepository) *CustomRuleService { return &CustomRuleService{ - Config: config.Get(), - Db: db, - cache: cache.New(1*time.Hour, 2*time.Hour), + config: config.Get(), + repository: customRuleRepo, + cache: cache.New(1*time.Hour, 2*time.Hour), } } -func (srv *CustomRuleService) GetCustomRuleById(customRuleId uint) (*models.CustomRule, error) { - r := &models.CustomRule{} - if err := srv.Db.Where(&models.CustomRule{ID: customRuleId}).First(r).Error; err != nil { - return r, err - } - return r, nil +func (srv *CustomRuleService) GetCustomRuleById(id uint) (*models.CustomRule, error) { + return srv.repository.GetById(id) } func (srv *CustomRuleService) GetCustomRuleForUser(userId string) ([]*models.CustomRule, error) { - var rules []*models.CustomRule if rules, found := srv.cache.Get(userId); found { return rules.([]*models.CustomRule), nil } - if err := srv.Db. - Where(&models.CustomRule{UserID: userId}). - Find(&rules).Error; err != nil { - return rules, err + rules, err := srv.repository.GetByUser(userId) + if err != nil { + return nil, err } srv.cache.Set(userId, rules, cache.DefaultExpiration) return rules, nil } func (srv *CustomRuleService) Create(rule *models.CustomRule) (*models.CustomRule, error) { - result := srv.Db.Create(rule) - if err := result.Error; err != nil { + result, err := srv.repository.Insert(rule) + if err != nil { return nil, err } - srv.cache.Delete(rule.UserID) - return rule, nil + srv.cache.Delete(result.UserID) + return result, nil } -func (srv *CustomRuleService) Delete(rule *models.CustomRule) { - srv.Db. - Where("id = ?", rule.ID). - Where("user_id = ?", rule.UserID). - Delete(models.CustomRule{}) +func (srv *CustomRuleService) Delete(rule *models.CustomRule) error { + err := srv.repository.Delete(rule.ID) srv.cache.Delete(rule.UserID) + return err } diff --git a/services/heartbeat.go b/services/heartbeat.go index 024cc47..5aa06ac 100644 --- a/services/heartbeat.go +++ b/services/heartbeat.go @@ -3,78 +3,44 @@ package services import ( "github.com/jasonlvhit/gocron" "github.com/muety/wakapi/config" + "github.com/muety/wakapi/repositories" "github.com/muety/wakapi/utils" "log" "time" - "github.com/jinzhu/gorm" "github.com/muety/wakapi/models" - gormbulk "github.com/t-tiger/gorm-bulk-insert" ) const ( - TableHeartbeat = "heartbeat" cleanUpInterval = time.Duration(aggregateIntervalDays) * 2 * 24 * time.Hour ) type HeartbeatService struct { - Config *config.Config - Db *gorm.DB + config *config.Config + repository *repositories.HeartbeatRepository } -func NewHeartbeatService(db *gorm.DB) *HeartbeatService { +func NewHeartbeatService(heartbeatRepo *repositories.HeartbeatRepository) *HeartbeatService { return &HeartbeatService{ - Config: config.Get(), - Db: db, + config: config.Get(), + repository: heartbeatRepo, } } func (srv *HeartbeatService) InsertBatch(heartbeats []*models.Heartbeat) error { - var batch []interface{} - for _, h := range heartbeats { - batch = append(batch, *h) - } - - if err := gormbulk.BulkInsert(srv.Db, batch, 3000); err != nil { - return err - } - return nil + return srv.repository.InsertBatch(heartbeats) } func (srv *HeartbeatService) GetAllWithin(from, to time.Time, user *models.User) ([]*models.Heartbeat, error) { - var heartbeats []*models.Heartbeat - if err := srv.Db. - Where(&models.Heartbeat{UserID: user.ID}). - Where("time >= ?", from). - Where("time < ?", to). - Order("time asc"). - Find(&heartbeats).Error; err != nil { - return nil, err - } - return heartbeats, nil + return srv.repository.GetAllWithin(from, to, user) } -// Will return *models.Heartbeat object with only user_id and time fields filled func (srv *HeartbeatService) GetFirstUserHeartbeats(userIds []string) ([]*models.Heartbeat, error) { - var heartbeats []*models.Heartbeat - if err := srv.Db. - Table("heartbeats"). - Select("user_id, min(time) as time"). - Where("user_id IN (?)", userIds). - Group("user_id"). - Scan(&heartbeats).Error; err != nil { - return nil, err - } - return heartbeats, nil + return srv.repository.GetFirstByUsers(userIds) } func (srv *HeartbeatService) DeleteBefore(t time.Time) error { - if err := srv.Db. - Where("time <= ?", t). - Delete(models.Heartbeat{}).Error; err != nil { - return err - } - return nil + return srv.repository.DeleteBefore(t) } func (srv *HeartbeatService) CleanUp() error { diff --git a/services/key_value.go b/services/key_value.go index c712852..1b839b2 100644 --- a/services/key_value.go +++ b/services/key_value.go @@ -1,63 +1,31 @@ package services import ( - "errors" - "github.com/jinzhu/gorm" "github.com/muety/wakapi/config" "github.com/muety/wakapi/models" + "github.com/muety/wakapi/repositories" ) type KeyValueService struct { - Config *config.Config - Db *gorm.DB + config *config.Config + repository *repositories.KeyValueRepository } -func NewKeyValueService(db *gorm.DB) *KeyValueService { +func NewKeyValueService(keyValueRepo *repositories.KeyValueRepository) *KeyValueService { return &KeyValueService{ - Config: config.Get(), - Db: db, + config: config.Get(), + repository: keyValueRepo, } } func (srv *KeyValueService) GetString(key string) (*models.KeyStringValue, error) { - kv := &models.KeyStringValue{} - if err := srv.Db. - Where(&models.KeyStringValue{Key: key}). - First(&kv).Error; err != nil { - return nil, err - } - - return kv, nil + return srv.repository.GetString(key) } func (srv *KeyValueService) PutString(kv *models.KeyStringValue) error { - result := srv.Db. - Where(&models.KeyStringValue{Key: kv.Key}). - Assign(kv). - FirstOrCreate(kv) - - if err := result.Error; err != nil { - return err - } - - if result.RowsAffected != 1 { - return errors.New("nothing updated") - } - - return nil + return srv.repository.PutString(kv) } func (srv *KeyValueService) DeleteString(key string) error { - result := srv.Db. - Delete(&models.KeyStringValue{}, &models.KeyStringValue{Key: key}) - - if err := result.Error; err != nil { - return err - } - - if result.RowsAffected != 1 { - return errors.New("nothing deleted") - } - - return nil + return srv.repository.DeleteString(key) } diff --git a/services/summary.go b/services/summary.go index aeac458..bcd30d5 100644 --- a/services/summary.go +++ b/services/summary.go @@ -4,35 +4,35 @@ import ( "crypto/md5" "errors" "github.com/muety/wakapi/config" + "github.com/muety/wakapi/repositories" "github.com/patrickmn/go-cache" "math" "sort" "strconv" "time" - "github.com/jinzhu/gorm" "github.com/muety/wakapi/models" ) const HeartbeatDiffThreshold = 2 * time.Minute type SummaryService struct { - Config *config.Config - Cache *cache.Cache - Db *gorm.DB - HeartbeatService *HeartbeatService - AliasService *AliasService - CustomRuleService *CustomRuleService + config *config.Config + cache *cache.Cache + repository *repositories.SummaryRepository + heartbeatService *HeartbeatService + aliasService *AliasService + customRuleService *CustomRuleService } -func NewSummaryService(db *gorm.DB, heartbeatService *HeartbeatService, aliasService *AliasService, customRuleService *CustomRuleService) *SummaryService { +func NewSummaryService(summaryRepo *repositories.SummaryRepository, heartbeatService *HeartbeatService, aliasService *AliasService, customRuleService *CustomRuleService) *SummaryService { return &SummaryService{ - Config: config.Get(), - Cache: cache.New(24*time.Hour, 24*time.Hour), - Db: db, - HeartbeatService: heartbeatService, - AliasService: aliasService, - CustomRuleService: customRuleService, + config: config.Get(), + cache: cache.New(24*time.Hour, 24*time.Hour), + repository: summaryRepo, + heartbeatService: heartbeatService, + aliasService: aliasService, + customRuleService: customRuleService, } } @@ -50,7 +50,7 @@ func (srv *SummaryService) Construct(from, to time.Time, user *models.User, reco existingSummaries = make([]*models.Summary, 0) } else { cacheKey = getHash([]time.Time{from, to}, user) - if result, ok := srv.Cache.Get(cacheKey); ok { + if result, ok := srv.cache.Get(cacheKey); ok { return result.(*models.Summary), nil } summaries, err := srv.GetByUserWithin(user, from, to) @@ -64,7 +64,7 @@ func (srv *SummaryService) Construct(from, to time.Time, user *models.User, reco heartbeats := make([]*models.Heartbeat, 0) for _, interval := range missingIntervals { - hb, err := srv.HeartbeatService.GetAllWithin(interval.Start, interval.End, user) + hb, err := srv.heartbeatService.GetAllWithin(interval.Start, interval.End, user) if err != nil { return nil, err } @@ -79,7 +79,7 @@ func (srv *SummaryService) Construct(from, to time.Time, user *models.User, reco var osItems []*models.SummaryItem var machineItems []*models.SummaryItem - if err := srv.AliasService.LoadUserAliases(user.ID); err != nil { + if err := srv.aliasService.LoadUserAliases(user.ID); err != nil { return nil, err } @@ -144,7 +144,7 @@ func (srv *SummaryService) Construct(from, to time.Time, user *models.User, reco } if cacheKey != "" { - srv.Cache.SetDefault(cacheKey, summary) + srv.cache.SetDefault(cacheKey, summary) } return summary, nil @@ -179,14 +179,14 @@ func (srv *SummaryService) PostProcess(summary *models.Summary) *models.Summary for _, item := range origin { // Add all "top-level" items, i.e. such without aliases - if key, _ := srv.AliasService.GetAliasOrDefault(summary.UserID, item.Type, item.Key); key == item.Key { + if key, _ := srv.aliasService.GetAliasOrDefault(summary.UserID, item.Type, item.Key); key == item.Key { target = append(target, item) } } for _, item := range origin { // Add all remaining projects and merge with their alias - if key, _ := srv.AliasService.GetAliasOrDefault(summary.UserID, item.Type, item.Key); key != item.Key { + if key, _ := srv.aliasService.GetAliasOrDefault(summary.UserID, item.Type, item.Key); key != item.Key { if targetItem := findItem(key); targetItem != nil { targetItem.Total += item.Total } else { @@ -215,41 +215,16 @@ func (srv *SummaryService) PostProcess(summary *models.Summary) *models.Summary } func (srv *SummaryService) Insert(summary *models.Summary) error { - if err := srv.Db.Create(summary).Error; err != nil { - return err - } - return nil + return srv.repository.Insert(summary) } func (srv *SummaryService) GetByUserWithin(user *models.User, from, to time.Time) ([]*models.Summary, error) { - var summaries []*models.Summary - if err := srv.Db. - Where(&models.Summary{UserID: user.ID}). - Where("from_time >= ?", from). - Where("to_time <= ?", to). - Order("from_time asc"). - Preload("Projects", "type = ?", models.SummaryProject). - Preload("Languages", "type = ?", models.SummaryLanguage). - Preload("Editors", "type = ?", models.SummaryEditor). - Preload("OperatingSystems", "type = ?", models.SummaryOS). - Preload("Machines", "type = ?", models.SummaryMachine). - Find(&summaries).Error; err != nil { - return nil, err - } - return summaries, nil + return srv.repository.GetByUserWithin(user, from, to) } // Will return *models.Index objects with only user_id and to_time filled func (srv *SummaryService) GetLatestByUser() ([]*models.Summary, error) { - var summaries []*models.Summary - if err := srv.Db. - Table("summaries"). - Select("user_id, max(to_time) as to_time"). - Group("user_id"). - Scan(&summaries).Error; err != nil { - return nil, err - } - return summaries, nil + return srv.repository.GetLatestByUser() } func (srv *SummaryService) aggregateBy(heartbeats []*models.Heartbeat, summaryType uint8, user *models.User, c chan models.SummaryItemContainer) { diff --git a/services/user.go b/services/user.go index 6440b23..710b10c 100644 --- a/services/user.go +++ b/services/user.go @@ -1,50 +1,35 @@ package services import ( - "errors" - "github.com/jinzhu/gorm" "github.com/muety/wakapi/config" "github.com/muety/wakapi/models" + "github.com/muety/wakapi/repositories" "github.com/muety/wakapi/utils" uuid "github.com/satori/go.uuid" ) type UserService struct { - Config *config.Config - Db *gorm.DB + Config *config.Config + repository *repositories.UserRepository } -func NewUserService(db *gorm.DB) *UserService { +func NewUserService(userRepo *repositories.UserRepository) *UserService { return &UserService{ - Config: config.Get(), - Db: db, + Config: config.Get(), + repository: userRepo, } } func (srv *UserService) GetUserById(userId string) (*models.User, error) { - u := &models.User{} - if err := srv.Db.Where(&models.User{ID: userId}).First(u).Error; err != nil { - return u, err - } - return u, nil + return srv.repository.GetById(userId) } func (srv *UserService) GetUserByKey(key string) (*models.User, error) { - u := &models.User{} - if err := srv.Db.Where(&models.User{ApiKey: key}).First(u).Error; err != nil { - return u, err - } - return u, nil + return srv.repository.GetByApiKey(key) } func (srv *UserService) GetAll() ([]*models.User, error) { - var users []*models.User - if err := srv.Db. - Table("users"). - Find(&users).Error; err != nil { - return nil, err - } - return users, nil + return srv.repository.GetAll() } func (srv *UserService) CreateOrGet(signup *models.Signup) (*models.User, bool, error) { @@ -60,29 +45,11 @@ func (srv *UserService) CreateOrGet(signup *models.Signup) (*models.User, bool, u.Password = hash } - result := srv.Db.FirstOrCreate(u, &models.User{ID: u.ID}) - if err := result.Error; err != nil { - return nil, false, err - } - - if result.RowsAffected == 1 { - return u, true, nil - } - - return u, false, nil + return srv.repository.InsertOrGet(u) } func (srv *UserService) Update(user *models.User) (*models.User, error) { - result := srv.Db.Model(&models.User{}).Updates(user) - if err := result.Error; err != nil { - return nil, err - } - - if result.RowsAffected != 1 { - return nil, errors.New("nothing updated") - } - - return user, nil + return srv.repository.Update(user) } func (srv *UserService) ResetApiKey(user *models.User) (*models.User, error) { @@ -91,16 +58,7 @@ func (srv *UserService) ResetApiKey(user *models.User) (*models.User, error) { } func (srv *UserService) ToggleBadges(user *models.User) (*models.User, error) { - result := srv.Db.Model(user).Update("badges_enabled", !user.BadgesEnabled) - if err := result.Error; err != nil { - return nil, err - } - - if result.RowsAffected != 1 { - return nil, errors.New("nothing updated") - } - - return user, nil + return srv.repository.UpdateField(user, "badges_enabled", !user.BadgesEnabled) } func (srv *UserService) MigrateMd5Password(user *models.User, login *models.Login) (*models.User, error) { @@ -110,13 +68,5 @@ func (srv *UserService) MigrateMd5Password(user *models.User, login *models.Logi } else { user.Password = hash } - - result := srv.Db.Model(user).Update("password", user.Password) - if err := result.Error; err != nil { - return nil, err - } else if result.RowsAffected < 1 { - return nil, errors.New("nothing changes") - } - - return user, nil + return srv.repository.UpdateField(user, "password", user.Password) }