From 8cad0e1980eff1d8087fe6cef68bdbce4233d2fa Mon Sep 17 00:00:00 2001 From: Ravi Prasad Date: Mon, 12 May 2025 19:40:51 -0500 Subject: [PATCH] advanced models/controllers/service docs update --- controllers/player_advanced_controller.go | 111 +++++++----- docs/docs.go | 172 ++++++++++++++++++- docs/swagger.json | 172 ++++++++++++++++++- docs/swagger.yaml | 117 ++++++++++++- import.go | 13 +- main.go | 2 + models/player_advanced_stat.go | 8 +- services/player_advanced_playoffs_service.go | 53 ++++++ services/player_advanced_service.go | 5 +- services/player_total_playoffs_service.go | 2 +- 10 files changed, 586 insertions(+), 69 deletions(-) create mode 100644 services/player_advanced_playoffs_service.go diff --git a/controllers/player_advanced_controller.go b/controllers/player_advanced_controller.go index 147532d..c4cb813 100644 --- a/controllers/player_advanced_controller.go +++ b/controllers/player_advanced_controller.go @@ -1,44 +1,58 @@ package controllers import ( - "github.com/gofiber/fiber/v2" - "gorm.io/gorm" - "github.com/nprasad2077/NBA_Go/services" - "github.com/nprasad2077/NBA_Go/models" + "github.com/gofiber/fiber/v2" + "gorm.io/gorm" + + "github.com/nprasad2077/NBA_Go/models" + "github.com/nprasad2077/NBA_Go/services" ) -func FetchPlayerAdvancedStats(db *gorm.DB) fiber.Handler { - return func(c *fiber.Ctx) error { - season := c.QueryInt("season", 2025) - - // Call service and assign err here - err := services.FetchAndStorePlayerAdvancedStats(db, season) - if err != nil { - return c.Status(500).JSON(fiber.Map{"error": err.Error()}) - } - - return c.JSON(fiber.Map{"message": "Player stats fetched and saved."}) - } +// AdvancedStatsResponse is the swagger response model for GetAllAdvancedPlayerStats +// It wraps the returned player advanced stats and pagination metadata. +type AdvancedStatsResponse struct { + Data []models.PlayerAdvancedStat `json:"data"` + Pagination struct { + Total int64 `json:"total"` + Page int `json:"page"` + PageSize int `json:"pageSize"` + Pages int64 `json:"pages"` + } `json:"pagination"` } +// FetchPlayerAdvancedStats returns a handler that imports advanced stats for a season +// Note: not exposed in Swagger docs, only internal use. +func FetchPlayerAdvancedStats(db *gorm.DB) fiber.Handler { + return func(c *fiber.Ctx) error { + season := c.QueryInt("season", 2025) + isPlayoff := c.QueryBool("isPlayoff", false) + if err := services.FetchAndStorePlayerAdvancedStats(db, season, isPlayoff); err != nil { + return c.Status(500).JSON(fiber.Map{"error": err.Error()}) + } -// GetAllPlayerStats godoc -// @Security ApiKeyAuth -// @Summary Get player stats -// @Description Returns filtered and paginated player stats -// @Tags PlayerStats -// @Accept json -// @Produce json -// @Param season query int false "Season (e.g., 2025)" -// @Param team query string false "Team abbreviation (e.g., MIL)" -// @Param playerId query string false "Player ID (e.g., greenaj01)" -// @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., per, games, winShares)" -// @Param ascending query bool false "Sort ascending (default false)" -// @Success 200 {object} map[string]interface{} -// @Router /api/playeradvancedstats [get] + return c.JSON(fiber.Map{"message": "Player stats fetched and saved."}) + } +} + +// GetAllAdvancedPlayerStats godoc +// @Security ApiKeyAuth +// @Summary Get player advanced stats +// @Description Returns filtered and paginated player advanced stats +// @Tags PlayerStats +// @Accept json +// @Produce json +// @Param season query int false "Season (e.g., 2025)" +// @Param team query string false "Team abbreviation (e.g., MIL)" +// @Param playerId query string false "Player ID (e.g., greenaj01)" +// @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" default(winShares) +// @Param ascending query bool false "Sort ascending" default(false) +// @Param isPlayoff query bool false "Whether playoffs?" +// @Success 200 {object} controllers.AdvancedStatsResponse +// @Failure 500 {object} map[string]string +// @Router /api/playeradvancedstats [get] func GetAllAdvancedPlayerStats(db *gorm.DB) fiber.Handler { return func(c *fiber.Ctx) error { var stats []models.PlayerAdvancedStat @@ -54,7 +68,7 @@ func GetAllAdvancedPlayerStats(db *gorm.DB) fiber.Handler { offset := (page - 1) * pageSize // Sorting - sortBy := c.Query("sortBy", "win_shares") // default field + sortBy := c.Query("sortBy", "win_shares") ascending := c.QueryBool("ascending", false) order := sortBy + " DESC" if ascending { @@ -74,23 +88,28 @@ func GetAllAdvancedPlayerStats(db *gorm.DB) fiber.Handler { query = query.Where("player_id = ?", playerId) } - var total int64 - query.Count(&total) // get total count before pagination + if c.Query("isPlayoff") != "" { + isPlayoff := c.QueryBool("isPlayoff", false) + query = query.Where("is_playoff = ?", isPlayoff) + } + // Count total + var total int64 + query.Count(&total) + + // Fetch page err := query.Order(order).Limit(pageSize).Offset(offset).Find(&stats).Error if err != nil { return c.Status(500).JSON(fiber.Map{"error": err.Error()}) } - // Response with metadata - return c.JSON(fiber.Map{ - "data": stats, - "pagination": fiber.Map{ - "total": total, - "page": page, - "pageSize": pageSize, - "pages": (total + int64(pageSize) - 1) / int64(pageSize), - }, - }) + // Build response + resp := AdvancedStatsResponse{Data: stats} + 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) } -} \ No newline at end of file +} diff --git a/docs/docs.go b/docs/docs.go index 73d7410..9a0504b 100644 --- a/docs/docs.go +++ b/docs/docs.go @@ -22,7 +22,7 @@ const docTemplate = `{ "ApiKeyAuth": [] } ], - "description": "Returns filtered and paginated player stats", + "description": "Returns filtered and paginated player advanced stats", "consumes": [ "application/json" ], @@ -32,7 +32,7 @@ const docTemplate = `{ "tags": [ "PlayerStats" ], - "summary": "Get player stats", + "summary": "Get player advanced stats", "parameters": [ { "type": "integer", @@ -68,23 +68,39 @@ const docTemplate = `{ }, { "type": "string", - "description": "Field to sort by (e.g., per, games, winShares)", + "default": "winShares", + "description": "Field to sort by", "name": "sortBy", "in": "query" }, { "type": "boolean", - "description": "Sort ascending (default false)", + "default": false, + "description": "Sort ascending", "name": "ascending", "in": "query" + }, + { + "type": "boolean", + "description": "Whether playoffs?", + "name": "isPlayoff", + "in": "query" } ], "responses": { "200": { "description": "OK", + "schema": { + "$ref": "#/definitions/controllers.AdvancedStatsResponse" + } + }, + "500": { + "description": "Internal Server Error", "schema": { "type": "object", - "additionalProperties": true + "additionalProperties": { + "type": "string" + } } } } @@ -181,6 +197,152 @@ const docTemplate = `{ } } }, + "definitions": { + "controllers.AdvancedStatsResponse": { + "type": "object", + "properties": { + "data": { + "type": "array", + "items": { + "$ref": "#/definitions/models.PlayerAdvancedStat" + } + }, + "pagination": { + "type": "object", + "properties": { + "page": { + "type": "integer" + }, + "pageSize": { + "type": "integer" + }, + "pages": { + "type": "integer" + }, + "total": { + "type": "integer" + } + } + } + } + }, + "gorm.DeletedAt": { + "type": "object", + "properties": { + "time": { + "type": "string" + }, + "valid": { + "description": "Valid is true if Time is not NULL", + "type": "boolean" + } + } + }, + "models.PlayerAdvancedStat": { + "type": "object", + "properties": { + "age": { + "type": "integer" + }, + "assistPercent": { + "type": "number" + }, + "blockPercent": { + "type": "number" + }, + "box": { + "type": "number" + }, + "createdAt": { + "type": "string" + }, + "defensiveBox": { + "type": "number" + }, + "defensiveRBPercent": { + "type": "number" + }, + "defensiveWS": { + "type": "number" + }, + "deletedAt": { + "$ref": "#/definitions/gorm.DeletedAt" + }, + "ftr": { + "type": "number" + }, + "games": { + "type": "integer" + }, + "id": { + "type": "integer" + }, + "isPlayoff": { + "type": "boolean" + }, + "minutesPlayed": { + "type": "integer" + }, + "offensiveBox": { + "type": "number" + }, + "offensiveRBPercent": { + "type": "number" + }, + "offensiveWS": { + "type": "number" + }, + "per": { + "type": "number" + }, + "playerId": { + "type": "string" + }, + "playerName": { + "type": "string" + }, + "position": { + "type": "string" + }, + "season": { + "type": "integer" + }, + "stealPercent": { + "type": "number" + }, + "team": { + "type": "string" + }, + "threePAR": { + "type": "number" + }, + "totalRBPercent": { + "type": "number" + }, + "tsPercent": { + "type": "number" + }, + "turnoverPercent": { + "type": "number" + }, + "updatedAt": { + "type": "string" + }, + "usagePercent": { + "type": "number" + }, + "vorp": { + "type": "number" + }, + "winShares": { + "type": "number" + }, + "winSharesPer": { + "type": "number" + } + } + } + }, "securityDefinitions": { "ApiKeyAuth": { "type": "apiKey", diff --git a/docs/swagger.json b/docs/swagger.json index 0767029..8fcf312 100644 --- a/docs/swagger.json +++ b/docs/swagger.json @@ -19,7 +19,7 @@ "ApiKeyAuth": [] } ], - "description": "Returns filtered and paginated player stats", + "description": "Returns filtered and paginated player advanced stats", "consumes": [ "application/json" ], @@ -29,7 +29,7 @@ "tags": [ "PlayerStats" ], - "summary": "Get player stats", + "summary": "Get player advanced stats", "parameters": [ { "type": "integer", @@ -65,23 +65,39 @@ }, { "type": "string", - "description": "Field to sort by (e.g., per, games, winShares)", + "default": "winShares", + "description": "Field to sort by", "name": "sortBy", "in": "query" }, { "type": "boolean", - "description": "Sort ascending (default false)", + "default": false, + "description": "Sort ascending", "name": "ascending", "in": "query" + }, + { + "type": "boolean", + "description": "Whether playoffs?", + "name": "isPlayoff", + "in": "query" } ], "responses": { "200": { "description": "OK", + "schema": { + "$ref": "#/definitions/controllers.AdvancedStatsResponse" + } + }, + "500": { + "description": "Internal Server Error", "schema": { "type": "object", - "additionalProperties": true + "additionalProperties": { + "type": "string" + } } } } @@ -178,6 +194,152 @@ } } }, + "definitions": { + "controllers.AdvancedStatsResponse": { + "type": "object", + "properties": { + "data": { + "type": "array", + "items": { + "$ref": "#/definitions/models.PlayerAdvancedStat" + } + }, + "pagination": { + "type": "object", + "properties": { + "page": { + "type": "integer" + }, + "pageSize": { + "type": "integer" + }, + "pages": { + "type": "integer" + }, + "total": { + "type": "integer" + } + } + } + } + }, + "gorm.DeletedAt": { + "type": "object", + "properties": { + "time": { + "type": "string" + }, + "valid": { + "description": "Valid is true if Time is not NULL", + "type": "boolean" + } + } + }, + "models.PlayerAdvancedStat": { + "type": "object", + "properties": { + "age": { + "type": "integer" + }, + "assistPercent": { + "type": "number" + }, + "blockPercent": { + "type": "number" + }, + "box": { + "type": "number" + }, + "createdAt": { + "type": "string" + }, + "defensiveBox": { + "type": "number" + }, + "defensiveRBPercent": { + "type": "number" + }, + "defensiveWS": { + "type": "number" + }, + "deletedAt": { + "$ref": "#/definitions/gorm.DeletedAt" + }, + "ftr": { + "type": "number" + }, + "games": { + "type": "integer" + }, + "id": { + "type": "integer" + }, + "isPlayoff": { + "type": "boolean" + }, + "minutesPlayed": { + "type": "integer" + }, + "offensiveBox": { + "type": "number" + }, + "offensiveRBPercent": { + "type": "number" + }, + "offensiveWS": { + "type": "number" + }, + "per": { + "type": "number" + }, + "playerId": { + "type": "string" + }, + "playerName": { + "type": "string" + }, + "position": { + "type": "string" + }, + "season": { + "type": "integer" + }, + "stealPercent": { + "type": "number" + }, + "team": { + "type": "string" + }, + "threePAR": { + "type": "number" + }, + "totalRBPercent": { + "type": "number" + }, + "tsPercent": { + "type": "number" + }, + "turnoverPercent": { + "type": "number" + }, + "updatedAt": { + "type": "string" + }, + "usagePercent": { + "type": "number" + }, + "vorp": { + "type": "number" + }, + "winShares": { + "type": "number" + }, + "winSharesPer": { + "type": "number" + } + } + } + }, "securityDefinitions": { "ApiKeyAuth": { "type": "apiKey", diff --git a/docs/swagger.yaml b/docs/swagger.yaml index b231759..2c142c4 100644 --- a/docs/swagger.yaml +++ b/docs/swagger.yaml @@ -1,4 +1,100 @@ basePath: / +definitions: + controllers.AdvancedStatsResponse: + properties: + data: + items: + $ref: '#/definitions/models.PlayerAdvancedStat' + type: array + pagination: + properties: + page: + type: integer + pageSize: + type: integer + pages: + type: integer + total: + type: integer + type: object + type: object + gorm.DeletedAt: + properties: + time: + type: string + valid: + description: Valid is true if Time is not NULL + type: boolean + type: object + models.PlayerAdvancedStat: + properties: + age: + type: integer + assistPercent: + type: number + blockPercent: + type: number + box: + type: number + createdAt: + type: string + defensiveBox: + type: number + defensiveRBPercent: + type: number + defensiveWS: + type: number + deletedAt: + $ref: '#/definitions/gorm.DeletedAt' + ftr: + type: number + games: + type: integer + id: + type: integer + isPlayoff: + type: boolean + minutesPlayed: + type: integer + offensiveBox: + type: number + offensiveRBPercent: + type: number + offensiveWS: + type: number + per: + type: number + playerId: + type: string + playerName: + type: string + position: + type: string + season: + type: integer + stealPercent: + type: number + team: + type: string + threePAR: + type: number + totalRBPercent: + type: number + tsPercent: + type: number + turnoverPercent: + type: number + updatedAt: + type: string + usagePercent: + type: number + vorp: + type: number + winShares: + type: number + winSharesPer: + type: number + type: object info: contact: {} description: Stats service with API-key auth @@ -9,7 +105,7 @@ paths: get: consumes: - application/json - description: Returns filtered and paginated player stats + description: Returns filtered and paginated player advanced stats parameters: - description: Season (e.g., 2025) in: query @@ -33,25 +129,36 @@ paths: in: query name: pageSize type: integer - - description: Field to sort by (e.g., per, games, winShares) + - default: winShares + description: Field to sort by in: query name: sortBy type: string - - description: Sort ascending (default false) + - default: false + description: Sort ascending in: query name: ascending type: boolean + - description: Whether playoffs? + in: query + name: isPlayoff + type: boolean produces: - application/json responses: "200": description: OK schema: - additionalProperties: true + $ref: '#/definitions/controllers.AdvancedStatsResponse' + "500": + description: Internal Server Error + schema: + additionalProperties: + type: string type: object security: - ApiKeyAuth: [] - summary: Get player stats + summary: Get player advanced stats tags: - PlayerStats /api/playertotals: diff --git a/import.go b/import.go index 3bf396d..617e746 100644 --- a/import.go +++ b/import.go @@ -11,7 +11,18 @@ import ( // importPlayerAdvanced fetches and stores advanced stats for seasons 2023–2025 func importPlayerAdvanced(db *gorm.DB) { for season := 2023; season <= 2025; season++ { - if err := services.FetchAndStorePlayerAdvancedStats(db, season); err != nil { + if err := services.FetchAndStorePlayerAdvancedStats(db, season, false); err != nil { + log.Printf("advanced import failed for %d: %v", season, err) + } + time.Sleep(1100 * time.Millisecond) + } +} + +// importPlayerAdvancedPlayoffs fetches and stores advanced stats for playoffs seasons 2023–2025 + +func importPlayerAdvancedPlayoffs (db *gorm.DB) { + for season := 2023; season <= 2024; season++ { + if err := services.FetchAndStorePlayerAdvancedPlayoffsStats(db, season, true); err != nil { log.Printf("advanced import failed for %d: %v", season, err) } time.Sleep(1100 * time.Millisecond) diff --git a/main.go b/main.go index b0e09fc..a6e51c4 100644 --- a/main.go +++ b/main.go @@ -38,6 +38,8 @@ func main() { db := config.InitDB() importPlayerAdvanced(db) log.Println("🎉 Player Advanced Import completed successfully") + importPlayerAdvancedPlayoffs(db) + log.Println("🎉 Player Advanced Playoffs Import completed successfully") importPlayerTotals(db) log.Println("🎉 Player Totals Import completed successfully") importPlayerPlayoffs(db) diff --git a/models/player_advanced_stat.go b/models/player_advanced_stat.go index 6a5c478..e5dc921 100644 --- a/models/player_advanced_stat.go +++ b/models/player_advanced_stat.go @@ -5,7 +5,7 @@ import "gorm.io/gorm" type PlayerAdvancedStat struct { gorm.Model ExternalID int `json:"id"` - PlayerID string `gorm:"not null;uniqueIndex:idx_player_season_team" json:"playerId"` + PlayerID string `gorm:"not null;index:idx_player_season_team,unique" json:"playerId"` PlayerName string `json:"playerName"` Position string `json:"position"` Age int `json:"age"` @@ -31,7 +31,7 @@ type PlayerAdvancedStat struct { DefensiveBox float64 `json:"defensiveBox"` Box float64 `json:"box"` VORP float64 `json:"vorp"` - Team string `gorm:"not null;uniqueIndex:idx_player_season_team" json:"team"` - Season int `gorm:"not null;uniqueIndex:idx_player_season_team" json:"season"` - isPlayoff bool `gorm:"not null;default:false;uniqueIndex:idx_player_season_team" json:"isPlayoff"` + Team string `gorm:"not null;index:idx_player_season_team,unique" json:"team"` + Season int `gorm:"not null;index:idx_player_season_team,unique" json:"season"` + IsPlayoff bool `gorm:"not null;default:false;index:idx_player_season_team,unique" json:"isPlayoff"` } \ No newline at end of file diff --git a/services/player_advanced_playoffs_service.go b/services/player_advanced_playoffs_service.go new file mode 100644 index 0000000..ef6c8d6 --- /dev/null +++ b/services/player_advanced_playoffs_service.go @@ -0,0 +1,53 @@ +package services + +import ( + "encoding/json" + "fmt" + "log" + "github.com/nprasad2077/NBA_Go/models" + "github.com/nprasad2077/NBA_Go/utils" + "github.com/nprasad2077/NBA_Go/utils/metrics" + "gorm.io/gorm" + "gorm.io/gorm/clause" +) + +func FetchAndStorePlayerAdvancedPlayoffsStats(db *gorm.DB, season int, isPlayoff bool) error { + metrics.DBOperationsTotal.WithLabelValues("fetch", "player_advanced").Inc() + url := fmt.Sprintf("http://rest.nbaapi.com/api/PlayerDataAdvancedPlayoffs/query?season=%d&sortBy=PlayerName&ascending=true&pageNumber=1&pageSize=1000", season) + + body, err := utils.GetJSON(url) + if err != nil { + return err + } + + var stats []models.PlayerAdvancedStat + if err := json.Unmarshal(body, &stats); err != nil { + return err + } + + for _, stat := range stats { + // Ensure the season is included from the query param + stat.Season = season + stat.IsPlayoff = isPlayoff + + err := db.Clauses(clause.OnConflict{ + Columns: []clause.Column{{Name: "player_id"}, {Name: "season"}, {Name: "team"}, {Name: "is_playoff"}}, + DoUpdates: clause.AssignmentColumns([]string{ + "external_id", "player_name", "position", "age", "games", + "minutes_played", "per", "ts_percent", "three_par", "ftr", + "offensive_rb_percent", "defensive_rb_percent", "total_rb_percent", + "assist_percent", "steal_percent", "block_percent", "turnover_percent", + "usage_percent", "offensive_ws", "defensive_ws", "win_shares", + "win_shares_per", "offensive_box", "defensive_box", "box", "vorp", + }), + }).Create(&stat).Error + + metrics.DBOperationsTotal.WithLabelValues("store", "player_advanced").Inc() + + if err != nil { + log.Printf("Failed to upsert stat for playerId %s (%s): %v", stat.PlayerID, stat.Team, err) + } + } + + return nil +} \ No newline at end of file diff --git a/services/player_advanced_service.go b/services/player_advanced_service.go index d12f9d6..37bd388 100644 --- a/services/player_advanced_service.go +++ b/services/player_advanced_service.go @@ -11,7 +11,7 @@ import ( "gorm.io/gorm/clause" ) -func FetchAndStorePlayerAdvancedStats(db *gorm.DB, season int) error { +func FetchAndStorePlayerAdvancedStats(db *gorm.DB, season int, isPlayoff bool) error { metrics.DBOperationsTotal.WithLabelValues("fetch", "player_advanced").Inc() url := fmt.Sprintf("http://rest.nbaapi.com/api/PlayerDataAdvanced/query?season=%d&sortBy=Points&ascending=false&pageNumber=1&pageSize=1000", season) @@ -28,9 +28,10 @@ func FetchAndStorePlayerAdvancedStats(db *gorm.DB, season int) error { for _, stat := range stats { // Ensure the season is included from the query param stat.Season = season + stat.IsPlayoff = isPlayoff err := db.Clauses(clause.OnConflict{ - Columns: []clause.Column{{Name: "player_id"}, {Name: "season"}, {Name: "team"}}, + Columns: []clause.Column{{Name: "player_id"}, {Name: "season"}, {Name: "team"}, {Name: "is_playoff"}}, DoUpdates: clause.AssignmentColumns([]string{ "external_id", "player_name", "position", "age", "games", "minutes_played", "per", "ts_percent", "three_par", "ftr", diff --git a/services/player_total_playoffs_service.go b/services/player_total_playoffs_service.go index 0a5978d..c6606c4 100644 --- a/services/player_total_playoffs_service.go +++ b/services/player_total_playoffs_service.go @@ -44,7 +44,7 @@ func FetchAndStorePlayerTotalPlayoffsStats(db *gorm.DB, season int, isPlayoff bo }).Create(&stat).Error if err != nil { - log.Printf("Failed to upsert PlayerTotalStat for playerId %s: %v", stat.PlayerID, err) + log.Printf("Failed to upsert PlayerTotalPlayoffStat for playerId %s: %v", stat.PlayerID, err) } }