From 0ced45ce55b2e2252b926d6653d918758422873e Mon Sep 17 00:00:00 2001 From: =?UTF-8?q?Fran=20Jurmanovi=C4=87?= Date: Wed, 28 May 2025 19:55:11 +0200 Subject: [PATCH] add filtering and base repository --- local/controller/api.go | 3 +- local/controller/controller.go | 37 --------- local/controller/server.go | 17 +++- local/controller/stateHistory.go | 19 ++++- local/model/filter.go | 74 +++++++++++++++++ local/model/server.go | 8 -- local/model/stateHistory.go | 59 ++++++++++++++ local/repository/api.go | 23 +----- local/repository/base.go | 121 ++++++++++++++++++++++++++++ local/repository/config.go | 59 +++----------- local/repository/server.go | 67 +++------------- local/repository/stateHistory.go | 33 ++------ local/service/api.go | 26 ++---- local/service/config.go | 14 ++-- local/service/server.go | 104 ++++++++++++++++++++---- local/service/stateHistory.go | 18 ++++- local/utl/common/common.go | 131 +++++++++++++++++++++++++++++++ 17 files changed, 567 insertions(+), 246 deletions(-) create mode 100644 local/model/filter.go create mode 100644 local/model/stateHistory.go create mode 100644 local/repository/base.go diff --git a/local/controller/api.go b/local/controller/api.go index 2e41464..5297ee9 100644 --- a/local/controller/api.go +++ b/local/controller/api.go @@ -43,8 +43,7 @@ func NewApiController(as *service.ApiService, routeGroups *common.RouteGroups) * // @Success 200 {array} string // @Router /v1/api [get] func (ac *ApiController) getFirst(c *fiber.Ctx) error { - apiModel := ac.service.GetFirst(c) - return c.SendString(apiModel.Api) + return c.SendStatus(fiber.StatusOK) } // getStatus returns service status diff --git a/local/controller/controller.go b/local/controller/controller.go index 3f8753e..a1a4d0d 100644 --- a/local/controller/controller.go +++ b/local/controller/controller.go @@ -1,15 +1,9 @@ package controller import ( - "acc-server-manager/local/model" "acc-server-manager/local/service" - "acc-server-manager/local/utl/common" - "fmt" "log" - "strconv" - "strings" - "github.com/gofiber/fiber/v2" "go.uber.org/dig" ) @@ -46,34 +40,3 @@ func InitializeControllers(c *dig.Container) { log.Panic("unable to initialize stateHistory controller") } } - -// FilteredResponse -// Gets query parameters and populates FilteredResponse model. -// -// Args: -// *gin.Context: Gin Application Context -// Returns: -// *model.FilteredResponse: Filtered response -func FilteredResponse(c *fiber.Ctx) *model.FilteredResponse { - filtered := new(model.FilteredResponse) - page := c.Params("page") - rpp := c.Params("rpp") - sortBy := c.Params("sortBy") - - dividers := [5]string{"|", " ", ".", "/", ","} - - for _, div := range dividers { - sortArr := strings.Split(sortBy, div) - - if len(sortArr) >= 2 { - sortBy = fmt.Sprintf("%s %s", common.ToSnakeCase(sortArr[0]), strings.ToUpper(sortArr[1])) - } - } - - filtered.Embed = c.Params("embed") - filtered.Page, _ = strconv.Atoi(page) - filtered.Rpp, _ = strconv.Atoi(rpp) - filtered.SortBy = sortBy - - return filtered -} diff --git a/local/controller/server.go b/local/controller/server.go index 16055b5..6669687 100644 --- a/local/controller/server.go +++ b/local/controller/server.go @@ -1,6 +1,7 @@ package controller import ( + "acc-server-manager/local/model" "acc-server-manager/local/service" "acc-server-manager/local/utl/common" @@ -38,7 +39,16 @@ func NewServerController(as *service.ServerService, routeGroups *common.RouteGro // @Success 200 {array} string // @Router /v1/server [get] func (ac *ServerController) getAll(c *fiber.Ctx) error { - ServerModel := ac.service.GetAll(c) + var filter model.ServerFilter + if err := common.ParseQueryFilter(c, &filter); err != nil { + return c.Status(fiber.StatusBadRequest).JSON(fiber.Map{ + "error": err.Error(), + }) + } + ServerModel, err := ac.service.GetAll(c, &filter) + if err != nil { + return c.Status(400).SendString(err.Error()) + } return c.JSON(ServerModel) } @@ -51,6 +61,9 @@ func (ac *ServerController) getAll(c *fiber.Ctx) error { // @Router /v1/server [get] func (ac *ServerController) getById(c *fiber.Ctx) error { serverID, _ := c.ParamsInt("id") - ServerModel := ac.service.GetById(c, serverID) + ServerModel, err := ac.service.GetById(c, serverID) + if err != nil { + return c.Status(400).SendString(err.Error()) + } return c.JSON(ServerModel) } diff --git a/local/controller/stateHistory.go b/local/controller/stateHistory.go index dd88240..5a640d6 100644 --- a/local/controller/stateHistory.go +++ b/local/controller/stateHistory.go @@ -1,6 +1,7 @@ package controller import ( + "acc-server-manager/local/model" "acc-server-manager/local/service" "acc-server-manager/local/utl/common" @@ -37,7 +38,19 @@ func NewStateHistoryController(as *service.StateHistoryService, routeGroups *com // @Success 200 {array} string // @Router /v1/StateHistory [get] func (ac *StateHistoryController) getAll(c *fiber.Ctx) error { - StateHistoryID, _ := c.ParamsInt("id") - StateHistoryModel := ac.service.GetAll(c, StateHistoryID) - return c.JSON(StateHistoryModel) + var filter model.StateHistoryFilter + if err := common.ParseQueryFilter(c, &filter); err != nil { + return c.Status(fiber.StatusBadRequest).JSON(fiber.Map{ + "error": err.Error(), + }) + } + + result, err := ac.service.GetAll(c, &filter) + if err != nil { + return c.Status(fiber.StatusInternalServerError).JSON(fiber.Map{ + "error": "Error retrieving state history", + }) + } + + return c.JSON(result) } \ No newline at end of file diff --git a/local/model/filter.go b/local/model/filter.go new file mode 100644 index 0000000..5255edc --- /dev/null +++ b/local/model/filter.go @@ -0,0 +1,74 @@ +package model + +import "time" + +// BaseFilter contains common filter fields that can be embedded in other filters +type BaseFilter struct { + Page int `query:"page"` + PageSize int `query:"page_size"` + SortBy string `query:"sort_by"` + SortDesc bool `query:"sort_desc"` +} + +// DateRangeFilter adds date range filtering capabilities +type DateRangeFilter struct { + StartDate time.Time `query:"start_date" time_format:"2006-01-02T15:04:05Z07:00"` + EndDate time.Time `query:"end_date" time_format:"2006-01-02T15:04:05Z07:00"` +} + +// ServerBasedFilter adds server ID filtering capability +type ServerBasedFilter struct { + ServerID int `param:"id"` +} + +// ServerFilter defines filtering options for Server queries +type ServerFilter struct { + BaseFilter + ServerBasedFilter + Name string `query:"name"` + ServiceName string `query:"service_name"` + Status string `query:"status"` +} + +// ConfigFilter defines filtering options for Config queries +type ConfigFilter struct { + BaseFilter + ServerBasedFilter + ConfigFile string `query:"config_file"` + ChangedAt time.Time `query:"changed_at" time_format:"2006-01-02T15:04:05Z07:00"` +} + +// ApiFilter defines filtering options for Api queries +type ApiFilter struct { + BaseFilter + Api string `query:"api"` +} + +// Pagination returns the offset and limit for database queries +func (f *BaseFilter) Pagination() (offset, limit int) { + if f.Page < 1 { + f.Page = 1 + } + if f.PageSize < 1 { + f.PageSize = 10 // Default page size + } + offset = (f.Page - 1) * f.PageSize + limit = f.PageSize + return +} + +// GetSorting returns the sort field and direction for database queries +func (f *BaseFilter) GetSorting() (field string, desc bool) { + if f.SortBy == "" { + return "id", false // Default sorting + } + return f.SortBy, f.SortDesc +} + +// IsDateRangeValid checks if both dates are set and start date is before end date +func (f *DateRangeFilter) IsDateRangeValid() bool { + if f.StartDate.IsZero() || f.EndDate.IsZero() { + return true // If either date is not set, consider it valid + } + return f.StartDate.Before(f.EndDate) +} \ No newline at end of file diff --git a/local/model/server.go b/local/model/server.go index f819153..0b333c4 100644 --- a/local/model/server.go +++ b/local/model/server.go @@ -48,12 +48,4 @@ type ServerState struct { MaxConnections int `json:"maxConnections"` // Players map[int]*PlayerState // etc. -} - -type StateHistory struct { - ID uint `gorm:"primaryKey" json:"id"` - ServerID uint `json:"serverId" gorm:"not null"` - Session string `json:"session"` - PlayerCount int `json:"playerCount"` - DateCreated time.Time `json:"dateCreated"` } \ No newline at end of file diff --git a/local/model/stateHistory.go b/local/model/stateHistory.go new file mode 100644 index 0000000..ee008ff --- /dev/null +++ b/local/model/stateHistory.go @@ -0,0 +1,59 @@ +package model + +import ( + "time" + + "gorm.io/gorm" +) + +// StateHistoryFilter combines common filter capabilities +type StateHistoryFilter struct { + BaseFilter // Adds pagination and sorting + ServerBasedFilter // Adds server ID from path parameter + DateRangeFilter // Adds date range filtering + + // Additional fields specific to state history + Session string `query:"session"` + MinPlayers *int `query:"min_players"` + MaxPlayers *int `query:"max_players"` +} + +// ApplyFilter implements the Filterable interface +func (f *StateHistoryFilter) ApplyFilter(query *gorm.DB) *gorm.DB { + // Apply server filter + if f.ServerID != 0 { + query = query.Where("server_id = ?", f.ServerID) + } + + // Apply date range filter if set + timeZero := time.Time{} + if f.StartDate != timeZero { + query = query.Where("date_created >= ?", f.StartDate) + } + if f.EndDate != timeZero { + query = query.Where("date_created <= ?", f.EndDate) + } + + // Apply session filter if set + if f.Session != "" { + query = query.Where("session = ?", f.Session) + } + + // Apply player count filters if set + if f.MinPlayers != nil { + query = query.Where("player_count >= ?", *f.MinPlayers) + } + if f.MaxPlayers != nil { + query = query.Where("player_count <= ?", *f.MaxPlayers) + } + + return query +} + +type StateHistory struct { + ID uint `gorm:"primaryKey" json:"id"` + ServerID uint `json:"serverId" gorm:"not null"` + Session string `json:"session"` + PlayerCount int `json:"playerCount"` + DateCreated time.Time `json:"dateCreated"` +} \ No newline at end of file diff --git a/local/repository/api.go b/local/repository/api.go index 8b18138..cb7ced6 100644 --- a/local/repository/api.go +++ b/local/repository/api.go @@ -2,35 +2,16 @@ package repository import ( "acc-server-manager/local/model" - "context" - "errors" "gorm.io/gorm" ) type ApiRepository struct { - db *gorm.DB + *BaseRepository[model.ApiModel, model.ApiFilter] } func NewApiRepository(db *gorm.DB) *ApiRepository { return &ApiRepository{ - db: db, + BaseRepository: NewBaseRepository[model.ApiModel, model.ApiFilter](db, model.ApiModel{}), } } - -// GetFirst -// Gets first row from API table. -// -// Args: -// context.Context: Application context -// Returns: -// model.ApiModel: Api object from database. -func (as ApiRepository) GetFirst(ctx context.Context) *model.ApiModel { - db := as.db.WithContext(ctx) - apiModel := new(model.ApiModel) - result := db.First(&apiModel) - if errors.Is(result.Error, gorm.ErrRecordNotFound) { - return nil - } - return apiModel -} diff --git a/local/repository/base.go b/local/repository/base.go new file mode 100644 index 0000000..63e5bef --- /dev/null +++ b/local/repository/base.go @@ -0,0 +1,121 @@ +package repository + +import ( + "context" + "fmt" + + "gorm.io/gorm" +) + +// BaseRepository provides generic CRUD operations for any model +type BaseRepository[T any, F any] struct { + db *gorm.DB + modelType T +} + +// NewBaseRepository creates a new base repository for the given model type +func NewBaseRepository[T any, F any](db *gorm.DB, model T) *BaseRepository[T, F] { + return &BaseRepository[T, F]{ + db: db, + modelType: model, + } +} + +// GetAll retrieves all records based on the filter +func (r *BaseRepository[T, F]) GetAll(ctx context.Context, filter *F) (*[]T, error) { + result := new([]T) + query := r.db.WithContext(ctx).Model(&r.modelType) + + // Apply filter conditions if filter implements Filterable + if filterable, ok := any(filter).(Filterable); ok { + query = filterable.ApplyFilter(query) + } + + // Apply pagination if filter implements Pageable + if pageable, ok := any(filter).(Pageable); ok { + offset, limit := pageable.Pagination() + query = query.Offset(offset).Limit(limit) + } + + // Apply sorting if filter implements Sortable + if sortable, ok := any(filter).(Sortable); ok { + field, desc := sortable.GetSorting() + if desc { + query = query.Order(field + " DESC") + } else { + query = query.Order(field) + } + } + + if err := query.Find(result).Error; err != nil { + return nil, fmt.Errorf("error getting records: %w", err) + } + + return result, nil +} + +// GetByID retrieves a single record by ID +func (r *BaseRepository[T, F]) GetByID(ctx context.Context, id interface{}) (*T, error) { + result := new(T) + if err := r.db.WithContext(ctx).First(result, id).Error; err != nil { + if err == gorm.ErrRecordNotFound { + return nil, nil + } + return nil, fmt.Errorf("error getting record by ID: %w", err) + } + return result, nil +} + +// Insert creates a new record +func (r *BaseRepository[T, F]) Insert(ctx context.Context, model *T) error { + if err := r.db.WithContext(ctx).Create(model).Error; err != nil { + return fmt.Errorf("error creating record: %w", err) + } + return nil +} + +// Update modifies an existing record +func (r *BaseRepository[T, F]) Update(ctx context.Context, model *T) error { + if err := r.db.WithContext(ctx).Save(model).Error; err != nil { + return fmt.Errorf("error updating record: %w", err) + } + return nil +} + +// Delete removes a record by ID +func (r *BaseRepository[T, F]) Delete(ctx context.Context, id interface{}) error { + if err := r.db.WithContext(ctx).Delete(new(T), id).Error; err != nil { + return fmt.Errorf("error deleting record: %w", err) + } + return nil +} + +// Count returns the total number of records matching the filter +func (r *BaseRepository[T, F]) Count(ctx context.Context, filter *F) (int64, error) { + var count int64 + query := r.db.WithContext(ctx).Model(&r.modelType) + + if filterable, ok := any(filter).(Filterable); ok { + query = filterable.ApplyFilter(query) + } + + if err := query.Count(&count).Error; err != nil { + return 0, fmt.Errorf("error counting records: %w", err) + } + + return count, nil +} + +// Interfaces for filter capabilities + +type Filterable interface { + ApplyFilter(*gorm.DB) *gorm.DB +} + +type Pageable interface { + Pagination() (offset, limit int) +} + +type Sortable interface { + GetSorting() (field string, desc bool) +} \ No newline at end of file diff --git a/local/repository/config.go b/local/repository/config.go index 2505e56..1e4b1e7 100644 --- a/local/repository/config.go +++ b/local/repository/config.go @@ -3,64 +3,27 @@ package repository import ( "acc-server-manager/local/model" "context" - "errors" "gorm.io/gorm" ) type ConfigRepository struct { - db *gorm.DB + *BaseRepository[model.Config, model.ConfigFilter] } func NewConfigRepository(db *gorm.DB) *ConfigRepository { return &ConfigRepository{ - db: db, + BaseRepository: NewBaseRepository[model.Config, model.ConfigFilter](db, model.Config{}), } } -// UpdateConfig -// Updates first row from Config table. -// -// Args: -// context.Context: Application context -// Returns: -// model.ConfigModel: Config object from database. -func (as ConfigRepository) UpdateFirst(ctx context.Context) *model.Config { - db := as.db.WithContext(ctx) - ConfigModel := new(model.Config) - db.First(&ConfigModel) - return ConfigModel -} - -// UpdateAll -// Updates All rows from Config table. -// -// Args: -// context.Context: Application context -// Returns: -// model.ConfigModel: Config object from database. -func (as ConfigRepository) UpdateAll(ctx context.Context) *[]model.Config { - db := as.db.WithContext(ctx) - ConfigModel := new([]model.Config) - db.Find(&ConfigModel) - return ConfigModel -} - -// UpdateConfig -// Updates Config row from Config table. -// -// Args: -// context.Context: Application context -// Returns: -// model.ConfigModel: Config object from database. -func (as ConfigRepository) UpdateConfig(ctx context.Context, body *model.Config) *model.Config { - db := as.db.WithContext(ctx) - - existingConfig := new(model.Config) - result := db.Where("server_id=?", body.ServerID).Where("config_file=?", body.ConfigFile).First(existingConfig) - if !errors.Is(result.Error, gorm.ErrRecordNotFound) { - body.ID = existingConfig.ID +// UpdateConfig updates or creates a Config record +func (r *ConfigRepository) UpdateConfig(ctx context.Context, config *model.Config) *model.Config { + if err := r.Update(ctx, config); err != nil { + // If update fails, try to insert + if err := r.Insert(ctx, config); err != nil { + return nil + } } - db.Save(body) - return body -} + return config +} \ No newline at end of file diff --git a/local/repository/server.go b/local/repository/server.go index 9ab2089..fca88cd 100644 --- a/local/repository/server.go +++ b/local/repository/server.go @@ -9,28 +9,15 @@ import ( ) type ServerRepository struct { - db *gorm.DB + *BaseRepository[model.Server, model.ServerFilter] } func NewServerRepository(db *gorm.DB) *ServerRepository { return &ServerRepository{ - db: db, + BaseRepository: NewBaseRepository[model.Server, model.ServerFilter](db, model.Server{}), } } -// GetFirst -// Gets first row from Server table. -// -// Args: -// context.Context: Application context -// Returns: -// model.ServerModel: Server object from database. -func (as ServerRepository) GetFirst(ctx context.Context, serverId int) *model.Server { - db := as.db.WithContext(ctx) - ServerModel := new(model.Server) - db.Where("id=?", serverId).First(&ServerModel) - return ServerModel -} // GetFirstByServiceName // Gets first row from Server table. @@ -39,45 +26,13 @@ func (as ServerRepository) GetFirst(ctx context.Context, serverId int) *model.Se // context.Context: Application context // Returns: // model.ServerModel: Server object from database. -func (as ServerRepository) GetFirstByServiceName(ctx context.Context, serviceName string) *model.Server { - db := as.db.WithContext(ctx) - ServerModel := new(model.Server) - result := db.Where("service_name=?", serviceName).First(&ServerModel) - if errors.Is(result.Error, gorm.ErrRecordNotFound) { - return nil +func (r *ServerRepository) GetFirstByServiceName(ctx context.Context, serviceName string) (*model.Server, error) { + result := new(model.Server) + if err := r.db.WithContext(ctx).Where("service_name = ?", serviceName).First(result).Error; err != nil { + if errors.Is(err, gorm.ErrRecordNotFound) { + return nil, err + } + return nil, err } - return ServerModel -} - -// GetAll -// Gets All rows from Server table. -// -// Args: -// context.Context: Application context -// Returns: -// model.ServerModel: Server object from database. -func (as ServerRepository) GetAll(ctx context.Context) *[]model.Server { - db := as.db.WithContext(ctx) - ServerModel := new([]model.Server) - db.Find(&ServerModel) - return ServerModel -} - -// UpdateServer -// Updates Server row from Server table. -// -// Args: -// context.Context: Application context -// Returns: -// model.Server: Server object from database. -func (as ServerRepository) UpdateServer(ctx context.Context, body *model.Server) *model.Server { - db := as.db.WithContext(ctx) - - existingServer := new(model.Server) - result := db.Where("id=?", body.ID).First(existingServer) - if !errors.Is(result.Error, gorm.ErrRecordNotFound) { - body.ID = existingServer.ID - } - db.Save(body) - return body -} + return result, nil +} \ No newline at end of file diff --git a/local/repository/stateHistory.go b/local/repository/stateHistory.go index 0e04b52..cbd5e09 100644 --- a/local/repository/stateHistory.go +++ b/local/repository/stateHistory.go @@ -8,38 +8,21 @@ import ( ) type StateHistoryRepository struct { - db *gorm.DB + *BaseRepository[model.StateHistory, model.StateHistoryFilter] } func NewStateHistoryRepository(db *gorm.DB) *StateHistoryRepository { return &StateHistoryRepository{ - db: db, + BaseRepository: NewBaseRepository[model.StateHistory, model.StateHistoryFilter](db, model.StateHistory{}), } } -// GetAll -// Gets All rows from Server table. -// -// Args: -// context.Context: Application context -// Returns: -// model.ServerModel: Server object from database. -func (as StateHistoryRepository) GetAll(ctx context.Context, id int) *[]model.StateHistory { - db := as.db.WithContext(ctx) - ServerModel := new([]model.StateHistory) - db.Find(&ServerModel).Where("ID = ?", id) - return ServerModel +// GetAll retrieves all state history records with the given filter +func (r *StateHistoryRepository) GetAll(ctx context.Context, filter *model.StateHistoryFilter) (*[]model.StateHistory, error) { + return r.BaseRepository.GetAll(ctx, filter) } -// UpdateServer -// Updates Server row from Server table. -// -// Args: -// context.Context: Application context -// Returns: -// model.Server: Server object from database. -func (as StateHistoryRepository) Insert(ctx context.Context, body *model.StateHistory) *model.StateHistory { - db := as.db.WithContext(ctx) - db.Save(body) - return body +// Insert creates a new state history record +func (r *StateHistoryRepository) Insert(ctx context.Context, model *model.StateHistory) error { + return r.BaseRepository.Insert(ctx, model) } diff --git a/local/service/api.go b/local/service/api.go index 22840b8..b7e0e24 100644 --- a/local/service/api.go +++ b/local/service/api.go @@ -30,17 +30,6 @@ func (as *ApiService) SetServerService(serverService *ServerService) { as.serverService = serverService } -// GetFirst -// Gets first row from API table. -// -// Args: -// context.Context: Application context -// Returns: -// string: Application version -func (as ApiService) GetFirst(ctx *fiber.Ctx) *model.ApiModel { - return as.repository.GetFirst(ctx.UserContext()) -} - func (as ApiService) GetStatus(ctx *fiber.Ctx) (string, error) { serviceName, err := as.GetServiceName(ctx) if err != nil { @@ -82,7 +71,7 @@ func (as ApiService) StatusServer(serviceName string) (string, error) { func (as ApiService) StartServer(serviceName string) (string, error) { status, err := ManageService(serviceName, "start") - server := as.serverRepository.GetFirstByServiceName(context.Background(), serviceName) + server, err := as.serverRepository.GetFirstByServiceName(context.Background(), serviceName) as.serverService.StartAccServerRuntime(server) return status, err } @@ -90,7 +79,7 @@ func (as ApiService) StartServer(serviceName string) (string, error) { func (as ApiService) StopServer(serviceName string) (string, error) { status, err := ManageService(serviceName, "stop") - server := as.serverRepository.GetFirstByServiceName(context.Background(), serviceName) + server, err := as.serverRepository.GetFirstByServiceName(context.Background(), serviceName) as.serverService.instances.Delete(server.ID) return status, err @@ -99,7 +88,7 @@ func (as ApiService) StopServer(serviceName string) (string, error) { func (as ApiService) RestartServer(serviceName string) (string, error) { status, err := ManageService(serviceName, "restart") - server := as.serverRepository.GetFirstByServiceName(context.Background(), serviceName) + server, err := as.serverRepository.GetFirstByServiceName(context.Background(), serviceName) as.serverService.StartAccServerRuntime(server) return status, err } @@ -115,18 +104,19 @@ func ManageService(serviceName string, action string) (string, error) { func (as ApiService) GetServiceName(ctx *fiber.Ctx) (string, error) { var server *model.Server + var err error serviceName, ok := ctx.Locals("service").(string) if !ok || serviceName == "" { serverId, ok2 := ctx.Locals("serverId").(int) if !ok2 || serverId == 0 { return "", errors.New("service name missing") } - server = as.serverRepository.GetFirst(ctx.UserContext(), serverId) + server, err = as.serverRepository.GetByID(ctx.UserContext(), serverId) } else { - server = as.serverRepository.GetFirstByServiceName(ctx.UserContext(), serviceName) + server, err = as.serverRepository.GetFirstByServiceName(ctx.UserContext(), serviceName) } - if server == nil { - return "", fiber.NewError(404, "Server not found") + if err != nil { + return "", err } return server.ServiceName, nil } diff --git a/local/service/config.go b/local/service/config.go index d5b0d05..28edc94 100644 --- a/local/service/config.go +++ b/local/service/config.go @@ -89,10 +89,10 @@ func (as ConfigService) UpdateConfig(ctx *fiber.Ctx, body *map[string]interface{ configFile := ctx.Params("file") override := ctx.QueryBool("override") - server := as.serverRepository.GetFirst(ctx.UserContext(), serverID) + server, err := as.serverRepository.GetByID(ctx.UserContext(), serverID) - if server == nil { - return nil, fiber.NewError(404, "Server not found") + if err != nil { + return nil, err } // Read existing config @@ -159,9 +159,9 @@ func (as ConfigService) GetConfig(ctx *fiber.Ctx) (interface{}, error) { serverID, _ := ctx.ParamsInt("id") configFile := ctx.Params("file") - server := as.serverRepository.GetFirst(ctx.UserContext(), serverID) + server, err := as.serverRepository.GetByID(ctx.UserContext(), serverID) - if server == nil { + if err != nil { log.Print("Server not found") return nil, fiber.NewError(404, "Server not found") } @@ -184,9 +184,9 @@ func (as ConfigService) GetConfig(ctx *fiber.Ctx) (interface{}, error) { func (as ConfigService) GetConfigs(ctx *fiber.Ctx) (*model.Configurations, error) { serverID, _ := ctx.ParamsInt("id") - server := as.serverRepository.GetFirst(ctx.UserContext(), serverID) + server, err := as.serverRepository.GetByID(ctx.UserContext(), serverID) - if server == nil { + if err != nil { log.Print("Server not found") return nil, fiber.NewError(404, "Server not found") } diff --git a/local/service/server.go b/local/service/server.go index 4019a42..58f7668 100644 --- a/local/service/server.go +++ b/local/service/server.go @@ -14,11 +14,18 @@ import ( ) type ServerService struct { - repository *repository.ServerRepository + repository *repository.ServerRepository stateHistoryRepo *repository.StateHistoryRepository - apiService *ApiService - instances sync.Map - configService *ConfigService + apiService *ApiService + instances sync.Map + configService *ConfigService + lastInsertTimes sync.Map // Track last insert time per server + debouncers sync.Map // Track debounce timers per server +} + +type pendingState struct { + timer *time.Timer + state *model.ServerState } func NewServerService(repository *repository.ServerRepository, stateHistoryRepo *repository.StateHistoryRepository, apiService *ApiService, configService *ConfigService) *ServerService { @@ -28,7 +35,10 @@ func NewServerService(repository *repository.ServerRepository, stateHistoryRepo configService: configService, stateHistoryRepo: stateHistoryRepo, } - servers := repository.GetAll(context.Background()) + servers, err := repository.GetAll(context.Background(), &model.ServerFilter{}) + if err != nil { + log.Print(err.Error()) + } for _, server := range *servers { status, err := service.apiService.StatusServer(server.ServiceName) if err != nil { @@ -41,15 +51,69 @@ func NewServerService(repository *repository.ServerRepository, stateHistoryRepo return service } +func (s *ServerService) shouldInsertStateHistory(serverID uint) bool { + insertInterval := 5 * time.Minute // Configure this as needed + + lastInsertInterface, exists := s.lastInsertTimes.Load(serverID) + if !exists { + s.lastInsertTimes.Store(serverID, time.Now().UTC()) + return true + } + + lastInsert := lastInsertInterface.(time.Time) + now := time.Now().UTC() + + if now.Sub(lastInsert) >= insertInterval { + s.lastInsertTimes.Store(serverID, now) + return true + } + + return false +} + +func (s *ServerService) insertStateHistory(serverID uint, state *model.ServerState) { + s.stateHistoryRepo.Insert(context.Background(), &model.StateHistory{ + ServerID: serverID, + Session: state.Session, + PlayerCount: state.PlayerCount, + DateCreated: time.Now().UTC(), + }) +} + +func (s *ServerService) handleStateChange(server *model.Server, state *model.ServerState) { + // Cancel existing timer if any + if debouncer, exists := s.debouncers.Load(server.ID); exists { + pending := debouncer.(*pendingState) + pending.timer.Stop() + } + + // Create new timer + timer := time.NewTimer(5 * time.Minute) + s.debouncers.Store(server.ID, &pendingState{ + timer: timer, + state: state, + }) + + // Start goroutine to handle the delayed insert + go func() { + <-timer.C + if debouncer, exists := s.debouncers.Load(server.ID); exists { + pending := debouncer.(*pendingState) + s.insertStateHistory(server.ID, pending.state) + s.debouncers.Delete(server.ID) + } + }() + + // If enough time has passed since last insert, insert immediately + if s.shouldInsertStateHistory(server.ID) { + s.insertStateHistory(server.ID, state) + } +} + func (s *ServerService) StartAccServerRuntime(server *model.Server) { s.instances.Delete(server.ID) instance := tracking.NewAccServerInstance(server, func(state *model.ServerState, states ...tracking.StateChange) { - s.stateHistoryRepo.Insert(context.Background(), &model.StateHistory{ - ServerID: server.ID, - Session: state.Session, - PlayerCount: state.PlayerCount, - DateCreated: time.Now().UTC(), - }) + s.handleStateChange(server, state) }) config, _ := DecodeFileName(ConfigurationJson)(server.ConfigPath) cfg := config.(model.Configuration) @@ -70,8 +134,11 @@ func (s *ServerService) StartAccServerRuntime(server *model.Server) { // context.Context: Application context // Returns: // string: Application version -func (as ServerService) GetAll(ctx *fiber.Ctx) *[]model.Server { - servers := as.repository.GetAll(ctx.UserContext()) +func (as ServerService) GetAll(ctx *fiber.Ctx, filter *model.ServerFilter) (*[]model.Server, error) { + servers, err := as.repository.GetAll(ctx.UserContext(), filter) + if err != nil { + return nil, err + } for i, server := range *servers { status, err := as.apiService.StatusServer(server.ServiceName) @@ -90,7 +157,7 @@ func (as ServerService) GetAll(ctx *fiber.Ctx) *[]model.Server { } } - return servers + return servers, nil } // GetById @@ -100,8 +167,11 @@ func (as ServerService) GetAll(ctx *fiber.Ctx) *[]model.Server { // context.Context: Application context // Returns: // string: Application version -func (as ServerService) GetById(ctx *fiber.Ctx, serverID int) *model.Server { - server := as.repository.GetFirst(ctx.UserContext(), serverID) +func (as ServerService) GetById(ctx *fiber.Ctx, serverID int) (*model.Server, error) { + server, err := as.repository.GetByID(ctx.UserContext(), serverID) + if err != nil { + return nil, err + } status, err := as.apiService.StatusServer(server.ServiceName) if err != nil { log.Print(err.Error()) @@ -117,5 +187,5 @@ func (as ServerService) GetById(ctx *fiber.Ctx, serverID int) *model.Server { } } - return server + return server, nil } \ No newline at end of file diff --git a/local/service/stateHistory.go b/local/service/stateHistory.go index 26138b0..7463934 100644 --- a/local/service/stateHistory.go +++ b/local/service/stateHistory.go @@ -3,6 +3,7 @@ package service import ( "acc-server-manager/local/model" "acc-server-manager/local/repository" + "log" "github.com/gofiber/fiber/v2" ) @@ -24,6 +25,19 @@ func NewStateHistoryService(repository *repository.StateHistoryRepository) *Stat // context.Context: Application context // Returns: // string: Application version -func (as StateHistoryService) GetAll(ctx *fiber.Ctx, id int) *[]model.StateHistory { - return as.repository.GetAll(ctx.UserContext(), id) +func (s *StateHistoryService) GetAll(ctx *fiber.Ctx, filter *model.StateHistoryFilter) (*[]model.StateHistory, error) { + result, err := s.repository.GetAll(ctx.UserContext(), filter) + if err != nil { + log.Printf("Error getting state history: %v", err) + return nil, err + } + return result, nil +} + +func (s *StateHistoryService) Insert(ctx *fiber.Ctx, model *model.StateHistory) error { + if err := s.repository.Insert(ctx.UserContext(), model); err != nil { + log.Printf("Error inserting state history: %v", err) + return err + } + return nil } \ No newline at end of file diff --git a/local/utl/common/common.go b/local/utl/common/common.go index ce31788..626f6bd 100644 --- a/local/utl/common/common.go +++ b/local/utl/common/common.go @@ -8,8 +8,11 @@ import ( "net" "os" "os/exec" + "reflect" "regexp" + "strconv" "strings" + "time" "github.com/gofiber/fiber/v2" ) @@ -82,3 +85,131 @@ func IndentJson(body []byte) ([]byte, error) { } return unmarshaledBody.Bytes(), nil } + +// ParseQueryFilter parses query parameters into a filter struct using reflection. +// It supports various field types and uses struct tags to determine parsing behavior. +// Supported tags: +// - `query:"field_name"` - specifies the query parameter name +// - `param:"param_name"` - specifies the path parameter name +// - `time_format:"format"` - specifies the time format for parsing dates (default: RFC3339) +func ParseQueryFilter(c *fiber.Ctx, filter interface{}) error { + val := reflect.ValueOf(filter) + if val.Kind() != reflect.Ptr || val.IsNil() { + return fmt.Errorf("filter must be a non-nil pointer") + } + + elem := val.Elem() + typ := elem.Type() + + for i := 0; i < elem.NumField(); i++ { + field := elem.Field(i) + fieldType := typ.Field(i) + + // Skip if field cannot be set + if !field.CanSet() { + continue + } + + // Check for param tag first (path parameters) + if paramName := fieldType.Tag.Get("param"); paramName != "" { + if err := parsePathParam(c, field, paramName); err != nil { + return fmt.Errorf("error parsing path parameter %s: %v", paramName, err) + } + continue + } + + // Then check for query tag + queryName := fieldType.Tag.Get("query") + if queryName == "" { + queryName = ToSnakeCase(fieldType.Name) // Default to snake_case of field name + } + + queryVal := c.Query(queryName) + if queryVal == "" { + continue // Skip empty values + } + + if err := parseValue(field, queryVal, fieldType.Tag); err != nil { + return fmt.Errorf("error parsing query parameter %s: %v", queryName, err) + } + } + + return nil +} + +func parsePathParam(c *fiber.Ctx, field reflect.Value, paramName string) error { + switch field.Kind() { + case reflect.Int, reflect.Int8, reflect.Int16, reflect.Int32, reflect.Int64: + val, err := c.ParamsInt(paramName) + if err != nil { + return err + } + field.SetInt(int64(val)) + case reflect.Uint, reflect.Uint8, reflect.Uint16, reflect.Uint32, reflect.Uint64: + val, err := c.ParamsInt(paramName) + if err != nil { + return err + } + field.SetUint(uint64(val)) + case reflect.String: + field.SetString(c.Params(paramName)) + default: + return fmt.Errorf("unsupported path parameter type: %v", field.Kind()) + } + return nil +} + +func parseValue(field reflect.Value, value string, tag reflect.StructTag) error { + switch field.Kind() { + case reflect.String: + field.SetString(value) + + case reflect.Int, reflect.Int8, reflect.Int16, reflect.Int32, reflect.Int64: + val, err := strconv.ParseInt(value, 10, 64) + if err != nil { + return err + } + field.SetInt(val) + + case reflect.Uint, reflect.Uint8, reflect.Uint16, reflect.Uint32, reflect.Uint64: + val, err := strconv.ParseUint(value, 10, 64) + if err != nil { + return err + } + field.SetUint(val) + + case reflect.Float32, reflect.Float64: + val, err := strconv.ParseFloat(value, 64) + if err != nil { + return err + } + field.SetFloat(val) + + case reflect.Bool: + val, err := strconv.ParseBool(value) + if err != nil { + return err + } + field.SetBool(val) + + case reflect.Struct: + if field.Type() == reflect.TypeOf(time.Time{}) { + format := tag.Get("time_format") + if format == "" { + format = time.RFC3339 + } + t, err := time.Parse(format, value) + if err != nil { + return err + } + field.Set(reflect.ValueOf(t)) + } else { + return fmt.Errorf("unsupported struct type: %v", field.Type()) + } + + default: + return fmt.Errorf("unsupported field type: %v", field.Kind()) + } + + return nil +}