mirror of
https://github.com/nprasad2077/NBA_Go.git
synced 2026-09-22 14:05:13 +00:00
games endpoint and controller
This commit is contained in:
@@ -0,0 +1,349 @@
|
||||
package controllers
|
||||
|
||||
import (
|
||||
"strings"
|
||||
"time"
|
||||
|
||||
"github.com/gofiber/fiber/v2"
|
||||
"gorm.io/gorm"
|
||||
|
||||
"github.com/nprasad2077/NBA_Go/models"
|
||||
"github.com/nprasad2077/NBA_Go/utils"
|
||||
)
|
||||
|
||||
// DTOs (Data Transfer Objects) for API responses
|
||||
// These structs define the exact JSON output, ensuring team names are abbreviated.
|
||||
|
||||
type LineScoreDTO struct {
|
||||
Team string `json:"team"` // Abbreviation
|
||||
Q1 int `json:"q1"`
|
||||
Q2 int `json:"q2"`
|
||||
Q3 int `json:"q3"`
|
||||
Q4 int `json:"q4"`
|
||||
OT1 int `json:"ot1"`
|
||||
OT2 int `json:"ot2"`
|
||||
OT3 int `json:"ot3"`
|
||||
Total int `json:"total"`
|
||||
}
|
||||
|
||||
type PlayerGameBasicStatDTO struct {
|
||||
PlayerID string `json:"playerId"`
|
||||
PlayerName string `json:"playerName"`
|
||||
Team string `json:"team"` // Abbreviation
|
||||
Status string `json:"status"`
|
||||
MP string `json:"mp"`
|
||||
FG int `json:"fg"`
|
||||
FGA int `json:"fga"`
|
||||
FGPercent float64 `json:"fgPercent"`
|
||||
ThreeP int `json:"threeP"`
|
||||
ThreePA int `json:"threePa"`
|
||||
ThreePPercent float64 `json:"threePPercent"`
|
||||
FT int `json:"ft"`
|
||||
FTA int `json:"fta"`
|
||||
FTPercent float64 `json:"ftPercent"`
|
||||
ORB int `json:"orb"`
|
||||
DRB int `json:"drb"`
|
||||
TRB int `json:"trb"`
|
||||
AST int `json:"ast"`
|
||||
STL int `json:"stl"`
|
||||
BLK int `json:"blk"`
|
||||
TOV int `json:"tov"`
|
||||
PF int `json:"pf"`
|
||||
PTS int `json:"pts"`
|
||||
GmSc float64 `json:"gmSc"`
|
||||
PlusMinus int `json:"plusMinus"`
|
||||
}
|
||||
|
||||
type PlayerGameAdvStatDTO struct {
|
||||
PlayerID string `json:"playerId"`
|
||||
PlayerName string `json:"playerName"`
|
||||
Team string `json:"team"` // Abbreviation
|
||||
MP string `json:"mp"`
|
||||
TSPercent float64 `json:"tsPercent"`
|
||||
EFGPercent float64 `json:"efgPercent"`
|
||||
ThreePAr float64 `json:"threePAr"`
|
||||
FTr float64 `json:"fTr"`
|
||||
ORBPercent float64 `json:"orbPercent"`
|
||||
DRBPercent float64 `json:"drbPercent"`
|
||||
TRBPercent float64 `json:"trbPercent"`
|
||||
ASTPercent float64 `json:"astPercent"`
|
||||
STLPercent float64 `json:"stlPercent"`
|
||||
BLKPercent float64 `json:"blkPercent"`
|
||||
TOVPercent float64 `json:"tovPercent"`
|
||||
USGPercent float64 `json:"usgPercent"`
|
||||
ORtg int `json:"oRtg"`
|
||||
DRtg int `json:"dRtg"`
|
||||
BPM float64 `json:"bpm"`
|
||||
}
|
||||
|
||||
type TeamGameBasicStatDTO struct {
|
||||
Team string `json:"team"` // Abbreviation
|
||||
FG int `json:"fg"`
|
||||
FGA int `json:"fga"`
|
||||
FGPercent float64 `json:"fgPercent"`
|
||||
ThreeP int `json:"threeP"`
|
||||
ThreePA int `json:"threePa"`
|
||||
ThreePPercent float64 `json:"threePPercent"`
|
||||
FT int `json:"ft"`
|
||||
FTA int `json:"fta"`
|
||||
FTPercent float64 `json:"ftPercent"`
|
||||
ORB int `json:"orb"`
|
||||
DRB int `json:"drb"`
|
||||
TRB int `json:"trb"`
|
||||
AST int `json:"ast"`
|
||||
STL int `json:"stl"`
|
||||
BLK int `json:"blk"`
|
||||
TOV int `json:"tov"`
|
||||
PF int `json:"pf"`
|
||||
PTS int `json:"pts"`
|
||||
}
|
||||
|
||||
type TeamGameAdvStatDTO struct {
|
||||
Team string `json:"team"` // Abbreviation
|
||||
ORtg float64 `json:"oRtg"`
|
||||
DRtg float64 `json:"dRtg"`
|
||||
TSPercent float64 `json:"tsPercent"`
|
||||
EFGPercent float64 `json:"efgPercent"`
|
||||
ThreePAr float64 `json:"threePAr"`
|
||||
FTr float64 `json:"fTr"`
|
||||
ORBPercent float64 `json:"orbPercent"`
|
||||
DRBPercent float64 `json:"drbPercent"`
|
||||
TRBPercent float64 `json:"trbPercent"`
|
||||
ASTPercent float64 `json:"astPercent"`
|
||||
STLPercent float64 `json:"stlPercent"`
|
||||
BLKPercent float64 `json:"blkPercent"`
|
||||
TOVPercent float64 `json:"tovPercent"`
|
||||
}
|
||||
|
||||
// GameResponseDTO is the top-level object for a single game in the response.
|
||||
type GameResponseDTO struct {
|
||||
ID uint `json:"id"`
|
||||
GameID string `json:"gameId"`
|
||||
Date time.Time `json:"date"`
|
||||
IsPlayoff bool `json:"isPlayoff"`
|
||||
StartTimeET string `json:"startTimeET"`
|
||||
Arena string `json:"arena"`
|
||||
VisitorTeam string `json:"visitorTeam"`
|
||||
VisitorPTS int `json:"visitorPts"`
|
||||
HomeTeam string `json:"homeTeam"`
|
||||
HomePTS int `json:"homePts"`
|
||||
GameDuration string `json:"gameDuration"`
|
||||
BoxScoreURL string `json:"boxScoreUrl"`
|
||||
LineScores []LineScoreDTO `json:"lineScores,omitempty"`
|
||||
PlayerGameBasicStats []PlayerGameBasicStatDTO `json:"playerGameBasicStats,omitempty"`
|
||||
PlayerGameAdvStats []PlayerGameAdvStatDTO `json:"playerGameAdvStats,omitempty"`
|
||||
TeamGameBasicStats []TeamGameBasicStatDTO `json:"teamGameBasicStats,omitempty"`
|
||||
TeamGameAdvStats []TeamGameAdvStatDTO `json:"teamGameAdvStats,omitempty"`
|
||||
}
|
||||
|
||||
// GamesResponse is the final paginated structure returned by the API.
|
||||
type GamesResponse struct {
|
||||
Data []GameResponseDTO `json:"data"`
|
||||
Pagination struct {
|
||||
Total int64 `json:"total"`
|
||||
Page int `json:"page"`
|
||||
PageSize int `json:"pageSize"`
|
||||
Pages int64 `json:"pages"`
|
||||
} `json:"pagination"`
|
||||
}
|
||||
|
||||
// gameSortMap translates API sort parameters to database column names.
|
||||
var gameSortMap = map[string]string{
|
||||
"date": "date",
|
||||
"visitorPts": "visitor_pts",
|
||||
"homePts": "home_pts",
|
||||
"gameDuration": "game_duration",
|
||||
}
|
||||
|
||||
// GetGames godoc
|
||||
// @Summary Get game data
|
||||
// @Description Returns a paginated list of games with optional filters and included associations.
|
||||
// @Tags Games
|
||||
// @Produce json
|
||||
// @Param date query string false "Filter by date (YYYY-MM-DD)"
|
||||
// @Param team query string false "Filter by team abbreviation (e.g., LAL)"
|
||||
// @Param gameId query string false "Filter by a specific Game ID"
|
||||
// @Param isPlayoff query bool false "Filter for playoff games"
|
||||
// @Param page query int false "Page number" default(1)
|
||||
// @Param pageSize query int false "Page size" default(20)
|
||||
// @Param sortBy query string false "Field to sort by (e.g., date)" default(date)
|
||||
// @Param ascending query bool false "Sort ascending" default(false)
|
||||
// @Param include query string false "Comma-separated list of associations to include (e.g., lineScores,playerGameBasicStats,teamGameAdvStats)"
|
||||
// @Success 200 {object} controllers.GamesResponse
|
||||
// @Failure 400 {object} map[string]string
|
||||
// @Failure 500 {object} map[string]string
|
||||
// @Router /api/games [get]
|
||||
func GetGames(db *gorm.DB) fiber.Handler {
|
||||
return func(c *fiber.Ctx) error {
|
||||
// --- Build Query ---
|
||||
query := db.Model(&models.Game{})
|
||||
|
||||
// --- Filtering ---
|
||||
if gameId := c.Query("gameId"); gameId != "" {
|
||||
query = query.Where("game_id = ?", gameId)
|
||||
}
|
||||
if dateStr := c.Query("date"); dateStr != "" {
|
||||
parsedDate, err := time.Parse("2006-01-02", dateStr)
|
||||
if err != nil {
|
||||
return c.Status(fiber.StatusBadRequest).JSON(fiber.Map{"error": "Invalid date format. Use YYYY-MM-DD."})
|
||||
}
|
||||
query = query.Where("date = ?", parsedDate)
|
||||
}
|
||||
if teamAbbr := c.Query("team"); teamAbbr != "" {
|
||||
// Use the helper function to get the full name for the DB query
|
||||
fullName := utils.GetFullName(strings.ToUpper(teamAbbr))
|
||||
if fullName != teamAbbr { // Check if a match was found
|
||||
query = query.Where("visitor_team = ? OR home_team = ?", fullName, fullName)
|
||||
}
|
||||
}
|
||||
if c.Query("isPlayoff") != "" {
|
||||
query = query.Where("is_playoff = ?", c.QueryBool("isPlayoff"))
|
||||
}
|
||||
|
||||
// --- Preloading Associations ---
|
||||
includeParam := c.Query("include")
|
||||
if includeParam != "" {
|
||||
associations := strings.Split(includeParam, ",")
|
||||
for _, assoc := range associations {
|
||||
// Preload the original model association based on the JSON field name
|
||||
// GORM is smart enough to handle the mapping from struct field to relation
|
||||
switch strings.TrimSpace(assoc) {
|
||||
case "lineScores":
|
||||
query = query.Preload("LineScores")
|
||||
case "playerGameBasicStats":
|
||||
query = query.Preload("PlayerGameBasicStats")
|
||||
case "playerGameAdvStats":
|
||||
query = query.Preload("PlayerGameAdvStats")
|
||||
case "teamGameBasicStats":
|
||||
query = query.Preload("TeamGameBasicStats")
|
||||
case "teamGameAdvStats":
|
||||
query = query.Preload("TeamGameAdvStats")
|
||||
}
|
||||
}
|
||||
}
|
||||
|
||||
// --- Sorting ---
|
||||
sortByParam := c.Query("sortBy", "date")
|
||||
sortBy, ok := gameSortMap[sortByParam]
|
||||
if !ok {
|
||||
sortBy = "date"
|
||||
}
|
||||
order := sortBy + " DESC"
|
||||
if c.QueryBool("ascending", false) {
|
||||
order = sortBy + " ASC"
|
||||
}
|
||||
query = query.Order(order)
|
||||
|
||||
// --- Pagination & Data Fetching ---
|
||||
var total int64
|
||||
query.Count(&total)
|
||||
|
||||
page := c.QueryInt("page", 1)
|
||||
pageSize := c.QueryInt("pageSize", 20)
|
||||
offset := (page - 1) * pageSize
|
||||
|
||||
var games []models.Game
|
||||
if err := query.Limit(pageSize).Offset(offset).Find(&games).Error; err != nil {
|
||||
return c.Status(fiber.StatusInternalServerError).JSON(fiber.Map{"error": err.Error()})
|
||||
}
|
||||
|
||||
// --- Transformation from Model to DTO ---
|
||||
gameDTOs := make([]GameResponseDTO, len(games))
|
||||
for i, game := range games {
|
||||
gameDTOs[i] = toGameResponseDTO(game)
|
||||
}
|
||||
|
||||
// --- Build Final Response ---
|
||||
resp := GamesResponse{Data: gameDTOs}
|
||||
resp.Pagination.Total = total
|
||||
resp.Pagination.Page = page
|
||||
resp.Pagination.PageSize = pageSize
|
||||
resp.Pagination.Pages = (total + int64(pageSize) - 1) / int64(pageSize)
|
||||
|
||||
return c.JSON(resp)
|
||||
}
|
||||
}
|
||||
|
||||
// toGameResponseDTO converts a Game model to its corresponding DTO, including nested associations.
|
||||
func toGameResponseDTO(game models.Game) GameResponseDTO {
|
||||
dto := GameResponseDTO{
|
||||
ID: game.ID,
|
||||
GameID: game.GameID,
|
||||
Date: game.Date,
|
||||
IsPlayoff: game.IsPlayoff,
|
||||
StartTimeET: game.StartTimeET,
|
||||
Arena: game.Arena,
|
||||
VisitorTeam: utils.GetAbbreviation(game.VisitorTeam),
|
||||
VisitorPTS: game.VisitorPTS,
|
||||
HomeTeam: utils.GetAbbreviation(game.HomeTeam),
|
||||
HomePTS: game.HomePTS,
|
||||
GameDuration: game.GameDuration,
|
||||
BoxScoreURL: game.BoxScoreURL,
|
||||
}
|
||||
|
||||
// Transform nested slices if they were loaded
|
||||
if len(game.LineScores) > 0 {
|
||||
dto.LineScores = make([]LineScoreDTO, len(game.LineScores))
|
||||
for i, ls := range game.LineScores {
|
||||
dto.LineScores[i] = LineScoreDTO{
|
||||
Team: utils.GetAbbreviation(ls.Team),
|
||||
Q1: ls.Q1, Q2: ls.Q2, Q3: ls.Q3, Q4: ls.Q4,
|
||||
OT1: ls.OT1, OT2: ls.OT2, OT3: ls.OT3,
|
||||
Total: ls.Total,
|
||||
}
|
||||
}
|
||||
}
|
||||
|
||||
if len(game.PlayerGameBasicStats) > 0 {
|
||||
dto.PlayerGameBasicStats = make([]PlayerGameBasicStatDTO, len(game.PlayerGameBasicStats))
|
||||
for i, pbs := range game.PlayerGameBasicStats {
|
||||
dto.PlayerGameBasicStats[i] = PlayerGameBasicStatDTO{
|
||||
PlayerID: pbs.PlayerID, PlayerName: pbs.PlayerName, Team: utils.GetAbbreviation(pbs.Team),
|
||||
Status: pbs.Status, MP: pbs.MP, FG: pbs.FG, FGA: pbs.FGA, FGPercent: pbs.FGPercent,
|
||||
ThreeP: pbs.ThreeP, ThreePA: pbs.ThreePA, ThreePPercent: pbs.ThreePPercent,
|
||||
FT: pbs.FT, FTA: pbs.FTA, FTPercent: pbs.FTPercent, ORB: pbs.ORB, DRB: pbs.DRB,
|
||||
TRB: pbs.TRB, AST: pbs.AST, STL: pbs.STL, BLK: pbs.BLK, TOV: pbs.TOV, PF: pbs.PF,
|
||||
PTS: pbs.PTS, GmSc: pbs.GmSc, PlusMinus: pbs.PlusMinus,
|
||||
}
|
||||
}
|
||||
}
|
||||
|
||||
if len(game.PlayerGameAdvStats) > 0 {
|
||||
dto.PlayerGameAdvStats = make([]PlayerGameAdvStatDTO, len(game.PlayerGameAdvStats))
|
||||
for i, pas := range game.PlayerGameAdvStats {
|
||||
dto.PlayerGameAdvStats[i] = PlayerGameAdvStatDTO{
|
||||
PlayerID: pas.PlayerID, PlayerName: pas.PlayerName, Team: utils.GetAbbreviation(pas.Team),
|
||||
MP: pas.MP, TSPercent: pas.TSPercent, EFGPercent: pas.EFGPercent, ThreePAr: pas.ThreePAr,
|
||||
FTr: pas.FTr, ORBPercent: pas.ORBPercent, DRBPercent: pas.DRBPercent, TRBPercent: pas.TRBPercent,
|
||||
ASTPercent: pas.ASTPercent, STLPercent: pas.STLPercent, BLKPercent: pas.BLKPercent,
|
||||
TOVPercent: pas.TOVPercent, USGPercent: pas.USGPercent, ORtg: pas.ORtg, DRtg: pas.DRtg, BPM: pas.BPM,
|
||||
}
|
||||
}
|
||||
}
|
||||
|
||||
if len(game.TeamGameBasicStats) > 0 {
|
||||
dto.TeamGameBasicStats = make([]TeamGameBasicStatDTO, len(game.TeamGameBasicStats))
|
||||
for i, tbs := range game.TeamGameBasicStats {
|
||||
dto.TeamGameBasicStats[i] = TeamGameBasicStatDTO{
|
||||
Team: utils.GetAbbreviation(tbs.Team), FG: tbs.FG, FGA: tbs.FGA, FGPercent: tbs.FGPercent,
|
||||
ThreeP: tbs.ThreeP, ThreePA: tbs.ThreePA, ThreePPercent: tbs.ThreePPercent,
|
||||
FT: tbs.FT, FTA: tbs.FTA, FTPercent: tbs.FTPercent, ORB: tbs.ORB, DRB: tbs.DRB,
|
||||
TRB: tbs.TRB, AST: tbs.AST, STL: tbs.STL, BLK: tbs.BLK, TOV: tbs.TOV, PF: tbs.PF, PTS: tbs.PTS,
|
||||
}
|
||||
}
|
||||
}
|
||||
|
||||
if len(game.TeamGameAdvStats) > 0 {
|
||||
dto.TeamGameAdvStats = make([]TeamGameAdvStatDTO, len(game.TeamGameAdvStats))
|
||||
for i, tas := range game.TeamGameAdvStats {
|
||||
dto.TeamGameAdvStats[i] = TeamGameAdvStatDTO{
|
||||
Team: utils.GetAbbreviation(tas.Team), ORtg: tas.ORtg, DRtg: tas.DRtg, TSPercent: tas.TSPercent,
|
||||
EFGPercent: tas.EFGPercent, ThreePAr: tas.ThreePAr, FTr: tas.FTr, ORBPercent: tas.ORBPercent,
|
||||
DRBPercent: tas.DRBPercent, TRBPercent: tas.TRBPercent, ASTPercent: tas.ASTPercent,
|
||||
STLPercent: tas.STLPercent, BLKPercent: tas.BLKPercent, TOVPercent: tas.TOVPercent,
|
||||
}
|
||||
}
|
||||
}
|
||||
|
||||
return dto
|
||||
}
|
||||
Reference in New Issue
Block a user