Commit 90a1bdcf by Augusto

new data and endpoint fix

parent 383db895
...@@ -3,6 +3,7 @@ module ScoutingSystemScoreData ...@@ -3,6 +3,7 @@ module ScoutingSystemScoreData
go 1.25.0 go 1.25.0
require ( require (
github.com/gin-contrib/cors v1.7.0
github.com/gin-gonic/gin v1.11.0 github.com/gin-gonic/gin v1.11.0
github.com/joho/godotenv v1.5.1 github.com/joho/godotenv v1.5.1
github.com/matoous/go-nanoid/v2 v2.1.0 github.com/matoous/go-nanoid/v2 v2.1.0
......
...@@ -16,6 +16,8 @@ github.com/davecgh/go-spew v1.1.1 h1:vj9j/u1bqnvCEfJOwUhtlOARqs3+rkHYY13jYWTU97c ...@@ -16,6 +16,8 @@ github.com/davecgh/go-spew v1.1.1 h1:vj9j/u1bqnvCEfJOwUhtlOARqs3+rkHYY13jYWTU97c
github.com/davecgh/go-spew v1.1.1/go.mod h1:J7Y8YcW2NihsgmVo/mv3lAwl/skON4iLHjSsI+c5H38= github.com/davecgh/go-spew v1.1.1/go.mod h1:J7Y8YcW2NihsgmVo/mv3lAwl/skON4iLHjSsI+c5H38=
github.com/gabriel-vasile/mimetype v1.4.8 h1:FfZ3gj38NjllZIeJAmMhr+qKL8Wu+nOoI3GqacKw1NM= github.com/gabriel-vasile/mimetype v1.4.8 h1:FfZ3gj38NjllZIeJAmMhr+qKL8Wu+nOoI3GqacKw1NM=
github.com/gabriel-vasile/mimetype v1.4.8/go.mod h1:ByKUIKGjh1ODkGM1asKUbQZOLGrPjydw3hYPU2YU9t8= github.com/gabriel-vasile/mimetype v1.4.8/go.mod h1:ByKUIKGjh1ODkGM1asKUbQZOLGrPjydw3hYPU2YU9t8=
github.com/gin-contrib/cors v1.7.0 h1:wZX2wuZ0o7rV2/1i7gb4Jn+gW7HBqaP91fizJkBUJOA=
github.com/gin-contrib/cors v1.7.0/go.mod h1:cI+h6iOAyxKRtUtC6iF/Si1KSFvGm/gK+kshxlCi8ro=
github.com/gin-contrib/gzip v0.0.6 h1:NjcunTcGAj5CO1gn4N8jHOSIeRFHIbn51z6K+xaN4d4= github.com/gin-contrib/gzip v0.0.6 h1:NjcunTcGAj5CO1gn4N8jHOSIeRFHIbn51z6K+xaN4d4=
github.com/gin-contrib/gzip v0.0.6/go.mod h1:QOJlmV2xmayAjkNS2Y8NQsMneuRShOU/kjovCXNuzzk= github.com/gin-contrib/gzip v0.0.6/go.mod h1:QOJlmV2xmayAjkNS2Y8NQsMneuRShOU/kjovCXNuzzk=
github.com/gin-contrib/sse v1.1.0 h1:n0w2GMuUpWDVp7qSpvze6fAu9iRxJY4Hmj6AmBOU05w= github.com/gin-contrib/sse v1.1.0 h1:n0w2GMuUpWDVp7qSpvze6fAu9iRxJY4Hmj6AmBOU05w=
......
package handlers package handlers
import ( import (
"context"
"fmt" "fmt"
"net/http" "net/http"
"strconv" "strconv"
...@@ -14,12 +15,13 @@ import ( ...@@ -14,12 +15,13 @@ import (
) )
type CoachHandler struct { type CoachHandler struct {
DB *gorm.DB
Service services.CoachService Service services.CoachService
} }
func RegisterCoachRoutes(rg *gin.RouterGroup, db *gorm.DB) { func RegisterCoachRoutes(rg *gin.RouterGroup, db *gorm.DB) {
service := services.NewCoachService(db) service := services.NewCoachService(db)
h := &CoachHandler{Service: service} h := &CoachHandler{DB: db, Service: service}
coaches := rg.Group("/coaches") coaches := rg.Group("/coaches")
coaches.GET("", h.List) coaches.GET("", h.List)
...@@ -32,6 +34,10 @@ type StructuredCoach struct { ...@@ -32,6 +34,10 @@ type StructuredCoach struct {
ID string `json:"id"` ID string `json:"id"`
WyID *int `json:"wyId"` WyID *int `json:"wyId"`
TsID string `json:"tsId"` TsID string `json:"tsId"`
CountryTsID *string `json:"countryTsId"`
CountryName *string `json:"countryName"`
TeamTsID *string `json:"teamTsId"`
TeamName *string `json:"teamName"`
FirstName string `json:"firstName"` FirstName string `json:"firstName"`
LastName string `json:"lastName"` LastName string `json:"lastName"`
MiddleName *string `json:"middleName"` MiddleName *string `json:"middleName"`
...@@ -57,35 +63,140 @@ type StructuredCoach struct { ...@@ -57,35 +63,140 @@ type StructuredCoach struct {
DeletedAt *time.Time `json:"deletedAt"` DeletedAt *time.Time `json:"deletedAt"`
} }
func toStructuredCoach(c models.Coach) StructuredCoach { type CoachListResponse struct {
return StructuredCoach{ Data []StructuredCoach `json:"data"`
ID: c.ID, Meta map[string]interface{} `json:"meta"`
WyID: nilIfZero(c.WyID), }
TsID: c.TsID,
FirstName: c.FirstName, type CoachResponse struct {
LastName: c.LastName, Data StructuredCoach `json:"data"`
MiddleName: c.MiddleName, Meta map[string]interface{} `json:"meta"`
ShortName: c.ShortName, }
DateOfBirth: c.DateOfBirth,
NationalityWyID: c.NationalityWyID,
CurrentTeamWyID: c.CurrentTeamWyID, func (h *CoachHandler) enrichCoaches(ctx context.Context, coaches []models.Coach) ([]StructuredCoach, error) {
Position: c.Position, areaTsIDs := make([]string, 0, len(coaches))
CoachingLicense: c.CoachingLicense, teamTsIDs := make([]string, 0, len(coaches))
YearsExperience: c.YearsExperience, seenArea := map[string]struct{}{}
PreferredFormation: c.PreferredFormation, seenTeam := map[string]struct{}{}
JoinedAt: c.JoinedAt,
ContractUntil: c.ContractUntil, for _, c := range coaches {
UID: c.UID, if c.CountryTsID != nil && *c.CountryTsID != "" {
Deathday: c.Deathday, if _, ok := seenArea[*c.CountryTsID]; !ok {
Status: c.Status, seenArea[*c.CountryTsID] = struct{}{}
ImageDataURL: c.ImageDataURL, areaTsIDs = append(areaTsIDs, *c.CountryTsID)
APILastSyncedAt: c.APILastSyncedAt, }
APISyncStatus: c.APISyncStatus, }
IsActive: c.IsActive, if c.TeamTsID != nil && *c.TeamTsID != "" {
CreatedAt: c.CreatedAt, if _, ok := seenTeam[*c.TeamTsID]; !ok {
UpdatedAt: c.UpdatedAt, seenTeam[*c.TeamTsID] = struct{}{}
DeletedAt: c.DeletedAt, teamTsIDs = append(teamTsIDs, *c.TeamTsID)
}
}
}
areaNameByTsID := map[string]string{}
if len(areaTsIDs) > 0 {
var areas []struct {
TsID string `gorm:"column:ts_id"`
Name string `gorm:"column:name"`
}
if err := h.DB.WithContext(ctx).
Model(&models.Area{}).
Select("ts_id", "name").
Where("ts_id IN ?", areaTsIDs).
Find(&areas).Error; err != nil {
return nil, err
}
for _, a := range areas {
if a.TsID != "" {
areaNameByTsID[a.TsID] = a.Name
}
}
} }
teamNameByTsID := map[string]string{}
if len(teamTsIDs) > 0 {
var teams []struct {
TsID string `gorm:"column:ts_id"`
Name string `gorm:"column:name"`
}
if err := h.DB.WithContext(ctx).
Model(&models.Team{}).
Select("ts_id", "name").
Where("ts_id IN ?", teamTsIDs).
Find(&teams).Error; err != nil {
return nil, err
}
for _, t := range teams {
if t.TsID != "" {
teamNameByTsID[t.TsID] = t.Name
}
}
}
out := make([]StructuredCoach, 0, len(coaches))
for _, c := range coaches {
var countryName *string
if c.CountryTsID != nil {
if n, ok := areaNameByTsID[*c.CountryTsID]; ok {
nn := n
countryName = &nn
}
}
var teamName *string
if c.TeamTsID != nil {
if n, ok := teamNameByTsID[*c.TeamTsID]; ok {
nn := n
teamName = &nn
}
}
out = append(out, StructuredCoach{
ID: c.ID,
WyID: nilIfZero(c.WyID),
TsID: c.TsID,
CountryTsID: c.CountryTsID,
CountryName: countryName,
TeamTsID: c.TeamTsID,
TeamName: teamName,
FirstName: c.FirstName,
LastName: c.LastName,
MiddleName: c.MiddleName,
ShortName: c.ShortName,
DateOfBirth: c.DateOfBirth,
NationalityWyID: c.NationalityWyID,
CurrentTeamWyID: c.CurrentTeamWyID,
Position: c.Position,
CoachingLicense: c.CoachingLicense,
YearsExperience: c.YearsExperience,
PreferredFormation: c.PreferredFormation,
JoinedAt: c.JoinedAt,
ContractUntil: c.ContractUntil,
UID: c.UID,
Deathday: c.Deathday,
Status: c.Status,
ImageDataURL: c.ImageDataURL,
APILastSyncedAt: c.APILastSyncedAt,
APISyncStatus: c.APISyncStatus,
IsActive: c.IsActive,
CreatedAt: c.CreatedAt,
UpdatedAt: c.UpdatedAt,
DeletedAt: c.DeletedAt,
})
}
return out, nil
}
func (h *CoachHandler) toStructuredCoach(ctx context.Context, c models.Coach) (StructuredCoach, error) {
rows, err := h.enrichCoaches(ctx, []models.Coach{c})
if err != nil {
return StructuredCoach{}, err
}
if len(rows) == 0 {
return StructuredCoach{}, nil
}
return rows[0], nil
} }
func nilIfZero(v *int) *int { func nilIfZero(v *int) *int {
...@@ -108,7 +219,7 @@ func nilIfZero(v *int) *int { ...@@ -108,7 +219,7 @@ func nilIfZero(v *int) *int {
// @Param teamId query string false "Filter coaches by current team ID (wy_id)" // @Param teamId query string false "Filter coaches by current team ID (wy_id)"
// @Param position query string false "Filter coaches by position (head_coach, assistant_coach, etc.)" // @Param position query string false "Filter coaches by position (head_coach, assistant_coach, etc.)"
// @Param active query bool false "Filter active coaches only" // @Param active query bool false "Filter active coaches only"
// @Success 200 {object} map[string]interface{} // @Success 200 {object} handlers.CoachListResponse
// @Failure 500 {object} map[string]string // @Failure 500 {object} map[string]string
// @Router /coaches [get] // @Router /coaches [get]
func (h *CoachHandler) List(c *gin.Context) { func (h *CoachHandler) List(c *gin.Context) {
...@@ -154,9 +265,10 @@ func (h *CoachHandler) List(c *gin.Context) { ...@@ -154,9 +265,10 @@ func (h *CoachHandler) List(c *gin.Context) {
return return
} }
structured := make([]StructuredCoach, 0, len(coaches)) structured, err := h.enrichCoaches(c.Request.Context(), coaches)
for _, coach := range coaches { if err != nil {
structured = append(structured, toStructuredCoach(coach)) respondError(c, err)
return
} }
page := offset/limit + 1 page := offset/limit + 1
...@@ -182,7 +294,7 @@ func (h *CoachHandler) List(c *gin.Context) { ...@@ -182,7 +294,7 @@ func (h *CoachHandler) List(c *gin.Context) {
// @Description Returns a single coach by its internal ID. // @Description Returns a single coach by its internal ID.
// @Tags Coaches // @Tags Coaches
// @Param id path string true "Coach internal identifier" // @Param id path string true "Coach internal identifier"
// @Success 200 {object} map[string]interface{} // @Success 200 {object} handlers.CoachResponse
// @Failure 404 {object} map[string]string // @Failure 404 {object} map[string]string
// @Failure 500 {object} map[string]string // @Failure 500 {object} map[string]string
// @Router /coaches/{id} [get] // @Router /coaches/{id} [get]
...@@ -195,7 +307,11 @@ func (h *CoachHandler) GetByID(c *gin.Context) { ...@@ -195,7 +307,11 @@ func (h *CoachHandler) GetByID(c *gin.Context) {
return return
} }
structured := toStructuredCoach(coach) structured, err := h.toStructuredCoach(c.Request.Context(), coach)
if err != nil {
respondError(c, err)
return
}
timestamp := time.Now().UTC().Format(time.RFC3339) timestamp := time.Now().UTC().Format(time.RFC3339)
endpoint := fmt.Sprintf("/coaches/%s", id) endpoint := fmt.Sprintf("/coaches/%s", id)
...@@ -214,7 +330,7 @@ func (h *CoachHandler) GetByID(c *gin.Context) { ...@@ -214,7 +330,7 @@ func (h *CoachHandler) GetByID(c *gin.Context) {
// @Description Returns a single coach by its provider identifier: numeric values are treated as Wyscout wy_id, non-numeric as TheSports ts_id. // @Description Returns a single coach by its provider identifier: numeric values are treated as Wyscout wy_id, non-numeric as TheSports ts_id.
// @Tags Coaches // @Tags Coaches
// @Param providerId path string true "Provider identifier (wy_id or ts_id)" // @Param providerId path string true "Provider identifier (wy_id or ts_id)"
// @Success 200 {object} map[string]interface{} // @Success 200 {object} handlers.CoachResponse
// @Failure 404 {object} map[string]string // @Failure 404 {object} map[string]string
// @Failure 500 {object} map[string]string // @Failure 500 {object} map[string]string
// @Router /coaches/provider/{providerId} [get] // @Router /coaches/provider/{providerId} [get]
...@@ -227,7 +343,11 @@ func (h *CoachHandler) GetByProviderID(c *gin.Context) { ...@@ -227,7 +343,11 @@ func (h *CoachHandler) GetByProviderID(c *gin.Context) {
return return
} }
structured := toStructuredCoach(coach) structured, err := h.toStructuredCoach(c.Request.Context(), coach)
if err != nil {
respondError(c, err)
return
}
timestamp := time.Now().UTC().Format(time.RFC3339) timestamp := time.Now().UTC().Format(time.RFC3339)
endpoint := fmt.Sprintf("/coaches/provider/%s", providerID) endpoint := fmt.Sprintf("/coaches/provider/%s", providerID)
...@@ -246,7 +366,7 @@ func (h *CoachHandler) GetByProviderID(c *gin.Context) { ...@@ -246,7 +366,7 @@ func (h *CoachHandler) GetByProviderID(c *gin.Context) {
// @Description Returns a single coach by its Wyscout wy_id identifier. // @Description Returns a single coach by its Wyscout wy_id identifier.
// @Tags Coaches // @Tags Coaches
// @Param wyId path int true "Wyscout wy_id identifier" // @Param wyId path int true "Wyscout wy_id identifier"
// @Success 200 {object} map[string]interface{} // @Success 200 {object} handlers.CoachResponse
// @Failure 400 {object} map[string]string // @Failure 400 {object} map[string]string
// @Failure 404 {object} map[string]string // @Failure 404 {object} map[string]string
// @Failure 500 {object} map[string]string // @Failure 500 {object} map[string]string
...@@ -265,7 +385,11 @@ func (h *CoachHandler) GetByWyID(c *gin.Context) { ...@@ -265,7 +385,11 @@ func (h *CoachHandler) GetByWyID(c *gin.Context) {
return return
} }
structured := toStructuredCoach(coach) structured, err := h.toStructuredCoach(c.Request.Context(), coach)
if err != nil {
respondError(c, err)
return
}
timestamp := time.Now().UTC().Format(time.RFC3339) timestamp := time.Now().UTC().Format(time.RFC3339)
endpoint := fmt.Sprintf("/coaches/wyscout/%d", wyID) endpoint := fmt.Sprintf("/coaches/wyscout/%d", wyID)
......
package handlers
import (
"fmt"
"net/http"
"strconv"
"time"
"github.com/gin-gonic/gin"
"gorm.io/gorm"
"ScoutingSystemScoreData/internal/services"
)
type CompetitionHandler struct {
Service services.CompetitionService
}
func RegisterCompetitionRoutes(rg *gin.RouterGroup, db *gorm.DB) {
service := services.NewCompetitionService(db)
h := &CompetitionHandler{Service: service}
competitions := rg.Group("/competitions")
competitions.GET("", h.List)
competitions.GET("/wyscout/:wyId", h.GetByWyID)
competitions.GET("/provider/:providerId", h.GetByProviderID)
competitions.GET("/:id", h.GetByID)
}
// List competitions
// @Summary List competitions
// @Description Returns a list of competitions with optional pagination.
// @Tags Competitions
// @Param limit query int false "Maximum number of items to return (default 100)"
// @Param offset query int false "Number of items to skip before starting to collect the result set (default 0)"
// @Success 200 {object} map[string]interface{}
// @Failure 500 {object} map[string]string
// @Router /competitions [get]
func (h *CompetitionHandler) List(c *gin.Context) {
limitStr := c.DefaultQuery("limit", "100")
offsetStr := c.DefaultQuery("offset", "0")
limit, err := strconv.Atoi(limitStr)
if err != nil || limit <= 0 {
limit = 100
}
offset, err := strconv.Atoi(offsetStr)
if err != nil || offset < 0 {
offset = 0
}
competitions, total, err := h.Service.ListCompetitions(c.Request.Context(), limit, offset)
if err != nil {
respondError(c, err)
return
}
page := offset/limit + 1
hasMore := int64(offset+limit) < total
timestamp := time.Now().UTC().Format(time.RFC3339)
c.JSON(http.StatusOK, gin.H{
"data": competitions,
"meta": gin.H{
"timestamp": timestamp,
"endpoint": "/competitions",
"method": "GET",
"totalItems": total,
"page": page,
"limit": limit,
"hasMore": hasMore,
},
})
}
// GetByID returns a single competition by internal ID
// @Summary Get competition by ID
// @Description Returns a single competition by its internal ID.
// @Tags Competitions
// @Param id path string true "Competition internal identifier"
// @Success 200 {object} map[string]interface{}
// @Failure 404 {object} map[string]string
// @Failure 500 {object} map[string]string
// @Router /competitions/{id} [get]
func (h *CompetitionHandler) GetByID(c *gin.Context) {
id := c.Param("id")
comp, err := h.Service.GetByID(c.Request.Context(), id)
if err != nil {
respondError(c, err)
return
}
timestamp := time.Now().UTC().Format(time.RFC3339)
endpoint := fmt.Sprintf("/competitions/%s", id)
c.JSON(http.StatusOK, gin.H{
"data": comp,
"meta": gin.H{
"timestamp": timestamp,
"endpoint": endpoint,
"method": "GET",
},
})
}
// GetByProviderID returns a single competition by wy_id (numeric) or ts_id (string)
func (h *CompetitionHandler) GetByProviderID(c *gin.Context) {
providerID := c.Param("providerId")
comp, err := h.Service.GetByAnyProviderID(c.Request.Context(), providerID)
if err != nil {
respondError(c, err)
return
}
timestamp := time.Now().UTC().Format(time.RFC3339)
endpoint := fmt.Sprintf("/competitions/provider/%s", providerID)
c.JSON(http.StatusOK, gin.H{
"data": comp,
"meta": gin.H{
"timestamp": timestamp,
"endpoint": endpoint,
"method": "GET",
},
})
}
// GetByWyID returns a single competition by WyScout WyID
// @Summary Get competition by Wyscout ID
// @Description Returns a single competition by its Wyscout wy_id identifier.
// @Tags Competitions
// @Param wyId path int true "Wyscout wy_id identifier"
// @Success 200 {object} map[string]interface{}
// @Failure 400 {object} map[string]string
// @Failure 404 {object} map[string]string
// @Failure 500 {object} map[string]string
// @Router /competitions/wyscout/{wyId} [get]
func (h *CompetitionHandler) GetByWyID(c *gin.Context) {
wyIDStr := c.Param("wyId")
wyID, err := strconv.Atoi(wyIDStr)
if err != nil {
c.JSON(http.StatusBadRequest, gin.H{"error": "invalid wyId"})
return
}
comp, err := h.Service.GetByWyID(c.Request.Context(), wyID)
if err != nil {
respondError(c, err)
return
}
timestamp := time.Now().UTC().Format(time.RFC3339)
endpoint := fmt.Sprintf("/competitions/wyscout/%d", wyID)
c.JSON(http.StatusOK, gin.H{
"data": comp,
"meta": gin.H{
"timestamp": timestamp,
"endpoint": endpoint,
"method": "GET",
},
})
}
...@@ -1776,7 +1776,13 @@ func (h *ImportHandler) ImportCoaches(c *gin.Context) { ...@@ -1776,7 +1776,13 @@ func (h *ImportHandler) ImportCoaches(c *gin.Context) {
for _, r := range payload.Results { for _, r := range payload.Results {
var coach models.Coach var coach models.Coach
if err := h.DB.Where("ts_id = ?", r.ID).First(&coach).Error; err != nil { var err error
if r.UID != nil && *r.UID != "" {
err = h.DB.Where("uid = ?", *r.UID).First(&coach).Error
} else {
err = h.DB.Where("ts_id = ?", r.ID).First(&coach).Error
}
if err != nil {
if err == gorm.ErrRecordNotFound { if err == gorm.ErrRecordNotFound {
coach = models.Coach{ coach = models.Coach{
TsID: r.ID, TsID: r.ID,
...@@ -1841,6 +1847,9 @@ func (h *ImportHandler) ImportCoaches(c *gin.Context) { ...@@ -1841,6 +1847,9 @@ func (h *ImportHandler) ImportCoaches(c *gin.Context) {
} }
// update existing coach with latest data // update existing coach with latest data
if coach.TsID != r.ID {
coach.TsID = r.ID
}
coach.ShortName = strPtrOrNil(r.ShortName) coach.ShortName = strPtrOrNil(r.ShortName)
if r.Birthday != nil { if r.Birthday != nil {
ts := time.Unix(*r.Birthday, 0).UTC() ts := time.Unix(*r.Birthday, 0).UTC()
...@@ -2195,7 +2204,13 @@ func (h *ImportHandler) ImportPlayers(c *gin.Context) { ...@@ -2195,7 +2204,13 @@ func (h *ImportHandler) ImportPlayers(c *gin.Context) {
for _, r := range payload.Results { for _, r := range payload.Results {
var player models.Player var player models.Player
if err := h.DB.Where("ts_id = ?", r.ID).First(&player).Error; err != nil { var err error
if r.UID != nil && *r.UID != "" {
err = h.DB.Where("uid = ?", *r.UID).First(&player).Error
} else {
err = h.DB.Where("ts_id = ?", r.ID).First(&player).Error
}
if err != nil {
if err == gorm.ErrRecordNotFound { if err == gorm.ErrRecordNotFound {
// new player // new player
player = models.Player{ player = models.Player{
...@@ -2271,6 +2286,9 @@ func (h *ImportHandler) ImportPlayers(c *gin.Context) { ...@@ -2271,6 +2286,9 @@ func (h *ImportHandler) ImportPlayers(c *gin.Context) {
} }
// update existing // update existing
if player.TsID != r.ID {
player.TsID = r.ID
}
player.ShortName = strPtrOrNil(r.ShortName) player.ShortName = strPtrOrNil(r.ShortName)
if r.Birthday != nil { if r.Birthday != nil {
ts := time.Unix(*r.Birthday, 0).UTC() ts := time.Unix(*r.Birthday, 0).UTC()
......
...@@ -4,6 +4,7 @@ import ( ...@@ -4,6 +4,7 @@ import (
"fmt" "fmt"
"net/http" "net/http"
"strconv" "strconv"
"strings"
"time" "time"
"github.com/gin-gonic/gin" "github.com/gin-gonic/gin"
...@@ -15,11 +16,15 @@ import ( ...@@ -15,11 +16,15 @@ import (
type PlayerHandler struct { type PlayerHandler struct {
Service services.PlayerService Service services.PlayerService
Teams services.TeamService
Areas services.AreaService
} }
func RegisterPlayerRoutes(rg *gin.RouterGroup, db *gorm.DB) { func RegisterPlayerRoutes(rg *gin.RouterGroup, db *gorm.DB) {
service := services.NewPlayerService(db) service := services.NewPlayerService(db)
h := &PlayerHandler{Service: service} teamService := services.NewTeamService(db)
areaService := services.NewAreaService(db)
h := &PlayerHandler{Service: service, Teams: teamService, Areas: areaService}
players := rg.Group("/players") players := rg.Group("/players")
players.GET("", h.List) players.GET("", h.List)
...@@ -47,6 +52,7 @@ type Meta struct { ...@@ -47,6 +52,7 @@ type Meta struct {
type StructuredPlayer struct { type StructuredPlayer struct {
ID string `json:"id"` ID string `json:"id"`
TsID string `json:"tsId"`
WyID *int `json:"wyId"` WyID *int `json:"wyId"`
GSMID *int `json:"gsmId"` GSMID *int `json:"gsmId"`
ShortName string `json:"shortName"` ShortName string `json:"shortName"`
...@@ -61,6 +67,8 @@ type StructuredPlayer struct { ...@@ -61,6 +67,8 @@ type StructuredPlayer struct {
Foot *string `json:"foot"` Foot *string `json:"foot"`
CurrentTeamID *int `json:"currentTeamId"` CurrentTeamID *int `json:"currentTeamId"`
CurrentNationalTeamID *int `json:"currentNationalTeamId"` CurrentNationalTeamID *int `json:"currentNationalTeamId"`
CountryTsID *string `json:"countryTsId"`
CountryName *string `json:"countryName"`
Gender *string `json:"gender"` Gender *string `json:"gender"`
Status string `json:"status"` Status string `json:"status"`
JerseyNumber *int `json:"jerseyNumber"` JerseyNumber *int `json:"jerseyNumber"`
...@@ -78,6 +86,16 @@ type StructuredPlayer struct { ...@@ -78,6 +86,16 @@ type StructuredPlayer struct {
UID *string `json:"uid"` UID *string `json:"uid"`
Deathday *time.Time `json:"deathday"` Deathday *time.Time `json:"deathday"`
RetireTime *time.Time `json:"retireTime"` RetireTime *time.Time `json:"retireTime"`
Team *TeamSummary `json:"team"`
}
type TeamSummary struct {
ID string `json:"id"`
TsID string `json:"tsId"`
WyID *int `json:"wyId"`
Name string `json:"name"`
ShortName *string `json:"shortName"`
ImageDataURL *string `json:"imageDataUrl"`
} }
func toStructuredPlayer(p models.Player) StructuredPlayer { func toStructuredPlayer(p models.Player) StructuredPlayer {
...@@ -107,6 +125,7 @@ func toStructuredPlayer(p models.Player) StructuredPlayer { ...@@ -107,6 +125,7 @@ func toStructuredPlayer(p models.Player) StructuredPlayer {
return StructuredPlayer{ return StructuredPlayer{
ID: p.ID, ID: p.ID,
TsID: p.TsID,
WyID: p.WyID, WyID: p.WyID,
GSMID: p.GSMID, GSMID: p.GSMID,
ShortName: valueOrDefault(p.ShortName, ""), ShortName: valueOrDefault(p.ShortName, ""),
...@@ -121,6 +140,8 @@ func toStructuredPlayer(p models.Player) StructuredPlayer { ...@@ -121,6 +140,8 @@ func toStructuredPlayer(p models.Player) StructuredPlayer {
Foot: p.Foot, Foot: p.Foot,
CurrentTeamID: p.CurrentTeamID, CurrentTeamID: p.CurrentTeamID,
CurrentNationalTeamID: p.CurrentNationalTeamID, CurrentNationalTeamID: p.CurrentNationalTeamID,
CountryTsID: p.CountryTsID,
CountryName: nil,
Gender: p.Gender, Gender: p.Gender,
Status: status, Status: status,
JerseyNumber: p.JerseyNumber, JerseyNumber: p.JerseyNumber,
...@@ -140,6 +161,51 @@ func toStructuredPlayer(p models.Player) StructuredPlayer { ...@@ -140,6 +161,51 @@ func toStructuredPlayer(p models.Player) StructuredPlayer {
} }
} }
func toTeamSummary(t models.Team) TeamSummary {
return TeamSummary{
ID: t.ID,
TsID: t.TsID,
WyID: t.WyID,
Name: t.Name,
ShortName: t.ShortName,
ImageDataURL: t.ImageDataURL,
}
}
func addPlayerTeams(structured []StructuredPlayer, players []models.Player, teamsByTsID map[string]models.Team) {
if len(structured) != len(players) {
return
}
for i := range structured {
if players[i].TeamTsID == nil || strings.TrimSpace(*players[i].TeamTsID) == "" {
continue
}
if t, ok := teamsByTsID[*players[i].TeamTsID]; ok {
ts := toTeamSummary(t)
structured[i].Team = &ts
}
}
}
func addPlayerCountries(structured []StructuredPlayer, players []models.Player, areasByTsID map[string]models.Area) {
if len(structured) != len(players) {
return
}
for i := range structured {
if players[i].CountryTsID == nil {
continue
}
tsID := strings.TrimSpace(*players[i].CountryTsID)
if tsID == "" {
continue
}
if a, ok := areasByTsID[tsID]; ok {
name := a.Name
structured[i].CountryName = &name
}
}
}
func valueOrDefault(s *string, def string) string { func valueOrDefault(s *string, def string) string {
if s != nil { if s != nil {
return *s return *s
...@@ -201,6 +267,64 @@ func (h *PlayerHandler) List(c *gin.Context) { ...@@ -201,6 +267,64 @@ func (h *PlayerHandler) List(c *gin.Context) {
structured = append(structured, toStructuredPlayer(p)) structured = append(structured, toStructuredPlayer(p))
} }
tsIDs := make([]string, 0, len(players))
seen := make(map[string]struct{}, len(players))
for _, p := range players {
if p.TeamTsID == nil {
continue
}
tsID := strings.TrimSpace(*p.TeamTsID)
if tsID == "" {
continue
}
if _, ok := seen[tsID]; ok {
continue
}
seen[tsID] = struct{}{}
tsIDs = append(tsIDs, tsID)
}
if len(tsIDs) > 0 {
teams, err := h.Teams.GetByTsIDs(c.Request.Context(), tsIDs)
if err != nil {
respondError(c, err)
return
}
teamsByTsID := make(map[string]models.Team, len(teams))
for _, t := range teams {
teamsByTsID[t.TsID] = t
}
addPlayerTeams(structured, players, teamsByTsID)
}
countryTsIDs := make([]string, 0, len(players))
countrySeen := make(map[string]struct{}, len(players))
for _, p := range players {
if p.CountryTsID == nil {
continue
}
tsID := strings.TrimSpace(*p.CountryTsID)
if tsID == "" {
continue
}
if _, ok := countrySeen[tsID]; ok {
continue
}
countrySeen[tsID] = struct{}{}
countryTsIDs = append(countryTsIDs, tsID)
}
if len(countryTsIDs) > 0 {
areas, err := h.Areas.GetByTsIDs(c.Request.Context(), countryTsIDs)
if err != nil {
respondError(c, err)
return
}
areasByTsID := make(map[string]models.Area, len(areas))
for _, a := range areas {
areasByTsID[a.TsID] = a
}
addPlayerCountries(structured, players, areasByTsID)
}
page := offset/limit + 1 page := offset/limit + 1
hasMore := int64(offset+limit) < total hasMore := int64(offset+limit) < total
timestamp := time.Now().UTC().Format(time.RFC3339) timestamp := time.Now().UTC().Format(time.RFC3339)
...@@ -237,6 +361,34 @@ func (h *PlayerHandler) GetByID(c *gin.Context) { ...@@ -237,6 +361,34 @@ func (h *PlayerHandler) GetByID(c *gin.Context) {
} }
structured := toStructuredPlayer(player) structured := toStructuredPlayer(player)
if player.CountryTsID != nil {
countryTsID := strings.TrimSpace(*player.CountryTsID)
if countryTsID != "" {
areas, err := h.Areas.GetByTsIDs(c.Request.Context(), []string{countryTsID})
if err != nil {
respondError(c, err)
return
}
if len(areas) > 0 {
name := areas[0].Name
structured.CountryName = &name
}
}
}
if player.TeamTsID != nil {
tsID := strings.TrimSpace(*player.TeamTsID)
if tsID != "" {
teams, err := h.Teams.GetByTsIDs(c.Request.Context(), []string{tsID})
if err != nil {
respondError(c, err)
return
}
if len(teams) > 0 {
ts := toTeamSummary(teams[0])
structured.Team = &ts
}
}
}
timestamp := time.Now().UTC().Format(time.RFC3339) timestamp := time.Now().UTC().Format(time.RFC3339)
endpoint := fmt.Sprintf("/players/%s", id) endpoint := fmt.Sprintf("/players/%s", id)
...@@ -261,6 +413,35 @@ func (h *PlayerHandler) GetByAnyProviderID(c *gin.Context) { ...@@ -261,6 +413,35 @@ func (h *PlayerHandler) GetByAnyProviderID(c *gin.Context) {
} }
structured := toStructuredPlayer(player) structured := toStructuredPlayer(player)
if player.CountryTsID != nil {
countryTsID := strings.TrimSpace(*player.CountryTsID)
if countryTsID != "" {
areas, err := h.Areas.GetByTsIDs(c.Request.Context(), []string{countryTsID})
if err != nil {
respondError(c, err)
return
}
if len(areas) > 0 {
name := areas[0].Name
structured.CountryName = &name
}
}
}
if player.TeamTsID != nil {
tsID := strings.TrimSpace(*player.TeamTsID)
if tsID != "" {
teams, err := h.Teams.GetByTsIDs(c.Request.Context(), []string{tsID})
if err != nil {
respondError(c, err)
return
}
if len(teams) > 0 {
ts := toTeamSummary(teams[0])
structured.Team = &ts
}
}
}
timestamp := time.Now().UTC().Format(time.RFC3339) timestamp := time.Now().UTC().Format(time.RFC3339)
endpoint := fmt.Sprintf("/players/provider/%s", providerID) endpoint := fmt.Sprintf("/players/provider/%s", providerID)
...@@ -298,6 +479,34 @@ func (h *PlayerHandler) GetByProviderID(c *gin.Context) { ...@@ -298,6 +479,34 @@ func (h *PlayerHandler) GetByProviderID(c *gin.Context) {
} }
structured := toStructuredPlayer(player) structured := toStructuredPlayer(player)
if player.CountryTsID != nil {
countryTsID := strings.TrimSpace(*player.CountryTsID)
if countryTsID != "" {
areas, err := h.Areas.GetByTsIDs(c.Request.Context(), []string{countryTsID})
if err != nil {
respondError(c, err)
return
}
if len(areas) > 0 {
name := areas[0].Name
structured.CountryName = &name
}
}
}
if player.TeamTsID != nil {
tsID := strings.TrimSpace(*player.TeamTsID)
if tsID != "" {
teams, err := h.Teams.GetByTsIDs(c.Request.Context(), []string{tsID})
if err != nil {
respondError(c, err)
return
}
if len(teams) > 0 {
ts := toTeamSummary(teams[0])
structured.Team = &ts
}
}
}
timestamp := time.Now().UTC().Format(time.RFC3339) timestamp := time.Now().UTC().Format(time.RFC3339)
endpoint := fmt.Sprintf("/players/wyscout/%d", wyID) endpoint := fmt.Sprintf("/players/wyscout/%d", wyID)
......
package handlers
import (
"fmt"
"net/http"
"strconv"
"time"
"github.com/gin-gonic/gin"
"gorm.io/gorm"
"ScoutingSystemScoreData/internal/services"
)
type SeasonHandler struct {
Service services.SeasonService
}
func RegisterSeasonRoutes(rg *gin.RouterGroup, db *gorm.DB) {
service := services.NewSeasonService(db)
h := &SeasonHandler{Service: service}
seasons := rg.Group("/seasons")
seasons.GET("", h.List)
seasons.GET("/wyscout/:wyId", h.GetByWyID)
seasons.GET("/provider/:providerId", h.GetByProviderID)
seasons.GET("/:id", h.GetByID)
}
// List seasons
// @Summary List seasons
// @Description Returns a list of seasons with optional pagination.
// @Tags Seasons
// @Param limit query int false "Maximum number of items to return (default 100)"
// @Param offset query int false "Number of items to skip before starting to collect the result set (default 0)"
// @Success 200 {object} map[string]interface{}
// @Failure 500 {object} map[string]string
// @Router /seasons [get]
func (h *SeasonHandler) List(c *gin.Context) {
limitStr := c.DefaultQuery("limit", "100")
offsetStr := c.DefaultQuery("offset", "0")
limit, err := strconv.Atoi(limitStr)
if err != nil || limit <= 0 {
limit = 100
}
offset, err := strconv.Atoi(offsetStr)
if err != nil || offset < 0 {
offset = 0
}
seasons, total, err := h.Service.ListSeasons(c.Request.Context(), limit, offset)
if err != nil {
respondError(c, err)
return
}
page := offset/limit + 1
hasMore := int64(offset+limit) < total
timestamp := time.Now().UTC().Format(time.RFC3339)
c.JSON(http.StatusOK, gin.H{
"data": seasons,
"meta": gin.H{
"timestamp": timestamp,
"endpoint": "/seasons",
"method": "GET",
"totalItems": total,
"page": page,
"limit": limit,
"hasMore": hasMore,
},
})
}
// GetByID returns a single season by internal ID
// @Summary Get season by ID
// @Description Returns a single season by its internal ID.
// @Tags Seasons
// @Param id path string true "Season internal identifier"
// @Success 200 {object} map[string]interface{}
// @Failure 404 {object} map[string]string
// @Failure 500 {object} map[string]string
// @Router /seasons/{id} [get]
func (h *SeasonHandler) GetByID(c *gin.Context) {
id := c.Param("id")
season, err := h.Service.GetByID(c.Request.Context(), id)
if err != nil {
respondError(c, err)
return
}
timestamp := time.Now().UTC().Format(time.RFC3339)
endpoint := fmt.Sprintf("/seasons/%s", id)
c.JSON(http.StatusOK, gin.H{
"data": season,
"meta": gin.H{
"timestamp": timestamp,
"endpoint": endpoint,
"method": "GET",
},
})
}
// GetByProviderID returns a single season by wy_id (numeric) or ts_id (string)
func (h *SeasonHandler) GetByProviderID(c *gin.Context) {
providerID := c.Param("providerId")
season, err := h.Service.GetByAnyProviderID(c.Request.Context(), providerID)
if err != nil {
respondError(c, err)
return
}
timestamp := time.Now().UTC().Format(time.RFC3339)
endpoint := fmt.Sprintf("/seasons/provider/%s", providerID)
c.JSON(http.StatusOK, gin.H{
"data": season,
"meta": gin.H{
"timestamp": timestamp,
"endpoint": endpoint,
"method": "GET",
},
})
}
// GetByWyID returns a single season by WyScout WyID
// @Summary Get season by Wyscout ID
// @Description Returns a single season by its Wyscout wy_id identifier.
// @Tags Seasons
// @Param wyId path int true "Wyscout wy_id identifier"
// @Success 200 {object} map[string]interface{}
// @Failure 400 {object} map[string]string
// @Failure 404 {object} map[string]string
// @Failure 500 {object} map[string]string
// @Router /seasons/wyscout/{wyId} [get]
func (h *SeasonHandler) GetByWyID(c *gin.Context) {
wyIDStr := c.Param("wyId")
wyID, err := strconv.Atoi(wyIDStr)
if err != nil {
c.JSON(http.StatusBadRequest, gin.H{"error": "invalid wyId"})
return
}
season, err := h.Service.GetByWyID(c.Request.Context(), wyID)
if err != nil {
respondError(c, err)
return
}
timestamp := time.Now().UTC().Format(time.RFC3339)
endpoint := fmt.Sprintf("/seasons/wyscout/%d", wyID)
c.JSON(http.StatusOK, gin.H{
"data": season,
"meta": gin.H{
"timestamp": timestamp,
"endpoint": endpoint,
"method": "GET",
},
})
}
...@@ -9,9 +9,20 @@ import ( ...@@ -9,9 +9,20 @@ import (
"github.com/gin-gonic/gin" "github.com/gin-gonic/gin"
"gorm.io/gorm" "gorm.io/gorm"
"ScoutingSystemScoreData/internal/models"
"ScoutingSystemScoreData/internal/services" "ScoutingSystemScoreData/internal/services"
) )
type TeamListResponse struct {
Data []models.Team `json:"data"`
Meta map[string]interface{} `json:"meta"`
}
type TeamResponse struct {
Data models.Team `json:"data"`
Meta map[string]interface{} `json:"meta"`
}
type TeamHandler struct { type TeamHandler struct {
Service services.TeamService Service services.TeamService
} }
...@@ -33,7 +44,7 @@ func RegisterTeamRoutes(rg *gin.RouterGroup, db *gorm.DB) { ...@@ -33,7 +44,7 @@ func RegisterTeamRoutes(rg *gin.RouterGroup, db *gorm.DB) {
// @Tags Teams // @Tags Teams
// @Param limit query int false "Maximum number of items to return (default 100)" // @Param limit query int false "Maximum number of items to return (default 100)"
// @Param offset query int false "Number of items to skip before starting to collect the result set (default 0)" // @Param offset query int false "Number of items to skip before starting to collect the result set (default 0)"
// @Success 200 {object} map[string]interface{} // @Success 200 {object} handlers.TeamListResponse
// @Failure 500 {object} map[string]string // @Failure 500 {object} map[string]string
// @Router /teams [get] // @Router /teams [get]
func (h *TeamHandler) List(c *gin.Context) { func (h *TeamHandler) List(c *gin.Context) {
...@@ -49,7 +60,14 @@ func (h *TeamHandler) List(c *gin.Context) { ...@@ -49,7 +60,14 @@ func (h *TeamHandler) List(c *gin.Context) {
offset = 0 offset = 0
} }
teams, total, err := h.Service.ListTeams(c.Request.Context(), limit, offset) name := c.Query("name")
endpoint := "/teams"
if name != "" {
endpoint = fmt.Sprintf("/teams?name=%s", name)
}
teams, total, err := h.Service.ListTeams(c.Request.Context(), limit, offset, name)
if err != nil { if err != nil {
respondError(c, err) respondError(c, err)
return return
...@@ -63,7 +81,7 @@ func (h *TeamHandler) List(c *gin.Context) { ...@@ -63,7 +81,7 @@ func (h *TeamHandler) List(c *gin.Context) {
"data": teams, "data": teams,
"meta": gin.H{ "meta": gin.H{
"timestamp": timestamp, "timestamp": timestamp,
"endpoint": "/teams", "endpoint": endpoint,
"method": "GET", "method": "GET",
"totalItems": total, "totalItems": total,
"page": page, "page": page,
...@@ -78,7 +96,7 @@ func (h *TeamHandler) List(c *gin.Context) { ...@@ -78,7 +96,7 @@ func (h *TeamHandler) List(c *gin.Context) {
// @Description Returns a single team by its internal ID. // @Description Returns a single team by its internal ID.
// @Tags Teams // @Tags Teams
// @Param id path string true "Team internal identifier" // @Param id path string true "Team internal identifier"
// @Success 200 {object} map[string]interface{} // @Success 200 {object} handlers.TeamResponse
// @Failure 404 {object} map[string]string // @Failure 404 {object} map[string]string
// @Failure 500 {object} map[string]string // @Failure 500 {object} map[string]string
// @Router /teams/{id} [get] // @Router /teams/{id} [get]
...@@ -132,7 +150,7 @@ func (h *TeamHandler) GetByProviderID(c *gin.Context) { ...@@ -132,7 +150,7 @@ func (h *TeamHandler) GetByProviderID(c *gin.Context) {
// @Description Returns a single team by its Wyscout wy_id identifier. // @Description Returns a single team by its Wyscout wy_id identifier.
// @Tags Teams // @Tags Teams
// @Param wyId path int true "Wyscout wy_id identifier" // @Param wyId path int true "Wyscout wy_id identifier"
// @Success 200 {object} map[string]interface{} // @Success 200 {object} handlers.TeamResponse
// @Failure 400 {object} map[string]string // @Failure 400 {object} map[string]string
// @Failure 404 {object} map[string]string // @Failure 404 {object} map[string]string
// @Failure 500 {object} map[string]string // @Failure 500 {object} map[string]string
......
package models package models
import ( import (
"encoding/json"
"time" "time"
gonanoid "github.com/matoous/go-nanoid/v2" gonanoid "github.com/matoous/go-nanoid/v2"
...@@ -109,13 +110,23 @@ type Team struct { ...@@ -109,13 +110,23 @@ type Team struct {
Type string `gorm:"column:type;type:team_type;default:club" json:"type"` Type string `gorm:"column:type;type:team_type;default:club" json:"type"`
Category string `gorm:"column:category;default:default" json:"category"` Category string `gorm:"column:category;default:default" json:"category"`
Gender *string `gorm:"column:gender;type:gender" json:"gender"` Gender *string `gorm:"column:gender;type:gender" json:"gender"`
CountryTsID *string `gorm:"column:country_ts_id;size:64" json:"countryTsId"`
CompetitionTsID *string `gorm:"column:competition_ts_id;size:64" json:"competitionTsId"`
AreaWyID *int `gorm:"column:area_wy_id" json:"areaWyId"` AreaWyID *int `gorm:"column:area_wy_id" json:"areaWyId"`
City *string `gorm:"column:city" json:"city"` City *string `gorm:"column:city" json:"city"`
CoachWyID *int `gorm:"column:coach_wy_id" json:"coachWyId"` CoachWyID *int `gorm:"column:coach_wy_id" json:"coachWyId"`
CoachTsID *string `gorm:"column:coach_ts_id;size:64" json:"coachTsId"`
CompetitionWyID *int `gorm:"column:competition_wy_id" json:"competitionWyId"` CompetitionWyID *int `gorm:"column:competition_wy_id" json:"competitionWyId"`
SeasonWyID *int `gorm:"column:season_wy_id" json:"seasonWyId"` SeasonWyID *int `gorm:"column:season_wy_id" json:"seasonWyId"`
League *string `gorm:"column:league" json:"league"` League *string `gorm:"column:league" json:"league"`
Season *string `gorm:"column:season" json:"season"` Season *string `gorm:"column:season" json:"season"`
VenueTsID *string `gorm:"column:venue_ts_id;size:64" json:"venueTsId"`
Website *string `gorm:"column:website" json:"website"`
MarketValue *int `gorm:"column:market_value" json:"marketValue"`
MarketValueCurrency *string `gorm:"column:market_value_currency" json:"marketValueCurrency"`
TotalPlayers *int `gorm:"column:total_players" json:"totalPlayers"`
ForeignPlayers *int `gorm:"column:foreign_players" json:"foreignPlayers"`
NationalPlayers *int `gorm:"column:national_players" json:"nationalPlayers"`
ImageDataURL *string `gorm:"column:image_data_url" json:"imageDataUrl"` ImageDataURL *string `gorm:"column:image_data_url" json:"imageDataUrl"`
Status string `gorm:"column:status;type:status;default:active" json:"status"` Status string `gorm:"column:status;type:status;default:active" json:"status"`
IsActive bool `gorm:"column:is_active;default:true" json:"isActive"` IsActive bool `gorm:"column:is_active;default:true" json:"isActive"`
...@@ -158,6 +169,8 @@ type Coach struct { ...@@ -158,6 +169,8 @@ type Coach struct {
ID string `gorm:"primaryKey;size:16" json:"id"` ID string `gorm:"primaryKey;size:16" json:"id"`
WyID *int `gorm:"column:wy_id;uniqueIndex" json:"wyId"` WyID *int `gorm:"column:wy_id;uniqueIndex" json:"wyId"`
TsID string `gorm:"column:ts_id;size:64" json:"tsId"` TsID string `gorm:"column:ts_id;size:64" json:"tsId"`
CountryTsID *string `gorm:"column:country_ts_id;size:64" json:"countryTsId"`
TeamTsID *string `gorm:"column:team_ts_id;size:64" json:"teamTsId"`
FirstName string `gorm:"column:first_name" json:"firstName"` FirstName string `gorm:"column:first_name" json:"firstName"`
LastName string `gorm:"column:last_name" json:"lastName"` LastName string `gorm:"column:last_name" json:"lastName"`
MiddleName *string `gorm:"column:middle_name" json:"middleName"` MiddleName *string `gorm:"column:middle_name" json:"middleName"`
...@@ -227,6 +240,7 @@ type Player struct { ...@@ -227,6 +240,7 @@ type Player struct {
TsID string `gorm:"column:ts_id;size:64" json:"tsId"` TsID string `gorm:"column:ts_id;size:64" json:"tsId"`
TeamWyID *int `gorm:"column:team_wy_id" json:"teamWyId"` TeamWyID *int `gorm:"column:team_wy_id" json:"teamWyId"`
TeamTsID *string `gorm:"column:team_ts_id;size:64" json:"teamTsId"` TeamTsID *string `gorm:"column:team_ts_id;size:64" json:"teamTsId"`
CountryTsID *string `gorm:"column:country_ts_id;size:64" json:"countryTsId"`
FirstName string `gorm:"column:first_name" json:"firstName"` FirstName string `gorm:"column:first_name" json:"firstName"`
LastName string `gorm:"column:last_name" json:"lastName"` LastName string `gorm:"column:last_name" json:"lastName"`
MiddleName *string `gorm:"column:middle_name" json:"middleName"` MiddleName *string `gorm:"column:middle_name" json:"middleName"`
...@@ -282,6 +296,8 @@ type Match struct { ...@@ -282,6 +296,8 @@ type Match struct {
ID string `gorm:"primaryKey;size:16" json:"id"` ID string `gorm:"primaryKey;size:16" json:"id"`
WyID *int `gorm:"column:wy_id;uniqueIndex" json:"wyId"` WyID *int `gorm:"column:wy_id;uniqueIndex" json:"wyId"`
TsID string `gorm:"column:ts_id;size:64;uniqueIndex" json:"tsId"` TsID string `gorm:"column:ts_id;size:64;uniqueIndex" json:"tsId"`
SeasonTsID *string `gorm:"column:season_ts_id;size:64" json:"seasonTsId"`
CompetitionTsID *string `gorm:"column:competition_ts_id;size:64" json:"competitionTsId"`
HomeTeamWyID *int `gorm:"column:home_team_wy_id" json:"homeTeamWyId"` HomeTeamWyID *int `gorm:"column:home_team_wy_id" json:"homeTeamWyId"`
HomeTeamTsID *string `gorm:"column:home_team_ts_id;size:64" json:"homeTeamTsId"` HomeTeamTsID *string `gorm:"column:home_team_ts_id;size:64" json:"homeTeamTsId"`
AwayTeamWyID *int `gorm:"column:away_team_wy_id" json:"awayTeamWyId"` AwayTeamWyID *int `gorm:"column:away_team_wy_id" json:"awayTeamWyId"`
...@@ -290,6 +306,11 @@ type Match struct { ...@@ -290,6 +306,11 @@ type Match struct {
SeasonWyID *int `gorm:"column:season_wy_id" json:"seasonWyId"` SeasonWyID *int `gorm:"column:season_wy_id" json:"seasonWyId"`
RoundWyID *int `gorm:"column:round_wy_id" json:"roundWyId"` RoundWyID *int `gorm:"column:round_wy_id" json:"roundWyId"`
MatchDate time.Time `gorm:"column:match_date" json:"matchDate"` MatchDate time.Time `gorm:"column:match_date" json:"matchDate"`
MatchTimeUnix *int64 `gorm:"column:match_time_unix" json:"matchTimeUnix"`
StatusID *int `gorm:"column:status_id" json:"statusId"`
VenueTsID *string `gorm:"column:venue_ts_id;size:64" json:"venueTsId"`
RefereeTsID *string `gorm:"column:referee_ts_id;size:64" json:"refereeTsId"`
Neutral *bool `gorm:"column:neutral" json:"neutral"`
Venue *string `gorm:"column:venue" json:"venue"` Venue *string `gorm:"column:venue" json:"venue"`
VenueCity *string `gorm:"column:venue_city" json:"venueCity"` VenueCity *string `gorm:"column:venue_city" json:"venueCity"`
VenueCountry *string `gorm:"column:venue_country" json:"venueCountry"` VenueCountry *string `gorm:"column:venue_country" json:"venueCountry"`
...@@ -299,6 +320,17 @@ type Match struct { ...@@ -299,6 +320,17 @@ type Match struct {
AwayScore int `gorm:"column:away_score;default:0" json:"awayScore"` AwayScore int `gorm:"column:away_score;default:0" json:"awayScore"`
HomeScorePenalties int `gorm:"column:home_score_penalties;default:0" json:"homeScorePenalties"` HomeScorePenalties int `gorm:"column:home_score_penalties;default:0" json:"homeScorePenalties"`
AwayScorePenalties int `gorm:"column:away_score_penalties;default:0" json:"awayScorePenalties"` AwayScorePenalties int `gorm:"column:away_score_penalties;default:0" json:"awayScorePenalties"`
HomeScoresJSON json.RawMessage `gorm:"column:home_scores_json;type:jsonb" json:"homeScores" swaggertype:"object"`
AwayScoresJSON json.RawMessage `gorm:"column:away_scores_json;type:jsonb" json:"awayScores" swaggertype:"object"`
HomePosition *string `gorm:"column:home_position" json:"homePosition"`
AwayPosition *string `gorm:"column:away_position" json:"awayPosition"`
CoverageMLive *bool `gorm:"column:coverage_mlive" json:"coverageMlive"`
CoverageLineup *bool `gorm:"column:coverage_lineup" json:"coverageLineup"`
RoundStageTsID *string `gorm:"column:round_stage_ts_id;size:64" json:"roundStageTsId"`
RoundGroupNum *int `gorm:"column:round_group_num" json:"roundGroupNum"`
RoundNum *int `gorm:"column:round_num" json:"roundNum"`
RelatedTsID *string `gorm:"column:related_ts_id;size:64" json:"relatedTsId"`
AggScoreJSON json.RawMessage `gorm:"column:agg_score_json;type:jsonb" json:"aggScore" swaggertype:"object"`
Attendance *int `gorm:"column:attendance" json:"attendance"` Attendance *int `gorm:"column:attendance" json:"attendance"`
MainRefereeWyID *int `gorm:"column:main_referee_wy_id" json:"mainRefereeWyId"` MainRefereeWyID *int `gorm:"column:main_referee_wy_id" json:"mainRefereeWyId"`
AssistantReferee1WyID *int `gorm:"column:assistant_referee_1_wy_id" json:"assistantReferee1WyId"` AssistantReferee1WyID *int `gorm:"column:assistant_referee_1_wy_id" json:"assistantReferee1WyId"`
...@@ -307,6 +339,15 @@ type Match struct { ...@@ -307,6 +339,15 @@ type Match struct {
VarRefereeWyID *int `gorm:"column:var_referee_wy_id" json:"varRefereeWyId"` VarRefereeWyID *int `gorm:"column:var_referee_wy_id" json:"varRefereeWyId"`
Weather *string `gorm:"column:weather" json:"weather"` Weather *string `gorm:"column:weather" json:"weather"`
Temperature *float64 `gorm:"column:temperature" json:"temperature"` Temperature *float64 `gorm:"column:temperature" json:"temperature"`
Pressure *string `gorm:"column:pressure" json:"pressure"`
Wind *string `gorm:"column:wind" json:"wind"`
Humidity *string `gorm:"column:humidity" json:"humidity"`
TBD *bool `gorm:"column:tbd" json:"tbd"`
HasOT *bool `gorm:"column:has_ot" json:"hasOt"`
EndedUnix *int64 `gorm:"column:ended_unix" json:"endedUnix"`
TeamReverse *bool `gorm:"column:team_reverse" json:"teamReverse"`
Loss *bool `gorm:"column:loss" json:"loss"`
ProviderUpdatedAtUnix *int64 `gorm:"column:provider_updated_at_unix" json:"providerUpdatedAtUnix"`
APILastSyncedAt *time.Time `gorm:"column:api_last_synced_at" json:"apiLastSyncedAt"` APILastSyncedAt *time.Time `gorm:"column:api_last_synced_at" json:"apiLastSyncedAt"`
APISyncStatus string `gorm:"column:api_sync_status;type:api_sync_status;default:pending" json:"apiSyncStatus"` APISyncStatus string `gorm:"column:api_sync_status;type:api_sync_status;default:pending" json:"apiSyncStatus"`
Notes *string `gorm:"column:notes" json:"notes"` Notes *string `gorm:"column:notes" json:"notes"`
......
...@@ -6,6 +6,7 @@ import ( ...@@ -6,6 +6,7 @@ import (
"os" "os"
"strings" "strings"
"github.com/gin-contrib/cors"
"github.com/gin-gonic/gin" "github.com/gin-gonic/gin"
"gorm.io/gorm" "gorm.io/gorm"
...@@ -84,6 +85,21 @@ func basicAuthMiddleware() gin.HandlerFunc { ...@@ -84,6 +85,21 @@ func basicAuthMiddleware() gin.HandlerFunc {
func New(db *gorm.DB) *gin.Engine { func New(db *gorm.DB) *gin.Engine {
r := gin.Default() r := gin.Default()
r.Use(cors.New(cors.Config{
AllowAllOrigins: true,
AllowMethods: []string{
http.MethodGet,
http.MethodPost,
http.MethodPut,
http.MethodPatch,
http.MethodDelete,
http.MethodOptions,
},
AllowHeaders: []string{"*"},
ExposeHeaders: []string{"*"},
AllowCredentials: false,
}))
r.GET("/health", func(c *gin.Context) { r.GET("/health", func(c *gin.Context) {
if sqlDB, err := db.DB(); err == nil { if sqlDB, err := db.DB(); err == nil {
if err := sqlDB.Ping(); err != nil { if err := sqlDB.Ping(); err != nil {
...@@ -106,6 +122,8 @@ func New(db *gorm.DB) *gin.Engine { ...@@ -106,6 +122,8 @@ func New(db *gorm.DB) *gin.Engine {
api.Use(basicAuthMiddleware()) api.Use(basicAuthMiddleware())
appCfg := config.Load() appCfg := config.Load()
handlers.RegisterAreaRoutes(api, db) handlers.RegisterAreaRoutes(api, db)
handlers.RegisterCompetitionRoutes(api, db)
handlers.RegisterSeasonRoutes(api, db)
handlers.RegisterTeamRoutes(api, db) handlers.RegisterTeamRoutes(api, db)
handlers.RegisterPlayerRoutes(api, db) handlers.RegisterPlayerRoutes(api, db)
handlers.RegisterCoachRoutes(api, db) handlers.RegisterCoachRoutes(api, db)
......
...@@ -22,6 +22,7 @@ type AreaService interface { ...@@ -22,6 +22,7 @@ type AreaService interface {
ListAreas(ctx context.Context, opts ListAreasOptions) ([]models.Area, int64, error) ListAreas(ctx context.Context, opts ListAreasOptions) ([]models.Area, int64, error)
GetByID(ctx context.Context, id string) (models.Area, error) GetByID(ctx context.Context, id string) (models.Area, error)
GetByProviderID(ctx context.Context, providerID string) (models.Area, error) GetByProviderID(ctx context.Context, providerID string) (models.Area, error)
GetByTsIDs(ctx context.Context, tsIDs []string) ([]models.Area, error)
} }
type areaService struct { type areaService struct {
...@@ -88,3 +89,16 @@ func (s *areaService) GetByProviderID(ctx context.Context, providerID string) (m ...@@ -88,3 +89,16 @@ func (s *areaService) GetByProviderID(ctx context.Context, providerID string) (m
} }
return area, nil return area, nil
} }
func (s *areaService) GetByTsIDs(ctx context.Context, tsIDs []string) ([]models.Area, error) {
if len(tsIDs) == 0 {
return []models.Area{}, nil
}
var areas []models.Area
if err := s.db.WithContext(ctx).Where("ts_id IN ?", tsIDs).Find(&areas).Error; err != nil {
return nil, errors.Wrap(errors.CodeInternal, "failed to fetch areas by ts_id", err)
}
return areas, nil
}
package services
import (
"context"
"strconv"
"gorm.io/gorm"
"ScoutingSystemScoreData/internal/errors"
"ScoutingSystemScoreData/internal/models"
)
type CompetitionService interface {
ListCompetitions(ctx context.Context, limit, offset int) ([]models.Competition, int64, error)
GetByID(ctx context.Context, id string) (models.Competition, error)
GetByWyID(ctx context.Context, wyID int) (models.Competition, error)
GetByAnyProviderID(ctx context.Context, providerID string) (models.Competition, error)
}
type competitionService struct {
db *gorm.DB
}
func NewCompetitionService(db *gorm.DB) CompetitionService {
return &competitionService{db: db}
}
func (s *competitionService) ListCompetitions(ctx context.Context, limit, offset int) ([]models.Competition, int64, error) {
var competitions []models.Competition
query := s.db.WithContext(ctx).Model(&models.Competition{})
var total int64
if err := query.Count(&total).Error; err != nil {
return nil, 0, errors.Wrap(errors.CodeInternal, "failed to count competitions", err)
}
if err := query.Limit(limit).Offset(offset).Find(&competitions).Error; err != nil {
return nil, 0, errors.Wrap(errors.CodeInternal, "failed to fetch competitions", err)
}
return competitions, total, nil
}
func (s *competitionService) GetByID(ctx context.Context, id string) (models.Competition, error) {
var comp models.Competition
if err := s.db.WithContext(ctx).First(&comp, "id = ?", id).Error; err != nil {
if err == gorm.ErrRecordNotFound {
return models.Competition{}, errors.New(errors.CodeNotFound, "competition not found")
}
return models.Competition{}, errors.Wrap(errors.CodeInternal, "failed to fetch competition", err)
}
return comp, nil
}
func (s *competitionService) GetByAnyProviderID(ctx context.Context, providerID string) (models.Competition, error) {
// If providerID is numeric, treat as wy_id; otherwise, treat as ts_id
if wyID, err := strconv.Atoi(providerID); err == nil {
return s.GetByWyID(ctx, wyID)
}
var comp models.Competition
if err := s.db.WithContext(ctx).First(&comp, "ts_id = ?", providerID).Error; err != nil {
if err == gorm.ErrRecordNotFound {
return models.Competition{}, errors.New(errors.CodeNotFound, "competition not found")
}
return models.Competition{}, errors.Wrap(errors.CodeInternal, "failed to fetch competition", err)
}
return comp, nil
}
func (s *competitionService) GetByWyID(ctx context.Context, wyID int) (models.Competition, error) {
var comp models.Competition
if err := s.db.WithContext(ctx).First(&comp, "wy_id = ?", wyID).Error; err != nil {
if err == gorm.ErrRecordNotFound {
return models.Competition{}, errors.New(errors.CodeNotFound, "competition not found")
}
return models.Competition{}, errors.Wrap(errors.CodeInternal, "failed to fetch competition", err)
}
return comp, nil
}
...@@ -2,6 +2,7 @@ package services ...@@ -2,6 +2,7 @@ package services
import ( import (
"context" "context"
"fmt"
"time" "time"
"gorm.io/gorm" "gorm.io/gorm"
...@@ -14,7 +15,7 @@ type MatchService interface { ...@@ -14,7 +15,7 @@ type MatchService interface {
ListMatches(ctx context.Context, opts ListMatchesOptions) ([]models.Match, int64, error) ListMatches(ctx context.Context, opts ListMatchesOptions) ([]models.Match, int64, error)
GetByID(ctx context.Context, id string) (models.Match, error) GetByID(ctx context.Context, id string) (models.Match, error)
GetByTsID(ctx context.Context, matchTsID string) (models.Match, error) GetByTsID(ctx context.Context, matchTsID string) (models.Match, error)
GetHeadToHeadMostRecent(ctx context.Context, teamATsID string, teamBTsID string) (models.Match, error) GetHeadToHeadMostRecent(ctx context.Context, teamHomeID string, teamAwayID string, from *time.Time, to *time.Time) (models.Match, error)
GetLineup(ctx context.Context, matchTsID string) (models.Match, []models.MatchTeam, []models.MatchLineupPlayer, error) GetLineup(ctx context.Context, matchTsID string) (models.Match, []models.MatchTeam, []models.MatchLineupPlayer, error)
GetLineupByID(ctx context.Context, id string) (models.Match, []models.MatchTeam, []models.MatchLineupPlayer, error) GetLineupByID(ctx context.Context, id string) (models.Match, []models.MatchTeam, []models.MatchLineupPlayer, error)
} }
...@@ -102,15 +103,43 @@ func (s *matchService) GetByID(ctx context.Context, id string) (models.Match, er ...@@ -102,15 +103,43 @@ func (s *matchService) GetByID(ctx context.Context, id string) (models.Match, er
return match, nil return match, nil
} }
func (s *matchService) GetHeadToHeadMostRecent(ctx context.Context, teamATsID string, teamBTsID string) (models.Match, error) { func (s *matchService) GetHeadToHeadMostRecent(ctx context.Context, teamHomeID string, teamAwayID string, from *time.Time, to *time.Time) (models.Match, error) {
var match models.Match var match models.Match
var homeTeam models.Team
if err := s.db.WithContext(ctx).First(&homeTeam, "id = ?", teamHomeID).Error; err != nil {
if err == gorm.ErrRecordNotFound {
return models.Match{}, errors.New(errors.CodeNotFound, "home team not found")
}
return models.Match{}, errors.Wrap(errors.CodeInternal, "failed to fetch home team", err)
}
var awayTeam models.Team
if err := s.db.WithContext(ctx).First(&awayTeam, "id = ?", teamAwayID).Error; err != nil {
if err == gorm.ErrRecordNotFound {
return models.Match{}, errors.New(errors.CodeNotFound, "away team not found")
}
return models.Match{}, errors.Wrap(errors.CodeInternal, "failed to fetch away team", err)
}
if homeTeam.TsID == "" || awayTeam.TsID == "" {
return models.Match{}, errors.New(errors.CodeNotFound, "team ts_id not found")
}
q := s.db.WithContext(ctx).Model(&models.Match{}). q := s.db.WithContext(ctx).Model(&models.Match{}).
Where("(home_team_ts_id = ? AND away_team_ts_id = ?) OR (home_team_ts_id = ? AND away_team_ts_id = ?)", teamATsID, teamBTsID, teamBTsID, teamATsID). Where("home_team_ts_id = ? AND away_team_ts_id = ?", homeTeam.TsID, awayTeam.TsID).
Order("match_date desc") Order("match_date desc")
if from != nil {
q = q.Where("match_date >= ?", *from)
}
if to != nil {
q = q.Where("match_date <= ?", *to)
}
if err := q.First(&match).Error; err != nil { if err := q.First(&match).Error; err != nil {
if err == gorm.ErrRecordNotFound { if err == gorm.ErrRecordNotFound {
return models.Match{}, errors.New(errors.CodeNotFound, "match not found") return models.Match{}, errors.New(errors.CodeNotFound, fmt.Sprintf("match not found for home=%s away=%s", teamHomeID, teamAwayID))
} }
return models.Match{}, errors.Wrap(errors.CodeInternal, "failed to fetch head-to-head match", err) return models.Match{}, errors.Wrap(errors.CodeInternal, "failed to fetch head-to-head match", err)
} }
......
...@@ -81,7 +81,13 @@ func (s *playerService) GetByAnyProviderID(ctx context.Context, providerID strin ...@@ -81,7 +81,13 @@ func (s *playerService) GetByAnyProviderID(ctx context.Context, providerID strin
var player models.Player var player models.Player
if err := s.db.WithContext(ctx).First(&player, "ts_id = ?", providerID).Error; err != nil { if err := s.db.WithContext(ctx).First(&player, "ts_id = ?", providerID).Error; err != nil {
if err == gorm.ErrRecordNotFound { if err == gorm.ErrRecordNotFound {
return models.Player{}, errors.New(errors.CodeNotFound, "player not found") if err := s.db.WithContext(ctx).First(&player, "uid = ?", providerID).Error; err != nil {
if err == gorm.ErrRecordNotFound {
return models.Player{}, errors.New(errors.CodeNotFound, "player not found")
}
return models.Player{}, errors.Wrap(errors.CodeInternal, "failed to fetch player", err)
}
return player, nil
} }
return models.Player{}, errors.Wrap(errors.CodeInternal, "failed to fetch player", err) return models.Player{}, errors.Wrap(errors.CodeInternal, "failed to fetch player", err)
} }
......
package services
import (
"context"
"strconv"
"gorm.io/gorm"
"ScoutingSystemScoreData/internal/errors"
"ScoutingSystemScoreData/internal/models"
)
type SeasonService interface {
ListSeasons(ctx context.Context, limit, offset int) ([]models.Season, int64, error)
GetByID(ctx context.Context, id string) (models.Season, error)
GetByWyID(ctx context.Context, wyID int) (models.Season, error)
GetByAnyProviderID(ctx context.Context, providerID string) (models.Season, error)
}
type seasonService struct {
db *gorm.DB
}
func NewSeasonService(db *gorm.DB) SeasonService {
return &seasonService{db: db}
}
func (s *seasonService) ListSeasons(ctx context.Context, limit, offset int) ([]models.Season, int64, error) {
var seasons []models.Season
query := s.db.WithContext(ctx).Model(&models.Season{})
var total int64
if err := query.Count(&total).Error; err != nil {
return nil, 0, errors.Wrap(errors.CodeInternal, "failed to count seasons", err)
}
if err := query.Limit(limit).Offset(offset).Find(&seasons).Error; err != nil {
return nil, 0, errors.Wrap(errors.CodeInternal, "failed to fetch seasons", err)
}
return seasons, total, nil
}
func (s *seasonService) GetByID(ctx context.Context, id string) (models.Season, error) {
var season models.Season
if err := s.db.WithContext(ctx).First(&season, "id = ?", id).Error; err != nil {
if err == gorm.ErrRecordNotFound {
return models.Season{}, errors.New(errors.CodeNotFound, "season not found")
}
return models.Season{}, errors.Wrap(errors.CodeInternal, "failed to fetch season", err)
}
return season, nil
}
func (s *seasonService) GetByAnyProviderID(ctx context.Context, providerID string) (models.Season, error) {
// If providerID is numeric, treat as wy_id; otherwise, treat as ts_id
if wyID, err := strconv.Atoi(providerID); err == nil {
return s.GetByWyID(ctx, wyID)
}
var season models.Season
if err := s.db.WithContext(ctx).First(&season, "ts_id = ?", providerID).Error; err != nil {
if err == gorm.ErrRecordNotFound {
return models.Season{}, errors.New(errors.CodeNotFound, "season not found")
}
return models.Season{}, errors.Wrap(errors.CodeInternal, "failed to fetch season", err)
}
return season, nil
}
func (s *seasonService) GetByWyID(ctx context.Context, wyID int) (models.Season, error) {
var season models.Season
if err := s.db.WithContext(ctx).First(&season, "wy_id = ?", wyID).Error; err != nil {
if err == gorm.ErrRecordNotFound {
return models.Season{}, errors.New(errors.CodeNotFound, "season not found")
}
return models.Season{}, errors.Wrap(errors.CodeInternal, "failed to fetch season", err)
}
return season, nil
}
...@@ -11,10 +11,11 @@ import ( ...@@ -11,10 +11,11 @@ import (
) )
type TeamService interface { type TeamService interface {
ListTeams(ctx context.Context, limit, offset int) ([]models.Team, int64, error) ListTeams(ctx context.Context, limit, offset int, name string) ([]models.Team, int64, error)
GetByID(ctx context.Context, id string) (models.Team, error) GetByID(ctx context.Context, id string) (models.Team, error)
GetByWyID(ctx context.Context, wyID int) (models.Team, error) GetByWyID(ctx context.Context, wyID int) (models.Team, error)
GetByAnyProviderID(ctx context.Context, providerID string) (models.Team, error) GetByAnyProviderID(ctx context.Context, providerID string) (models.Team, error)
GetByTsIDs(ctx context.Context, tsIDs []string) ([]models.Team, error)
} }
type teamService struct { type teamService struct {
...@@ -25,9 +26,12 @@ func NewTeamService(db *gorm.DB) TeamService { ...@@ -25,9 +26,12 @@ func NewTeamService(db *gorm.DB) TeamService {
return &teamService{db: db} return &teamService{db: db}
} }
func (s *teamService) ListTeams(ctx context.Context, limit, offset int) ([]models.Team, int64, error) { func (s *teamService) ListTeams(ctx context.Context, limit, offset int, name string) ([]models.Team, int64, error) {
var teams []models.Team var teams []models.Team
query := s.db.WithContext(ctx).Model(&models.Team{}) query := s.db.WithContext(ctx).Model(&models.Team{})
if name != "" {
query = query.Where("name ILIKE ?", "%"+name+"%")
}
var total int64 var total int64
if err := query.Count(&total).Error; err != nil { if err := query.Count(&total).Error; err != nil {
...@@ -78,3 +82,16 @@ func (s *teamService) GetByWyID(ctx context.Context, wyID int) (models.Team, err ...@@ -78,3 +82,16 @@ func (s *teamService) GetByWyID(ctx context.Context, wyID int) (models.Team, err
} }
return team, nil return team, nil
} }
func (s *teamService) GetByTsIDs(ctx context.Context, tsIDs []string) ([]models.Team, error) {
if len(tsIDs) == 0 {
return []models.Team{}, nil
}
var teams []models.Team
if err := s.db.WithContext(ctx).Where("ts_id IN ?", tsIDs).Find(&teams).Error; err != nil {
return nil, errors.Wrap(errors.CodeInternal, "failed to fetch teams by ts_id", err)
}
return teams, nil
}
-- Migration 0005: add TheSports country_id field to players
ALTER TABLE players
ADD COLUMN IF NOT EXISTS country_ts_id varchar(64);
ALTER TABLE teams
ADD COLUMN IF NOT EXISTS country_ts_id VARCHAR(64),
ADD COLUMN IF NOT EXISTS competition_ts_id VARCHAR(64),
ADD COLUMN IF NOT EXISTS coach_ts_id VARCHAR(64),
ADD COLUMN IF NOT EXISTS venue_ts_id VARCHAR(64),
ADD COLUMN IF NOT EXISTS website TEXT,
ADD COLUMN IF NOT EXISTS market_value INTEGER,
ADD COLUMN IF NOT EXISTS market_value_currency TEXT,
ADD COLUMN IF NOT EXISTS total_players INTEGER,
ADD COLUMN IF NOT EXISTS foreign_players INTEGER,
ADD COLUMN IF NOT EXISTS national_players INTEGER;
ALTER TABLE coaches
ADD COLUMN IF NOT EXISTS country_ts_id VARCHAR(64),
ADD COLUMN IF NOT EXISTS team_ts_id VARCHAR(64);
File added
Markdown is supported
0% or
You are about to add 0 people to the discussion. Proceed with caution.
Finish editing this message first!
Please register or sign in to comment