diff options
| author | nsfisis <nsfisis@gmail.com> | 2026-02-15 11:32:38 +0900 |
|---|---|---|
| committer | nsfisis <nsfisis@gmail.com> | 2026-02-15 11:32:38 +0900 |
| commit | 557b238e88189e1314c39af82d77c94ba9dbd19e (patch) | |
| tree | 2014b2988fdc7ae90cce30159402ff3ac215725f /backend/api | |
| parent | a6b88139afc7c994ddb604757304d44214c00a90 (diff) | |
| download | phperkaigi-2026-albatross-557b238e88189e1314c39af82d77c94ba9dbd19e.tar.gz phperkaigi-2026-albatross-557b238e88189e1314c39af82d77c94ba9dbd19e.tar.zst phperkaigi-2026-albatross-557b238e88189e1314c39af82d77c94ba9dbd19e.zip | |
fix(backend): resolve TODO items for transactions, validation, and error handling
- Wrap multi-step DB operations in transactions (signup, submit, game
edit, task result processing)
- Add game running checks to PostGamePlayCode and PostGamePlaySubmit
- Hide ranking code when game is not yet finished
- Replace silenced errors in processTaskResults with slog.Error logging
- Add pgxpool.Pool to Handler/Hub structs for transaction support
Co-Authored-By: Claude Opus 4.6 <noreply@anthropic.com>
Diffstat (limited to 'backend/api')
| -rw-r--r-- | backend/api/handler.go | 90 | ||||
| -rw-r--r-- | backend/api/handler_wrapper.go | 5 |
2 files changed, 84 insertions, 11 deletions
diff --git a/backend/api/handler.go b/backend/api/handler.go index 05a185a..3fe7e3c 100644 --- a/backend/api/handler.go +++ b/backend/api/handler.go @@ -11,6 +11,7 @@ import ( "github.com/jackc/pgx/v5" "github.com/jackc/pgx/v5/pgtype" + "github.com/jackc/pgx/v5/pgxpool" "github.com/labstack/echo/v4" "github.com/oapi-codegen/nullable" @@ -21,6 +22,7 @@ import ( type Handler struct { q *db.Queries + pool *pgxpool.Pool hub GameHubInterface conf *config.Config } @@ -45,7 +47,7 @@ func (r postLoginCookieResponse) VisitPostLoginResponse(w http.ResponseWriter) e func (h *Handler) PostLogin(ctx context.Context, request PostLoginRequestObject) (PostLoginResponseObject, error) { username := request.Body.Username password := request.Body.Password - userID, err := auth.Login(ctx, h.q, username, password) + userID, err := auth.Login(ctx, h.q, h.pool, username, password) if err != nil { slog.Error("login failed", "error", err) var msg string @@ -321,6 +323,18 @@ func (h *Handler) GetGameWatchLatestStates(ctx context.Context, request GetGameW func (h *Handler) GetGameWatchRanking(ctx context.Context, request GetGameWatchRankingRequestObject, _ *db.User) (GetGameWatchRankingResponseObject, error) { gameID := request.GameID + + gameRow, err := h.q.GetGameByID(ctx, int32(gameID)) + if err != nil { + if errors.Is(err, pgx.ErrNoRows) { + return GetGameWatchRanking404JSONResponse{ + Message: "Game not found", + }, nil + } + return nil, echo.NewHTTPError(http.StatusInternalServerError, err.Error()) + } + gameFinished := isGameFinished(gameRow) + rows, err := h.q.GetRanking(ctx, int32(gameID)) if err != nil { if errors.Is(err, pgx.ErrNoRows) { @@ -329,8 +343,12 @@ func (h *Handler) GetGameWatchRanking(ctx context.Context, request GetGameWatchR } ranking := make([]RankingEntry, len(rows)) for i, row := range rows { - // TODO: check if game is finished. - code := &row.Submission.Code + var code nullable.Nullable[string] + if gameFinished { + code = nullable.NewNullableWithValue(row.Submission.Code) + } else { + code = nullable.NewNullNullable[string]() + } ranking[i] = RankingEntry{ Player: User{ UserID: int(row.User.UserID), @@ -342,7 +360,7 @@ func (h *Handler) GetGameWatchRanking(ctx context.Context, request GetGameWatchR }, Score: int(row.Submission.CodeSize), SubmittedAt: row.Submission.CreatedAt.Time.Unix(), - Code: toNullable(code), + Code: code, } } return GetGameWatchRanking200JSONResponse{ @@ -352,8 +370,23 @@ func (h *Handler) GetGameWatchRanking(ctx context.Context, request GetGameWatchR func (h *Handler) PostGamePlayCode(ctx context.Context, request PostGamePlayCodeRequestObject, user *db.User) (PostGamePlayCodeResponseObject, error) { gameID := request.GameID - // TODO: check if the game is running - err := h.q.UpdateCode(ctx, db.UpdateCodeParams{ + + gameRow, err := h.q.GetGameByID(ctx, int32(gameID)) + if err != nil { + if errors.Is(err, pgx.ErrNoRows) { + return PostGamePlayCode404JSONResponse{ + Message: "Game not found", + }, nil + } + return nil, echo.NewHTTPError(http.StatusInternalServerError, err.Error()) + } + if !isGameRunning(gameRow) { + return PostGamePlayCode403JSONResponse{ + Message: "Game is not running", + }, nil + } + + err = h.q.UpdateCode(ctx, db.UpdateCodeParams{ GameID: int32(gameID), UserID: user.UserID, Code: request.Body.Code, @@ -379,9 +412,25 @@ func (h *Handler) PostGamePlaySubmit(ctx context.Context, request PostGamePlaySu language := gameRow.Language codeSize := h.hub.CalcCodeSize(code, language) - // TODO: check if the game is running - // TODO: transaction - err = h.q.UpdateCodeAndStatus(ctx, db.UpdateCodeAndStatusParams{ + + if !isGameRunning(gameRow) { + return PostGamePlaySubmit403JSONResponse{ + Message: "Game is not running", + }, nil + } + + tx, err := h.pool.Begin(ctx) + if err != nil { + return nil, echo.NewHTTPError(http.StatusInternalServerError, err.Error()) + } + defer func() { + if err := tx.Rollback(ctx); err != nil && err != pgx.ErrTxClosed { + slog.Error("failed to rollback transaction", "error", err) + } + }() + + qtx := h.q.WithTx(tx) + err = qtx.UpdateCodeAndStatus(ctx, db.UpdateCodeAndStatusParams{ GameID: int32(gameID), UserID: user.UserID, Code: code, @@ -390,7 +439,7 @@ func (h *Handler) PostGamePlaySubmit(ctx context.Context, request PostGamePlaySu if err != nil { return nil, echo.NewHTTPError(http.StatusInternalServerError, err.Error()) } - submissionID, err := h.q.CreateSubmission(ctx, db.CreateSubmissionParams{ + submissionID, err := qtx.CreateSubmission(ctx, db.CreateSubmissionParams{ GameID: int32(gameID), UserID: user.UserID, Code: code, @@ -399,6 +448,11 @@ func (h *Handler) PostGamePlaySubmit(ctx context.Context, request PostGamePlaySu if err != nil { return nil, echo.NewHTTPError(http.StatusInternalServerError, err.Error()) } + + if err := tx.Commit(ctx); err != nil { + return nil, echo.NewHTTPError(http.StatusInternalServerError, err.Error()) + } + err = h.hub.EnqueueTestTasks(ctx, int(submissionID), gameID, int(user.UserID), language, code) if err != nil { return nil, echo.NewHTTPError(http.StatusInternalServerError, err.Error()) @@ -500,6 +554,22 @@ func (h *Handler) GetTournament(ctx context.Context, request GetTournamentReques }, nil } +func isGameRunning(game db.GetGameByIDRow) bool { + if !game.StartedAt.Valid { + return false + } + endTime := game.StartedAt.Time.Add(time.Duration(game.DurationSeconds) * time.Second) + return time.Now().Before(endTime) +} + +func isGameFinished(game db.GetGameByIDRow) bool { + if !game.StartedAt.Valid { + return false + } + endTime := game.StartedAt.Time.Add(time.Duration(game.DurationSeconds) * time.Second) + return !time.Now().Before(endTime) +} + func toNullable[T any](p *T) nullable.Nullable[T] { if p == nil { return nullable.NewNullNullable[T]() diff --git a/backend/api/handler_wrapper.go b/backend/api/handler_wrapper.go index 8e3e8cd..7448d13 100644 --- a/backend/api/handler_wrapper.go +++ b/backend/api/handler_wrapper.go @@ -5,6 +5,8 @@ package api import ( "context" + "github.com/jackc/pgx/v5/pgxpool" + "albatross-2026-backend/config" "albatross-2026-backend/db" ) @@ -15,10 +17,11 @@ type HandlerWrapper struct { impl Handler } -func NewHandler(queries *db.Queries, hub GameHubInterface, conf *config.Config) *HandlerWrapper { +func NewHandler(queries *db.Queries, pool *pgxpool.Pool, hub GameHubInterface, conf *config.Config) *HandlerWrapper { return &HandlerWrapper{ impl: Handler{ q: queries, + pool: pool, hub: hub, conf: conf, }, |
