cab-backend/internal/handlers/game_variables.go
2026-03-30 21:00:35 +03:00

474 lines
13 KiB
Go

package handlers
import (
"context"
"fmt"
"net/http"
"strconv"
"strings"
"time"
"game-admin/internal/models"
"github.com/gin-gonic/gin"
"github.com/uptrace/bun"
)
func (h *GameHandler) ListVariables(c *gin.Context) {
gameID, err := strconv.ParseInt(c.Param("id"), 10, 64)
if err != nil {
c.JSON(http.StatusBadRequest, gin.H{"error": "Invalid game id"})
return
}
exists, err := h.gameExists(c, gameID)
if err != nil {
c.JSON(http.StatusInternalServerError, gin.H{"error": err.Error()})
return
}
if !exists {
c.JSON(http.StatusNotFound, gin.H{"error": "Game not found"})
return
}
hasModule, err := h.gameHasModule(c, gameID, "variables")
if err != nil {
c.JSON(http.StatusInternalServerError, gin.H{"error": err.Error()})
return
}
if !hasModule {
c.JSON(http.StatusForbidden, gin.H{"error": "Variables module is not enabled for this game"})
return
}
var variables []models.GameVariable
err = h.db.NewSelect().
Model(&variables).
Relation("Items", func(q *bun.SelectQuery) *bun.SelectQuery {
return q.OrderExpr("gvi.id ASC")
}).
Where("gv.game_id = ?", gameID).
OrderExpr("gv.id DESC").
Scan(c)
if err != nil {
c.JSON(http.StatusInternalServerError, gin.H{"error": err.Error()})
return
}
for i := range variables {
normalizeGameVariableResponse(&variables[i])
}
c.JSON(http.StatusOK, variables)
}
func (h *GameHandler) CreateVariable(c *gin.Context) {
gameID, err := strconv.ParseInt(c.Param("id"), 10, 64)
if err != nil {
c.JSON(http.StatusBadRequest, gin.H{"error": "Invalid game id"})
return
}
var req models.UpsertGameVariableRequest
if err := c.ShouldBindJSON(&req); err != nil {
c.JSON(http.StatusBadRequest, gin.H{"error": err.Error()})
return
}
if err := validateUpsertGameVariableRequest(&req); err != nil {
c.JSON(http.StatusBadRequest, gin.H{"error": err.Error()})
return
}
exists, err := h.gameExists(c, gameID)
if err != nil {
c.JSON(http.StatusInternalServerError, gin.H{"error": err.Error()})
return
}
if !exists {
c.JSON(http.StatusNotFound, gin.H{"error": "Game not found"})
return
}
hasModule, err := h.gameHasModule(c, gameID, "variables")
if err != nil {
c.JSON(http.StatusInternalServerError, gin.H{"error": err.Error()})
return
}
if !hasModule {
c.JSON(http.StatusForbidden, gin.H{"error": "Variables module is not enabled for this game"})
return
}
tx, err := h.db.BeginTx(c, nil)
if err != nil {
c.JSON(http.StatusInternalServerError, gin.H{"error": err.Error()})
return
}
defer tx.Rollback()
now := time.Now()
variable := &models.GameVariable{
GameID: gameID,
Key: strings.TrimSpace(req.Key),
Type: req.Type,
NumberValue: req.NumberValue,
StringValue: req.StringValue,
TableValueType: req.TableValueType,
CreatedAt: now,
UpdatedAt: now,
}
_, err = tx.NewInsert().Model(variable).Exec(c)
if err != nil {
c.JSON(http.StatusConflict, gin.H{"error": "Game variable key already exists"})
return
}
if err := insertGameVariableItems(c, tx, variable.ID, req); err != nil {
c.JSON(http.StatusBadRequest, gin.H{"error": err.Error()})
return
}
if err := tx.Commit(); err != nil {
c.JSON(http.StatusInternalServerError, gin.H{"error": err.Error()})
return
}
created, err := h.getGameVariable(c, gameID, variable.ID)
if err != nil {
c.JSON(http.StatusInternalServerError, gin.H{"error": err.Error()})
return
}
c.JSON(http.StatusCreated, created)
}
func (h *GameHandler) UpdateVariable(c *gin.Context) {
gameID, err := strconv.ParseInt(c.Param("id"), 10, 64)
if err != nil {
c.JSON(http.StatusBadRequest, gin.H{"error": "Invalid game id"})
return
}
variableID, err := strconv.ParseInt(c.Param("var_id"), 10, 64)
if err != nil {
c.JSON(http.StatusBadRequest, gin.H{"error": "Invalid variable id"})
return
}
var req models.UpsertGameVariableRequest
if err := c.ShouldBindJSON(&req); err != nil {
c.JSON(http.StatusBadRequest, gin.H{"error": err.Error()})
return
}
if err := validateUpsertGameVariableRequest(&req); err != nil {
c.JSON(http.StatusBadRequest, gin.H{"error": err.Error()})
return
}
hasModule, err := h.gameHasModule(c, gameID, "variables")
if err != nil {
c.JSON(http.StatusInternalServerError, gin.H{"error": err.Error()})
return
}
if !hasModule {
c.JSON(http.StatusForbidden, gin.H{"error": "Variables module is not enabled for this game"})
return
}
existing, err := h.getGameVariable(c, gameID, variableID)
if err != nil {
c.JSON(http.StatusNotFound, gin.H{"error": "Game variable not found"})
return
}
tx, err := h.db.BeginTx(c, nil)
if err != nil {
c.JSON(http.StatusInternalServerError, gin.H{"error": err.Error()})
return
}
defer tx.Rollback()
numberValue, stringValue, tableValueType := dbValuesFromVariableRequest(req)
_, err = tx.NewUpdate().
Model((*models.GameVariable)(nil)).
Set(`"key" = ?`, strings.TrimSpace(req.Key)).
Set("type = ?", req.Type).
Set("number_value = ?", numberValue).
Set("string_value = ?", stringValue).
Set("table_value_type = ?", tableValueType).
Set("updated_at = ?", time.Now()).
Where("id = ? AND game_id = ?", existing.ID, gameID).
Exec(c)
if err != nil {
c.JSON(http.StatusConflict, gin.H{"error": "Could not update game variable"})
return
}
_, err = tx.NewDelete().
Model((*models.GameVariableItem)(nil)).
Where("game_variable_id = ?", existing.ID).
Exec(c)
if err != nil {
c.JSON(http.StatusInternalServerError, gin.H{"error": err.Error()})
return
}
if err := insertGameVariableItems(c, tx, existing.ID, req); err != nil {
c.JSON(http.StatusBadRequest, gin.H{"error": err.Error()})
return
}
if err := tx.Commit(); err != nil {
c.JSON(http.StatusInternalServerError, gin.H{"error": err.Error()})
return
}
updated, err := h.getGameVariable(c, gameID, existing.ID)
if err != nil {
c.JSON(http.StatusInternalServerError, gin.H{"error": err.Error()})
return
}
c.JSON(http.StatusOK, updated)
}
func (h *GameHandler) DeleteVariable(c *gin.Context) {
gameID, err := strconv.ParseInt(c.Param("id"), 10, 64)
if err != nil {
c.JSON(http.StatusBadRequest, gin.H{"error": "Invalid game id"})
return
}
variableID, err := strconv.ParseInt(c.Param("var_id"), 10, 64)
if err != nil {
c.JSON(http.StatusBadRequest, gin.H{"error": "Invalid variable id"})
return
}
existing, err := h.getGameVariable(c, gameID, variableID)
if err != nil {
c.JSON(http.StatusNotFound, gin.H{"error": "Game variable not found"})
return
}
hasModule, err := h.gameHasModule(c, gameID, "variables")
if err != nil {
c.JSON(http.StatusInternalServerError, gin.H{"error": err.Error()})
return
}
if !hasModule {
c.JSON(http.StatusForbidden, gin.H{"error": "Variables module is not enabled for this game"})
return
}
tx, err := h.db.BeginTx(c, nil)
if err != nil {
c.JSON(http.StatusInternalServerError, gin.H{"error": err.Error()})
return
}
defer tx.Rollback()
_, err = tx.NewDelete().
Model((*models.GameVariableItem)(nil)).
Where("game_variable_id = ?", existing.ID).
Exec(c)
if err != nil {
c.JSON(http.StatusInternalServerError, gin.H{"error": err.Error()})
return
}
_, err = tx.NewDelete().
Model((*models.GameVariable)(nil)).
Where("id = ? AND game_id = ?", existing.ID, gameID).
Exec(c)
if err != nil {
c.JSON(http.StatusInternalServerError, gin.H{"error": err.Error()})
return
}
if err := tx.Commit(); err != nil {
c.JSON(http.StatusInternalServerError, gin.H{"error": err.Error()})
return
}
c.JSON(http.StatusOK, gin.H{"message": "Game variable deleted"})
}
func (h *GameHandler) gameExists(ctx context.Context, gameID int64) (bool, error) {
return h.db.NewSelect().
Model((*models.Game)(nil)).
Where("id = ?", gameID).
Exists(ctx)
}
func (h *GameHandler) getGameVariable(ctx context.Context, gameID, variableID int64) (*models.GameVariable, error) {
variable := new(models.GameVariable)
err := h.db.NewSelect().
Model(variable).
Relation("Items", func(q *bun.SelectQuery) *bun.SelectQuery {
return q.OrderExpr("gvi.id ASC")
}).
Where("gv.id = ? AND gv.game_id = ?", variableID, gameID).
Scan(ctx)
if err != nil {
return nil, err
}
normalizeGameVariableResponse(variable)
return variable, nil
}
func insertGameVariableItems(ctx context.Context, tx bun.Tx, variableID int64, req models.UpsertGameVariableRequest) error {
if req.Type != models.GameVariableTypeTable && req.Type != models.GameVariableTypeVector {
return nil
}
for _, itemReq := range req.Items {
key := strings.TrimSpace(itemReq.Key)
if req.Type == models.GameVariableTypeVector {
key = strconv.FormatInt(*itemReq.Index, 10)
}
item := &models.GameVariableItem{
GameVariableID: variableID,
Key: key,
NumberValue: itemReq.NumberValue,
StringValue: itemReq.StringValue,
CreatedAt: time.Now(),
UpdatedAt: time.Now(),
}
if _, err := tx.NewInsert().Model(item).Exec(ctx); err != nil {
return fmt.Errorf("could not save table items")
}
}
return nil
}
func validateUpsertGameVariableRequest(req *models.UpsertGameVariableRequest) error {
req.Key = strings.TrimSpace(req.Key)
if req.Key == "" {
return fmt.Errorf("key is required")
}
switch req.Type {
case models.GameVariableTypeNumber:
if req.NumberValue == nil {
return fmt.Errorf("number_value is required for number type")
}
if req.StringValue != nil || req.TableValueType != nil || len(req.Items) > 0 {
return fmt.Errorf("number type only accepts number_value")
}
case models.GameVariableTypeString:
if req.StringValue == nil {
return fmt.Errorf("string_value is required for string type")
}
if req.NumberValue != nil || req.TableValueType != nil || len(req.Items) > 0 {
return fmt.Errorf("string type only accepts string_value")
}
case models.GameVariableTypeTable:
if req.NumberValue != nil || req.StringValue != nil {
return fmt.Errorf("table type does not accept scalar values")
}
if req.TableValueType == nil {
return fmt.Errorf("table_value_type is required for table type")
}
if *req.TableValueType != models.GameVariableTableValueTypeNumber &&
*req.TableValueType != models.GameVariableTableValueTypeString {
return fmt.Errorf("unsupported table_value_type")
}
seenKeys := make(map[string]struct{}, len(req.Items))
for i := range req.Items {
req.Items[i].Key = strings.TrimSpace(req.Items[i].Key)
if req.Items[i].Key == "" {
return fmt.Errorf("items[%d].key is required", i)
}
if _, exists := seenKeys[req.Items[i].Key]; exists {
return fmt.Errorf("duplicate table item key: %s", req.Items[i].Key)
}
seenKeys[req.Items[i].Key] = struct{}{}
switch *req.TableValueType {
case models.GameVariableTableValueTypeNumber:
if req.Items[i].NumberValue == nil || req.Items[i].StringValue != nil {
return fmt.Errorf("items[%d] must contain only number_value", i)
}
case models.GameVariableTableValueTypeString:
if req.Items[i].StringValue == nil || req.Items[i].NumberValue != nil {
return fmt.Errorf("items[%d] must contain only string_value", i)
}
}
}
case models.GameVariableTypeVector:
if req.NumberValue != nil || req.StringValue != nil {
return fmt.Errorf("vector type does not accept scalar values")
}
if req.TableValueType == nil {
return fmt.Errorf("table_value_type is required for vector type")
}
if *req.TableValueType != models.GameVariableTableValueTypeNumber &&
*req.TableValueType != models.GameVariableTableValueTypeString {
return fmt.Errorf("unsupported table_value_type")
}
seenIndexes := make(map[int64]struct{}, len(req.Items))
for i := range req.Items {
if req.Items[i].Index == nil {
return fmt.Errorf("items[%d].index is required", i)
}
if *req.Items[i].Index < 0 {
return fmt.Errorf("items[%d].index must be >= 0", i)
}
if _, exists := seenIndexes[*req.Items[i].Index]; exists {
return fmt.Errorf("duplicate vector item index: %d", *req.Items[i].Index)
}
seenIndexes[*req.Items[i].Index] = struct{}{}
switch *req.TableValueType {
case models.GameVariableTableValueTypeNumber:
if req.Items[i].NumberValue == nil || req.Items[i].StringValue != nil {
return fmt.Errorf("items[%d] must contain only number_value", i)
}
case models.GameVariableTableValueTypeString:
if req.Items[i].StringValue == nil || req.Items[i].NumberValue != nil {
return fmt.Errorf("items[%d] must contain only string_value", i)
}
}
}
default:
return fmt.Errorf("unsupported variable type")
}
return nil
}
func dbValuesFromVariableRequest(req models.UpsertGameVariableRequest) (interface{}, interface{}, interface{}) {
var numberValue interface{}
if req.NumberValue != nil {
numberValue = *req.NumberValue
}
var stringValue interface{}
if req.StringValue != nil {
stringValue = *req.StringValue
}
var tableValueType interface{}
if req.TableValueType != nil {
tableValueType = string(*req.TableValueType)
}
return numberValue, stringValue, tableValueType
}
func normalizeGameVariableResponse(variable *models.GameVariable) {
if variable.Type != models.GameVariableTypeVector {
return
}
for i := range variable.Items {
index, err := strconv.ParseInt(variable.Items[i].Key, 10, 64)
if err != nil {
continue
}
variable.Items[i].Index = &index
variable.Items[i].Key = ""
}
}