add filtering and base repository
This commit is contained in:
@@ -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
|
||||
|
||||
@@ -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
|
||||
}
|
||||
|
||||
@@ -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)
|
||||
}
|
||||
|
||||
@@ -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
74
local/model/filter.go
Normal 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)
|
||||
}
|
||||
@@ -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"`
|
||||
}
|
||||
59
local/model/stateHistory.go
Normal file
59
local/model/stateHistory.go
Normal 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"`
|
||||
}
|
||||
@@ -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
121
local/repository/base.go
Normal 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)
|
||||
}
|
||||
@@ -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
|
||||
}
|
||||
@@ -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
|
||||
}
|
||||
@@ -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)
|
||||
}
|
||||
|
||||
@@ -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
|
||||
}
|
||||
|
||||
@@ -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")
|
||||
}
|
||||
|
||||
@@ -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
|
||||
}
|
||||
@@ -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
|
||||
}
|
||||
@@ -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
|
||||
}
|
||||
|
||||
Reference in New Issue
Block a user