351 lines
11 KiB
Go
351 lines
11 KiB
Go
package db
|
|
|
|
import (
|
|
"context"
|
|
|
|
"github.com/pkg/errors"
|
|
"github.com/uptrace/bun"
|
|
)
|
|
|
|
type Player struct {
|
|
bun.BaseModel `bun:"table:players,alias:p"`
|
|
|
|
ID int `bun:"id,pk,autoincrement" json:"id"`
|
|
SlapID *uint32 `bun:"slap_id,unique" json:"slap_id"`
|
|
DiscordID string `bun:"discord_id,unique,notnull" json:"discord_id"`
|
|
UserID *int `bun:"user_id,unique" json:"user_id"`
|
|
Name string `bun:"name,notnull" json:"name"`
|
|
|
|
User *User `bun:"rel:belongs-to,join:user_id=id" json:"-"`
|
|
}
|
|
|
|
func (p *Player) DisplayName() string {
|
|
if p.User != nil {
|
|
return p.User.Username
|
|
}
|
|
return p.Name
|
|
}
|
|
|
|
// NewPlayer creates a new player in the database. If there is an existing user with the same
|
|
// discordID, it will automatically link that user to the player
|
|
func NewPlayer(ctx context.Context, tx bun.Tx, name, discordID string, audit *AuditMeta) (*Player, error) {
|
|
player := &Player{DiscordID: discordID, Name: name}
|
|
user, err := GetUserByDiscordID(ctx, tx, discordID)
|
|
if err != nil && !IsBadRequest(err) {
|
|
return nil, errors.Wrap(err, "GetUserByDiscordID")
|
|
}
|
|
if user != nil {
|
|
player.UserID = &user.ID
|
|
player.Name = user.Username
|
|
}
|
|
err = Insert(tx, player).
|
|
WithAudit(audit, nil).Exec(ctx)
|
|
if err != nil {
|
|
return nil, errors.Wrap(err, "Insert")
|
|
}
|
|
return player, nil
|
|
}
|
|
|
|
// ConnectPlayer links the user to an existing player, or creates a new player to link if not found
|
|
// Populates User.Player on success
|
|
func (u *User) ConnectPlayer(ctx context.Context, tx bun.Tx, audit *AuditMeta) error {
|
|
player, err := GetByField[Player](tx, "p.discord_id", u.DiscordID).
|
|
Relation("User").Get(ctx)
|
|
if err != nil {
|
|
if !IsBadRequest(err) {
|
|
// Unexpected error occured
|
|
return errors.Wrap(err, "GetByField")
|
|
}
|
|
// Player doesn't exist, create a new one
|
|
player, err = NewPlayer(ctx, tx, u.Username, u.DiscordID, audit)
|
|
if err != nil {
|
|
return errors.Wrap(err, "NewPlayer")
|
|
}
|
|
// New player should automatically get linked to the user
|
|
u.Player = player
|
|
return nil
|
|
}
|
|
// Player was found
|
|
if player.UserID != nil {
|
|
if player.UserID == &u.ID {
|
|
return nil
|
|
}
|
|
return errors.New("player with that discord_id already linked to a user")
|
|
}
|
|
player.UserID = &u.ID
|
|
err = UpdateByID(tx, player.ID, player).Column("user_id").Exec(ctx)
|
|
if err != nil {
|
|
return errors.Wrap(err, "UpdateByID")
|
|
}
|
|
u.Player = player
|
|
return nil
|
|
}
|
|
|
|
func GetPlayer(ctx context.Context, tx bun.Tx, playerID int) (*Player, error) {
|
|
return GetByID[Player](tx, playerID).Relation("User").Get(ctx)
|
|
}
|
|
|
|
func UpdatePlayerSlapID(ctx context.Context, tx bun.Tx, playerID int, slapID uint32, audit *AuditMeta) error {
|
|
player, err := GetPlayer(ctx, tx, playerID)
|
|
if err != nil {
|
|
return errors.Wrap(err, "GetPlayer")
|
|
}
|
|
player.SlapID = &slapID
|
|
err = UpdateByID(tx, player.ID, player).Column("slap_id").
|
|
WithAudit(audit, nil).Exec(ctx)
|
|
if err != nil {
|
|
return errors.Wrap(err, "UpdateByID")
|
|
}
|
|
return nil
|
|
}
|
|
|
|
// PlayerAllTimeStats holds aggregated all-time stats for a single player
|
|
type PlayerAllTimeStats struct {
|
|
GamesPlayed int `bun:"games_played"`
|
|
PeriodsPlayed int `bun:"total_periods_played"`
|
|
Goals int `bun:"total_goals"`
|
|
Assists int `bun:"total_assists"`
|
|
Saves int `bun:"total_saves"`
|
|
Shots int `bun:"total_shots"`
|
|
Blocks int `bun:"total_blocks"`
|
|
Passes int `bun:"total_passes"`
|
|
}
|
|
|
|
// GetPlayerAllTimeStats returns aggregated all-time stats for a player
|
|
// across all finalized fixture results (period 3 totals).
|
|
func GetPlayerAllTimeStats(ctx context.Context, tx bun.Tx, playerID int) (*PlayerAllTimeStats, error) {
|
|
if playerID == 0 {
|
|
return nil, errors.New("playerID not provided")
|
|
}
|
|
stats := new(PlayerAllTimeStats)
|
|
err := tx.NewRaw(`
|
|
SELECT
|
|
COUNT(DISTINCT frps.fixture_result_id) AS games_played,
|
|
COALESCE(SUM(frps.periods_played), 0) AS total_periods_played,
|
|
COALESCE(SUM(frps.goals), 0) AS total_goals,
|
|
COALESCE(SUM(frps.assists), 0) AS total_assists,
|
|
COALESCE(SUM(frps.saves), 0) AS total_saves,
|
|
COALESCE(SUM(frps.shots), 0) AS total_shots,
|
|
COALESCE(SUM(frps.blocks), 0) AS total_blocks,
|
|
COALESCE(SUM(frps.passes), 0) AS total_passes
|
|
FROM fixture_result_player_stats frps
|
|
JOIN fixture_results fr ON fr.id = frps.fixture_result_id
|
|
WHERE fr.finalized = true
|
|
AND frps.player_id = ?
|
|
AND frps.period_num = 3
|
|
`, playerID).Scan(ctx, stats)
|
|
if err != nil {
|
|
return nil, errors.Wrap(err, "tx.NewRaw")
|
|
}
|
|
return stats, nil
|
|
}
|
|
|
|
// GetPlayerStatsBySeason returns aggregated stats for a player filtered by season.
|
|
func GetPlayerStatsBySeason(ctx context.Context, tx bun.Tx, playerID, seasonID int) (*PlayerAllTimeStats, error) {
|
|
if playerID == 0 {
|
|
return nil, errors.New("playerID not provided")
|
|
}
|
|
if seasonID == 0 {
|
|
return nil, errors.New("seasonID not provided")
|
|
}
|
|
stats := new(PlayerAllTimeStats)
|
|
err := tx.NewRaw(`
|
|
SELECT
|
|
COUNT(DISTINCT frps.fixture_result_id) AS games_played,
|
|
COALESCE(SUM(frps.periods_played), 0) AS total_periods_played,
|
|
COALESCE(SUM(frps.goals), 0) AS total_goals,
|
|
COALESCE(SUM(frps.assists), 0) AS total_assists,
|
|
COALESCE(SUM(frps.saves), 0) AS total_saves,
|
|
COALESCE(SUM(frps.shots), 0) AS total_shots,
|
|
COALESCE(SUM(frps.blocks), 0) AS total_blocks,
|
|
COALESCE(SUM(frps.passes), 0) AS total_passes
|
|
FROM fixture_result_player_stats frps
|
|
JOIN fixture_results fr ON fr.id = frps.fixture_result_id
|
|
JOIN fixtures f ON f.id = fr.fixture_id
|
|
WHERE fr.finalized = true
|
|
AND frps.player_id = ?
|
|
AND frps.period_num = 3
|
|
AND f.season_id = ?
|
|
`, playerID, seasonID).Scan(ctx, stats)
|
|
if err != nil {
|
|
return nil, errors.Wrap(err, "tx.NewRaw")
|
|
}
|
|
return stats, nil
|
|
}
|
|
|
|
// GetPlayerStatsByTeam returns aggregated stats for a player filtered by team.
|
|
func GetPlayerStatsByTeam(ctx context.Context, tx bun.Tx, playerID, teamID int) (*PlayerAllTimeStats, error) {
|
|
if playerID == 0 {
|
|
return nil, errors.New("playerID not provided")
|
|
}
|
|
if teamID == 0 {
|
|
return nil, errors.New("teamID not provided")
|
|
}
|
|
stats := new(PlayerAllTimeStats)
|
|
err := tx.NewRaw(`
|
|
SELECT
|
|
COUNT(DISTINCT frps.fixture_result_id) AS games_played,
|
|
COALESCE(SUM(frps.periods_played), 0) AS total_periods_played,
|
|
COALESCE(SUM(frps.goals), 0) AS total_goals,
|
|
COALESCE(SUM(frps.assists), 0) AS total_assists,
|
|
COALESCE(SUM(frps.saves), 0) AS total_saves,
|
|
COALESCE(SUM(frps.shots), 0) AS total_shots,
|
|
COALESCE(SUM(frps.blocks), 0) AS total_blocks,
|
|
COALESCE(SUM(frps.passes), 0) AS total_passes
|
|
FROM fixture_result_player_stats frps
|
|
JOIN fixture_results fr ON fr.id = frps.fixture_result_id
|
|
WHERE fr.finalized = true
|
|
AND frps.player_id = ?
|
|
AND frps.period_num = 3
|
|
AND frps.team_id = ?
|
|
`, playerID, teamID).Scan(ctx, stats)
|
|
if err != nil {
|
|
return nil, errors.Wrap(err, "tx.NewRaw")
|
|
}
|
|
return stats, nil
|
|
}
|
|
|
|
// PlayerTeamInfo holds a team the player has played on and how many seasons
|
|
type PlayerTeamInfo struct {
|
|
Team *Team
|
|
SeasonsCount int
|
|
}
|
|
|
|
// GetPlayerTeams returns all teams the player has been rostered on,
|
|
// with a count of distinct seasons per team.
|
|
func GetPlayerTeams(ctx context.Context, tx bun.Tx, playerID int) ([]*PlayerTeamInfo, error) {
|
|
if playerID == 0 {
|
|
return nil, errors.New("playerID not provided")
|
|
}
|
|
type teamRow struct {
|
|
TeamID int `bun:"team_id"`
|
|
SeasonsCount int `bun:"seasons_count"`
|
|
Name string `bun:"name"`
|
|
ShortName string `bun:"short_name"`
|
|
AltShortName string `bun:"alt_short_name"`
|
|
Color string `bun:"color"`
|
|
}
|
|
var rows []teamRow
|
|
err := tx.NewRaw(`
|
|
SELECT
|
|
t.id AS team_id,
|
|
t.name AS name,
|
|
t.short_name AS short_name,
|
|
t.alt_short_name AS alt_short_name,
|
|
t.color AS color,
|
|
COUNT(DISTINCT tr.season_id) AS seasons_count
|
|
FROM team_rosters tr
|
|
JOIN teams t ON t.id = tr.team_id
|
|
WHERE tr.player_id = ?
|
|
GROUP BY t.id, t.name, t.short_name, t.alt_short_name, t.color
|
|
ORDER BY seasons_count DESC
|
|
`, playerID).Scan(ctx, &rows)
|
|
if err != nil {
|
|
return nil, errors.Wrap(err, "tx.NewRaw")
|
|
}
|
|
|
|
var results []*PlayerTeamInfo
|
|
for _, row := range rows {
|
|
results = append(results, &PlayerTeamInfo{
|
|
Team: &Team{
|
|
ID: row.TeamID,
|
|
Name: row.Name,
|
|
ShortName: row.ShortName,
|
|
AltShortName: row.AltShortName,
|
|
Color: row.Color,
|
|
},
|
|
SeasonsCount: row.SeasonsCount,
|
|
})
|
|
}
|
|
return results, nil
|
|
}
|
|
|
|
// PlayerSeasonInfo holds info about a player's participation in a specific season
|
|
type PlayerSeasonInfo struct {
|
|
Season *Season
|
|
League *League
|
|
Team *Team
|
|
IsManager bool
|
|
}
|
|
|
|
// GetPlayerSeasons returns all season/league/team combos the player has been rostered in.
|
|
func GetPlayerSeasons(ctx context.Context, tx bun.Tx, playerID int) ([]*PlayerSeasonInfo, error) {
|
|
if playerID == 0 {
|
|
return nil, errors.New("playerID not provided")
|
|
}
|
|
var rosters []*TeamRoster
|
|
err := tx.NewSelect().
|
|
Model(&rosters).
|
|
Where("tr.player_id = ?", playerID).
|
|
Relation("Season").
|
|
Relation("League").
|
|
Relation("Team").
|
|
Order("season.start_date DESC").
|
|
Scan(ctx)
|
|
if err != nil {
|
|
return nil, errors.Wrap(err, "tx.NewSelect")
|
|
}
|
|
|
|
var results []*PlayerSeasonInfo
|
|
for _, r := range rosters {
|
|
results = append(results, &PlayerSeasonInfo{
|
|
Season: r.Season,
|
|
League: r.League,
|
|
Team: r.Team,
|
|
IsManager: r.IsManager,
|
|
})
|
|
}
|
|
return results, nil
|
|
}
|
|
|
|
// GetPlayerSeasonsList returns distinct seasons the player has participated in (for filter dropdowns).
|
|
func GetPlayerSeasonsList(ctx context.Context, tx bun.Tx, playerID int) ([]*Season, error) {
|
|
if playerID == 0 {
|
|
return nil, errors.New("playerID not provided")
|
|
}
|
|
var seasons []*Season
|
|
err := tx.NewSelect().
|
|
Model(&seasons).
|
|
Join("JOIN team_rosters tr ON tr.season_id = s.id").
|
|
Where("tr.player_id = ?", playerID).
|
|
GroupExpr("s.id").
|
|
Order("s.start_date DESC").
|
|
Scan(ctx)
|
|
if err != nil {
|
|
return nil, errors.Wrap(err, "tx.NewSelect")
|
|
}
|
|
return seasons, nil
|
|
}
|
|
|
|
// GetPlayerTeamsList returns distinct teams the player has played on (for filter dropdowns).
|
|
func GetPlayerTeamsList(ctx context.Context, tx bun.Tx, playerID int) ([]*Team, error) {
|
|
if playerID == 0 {
|
|
return nil, errors.New("playerID not provided")
|
|
}
|
|
var teams []*Team
|
|
err := tx.NewSelect().
|
|
Model(&teams).
|
|
Join("JOIN team_rosters tr ON tr.team_id = t.id").
|
|
Where("tr.player_id = ?", playerID).
|
|
GroupExpr("t.id").
|
|
Order("t.name ASC").
|
|
Scan(ctx)
|
|
if err != nil {
|
|
return nil, errors.Wrap(err, "tx.NewSelect")
|
|
}
|
|
return teams, nil
|
|
}
|
|
|
|
func GetPlayersNotOnTeam(ctx context.Context, tx bun.Tx, seasonID, leagueID int) ([]*Player, error) {
|
|
players, err := GetList[Player](tx).Relation("User").
|
|
Join("LEFT JOIN team_rosters tr ON tr.player_id = p.id").
|
|
Where("NOT (tr.season_id = ? and tr.league_id = ?) OR (tr.season_id IS NULL and tr.league_id IS NULL)",
|
|
seasonID, leagueID).
|
|
Order("p.name ASC").
|
|
GetAll(ctx)
|
|
if err != nil {
|
|
return nil, errors.Wrap(err, "GetList")
|
|
}
|
|
return players, nil
|
|
}
|