diff --git a/handlers/actions.go b/handlers/actions.go index 967fea7..3536780 100644 --- a/handlers/actions.go +++ b/handlers/actions.go @@ -75,13 +75,13 @@ func createRoom(ctx context.Context, req *models.RoomReq) (*models.Room, error) // repo.CreateRoom() // } -func saveFullInfo(fi *models.FullInfo) error { +func saveFullInfo(ctx context.Context, fi *models.FullInfo) error { // INFO: no transactions; so case is possible where first object is updated but the second is not - if err := repo.PlayerUpdate(fi.State); err != nil { + if err := repo.PlayerUpdate(ctx, fi.State); err != nil { return err } log.Debug("saved user state", "state", fi.State) - if err := repo.RoomUpdate(context.Background(), fi.Room); err != nil { + if err := repo.RoomUpdate(ctx, fi.Room); err != nil { return err } return nil @@ -155,7 +155,7 @@ func getPlayerByCtx(ctx context.Context) (*models.Player, error) { log.Debug("no username in ctx") return &models.Player{}, errors.New("no username in ctx") } - return repo.PlayerGetByName(username) + return repo.PlayerGetByName(ctx, username) } // // DEPRECATED @@ -219,7 +219,7 @@ func joinTeam(ctx context.Context, role, team string) (*models.FullInfo, error) err := errors.New("uknown role:" + role) return nil, err } - if err := saveFullInfo(fi); err != nil { + if err := saveFullInfo(ctx, fi); err != nil { return nil, err } return fi, nil @@ -247,7 +247,7 @@ func joinTeam(ctx context.Context, role, team string) (*models.FullInfo, error) // get bots func listBots() []models.Player { - bots, err := repo.PlayerList(true) + bots, err := repo.PlayerList(context.Background(), true) if err != nil { log.Error("failed to fetch bots from db", "error", err) } diff --git a/handlers/actions_test.go b/handlers/actions_test.go deleted file mode 100644 index 47f413c..0000000 --- a/handlers/actions_test.go +++ /dev/null @@ -1,107 +0,0 @@ -package handlers - -import ( - "encoding/json" - "gralias/models" - "os" - "testing" -) - -func TestSaveState(t *testing.T) { - // Create test state - state := &models.UserState{ - Username: "testuser", - RoomID: "testroom", - Team: models.UserTeamBlue, - Role: models.UserRoleMime, - } - - // Save state - err := saveState(state.Username, state) - if err != nil { - t.Fatalf("saveState failed: %v", err) - } - - // Load state - loadedState, err := loadState(state.Username) - if err != nil { - t.Fatalf("loadState failed: %v", err) - } - - // Verify loaded state matches original - if loadedState.Username != state.Username { - t.Errorf("Username mismatch: got %s, want %s", loadedState.Username, state.Username) - } - if loadedState.RoomID != state.RoomID { - t.Errorf("RoomID mismatch: got %s, want %s", loadedState.RoomID, state.RoomID) - } - if loadedState.Team != state.Team { - t.Errorf("Team mismatch: got %s, want %s", loadedState.Team, state.Team) - } - if loadedState.Role != state.Role { - t.Errorf("Role mismatch: got %s, want %s", loadedState.Role, state.Role) - } - - // Test JSON serialization/deserialization - data, err := json.Marshal(state) - if err != nil { - t.Fatalf("marshal failed: %v", err) - } - testMap := make(map[string][]byte) - testMap["testkey"] = data - - // Create a temporary file - tmpFile, err := os.CreateTemp("", "test_store_*.json") - if err != nil { - t.Fatalf("failed to create temp file: %v", err) - } - tmpFileName := tmpFile.Name() - // defer os.Remove(tmpFileName) - - // Write testMap to the temp file - fileData, err := json.Marshal(testMap) - if err != nil { - t.Fatalf("failed to marshal testMap: %v", err) - } - if err := os.WriteFile(tmpFileName, fileData, 0644); err != nil { - t.Fatalf("failed to write to temp file: %v", err) - } - - // Read the temp file - readData, err := os.ReadFile(tmpFileName) - if err != nil { - t.Fatalf("failed to read temp file: %v", err) - } - - // Unmarshal the data - var testMapRead map[string][]byte - if err := json.Unmarshal(readData, &testMapRead); err != nil { - t.Fatalf("failed to unmarshal testMap: %v", err) - } - - // Get the state bytes from the map - stateBytes, ok := testMapRead["testkey"] - if !ok { - t.Fatalf("key 'testkey' not found in testMapRead") - } - - // Unmarshal the state bytes - stateRead := &models.UserState{} - if err := json.Unmarshal(stateBytes, stateRead); err != nil { - t.Fatalf("failed to unmarshal state: %v", err) - } - - // Compare the state - if stateRead.Username != state.Username { - t.Errorf("Username mismatch from file: got %s, want %s", stateRead.Username, state.Username) - } - if stateRead.RoomID != state.RoomID { - t.Errorf("RoomID mismatch from file: got %s, want %s", stateRead.RoomID, state.RoomID) - } - if stateRead.Team != state.Team { - t.Errorf("Team mismatch from file: got %s, want %s", stateRead.Team, state.Team) - } - if stateRead.Role != state.Role { - t.Errorf("Role mismatch from file: got %s, want %s", stateRead.Role, state.Role) - } -} diff --git a/handlers/auth.go b/handlers/auth.go index 8c0e645..dd052bf 100644 --- a/handlers/auth.go +++ b/handlers/auth.go @@ -37,7 +37,7 @@ func HandleNameCheck(w http.ResponseWriter, r *http.Request) { } cleanName := utils.RemoveSpacesFromStr(username) // allNames := getAllNames() - allNames, err := repo.PlayerListNames() + allNames, err := repo.PlayerListNames(r.Context()) if err != nil { abortWithError(w, err.Error()) return @@ -86,7 +86,7 @@ func HandleFrontLogin(w http.ResponseWriter, r *http.Request) { http.SetCookie(w, cookie) // check if that user was already in db // userstate, err := loadState(cleanName) - userstate, err := repo.PlayerGetByName(cleanName) + userstate, err := repo.PlayerGetByName(r.Context(), cleanName) if err != nil || userstate == nil { userstate = models.InitPlayer(cleanName) } @@ -106,10 +106,10 @@ func HandleFrontLogin(w http.ResponseWriter, r *http.Request) { fi.Room = room fi.List = nil fi.State.RoomID = room.ID - repo.PlayerSetRoomID(fi.State.Username, room.ID) + repo.PlayerSetRoomID(r.Context(), fi.State.Username, room.ID) // repo.RoomUpdate() // save full info instead - // if err := saveFullInfo(fi); err != nil { + // if err := saveFullInfo(r.Context(), fi); err != nil { // abortWithError(w, err.Error()) // return // } @@ -123,8 +123,8 @@ func HandleFrontLogin(w http.ResponseWriter, r *http.Request) { } // save state to cache // if err := saveState(cleanName, userstate); err != nil { - if err := repo.PlayerUpdate(userstate); err != nil { - // if err := saveFullInfo(fi); err != nil { + if err := repo.PlayerUpdate(r.Context(), userstate); err != nil { + // if err := saveFullInfo(r.Context(), fi); err != nil { log.Error("failed to save state", "error", err) abortWithError(w, err.Error()) return diff --git a/handlers/elements.go b/handlers/elements.go index 3532b5e..a1ec17f 100644 --- a/handlers/elements.go +++ b/handlers/elements.go @@ -157,7 +157,7 @@ func HandleShowColor(w http.ResponseWriter, r *http.Request) { fi.Room.ClearMarks() } } - if err := saveFullInfo(fi); err != nil { + if err := saveFullInfo(r.Context(), fi); err != nil { abortWithError(w, err.Error()) return } @@ -221,7 +221,7 @@ func HandleMarkCard(w http.ResponseWriter, r *http.Request) { fi.Room.Cards[i].Mark = newMarks cardword = fi.Room.Cards[i] } - if err := saveFullInfo(fi); err != nil { + if err := saveFullInfo(r.Context(), fi); err != nil { abortWithError(w, err.Error()) return } diff --git a/handlers/game.go b/handlers/game.go index e81f854..04a2c84 100644 --- a/handlers/game.go +++ b/handlers/game.go @@ -46,11 +46,11 @@ func HandleCreateRoom(w http.ResponseWriter, r *http.Request) { abortWithError(w, err.Error()) return } - if err := repo.PlayerSetRoomID(fi.State.Username, room.ID); err != nil { + if err := repo.PlayerSetRoomID(r.Context(), fi.State.Username, room.ID); err != nil { abortWithError(w, err.Error()) return } - // if err := saveFullInfo(fi); err != nil { + // if err := saveFullInfo(r.Context(), fi); err != nil { // msg := "failed to set current room to session" // log.Error(msg, "error", err) // abortWithError(w, msg) @@ -131,7 +131,7 @@ func HandleEndTurn(w http.ResponseWriter, r *http.Request) { fi.Room.ChangeTurn() fi.Room.MimeDone = false StopTurnTimer(fi.Room.ID) - if err := saveFullInfo(fi); err != nil { + if err := saveFullInfo(r.Context(), fi); err != nil { abortWithError(w, err.Error()) return } @@ -174,7 +174,7 @@ func HandleStartGame(w http.ResponseWriter, r *http.Request) { Action: models.ActionTypeGameStarted, } fi.Room.ActionHistory = append(fi.Room.ActionHistory, action) - if err := saveFullInfo(fi); err != nil { + if err := saveFullInfo(r.Context(), fi); err != nil { abortWithError(w, err.Error()) return } @@ -226,7 +226,7 @@ func HandleJoinRoom(w http.ResponseWriter, r *http.Request) { fi.State.RoomID = room.ID fi.Room = room fi.List = nil - if err := saveFullInfo(fi); err != nil { + if err := saveFullInfo(r.Context(), fi); err != nil { abortWithError(w, err.Error()) return } @@ -297,7 +297,7 @@ func HandleGiveClue(w http.ResponseWriter, r *http.Request) { log.Debug("given clue", "clue", clue, "limit", fi.Room.ThisTurnLimit) notify(models.NotifyBacklogPrefix+fi.Room.ID, clue+num) notifyBotIfNeeded(fi.Room) - if err := saveFullInfo(fi); err != nil { + if err := saveFullInfo(r.Context(), fi); err != nil { abortWithError(w, err.Error()) return } diff --git a/handlers/handlers.go b/handlers/handlers.go index 72d2e68..11b929b 100644 --- a/handlers/handlers.go +++ b/handlers/handlers.go @@ -103,11 +103,14 @@ func HandleExit(w http.ResponseWriter, r *http.Request) { notify(models.NotifyRoomListUpdate, "") } // scary to update the whole room - if err := repo.RoomUpdate(r.Context(), exitedRoom); err != nil { + fiToSave := &models.FullInfo{ + Room: exitedRoom, + } + if err := saveFullInfo(r.Context(), fiToSave); err != nil { abortWithError(w, err.Error()) return } - if err := repo.PlayerExitRoom(fi.State.Username); err != nil { + if err := repo.PlayerExitRoom(r.Context(), fi.State.Username); err != nil { abortWithError(w, err.Error()) return } diff --git a/models/main.go b/models/main.go index bc481cd..672f3db 100644 --- a/models/main.go +++ b/models/main.go @@ -15,6 +15,8 @@ type ( ) const ( + // Context keys + TxContextKey = "tx" // UserTeam UserTeamBlue = "blue" UserTeamRed = "red" @@ -384,10 +386,13 @@ type WordCard struct { // table: settings type GameSettings struct { + ID uint32 `json:"id" db:"id"` + RoomID string `db:"room_id"` Language string `json:"language" example:"en" form:"language" db:"language"` RoomPass string `json:"room_pass" db:"room_pass"` TurnSecondsLeft uint32 `db:"-"` - RoundTime uint32 `json:"round_time" db:"round_time"` + RoundTime uint32 `json:"round_time" db:"turn_time"` + CreatedAt time.Time `db:"created_at"` } // ===== diff --git a/repos/actions.go b/repos/actions.go index 27da40a..54f1933 100644 --- a/repos/actions.go +++ b/repos/actions.go @@ -4,6 +4,8 @@ import ( "context" "gralias/models" "time" + + "github.com/jmoiron/sqlx" ) type ActionsRepo interface { @@ -15,7 +17,7 @@ type ActionsRepo interface { func (p *RepoProvider) ListActions(ctx context.Context, roomID string) ([]models.Action, error) { actions := []models.Action{} - err := p.DB.SelectContext(ctx, &actions, `SELECT actor, actor_color, action_type, word, word_color, number_associated, created_at FROM actions WHERE room_id = ? ORDER BY created_at ASC`, roomID) + err := sqlx.SelectContext(ctx, p.DB, &actions, `SELECT actor, actor_color, action_type, word, word_color, number_associated, created_at FROM actions WHERE room_id = ? ORDER BY created_at ASC`, roomID) if err != nil { return nil, err } @@ -26,13 +28,14 @@ func (p *RepoProvider) ListActions(ctx context.Context, roomID string) ([]models } func (p *RepoProvider) CreateAction(ctx context.Context, roomID string, a *models.Action) error { - _, err := p.DB.ExecContext(ctx, `INSERT INTO actions (room_id, actor, actor_color, action_type, word, word_color, number_associated, created_at) VALUES (?, ?, ?, ?, ?, ?, ?, ?)`, roomID, a.Actor, a.ActorColor, a.Action, a.Word, a.WordColor, a.Number, a.CreatedAt.UnixNano()) + db := getDB(ctx, p.DB) + _, err := db.ExecContext(ctx, `INSERT INTO actions (room_id, actor, actor_color, action_type, word, word_color, number_associated, created_at) VALUES (?, ?, ?, ?, ?, ?, ?, ?)`, roomID, a.Actor, a.ActorColor, a.Action, a.Word, a.WordColor, a.Number, a.CreatedAt.UnixNano()) return err } func (p *RepoProvider) GetLastClue(ctx context.Context, roomID string) (*models.Action, error) { action := &models.Action{} - err := p.DB.GetContext(ctx, action, `SELECT actor, actor_color, action_type, word, word_color, number_associated, created_at FROM actions WHERE room_id = ? AND action_type = 'gave_clue' ORDER BY created_at DESC LIMIT 1`, roomID) + err := sqlx.GetContext(ctx, p.DB, action, `SELECT actor, actor_color, action_type, word, word_color, number_associated, created_at FROM actions WHERE room_id = ? AND action_type = 'gave_clue' ORDER BY created_at DESC LIMIT 1`, roomID) if err != nil { return nil, err } @@ -41,6 +44,7 @@ func (p *RepoProvider) GetLastClue(ctx context.Context, roomID string) (*models. } func (p *RepoProvider) DeleteActionsByRoomID(ctx context.Context, roomID string) error { - _, err := p.DB.ExecContext(ctx, `DELETE FROM actions WHERE room_id = ?`, roomID) + db := getDB(ctx, p.DB) + _, err := db.ExecContext(ctx, `DELETE FROM actions WHERE room_id = ?`, roomID) return err -} \ No newline at end of file +} diff --git a/repos/main.go b/repos/main.go index ba30738..3d4efb3 100644 --- a/repos/main.go +++ b/repos/main.go @@ -1,6 +1,7 @@ package repos import ( + "context" "log/slog" "os" @@ -12,11 +13,11 @@ type AllRepos interface { RoomsRepo ActionsRepo PlayersRepo + SessionsRepo } type RepoProvider struct { - DB *sqlx.DB - Ext sqlx.Ext + DB *sqlx.DB } func NewRepoProvider(pathToDB string) *RepoProvider { @@ -30,3 +31,11 @@ func NewRepoProvider(pathToDB string) *RepoProvider { DB: db, } } + +func getDB(ctx context.Context, db *sqlx.DB) sqlx.ExtContext { + if tx, ok := ctx.Value("tx").(*sqlx.Tx); + ok { + return tx + } + return db +} diff --git a/repos/players.go b/repos/players.go index 09531f5..a4fb7ce 100644 --- a/repos/players.go +++ b/repos/players.go @@ -3,65 +3,72 @@ package repos import ( "context" "gralias/models" + + "github.com/jmoiron/sqlx" ) type PlayersRepo interface { - PlayerGetByName(username string) (*models.Player, error) - PlayerAdd(player *models.Player) error - PlayerUpdate(player *models.Player) error - PlayerDelete(roomID, username string) error - PlayerSetRoomID(username, roomID string) error - PlayerExitRoom(username string) error - PlayerListNames() ([]string, error) - PlayerList(isBot bool) ([]models.Player, error) + PlayerGetByName(ctx context.Context, username string) (*models.Player, error) + PlayerAdd(ctx context.Context, player *models.Player) error + PlayerUpdate(ctx context.Context, player *models.Player) error + PlayerDelete(ctx context.Context, roomID, username string) error + PlayerSetRoomID(ctx context.Context, username, roomID string) error + PlayerExitRoom(ctx context.Context, username string) error + PlayerListNames(ctx context.Context) ([]string, error) + PlayerList(ctx context.Context, isBot bool) ([]models.Player, error) } -func (p *RepoProvider) PlayerListNames() ([]string, error) { +func (p *RepoProvider) PlayerListNames(ctx context.Context) ([]string, error) { var names []string - err := p.DB.SelectContext(context.Background(), &names, "SELECT username FROM players;") + err := sqlx.SelectContext(ctx, p.DB, &names, "SELECT username FROM players;") if err != nil { return nil, err } return names, nil } -func (p *RepoProvider) PlayerGetByName(username string) (*models.Player, error) { +func (p *RepoProvider) PlayerGetByName(ctx context.Context, username string) (*models.Player, error) { var player models.Player - err := p.DB.GetContext(context.Background(), &player, "SELECT id, room_id, username, team, role, is_bot FROM players WHERE username = ?", username) + err := sqlx.GetContext(ctx, p.DB, &player, "SELECT id, room_id, username, team, role, is_bot FROM players WHERE username = ?", username) if err != nil { return nil, err } return &player, nil } -func (p *RepoProvider) PlayerAdd(player *models.Player) error { - _, err := p.DB.ExecContext(context.Background(), "INSERT INTO players (room_id, username, team, role, is_bot) VALUES (?, ?, ?, ?, ?)", +func (p *RepoProvider) PlayerAdd(ctx context.Context, player *models.Player) error { + db := getDB(ctx, p.DB) + _, err := db.ExecContext(ctx, "INSERT INTO players (room_id, username, team, role, is_bot) VALUES (?, ?, ?, ?, ?)", player.RoomID, player.Username, player.Team, player.Role, player.IsBot) return err } -func (p *RepoProvider) PlayerUpdate(player *models.Player) error { - _, err := p.DB.ExecContext(context.Background(), "UPDATE players SET room_id = ?, username = ?, team = ?, role = ?, is_bot = ? WHERE id = ?", +func (p *RepoProvider) PlayerUpdate(ctx context.Context, player *models.Player) error { + db := getDB(ctx, p.DB) + _, err := db.ExecContext(ctx, "UPDATE players SET room_id = ?, username = ?, team = ?, role = ?, is_bot = ? WHERE id = ?", player.RoomID, player.Username, player.Team, player.Role, player.IsBot, player.ID) return err } -func (p *RepoProvider) PlayerDelete(roomID, username string) error { - _, err := p.DB.ExecContext(context.Background(), "DELETE FROM players WHERE room_id = ? AND username = ?", roomID, username) +func (p *RepoProvider) PlayerDelete(ctx context.Context, roomID, username string) error { + db := getDB(ctx, p.DB) + _, err := db.ExecContext(ctx, "DELETE FROM players WHERE room_id = ? AND username = ?", roomID, username) return err } -func (p *RepoProvider) PlayerSetRoomID(username, roomID string) error { - _, err := p.DB.ExecContext(context.Background(), "UPDATE players SET room_id = ? WHERE username = ?", roomID, username) +func (p *RepoProvider) PlayerSetRoomID(ctx context.Context, username, roomID string) error { + db := getDB(ctx, p.DB) + _, err := db.ExecContext(ctx, "UPDATE players SET room_id = ? WHERE username = ?", roomID, username) return err } -func (p *RepoProvider) PlayerExitRoom(username string) error { - _, err := p.DB.ExecContext(context.Background(), "UPDATE players SET room_id = null WHERE username = ?", username) +func (p *RepoProvider) PlayerExitRoom(ctx context.Context, username string) error { + db := getDB(ctx, p.DB) + _, err := db.ExecContext(ctx, "UPDATE players SET room_id = null WHERE username = ?", username) return err } -func (p *RepoProvider) PlayerList(isBot bool) ([]models.Player, error) { +func (p *RepoProvider) PlayerList(ctx context.Context, isBot bool) ([]models.Player, error) { var players []models.Player var query string if isBot { @@ -69,7 +76,7 @@ func (p *RepoProvider) PlayerList(isBot bool) ([]models.Player, error) { } else { query = "SELECT id, room_id, username, team, role, is_bot FROM players WHERE is_bot = false;" } - err := p.DB.SelectContext(context.Background(), &players, query) + err := sqlx.SelectContext(ctx, p.DB, &players, query) if err != nil { return nil, err } diff --git a/repos/players_test.go b/repos/players_test.go index 73bc9bd..bf8a919 100644 --- a/repos/players_test.go +++ b/repos/players_test.go @@ -1,6 +1,7 @@ package repos import ( + "context" "gralias/models" "testing" @@ -45,7 +46,7 @@ func TestPlayersRepo_AddPlayer(t *testing.T) { IsBot: false, } - err := repo.AddPlayer(player) + err := repo.PlayerAdd(context.Background(), player) assert.NoError(t, err) var retrievedPlayer models.Player @@ -71,7 +72,7 @@ func TestPlayersRepo_GetPlayer(t *testing.T) { _, err := db.Exec(`INSERT INTO players (room_id, username, team, role, is_bot) VALUES (?, ?, ?, ?, ?)`, player.RoomID, player.Username, player.Team, player.Role, player.IsBot) assert.NoError(t, err) - retrievedPlayer, err := repo.GetPlayer(player.RoomID, player.Username) + retrievedPlayer, err := repo.PlayerGetByName(context.Background(), player.Username) assert.NoError(t, err) assert.NotNil(t, retrievedPlayer) assert.Equal(t, player.Username, retrievedPlayer.Username) @@ -94,7 +95,7 @@ func TestPlayersRepo_DeletePlayer(t *testing.T) { _, err := db.Exec(`INSERT INTO players (room_id, username, team, role, is_bot) VALUES (?, ?, ?, ?, ?)`, player.RoomID, player.Username, player.Team, player.Role, player.IsBot) assert.NoError(t, err) - err = repo.DeletePlayer(player.RoomID, player.Username) + err = repo.PlayerDelete(context.Background(), player.RoomID, player.Username) assert.NoError(t, err) var count int diff --git a/repos/rooms.go b/repos/rooms.go index 3978d58..6f1435d 100644 --- a/repos/rooms.go +++ b/repos/rooms.go @@ -3,6 +3,8 @@ package repos import ( "context" "gralias/models" + + "github.com/jmoiron/sqlx" ) type RoomsRepo interface { @@ -16,7 +18,7 @@ type RoomsRepo interface { func (p *RepoProvider) RoomList(ctx context.Context) ([]*models.Room, error) { rooms := []*models.Room{} - err := p.DB.SelectContext(ctx, &rooms, `SELECT * FROM rooms`) + err := sqlx.SelectContext(ctx, p.DB, &rooms, `SELECT * FROM rooms`) if err != nil { return nil, err } @@ -25,37 +27,58 @@ func (p *RepoProvider) RoomList(ctx context.Context) ([]*models.Room, error) { func (p *RepoProvider) RoomGetByID(ctx context.Context, id string) (*models.Room, error) { room := &models.Room{} - err := p.DB.GetContext(ctx, room, `SELECT * FROM rooms WHERE id = ?`, id) + err := sqlx.GetContext(ctx, p.DB, room, `SELECT * FROM rooms WHERE id = ?`, id) if err != nil { return nil, err } + settings := &models.GameSettings{} + err = sqlx.GetContext(ctx, p.DB, settings, `SELECT * FROM settings WHERE room_id = ?`, id) + if err != nil { + return nil, err + } + room.Settings = *settings return room, nil } func (p *RepoProvider) RoomCreate(ctx context.Context, r *models.Room) error { - _, err := p.DB.ExecContext(ctx, `INSERT INTO rooms (id, created_at, creator_name, team_turn, this_turn_limit, opened_this_turn, blue_counter, red_counter, red_turn, mime_done, , is_running, is_over, team_won, room_link) VALUES (?, ?, ?, ?, ?, ?, ?, ?, ?, ?, ?, ?, ?, ?, ?, ?, ?)`, r.ID, r.CreatedAt, r.CreatorName, r.TeamTurn, r.ThisTurnLimit, r.OpenedThisTurn, r.BlueCounter, r.RedCounter, r.RedTurn, r.MimeDone, r.IsRunning, r.IsOver, r.TeamWon, r.RoomLink) + db := getDB(ctx, p.DB) + _, err := db.ExecContext(ctx, `INSERT INTO rooms (id, created_at, creator_name, team_turn, this_turn_limit, opened_this_turn, blue_counter, red_counter, red_turn, mime_done, is_running, is_over, team_won, room_link) VALUES (?, ?, ?, ?, ?, ?, ?, ?, ?, ?, ?, ?, ?, ?)`, r.ID, r.CreatedAt, r.CreatorName, r.TeamTurn, r.ThisTurnLimit, r.OpenedThisTurn, r.BlueCounter, r.RedCounter, r.RedTurn, r.MimeDone, r.IsRunning, r.IsOver, r.TeamWon, r.RoomLink) + if err != nil { + return err + } + _, err = db.ExecContext(ctx, `INSERT INTO settings (room_id, language, room_pass, turn_time) VALUES (?, ?, ?, ?)`, r.ID, r.Settings.Language, r.Settings.RoomPass, r.Settings.RoundTime) return err } func (p *RepoProvider) RoomDeleteByID(ctx context.Context, id string) error { - _, err := p.DB.ExecContext(ctx, `DELETE FROM rooms WHERE id = ?`, id) + db := getDB(ctx, p.DB) + _, err := db.ExecContext(ctx, `DELETE FROM rooms WHERE id = ?`, id) + if err != nil { + return err + } + _, err = db.ExecContext(ctx, `DELETE FROM settings WHERE room_id = ?`, id) return err } func (p *RepoProvider) RoomUpdate(ctx context.Context, r *models.Room) error { - _, err := p.DB.ExecContext(ctx, `UPDATE rooms SET team_turn = ?, this_turn_limit = ?, opened_this_turn = ?, blue_counter = ?, red_counter = ?, red_turn = ?, mime_done = ?, = ?, is_running = ?, is_over = ?, team_won = ?, room_link = ? WHERE id = ?`, r.TeamTurn, r.ThisTurnLimit, r.OpenedThisTurn, r.BlueCounter, r.RedCounter, r.RedTurn, r.MimeDone, r.IsRunning, r.IsOver, r.TeamWon, r.RoomLink, r.ID) + db := getDB(ctx, p.DB) + _, err := db.ExecContext(ctx, `UPDATE rooms SET team_turn = ?, this_turn_limit = ?, opened_this_turn = ?, blue_counter = ?, red_counter = ?, red_turn = ?, mime_done = ?, is_running = ?, is_over = ?, team_won = ?, room_link = ? WHERE id = ?`, r.TeamTurn, r.ThisTurnLimit, r.OpenedThisTurn, r.BlueCounter, r.RedCounter, r.RedTurn, r.MimeDone, r.IsRunning, r.IsOver, r.TeamWon, r.RoomLink, r.ID) + if err != nil { + return err + } + _, err = db.ExecContext(ctx, `UPDATE settings SET language = ?, room_pass = ?, turn_time = ? WHERE room_id = ?`, r.Settings.Language, r.Settings.RoomPass, r.Settings.RoundTime, r.ID) return err } func (p *RepoProvider) RoomGetExtended(ctx context.Context, id string) (*models.Room, error) { room := &models.Room{} - err := p.DB.GetContext(ctx, room, `SELECT * FROM rooms WHERE id = ?`, id) + err := sqlx.GetContext(ctx, p.DB, room, `SELECT * FROM rooms WHERE id = ?`, id) if err != nil { return nil, err } // Get players players := []*models.Player{} - err = p.DB.SelectContext(ctx, &players, `SELECT * FROM players WHERE room_id = ?`, id) + err = sqlx.SelectContext(ctx, p.DB, &players, `SELECT * FROM players WHERE room_id = ?`, id) if err != nil { return nil, err } @@ -87,21 +110,21 @@ func (p *RepoProvider) RoomGetExtended(ctx context.Context, id string) (*models. } // Get word cards wordCards := []models.WordCard{} - err = p.DB.SelectContext(ctx, &wordCards, `SELECT * FROM word_cards WHERE room_id = ?`, id) + err = sqlx.SelectContext(ctx, p.DB, &wordCards, `SELECT * FROM word_cards WHERE room_id = ?`, id) if err != nil { return nil, err } room.Cards = wordCards // Get actions actions := []models.Action{} - err = p.DB.SelectContext(ctx, &actions, `SELECT * FROM actions WHERE room_id = ? ORDER BY created_at ASC`, id) + err = sqlx.SelectContext(ctx, p.DB, &actions, `SELECT * FROM actions WHERE room_id = ? ORDER BY created_at ASC`, id) if err != nil { return nil, err } room.ActionHistory = actions // Get settings settings := &models.GameSettings{} - err = p.DB.GetContext(ctx, settings, `SELECT * FROM settings WHERE room_id = ?`, id) + err = sqlx.GetContext(ctx, p.DB, settings, `SELECT * FROM settings WHERE room_id = ?`, id) if err != nil { return nil, err } diff --git a/repos/rooms_test.go b/repos/rooms_test.go index 32d0bfb..b022e16 100644 --- a/repos/rooms_test.go +++ b/repos/rooms_test.go @@ -18,22 +18,29 @@ func setupTestDB(t *testing.T) (*sqlx.DB, func()) { schema := ` CREATE TABLE IF NOT EXISTS rooms ( id TEXT PRIMARY KEY, - created_at DATETIME, - creator_name TEXT, - team_turn TEXT, - this_turn_limit INTEGER, - opened_this_turn INTEGER, - blue_counter INTEGER, - red_counter INTEGER, - red_turn BOOLEAN, - mime_done BOOLEAN, - is_public BOOLEAN, - is_running BOOLEAN, - language TEXT, - round_time INTEGER, - is_over BOOLEAN, - team_won TEXT, - room_pass TEXT + created_at TIMESTAMP NOT NULL DEFAULT CURRENT_TIMESTAMP, + creator_name TEXT NOT NULL, + team_turn TEXT NOT NULL DEFAULT '', + this_turn_limit INTEGER NOT NULL DEFAULT 0, + opened_this_turn INTEGER NOT NULL DEFAULT 0, + blue_counter INTEGER NOT NULL DEFAULT 0, + red_counter INTEGER NOT NULL DEFAULT 0, + red_turn BOOLEAN NOT NULL DEFAULT FALSE, + mime_done BOOLEAN NOT NULL DEFAULT FALSE, + is_running BOOLEAN NOT NULL DEFAULT FALSE, + is_over BOOLEAN NOT NULL DEFAULT FALSE, + team_won TEXT NOT NULL DEFAULT '', + room_link TEXT NOT NULL DEFAULT '' + ); + + CREATE TABLE IF NOT EXISTS settings ( + id INTEGER PRIMARY KEY AUTOINCREMENT, + room_id TEXT NOT NULL, + language TEXT NOT NULL DEFAULT 'en', + room_pass TEXT NOT NULL DEFAULT '', + turn_time INTEGER NOT NULL DEFAULT 60, + created_at TIMESTAMP NOT NULL DEFAULT CURRENT_TIMESTAMP, + FOREIGN KEY (room_id) REFERENCES rooms(id) ); ` _, err = db.Exec(schema) @@ -61,16 +68,18 @@ func TestRoomsRepo_CreateRoom(t *testing.T) { RedCounter: 0, RedTurn: false, MimeDone: false, - IsPublic: true, IsRunning: false, - Language: "en", - RoundTime: 60, IsOver: false, TeamWon: "", - RoomPass: "", + RoomLink: "", + Settings: models.GameSettings{ + Language: "en", + RoundTime: 60, + RoomPass: "", + }, } - err := repo.CreateRoom(context.Background(), room) + err := repo.RoomCreate(context.Background(), room) assert.NoError(t, err) // Verify the room was created @@ -79,6 +88,13 @@ func TestRoomsRepo_CreateRoom(t *testing.T) { assert.NoError(t, err) assert.Equal(t, room.ID, retrievedRoom.ID) assert.Equal(t, room.CreatorName, retrievedRoom.CreatorName) + + var retrievedSettings models.GameSettings + err = db.Get(&retrievedSettings, "SELECT id, language, room_pass, turn_time FROM settings WHERE room_id = ?", room.ID) + assert.NoError(t, err) + assert.Equal(t, room.Settings.Language, retrievedSettings.Language) + assert.Equal(t, room.Settings.RoundTime, retrievedSettings.RoundTime) + assert.Equal(t, room.Settings.RoomPass, retrievedSettings.RoomPass) } func TestRoomsRepo_GetRoomByID(t *testing.T) { @@ -98,24 +114,29 @@ func TestRoomsRepo_GetRoomByID(t *testing.T) { RedCounter: 0, RedTurn: true, MimeDone: false, - IsPublic: true, IsRunning: false, - Language: "en", - RoundTime: 60, IsOver: false, TeamWon: "", - RoomPass: "", + RoomLink: "", + Settings: models.GameSettings{ + Language: "en", + RoundTime: 60, + RoomPass: "", + }, } // Insert a room directly into the database for testing GetRoomByID - _, err := db.Exec(`INSERT INTO rooms (id, created_at, creator_name, team_turn, this_turn_limit, opened_this_turn, blue_counter, red_counter, red_turn, mime_done, is_public, is_running, language, round_time, is_over, team_won, room_pass) VALUES (?, ?, ?, ?, ?, ?, ?, ?, ?, ?, ?, ?, ?, ?, ?, ?, ?)`, room.ID, room.CreatedAt, room.CreatorName, room.TeamTurn, room.ThisTurnLimit, room.OpenedThisTurn, room.BlueCounter, room.RedCounter, room.RedTurn, room.MimeDone, room.IsPublic, room.IsRunning, room.Language, room.RoundTime, room.IsOver, room.TeamWon, room.RoomPass) + _, err := db.Exec(`INSERT INTO rooms (id, created_at, creator_name, team_turn, this_turn_limit, opened_this_turn, blue_counter, red_counter, red_turn, mime_done, is_running, is_over, team_won, room_link) VALUES (?, ?, ?, ?, ?, ?, ?, ?, ?, ?, ?, ?, ?, ?)`, room.ID, room.CreatedAt, room.CreatorName, room.TeamTurn, room.ThisTurnLimit, room.OpenedThisTurn, room.BlueCounter, room.RedCounter, room.RedTurn, room.MimeDone, room.IsRunning, room.IsOver, room.TeamWon, room.RoomLink) + assert.NoError(t, err) + _, err = db.Exec(`INSERT INTO settings (room_id, language, room_pass, turn_time) VALUES (?, ?, ?, ?)`, room.ID, room.Settings.Language, room.Settings.RoomPass, room.Settings.RoundTime) assert.NoError(t, err) - retrievedRoom, err := repo.GetRoomByID(context.Background(), room.ID) + retrievedRoom, err := repo.RoomGetByID(context.Background(), room.ID) assert.NoError(t, err) assert.NotNil(t, retrievedRoom) assert.Equal(t, room.ID, retrievedRoom.ID) assert.Equal(t, room.CreatorName, retrievedRoom.CreatorName) + assert.Equal(t, room.Settings.Language, retrievedRoom.Settings.Language) } func TestRoomsRepo_ListRooms(t *testing.T) { @@ -135,13 +156,15 @@ func TestRoomsRepo_ListRooms(t *testing.T) { RedCounter: 0, RedTurn: false, MimeDone: false, - IsPublic: true, IsRunning: false, - Language: "en", - RoundTime: 60, IsOver: false, TeamWon: "", - RoomPass: "", + RoomLink: "", + Settings: models.GameSettings{ + Language: "en", + RoundTime: 60, + RoomPass: "", + }, } room2 := &models.Room{ ID: "list_room_2", @@ -154,21 +177,28 @@ func TestRoomsRepo_ListRooms(t *testing.T) { RedCounter: 0, RedTurn: true, MimeDone: false, - IsPublic: true, IsRunning: false, - Language: "en", - RoundTime: 60, IsOver: false, TeamWon: "", - RoomPass: "", + RoomLink: "", + Settings: models.GameSettings{ + Language: "en", + RoundTime: 60, + RoomPass: "", + }, } - _, err := db.Exec(`INSERT INTO rooms (id, created_at, creator_name, team_turn, this_turn_limit, opened_this_turn, blue_counter, red_counter, red_turn, mime_done, is_public, is_running, language, round_time, is_over, team_won, room_pass) VALUES (?, ?, ?, ?, ?, ?, ?, ?, ?, ?, ?, ?, ?, ?, ?, ?, ?)`, room1.ID, room1.CreatedAt, room1.CreatorName, room1.TeamTurn, room1.ThisTurnLimit, room1.OpenedThisTurn, room1.BlueCounter, room1.RedCounter, room1.RedTurn, room1.MimeDone, room1.IsPublic, room1.IsRunning, room1.Language, room1.RoundTime, room1.IsOver, room1.TeamWon, room1.RoomPass) + _, err := db.Exec(`INSERT INTO rooms (id, created_at, creator_name, team_turn, this_turn_limit, opened_this_turn, blue_counter, red_counter, red_turn, mime_done, is_running, is_over, team_won, room_link) VALUES (?, ?, ?, ?, ?, ?, ?, ?, ?, ?, ?, ?, ?, ?)`, room1.ID, room1.CreatedAt, room1.CreatorName, room1.TeamTurn, room1.ThisTurnLimit, room1.OpenedThisTurn, room1.BlueCounter, room1.RedCounter, room1.RedTurn, room1.MimeDone, room1.IsRunning, room1.IsOver, room1.TeamWon, room1.RoomLink) assert.NoError(t, err) - _, err = db.Exec(`INSERT INTO rooms (id, created_at, creator_name, team_turn, this_turn_limit, opened_this_turn, blue_counter, red_counter, red_turn, mime_done, is_public, is_running, language, round_time, is_over, team_won, room_pass) VALUES (?, ?, ?, ?, ?, ?, ?, ?, ?, ?, ?, ?, ?, ?, ?, ?, ?)`, room2.ID, room2.CreatedAt, room2.CreatorName, room2.TeamTurn, room2.ThisTurnLimit, room2.OpenedThisTurn, room2.BlueCounter, room2.RedCounter, room2.RedTurn, room2.MimeDone, room2.IsPublic, room2.IsRunning, room2.Language, room2.RoundTime, room2.IsOver, room2.TeamWon, room2.RoomPass) + _, err = db.Exec(`INSERT INTO settings (room_id, language, room_pass, turn_time) VALUES (?, ?, ?, ?)`, room1.ID, room1.Settings.Language, room1.Settings.RoomPass, room1.Settings.RoundTime) assert.NoError(t, err) - rooms, err := repo.ListRooms(context.Background()) + _, err = db.Exec(`INSERT INTO rooms (id, created_at, creator_name, team_turn, this_turn_limit, opened_this_turn, blue_counter, red_counter, red_turn, mime_done, is_running, is_over, team_won, room_link) VALUES (?, ?, ?, ?, ?, ?, ?, ?, ?, ?, ?, ?, ?, ?)`, room2.ID, room2.CreatedAt, room2.CreatorName, room2.TeamTurn, room2.ThisTurnLimit, room2.OpenedThisTurn, room2.BlueCounter, room2.RedCounter, room2.RedTurn, room2.MimeDone, room2.IsRunning, room2.IsOver, room2.TeamWon, room2.RoomLink) + assert.NoError(t, err) + _, err = db.Exec(`INSERT INTO settings (room_id, language, room_pass, turn_time) VALUES (?, ?, ?, ?)`, room2.ID, room2.Settings.Language, room2.Settings.RoomPass, room2.Settings.RoundTime) + assert.NoError(t, err) + + rooms, err := repo.RoomList(context.Background()) assert.NoError(t, err) assert.Len(t, rooms, 2) } @@ -190,25 +220,33 @@ func TestRoomsRepo_DeleteRoomByID(t *testing.T) { RedCounter: 0, RedTurn: false, MimeDone: false, - IsPublic: true, IsRunning: false, - Language: "en", - RoundTime: 60, IsOver: false, TeamWon: "", - RoomPass: "", + RoomLink: "", + Settings: models.GameSettings{ + Language: "en", + RoundTime: 60, + RoomPass: "", + }, } - _, err := db.Exec(`INSERT INTO rooms (id, created_at, creator_name, team_turn, this_turn_limit, opened_this_turn, blue_counter, red_counter, red_turn, mime_done, is_public, is_running, language, round_time, is_over, team_won, room_pass) VALUES (?, ?, ?, ?, ?, ?, ?, ?, ?, ?, ?, ?, ?, ?, ?, ?, ?)`, room.ID, room.CreatedAt, room.CreatorName, room.TeamTurn, room.ThisTurnLimit, room.OpenedThisTurn, room.BlueCounter, room.RedCounter, room.RedTurn, room.MimeDone, room.IsPublic, room.IsRunning, room.Language, room.RoundTime, room.IsOver, room.TeamWon, room.RoomPass) + _, err := db.Exec(`INSERT INTO rooms (id, created_at, creator_name, team_turn, this_turn_limit, opened_this_turn, blue_counter, red_counter, red_turn, mime_done, is_running, is_over, team_won, room_link) VALUES (?, ?, ?, ?, ?, ?, ?, ?, ?, ?, ?, ?, ?, ?)`, room.ID, room.CreatedAt, room.CreatorName, room.TeamTurn, room.ThisTurnLimit, room.OpenedThisTurn, room.BlueCounter, room.RedCounter, room.RedTurn, room.MimeDone, room.IsRunning, room.IsOver, room.TeamWon, room.RoomLink) + assert.NoError(t, err) + _, err = db.Exec(`INSERT INTO settings (room_id, language, room_pass, turn_time) VALUES (?, ?, ?, ?)`, room.ID, room.Settings.Language, room.Settings.RoomPass, room.Settings.RoundTime) assert.NoError(t, err) - err = repo.DeleteRoomByID(context.Background(), room.ID) + err = repo.RoomDeleteByID(context.Background(), room.ID) assert.NoError(t, err) var count int err = db.Get(&count, "SELECT COUNT(*) FROM rooms WHERE id = ?", room.ID) assert.NoError(t, err) assert.Equal(t, 0, count) + + err = db.Get(&count, "SELECT COUNT(*) FROM settings WHERE room_id = ?", room.ID) + assert.NoError(t, err) + assert.Equal(t, 0, count) } func TestRoomsRepo_UpdateRoom(t *testing.T) { @@ -228,27 +266,37 @@ func TestRoomsRepo_UpdateRoom(t *testing.T) { RedCounter: 0, RedTurn: false, MimeDone: false, - IsPublic: true, IsRunning: false, - Language: "en", - RoundTime: 60, IsOver: false, TeamWon: "", - RoomPass: "", + RoomLink: "", + Settings: models.GameSettings{ + Language: "en", + RoundTime: 60, + RoomPass: "", + }, } - _, err := db.Exec(`INSERT INTO rooms (id, created_at, creator_name, team_turn, this_turn_limit, opened_this_turn, blue_counter, red_counter, red_turn, mime_done, is_public, is_running, language, round_time, is_over, team_won, room_pass) VALUES (?, ?, ?, ?, ?, ?, ?, ?, ?, ?, ?, ?, ?, ?, ?, ?, ?)`, room.ID, room.CreatedAt, room.CreatorName, room.TeamTurn, room.ThisTurnLimit, room.OpenedThisTurn, room.BlueCounter, room.RedCounter, room.RedTurn, room.MimeDone, room.IsPublic, room.IsRunning, room.Language, room.RoundTime, room.IsOver, room.TeamWon, room.RoomPass) + _, err := db.Exec(`INSERT INTO rooms (id, created_at, creator_name, team_turn, this_turn_limit, opened_this_turn, blue_counter, red_counter, red_turn, mime_done, is_running, is_over, team_won, room_link) VALUES (?, ?, ?, ?, ?, ?, ?, ?, ?, ?, ?, ?, ?, ?)`, room.ID, room.CreatedAt, room.CreatorName, room.TeamTurn, room.ThisTurnLimit, room.OpenedThisTurn, room.BlueCounter, room.RedCounter, room.RedTurn, room.MimeDone, room.IsRunning, room.IsOver, room.TeamWon, room.RoomLink) + assert.NoError(t, err) + _, err = db.Exec(`INSERT INTO settings (room_id, language, room_pass, turn_time) VALUES (?, ?, ?, ?)`, room.ID, room.Settings.Language, room.Settings.RoomPass, room.Settings.RoundTime) assert.NoError(t, err) room.TeamTurn = "red" room.BlueCounter = 10 + room.Settings.RoundTime = 120 - err = repo.UpdateRoom(context.Background(), room) + err = repo.RoomUpdate(context.Background(), room) assert.NoError(t, err) var updatedRoom models.Room err = db.Get(&updatedRoom, "SELECT * FROM rooms WHERE id = ?", room.ID) assert.NoError(t, err) - assert.Equal(t, models.UserTeam("red"), updatedRoom.TeamTurn) + assert.Equal(t, models.UserTeam("red"), updatedRoom.TeamTurn) assert.Equal(t, uint8(10), updatedRoom.BlueCounter) + + var updatedSettings models.GameSettings + err = db.Get(&updatedSettings, "SELECT * FROM settings WHERE room_id = ?", room.ID) + assert.NoError(t, err) + assert.Equal(t, uint32(120), updatedSettings.RoundTime) } \ No newline at end of file diff --git a/repos/session.go b/repos/session.go index 399d40e..1991c43 100644 --- a/repos/session.go +++ b/repos/session.go @@ -16,9 +16,10 @@ type SessionsRepo interface { } func (p *RepoProvider) SessionByToken(ctx context.Context, token string) (*models.Session, error) { + db := getDB(ctx, p.DB) session := &models.Session{} // The lifetime in the DB is in seconds, but in the model it is in minutes. - err := p.DB.GetContext(ctx, session, `SELECT id, updated_at, lifetime / 60 as lifetime, cookie_token, username FROM sessions WHERE cookie_token = ?`, token) + err := sqlx.GetContext(ctx, db, session, `SELECT id, updated_at, lifetime / 60 as lifetime, cookie_token, username FROM sessions WHERE cookie_token = ?`, token) if err != nil { return nil, err } @@ -26,36 +27,23 @@ func (p *RepoProvider) SessionByToken(ctx context.Context, token string) (*model } func (p *RepoProvider) SessionCreate(ctx context.Context, session *models.Session) error { - p.Ext = p.DB - tx, ok := ctx.Value("sqltx").(*sqlx.Tx) - if ok && tx != nil { - // how to know if it is a final exec in chain? - // or is it better to commit outside? - tocommit, ok := ctx.Value("tocommit").(bool) - if ok && tocommit { - defer func() { - if err := tx.Commit(); err != nil { - // log - // return err - } - }() - } - p.Ext = tx - } + db := getDB(ctx, p.DB) // The lifetime in the model is in minutes, but in the DB it is in seconds. - _, err := p.Ext.Exec(`INSERT INTO sessions (updated_at, lifetime, cookie_token, username) VALUES (?, ?, ?, ?)`, + _, err := db.ExecContext(ctx, `INSERT INTO sessions (updated_at, lifetime, cookie_token, username) VALUES (?, ?, ?, ?)`, time.Now(), session.Lifetime*60, session.CookieToken, session.Username) return err } func (p *RepoProvider) SessionUpdate(ctx context.Context, session *models.Session) error { + db := getDB(ctx, p.DB) // The lifetime in the model is in minutes, but in the DB it is in seconds. - _, err := p.DB.ExecContext(ctx, `UPDATE sessions SET updated_at = ?, lifetime = ? WHERE cookie_token = ?`, + _, err := db.ExecContext(ctx, `UPDATE sessions SET updated_at = ?, lifetime = ? WHERE cookie_token = ?`, time.Now(), session.Lifetime*60, session.CookieToken) return err } func (p *RepoProvider) SessionDelete(ctx context.Context, token string) error { - _, err := p.DB.ExecContext(ctx, `DELETE FROM sessions WHERE cookie_token = ?`, token) + db := getDB(ctx, p.DB) + _, err := db.ExecContext(ctx, `DELETE FROM sessions WHERE cookie_token = ?`, token) return err } diff --git a/todos.md b/todos.md index 9beda88..c0405e0 100644 --- a/todos.md +++ b/todos.md @@ -1,4 +1,5 @@ ### feats +- implement transactional pattern in db write methods; + - implement the db methods for sessions in repos/session.go; + - auto close room if nothing is going on there (hmm) for ~1h; + - words database (file) load and form random 25 words; +