advanced models/controllers/service docs update

This commit is contained in:
Ravi Prasad
2025-05-12 19:40:51 -05:00
parent b6a3b59b3c
commit 8cad0e1980
10 changed files with 586 additions and 69 deletions
+65 -46
View File
@@ -1,44 +1,58 @@
package controllers package controllers
import ( import (
"github.com/gofiber/fiber/v2" "github.com/gofiber/fiber/v2"
"gorm.io/gorm" "gorm.io/gorm"
"github.com/nprasad2077/NBA_Go/services"
"github.com/nprasad2077/NBA_Go/models" "github.com/nprasad2077/NBA_Go/models"
"github.com/nprasad2077/NBA_Go/services"
) )
func FetchPlayerAdvancedStats(db *gorm.DB) fiber.Handler { // AdvancedStatsResponse is the swagger response model for GetAllAdvancedPlayerStats
return func(c *fiber.Ctx) error { // It wraps the returned player advanced stats and pagination metadata.
season := c.QueryInt("season", 2025) type AdvancedStatsResponse struct {
Data []models.PlayerAdvancedStat `json:"data"`
// Call service and assign err here Pagination struct {
err := services.FetchAndStorePlayerAdvancedStats(db, season) Total int64 `json:"total"`
if err != nil { Page int `json:"page"`
return c.Status(500).JSON(fiber.Map{"error": err.Error()}) PageSize int `json:"pageSize"`
} Pages int64 `json:"pages"`
} `json:"pagination"`
return c.JSON(fiber.Map{"message": "Player stats fetched and saved."})
}
} }
// 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 return c.JSON(fiber.Map{"message": "Player stats fetched and saved."})
// @Security ApiKeyAuth }
// @Summary Get player stats }
// @Description Returns filtered and paginated player stats
// @Tags PlayerStats // GetAllAdvancedPlayerStats godoc
// @Accept json // @Security ApiKeyAuth
// @Produce json // @Summary Get player advanced stats
// @Param season query int false "Season (e.g., 2025)" // @Description Returns filtered and paginated player advanced stats
// @Param team query string false "Team abbreviation (e.g., MIL)" // @Tags PlayerStats
// @Param playerId query string false "Player ID (e.g., greenaj01)" // @Accept json
// @Param page query int false "Page number" default(1) // @Produce json
// @Param pageSize query int false "Page size" default(20) // @Param season query int false "Season (e.g., 2025)"
// @Param sortBy query string false "Field to sort by (e.g., per, games, winShares)" // @Param team query string false "Team abbreviation (e.g., MIL)"
// @Param ascending query bool false "Sort ascending (default false)" // @Param playerId query string false "Player ID (e.g., greenaj01)"
// @Success 200 {object} map[string]interface{} // @Param page query int false "Page number" default(1)
// @Router /api/playeradvancedstats [get] // @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 { func GetAllAdvancedPlayerStats(db *gorm.DB) fiber.Handler {
return func(c *fiber.Ctx) error { return func(c *fiber.Ctx) error {
var stats []models.PlayerAdvancedStat var stats []models.PlayerAdvancedStat
@@ -54,7 +68,7 @@ func GetAllAdvancedPlayerStats(db *gorm.DB) fiber.Handler {
offset := (page - 1) * pageSize offset := (page - 1) * pageSize
// Sorting // Sorting
sortBy := c.Query("sortBy", "win_shares") // default field sortBy := c.Query("sortBy", "win_shares")
ascending := c.QueryBool("ascending", false) ascending := c.QueryBool("ascending", false)
order := sortBy + " DESC" order := sortBy + " DESC"
if ascending { if ascending {
@@ -74,23 +88,28 @@ func GetAllAdvancedPlayerStats(db *gorm.DB) fiber.Handler {
query = query.Where("player_id = ?", playerId) query = query.Where("player_id = ?", playerId)
} }
var total int64 if c.Query("isPlayoff") != "" {
query.Count(&total) // get total count before pagination 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 err := query.Order(order).Limit(pageSize).Offset(offset).Find(&stats).Error
if err != nil { if err != nil {
return c.Status(500).JSON(fiber.Map{"error": err.Error()}) return c.Status(500).JSON(fiber.Map{"error": err.Error()})
} }
// Response with metadata // Build response
return c.JSON(fiber.Map{ resp := AdvancedStatsResponse{Data: stats}
"data": stats, resp.Pagination.Total = total
"pagination": fiber.Map{ resp.Pagination.Page = page
"total": total, resp.Pagination.PageSize = pageSize
"page": page, resp.Pagination.Pages = (total + int64(pageSize) - 1) / int64(pageSize)
"pageSize": pageSize,
"pages": (total + int64(pageSize) - 1) / int64(pageSize), return c.JSON(resp)
},
})
} }
} }
+167 -5
View File
@@ -22,7 +22,7 @@ const docTemplate = `{
"ApiKeyAuth": [] "ApiKeyAuth": []
} }
], ],
"description": "Returns filtered and paginated player stats", "description": "Returns filtered and paginated player advanced stats",
"consumes": [ "consumes": [
"application/json" "application/json"
], ],
@@ -32,7 +32,7 @@ const docTemplate = `{
"tags": [ "tags": [
"PlayerStats" "PlayerStats"
], ],
"summary": "Get player stats", "summary": "Get player advanced stats",
"parameters": [ "parameters": [
{ {
"type": "integer", "type": "integer",
@@ -68,23 +68,39 @@ const docTemplate = `{
}, },
{ {
"type": "string", "type": "string",
"description": "Field to sort by (e.g., per, games, winShares)", "default": "winShares",
"description": "Field to sort by",
"name": "sortBy", "name": "sortBy",
"in": "query" "in": "query"
}, },
{ {
"type": "boolean", "type": "boolean",
"description": "Sort ascending (default false)", "default": false,
"description": "Sort ascending",
"name": "ascending", "name": "ascending",
"in": "query" "in": "query"
},
{
"type": "boolean",
"description": "Whether playoffs?",
"name": "isPlayoff",
"in": "query"
} }
], ],
"responses": { "responses": {
"200": { "200": {
"description": "OK", "description": "OK",
"schema": {
"$ref": "#/definitions/controllers.AdvancedStatsResponse"
}
},
"500": {
"description": "Internal Server Error",
"schema": { "schema": {
"type": "object", "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": { "securityDefinitions": {
"ApiKeyAuth": { "ApiKeyAuth": {
"type": "apiKey", "type": "apiKey",
+167 -5
View File
@@ -19,7 +19,7 @@
"ApiKeyAuth": [] "ApiKeyAuth": []
} }
], ],
"description": "Returns filtered and paginated player stats", "description": "Returns filtered and paginated player advanced stats",
"consumes": [ "consumes": [
"application/json" "application/json"
], ],
@@ -29,7 +29,7 @@
"tags": [ "tags": [
"PlayerStats" "PlayerStats"
], ],
"summary": "Get player stats", "summary": "Get player advanced stats",
"parameters": [ "parameters": [
{ {
"type": "integer", "type": "integer",
@@ -65,23 +65,39 @@
}, },
{ {
"type": "string", "type": "string",
"description": "Field to sort by (e.g., per, games, winShares)", "default": "winShares",
"description": "Field to sort by",
"name": "sortBy", "name": "sortBy",
"in": "query" "in": "query"
}, },
{ {
"type": "boolean", "type": "boolean",
"description": "Sort ascending (default false)", "default": false,
"description": "Sort ascending",
"name": "ascending", "name": "ascending",
"in": "query" "in": "query"
},
{
"type": "boolean",
"description": "Whether playoffs?",
"name": "isPlayoff",
"in": "query"
} }
], ],
"responses": { "responses": {
"200": { "200": {
"description": "OK", "description": "OK",
"schema": {
"$ref": "#/definitions/controllers.AdvancedStatsResponse"
}
},
"500": {
"description": "Internal Server Error",
"schema": { "schema": {
"type": "object", "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": { "securityDefinitions": {
"ApiKeyAuth": { "ApiKeyAuth": {
"type": "apiKey", "type": "apiKey",
+112 -5
View File
@@ -1,4 +1,100 @@
basePath: / 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: info:
contact: {} contact: {}
description: Stats service with API-key auth description: Stats service with API-key auth
@@ -9,7 +105,7 @@ paths:
get: get:
consumes: consumes:
- application/json - application/json
description: Returns filtered and paginated player stats description: Returns filtered and paginated player advanced stats
parameters: parameters:
- description: Season (e.g., 2025) - description: Season (e.g., 2025)
in: query in: query
@@ -33,25 +129,36 @@ paths:
in: query in: query
name: pageSize name: pageSize
type: integer type: integer
- description: Field to sort by (e.g., per, games, winShares) - default: winShares
description: Field to sort by
in: query in: query
name: sortBy name: sortBy
type: string type: string
- description: Sort ascending (default false) - default: false
description: Sort ascending
in: query in: query
name: ascending name: ascending
type: boolean type: boolean
- description: Whether playoffs?
in: query
name: isPlayoff
type: boolean
produces: produces:
- application/json - application/json
responses: responses:
"200": "200":
description: OK description: OK
schema: schema:
additionalProperties: true $ref: '#/definitions/controllers.AdvancedStatsResponse'
"500":
description: Internal Server Error
schema:
additionalProperties:
type: string
type: object type: object
security: security:
- ApiKeyAuth: [] - ApiKeyAuth: []
summary: Get player stats summary: Get player advanced stats
tags: tags:
- PlayerStats - PlayerStats
/api/playertotals: /api/playertotals:
+12 -1
View File
@@ -11,7 +11,18 @@ import (
// importPlayerAdvanced fetches and stores advanced stats for seasons 20232025 // importPlayerAdvanced fetches and stores advanced stats for seasons 20232025
func importPlayerAdvanced(db *gorm.DB) { func importPlayerAdvanced(db *gorm.DB) {
for season := 2023; season <= 2025; season++ { 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 20232025
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) log.Printf("advanced import failed for %d: %v", season, err)
} }
time.Sleep(1100 * time.Millisecond) time.Sleep(1100 * time.Millisecond)
+2
View File
@@ -38,6 +38,8 @@ func main() {
db := config.InitDB() db := config.InitDB()
importPlayerAdvanced(db) importPlayerAdvanced(db)
log.Println("🎉 Player Advanced Import completed successfully") log.Println("🎉 Player Advanced Import completed successfully")
importPlayerAdvancedPlayoffs(db)
log.Println("🎉 Player Advanced Playoffs Import completed successfully")
importPlayerTotals(db) importPlayerTotals(db)
log.Println("🎉 Player Totals Import completed successfully") log.Println("🎉 Player Totals Import completed successfully")
importPlayerPlayoffs(db) importPlayerPlayoffs(db)
+4 -4
View File
@@ -5,7 +5,7 @@ import "gorm.io/gorm"
type PlayerAdvancedStat struct { type PlayerAdvancedStat struct {
gorm.Model gorm.Model
ExternalID int `json:"id"` 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"` PlayerName string `json:"playerName"`
Position string `json:"position"` Position string `json:"position"`
Age int `json:"age"` Age int `json:"age"`
@@ -31,7 +31,7 @@ type PlayerAdvancedStat struct {
DefensiveBox float64 `json:"defensiveBox"` DefensiveBox float64 `json:"defensiveBox"`
Box float64 `json:"box"` Box float64 `json:"box"`
VORP float64 `json:"vorp"` VORP float64 `json:"vorp"`
Team string `gorm:"not null;uniqueIndex:idx_player_season_team" json:"team"` Team string `gorm:"not null;index:idx_player_season_team,unique" json:"team"`
Season int `gorm:"not null;uniqueIndex:idx_player_season_team" json:"season"` Season int `gorm:"not null;index:idx_player_season_team,unique" json:"season"`
isPlayoff bool `gorm:"not null;default:false;uniqueIndex:idx_player_season_team" json:"isPlayoff"` IsPlayoff bool `gorm:"not null;default:false;index:idx_player_season_team,unique" json:"isPlayoff"`
} }
@@ -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
}
+3 -2
View File
@@ -11,7 +11,7 @@ import (
"gorm.io/gorm/clause" "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() 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) 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 { for _, stat := range stats {
// Ensure the season is included from the query param // Ensure the season is included from the query param
stat.Season = season stat.Season = season
stat.IsPlayoff = isPlayoff
err := db.Clauses(clause.OnConflict{ 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{ DoUpdates: clause.AssignmentColumns([]string{
"external_id", "player_name", "position", "age", "games", "external_id", "player_name", "position", "age", "games",
"minutes_played", "per", "ts_percent", "three_par", "ftr", "minutes_played", "per", "ts_percent", "three_par", "ftr",
+1 -1
View File
@@ -44,7 +44,7 @@ func FetchAndStorePlayerTotalPlayoffsStats(db *gorm.DB, season int, isPlayoff bo
}).Create(&stat).Error }).Create(&stat).Error
if err != nil { 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)
} }
} }