initial commit

This commit is contained in:
2025-02-02 15:11:35 +01:00
commit 3640116dd5
39 changed files with 10298 additions and 0 deletions

71
internal/beerpong-elo.go Normal file
View File

@@ -0,0 +1,71 @@
package beerpongelo
import (
"encoding/json"
"log"
"net/http"
"os"
"git.kapelle.org/niklas/beerpong-elo/internal/model"
"git.kapelle.org/niklas/beerpong-elo/internal/repo"
"git.kapelle.org/niklas/beerpong-elo/internal/web"
)
type Config struct {
DBHost string
DBUsername string
DBPassword string
DBName string
Address string
}
func Start(config Config) {
repo, err := repo.NewSQLRepo(config.DBHost, config.DBUsername, config.DBPassword, config.DBName)
if err != nil {
log.Fatalf("Failed to start server: %s", err)
}
mux := web.CreateWebserver(repo)
http.ListenAndServe(config.Address, mux)
}
func Import(config Config, file string) {
repo, err := repo.NewSQLRepo(config.DBHost, config.DBUsername, config.DBPassword, config.DBName)
if err != nil {
log.Fatalf("Failed to import file %s: %s", file, err)
}
controller := newController(repo)
err = loadFromFile(controller, file)
if err != nil {
log.Fatalf("Failed to import file %s: %s", file, err)
}
}
func loadFromFile(c Controller, path string) error {
content, err := os.ReadFile(path)
if err != nil {
return err
}
var payload []model.Game
err = json.Unmarshal(content, &payload)
if err != nil {
return err
}
for _, game := range payload {
_, err = c.AddGame(game)
if err != nil {
return err
}
}
log.Printf("Imported %d games", len(payload))
return nil
}

142
internal/controller.go Normal file
View File

@@ -0,0 +1,142 @@
package beerpongelo
import (
"math"
"git.kapelle.org/niklas/beerpong-elo/internal/model"
"git.kapelle.org/niklas/beerpong-elo/internal/repo"
)
type Controller struct {
repo repo.Repo
}
func newController(repo repo.Repo) Controller {
return Controller{
repo: repo,
}
}
func (c *Controller) AddGame(game model.Game) (model.GameID, error) {
parsedGame := model.Game{
Added: game.Added,
Score: game.Score,
Overtime: game.Overtime,
}
author, err := c.repo.GetOrCreatePlayerID(string(game.Author))
if err != nil {
return "", err
}
parsedGame.Author = author
t0p0, err := c.repo.GetOrCreatePlayerID(string(game.Team0Player0))
if err != nil {
return "", err
}
parsedGame.Team0Player0 = t0p0
t0p1, err := c.repo.GetOrCreatePlayerID(string(game.Team0Player1))
if err != nil {
return "", err
}
parsedGame.Team0Player1 = t0p1
t1p0, err := c.repo.GetOrCreatePlayerID(string(game.Team1Player0))
if err != nil {
return "", err
}
parsedGame.Team1Player0 = t1p0
t1p1, err := c.repo.GetOrCreatePlayerID(string(game.Team1Player1))
if err != nil {
return "", err
}
parsedGame.Team1Player1 = t1p1
id, err := c.repo.AddGame(parsedGame)
if err != nil {
return "", err
}
parsedGame.ID = id
err = c.createGameResult(parsedGame)
if err != nil {
return "", err
}
return id, nil
}
func (c *Controller) createGameResult(game model.Game) error {
t0p0, err := c.repo.GetPlayer(game.Team0Player0)
if err != nil {
return err
}
t0p1, err := c.repo.GetPlayer(game.Team0Player1)
if err != nil {
return err
}
t1p0, err := c.repo.GetPlayer(game.Team1Player0)
if err != nil {
return err
}
t1p1, err := c.repo.GetPlayer(game.Team1Player1)
if err != nil {
return err
}
newT0p0, newT0p1, newT1p0, newT1p1 := NewRating(float64(t0p0.Elo), float64(t0p1.Elo), float64(t1p0.Elo), float64(t1p1.Elo), game.Score)
t0p0Result := model.GameResult{
Game: game.ID,
Player: t0p0.ID,
StartElo: t0p0.Elo,
EndElo: int(math.Round(newT0p0)),
}
t0p1Result := model.GameResult{
Game: game.ID,
Player: t0p1.ID,
StartElo: t0p1.Elo,
EndElo: int(math.Round(newT0p1)),
}
t1p0Result := model.GameResult{
Game: game.ID,
Player: t1p0.ID,
StartElo: t1p0.Elo,
EndElo: int(math.Round(newT1p0)),
}
t1p1Result := model.GameResult{
Game: game.ID,
Player: t1p1.ID,
StartElo: t1p1.Elo,
EndElo: int(math.Round(newT1p1)),
}
_,err = c.repo.AddGameResult(t0p0Result)
if err != nil{
return err
}
_,err = c.repo.AddGameResult(t0p1Result)
if err != nil{
return err
}
_,err = c.repo.AddGameResult(t1p0Result)
if err != nil{
return err
}
_,err = c.repo.AddGameResult(t1p1Result)
if err != nil{
return err
}
return nil
}

52
internal/elo.go Normal file
View File

@@ -0,0 +1,52 @@
package beerpongelo
import (
"math"
)
func winProbPlayer(p, e1, e2 float64) float64 {
const MAGIC float64 = 500
lhs := 1 / (1 + math.Pow(10., (p-e1)/MAGIC))
rhs := 1 / (1 + math.Pow(10., (p-e2)/MAGIC))
return (lhs + rhs) / 2.
}
func NewRating(t0p0, t0p1, t1p0, t1p1 float64, score int) (float64, float64, float64, float64) {
const K float64 = 50
const SCORE_BIAS float64 = 1
const SCORE_SCALE float64 = 0.4
const MAX_SCORE float64 = 10
et0p0 := winProbPlayer(t0p0, t1p0, t1p1)
et0p1 := winProbPlayer(t0p1, t1p0, t1p1)
et1p0 := winProbPlayer(t1p0, t0p0, t0p1)
et1p1 := winProbPlayer(t1p1, t0p0, t0p1)
et0 := (et0p0 + et0p1) / 2.
et1 := (et1p0 + et1p1) / 2.
ct0p0 := t0p0 / (t0p0 + t0p1)
ct0p1 := t0p1 / (t0p0 + t0p1)
ct1p0 := t1p0 / (t1p0 + t1p1)
ct1p1 := t1p1 / (t1p0 + t1p1)
var st0 float64
var st1 float64
if score > 0 {
st0 = 1
st1 = 0
} else {
st0 = 0
st1 = 1
}
scoreMod := SCORE_SCALE * math.Log(math.Abs(float64(score))+SCORE_BIAS) / (math.Log(MAX_SCORE) + SCORE_BIAS)
newt0p0 := t0p0 + K*scoreMod*((st0*ct0p0)-(et0*ct0p0))
newt0p1 := t0p1 + K*scoreMod*((st0*ct0p1)-(et0*ct0p1))
newt1p0 := t1p0 + K*scoreMod*((st1*ct1p0)-(et1*ct1p0))
newt1p1 := t1p1 + K*scoreMod*((st1*ct1p1)-(et1*ct1p1))
return newt0p0, newt0p1, newt1p0, newt1p1
}

33
internal/model/game.go Normal file
View File

@@ -0,0 +1,33 @@
package model
import (
"fmt"
"time"
)
type GameID string
type Game struct {
ID GameID `json:"id"`
Added time.Time `json:"added"`
Author PlayerID `json:"author"`
Team0Player0 PlayerID `json:"t0p0"`
Team0Player1 PlayerID `json:"t0p1"`
Team1Player0 PlayerID `json:"t1p0"`
Team1Player1 PlayerID `json:"t1p1"`
Score int `json:"score"`
Overtime bool `jsone:"ot"`
}
func (id *GameID) Scan(src any) error {
switch v := src.(type) {
case int64:
*id = GameID(fmt.Sprintf("%d", v))
return nil
case string:
*id = GameID(v)
return nil
default:
return fmt.Errorf("unsupported type %T for GameID", src)
}
}

View File

@@ -0,0 +1,26 @@
package model
import "fmt"
type GameResultID string
type GameResult struct {
ID GameResultID
Game GameID
Player PlayerID
StartElo int
EndElo int
}
func (id *GameResultID) Scan(src any) error {
switch v := src.(type) {
case int64:
*id = GameResultID(fmt.Sprintf("%d", v)) // Convert int64 to string and assign it
return nil
case string:
*id = GameResultID(v)
return nil
default:
return fmt.Errorf("unsupported type %T for GameResultID", src)
}
}

31
internal/model/player.go Normal file
View File

@@ -0,0 +1,31 @@
package model
import "fmt"
type PlayerID string
type Player struct {
ID PlayerID
Name string
Elo int
}
func NewPlayer(name string) Player {
return Player{
Name: name,
Elo: 1000,
}
}
func (id *PlayerID) Scan(src any) error {
switch v := src.(type) {
case int64:
*id = PlayerID(fmt.Sprintf("%d", v)) // Convert int64 to string and assign it
return nil
case string:
*id = PlayerID(v)
return nil
default:
return fmt.Errorf("unsupported type %T for PlayerID", src)
}
}

40
internal/repo/repo.go Normal file
View File

@@ -0,0 +1,40 @@
package repo
import (
model "git.kapelle.org/niklas/beerpong-elo/internal/model"
)
type Repo interface {
// Add a new game to the repo. ID needs to be nil on argument. PlayerID are fetched beforehand.
AddGame(model.Game) (model.GameID, error)
// Get a game by ID. Returns nil if not found.
GetGame(model.GameID) (*model.Game, error)
// Get ID of a player. Creates one if not found.
GetOrCreatePlayerID(string) (model.PlayerID, error)
// Get player by id. Returns nil if not found.
GetPlayer(model.PlayerID) (*model.Player, error)
// Adds a game result. ID needs to be nil on argument.
AddGameResult(model.GameResult) (model.GameResultID, error)
// Get a game result by ID. Returns nil if not found.
GetGameResult(model.GameResultID) (*model.GameResult, error)
// Get a game result for a player in a game
GetGameResultForPlayerAndGame(model.GameID, model.PlayerID) (*model.GameResult, error)
// Get all game results for a player
GetGameResultsForPlayer(model.PlayerID) ([]*model.GameResult, error)
// Get games for a player
GetGamesForPlayer(model.PlayerID) ([]*model.Game, error)
// Get all players
GetPlayers() ([]*model.Player, error)
// Get all games
GetGames() ([]*model.Game, error)
}

279
internal/repo/sqlRepo.go Normal file
View File

@@ -0,0 +1,279 @@
package repo
import (
"database/sql"
"fmt"
"time"
"git.kapelle.org/niklas/beerpong-elo/internal/model"
_ "github.com/go-sql-driver/mysql"
)
type SQLRepo struct {
db *sql.DB
}
func NewSQLRepo(host, username, password, dbName string) (Repo, error) {
db, err := sql.Open("mysql", fmt.Sprintf("%s:%s@tcp(%s)/%s?parseTime=true", username, password, host, dbName))
if err != nil {
return nil, err
}
db.SetConnMaxLifetime(time.Minute * 3)
db.SetMaxOpenConns(10)
db.SetMaxIdleConns(10)
return &SQLRepo{
db: db,
}, nil
}
func (s *SQLRepo) AddGame(game model.Game) (model.GameID, error) {
stmt, err := s.db.Prepare("INSERT INTO Games(added,score,overtime,author,team0player0,team0player1,team1player0,team1player1) VALUES (?,?,?,?,?,?,?,?)")
if err != nil {
return "", err
}
res, err := stmt.Exec(game.Added, game.Score, game.Overtime, game.Author, game.Team0Player0, game.Team0Player1, game.Team1Player0, game.Team1Player1)
if err != nil {
return "", err
}
id, err := res.LastInsertId()
if err != nil {
return "", err
}
var gameID model.GameID
gameID.Scan(id)
return gameID, nil
}
func (s *SQLRepo) AddGameResult(gameResult model.GameResult) (model.GameResultID, error) {
stms, err := s.db.Prepare("INSERT INTO GameResults(game,player,startElo,endElo) VALUES (?,?,?,?)")
if err != nil {
return "", err
}
res, err := stms.Exec(gameResult.Game, gameResult.Player, gameResult.StartElo, gameResult.EndElo)
if err != nil {
return "", err
}
id, err := res.LastInsertId()
if err != nil {
return "", err
}
var gameResultID model.GameResultID
gameResultID.Scan(id)
return gameResultID, nil
}
func (s *SQLRepo) GetGame(id model.GameID) (*model.Game, error) {
rows := s.db.QueryRow("SELECT id,added, score,overtime,author,team0player0,team0player1,team1player0,team1player1 FROM Games WHERE id = ? ", id)
var game model.Game
if err := rows.Scan(&game.ID, &game.Added, &game.Score, &game.Overtime, &game.Author, &game.Team0Player0, &game.Team0Player1, &game.Team1Player0, &game.Team1Player1); err != nil {
if err == sql.ErrNoRows {
return nil, nil
}
return nil, err
}
return &game, nil
}
func (s *SQLRepo) GetGameResult(id model.GameResultID) (*model.GameResult, error) {
rows := s.db.QueryRow("SELECT game,player,startElo,endElo FROM GameResults WHERE id = ? ", id)
var gameResult model.GameResult
gameResult.ID = id
if err := rows.Scan(&gameResult.Game, &gameResult.Player, &gameResult.StartElo, &gameResult.EndElo); err != nil {
if err == sql.ErrNoRows {
return nil, nil
}
return nil, err
}
return &gameResult, nil
}
func (s *SQLRepo) playerWithNameExists(name string) (*model.PlayerID, error) {
rows := s.db.QueryRow("SELECT id FROM Players WHERE name = ?", name)
var id model.PlayerID
if err := rows.Scan(&id); err != nil {
if err == sql.ErrNoRows {
return nil, nil
}
return nil, err
}
return &id, nil
}
func (s *SQLRepo) GetOrCreatePlayerID(name string) (model.PlayerID, error) {
foundID, err := s.playerWithNameExists(name)
if err != nil {
return "", err
}
if foundID != nil {
return *foundID, nil
}
stmt, err := s.db.Prepare("INSERT INTO Players(name,elo) VALUES (?,?)")
if err != nil {
return "", err
}
newPlayer := model.NewPlayer(name)
res, err := stmt.Exec(newPlayer.Name, newPlayer.Elo)
if err != nil {
return "", err
}
id, err := res.LastInsertId()
if err != nil {
return "", err
}
var playerID model.PlayerID
playerID.Scan(id)
return playerID, nil
}
func (s *SQLRepo) GetPlayer(id model.PlayerID) (*model.Player, error) {
rows := s.db.QueryRow("SELECT name,elo from Players WHERE id = ?", id)
var player model.Player
player.ID = id
if err := rows.Scan(&player.Name, &player.Elo); err != nil {
if err == sql.ErrNoRows {
return nil, nil
}
return nil, err
}
return &player, nil
}
func (s *SQLRepo) GetGamesForPlayer(id model.PlayerID) ([]*model.Game, error) {
rows, err := s.db.Query(`
SELECT id,added, score,overtime,author,team0player0,team0player1,team1player0,team1player1
FROM Games
WHERE team0player0 = ? OR team0player1 = ? OR team1player0 = ? OR team1player1 = ?
`, id, id, id, id)
if err != nil {
return nil, err
}
defer rows.Close()
rtn := []*model.Game{}
for rows.Next() {
var game model.Game
err = rows.Scan(&game.ID, &game.Added, &game.Score, &game.Overtime, &game.Author, &game.Team0Player0, &game.Team0Player1, &game.Team1Player0, &game.Team1Player1)
if err != nil {
return nil, err
}
rtn = append(rtn, &game)
}
return rtn, nil
}
func (s *SQLRepo) GetGameResultForPlayerAndGame(gameID model.GameID, playerID model.PlayerID) (*model.GameResult, error) {
rows := s.db.QueryRow("SELECT id,game,player,startElo,endElo FROM GameResults WHERE game = ? AND player = ? ", gameID, playerID)
var gameResult model.GameResult
if err := rows.Scan(&gameResult.ID, &gameResult.Game, &gameResult.Player, &gameResult.StartElo, &gameResult.EndElo); err != nil {
if err == sql.ErrNoRows {
return nil, nil
}
return nil, err
}
return &gameResult, nil
}
func (s *SQLRepo) GetGameResultsForPlayer(id model.PlayerID) ([]*model.GameResult, error) {
rows, err := s.db.Query("SELECT id,game,player,startElo,endElo FROM GameResults WHERE player = ? ", id)
if err != nil {
return nil, err
}
rtn := []*model.GameResult{}
for rows.Next() {
var gameResult model.GameResult
err = rows.Scan(&gameResult.ID, &gameResult.Game, &gameResult.Player, &gameResult.StartElo, &gameResult.EndElo)
if err != nil {
return nil, err
}
rtn = append(rtn, &gameResult)
}
return rtn, nil
}
func (s *SQLRepo) GetPlayers() ([]*model.Player, error) {
rows, err := s.db.Query("SELECT id, name, elo FROM Players")
if err != nil {
return nil, err
}
rtn := []*model.Player{}
for rows.Next() {
var player model.Player
err = rows.Scan(&player.ID, &player.Name, &player.Elo)
if err != nil {
return nil, err
}
rtn = append(rtn, &player)
}
return rtn, nil
}
func (s *SQLRepo) GetGames() ([]*model.Game, error) {
rows, err := s.db.Query("SELECT id,added, score,overtime,author,team0player0,team0player1,team1player0,team1player1 FROM Games")
if err != nil {
return nil, err
}
rtn := []*model.Game{}
for rows.Next() {
var game model.Game
err = rows.Scan(&game.ID, &game.Added, &game.Score, &game.Overtime, &game.Author, &game.Team0Player0, &game.Team0Player1, &game.Team1Player0, &game.Team1Player1)
if err != nil {
return nil, err
}
rtn = append(rtn, &game)
}
return rtn, nil
}

View File

@@ -0,0 +1,44 @@
CREATE TABLE Players (
id INT AUTO_INCREMENT PRIMARY KEY,
name VARCHAR(255) NOT NULL,
elo INT
);
CREATE TABLE Games (
id INT AUTO_INCREMENT PRIMARY KEY,
added DATETIME NOT NULL,
score INT NOT NULL,
overtime BOOL NOT NULL,
author INT,
team0player0 INT,
team0player1 INT,
team1player0 INT,
team1player1 INT,
FOREIGN KEY (author) REFERENCES Players(id) on DELETE SET NULL,
FOREIGN KEY (team0player0) REFERENCES Players(id) ON DELETE SET NULL,
FOREIGN KEY (team0player1) REFERENCES Players(id) ON DELETE SET NULL,
FOREIGN KEY (team1player0) REFERENCES Players(id) ON DELETE SET NULL,
FOREIGN KEY (team1player1) REFERENCES Players(id) ON DELETE SET NULL
);
CREATE TABLE GameResults (
id INT AUTO_INCREMENT PRIMARY KEY,
game INT,
player INT,
startElo INT,
endElo INT,
FOREIGN KEY (game) REFERENCES Games(id) on DELETE SET NULL,
FOREIGN KEY (player) REFERENCES Players(id) on DELETE SET NULL
)
DELIMITER $$
CREATE TRIGGER UpdateCurrentELO
AFTER INSERT ON GameResults
FOR EACH ROW
BEGIN
UPDATE Players
SET elo = NEW.endElo
WHERE id = NEW.player;
END$$
DELIMITER ;

369
internal/web/gqlSchema.go Normal file
View File

@@ -0,0 +1,369 @@
package web
import (
"log"
"git.kapelle.org/niklas/beerpong-elo/internal/model"
"git.kapelle.org/niklas/beerpong-elo/internal/repo"
"github.com/graphql-go/graphql"
)
func createShema(repo repo.Repo) graphql.Schema {
player := graphql.NewObject(graphql.ObjectConfig{
Name: "Player",
Description: "A player. Can also be authors of games",
Fields: graphql.Fields{
"ID": &graphql.Field{
Type: graphql.NewNonNull(graphql.ID),
},
"name": &graphql.Field{
Type: graphql.NewNonNull(graphql.String),
},
"elo": &graphql.Field{
Type: graphql.NewNonNull(graphql.Int),
},
// Field "games" added below
// Field "history" added below
},
})
game := graphql.NewObject(graphql.ObjectConfig{
Name: "Game",
Description: "A game played",
Fields: graphql.Fields{
"id": &graphql.Field{
Type: graphql.NewNonNull(graphql.ID),
},
"added": &graphql.Field{
Type: graphql.NewNonNull(graphql.DateTime),
},
"overtime": &graphql.Field{
Type: graphql.NewNonNull(graphql.Boolean),
},
"score": &graphql.Field{
Type: graphql.NewNonNull(graphql.Int),
},
"author": &graphql.Field{
Type: graphql.NewNonNull(player),
Resolve: func(p graphql.ResolveParams) (interface{}, error) {
game, ok := p.Source.(*model.Game)
if !ok {
log.Printf("Can't get source for field 'author' on 'Game' is %v", p.Source)
return nil, nil
}
player, err := repo.GetPlayer(game.Author)
if err != nil {
return nil, err
}
return player, nil
},
},
"team0player0": &graphql.Field{
Type: graphql.NewNonNull(player),
Resolve: func(p graphql.ResolveParams) (interface{}, error) {
game, ok := p.Source.(*model.Game)
if !ok {
log.Printf("Can't get source for field 'team0player0' on 'Game' is %v", p.Source)
return nil, nil
}
player, err := repo.GetPlayer(game.Team0Player0)
if err != nil {
return nil, err
}
return player, nil
},
},
"team0player1": &graphql.Field{
Type: graphql.NewNonNull(player),
Resolve: func(p graphql.ResolveParams) (interface{}, error) {
game, ok := p.Source.(*model.Game)
if !ok {
log.Printf("Can't get source for field 'team0player1' on 'Game' is %v", p.Source)
return nil, nil
}
player, err := repo.GetPlayer(game.Team0Player1)
if err != nil {
return nil, err
}
return player, nil
},
},
"team1player0": &graphql.Field{
Type: graphql.NewNonNull(player),
Resolve: func(p graphql.ResolveParams) (interface{}, error) {
game, ok := p.Source.(*model.Game)
if !ok {
log.Printf("Can't get source for field 'team1player0' on 'Game' is %v", p.Source)
return nil, nil
}
player, err := repo.GetPlayer(game.Team1Player0)
if err != nil {
return nil, err
}
return player, nil
},
},
"team1player1": &graphql.Field{
Type: graphql.NewNonNull(player),
Resolve: func(p graphql.ResolveParams) (interface{}, error) {
game, ok := p.Source.(*model.Game)
if !ok {
log.Printf("Can't get source for field 'team1player1' on 'Game' is %v", p.Source)
return nil, nil
}
player, err := repo.GetPlayer(game.Team1Player1)
if err != nil {
return nil, err
}
return player, nil
},
},
},
})
player.AddFieldConfig("games", &graphql.Field{
Type: graphql.NewNonNull(graphql.NewList(game)),
Resolve: func(p graphql.ResolveParams) (interface{}, error) {
player, ok := p.Source.(*model.Player)
if !ok {
log.Printf("Can't get source for field 'games' on 'Player' is %v", p.Source)
return nil, nil
}
games, err := repo.GetGamesForPlayer(player.ID)
if err != nil {
return nil, err
}
return games, nil
},
})
gameResult := graphql.NewObject(graphql.ObjectConfig{
Name: "GameResult",
Description: "The ELO change for a player in a game",
Fields: graphql.Fields{
"id": &graphql.Field{
Type: graphql.NewNonNull(graphql.ID),
},
"player": &graphql.Field{
Type: player,
Resolve: func(p graphql.ResolveParams) (interface{}, error) {
gameResult, ok := p.Source.(*model.GameResult)
if !ok {
log.Printf("Can't get source for field 'player' on 'GameResult' is %v", p.Source)
return nil, nil
}
player, err := repo.GetPlayer(gameResult.Player)
if err != nil {
return nil, err
}
return player, nil
},
},
"game": &graphql.Field{
Type: graphql.NewNonNull(game),
Resolve: func(p graphql.ResolveParams) (interface{}, error) {
gameResult, ok := p.Source.(*model.GameResult)
if !ok {
log.Printf("Can't get source for field 'game' on 'GameResult' is %v", p.Source)
return nil, nil
}
game, err := repo.GetGame(gameResult.Game)
if err != nil {
return nil, err
}
return game, nil
},
},
"startElo": &graphql.Field{
Type: graphql.NewNonNull(graphql.Int),
},
"endElo": &graphql.Field{
Type: graphql.NewNonNull(graphql.Int),
},
"delta": &graphql.Field{
Type: graphql.NewNonNull(graphql.Int),
Resolve: func(p graphql.ResolveParams) (interface{}, error) {
gameResult, ok := p.Source.(*model.GameResult)
if !ok {
log.Printf("Can't get source for field 'delta' on 'GameResult' is %v", p.Source)
return nil, nil
}
delta := gameResult.EndElo - gameResult.StartElo
return delta, nil
},
},
},
})
player.AddFieldConfig("history", &graphql.Field{
Type: graphql.NewNonNull(graphql.NewList(gameResult)),
Args: graphql.FieldConfigArgument{
"game": &graphql.ArgumentConfig{
Type: graphql.ID,
Description: "Filter by Game ID",
},
},
Resolve: func(p graphql.ResolveParams) (interface{}, error) {
player, ok := p.Source.(*model.Player)
if !ok {
log.Printf("Can't get source from field 'history' on 'player' is %v", p.Source)
return nil, nil
}
if p.Args["game"] != nil {
// Find result for single game
gameID, ok := p.Args["game"].(string)
if !ok {
log.Printf("Failed to parse 'game' at field 'history' on 'player' is %v", p.Args["game"])
return nil, nil
}
gameResult, err := repo.GetGameResultForPlayerAndGame(model.GameID(gameID), player.ID)
if err != nil {
return nil, err
}
return []*model.GameResult{gameResult}, nil
}
// Find all gameResults for a player
gameResults, err := repo.GetGameResultsForPlayer(player.ID)
if err != nil {
return nil, err
}
return gameResults, nil
},
})
queryFields := graphql.Fields{
"player": &graphql.Field{
Type: player,
Description: "Get player by ID",
Args: graphql.FieldConfigArgument{
"id": &graphql.ArgumentConfig{
Type: graphql.NewNonNull(graphql.ID),
},
},
Resolve: func(p graphql.ResolveParams) (interface{}, error) {
id, ok := p.Args["id"].(string)
if !ok {
log.Printf("Failed to parse ID at player: %v", p.Args["id"])
return nil, nil
}
player, err := repo.GetPlayer(model.PlayerID(id))
if err != nil {
return nil, err
}
return player, nil
},
},
"game": &graphql.Field{
Type: game,
Description: "Get game by ID",
Args: graphql.FieldConfigArgument{
"id": &graphql.ArgumentConfig{
Type: graphql.NewNonNull(graphql.ID),
},
},
Resolve: func(p graphql.ResolveParams) (interface{}, error) {
id, ok := p.Args["id"].(string)
if !ok {
log.Printf("Failed to parse ID at game: %v", p.Args["id"])
return nil, nil
}
game, err := repo.GetGame(model.GameID(id))
if err != nil {
return nil, err
}
return game, nil
},
},
"gameResult": &graphql.Field{
Type: gameResult,
Description: "Result of a game on a players ELO",
Args: graphql.FieldConfigArgument{
"id": &graphql.ArgumentConfig{
Type: graphql.NewNonNull(graphql.ID),
},
},
Resolve: func(p graphql.ResolveParams) (interface{}, error) {
id, ok := p.Args["id"].(string)
if !ok {
log.Printf("Failed to parse ID at gameResult: %v", p.Args["id"])
return nil, nil
}
gameResult, err := repo.GetGameResult(model.GameResultID(id))
if err != nil {
return nil, err
}
return gameResult, nil
},
},
"players": &graphql.Field{
Type: graphql.NewNonNull(graphql.NewList(player)),
Description: "Get all players",
Resolve: func(p graphql.ResolveParams) (interface{}, error) {
players, err := repo.GetPlayers()
if err != nil {
return nil, err
}
return players, nil
},
},
"games": &graphql.Field{
Type: graphql.NewNonNull(graphql.NewList(game)),
Description: "Get all games",
Resolve: func(p graphql.ResolveParams) (interface{}, error) {
games, err := repo.GetGames()
if err != nil {
return nil, err
}
return games, nil
},
},
}
querySchema := graphql.ObjectConfig{
Name: "Query",
Fields: queryFields,
}
schemaConfig := graphql.SchemaConfig{Query: graphql.NewObject(querySchema)}
schema, err := graphql.NewSchema(schemaConfig)
if err != nil {
log.Fatalf("failed to create new schema, error: %v", err)
}
return schema
}

View File

@@ -0,0 +1,28 @@
package web
import (
"embed"
"io/fs"
"net/http"
"os"
)
//go:embed static/*
var staticFiles embed.FS
type spaFileSystem struct {
root http.FileSystem
}
func (spa *spaFileSystem) Open(name string) (http.File, error) {
f, err := spa.root.Open(name)
if os.IsNotExist(err) {
return spa.root.Open("index.html")
}
return f, err
}
func initStatic(mux *http.ServeMux) {
staticFS, _ := fs.Sub(staticFiles, "static")
mux.Handle("/", http.FileServer(&spaFileSystem{http.FS(staticFS)}))
}

24
internal/web/web.go Normal file
View File

@@ -0,0 +1,24 @@
package web
import (
"net/http"
"git.kapelle.org/niklas/beerpong-elo/internal/repo"
"github.com/graphql-go/handler"
)
func CreateWebserver(repo repo.Repo) *http.ServeMux {
router := http.NewServeMux()
schema := createShema(repo)
gqlHandler := handler.New(&handler.Config{
Schema: &schema,
Pretty: true,
GraphiQL: true,
})
router.Handle("/graphql", gqlHandler)
initStatic(router)
return router
}