add filtering and base repository

This commit is contained in:
Fran Jurmanović
2025-05-28 19:55:11 +02:00
parent 56ef5e1484
commit 0ced45ce55
17 changed files with 567 additions and 246 deletions

View File

@@ -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

View File

@@ -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
}

View File

@@ -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)
}

View File

@@ -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)
}

74
local/model/filter.go Normal file
View File

@@ -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)
}

View File

@@ -49,11 +49,3 @@ type ServerState struct {
// 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"`
}

View File

@@ -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"`
}

View File

@@ -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
}

121
local/repository/base.go Normal file
View File

@@ -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)
}

View File

@@ -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
}

View File

@@ -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
}

View File

@@ -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)
}

View File

@@ -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
}

View File

@@ -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")
}

View File

@@ -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
}

View File

@@ -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
}

View File

@@ -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
}