626 lines
24 KiB
Go
626 lines
24 KiB
Go
package postgres
|
|
|
|
import (
|
|
"context"
|
|
"crypto/sha256"
|
|
"encoding/hex"
|
|
"encoding/json"
|
|
"errors"
|
|
"fmt"
|
|
"os"
|
|
"path/filepath"
|
|
"sort"
|
|
"strings"
|
|
"time"
|
|
|
|
"github.com/jackc/pgx/v5"
|
|
"github.com/jackc/pgx/v5/pgxpool"
|
|
|
|
"mixmaker/backend/internal/application"
|
|
"mixmaker/backend/internal/domain"
|
|
)
|
|
|
|
type Store struct{ pool *pgxpool.Pool }
|
|
|
|
func Open(ctx context.Context, url string) (*Store, error) {
|
|
pool, err := pgxpool.New(ctx, url)
|
|
if err != nil {
|
|
return nil, err
|
|
}
|
|
if err := pool.Ping(ctx); err != nil {
|
|
pool.Close()
|
|
return nil, err
|
|
}
|
|
return &Store{pool: pool}, nil
|
|
}
|
|
|
|
func (s *Store) Close() { s.pool.Close() }
|
|
func (s *Store) Ready(ctx context.Context) error { return s.pool.Ping(ctx) }
|
|
|
|
func HashToken(token string) string {
|
|
sum := sha256.Sum256([]byte(token))
|
|
return hex.EncodeToString(sum[:])
|
|
}
|
|
|
|
func (s *Store) UpsertDiscordAccount(ctx context.Context, account domain.Account, admin bool) (domain.Account, domain.Player, error) {
|
|
tx, err := s.pool.Begin(ctx)
|
|
if err != nil {
|
|
return domain.Account{}, domain.Player{}, err
|
|
}
|
|
defer tx.Rollback(ctx)
|
|
role := domain.RolePlayer
|
|
if admin {
|
|
role = domain.RoleAdmin
|
|
}
|
|
err = tx.QueryRow(ctx, `INSERT INTO accounts(id,discord_id,username,avatar_url,role,created_at)
|
|
VALUES($1,$2,$3,$4,$5,$6) ON CONFLICT(discord_id) DO UPDATE
|
|
SET username=excluded.username,avatar_url=excluded.avatar_url,
|
|
role=CASE WHEN accounts.role='admin' THEN accounts.role ELSE excluded.role END
|
|
RETURNING id,discord_id,username,avatar_url,role,created_at`,
|
|
account.ID, account.DiscordID, account.Username, account.AvatarURL, role, account.CreatedAt).
|
|
Scan(&account.ID, &account.DiscordID, &account.Username, &account.AvatarURL, &account.Role, &account.CreatedAt)
|
|
if err != nil {
|
|
return domain.Account{}, domain.Player{}, err
|
|
}
|
|
p := domain.Player{ID: application.NewID(), AccountID: account.ID, DisplayName: account.Username, Ratings: domain.Ratings{Tank: 13, Damage: 13, Support: 13}, CreatedAt: account.CreatedAt, UpdatedAt: account.CreatedAt}
|
|
var preferredRoles, preferredPlayers, avoidedPlayers []byte
|
|
err = tx.QueryRow(ctx, `INSERT INTO players(id,account_id,display_name,tank_rating,damage_rating,support_rating,created_at,updated_at)
|
|
VALUES($1,$2,$3,$4,$5,$6,$7,$8) ON CONFLICT(account_id) DO UPDATE SET account_id=excluded.account_id
|
|
RETURNING id,account_id,display_name,tank_rating,damage_rating,support_rating,preferred_roles,preferred_player_ids,avoided_player_ids,created_at,updated_at`,
|
|
p.ID, p.AccountID, p.DisplayName, p.Ratings.Tank, p.Ratings.Damage, p.Ratings.Support, p.CreatedAt, p.UpdatedAt).
|
|
Scan(&p.ID, &p.AccountID, &p.DisplayName, &p.Ratings.Tank, &p.Ratings.Damage, &p.Ratings.Support, &preferredRoles, &preferredPlayers, &avoidedPlayers, &p.CreatedAt, &p.UpdatedAt)
|
|
if err != nil {
|
|
return domain.Account{}, domain.Player{}, err
|
|
}
|
|
if err = json.Unmarshal(preferredRoles, &p.PreferredRoles); err != nil {
|
|
return domain.Account{}, domain.Player{}, err
|
|
}
|
|
if err = json.Unmarshal(preferredPlayers, &p.PreferredPlayerIDs); err != nil {
|
|
return domain.Account{}, domain.Player{}, err
|
|
}
|
|
if err = json.Unmarshal(avoidedPlayers, &p.AvoidedPlayerIDs); err != nil {
|
|
return domain.Account{}, domain.Player{}, err
|
|
}
|
|
return account, p, tx.Commit(ctx)
|
|
}
|
|
|
|
func (s *Store) CreateSession(ctx context.Context, token, accountID string, expires time.Time) error {
|
|
_, err := s.pool.Exec(ctx, `INSERT INTO sessions(token_hash,account_id,expires_at) VALUES($1,$2,$3)`, HashToken(token), accountID, expires)
|
|
return err
|
|
}
|
|
|
|
func (s *Store) DeleteSession(ctx context.Context, token string) error {
|
|
_, err := s.pool.Exec(ctx, `DELETE FROM sessions WHERE token_hash=$1`, HashToken(token))
|
|
return err
|
|
}
|
|
|
|
func (s *Store) AccountBySession(ctx context.Context, token string) (domain.Account, domain.Player, error) {
|
|
var a domain.Account
|
|
var p domain.Player
|
|
var preferredRoles, preferredPlayers, avoidedPlayers []byte
|
|
err := s.pool.QueryRow(ctx, `SELECT a.id,a.discord_id,a.username,a.avatar_url,a.role,a.created_at,
|
|
p.id,p.account_id,p.display_name,p.tank_rating,p.damage_rating,p.support_rating,p.preferred_roles,p.preferred_player_ids,p.avoided_player_ids,p.created_at,p.updated_at
|
|
FROM sessions s JOIN accounts a ON a.id=s.account_id JOIN players p ON p.account_id=a.id
|
|
WHERE s.token_hash=$1 AND s.expires_at>now()`, HashToken(token)).
|
|
Scan(&a.ID, &a.DiscordID, &a.Username, &a.AvatarURL, &a.Role, &a.CreatedAt,
|
|
&p.ID, &p.AccountID, &p.DisplayName, &p.Ratings.Tank, &p.Ratings.Damage, &p.Ratings.Support, &preferredRoles, &preferredPlayers, &avoidedPlayers, &p.CreatedAt, &p.UpdatedAt)
|
|
if err == nil {
|
|
err = json.Unmarshal(preferredRoles, &p.PreferredRoles)
|
|
}
|
|
if err == nil {
|
|
err = json.Unmarshal(preferredPlayers, &p.PreferredPlayerIDs)
|
|
}
|
|
if err == nil {
|
|
err = json.Unmarshal(avoidedPlayers, &p.AvoidedPlayerIDs)
|
|
}
|
|
return a, p, mapError(err)
|
|
}
|
|
|
|
func (s *Store) UpdatePlayer(ctx context.Context, p domain.Player) (domain.Player, error) {
|
|
preferredRoles, _ := json.Marshal(p.PreferredRoles)
|
|
preferredPlayers, _ := json.Marshal(p.PreferredPlayerIDs)
|
|
avoidedPlayers, _ := json.Marshal(p.AvoidedPlayerIDs)
|
|
err := s.pool.QueryRow(ctx, `UPDATE players SET display_name=$2,tank_rating=$3,damage_rating=$4,support_rating=$5,
|
|
preferred_roles=$6,preferred_player_ids=$7,avoided_player_ids=$8,updated_at=$9
|
|
WHERE id=$1 RETURNING id,account_id,display_name,tank_rating,damage_rating,support_rating,created_at,updated_at`,
|
|
p.ID, p.DisplayName, p.Ratings.Tank, p.Ratings.Damage, p.Ratings.Support, preferredRoles, preferredPlayers, avoidedPlayers, p.UpdatedAt).
|
|
Scan(&p.ID, &p.AccountID, &p.DisplayName, &p.Ratings.Tank, &p.Ratings.Damage, &p.Ratings.Support, &p.CreatedAt, &p.UpdatedAt)
|
|
return p, mapError(err)
|
|
}
|
|
|
|
func (s *Store) ListPlayers(ctx context.Context) ([]domain.Player, error) {
|
|
rows, err := s.pool.Query(ctx, `SELECT id,COALESCE(account_id,''),display_name,tank_rating,damage_rating,support_rating,
|
|
preferred_roles,preferred_player_ids,avoided_player_ids,created_at,updated_at FROM players ORDER BY id`)
|
|
if err != nil {
|
|
return nil, err
|
|
}
|
|
defer rows.Close()
|
|
out := make([]domain.Player, 0)
|
|
for rows.Next() {
|
|
var p domain.Player
|
|
var preferredRoles, preferredPlayers, avoidedPlayers []byte
|
|
if err := rows.Scan(&p.ID, &p.AccountID, &p.DisplayName, &p.Ratings.Tank, &p.Ratings.Damage, &p.Ratings.Support,
|
|
&preferredRoles, &preferredPlayers, &avoidedPlayers, &p.CreatedAt, &p.UpdatedAt); err != nil {
|
|
return nil, err
|
|
}
|
|
if err := json.Unmarshal(preferredRoles, &p.PreferredRoles); err != nil {
|
|
return nil, err
|
|
}
|
|
if err := json.Unmarshal(preferredPlayers, &p.PreferredPlayerIDs); err != nil {
|
|
return nil, err
|
|
}
|
|
if err := json.Unmarshal(avoidedPlayers, &p.AvoidedPlayerIDs); err != nil {
|
|
return nil, err
|
|
}
|
|
out = append(out, p)
|
|
}
|
|
return out, rows.Err()
|
|
}
|
|
|
|
func (s *Store) CreateParticipant(ctx context.Context, p domain.Player, r domain.RSVP) (domain.Player, domain.RSVP, error) {
|
|
tx, err := s.pool.Begin(ctx)
|
|
if err != nil {
|
|
return domain.Player{}, domain.RSVP{}, err
|
|
}
|
|
defer tx.Rollback(ctx)
|
|
var preferredRoles, preferredPlayers, avoidedPlayers []byte
|
|
err = tx.QueryRow(ctx, `INSERT INTO players(id,account_id,display_name,tank_rating,damage_rating,support_rating,created_at,updated_at)
|
|
VALUES($1,NULL,$2,$3,$4,$5,$6,$7)
|
|
RETURNING id,COALESCE(account_id,''),display_name,tank_rating,damage_rating,support_rating,preferred_roles,preferred_player_ids,avoided_player_ids,created_at,updated_at`,
|
|
p.ID, p.DisplayName, p.Ratings.Tank, p.Ratings.Damage, p.Ratings.Support, p.CreatedAt, p.UpdatedAt).
|
|
Scan(&p.ID, &p.AccountID, &p.DisplayName, &p.Ratings.Tank, &p.Ratings.Damage, &p.Ratings.Support, &preferredRoles, &preferredPlayers, &avoidedPlayers, &p.CreatedAt, &p.UpdatedAt)
|
|
if err == nil {
|
|
err = json.Unmarshal(preferredRoles, &p.PreferredRoles)
|
|
}
|
|
if err == nil {
|
|
err = json.Unmarshal(preferredPlayers, &p.PreferredPlayerIDs)
|
|
}
|
|
if err == nil {
|
|
err = json.Unmarshal(avoidedPlayers, &p.AvoidedPlayerIDs)
|
|
}
|
|
if err == nil {
|
|
err = tx.QueryRow(ctx, `INSERT INTO rsvps(event_id,player_id,status,actor_account_id,source,updated_at)
|
|
VALUES($1,$2,$3,$4,$5,$6)
|
|
RETURNING event_id,player_id,status,actor_account_id,source,updated_at`,
|
|
r.EventID, r.PlayerID, r.Status, r.ActorAccountID, r.Source, r.UpdatedAt).
|
|
Scan(&r.EventID, &r.PlayerID, &r.Status, &r.ActorAccountID, &r.Source, &r.UpdatedAt)
|
|
}
|
|
if err == nil {
|
|
err = tx.Commit(ctx)
|
|
}
|
|
if err != nil {
|
|
return domain.Player{}, domain.RSVP{}, mapError(err)
|
|
}
|
|
return p, r, nil
|
|
}
|
|
|
|
func (s *Store) CreateEvent(ctx context.Context, e domain.Event) (domain.Event, error) {
|
|
err := s.pool.QueryRow(ctx, `INSERT INTO events(id,name,description,starts_at,ends_at,registration_deadline,created_by,created_at,updated_at,state,version,ruleset_id)
|
|
VALUES($1,$2,$3,$4,$5,$6,$7,$8,$9,$10,$11,$12)
|
|
RETURNING id,name,description,starts_at,ends_at,registration_deadline,created_by,created_at,updated_at,state,version,ruleset_id,active_series_id,tournament_id`,
|
|
e.ID, e.Name, e.Description, e.StartsAt, e.EndsAt, e.RegistrationDeadline, e.CreatedBy, e.CreatedAt, e.UpdatedAt, e.State, e.Version, e.RulesetID).
|
|
Scan(&e.ID, &e.Name, &e.Description, &e.StartsAt, &e.EndsAt, &e.RegistrationDeadline, &e.CreatedBy, &e.CreatedAt, &e.UpdatedAt,
|
|
&e.State, &e.Version, &e.RulesetID, &e.ActiveSeriesID, &e.TournamentID)
|
|
return e, err
|
|
}
|
|
|
|
func (s *Store) GetEvent(ctx context.Context, id string) (domain.Event, error) {
|
|
var event domain.Event
|
|
err := s.pool.QueryRow(ctx, `SELECT id,name,description,starts_at,ends_at,registration_deadline,created_by,created_at,updated_at,state,version,ruleset_id,active_series_id,tournament_id
|
|
FROM events WHERE id=$1`, id).
|
|
Scan(&event.ID, &event.Name, &event.Description, &event.StartsAt, &event.EndsAt, &event.RegistrationDeadline, &event.CreatedBy, &event.CreatedAt, &event.UpdatedAt,
|
|
&event.State, &event.Version, &event.RulesetID, &event.ActiveSeriesID, &event.TournamentID)
|
|
return event, mapError(err)
|
|
}
|
|
|
|
func (s *Store) UpdateEvent(ctx context.Context, event domain.Event) (domain.Event, error) {
|
|
err := s.pool.QueryRow(ctx, `UPDATE events
|
|
SET name=$2,description=$3,starts_at=$4,ends_at=$5,registration_deadline=$6,updated_at=$7
|
|
WHERE id=$1
|
|
RETURNING id,name,description,starts_at,ends_at,registration_deadline,created_by,created_at,updated_at,state,version,ruleset_id,active_series_id,tournament_id`,
|
|
event.ID, event.Name, event.Description, event.StartsAt, event.EndsAt, event.RegistrationDeadline, event.UpdatedAt).
|
|
Scan(&event.ID, &event.Name, &event.Description, &event.StartsAt, &event.EndsAt, &event.RegistrationDeadline, &event.CreatedBy, &event.CreatedAt, &event.UpdatedAt,
|
|
&event.State, &event.Version, &event.RulesetID, &event.ActiveSeriesID, &event.TournamentID)
|
|
return event, mapError(err)
|
|
}
|
|
|
|
func (s *Store) ListEvents(ctx context.Context, from time.Time) ([]domain.Event, error) {
|
|
rows, err := s.pool.Query(ctx, `SELECT id,name,description,starts_at,ends_at,registration_deadline,created_by,created_at,updated_at,state,version,ruleset_id,active_series_id,tournament_id FROM events WHERE ends_at >= $1 ORDER BY starts_at`, from)
|
|
if err != nil {
|
|
return nil, err
|
|
}
|
|
defer rows.Close()
|
|
out := make([]domain.Event, 0)
|
|
for rows.Next() {
|
|
var e domain.Event
|
|
if err := rows.Scan(&e.ID, &e.Name, &e.Description, &e.StartsAt, &e.EndsAt, &e.RegistrationDeadline, &e.CreatedBy, &e.CreatedAt, &e.UpdatedAt,
|
|
&e.State, &e.Version, &e.RulesetID, &e.ActiveSeriesID, &e.TournamentID); err != nil {
|
|
return nil, err
|
|
}
|
|
out = append(out, e)
|
|
}
|
|
return out, rows.Err()
|
|
}
|
|
|
|
func (s *Store) SaveEventWorkflow(ctx context.Context, event domain.Event, expectedVersion int) (domain.Event, error) {
|
|
tag, err := s.pool.Exec(ctx, `UPDATE events SET state=$2,version=$3,ruleset_id=$4,active_series_id=$5,tournament_id=$6,updated_at=$7
|
|
WHERE id=$1 AND version=$8`, event.ID, event.State, event.Version, event.RulesetID, event.ActiveSeriesID, event.TournamentID, event.UpdatedAt, expectedVersion)
|
|
if err == nil && tag.RowsAffected() == 0 {
|
|
return domain.Event{}, domain.ErrConflict
|
|
}
|
|
if err != nil {
|
|
return domain.Event{}, err
|
|
}
|
|
return s.GetEvent(ctx, event.ID)
|
|
}
|
|
|
|
func (s *Store) DeleteEvent(ctx context.Context, eventID string) error {
|
|
tx, err := s.pool.Begin(ctx)
|
|
if err != nil {
|
|
return err
|
|
}
|
|
defer tx.Rollback(ctx)
|
|
rows, err := tx.Query(ctx, `SELECT p.id FROM players p JOIN rsvps r ON r.player_id=p.id
|
|
WHERE r.event_id=$1 AND p.account_id IS NULL`, eventID)
|
|
if err != nil {
|
|
return err
|
|
}
|
|
guestIDs := make([]string, 0)
|
|
for rows.Next() {
|
|
var id string
|
|
if err = rows.Scan(&id); err != nil {
|
|
rows.Close()
|
|
return err
|
|
}
|
|
guestIDs = append(guestIDs, id)
|
|
}
|
|
rows.Close()
|
|
if err = rows.Err(); err != nil {
|
|
return err
|
|
}
|
|
if _, err = tx.Exec(ctx, `DELETE FROM audit_log WHERE subject_id=$1
|
|
OR subject_id IN (SELECT id FROM teams WHERE event_id=$1)
|
|
OR subject_id IN (SELECT id FROM series WHERE body->>'eventId'=$1)
|
|
OR subject_id IN (SELECT id FROM tournaments WHERE event_id=$1)`, eventID); err != nil {
|
|
return err
|
|
}
|
|
if _, err = tx.Exec(ctx, `DELETE FROM series WHERE body->>'eventId'=$1`, eventID); err != nil {
|
|
return err
|
|
}
|
|
tag, err := tx.Exec(ctx, `DELETE FROM events WHERE id=$1`, eventID)
|
|
if err != nil {
|
|
return err
|
|
}
|
|
if tag.RowsAffected() == 0 {
|
|
return domain.ErrNotFound
|
|
}
|
|
for _, guestID := range guestIDs {
|
|
if _, err = tx.Exec(ctx, `DELETE FROM players WHERE id=$1 AND account_id IS NULL
|
|
AND NOT EXISTS (SELECT 1 FROM rsvps WHERE player_id=$1)`, guestID); err != nil {
|
|
return err
|
|
}
|
|
}
|
|
return tx.Commit(ctx)
|
|
}
|
|
|
|
func (s *Store) SaveRoster(ctx context.Context, roster domain.RosterDraft, expectedVersion int) (domain.RosterDraft, error) {
|
|
body, _ := json.Marshal(roster)
|
|
var err error
|
|
if expectedVersion < 0 {
|
|
_, err = s.pool.Exec(ctx, `INSERT INTO event_rosters(event_id,body,version) VALUES($1,$2,$3)
|
|
ON CONFLICT(event_id) DO UPDATE SET body=excluded.body,version=excluded.version,updated_at=now()`,
|
|
roster.EventID, body, roster.Version)
|
|
} else {
|
|
tag, execErr := s.pool.Exec(ctx, `UPDATE event_rosters SET body=$2,version=$3,updated_at=now() WHERE event_id=$1 AND version=$4`,
|
|
roster.EventID, body, roster.Version, expectedVersion)
|
|
err = execErr
|
|
if execErr == nil && tag.RowsAffected() == 0 {
|
|
err = domain.ErrConflict
|
|
}
|
|
}
|
|
return roster, err
|
|
}
|
|
|
|
func (s *Store) GetRoster(ctx context.Context, eventID string) (domain.RosterDraft, error) {
|
|
var roster domain.RosterDraft
|
|
var body []byte
|
|
err := s.pool.QueryRow(ctx, `SELECT body FROM event_rosters WHERE event_id=$1`, eventID).Scan(&body)
|
|
if err == nil {
|
|
err = json.Unmarshal(body, &roster)
|
|
}
|
|
return roster, mapError(err)
|
|
}
|
|
|
|
func (s *Store) StartScrim(ctx context.Context, event domain.Event, expectedVersion int, series []domain.Series, tournament *domain.Tournament) error {
|
|
tx, err := s.pool.Begin(ctx)
|
|
if err != nil {
|
|
return err
|
|
}
|
|
defer tx.Rollback(ctx)
|
|
for _, item := range series {
|
|
body, _ := json.Marshal(item)
|
|
if _, err = tx.Exec(ctx, `INSERT INTO series(id,tournament_id,body,version) VALUES($1,$2,$3,$4)`,
|
|
item.ID, item.TournamentID, body, item.Version); err != nil {
|
|
return mapError(err)
|
|
}
|
|
}
|
|
if tournament != nil {
|
|
body, _ := json.Marshal(tournament)
|
|
if _, err = tx.Exec(ctx, `INSERT INTO tournaments(id,event_id,body) VALUES($1,$2,$3)`,
|
|
tournament.ID, tournament.EventID, body); err != nil {
|
|
return mapError(err)
|
|
}
|
|
}
|
|
tag, err := tx.Exec(ctx, `UPDATE events SET state=$2,version=$3,active_series_id=$4,tournament_id=$5,updated_at=$6
|
|
WHERE id=$1 AND version=$7`, event.ID, event.State, event.Version, event.ActiveSeriesID, event.TournamentID, event.UpdatedAt, expectedVersion)
|
|
if err != nil {
|
|
return err
|
|
}
|
|
if tag.RowsAffected() == 0 {
|
|
return domain.ErrConflict
|
|
}
|
|
return tx.Commit(ctx)
|
|
}
|
|
|
|
func (s *Store) UpsertRSVP(ctx context.Context, r domain.RSVP) (domain.RSVP, error) {
|
|
err := s.pool.QueryRow(ctx, `INSERT INTO rsvps(event_id,player_id,status,actor_account_id,source,updated_at)
|
|
VALUES($1,$2,$3,$4,$5,$6) ON CONFLICT(event_id,player_id) DO UPDATE
|
|
SET status=excluded.status,actor_account_id=excluded.actor_account_id,source=excluded.source,updated_at=excluded.updated_at
|
|
RETURNING event_id,player_id,status,actor_account_id,source,updated_at`,
|
|
r.EventID, r.PlayerID, r.Status, r.ActorAccountID, r.Source, r.UpdatedAt).
|
|
Scan(&r.EventID, &r.PlayerID, &r.Status, &r.ActorAccountID, &r.Source, &r.UpdatedAt)
|
|
return r, err
|
|
}
|
|
|
|
func (s *Store) ListRSVPs(ctx context.Context, eventID string) ([]domain.RSVP, error) {
|
|
rows, err := s.pool.Query(ctx, `SELECT event_id,player_id,status,actor_account_id,source,updated_at FROM rsvps WHERE event_id=$1 ORDER BY player_id`, eventID)
|
|
if err != nil {
|
|
return nil, err
|
|
}
|
|
defer rows.Close()
|
|
out := make([]domain.RSVP, 0)
|
|
for rows.Next() {
|
|
var r domain.RSVP
|
|
if err := rows.Scan(&r.EventID, &r.PlayerID, &r.Status, &r.ActorAccountID, &r.Source, &r.UpdatedAt); err != nil {
|
|
return nil, err
|
|
}
|
|
out = append(out, r)
|
|
}
|
|
return out, rows.Err()
|
|
}
|
|
|
|
func (s *Store) DeleteRSVP(ctx context.Context, eventID, playerID string) error {
|
|
tag, err := s.pool.Exec(ctx, `DELETE FROM rsvps WHERE event_id=$1 AND player_id=$2`, eventID, playerID)
|
|
if err == nil && tag.RowsAffected() == 0 {
|
|
return domain.ErrNotFound
|
|
}
|
|
return err
|
|
}
|
|
|
|
func (s *Store) SaveTeams(ctx context.Context, eventID string, teams []domain.Team) error {
|
|
tx, err := s.pool.Begin(ctx)
|
|
if err != nil {
|
|
return err
|
|
}
|
|
defer tx.Rollback(ctx)
|
|
if _, err := tx.Exec(ctx, `DELETE FROM teams WHERE event_id=$1`, eventID); err != nil {
|
|
return err
|
|
}
|
|
for _, t := range teams {
|
|
body, _ := json.Marshal(t.Slots)
|
|
if _, err := tx.Exec(ctx, `INSERT INTO teams(id,event_id,name,captain_player_id,slots) VALUES($1,$2,$3,NULLIF($4,''),$5)`, t.ID, eventID, t.Name, t.CaptainPlayerID, body); err != nil {
|
|
return err
|
|
}
|
|
}
|
|
return tx.Commit(ctx)
|
|
}
|
|
|
|
func (s *Store) ListTeams(ctx context.Context, eventID string) ([]domain.Team, error) {
|
|
rows, err := s.pool.Query(ctx, `SELECT id,event_id,name,COALESCE(captain_player_id,''),slots FROM teams WHERE event_id=$1 ORDER BY id`, eventID)
|
|
if err != nil {
|
|
return nil, err
|
|
}
|
|
defer rows.Close()
|
|
out := make([]domain.Team, 0)
|
|
for rows.Next() {
|
|
var t domain.Team
|
|
var body []byte
|
|
if err := rows.Scan(&t.ID, &t.EventID, &t.Name, &t.CaptainPlayerID, &body); err != nil {
|
|
return nil, err
|
|
}
|
|
if err := json.Unmarshal(body, &t.Slots); err != nil {
|
|
return nil, err
|
|
}
|
|
out = append(out, t)
|
|
}
|
|
return out, rows.Err()
|
|
}
|
|
|
|
func (s *Store) AssignCaptain(ctx context.Context, teamID, playerID string) (domain.Team, error) {
|
|
var eventID string
|
|
if err := s.pool.QueryRow(ctx, `SELECT event_id FROM teams WHERE id=$1 AND slots @> $2::jsonb`, teamID, fmt.Sprintf(`[{"playerId":%q}]`, playerID)).Scan(&eventID); err != nil {
|
|
return domain.Team{}, mapError(err)
|
|
}
|
|
if _, err := s.pool.Exec(ctx, `UPDATE teams SET captain_player_id=$2 WHERE id=$1`, teamID, playerID); err != nil {
|
|
return domain.Team{}, err
|
|
}
|
|
teams, err := s.ListTeams(ctx, eventID)
|
|
for _, t := range teams {
|
|
if t.ID == teamID {
|
|
return t, err
|
|
}
|
|
}
|
|
return domain.Team{}, domain.ErrNotFound
|
|
}
|
|
|
|
func (s *Store) SaveRuleset(ctx context.Context, r domain.Ruleset) (domain.Ruleset, error) {
|
|
body, _ := json.Marshal(r)
|
|
_, err := s.pool.Exec(ctx, `INSERT INTO rulesets(id,name,body) VALUES($1,$2,$3)
|
|
ON CONFLICT(id) DO UPDATE SET name=excluded.name,body=excluded.body`, r.ID, r.Name, body)
|
|
return r, err
|
|
}
|
|
|
|
func (s *Store) ListRulesets(ctx context.Context) ([]domain.Ruleset, error) {
|
|
rows, err := s.pool.Query(ctx, `SELECT body FROM rulesets ORDER BY name`)
|
|
if err != nil {
|
|
return nil, err
|
|
}
|
|
defer rows.Close()
|
|
rulesets := make([]domain.Ruleset, 0)
|
|
for rows.Next() {
|
|
var body []byte
|
|
var ruleset domain.Ruleset
|
|
if err := rows.Scan(&body); err != nil {
|
|
return nil, err
|
|
}
|
|
if err := json.Unmarshal(body, &ruleset); err != nil {
|
|
return nil, err
|
|
}
|
|
rulesets = append(rulesets, ruleset)
|
|
}
|
|
return rulesets, rows.Err()
|
|
}
|
|
|
|
func (s *Store) GetRuleset(ctx context.Context, id string) (domain.Ruleset, error) {
|
|
var body []byte
|
|
var r domain.Ruleset
|
|
err := s.pool.QueryRow(ctx, `SELECT body FROM rulesets WHERE id=$1`, id).Scan(&body)
|
|
if err == nil {
|
|
err = json.Unmarshal(body, &r)
|
|
}
|
|
return r, mapError(err)
|
|
}
|
|
|
|
func (s *Store) SaveSeries(ctx context.Context, series domain.Series) (domain.Series, error) {
|
|
body, _ := json.Marshal(series)
|
|
tag, err := s.pool.Exec(ctx, `INSERT INTO series(id,tournament_id,body,version) VALUES($1,$2,$3,$4)
|
|
ON CONFLICT(id) DO UPDATE SET body=excluded.body,version=excluded.version
|
|
WHERE series.version=excluded.version-1`, series.ID, series.TournamentID, body, series.Version)
|
|
if err == nil && tag.RowsAffected() == 0 {
|
|
err = domain.ErrConflict
|
|
}
|
|
return series, err
|
|
}
|
|
|
|
func (s *Store) GetSeries(ctx context.Context, id string) (domain.Series, error) {
|
|
var body []byte
|
|
var out domain.Series
|
|
err := s.pool.QueryRow(ctx, `SELECT body FROM series WHERE id=$1`, id).Scan(&body)
|
|
if err == nil {
|
|
err = json.Unmarshal(body, &out)
|
|
}
|
|
return out, mapError(err)
|
|
}
|
|
|
|
func (s *Store) SaveTournament(ctx context.Context, t domain.Tournament) (domain.Tournament, error) {
|
|
body, _ := json.Marshal(t)
|
|
_, err := s.pool.Exec(ctx, `INSERT INTO tournaments(id,event_id,body) VALUES($1,$2,$3)
|
|
ON CONFLICT(id) DO UPDATE SET body=excluded.body`, t.ID, t.EventID, body)
|
|
return t, err
|
|
}
|
|
|
|
func (s *Store) GetTournament(ctx context.Context, id string) (domain.Tournament, error) {
|
|
var body []byte
|
|
var out domain.Tournament
|
|
err := s.pool.QueryRow(ctx, `SELECT body FROM tournaments WHERE id=$1`, id).Scan(&body)
|
|
if err == nil {
|
|
err = json.Unmarshal(body, &out)
|
|
}
|
|
return out, mapError(err)
|
|
}
|
|
|
|
func (s *Store) GetTournamentByEvent(ctx context.Context, eventID string) (domain.Tournament, error) {
|
|
var body []byte
|
|
var out domain.Tournament
|
|
err := s.pool.QueryRow(ctx, `SELECT body FROM tournaments WHERE event_id=$1 ORDER BY id DESC LIMIT 1`, eventID).Scan(&body)
|
|
if err == nil {
|
|
err = json.Unmarshal(body, &out)
|
|
}
|
|
return out, mapError(err)
|
|
}
|
|
|
|
func (s *Store) SaveDraft(ctx context.Context, id, kind string, value any, expectedVersion int) error {
|
|
body, _ := json.Marshal(value)
|
|
if expectedVersion < 0 {
|
|
_, err := s.pool.Exec(ctx, `INSERT INTO draft_states(id,kind,body,version) VALUES($1,$2,$3,0)`, id, kind, body)
|
|
return err
|
|
}
|
|
tag, err := s.pool.Exec(ctx, `UPDATE draft_states SET body=$3,version=version+1,updated_at=now()
|
|
WHERE id=$1 AND kind=$2 AND version=$4`, id, kind, body, expectedVersion)
|
|
if err == nil && tag.RowsAffected() == 0 {
|
|
return domain.ErrConflict
|
|
}
|
|
return err
|
|
}
|
|
|
|
func (s *Store) GetDraft(ctx context.Context, id string, target any) (string, int, error) {
|
|
var kind string
|
|
var version int
|
|
var body []byte
|
|
err := s.pool.QueryRow(ctx, `SELECT kind,version,body FROM draft_states WHERE id=$1`, id).Scan(&kind, &version, &body)
|
|
if err == nil {
|
|
err = json.Unmarshal(body, target)
|
|
}
|
|
return kind, version, mapError(err)
|
|
}
|
|
|
|
func (s *Store) AppendAudit(ctx context.Context, actor, action, subject string, payload any) error {
|
|
body, _ := json.Marshal(payload)
|
|
_, err := s.pool.Exec(ctx, `INSERT INTO audit_log(actor_account_id,action,subject_id,payload) VALUES($1,$2,$3,$4)`, actor, action, subject, body)
|
|
return err
|
|
}
|
|
|
|
func Migrate(ctx context.Context, poolURL, directory string) error {
|
|
pool, err := pgxpool.New(ctx, poolURL)
|
|
if err != nil {
|
|
return err
|
|
}
|
|
defer pool.Close()
|
|
if _, err = pool.Exec(ctx, `CREATE TABLE IF NOT EXISTS schema_migrations(version text PRIMARY KEY, applied_at timestamptz NOT NULL DEFAULT now())`); err != nil {
|
|
return err
|
|
}
|
|
files, err := filepath.Glob(filepath.Join(directory, "*.sql"))
|
|
if err != nil {
|
|
return err
|
|
}
|
|
sort.Strings(files)
|
|
for _, file := range files {
|
|
version := filepath.Base(file)
|
|
var exists bool
|
|
if err := pool.QueryRow(ctx, `SELECT EXISTS(SELECT 1 FROM schema_migrations WHERE version=$1)`, version).Scan(&exists); err != nil {
|
|
return err
|
|
}
|
|
if exists {
|
|
continue
|
|
}
|
|
body, err := os.ReadFile(file)
|
|
if err != nil {
|
|
return err
|
|
}
|
|
up := strings.Split(string(body), "-- +mixmaker Down")[0]
|
|
tx, err := pool.Begin(ctx)
|
|
if err != nil {
|
|
return err
|
|
}
|
|
if _, err = tx.Exec(ctx, up); err == nil {
|
|
_, err = tx.Exec(ctx, `INSERT INTO schema_migrations(version) VALUES($1)`, version)
|
|
}
|
|
if err == nil {
|
|
err = tx.Commit(ctx)
|
|
} else {
|
|
_ = tx.Rollback(ctx)
|
|
}
|
|
if err != nil {
|
|
return fmt.Errorf("apply %s: %w", version, err)
|
|
}
|
|
}
|
|
return nil
|
|
}
|
|
|
|
func mapError(err error) error {
|
|
if errors.Is(err, pgx.ErrNoRows) {
|
|
return domain.ErrNotFound
|
|
}
|
|
return err
|
|
}
|